/** * Whisper laeuft im Worker, nicht im Hauptthread: die Erkennung rechnet je nach * Modell mehrere Sekunden, und solange darf die Oberflaeche nicht einfrieren. * * Das Modell wird beim ersten Laden vom Hugging-Face-CDN geholt und danach im * Browser-Cache gehalten. Die Aufnahme selbst verlaesst das Geraet nie. */ import { pipeline, env, type AutomaticSpeechRecognitionPipeline } from '@huggingface/transformers'; // Ohne das versucht transformers.js zuerst einen lokalen Pfad und laeuft in // unnoetige 404er. env.allowLocalModels = false; env.allowRemoteModels = true; export type WorkerRequest = | { type: 'load'; model: string; device: 'webgpu' | 'wasm'; dtype: string } | { type: 'transcribe'; id: number; audio: Float32Array; language: string }; export type WorkerResponse = | { type: 'progress'; loaded: number; total: number; file: string } | { type: 'ready'; ms: number; downloadedBytes: number } | { type: 'result'; id: number; text: string; ms: number } | { type: 'error'; id?: number; message: string }; /** * `pipeline()` ist ueber alle Aufgabenarten hinweg typisiert; TypeScript kann * die entstehende Union nicht mehr darstellen ("union type too complex"). * Deshalb hier eine schmale Signatur nur fuer die Spracherkennung. */ type CreatePipeline = ( task: 'automatic-speech-recognition', model: string, options: Record ) => Promise; const createPipeline = pipeline as unknown as CreatePipeline; let transcriber: AutomaticSpeechRecognitionPipeline | null = null; /** Groesse je Datei, damit der Fortschritt ueber alle Dateien stimmt. */ const files = new Map(); const post = (message: WorkerResponse) => self.postMessage(message); async function load(model: string, device: 'webgpu' | 'wasm', dtype: string) { files.clear(); const started = performance.now(); transcriber = await createPipeline('automatic-speech-recognition', model, { device, dtype, progress_callback: (event: { status: string; file?: string; loaded?: number; total?: number; }) => { if (event.status !== 'progress' || !event.file) return; files.set(event.file, { loaded: event.loaded ?? 0, total: event.total ?? 0 }); let loaded = 0; let total = 0; for (const entry of files.values()) { loaded += entry.loaded; total += entry.total; } post({ type: 'progress', loaded, total, file: event.file }); }, }); let downloadedBytes = 0; for (const entry of files.values()) downloadedBytes += entry.total; post({ type: 'ready', ms: Math.round(performance.now() - started), downloadedBytes }); } async function transcribe(id: number, audio: Float32Array, language: string) { if (!transcriber) throw new Error('Das Modell ist nicht geladen.'); const started = performance.now(); const output = await transcriber(audio, { language, task: 'transcribe', // Laengere Aufnahmen werden in Fenster zerlegt; der Ueberlappungsbereich // verhindert abgeschnittene Woerter an den Grenzen. chunk_length_s: 30, stride_length_s: 5, }); const text = Array.isArray(output) ? output.map((part) => part.text ?? '').join(' ') : (output.text ?? ''); post({ type: 'result', id, text: text.trim(), ms: Math.round(performance.now() - started) }); } /** * Beim Anlegen der ONNX-Sitzung koennen Fehler ausserhalb der await-Kette * auftreten - etwa "Array buffer allocation failed", wenn das Modell zu gross * fuer einen zusammenhaengenden Speicherblock ist. Ohne diese Handler bleibt * das Lade-Promise fuer immer offen und die Oberflaeche haengt bei "Laedt...". */ function reportFatal(reason: unknown) { const raw = reason instanceof Error ? reason.message : String(reason); const isMemory = /allocation failed|out of memory|Array buffer/i.test(raw); post({ type: 'error', message: isMemory ? `Der Speicher reicht fuer dieses Modell in dieser Genauigkeit nicht (${raw}). Bitte eine kleinere Genauigkeit wie q4 waehlen.` : raw, }); } self.addEventListener('error', (event) => { event.preventDefault(); reportFatal(event.error ?? event.message); }); self.addEventListener('unhandledrejection', (event) => { event.preventDefault(); reportFatal(event.reason); }); self.onmessage = async (event: MessageEvent) => { const request = event.data; try { if (request.type === 'load') { await load(request.model, request.device, request.dtype); } else { await transcribe(request.id, request.audio, request.language); } } catch (err) { post({ type: 'error', id: request.type === 'transcribe' ? request.id : undefined, message: err instanceof Error ? err.message : String(err), }); } };