dashboard.html 41 KB

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