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

808 lines
22 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 id="AiButton">
<!-- 录制 -->
<img class="startRec" src="https://img.agrimedia.cn/bmsc/apps/start-tuya.png">
<!-- 录制中 -->
<img class="runRec" src="https://img.agrimedia.cn/bmsc/apps/runnig-tuya.png">
<!-- 停止 -->
<img class="endRec" src="https://img.agrimedia.cn/bmsc/apps/end-tuya.png">
</div>
<!-- 讯飞测试 -->
<div style="opacity: 1">
<div class="voice-box">
<input class="voice-input" type="search" name="voice" id="voice-txt" style="pointer-events: none"/>
</div>
</div>
</div>
</div>
<script>
var Items = ['血糖', '睡眠', '血氧', '血压', '尿酸', '梅拖', '心率', '体温', '心电图', '身体成份', '运动', '血脂'];
var Question = '';
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;
}
// 筛选关键词
function containsKeywordRegex(text) {
var index = Items.findIndex(item => text.includes(item));
if (index !== -1) {
return index
} else {
return 99999
}
}
// 语音所需时间
function calculateSpeakingTime(text) {
// 假设平均每分钟说230个单词
const wordsPerMinute = 230;
// 计算文字的长度
const wordCount = text.trim().length;
// 计算所需时间(以分钟为单位)
const speakingTime = wordCount / wordsPerMinute;
// 转换为秒数
const speakingTimeInSeconds = speakingTime * 60;
return speakingTimeInSeconds;
}
</script>
<!-- 讯飞语音识别 -->
<script>
window.onload = function () {
var demoData = {
bloodGlucose: "血糖",
SleepDatas: "睡眠",
bloodOxygen: "血氧",
bloodPressure: '血压',
bloodLiquid: "血脂",
meiTuo: '梅脱',
pulseReat: '心率',
bodyTemperature: '体温',
ECGData: '心电图',
bodyData: '身体成份',
stepIndex: '运动'
};
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 runRec = document.getElementsByClassName('runRec')[0];
var endRec = document.getElementsByClassName('endRec')[0];
var token = null;
let times = null;
// 获取微软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 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]
}
/*
* 实例化迅飞语音听写流式版WebAPI
*/
const voice = new Voice({
// 服务接口认证信息
appId: '5f4ffdeb',
apiSecret: 'OGIwM2RlMjBkOTI5Mzk5YTJlMzUwODI5',
apiKey: '0b17a761b6b7174b789f639119d7e29a',
onWillStatusChange: function (oldStatus, newStatus) {},
onTextChange: function (text) {
// 监听识别结果的变化
console.log(text, '监听')
voiceTxt.value = text;
// 3秒钟内没有说话就自动关闭
if (text) {
clearTimeout(times);
if (!isCallbackExecuted) {
times = setTimeout(() => {
this.stop();
// voice.stop();
const params = { msg: text }
alert(params.msg)
/*
* 拿到匹配的文字下标
*/
let QSindex = containsKeywordRegex(params.msg);
console.log(QSindex)
if (QSindex == 0) {
const obj = exampleData.filter(item => item.type == "bloodGlucose");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的血糖为${obj[0].data_msg}毫摩尔/升`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 1) {
const obj = exampleData.filter(item => item.type == "SleepDatas");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近睡眠时长为${obj[0].data_msg[0].sleepTotalTime}分钟`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 2) {
const obj = exampleData.filter(item => item.type == "bloodOxygen");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的血氧为${obj[0].data_msg}毫摩尔/升`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 3) {
const obj = exampleData.filter(item => item.type == "bloodPressure");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的血压为${obj[0].data_msg.bloodPressureLow}/${obj[0].data_msg.bloodPressureHigh}毫摩尔/升`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 4 || QSindex == 11) {
const obj = exampleData.filter(item => item.type == "bloodLiquid");
if (obj[0].data_msg.cholesterol) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的血脂状况为,
尿酸为${obj[0].data_msg.uricAcidVal/10},
总胆固醇为${obj[0].data_msg.cholesterol/100},
甘油三酯为${obj[0].data_msg.cholesterol/100},
高密度脂蛋白为${obj[0].data_msg.cholesterol/100},
低密度脂蛋白为${obj[0].data_msg.cholesterol/100}, `
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 6) {
const obj = exampleData.filter(item => item.type == "pulseReat");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的心率为${obj[0].data_msg[0]}, `
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 7) {
const obj = exampleData.filter(item => item.type == "bodyTemperature");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的体温为${obj[0].data_msg}摄氏度`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 8) {
const obj = exampleData.filter(item => item.type == "ECGData");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近心电图测量结果为${obj[0].data_msg.heartRate}次/分`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 9) {
const obj = exampleData.filter(item => item.type == "bodyData");
if (obj[0].data_msg.BMI) {
Question = `请模仿全科医生的口吻与我对话,我最近身体成分结果为${obj[0].data_msg.BMI}`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 10) {
const obj = exampleData.filter(item => item.type == "stepIndex");
if (obj[0].data_msg) {
Question = `请模仿全科医生的口吻与我对话,我最近测量的运动为${obj[0].data_msg.step}步数,
${obj[0].data_msg.calorie/10}千卡,
${obj[0].data_msg.distance/1000}公里`
} else {
alert ('当前数据为空');
return
}
}
if (QSindex == 99999) {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://sc2.agrimedia.cn:8787/api/user/ask', true);
var data = JSON.stringify({
"messages": [
{"role": "system", "content": params.msg},
{"role": "user", "content": params.msg}
]
})
console.log(str.output.text, '返回的答案')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const chunk = xhr.responseText;
const str = extractStopEvent(chunk);
/*
* 微软接口识别
*/
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%">
${str.output.text}
</prosody>
</mstts:express-as>
</voice>
</speak> `,
}).then(async(response) => {
const content_bytes = await response.arrayBuffer();
const blob = new Blob([content_bytes], { type: 'audio/mp3' });
const blobUrl = URL.createObjectURL(blob);
// 设置音频源
audioElement.src = blobUrl;
// 播放音频
audioElement.play();
// 循环视频
replayVideoSegment(60, 120);
// 计算所需时间
const speakingTime = calculateSpeakingTime(content.data.choices[0].text);
// 开始倒计时
var totalTime = speakingTime;
var countdown = setInterval(function() {
// 更新剩余时间
totalTime --;
if (totalTime <= 0) {
// 停止倒计时
clearInterval(countdown);
replayVideoSegment(0, 60);
}
}, 1000);
hideModal()
}).catch(e => {
hideModal();
});
startRec.style.display = 'block';
runRec.style.display = 'none';
// endRec.style.opacity = 0;
}
};
xhr.send(data);
isCallbackExecuted = true;
return
} else {
/*
* 调用接口 传递关键信息 文字转语音
*/
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://sc2.agrimedia.cn:8787/api/user/ask', true);
/*
* 关键字转换
*/
var data = JSON.stringify({
"messages": [
{"role": "system", "content": Question},
{"role": "user", "content": `请问我${Items[QSindex]}正常吗`}
]
})
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const chunk = xhr.responseText;
const str = extractStopEvent(chunk);
/*
* 微软接口识别
*/
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%">
${str.output.text}
</prosody>
</mstts:express-as>
</voice>
</speak> `,
}).then(async(response) => {
const content_bytes = await response.arrayBuffer();
const blob = new Blob([content_bytes], { type: 'audio/mp3' });
const blobUrl = URL.createObjectURL(blob);
// 设置音频源
audioElement.src = blobUrl;
// 播放音频
audioElement.play();
// 循环视频
replayVideoSegment(60, 120);
// 计算所需时间
const speakingTime = calculateSpeakingTime(content.data.choices[0].text);
// 开始倒计时
var totalTime = speakingTime;
var countdown = setInterval(function() {
// 更新剩余时间
totalTime --;
if (totalTime <= 0) {
// 停止倒计时
clearInterval(countdown);
replayVideoSegment(0, 60);
}
}, 1000);
hideModal()
}).catch(e => {
hideModal();
});
startRec.style.display = 'block';
runRec.style.display = 'none';
// endRec.style.opacity = 0;
}
};
xhr.send(data);
isCallbackExecuted = true;
return
}
}, 3000);
}
}
}
});
// 开始识别
startRec.addEventListener("click", function() {
/**开始识别**/
voiceTxt.value = '';
voice.start();
isCallbackExecuted = false;
// 先静音即可处理解决(提前做交互)
audioElement.muted = false;
audioElement.pause();
audioElement.currentTime = 0;
startRec.style.display = 'none';
runRec.style.display = 'block';
// endRec.style.opacity = 0;
showModal()
});
// 关闭识别
endRec.addEventListener("click", function() {
/**关闭识别**/
voiceTxt.value = '';
voice.stop();
// 音频
audioElement.pause();
audioElement.currentTime = 0;
// 视频
replayVideoSegment(0, 60);
isCallbackExecuted = false;
startRec.style.display = 'block';
runRec.style.display = 'none';
// endRec.style.opacity = 0;
hideModal()
});
// 显示弹窗和遮罩
function showModal() {
// overlay.style.display = 'block';
modal.style.display = 'block';
dialog.style.display = 'block';
}
// 隐藏弹窗和遮罩
function hideModal() {
// overlay.style.display = 'none';
modal.style.display = 'none';
dialog.style.display = 'none';
}
};
</script>
<style>
body {
margin: 0;
}
.content {
width: 100%;
height: 100vh;
overflow: hidden;
background-image: url('https://img.agrimedia.cn/bmsc/index/ai-persion-bg-tuya.jpeg');
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/index/video-bg.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-top: 30px;
}
#AiButton > img {
width: 120px;
height: 30px;
}
/* 遮罩样式 */
.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;
}
</style>
</body>
</html>