Spaces:
Running
Running
Download index.html from fdbw/g-2-render: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/fdbw/g-2-render/resolve/main/index.html
- Command line
-
hf download hf://spaces/fdbw/g-2-render/index.html
-
curl -L -o index.html https://huggingface.co/spaces/fdbw/g-2-render/resolve/main/index.html
14.4 kB
| <html lang="zh"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | |
| <title>实时互译 - 诊断版</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; background: #0d0d0d; color: #eee; | |
| font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; | |
| touch-action: manipulation; overflow: hidden; | |
| } | |
| .screen { display: none; height: 100%; flex-direction: column; } | |
| .screen.active { display: flex; } | |
| #startScreen { | |
| justify-content: center; align-items: center; gap: 20px; | |
| padding: 24px 16px; text-align: center; | |
| } | |
| #startScreen h2 { font-size: 22px; color: #8ecfff; } | |
| .subtitle { font-size: 13px; color: #888; line-height: 1.6; } | |
| .lang-row { display: flex; gap: 14px; justify-content: center; width: 100%; max-width: 340px; } | |
| .lang-box { display: flex; flex-direction: column; gap: 6px; flex: 1; } | |
| .lang-box label { font-size: 13px; } | |
| .lang-label-a { color: #4fc3f7; } | |
| .lang-label-b { color: #ffb74d; } | |
| .lang-box select { | |
| width: 100%; font-size: 15px; padding: 10px; | |
| background: #1a1a1a; color: #eee; border: 1px solid #333; border-radius: 10px; | |
| text-align: center; | |
| } | |
| .bigBtn { | |
| width: 200px; padding: 14px; font-size: 16px; border: none; border-radius: 12px; | |
| color: #fff; background: linear-gradient(135deg, #1b5e20, #2e7d32); | |
| } | |
| .warning { | |
| background: #3e2723; color: #ffcc80; padding: 10px 14px; border-radius: 10px; | |
| font-size: 13px; line-height: 1.5; max-width: 320px; | |
| } | |
| #callScreen { height: 100%; } | |
| #topBar { | |
| display: flex; justify-content: space-between; align-items: center; | |
| padding: 10px 12px; background: #111; border-bottom: 1px solid #222; | |
| } | |
| #topBar .title { font-size: 15px; font-weight: bold; color: #8ecfff; } | |
| .top-btn { | |
| background: rgba(255,255,255,0.06); color: #aaa; border: 1px solid #333; | |
| border-radius: 8px; padding: 5px 10px; font-size: 12px; margin-left: 6px; | |
| } | |
| #statusBar { | |
| padding: 8px 12px; text-align: center; font-size: 13px; color: #ccc; | |
| background: #111; border-bottom: 1px solid #1a1a1a; min-height: 40px; | |
| } | |
| #messageArea { | |
| flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; | |
| } | |
| #messageArea:empty::after { | |
| content: "先点麦克风开始 → 说话 → 再点同一个麦克风结束"; | |
| display: block; margin: auto; color: #444; font-size: 14px; text-align: center; padding: 20px; | |
| } | |
| .msg-item { | |
| background: #141414; border-radius: 10px; padding: 10px 12px; | |
| border-left: 4px solid #333; | |
| } | |
| .msg-item.role-a { border-left-color: #4fc3f7; } | |
| .msg-item.role-b { border-left-color: #ffb74d; } | |
| .msg-header { font-size: 11px; color: #888; margin-bottom: 4px; } | |
| .msg-original { font-size: 16px; color: #ddd; margin-bottom: 4px; } | |
| .msg-translation { font-size: 15px; color: #ffe58e; border-top: 1px solid #2a2a2a; padding-top: 4px; } | |
| .msg-translation.pending { color: #666; font-style: italic; } | |
| #debugBar { | |
| font-size: 12px; color: #aaa; padding: 6px 10px; background: #1a1a1a; | |
| border-top: 1px solid #333; text-align: center; min-height: 32px; | |
| } | |
| #micArea { display: flex; border-top: 1px solid #1a1a1a; background: #0a0a0a; } | |
| .mic-btn-wrap { | |
| flex: 1; display: flex; flex-direction: column; align-items: center; | |
| padding: 14px 8px 16px; gap: 6px; border-right: 1px solid #1a1a1a; | |
| } | |
| .mic-btn-wrap:last-child { border-right: none; } | |
| .mic-label { font-size: 12px; font-weight: 600; } | |
| .mic-label-a { color: #4fc3f7; } | |
| .mic-label-b { color: #ffb74d; } | |
| .mic-btn { | |
| width: 70px; height: 70px; border-radius: 50%; border: none; font-size: 26px; | |
| display: flex; align-items: center; justify-content: center; | |
| } | |
| #micA { background: #1a2a3a; color: #4fc3f7; box-shadow: 0 0 0 3px rgba(79,195,247,0.25); } | |
| #micB { background: #2a1e0a; color: #ffb74d; box-shadow: 0 0 0 3px rgba(255,183,77,0.25); } | |
| .mic-btn.recording { animation: pulse 1.1s infinite; } | |
| #micA.recording { background: #0a4a7a; } | |
| #micB.recording { background: #5a2e00; } | |
| .mic-btn.locked { opacity: 0.3; } | |
| @keyframes pulse { | |
| 0%,100% { transform: scale(1); } | |
| 50% { transform: scale(1.07); } | |
| } | |
| .mic-status { font-size: 11px; color: #666; height: 16px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="startScreen" class="screen active"> | |
| <h2>🗣️ 实时互译 · 诊断版</h2> | |
| <div class="lang-row"> | |
| <div class="lang-box"> | |
| <label class="lang-label-a">语言 A</label> | |
| <select id="langA"></select> | |
| </div> | |
| <div class="lang-box"> | |
| <label class="lang-label-b">语言 B</label> | |
| <select id="langB"></select> | |
| </div> | |
| </div> | |
| <button class="bigBtn" id="startBtn">开始通话</button> | |
| <div class="warning"> | |
| <b>重要:</b>请务必用「新标签页打开」本页面!<br> | |
| 在 Hugging Face 嵌入窗口里识别经常失败。 | |
| </div> | |
| <div class="subtitle"> | |
| 操作方法:点击麦克风 → 说话 → <b>再点一次同一个麦克风结束</b> | |
| </div> | |
| </div> | |
| <div id="callScreen" class="screen"> | |
| <div id="topBar"> | |
| <span class="title">互译中 · 诊断</span> | |
| <div> | |
| <button class="top-btn" id="clearBtn">清空</button> | |
| <button class="top-btn" id="exitBtn">退出</button> | |
| </div> | |
| </div> | |
| <div id="statusBar">准备就绪</div> | |
| <div id="messageArea"></div> | |
| <div id="debugBar">等待操作</div> | |
| <div id="micArea"> | |
| <div class="mic-btn-wrap"> | |
| <div class="mic-label mic-label-a">A 说话</div> | |
| <button class="mic-btn" id="micA">🎤</button> | |
| <div class="mic-status" id="statusA">点击开始</div> | |
| </div> | |
| <div class="mic-btn-wrap"> | |
| <div class="mic-label mic-label-b">B 说话</div> | |
| <button class="mic-btn" id="micB">🎤</button> | |
| <div class="mic-status" id="statusB">点击开始</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const LANGS = [ | |
| { code: 'zh-CN', name: '中文' }, | |
| { code: 'en-US', name: 'English' }, | |
| { code: 'fr-FR', name: 'Français' }, | |
| { code: 'ja-JP', name: '日本語' }, | |
| ]; | |
| const langASel = document.getElementById('langA'); | |
| const langBSel = document.getElementById('langB'); | |
| LANGS.forEach(l => { | |
| [langASel, langBSel].forEach(sel => { | |
| const o = document.createElement('option'); | |
| o.value = l.code; o.textContent = l.name; | |
| sel.appendChild(o); | |
| }); | |
| }); | |
| langASel.value = 'zh-CN'; | |
| langBSel.value = 'en-US'; | |
| let langA = 'zh-CN', langB = 'en-US'; | |
| let recordingRole = null; | |
| let isPlaying = false; | |
| let recognition = null; | |
| let currentItem = null; | |
| let collectedText = ''; // 收集到的所有文字 | |
| let maxTimer = null; | |
| const statusBar = document.getElementById('statusBar'); | |
| const debugBar = document.getElementById('debugBar'); | |
| const messageArea = document.getElementById('messageArea'); | |
| const micA = document.getElementById('micA'); | |
| const micB = document.getElementById('micB'); | |
| const statusA = document.getElementById('statusA'); | |
| const statusB = document.getElementById('statusB'); | |
| function log(msg) { | |
| console.log(msg); | |
| debugBar.textContent = msg; | |
| } | |
| function setUI(role, state) { | |
| const btn = role === 'a' ? micA : micB; | |
| const sts = role === 'a' ? statusA : statusB; | |
| btn.classList.remove('recording', 'locked'); | |
| if (state === 'recording') { | |
| btn.classList.add('recording'); | |
| sts.textContent = '再说完点一次结束'; | |
| } else if (state === 'locked') { | |
| btn.classList.add('locked'); | |
| sts.textContent = isPlaying ? '播放中' : '锁定'; | |
| } else { | |
| sts.textContent = '点击开始'; | |
| } | |
| } | |
| function refreshUI() { | |
| if (isPlaying) { | |
| setUI('a', 'locked'); setUI('b', 'locked'); | |
| } else if (recordingRole === 'a') { | |
| setUI('a', 'recording'); setUI('b', 'idle'); | |
| } else if (recordingRole === 'b') { | |
| setUI('a', 'idle'); setUI('b', 'recording'); | |
| } else { | |
| setUI('a', 'idle'); setUI('b', 'idle'); | |
| } | |
| } | |
| function forceStop() { | |
| if (maxTimer) { clearTimeout(maxTimer); maxTimer = null; } | |
| if (recognition) { | |
| try { | |
| recognition.onresult = null; | |
| recognition.onerror = null; | |
| recognition.onend = null; | |
| recognition.stop(); | |
| } catch(e){} | |
| recognition = null; | |
| } | |
| } | |
| function startListening(role) { | |
| if (isPlaying) { | |
| statusBar.textContent = '请等待播放结束'; | |
| return; | |
| } | |
| // 再次点击同一个麦克风 = 结束 | |
| if (recordingRole === role) { | |
| log('手动结束,当前收集文字: ' + (collectedText || '(空)')); | |
| statusBar.textContent = '正在处理...'; | |
| forceStop(); | |
| setTimeout(() => { | |
| const text = collectedText.trim(); | |
| recordingRole = null; | |
| refreshUI(); | |
| if (text) { | |
| processFinal(role, text); | |
| } else { | |
| statusBar.textContent = '没有识别到任何文字,请重试(建议用新标签页打开)'; | |
| log('最终结果为空'); | |
| } | |
| }, 500); | |
| return; | |
| } | |
| // 切换角色时先停掉上一个 | |
| if (recordingRole) { | |
| forceStop(); | |
| recordingRole = null; | |
| } | |
| collectedText = ''; | |
| currentItem = null; | |
| forceStop(); | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SR) { | |
| alert('当前浏览器不支持语音识别'); | |
| return; | |
| } | |
| recognition = new SR(); | |
| recognition.lang = (role === 'a' ? langA : langB); | |
| recognition.continuous = true; | |
| recognition.interimResults = true; | |
| recognition.maxAlternatives = 1; | |
| recordingRole = role; | |
| refreshUI(); | |
| statusBar.textContent = role.toUpperCase() + ' 请说话,说完后再点一次麦克风'; | |
| log('开始识别 lang=' + recognition.lang); | |
| recognition.onstart = () => log('onstart 触发'); | |
| recognition.onresult = (event) => { | |
| let interim = ''; | |
| let final = ''; | |
| for (let i = event.resultIndex; i < event.results.length; i++) { | |
| const t = event.results[i][0].transcript; | |
| if (event.results[i].isFinal) final += t; | |
| else interim += t; | |
| } | |
| if (final) { | |
| collectedText = (collectedText + ' ' + final).trim(); | |
| log('收到最终结果: ' + final); | |
| } | |
| if (interim) log('中间结果: ' + interim); | |
| const show = (collectedText + (interim ? ' ' + interim : '')).trim(); | |
| if (show) { | |
| if (!currentItem) currentItem = createItem(role); | |
| currentItem.querySelector('.msg-original').textContent = show + (interim ? '…' : ''); | |
| currentItem.querySelector('.msg-translation').textContent = '识别中,说完请再点麦克风'; | |
| currentItem.querySelector('.msg-translation').classList.add('pending'); | |
| scrollBottom(); | |
| } | |
| }; | |
| recognition.onerror = (e) => { | |
| log('onerror: ' + e.error); | |
| if (e.error !== 'aborted') { | |
| statusBar.textContent = '错误: ' + e.error; | |
| } | |
| }; | |
| recognition.onend = () => log('onend 触发'); | |
| try { | |
| recognition.start(); | |
| // 20秒保护 | |
| maxTimer = setTimeout(() => { | |
| if (recordingRole === role) { | |
| log('超时自动结束'); | |
| statusBar.textContent = '超时,正在处理...'; | |
| forceStop(); | |
| setTimeout(() => { | |
| const text = collectedText.trim(); | |
| recordingRole = null; | |
| refreshUI(); | |
| if (text) processFinal(role, text); | |
| else statusBar.textContent = '超时且无结果'; | |
| }, 400); | |
| } | |
| }, 20000); | |
| } catch (e) { | |
| log('start 异常: ' + e.message); | |
| alert('启动失败: ' + e.message); | |
| recordingRole = null; | |
| refreshUI(); | |
| } | |
| } | |
| async function processFinal(role, text) { | |
| const sourceLang = role === 'a' ? langA : langB; | |
| const targetLang = role === 'a' ? langB : langA; | |
| if (!currentItem) currentItem = createItem(role); | |
| currentItem.querySelector('.msg-original').textContent = text; | |
| currentItem.querySelector('.msg-translation').textContent = '翻译中...'; | |
| currentItem.querySelector('.msg-translation').classList.add('pending'); | |
| scrollBottom(); | |
| statusBar.textContent = '正在翻译...'; | |
| log('翻译原文: ' + text); | |
| let translated = text; | |
| try { | |
| // 简单 fallback | |
| const from = sourceLang.split('-')[0]; | |
| const to = targetLang.split('-')[0]; | |
| const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${from}|${to}`; | |
| const res = await fetch(url); | |
| const data = await res.json(); | |
| if (data.responseData?.translatedText) { | |
| translated = data.responseData.translatedText; | |
| } | |
| } catch (e) { | |
| log('翻译失败'); | |
| } | |
| currentItem.querySelector('.msg-translation').textContent = translated; | |
| currentItem.querySelector('.msg-translation').classList.remove('pending'); | |
| scrollBottom(); | |
| // 播放 | |
| if (window.speechSynthesis) { | |
| isPlaying = true; | |
| refreshUI(); | |
| statusBar.textContent = '播放译音中...'; | |
| const u = new SpeechSynthesisUtterance(translated); | |
| u.lang = targetLang; | |
| u.onend = () => { | |
| isPlaying = false; | |
| refreshUI(); | |
| statusBar.textContent = '播放结束,可继续'; | |
| }; | |
| speechSynthesis.cancel(); | |
| speechSynthesis.speak(u); | |
| } else { | |
| statusBar.textContent = '翻译完成(不支持播放)'; | |
| } | |
| } | |
| function createItem(role) { | |
| const item = document.createElement('div'); | |
| item.className = 'msg-item role-' + role; | |
| item.innerHTML = ` | |
| <div class="msg-header">${role.toUpperCase()}</div> | |
| <div class="msg-original">…</div> | |
| <div class="msg-translation pending">…</div> | |
| `; | |
| messageArea.appendChild(item); | |
| return item; | |
| } | |
| function scrollBottom() { | |
| messageArea.scrollTop = messageArea.scrollHeight; | |
| } | |
| micA.onclick = () => startListening('a'); | |
| micB.onclick = () => startListening('b'); | |
| document.getElementById('startBtn').onclick = () => { | |
| langA = langASel.value; | |
| langB = langBSel.value; | |
| if (langA === langB) return alert('请选择不同语言'); | |
| document.getElementById('startScreen').classList.remove('active'); | |
| document.getElementById('callScreen').classList.add('active'); | |
| messageArea.innerHTML = ''; | |
| collectedText = ''; | |
| recordingRole = null; | |
| isPlaying = false; | |
| statusBar.textContent = '点击麦克风开始,说完再点一次结束'; | |
| log('进入通话界面'); | |
| refreshUI(); | |
| }; | |
| document.getElementById('clearBtn').onclick = () => { | |
| messageArea.innerHTML = ''; | |
| currentItem = null; | |
| }; | |
| document.getElementById('exitBtn').onclick = () => { | |
| forceStop(); | |
| speechSynthesis.cancel(); | |
| document.getElementById('callScreen').classList.remove('active'); | |
| document.getElementById('startScreen').classList.add('active'); | |
| }; | |
| log('页面加载完成 - 请用新标签页打开'); | |
| </script> | |
| </body> | |
| </html> |