extract shared whisper transcribe helper, use in file-browser and pi stt routes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// Whisper transcription with language-aware translation.
|
||||
//
|
||||
// Strategy: detect the audio's language, then transcribe in that language.
|
||||
// Only ask Whisper to translate (which is English-only) when the detected
|
||||
// language is NOT one the user speaks — i.e. they wouldn't understand the
|
||||
// raw transcript anyway.
|
||||
|
||||
type TranscribeArgs = {
|
||||
file: File | Blob;
|
||||
whisperUrl: string;
|
||||
spokenLanguages: string[];
|
||||
};
|
||||
|
||||
export type TranscribeResult = {
|
||||
text: string;
|
||||
detectedLanguage: string;
|
||||
translated: boolean;
|
||||
};
|
||||
|
||||
// whisper.cpp's "language" field uses full names ("english"); ISO codes live in
|
||||
// `language_probabilities`. Map covers what the user can pick in Profile settings.
|
||||
const LANG_NAME_TO_ISO: Record<string, string> = {
|
||||
english: 'en',
|
||||
portuguese: 'pt',
|
||||
spanish: 'es',
|
||||
french: 'fr',
|
||||
german: 'de',
|
||||
italian: 'it',
|
||||
dutch: 'nl',
|
||||
russian: 'ru',
|
||||
chinese: 'zh',
|
||||
japanese: 'ja',
|
||||
korean: 'ko',
|
||||
};
|
||||
|
||||
export async function transcribeAudio({ file, whisperUrl, spokenLanguages }: TranscribeArgs): Promise<TranscribeResult> {
|
||||
const base = whisperUrl.replace(/\/+$/, '');
|
||||
|
||||
// Step 1: detect language. Bias toward the user's spoken languages — Whisper's
|
||||
// raw auto-detect is unreliable on short clips and often picks English by default.
|
||||
const detectForm = new FormData();
|
||||
detectForm.append('file', file);
|
||||
detectForm.append('temperature', '0.0');
|
||||
detectForm.append('response_format', 'verbose_json');
|
||||
detectForm.append('detect_language', 'true');
|
||||
|
||||
const detectRes = await fetch(`${base}/inference`, { method: 'POST', body: detectForm });
|
||||
if (!detectRes.ok) throw new Error(`Language detection failed (${detectRes.status})`);
|
||||
const detectJson = (await detectRes.json()) as {
|
||||
language?: string;
|
||||
language_probabilities?: Record<string, number>;
|
||||
};
|
||||
|
||||
const probs = detectJson.language_probabilities;
|
||||
let detectedLanguage: string;
|
||||
if (probs && spokenLanguages.length > 0) {
|
||||
// Pick the spoken language with the highest probability
|
||||
const ranked = spokenLanguages
|
||||
.map((code) => [code, probs[code] ?? 0] as const)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
detectedLanguage = ranked[0]?.[0] ?? spokenLanguages[0]!;
|
||||
} else if (probs) {
|
||||
// No spoken-language hint: take the highest-probability ISO code
|
||||
const top = Object.entries(probs).sort((a, b) => b[1] - a[1])[0];
|
||||
detectedLanguage = top?.[0] ?? 'en';
|
||||
} else {
|
||||
// No probabilities field (non-whisper.cpp backend) — try to map the name
|
||||
const raw = (detectJson.language ?? 'en').toLowerCase();
|
||||
detectedLanguage = LANG_NAME_TO_ISO[raw] ?? raw;
|
||||
}
|
||||
|
||||
// Step 2: translate only when the user doesn't speak the detected language
|
||||
const translated = spokenLanguages.length > 0 && !spokenLanguages.includes(detectedLanguage);
|
||||
|
||||
// Step 3: full transcription
|
||||
const transcribeForm = new FormData();
|
||||
transcribeForm.append('file', file);
|
||||
transcribeForm.append('temperature', '0.0');
|
||||
transcribeForm.append('temperature_inc', '0.2');
|
||||
transcribeForm.append('response_format', 'text');
|
||||
transcribeForm.append('language', detectedLanguage);
|
||||
if (translated) transcribeForm.append('translate', 'true');
|
||||
|
||||
const res = await fetch(`${base}/inference`, { method: 'POST', body: transcribeForm });
|
||||
if (!res.ok) throw new Error(`Transcription failed (${res.status})`);
|
||||
const text = (await res.text()).trim();
|
||||
|
||||
return { text, detectedLanguage, translated };
|
||||
}
|
||||
Reference in New Issue
Block a user