ai-watch-app/hybrid/html/ai.html

942 lines
25 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="shortcut icon" type="image/png" href="assets/icon.png">
<title>家庭管理语音AI页面</title>
</head>
<body>
<!-- 讯飞 -->
<script src="./js/voice.js"></script>
<script src="./js/crypto-js.min.js"></script>
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
<script src="js/w_md5.js"></script>
<!-- 构建界面 -->
<div class="main">
<div class="content">
<!-- 加载遮罩 -->
<div class="dialog" id="dialog" style="display: none;">
<!-- <div class="overlay" id="overlay"></div> -->
<div class="modal" id="modal">
<div class="ld"><em></em></div>
</div>
</div>
<!-- 视频 -->
<div class="video-wrap">
<video id="myVideo" muted loop autoplay playsinline>
<source src="https://img.agrimedia.cn/bmsc/%E9%A3%9E%E4%B9%A620240918-175041.mp4" type="video/mp4">
</video>
</div>
<audio
id="myAudio"
autoplay
controls
style="width: 100% height: 10px">
</audio>
<div class="status">
<input class="voice-input" type="search" name="voice" id="status-txt" style="pointer-events: none"/>
</div>
<div id="AiButton">
<!-- 录制 -->
<div class="buttons startRec">点击说话</div>
<!-- 停止 -->
<div class="buttons endRec">停止</div>
</div>
<!-- 讯飞测试 -->
<div class="voice">
<div id="marquee">
<span id="voice-txt">我是您的健康助手, 请问您有什么帮助吗?</span>
</div>
</div>
</div>
</div>
<script type="text/javascript">
marquee("marquee", "voice-txt");
function marquee(p, s) {
var scrollWidth = document.getElementById(p).offsetWidth;
var textWidth = document.getElementById(s).offsetWidth;
var i = scrollWidth;
function change() {
i--;
if (i < -textWidth) {
i = scrollWidth;
}
document.getElementById(s).style.left = i + "px";
window.requestAnimationFrame(change);
}
window.requestAnimationFrame(change);
}
</script>
<script>
var Items = ['血糖', '睡眠', '血氧', '血压', '尿酸', '梅拖', '心率', '体温', '心电图', '身体成份', '运动', '血脂', '血液成分'];
var Question = '';
var Subtitles = '';
function getURLParameter(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
// SEE事件解析
function extractStopEvent(sseString) {
// 使用正则表达式和字符串的 split 方法来分割事件
// 假设每个事件由空行分隔(\n\n但请注意这取决于实际的换行符
const events = sseString.split(/\r?\n\r?\n/);
let stopEvent = null;
// 遍历每个事件
events.forEach(event => {
if (!event.trim()) {
// 忽略空字符串
return;
}
// 假设 data 字段是最后一个字段,并且可能跨越多行
let dataLines = [];
let isDataLine = false;
// 分解当前事件为行
const lines = event.split(/\r?\n/);
// 遍历每一行来找到 data 字段
lines.forEach(line => {
if (line.trim().startsWith('data:')) {
isDataLine = true;
dataLines.push(line.trim().slice(5)); // 移除 'data:' 前缀
} else if (isDataLine && !line.trim()) {
// 如果在 data 字段后遇到了空行,则停止收集
isDataLine = false;
} else if (isDataLine) {
// 继续收集 data 字段的行(跨越多行的情况)
dataLines.push(line.trim());
}
});
const dataJson = dataLines.join('');
try {
if (dataJson) {
const eventData = JSON.parse(dataJson);
if (eventData.output?.finish_reason === "stop") {
stopEvent = eventData;
}
}
} catch (error) {
console.error('解析 data 字段为 JSON 时出错:', error);
}
});
// 如果没有找到 finish_reason 为 "stop" 的事件,则返回 null 或其他默认值
return stopEvent;
}
</script>
<!-- 讯飞语音识别 -->
<script>
window.onload = function () {
var videoElement = document.getElementById('myVideo');
var startTime = 5; // 开始时间(以秒为单位)
var endTime = 10; // 结束时间(以秒为单位)
var timeUpdateListener; // 保存timeupdate事件的监听器
// 指定段落
function playVideoSegment(startTime, endTime) {
videoElement.currentTime = startTime;
videoElement.play();
// 添加timeupdate事件监听器
timeUpdateListener = function() {
if (videoElement.currentTime >= endTime) {
videoElement.currentTime = startTime;
}
};
videoElement.addEventListener('timeupdate', timeUpdateListener);
}
// 重新播放
function replayVideoSegment(startTime, endTime) {
stopVideo();
playVideoSegment(startTime, endTime);
}
// 停止播放
function stopVideo() {
videoElement.pause();
videoElement.currentTime = 0;
// 移除timeupdate事件监听器
if (timeUpdateListener) {
videoElement.removeEventListener('timeupdate', timeUpdateListener);
timeUpdateListener = null;
}
}
// 当前AI视频循环
replayVideoSegment(0, 60);
// 获取 audio 元素的引用
var audioElement = document.getElementById('myAudio');
audioElement.muted = true; // 先静音
// 获取页面元素
var element = document.getElementById("elementId");
// 获取遮罩和弹窗元素
var dialog = document.getElementById('dialog');
// 点击事件
var startRec = document.getElementsByClassName('startRec')[0];
var endRec = document.getElementsByClassName('endRec')[0];
endRec.style.display = "none";
var token = null;
let times = null;
// 个人信息
var user = userInfo(JSON.parse(getURLParameter('user')));
var apitoken = getURLParameter('token');
var deviceId = getURLParameter('deviceId');
// 获取微软token
fetch("https://eastasia.api.cognitive.microsoft.com/sts/v1.0/issueToken", {
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': '58e9b39b8f6f48fe8d01f85b727ff737'
},
}).then(async(response) => {
token = await response.text();
}).catch(e => {});
// 文本输入框
const statusTxt = document.querySelector('#status-txt');
const voiceTxt = document.querySelector('#voice-txt');
// 防止多次请求
var isCallbackExecuted = false;
/*
* 给予数据文字标识
*/
let exampleData = JSON.parse(getURLParameter('data'));
for (let i = 0; i < exampleData.length; i++) {
exampleData[i].name = demoData[exampleData[i].type]
};
// 过滤掉非对象类型的值
const filteredData = Object.entries(exampleData).filter(([key, value]) => typeof value === 'object');
// 将键值对转换为所需格式的数组
const arrayOfObjects = filteredData.map(([key, value]) => ({
type: key,
...value
}));
/*
* 实例化迅飞语音听写流式版WebAPI
*/
const voice = new Voice({
// 服务接口认证信息
appId: '5f4ffdeb',
apiSecret: 'OGIwM2RlMjBkOTI5Mzk5YTJlMzUwODI5',
apiKey: '0b17a761b6b7174b789f639119d7e29a',
onWillStatusChange: function (oldStatus, newStatus) {},
onTextChange: function (text) {
// 监听识别结果的变化
voiceTxt.innerText = text;
marquee("marquee", "voice-txt");
// 3秒钟内没有说话就自动关闭
if (text) {
statusTxt.value = '正在听…';
if (!isCallbackExecuted) {
clearTimeout(times);
times = setTimeout(() => {
this.stop();
statusTxt.value = '正在说话';
const params = { msg: text };
// 判断关键字是否存在
const result = parseHealthQuery(params.msg);
isCallbackExecuted = true;
if (result) {
DetailDay(result.dataKey, result.date).then(res => {
let TargetData = res.data.data;
if (result.dataKey == 'ECGData') {
TargetData.map(item => {
delete item.data_msg.wavefrom;
delete item.data_msg.list;
})
}
if (result.dataKey == 'bloodLiquid') {
TargetData.map(item => {
item.data_msg.cholesterol = item.data_msg.cholesterol/100;
item.data_msg.highDensity = item.data_msg.highDensity/100;
item.data_msg.triacylglycerol = item.data_msg.triacylglycerol/100;
item.data_msg.uricAcidVal = item.data_msg.uricAcidVal/10000;
item.data_msg.lowDensity = item.data_msg.lowDensity/100;
})
}
Question = `请模仿全科医生的口吻与我: ${user}对话,我最近测量的${result.dataKey}数据为${JSON.stringify(TargetData)}, #提示data_msg为值hour_minute为检测时间。#提示:“[]”表示数据为空,请在小程序上传数据。#限制:回复不要带英文,要都转化成汉语。#限制回复内容控制在150字。#限制:忽略“压力指数、疲劳指数、心肌炎风险、冠心病风险和动脉硬化”等数据。`
console.log(Question, '问题=========================问题');
/*
* 调用接口 传递关键信息 文字转语音
*/
const xhr = new XMLHttpRequest();
const url = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
const apiKey = 'sk-cbb9b5ff44374fa2a8a258160ebb292d';
// 打开请求,设置为异步
xhr.open('POST', url, true);
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + apiKey);
xhr.setRequestHeader('X-DashScope-SSE', 'enable');
// 处理流式数据的接收(使用 progress 事件)
xhr.onprogress = function () {
const data = xhr.responseText;
// SSE 数据解析处理
const lines = data.split('\n');
lines.forEach(line => {
if (line.startsWith('data:')) {
const jsonStr = line.substring(5); // 去掉 'data:' 前缀
const parsedData = JSON.parse(jsonStr);
// 解析 content 内容
const content = parsedData.output.choices[0].message.content;
if (content) {
// 检查内容是否已经存在
if (!Subtitles.includes(content)) {
Subtitles += content;
}
};
}
});
};
// 检查请求完成
xhr.onload = function () {
if (xhr.status === 200) {
statusTxt.value = '正在说话';
RequestMicrosoft();
}
};
// 错误处理
xhr.onerror = function () {
console.error('An error occurred during the transaction', xhr.statusText);
};
/*
* 关键字转换
*/
const requestBody = {
model: 'qwen-turbo',
input: {
messages: [
{"role": "system", "content": Question},
{"role": "user", "content": `请问我${params.msg}正常吗`}
]
},
parameters: {
result_format: 'message',
incremental_output: true
}
};
xhr.send(JSON.stringify(requestBody));
isCallbackExecuted = true;
return;
})
} else {
const xhr = new XMLHttpRequest();
const url = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
const apiKey = 'sk-cbb9b5ff44374fa2a8a258160ebb292d';
// 打开请求,设置为异步
xhr.open('POST', url, true);
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + apiKey);
xhr.setRequestHeader('X-DashScope-SSE', 'enable');
// 处理流式数据的接收(使用 progress 事件)
xhr.onprogress = function () {
const data = xhr.responseText;
// SSE 数据解析处理
const lines = data.split('\n');
lines.forEach(line => {
if (line.startsWith('data:')) {
const jsonStr = line.substring(5); // 去掉 'data:' 前缀
const parsedData = JSON.parse(jsonStr);
// 解析 content 内容
const content = parsedData.output.choices[0].message.content;
if (content) {
// 检查内容是否已经存在
if (!Subtitles.includes(content)) {
Subtitles += content;
}
};
}
});
};
// 检查请求完成
xhr.onload = function () {
if (xhr.status === 200) {
statusTxt.value = '正在说话';
RequestMicrosoft();
}
};
// 错误处理
xhr.onerror = function () {
console.error('An error occurred during the transaction', xhr.statusText);
};
// 发送请求
const requestBody = {
model: 'qwen-turbo',
input: {
messages: [
{"role": "system", "content": `请模仿全科医生的口吻与我对话`},
{"role": "user", "content": params.msg}
]
},
parameters: {
result_format: 'message',
incremental_output: true
}
};
xhr.send(JSON.stringify(requestBody));
isCallbackExecuted = true;
return;
}
}, 3000);
}
}
}
});
// 开始识别
startRec.addEventListener("click", function() {
startShibie();
});
// 关闭识别
endRec.addEventListener("click", function() {
closeShibie();
});
function startShibie() {
/**开始识别**/
voiceTxt.innerText = '';
voice.start();
isCallbackExecuted = false;
// 先静音即可处理解决(提前做交互)
audioElement.muted = false;
audioElement.pause();
audioElement.currentTime = 0;
startRec.style.display = 'none';
endRec.style.display = 'none';
showModal()
}
function closeShibie() {
/**关闭识别**/
voiceTxt.innerText = '';
statusTxt.value = '';
voice.stop();
// 音频
audioElement.pause();
audioElement.currentTime = 0;
// 视频
replayVideoSegment(0, 60);
isCallbackExecuted = false;
startRec.style.display = 'block';
endRec.style.display = 'none';
hideModal()
}
// 显示弹窗和遮罩
function showModal() {
modal.style.display = 'block';
dialog.style.display = 'block';
}
// 隐藏弹窗和遮罩
function hideModal() {
modal.style.display = 'none';
dialog.style.display = 'none';
}
// 查询详情数据
async function DetailDay(type, data) {
try {
// const response = await fetch('https://test.sc2.agrimedia.cn/watch/device/getDeviceListDays', {
const response = await fetch('https://ai.agrimedia.cn/watch/device/getDeviceListDays', {
method: 'POST',
headers: {
'ApiToken': apitoken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
"device_real_time": data,
"device_id": deviceId,
"type": type
})
});
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
const result = await response.json();
return result;
} catch (error) {
console.error('There was a problem with your fetch operation:', error);
throw error; // 可选:重新抛出错误以便上层可以捕获并处理
}
}
// 请求微软文字转语音
function RequestMicrosoft() {
fetch("https://eastasia.tts.speech.microsoft.com/cognitiveservices/v1", {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Ocp-Apim-Subscription-Key': '58e9b39b8f6f48fe8d01f85b727ff737',
'Content-Type': 'application/ssml+xml',
'X-Microsoft-OutputFormat': 'audio-24khz-48kbitrate-mono-mp3'
},
responseType: 'arraybuffer',
body: `<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
<voice name="zh-CN-XiaoxiaoNeural">
<mstts:express-as style="Default" >
<prosody rate="0%" pitch="0%">
${filterString(Subtitles, ['*', ' '])}
</prosody>
</mstts:express-as>
</voice>
</speak> `,
}).then(async(response) => {
startRec.style.display = "none";
endRec.style.display = "block";
NextPlayVideo(response, filterString(Subtitles, ['*', ' ']));
}).catch(e => {
hideModal();
Subtitles = "";
});
}
// 字幕播放视频等操作
async function NextPlayVideo(response, str) {
const content_bytes = await response.arrayBuffer();
const blob = new Blob([content_bytes], { type: 'audio/mp3' });
const blobUrl = URL.createObjectURL(blob);
// 字幕文字
voiceTxt.innerText = str;
marquee("marquee", "voice-txt");
// 设置音频源
audioElement.src = blobUrl;
// 播放音频
audioElement.play();
// 循环视频
replayVideoSegment(60, 120);
// 监听播放技术
myAudio.addEventListener('ended', function() {
console.log('音频播放已结束!');
// 停止倒计时
startRec.style.display = 'block';
endRec.style.display = 'none';
statusTxt.value = '';
voiceTxt.innerText = '';
voice.stop();
replayVideoSegment(0, 60);
hideModal()
});
Subtitles = "";
}
// 防抖
function throttle(fn, wait) {
let lastTime = 0; // 上一次调用的时间
return function (...args) {
const now = Date.now(); // 当前时间
// 如果距离上次调用已经超过设定时间,则调用函数
if (now - lastTime > wait) {
lastTime = now; // 更新上一次调用的时间
fn.apply(this, args); // 以正确的`this`和参数调用函数
}
};
}
// 拿到的数据移除*
function filterString(str, charsToRemove) {
// 这里的正则表达式是通过将charsToRemove数组中的字符转换为字符类character class来构建的
// 例如如果charsToRemove是['*', ' '],则正则表达式将是/[* ]/g
const regex = new RegExp(`[${charsToRemove.join('')}]`, 'g');
// 使用replace方法和正则表达式来移除所有匹配的字符
return str.replace(regex, '');
}
// 个人信息
function userInfo(user) {
if (user) {
return `年龄${user.birthday}, 身高${user.height}, 体重${user.weight}`
}
}
// 解析文字
function parseHealthQuery(query, demoData) {
if (!demoData) {
demoData = {
bloodGlucose: "血糖",
SleepDatas: "睡眠",
bloodOxygen: "血氧",
bloodPressure: '血压',
meiTuo: '梅脱',
pulseReat: '心率',
bodyTemperature: '体温',
ECGData: '心电图',
bloodLiquid: ['血脂', '血液', '尿酸'],
bodyData: '身体成分',
stepIndex: ['运动', '步数']
};
}
// 获取当前日期
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1; // 月份从0开始所以需要+1
const day = today.getDate();
// 初始化日期为今天
let targetDate = new Date(year, month, day);
// 检查时间词并设置目标日期
if (query.includes("昨天")) {
targetDate.setDate(day - 1);
} else if (query.includes("前天")) {
targetDate.setDate(day - 2);
}
const formattedDate = `${targetDate.getFullYear()}-${('0' + targetDate.getMonth()).slice(-2)}-${('0' + targetDate.getDate()).slice(-2)}`;
// 创建一个辅助函数来检查 bloodLiquid 对象是否包含查询字符串
function isBloodLiquidMatch(bloodLiquidData, query) {
return bloodLiquidData.some(element => query.includes(element))
}
// 在查询逻辑中使用这个辅助函数
for (let key in demoData) {
if (typeof demoData[key] === 'object' && key === 'bloodLiquid' || typeof demoData[key] === 'object' && key === 'stepIndex' ) {
if (isBloodLiquidMatch(demoData[key], query)) {
key === 'stepIndex'? key = 'step_split' : ''; // 修改步数参数
return {
dataKey: key,
date: formattedDate
};
}
} else if (query.includes(demoData[key])) {
return {
dataKey: key,
date: formattedDate
};
}
}
// 如果没有找到匹配的数据项则返回null或错误信息这里选择返回null
return null;
}
};
</script>
<style>
body {
margin: 0;
}
.content {
width: 100%;
height: 100vh;
overflow: hidden;
background-image: url('https://img.agrimedia.cn/bmsc/bg-cideo-tuya.png');
background-size: 100% 100%;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
box-sizing: border-box;
}
.video-wrap {
width: 80%;
top: 10%;
height: auto;
background-image: url('https://img.agrimedia.cn/bmsc/bg-cideo-tuya.png');
background-size: 100% 100%;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
border-radius: 10px;
}
#myVideo {
width: 100%;
height: 100% !important;
border-radius: 10px;
}
#myAudio {
position: absolute;
top: 0px;
left: 0px;
opacity: 0;
}
#AiButton {
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
text-align: center;
margin-bottom: 30px;
}
#AiButton > img {
width: 120px;
height: 30px;
}
.status > .btn {
text-align: center;
font-size: 30px;
color: #fff;
}
.buttons {
border: 2px solid #fff;
color: #fff;
font-size: 28px;
padding: 4px 20px;
border-radius: 5px;
}
/* 遮罩样式 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3); /* 半透明黑色背景 */
display: none; /* 初始隐藏 */
z-index: 9999; /* 设置为最高层级 */
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 居中对齐 */
padding: 20px;
border-radius: 5px;
text-align: center;
z-index: 99999;
}
.ld {
background: #000;
transform: rotate(45deg);
transform-origin: center;
margin-top: calc(var(--dis) / -1.41421); /* Math.sqrt(2) */
--dis: 20px; /*圆尺寸和偏移量*/
--dur: 2s; /*动画时长*/
}
.ld em {
position: relative;
opacity: 0.7;
}
.ld::before {
background-color: #38597A;
}
.ld::after {
background-color: #3FEBFF;
}
.ld em::before {
background-color: #87B2DD;
}
.ld em::after {
background-color: #58D5FF;
}
.ld::before,
.ld::after,
.ld em::before,
.ld em::after {
content: "";
position: absolute;
width: var(--dis);
height: var(--dis);
border-radius: var(--dis);
left: 0;
top: 0;
animation-duration: var(--dur);
animation-iteration-count: infinite;
animation-fill-mode: both;
--dir: 1;
--tr: calc(var(--dir) * var(--dis));
--ttr: calc(-1 * var(--tr));
--tx: translateX(var(--tr));
--ttx: translateX(var(--ttr));
--ty: translateY(var(--tr));
--tty: translateY(var(--ttr));
}
.ld::before,
.ld::after {
--dir: -1;
}
.ld::before,
.ld em::before {
transform: var(--ttx);
animation-name: tx;
}
.ld::after,
.ld em::after {
transform: var(--tty);
animation-name: ty;
}
@keyframes tx {
50% {
transform: var(--tx);
}
}
@keyframes ty {
50% {
transform: var(--ty);
}
}
.rule {
width: 1px;
background: #aaa;
position: fixed;
left: 50%;
top: 0;
bottom: 0;
}
.rule::after {
content: "";
height: 1px;
background: #aaa;
position: fixed;
top: 50%;
left: 0;
right: 0;
}
.runRec {
display: none;
}
.voice-input {
margin: 0 auto;
width: 100%;
height: 50px;
background-color: none;
line-height: 50px;
border: 0;/*清除自带的2px的边框*/
padding: 0;/*清除自带的padding间距*/
outline: none;/*清除input点击之后的黑色边框*/
text-align: center;
background-color: transparent;
color: #fff;
font-size: 26px;
font-weight: 800;
}
.voice {
position: fixed;
bottom: 0px;
width: 100%;
/* background-color: #000; */
}
#marquee {
display: block;
width: 100%;
height: 60px;
margin: 0 auto;
position: relative;
overflow: hidden;
}
#voice-txt {
position: absolute;
top: 0;
left: 100%;
line-height: 60px;
font-size:29px;
color: #fff;
display: block;
word-break: keep-all;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</body>
</html>