| |
| |
| |
| |
|
|
| const DEFAULT_VOICE = "af_heart"; |
|
|
| type WorkerReply = { id: number; ok: boolean; buffer?: ArrayBuffer; error?: string }; |
|
|
| let worker: Worker | null = null; |
| let nextId = 1; |
| const pending = new Map<number, { resolve: (v: ArrayBuffer | null) => void }>(); |
|
|
| function getWorker(): Worker | null { |
| if (worker) return worker; |
| if (typeof Worker === "undefined") return null; |
| try { |
| worker = new Worker(new URL("./tts.worker.ts", import.meta.url), { type: "module" }); |
| worker.addEventListener("message", (event: MessageEvent<WorkerReply>) => { |
| const { id, ok, buffer } = event.data || ({} as WorkerReply); |
| const entry = pending.get(id); |
| if (!entry) return; |
| pending.delete(id); |
| entry.resolve(ok && buffer ? buffer : null); |
| }); |
| worker.addEventListener("error", () => { |
| |
| for (const [, entry] of pending) entry.resolve(null); |
| pending.clear(); |
| worker = null; |
| }); |
| } catch { |
| worker = null; |
| } |
| return worker; |
| } |
|
|
| |
| export function preloadKokoro(): void { |
| const w = getWorker(); |
| if (w) w.postMessage({ id: nextId++, type: "preload" }); |
| } |
|
|
| |
| |
| export async function synthesizeKokoro( |
| text: string, |
| voice: string = DEFAULT_VOICE, |
| ): Promise<string | null> { |
| const w = getWorker(); |
| if (!w) return null; |
| const id = nextId++; |
| const buffer = await new Promise<ArrayBuffer | null>((resolve) => { |
| pending.set(id, { resolve }); |
| w.postMessage({ id, type: "generate", text, voice }); |
| }); |
| if (!buffer) return null; |
| return URL.createObjectURL(new Blob([buffer], { type: "audio/wav" })); |
| } |
|
|