| |
| |
| |
| |
|
|
| const MODEL_ID = "onnx-community/Kokoro-82M-v1.0-ONNX"; |
|
|
| type KokoroModel = { |
| generate: (text: string, options: { voice: string }) => Promise<{ toBlob: () => Blob }>; |
| }; |
|
|
| |
| |
| const ctx = self as unknown as { |
| postMessage: (message: unknown, options?: { transfer?: Transferable[] }) => void; |
| addEventListener: (type: "message", listener: (event: { data: WorkerRequest }) => void) => void; |
| }; |
|
|
| type WorkerRequest = { id: number; type: "generate" | "preload"; text?: string; voice?: string }; |
|
|
| let modelPromise: Promise<KokoroModel> | null = null; |
| let queue: Promise<unknown> = Promise.resolve(); |
|
|
| async function loadModel(): Promise<KokoroModel> { |
| if (!modelPromise) { |
| modelPromise = (async () => { |
| const { KokoroTTS } = await import("kokoro-js"); |
| return (await KokoroTTS.from_pretrained(MODEL_ID, { |
| dtype: "q8", |
| device: "wasm", |
| })) as unknown as KokoroModel; |
| })().catch((error) => { |
| modelPromise = null; |
| throw error; |
| }); |
| } |
| return modelPromise; |
| } |
|
|
| ctx.addEventListener("message", (event) => { |
| const { id, type, text, voice } = event.data || ({} as WorkerRequest); |
| if (type === "preload") { |
| void loadModel().then( |
| () => ctx.postMessage({ id, ok: true }), |
| (error) => ctx.postMessage({ id, ok: false, error: String(error) }), |
| ); |
| return; |
| } |
| if (type === "generate") { |
| queue = queue.then(async () => { |
| try { |
| const model = await loadModel(); |
| const audio = await model.generate(String(text ?? ""), { voice: voice || "af_heart" }); |
| const buffer = await audio.toBlob().arrayBuffer(); |
| ctx.postMessage({ id, ok: true, buffer }, { transfer: [buffer] }); |
| } catch (error) { |
| ctx.postMessage({ id, ok: false, error: String(error) }); |
| } |
| }); |
| } |
| }); |
|
|