From f34478391e13bb9a6c34cd9924479df307c98482 Mon Sep 17 00:00:00 2001 From: Kresna Date: Sat, 1 Aug 2026 17:44:38 +0700 Subject: [PATCH] feat(voice-to-text): language selector + larger multilingual model for Bahasa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bahasa (and other non-English) accuracy was poor because Whisper was auto- detecting the language (often mis-guessing on short clips) on the small whisper-base model. - Add a Language selector (default Auto-detect) shown for multilingual models; pass the chosen language + task:'transcribe' to Whisper. Naming the language is the biggest accuracy win at zero download cost. Indonesian/Malay/Javanese/ Sundanese listed first, plus major world languages. - Add 'Multilingual · Better' (onnx-community/whisper-small) as an opt-in model — markedly better for non-English, larger one-time download. whisper-base stays the default multilingual. - Engine: SttModelId gains whisper-small; transcribe() takes an optional { language } (applied only for multilingual models). 545 tests · lint clean · build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/islands/media/VoiceToText.tsx | 51 +++++++++++++++++++++++++++++-- src/tools/media/stt.engine.ts | 15 +++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/islands/media/VoiceToText.tsx b/src/islands/media/VoiceToText.tsx index cc8b459..d77c749 100644 --- a/src/islands/media/VoiceToText.tsx +++ b/src/islands/media/VoiceToText.tsx @@ -17,10 +17,38 @@ import { type TranscriptSegment, } from '@/tools/media/stt.lib'; -const MODELS: { value: SttModelId; label: string; note: string }[] = [ +const MODELS: { value: SttModelId; label: string; note: string; multilingual?: boolean }[] = [ { value: 'onnx-community/whisper-tiny.en', label: 'English · Fast', note: 'smallest download' }, { value: 'onnx-community/whisper-base.en', label: 'English · Accurate', note: 'larger, more accurate' }, - { value: 'onnx-community/whisper-base', label: 'Multilingual', note: 'auto-detects language' }, + { value: 'onnx-community/whisper-base', label: 'Multilingual', note: 'auto-detects language', multilingual: true }, + { value: 'onnx-community/whisper-small', label: 'Multilingual · Better', note: 'much better for non-English (e.g. Bahasa); larger download', multilingual: true }, +]; + +// Whisper source-language options (value = the lowercase name Whisper expects). +// Empty value = auto-detect. Region-relevant languages first. +const LANGUAGES: { value: string; label: string }[] = [ + { value: '', label: 'Auto-detect' }, + { value: 'indonesian', label: 'Indonesian (Bahasa)' }, + { value: 'malay', label: 'Malay' }, + { value: 'javanese', label: 'Javanese' }, + { value: 'sundanese', label: 'Sundanese' }, + { value: 'english', label: 'English' }, + { value: 'chinese', label: 'Chinese' }, + { value: 'japanese', label: 'Japanese' }, + { value: 'korean', label: 'Korean' }, + { value: 'arabic', label: 'Arabic' }, + { value: 'hindi', label: 'Hindi' }, + { value: 'tagalog', label: 'Tagalog' }, + { value: 'thai', label: 'Thai' }, + { value: 'vietnamese', label: 'Vietnamese' }, + { value: 'spanish', label: 'Spanish' }, + { value: 'portuguese', label: 'Portuguese' }, + { value: 'french', label: 'French' }, + { value: 'german', label: 'German' }, + { value: 'italian', label: 'Italian' }, + { value: 'dutch', label: 'Dutch' }, + { value: 'russian', label: 'Russian' }, + { value: 'turkish', label: 'Turkish' }, ]; type Tab = 'text' | 'timestamped' | 'subtitles'; @@ -30,6 +58,7 @@ export default function VoiceToText() { const [audioBlob, setAudioBlob] = useState(null); const [audioUrl, setAudioUrl] = useState(''); const [model, setModel] = useState('onnx-community/whisper-tiny.en'); + const [language, setLanguage] = useState(''); const [modelProgress, setModelProgress] = useState(null); const [transcribing, setTranscribing] = useState(false); const [segments, setSegments] = useState(null); @@ -96,7 +125,8 @@ export default function VoiceToText() { const audio = await decodeToMono16k(audioBlob); const engine = await createTranscriber(model, r => setModelProgress(r)); setModelProgress(null); // model ready (or cached) — now inference (indeterminate) - const segs = await engine.transcribe(audio); + const isMultilingual = MODELS.find(m => m.value === model)?.multilingual; + const segs = await engine.transcribe(audio, { language: isMultilingual ? language || undefined : undefined }); setSegments(segs); setEditedText(segmentsToText(segs)); setTab('text'); @@ -171,6 +201,21 @@ export default function VoiceToText() { + {MODELS.find(m => m.value === model)?.multilingual && ( + + )} +