g-2-render / index.html
fdbw's picture
Update index.html
76e1193 verified
Raw History Blame Contribute Delete
14.4 kB
<!DOCTYPE html>
<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>