dashboard.html 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>livetalking数字人交互平台</title>
  7. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  8. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
  9. <style>
  10. :root {
  11. --primary-color: #4361ee;
  12. --secondary-color: #3f37c9;
  13. --accent-color: #4895ef;
  14. --background-color: #f8f9fa;
  15. --card-bg: #ffffff;
  16. --text-color: #212529;
  17. --border-radius: 10px;
  18. --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  19. }
  20. body {
  21. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  22. background-color: var(--background-color);
  23. color: var(--text-color);
  24. min-height: 100vh;
  25. padding-top: 20px;
  26. }
  27. .dashboard-container {
  28. max-width: 1400px;
  29. margin: 0 auto;
  30. padding: 20px;
  31. }
  32. .card {
  33. background-color: var(--card-bg);
  34. border-radius: var(--border-radius);
  35. box-shadow: var(--box-shadow);
  36. border: none;
  37. margin-bottom: 20px;
  38. overflow: hidden;
  39. }
  40. .card-header {
  41. background-color: var(--primary-color);
  42. color: white;
  43. font-weight: 600;
  44. padding: 15px 20px;
  45. border-bottom: none;
  46. }
  47. .video-container {
  48. position: relative;
  49. width: 100%;
  50. background-color: #000;
  51. border-radius: var(--border-radius);
  52. overflow: hidden;
  53. display: flex;
  54. justify-content: center;
  55. align-items: center;
  56. }
  57. video {
  58. max-width: 100%;
  59. max-height: 100%;
  60. display: block;
  61. border-radius: var(--border-radius);
  62. }
  63. .controls-container {
  64. padding: 20px;
  65. }
  66. .btn-primary {
  67. background-color: var(--primary-color);
  68. border-color: var(--primary-color);
  69. }
  70. .btn-primary:hover {
  71. background-color: var(--secondary-color);
  72. border-color: var(--secondary-color);
  73. }
  74. .btn-outline-primary {
  75. color: var(--primary-color);
  76. border-color: var(--primary-color);
  77. }
  78. .btn-outline-primary:hover {
  79. background-color: var(--primary-color);
  80. color: white;
  81. }
  82. .form-control {
  83. border-radius: var(--border-radius);
  84. padding: 10px 15px;
  85. border: 1px solid #ced4da;
  86. }
  87. .form-control:focus {
  88. border-color: var(--accent-color);
  89. box-shadow: 0 0 0 0.25rem rgba(67, 97, 238, 0.25);
  90. }
  91. .status-indicator {
  92. width: 10px;
  93. height: 10px;
  94. border-radius: 50%;
  95. display: inline-block;
  96. margin-right: 5px;
  97. }
  98. .status-connected {
  99. background-color: #28a745;
  100. }
  101. .status-disconnected {
  102. background-color: #dc3545;
  103. }
  104. .status-connecting {
  105. background-color: #ffc107;
  106. }
  107. .asr-container {
  108. height: 120px;
  109. overflow-y: auto;
  110. padding: 15px;
  111. background-color: #f8f9fa;
  112. border-radius: var(--border-radius);
  113. border: 1px solid #ced4da;
  114. }
  115. .asr-text {
  116. margin-bottom: 10px;
  117. padding: 10px;
  118. background-color: white;
  119. border-radius: var(--border-radius);
  120. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  121. }
  122. .user-message {
  123. background-color: #e3f2fd;
  124. border-left: 4px solid var(--primary-color);
  125. }
  126. .system-message {
  127. background-color: #f1f8e9;
  128. border-left: 4px solid #8bc34a;
  129. }
  130. .recording-indicator {
  131. position: absolute;
  132. top: 15px;
  133. right: 15px;
  134. background-color: rgba(220, 53, 69, 0.8);
  135. color: white;
  136. padding: 5px 10px;
  137. border-radius: 20px;
  138. font-size: 0.8rem;
  139. display: none;
  140. }
  141. .recording-indicator.active {
  142. display: flex;
  143. align-items: center;
  144. }
  145. .recording-indicator .blink {
  146. width: 10px;
  147. height: 10px;
  148. background-color: #fff;
  149. border-radius: 50%;
  150. margin-right: 5px;
  151. animation: blink 1s infinite;
  152. }
  153. @keyframes blink {
  154. 0% { opacity: 1; }
  155. 50% { opacity: 0.3; }
  156. 100% { opacity: 1; }
  157. }
  158. .mode-switch {
  159. margin-bottom: 20px;
  160. }
  161. .nav-tabs .nav-link {
  162. color: var(--text-color);
  163. border: none;
  164. padding: 10px 20px;
  165. border-radius: var(--border-radius) var(--border-radius) 0 0;
  166. }
  167. .nav-tabs .nav-link.active {
  168. color: var(--primary-color);
  169. background-color: var(--card-bg);
  170. border-bottom: 3px solid var(--primary-color);
  171. font-weight: 600;
  172. }
  173. .tab-content {
  174. padding: 20px;
  175. background-color: var(--card-bg);
  176. border-radius: 0 0 var(--border-radius) var(--border-radius);
  177. }
  178. .settings-panel {
  179. padding: 15px;
  180. background-color: #f8f9fa;
  181. border-radius: var(--border-radius);
  182. margin-top: 15px;
  183. }
  184. .footer {
  185. text-align: center;
  186. margin-top: 30px;
  187. padding: 20px 0;
  188. color: #6c757d;
  189. font-size: 0.9rem;
  190. }
  191. .voice-record-btn {
  192. width: 60px;
  193. height: 60px;
  194. border-radius: 50%;
  195. background-color: var(--primary-color);
  196. color: white;
  197. display: flex;
  198. justify-content: center;
  199. align-items: center;
  200. cursor: pointer;
  201. transition: all 0.2s ease;
  202. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  203. margin: 0 auto;
  204. }
  205. .voice-record-btn:hover {
  206. background-color: var(--secondary-color);
  207. transform: scale(1.05);
  208. }
  209. .voice-record-btn:active {
  210. background-color: #dc3545;
  211. transform: scale(0.95);
  212. }
  213. .voice-record-btn i {
  214. font-size: 24px;
  215. }
  216. .voice-record-label {
  217. text-align: center;
  218. margin-top: 10px;
  219. font-size: 14px;
  220. color: #6c757d;
  221. }
  222. .recording-pulse {
  223. animation: pulse 1.5s infinite;
  224. }
  225. @keyframes pulse {
  226. 0% {
  227. box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7);
  228. }
  229. 70% {
  230. box-shadow: 0 0 0 15px rgba(220, 53, 69, 0);
  231. }
  232. 100% {
  233. box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
  234. }
  235. }
  236. </style>
  237. </head>
  238. <body>
  239. <div class="dashboard-container">
  240. <div class="row">
  241. <!-- 视频区域 -->
  242. <div class="col-lg-12">
  243. <div class="card">
  244. <div class="card-body p-0">
  245. <div class="video-container">
  246. <video id="video" autoplay playsinline></video>
  247. <div class="recording-indicator" id="recording-indicator">
  248. <div class="blink"></div>
  249. <span>录制中</span>
  250. </div>
  251. </div>
  252. <div class="controls-container">
  253. <div class="row">
  254. <div class="col-md-6 mb-3" style="display: none;">
  255. <button class="btn btn-primary w-100" id="startBtn">
  256. <i class="bi bi-play-fill"></i> 开始连接
  257. </button>
  258. <button class="btn btn-danger w-100" id="stopBtn" style="display: none;">
  259. <i class="bi bi-stop-fill"></i> 停止连接
  260. </button>
  261. </div>
  262. </div>
  263. <!-- 对话模式已移除 - 现在通过独立API /api/chat 调用 -->
  264. <div class="settings-panel mt-3">
  265. <div class="row">
  266. <div class="col-md-12">
  267. <div class="form-check form-switch mb-3" style="display: none;">
  268. <input class="form-check-input" type="checkbox" id="use-stun">
  269. <label class="form-check-label" for="use-stun">使用STUN服务器</label>
  270. </div>
  271. </div>
  272. </div>
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. <!-- 隐藏的会话ID -->
  281. <input type="hidden" id="sessionid" value="0">
  282. <script src="client-v2.js"></script>
  283. <script src="srs.sdk.js"></script>
  284. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
  285. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  286. <script>
  287. $(document).ready(function() {
  288. function updateConnectionStatus(status) {
  289. const statusIndicator = $('#connection-status');
  290. const statusText = $('#status-text');
  291. statusIndicator.removeClass('status-connected status-disconnected status-connecting');
  292. switch(status) {
  293. case 'connected':
  294. statusIndicator.addClass('status-connected');
  295. statusText.text('已连接');
  296. break;
  297. case 'connecting':
  298. statusIndicator.addClass('status-connecting');
  299. statusText.text('连接中...');
  300. break;
  301. case 'disconnected':
  302. default:
  303. statusIndicator.addClass('status-disconnected');
  304. statusText.text('未连接');
  305. break;
  306. }
  307. }
  308. // 计算文本播放时长(毫秒)
  309. function calculatePlayDuration(text) {
  310. // 中文字符和英文单词的播放速度不同
  311. const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
  312. const englishWords = (text.match(/[a-zA-Z]+/g) || []).length;
  313. const otherChars = text.length - chineseChars - englishWords;
  314. // 中文约 4-5 字/秒,英文约 3-4 词/秒,其他字符约 5 个/秒
  315. // 加上 1.5 秒的开头和结尾缓冲时间
  316. const duration = (chineseChars / 4.5 + englishWords / 3.5 + otherChars / 5) * 1000 + 1500;
  317. // 最小 3 秒,最大 20 秒
  318. return Math.max(3000, Math.min(20000, duration));
  319. }
  320. // 处理介绍播放完成,自动播放下一条
  321. function handleIntroPlayCompleted() {
  322. if (!isDuringIntro) return;
  323. fetch('/intro_play_completed', {
  324. body: JSON.stringify({
  325. sessionid: parseInt(document.getElementById('sessionid').value),
  326. }),
  327. headers: {
  328. 'Content-Type': 'application/json'
  329. },
  330. method: 'POST'
  331. }).then(function(response) {
  332. return response.json();
  333. }).then(function(data) {
  334. if (data.code === 0 && data.text) {
  335. console.log('下一条介绍内容开始播放:', data.text);
  336. // 根据文本长度计算播放时长,再触发下一条
  337. const playDuration = calculatePlayDuration(data.text);
  338. console.log('计算播放时长:', playDuration, 'ms');
  339. setTimeout(handleIntroPlayCompleted, playDuration);
  340. } else if (data.code === 0 && !data.text) {
  341. console.log('介绍内容播放完成');
  342. // 标记介绍过程结束
  343. isDuringIntro = false;
  344. } else {
  345. console.error('获取下一条介绍内容失败:', data.msg);
  346. }
  347. }).catch(function(error) {
  348. console.error('获取下一条介绍内容错误:', error);
  349. });
  350. }
  351. //播本地生活App商品介绍语音(使用新API)
  352. function playKnowledgeIntro() {
  353. fetch('/knowledge_intro', {
  354. body: JSON.stringify({
  355. sessionid: parseInt(document.getElementById('sessionid').value),
  356. }),
  357. headers: {
  358. 'Content-Type': 'application/json'
  359. },
  360. method: 'POST'
  361. }).then(function(response) {
  362. return response.json();
  363. }).then(function(data) {
  364. if (data.code === 0) {
  365. console.log('商品介绍开始播放:', data.text);
  366. // 启动自动续播逻辑,根据文本长度计算播放时长
  367. const playDuration = calculatePlayDuration(data.text);
  368. console.log('第一条播放时长:', playDuration, 'ms');
  369. setTimeout(handleIntroPlayCompleted, playDuration);
  370. } else {
  371. console.error('商品介绍播放失败:', data.msg);
  372. }
  373. }).catch(function(error) {
  374. console.error('商品介绍播放错误:', error);
  375. // 降级到旧的硬编码方式
  376. fallbackPlayKnowledgeIntro();
  377. });
  378. }
  379. // 降级播放函数(兼容旧版本)
  380. function fallbackPlayKnowledgeIntro() {
  381. const introText = "欢迎使用本地生活App!我们为您提供丰富的本地商品和服务:美食外卖汇聚各类餐厅美食,快速配送到家;休闲娱乐提供电影院、KTV、游乐场等娱乐场所优惠;生活服务涵盖家政、维修、美容美发等便民服务;附近团购有周边商户超值团购,省钱又方便。现在您可以随时查询商品信息,我们会为您推荐最合适的本地服务!";
  382. fetch('/human', {
  383. body: JSON.stringify({
  384. text: introText,
  385. type: 'echo',
  386. interrupt: false,
  387. during_intro: true, //标记为介绍过程中
  388. sessionid: parseInt(document.getElementById('sessionid').value),
  389. }),
  390. headers: {
  391. 'Content-Type': 'application/json'
  392. },
  393. method: 'POST'
  394. });
  395. }
  396. // 智能问答功能
  397. function handleQuestion(question) {
  398. //简单的关键词识别来判断应该使用哪个知识库
  399. const techKeywords = ['AI', '机器学习', '深度学习', '神经网络', 'WebRTC', 'Python', 'JavaScript', '算法', '编程', '技术', '开发', '代码'];
  400. const lifeKeywords = ['健康', '生活', '饮食', '运动', '娱乐', '旅游', '家庭', '工作', '学习', '情感', '日常'];
  401. const localLivingKeywords = ['美食', '外卖', '餐厅', '配送', '休闲', '娱乐', '生活服务', '团购', '会员', '积分', '优惠', '客服', '退款', '预约', '支付', '热门商圈', '安全保障', '评价', '售后', '优惠券', 'App', '本地', '生活', '服务'];
  402. let knowledgeBase = '通用';
  403. if (techKeywords.some(keyword => question.includes(keyword))) {
  404. knowledgeBase = '技术文档库';
  405. } else if (localLivingKeywords.some(keyword => question.includes(keyword))) {
  406. knowledgeBase = '本地生活 App';
  407. } else if (lifeKeywords.some(keyword => question.includes(keyword))) {
  408. knowledgeBase = '生活百科库';
  409. }
  410. return knowledgeBase;
  411. }
  412. // 页面加载后自动连接
  413. setTimeout(function() {
  414. updateConnectionStatus('connecting');
  415. // 设置一个标志,用于跟踪 track 事件是否已触发
  416. let trackEventFired = false;
  417. // 先保存原始的 onWebRTCConnected 回调
  418. const originalOnConnected = window.onWebRTCConnected || function() {};
  419. // 重写 onWebRTCConnected,在 track 事件后才更新 UI 状态
  420. window.onWebRTCConnected = function() {
  421. console.log('✅ WebRTC connected callback triggered');
  422. const video = document.getElementById('video');
  423. if (video) {
  424. console.log('Video element found, checking stream...');
  425. const stream = video.srcObject;
  426. if (stream) {
  427. console.log('Stream ID:', stream.id);
  428. console.log('Video tracks:', stream.getVideoTracks());
  429. console.log('Audio tracks:', stream.getAudioTracks());
  430. // 检查视频是否有实际数据
  431. video.onloadedmetadata = function() {
  432. console.log('Video metadata loaded:', {
  433. width: video.videoWidth,
  434. height: video.videoHeight,
  435. readyState: video.readyState
  436. });
  437. };
  438. } else {
  439. console.error('❌ No stream attached to video element!');
  440. }
  441. }
  442. trackEventFired = true;
  443. updateConnectionStatus('connected');
  444. // 连接成功后延迟播放介绍
  445. setTimeout(function() {
  446. // 标记进入介绍过程
  447. isDuringIntro = true;
  448. // 自动播放介绍语音
  449. playKnowledgeIntro();
  450. }, 2000);
  451. // 调用原始回调
  452. if (typeof originalOnConnected === 'function') {
  453. originalOnConnected();
  454. }
  455. };
  456. // 启动 WebRTC 连接
  457. start();
  458. // 添加定时器检查视频流是否已加载(作为备用方案)
  459. let connectionCheckTimer = setInterval(function() {
  460. const video = document.getElementById('video');
  461. // 检查视频是否有数据
  462. if (video.readyState >= 3 && video.videoWidth > 0) {
  463. console.log('Video loaded via readyState check:', video.videoWidth + 'x' + video.videoHeight);
  464. // 如果 track 事件还没触发,也更新状态
  465. if (!trackEventFired) {
  466. updateConnectionStatus('connected');
  467. trackEventFired = true;
  468. // 连接成功
  469. setTimeout(function() {
  470. // 标记进入介绍过程
  471. isDuringIntro = true;
  472. // 自动播放介绍语音
  473. playKnowledgeIntro();
  474. }, 2000);
  475. }
  476. clearInterval(connectionCheckTimer);
  477. }
  478. }, 2000); // 每 2 秒检查一次
  479. // 60 秒后如果还是连接中状态,就停止检查
  480. setTimeout(function() {
  481. if (connectionCheckTimer) {
  482. clearInterval(connectionCheckTimer);
  483. }
  484. // 如果 60 秒后 track 事件还没触发,检查连接状态
  485. if (!trackEventFired) {
  486. console.warn('⚠️ WebRTC track event not fired after 60 seconds');
  487. // 尝试重新连接
  488. updateConnectionStatus('disconnected');
  489. alert('连接超时,请检查网络连接或刷新页面重试');
  490. }
  491. }, 60000);
  492. }, 500);
  493. function addChatMessage(message, type = 'user') {
  494. const messagesContainer = $('#chat-messages');
  495. const messageClass = type === 'user' ? 'user-message' : 'system-message';
  496. const sender = type === 'user' ? '您' : '数字人';
  497. const messageElement = $(`
  498. <div class="asr-text ${messageClass}">
  499. ${sender}: ${message}
  500. </div>
  501. `);
  502. messagesContainer.append(messageElement);
  503. messagesContainer.scrollTop(messagesContainer[0].scrollHeight);
  504. }
  505. // 标记是否在介绍过程中
  506. let isDuringIntro = false;
  507. // 开始/停止按钮
  508. $('#startBtn').click(function() {
  509. updateConnectionStatus('connecting');
  510. start();
  511. $(this).hide();
  512. $('#stopBtn').show();
  513. // 添加定时器检查视频流是否已加载
  514. let connectionCheckTimer = setInterval(function() {
  515. const video = document.getElementById('video');
  516. // 检查视频是否有数据
  517. if (video.readyState >= 3 && video.videoWidth > 0) {
  518. updateConnectionStatus('connected');
  519. clearInterval(connectionCheckTimer);
  520. // 连接成功
  521. setTimeout(function() {
  522. // 标记进入介绍过程
  523. isDuringIntro = true;
  524. // 自动播放介绍语音
  525. playKnowledgeIntro();
  526. }, 2000);
  527. }
  528. }, 2000); // 每2秒检查一次
  529. // 60秒后如果还是连接中状态,就停止检查
  530. setTimeout(function() {
  531. if (connectionCheckTimer) {
  532. clearInterval(connectionCheckTimer);
  533. }
  534. }, 60000);
  535. });
  536. $('#stopBtn').click(function() {
  537. stop();
  538. $(this).hide();
  539. $('#startBtn').show();
  540. updateConnectionStatus('disconnected');
  541. });
  542. // 录制功能
  543. $('#btn_start_record').click(function() {
  544. console.log('Starting recording...');
  545. fetch('/record', {
  546. body: JSON.stringify({
  547. type: 'start_record',
  548. sessionid: parseInt(document.getElementById('sessionid').value),
  549. }),
  550. headers: {
  551. 'Content-Type': 'application/json'
  552. },
  553. method: 'POST'
  554. }).then(function(response) {
  555. if (response.ok) {
  556. console.log('Recording started.');
  557. $('#btn_start_record').prop('disabled', true);
  558. $('#btn_stop_record').prop('disabled', false);
  559. $('#recording-indicator').addClass('active');
  560. } else {
  561. console.error('Failed to start recording.');
  562. }
  563. }).catch(function(error) {
  564. console.error('Error:', error);
  565. });
  566. });
  567. $('#btn_stop_record').click(function() {
  568. console.log('Stopping recording...');
  569. fetch('/record', {
  570. body: JSON.stringify({
  571. type: 'end_record',
  572. sessionid: parseInt(document.getElementById('sessionid').value),
  573. }),
  574. headers: {
  575. 'Content-Type': 'application/json'
  576. },
  577. method: 'POST'
  578. }).then(function(response) {
  579. if (response.ok) {
  580. console.log('Recording stopped.');
  581. $('#btn_start_record').prop('disabled', false);
  582. $('#btn_stop_record').prop('disabled', true);
  583. $('#recording-indicator').removeClass('active');
  584. } else {
  585. console.error('Failed to stop recording.');
  586. }
  587. }).catch(function(error) {
  588. console.error('Error:', error);
  589. });
  590. });
  591. // 继续播放之前的内容(问答结束后)
  592. $('#btn_resume_interrupted').click(function() {
  593. console.log('Continuing playback after QA...');
  594. fetch('/continue_after_qa', {
  595. body: JSON.stringify({
  596. sessionid: parseInt(document.getElementById('sessionid').value),
  597. }),
  598. headers: {
  599. 'Content-Type': 'application/json'
  600. },
  601. method: 'POST'
  602. }).then(function(response) {
  603. return response.json();
  604. }).then(function(data) {
  605. if (data.code === 0) {
  606. console.log('Previous content continued:', data.msg);
  607. // 标记为介绍过程
  608. isDuringIntro = true;
  609. // 启动自动续播逻辑
  610. setTimeout(handleIntroPlayCompleted, 5000); // 假设每条介绍播放时间约为5秒
  611. } else {
  612. console.error('Failed to continue previous content:', data.msg);
  613. }
  614. }).catch(function(error) {
  615. console.error('Error continuing previous content:', error);
  616. });
  617. });
  618. $('#echo-form').on('submit', function(e) {
  619. e.preventDefault();
  620. var message = $('#message').val();
  621. if (!message.trim()) return;
  622. console.log('Sending echo message:', message);
  623. fetch('/human', {
  624. body: JSON.stringify({
  625. text: message,
  626. type: 'echo',
  627. interrupt: true,
  628. sessionid: parseInt(document.getElementById('sessionid').value),
  629. }),
  630. headers: {
  631. 'Content-Type': 'application/json'
  632. },
  633. method: 'POST'
  634. });
  635. $('#message').val('');
  636. });
  637. // 聊天模式表单提交
  638. $('#chat-form').on('submit', function(e) {
  639. e.preventDefault();
  640. var message = $('#chat-message').val();
  641. var selectedKnowledgeBase = $('#knowledge-base-select').val(); // 获取用户选择的知识库
  642. if (!message.trim()) return;
  643. // 检查sessionid是否有效
  644. var sessionid = parseInt(document.getElementById('sessionid').value);
  645. if (sessionid === 0) {
  646. return;
  647. }
  648. console.log('Sending chat message:', message);
  649. console.log('Selected knowledge base:', selectedKnowledgeBase);
  650. console.log('Session ID:', sessionid);
  651. // 如果用户选择了特定知识库,使用该知识库;否则智能识别
  652. const knowledgeBase = selectedKnowledgeBase && selectedKnowledgeBase !== '' ? selectedKnowledgeBase : handleQuestion(message);
  653. fetch('/human', {
  654. body: JSON.stringify({
  655. text: message,
  656. type: 'chat',
  657. interrupt: true,
  658. during_intro: isDuringIntro, // 根据当前是否在介绍过程中设置
  659. sessionid: sessionid,
  660. knowledge_base: knowledgeBase // 添加知识库信息
  661. }),
  662. headers: {
  663. 'Content-Type': 'application/json'
  664. },
  665. method: 'POST'
  666. }).then(function(response) {
  667. return response.json();
  668. }).then(function(data) {
  669. console.log('Response from server:', data);
  670. }).catch(function(error) {
  671. console.error('Error sending message:', error);
  672. });
  673. // 保留isDuringIntro状态,不要立即设置为false,以便后端正确处理中断
  674. // 只有在介绍播放完成后才设置为false
  675. addChatMessage(message, 'user');
  676. $('#chat-message').val('');
  677. });
  678. // 按住说话功能
  679. let mediaRecorder;
  680. let audioChunks = [];
  681. let isRecording = false;
  682. let recognition;
  683. // 检查浏览器是否支持语音识别
  684. const isSpeechRecognitionSupported = 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window;
  685. if (isSpeechRecognitionSupported) {
  686. recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
  687. recognition.continuous = true;
  688. recognition.interimResults = true;
  689. recognition.lang = 'zh-CN';
  690. recognition.onresult = function(event) {
  691. let interimTranscript = '';
  692. let finalTranscript = '';
  693. for (let i = event.resultIndex; i < event.results.length; ++i) {
  694. if (event.results[i].isFinal) {
  695. finalTranscript += event.results[i][0].transcript;
  696. } else {
  697. interimTranscript += event.results[i][0].transcript;
  698. $('#chat-message').val(interimTranscript);
  699. }
  700. }
  701. if (finalTranscript) {
  702. $('#chat-message').val(finalTranscript);
  703. }
  704. };
  705. recognition.onerror = function(event) {
  706. console.error('语音识别错误:', event.error);
  707. };
  708. }
  709. // 按住说话按钮事件
  710. $('#voice-record-btn').on('mousedown touchstart', function(e) {
  711. e.preventDefault();
  712. startRecording();
  713. }).on('mouseup mouseleave touchend', function() {
  714. if (isRecording) {
  715. stopRecording();
  716. }
  717. });
  718. // 开始录音
  719. function startRecording() {
  720. if (isRecording) return;
  721. navigator.mediaDevices.getUserMedia({ audio: true })
  722. .then(function(stream) {
  723. audioChunks = [];
  724. mediaRecorder = new MediaRecorder(stream);
  725. mediaRecorder.ondataavailable = function(e) {
  726. if (e.data.size > 0) {
  727. audioChunks.push(e.data);
  728. }
  729. };
  730. mediaRecorder.start();
  731. isRecording = true;
  732. $('#voice-record-btn').addClass('recording-pulse');
  733. $('#voice-record-btn').css('background-color', '#dc3545');
  734. if (recognition) {
  735. recognition.start();
  736. }
  737. })
  738. .catch(function(error) {
  739. console.error('无法访问麦克风:', error);
  740. alert('无法访问麦克风,请检查浏览器权限设置。');
  741. });
  742. }
  743. function stopRecording() {
  744. if (!isRecording) return;
  745. mediaRecorder.stop();
  746. isRecording = false;
  747. // 停止所有音轨
  748. mediaRecorder.stream.getTracks().forEach(track => track.stop());
  749. // 视觉反馈恢复
  750. $('#voice-record-btn').removeClass('recording-pulse');
  751. $('#voice-record-btn').css('background-color', '');
  752. // 停止语音识别
  753. if (recognition) {
  754. recognition.stop();
  755. }
  756. // 获取识别的文本并发送
  757. setTimeout(function() {
  758. const recognizedText = $('#chat-message').val().trim();
  759. if (recognizedText) {
  760. // 检查sessionid是否有效
  761. var sessionid = parseInt(document.getElementById('sessionid').value);
  762. if (sessionid === 0) {
  763. return;
  764. }
  765. // 发送识别的文本
  766. var selectedKnowledgeBase = $('#knowledge-base-select').val(); // 获取用户选择的知识库
  767. const knowledgeBase = selectedKnowledgeBase && selectedKnowledgeBase !== '' ? selectedKnowledgeBase : handleQuestion(recognizedText);
  768. console.log('Sending voice message:', recognizedText);
  769. console.log('Session ID:', sessionid);
  770. fetch('/human', {
  771. body: JSON.stringify({
  772. text: recognizedText,
  773. type: 'chat',
  774. interrupt: true,
  775. during_intro: isDuringIntro, // 根据当前是否在介绍过程中设置
  776. sessionid: sessionid,
  777. knowledge_base: knowledgeBase
  778. }),
  779. headers: {
  780. 'Content-Type': 'application/json'
  781. },
  782. method: 'POST'
  783. }).then(function(response) {
  784. return response.json();
  785. }).then(function(data) {
  786. console.log('Response from server:', data);
  787. }).catch(function(error) {
  788. console.error('Error sending message:', error);
  789. });
  790. // 保留isDuringIntro状态,不要立即设置为false,以便后端正确处理中断
  791. addChatMessage(recognizedText, 'user');
  792. $('#chat-message').val('');
  793. }
  794. }, 500);
  795. }
  796. // WebRTC 相关功能
  797. if (typeof window.onWebRTCConnected === 'function') {
  798. const originalOnConnected = window.onWebRTCConnected;
  799. window.onWebRTCConnected = function() {
  800. updateConnectionStatus('connected');
  801. if (originalOnConnected) originalOnConnected();
  802. };
  803. } else {
  804. window.onWebRTCConnected = function() {
  805. updateConnectionStatus('connected');
  806. };
  807. }
  808. // 当连接断开时更新状态
  809. if (typeof window.onWebRTCDisconnected === 'function') {
  810. const originalOnDisconnected = window.onWebRTCDisconnected;
  811. window.onWebRTCDisconnected = function() {
  812. updateConnectionStatus('disconnected');
  813. if (originalOnDisconnected) originalOnDisconnected();
  814. };
  815. } else {
  816. window.onWebRTCDisconnected = function() {
  817. updateConnectionStatus('disconnected');
  818. };
  819. }
  820. // SRS WebRTC播放功能
  821. var sdk = null; // 全局处理器,用于在重新发布时进行清理
  822. function startPlay() {
  823. // 关闭之前的连接
  824. if (sdk) {
  825. sdk.close();
  826. }
  827. sdk = new SrsRtcWhipWhepAsync();
  828. $('#video').prop('srcObject', sdk.stream);
  829. var host = window.location.hostname;
  830. var url = "http://" + host + ":1985/rtc/v1/whep/?app=live&stream=livestream";
  831. sdk.play(url).then(function(session) {
  832. console.log('WebRTC播放已启动,会话ID:', session.sessionid);
  833. }).catch(function(reason) {
  834. sdk.close();
  835. console.error('WebRTC播放失败:', reason);
  836. });
  837. }
  838. });
  839. </script>
  840. </body>
  841. </html>