|
<!DOCTYPE html> |
|
<html lang="zh"> |
|
<head> |
|
<meta charset="UTF-8" /> |
|
<title>试试翻译</title> |
|
<style> |
|
body { |
|
background-color: #f9f9fc; |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; |
|
margin: 0; |
|
padding: 2rem; |
|
} |
|
.translation-box { |
|
background: #f2f2f8; |
|
border-radius: 12px; |
|
padding: 1.5rem; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
min-height: 200px; |
|
} |
|
.entry { |
|
margin-bottom: 1.5rem; |
|
} |
|
.timestamp { |
|
font-size: 0.75rem; |
|
color: #999; |
|
} |
|
.original { |
|
font-size: 1rem; |
|
color: #333; |
|
} |
|
.translation { |
|
font-size: 1rem; |
|
font-weight: bold; |
|
color: #000; |
|
} |
|
.footer { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-top: 2rem; |
|
} |
|
.lang-select { |
|
background: white; |
|
border-radius: 9999px; |
|
padding: 0.4rem 1rem; |
|
border: none; |
|
font-size: 1rem; |
|
box-shadow: 0 0 0 1px #ddd; |
|
} |
|
.record-button { |
|
background-color: #1e40af; |
|
color: white; |
|
border: none; |
|
padding: 0.6rem 1.2rem; |
|
border-radius: 9999px; |
|
font-size: 1rem; |
|
cursor: pointer; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="translation-box" id="translationBox"> |
|
|
|
</div> |
|
|
|
<div class="footer"> |
|
<select class="lang-select"> |
|
<option>中文 » 英语</option> |
|
</select> |
|
<button class="record-button" onclick="startRecording()">🎤 录音</button> |
|
</div> |
|
|
|
<script> |
|
let ws; |
|
let mediaRecorder; |
|
|
|
function formatTimestamp(ms) { |
|
const sec = ms / 1000; |
|
const min = Math.floor(sec / 60); |
|
const s = (sec % 60).toFixed(1); |
|
return `${String(min).padStart(2, '0')}:${s.padStart(4, '0')}`; |
|
} |
|
|
|
let lastSegId = null; |
|
|
|
function addTranslation(result) { |
|
const box = document.getElementById('translationBox'); |
|
|
|
|
|
const entry = document.createElement('div'); |
|
entry.className = 'entry'; |
|
|
|
console.log(result); |
|
|
|
const start = formatTimestamp(result.bg); |
|
const end = formatTimestamp(result.ed); |
|
|
|
|
|
if (result.seg_id === lastSegId) { |
|
|
|
const existingEntry = box.querySelector(`.entry[data-seg-id="${result.seg_id}"]`); |
|
if (existingEntry) { |
|
const translationDiv = existingEntry.querySelector('.translation'); |
|
translationDiv.innerHTML = result.tranContent; |
|
} |
|
} else { |
|
|
|
entry.setAttribute('data-seg-id', result.seg_id); |
|
entry.innerHTML = ` |
|
<div class="original">${result.context}</div> |
|
<div class="translation">${result.tranContent}</div> |
|
`; |
|
box.appendChild(entry); |
|
} |
|
|
|
|
|
lastSegId = result.seg_id; |
|
} |
|
|
|
|
|
|
|
async function startRecording() { |
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|
const audioContext = new AudioContext({ sampleRate: 16000 }); |
|
const source = audioContext.createMediaStreamSource(stream); |
|
const processor = audioContext.createScriptProcessor(4096, 1, 1); |
|
|
|
const wsUrl = "ws://localhost:9090?from=zh&to=en"; |
|
ws = new WebSocket(wsUrl); |
|
|
|
ws.binaryType = "arraybuffer"; |
|
|
|
ws.onopen = () => { |
|
console.log("WebSocket opened"); |
|
source.connect(processor); |
|
processor.connect(audioContext.destination); |
|
|
|
processor.onaudioprocess = (e) => { |
|
const input = e.inputBuffer.getChannelData(0); |
|
const buffer = new Int16Array(input.length); |
|
for (let i = 0; i < input.length; i++) { |
|
buffer[i] = Math.max(-1, Math.min(1, input[i])) * 0x7FFF; |
|
} |
|
ws.send(buffer); |
|
}; |
|
}; |
|
|
|
ws.onmessage = (event) => { |
|
try { |
|
const msg = JSON.parse(event.data); |
|
if (msg.result) { |
|
addTranslation(msg.result); |
|
} |
|
} catch (e) { |
|
console.error("Parse error:", e); |
|
} |
|
}; |
|
|
|
ws.onerror = (e) => console.error("WebSocket error:", e); |
|
ws.onclose = () => { |
|
console.log("WebSocket closed"); |
|
processor.disconnect(); |
|
source.disconnect(); |
|
}; |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
|