import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2'; import * as OpenCC from 'https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/+esm'; env.allowLocalModels = false; let progressCallback = (progress) => { const status = document.getElementById('status'); if (progress.status === 'progress') { const pct = progress.progress != null ? ` ${Math.round(progress.progress)}%` : ''; status.textContent = `Downloading model... ${progress.file}${pct}`; } else if (progress.status === 'done') { status.textContent = 'Model loaded'; } }; env.progress_callback = progressCallback; const languageSelect = document.getElementById('language'); const recordBtn = document.getElementById('record-btn'); const stopBtn = document.getElementById('stop-btn'); const fileInput = document.getElementById('file-input'); const uploadBtn = document.getElementById('upload-btn'); const output = document.getElementById('output'); const status = document.getElementById('status'); const copyBtn = document.getElementById('copy-btn'); let recognition; let transcriber = null; let currentModel = null; const getModelForLanguage = (_lang) => 'Xenova/whisper-small'; fileInput.addEventListener('change', () => { uploadBtn.disabled = !fileInput.files.length; }); const extractAudio = async (file) => { status.textContent = `Reading file... (${(file.size / 1024 / 1024).toFixed(1)} MB)`; const arrayBuffer = await file.arrayBuffer(); status.textContent = 'Decoding audio... (page may be briefly unresponsive)'; await new Promise(r => setTimeout(r, 100)); const audioContext = new AudioContext({ sampleRate: 16000 }); const audioBuffer = await new Promise((resolve, reject) => { audioContext.decodeAudioData(arrayBuffer, resolve, reject); }); status.textContent = `Audio decoded (${Math.round(audioBuffer.duration)}s)`; await audioContext.close(); return audioBuffer.getChannelData(0); }; uploadBtn.addEventListener('click', async () => { const file = fileInput.files[0]; if (!file) return; uploadBtn.disabled = true; output.textContent = ''; try { const selectedLang = languageSelect.value; const modelName = getModelForLanguage(selectedLang); if (!transcriber || currentModel !== modelName) { status.textContent = 'Downloading model...'; transcriber = await pipeline('automatic-speech-recognition', modelName); currentModel = modelName; } status.textContent = 'Processing audio...'; const audio = await extractAudio(file); status.textContent = 'Transcribing...'; const langCode = selectedLang.split('-')[0]; const result = await transcriber(audio, { language: langCode, chunk_length_s: 30, stride_length_s: 5, return_timestamps: false }); let text = result.text; if (selectedLang === 'zh-CN') { const converter = OpenCC.Converter({ from: 'tw', to: 'cn' }); text = converter(text); } output.textContent = text; copyBtn.style.display = 'block'; status.textContent = 'Complete'; } catch (err) { status.textContent = `Error: ${err.message}`; } finally { uploadBtn.disabled = false; } }); recordBtn.addEventListener('click', async () => { try { await navigator.mediaDevices.getUserMedia({ audio: true }); const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { status.textContent = 'Speech recognition not supported in this browser'; return; } recognition = new SpeechRecognition(); recognition.continuous = true; recognition.interimResults = true; recognition.lang = languageSelect.value; let transcript = ''; recognition.onresult = (event) => { let interim = ''; for (let i = event.resultIndex; i < event.results.length; i++) { if (event.results[i].isFinal) { transcript += event.results[i][0].transcript + ' '; } else { interim += event.results[i][0].transcript; } } output.textContent = transcript + interim; copyBtn.style.display = 'block'; }; recognition.start(); recordBtn.disabled = true; stopBtn.disabled = false; status.textContent = 'Recording...'; } catch (err) { status.textContent = 'Microphone access denied'; } }); stopBtn.addEventListener('click', () => { if (recognition) { recognition.stop(); recordBtn.disabled = false; stopBtn.disabled = true; status.textContent = 'Recording stopped'; } }); copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(output.textContent); status.textContent = 'Copied to clipboard'; });