| <!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:9191/ws?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> |
|
|