Spaces:
Running
Running
cfahlgren1 HF Staff
convert talking pixel to a sveltekit app that runs pivo-1.4m tts in the browser
3ec6b9e Download src/lib/bench-worker.ts from cfahlgren1/talking-pixel: direct link, hf CLI and curl.
- Browser
- Download file 4.8 kB
-
https://huggingface.co/spaces/cfahlgren1/talking-pixel/resolve/main/src/lib/bench-worker.ts
- Command line
-
hf download hf://spaces/cfahlgren1/talking-pixel/src/lib/bench-worker.ts
-
curl -L -o bench-worker.ts https://huggingface.co/spaces/cfahlgren1/talking-pixel/resolve/main/src/lib/bench-worker.ts
4.8 kB
| // Benchmark worker: runs one backend over the test sentences and reports timings. | |
| import { loadAssets } from './assets.ts'; | |
| import type { LoadedAssets } from './assets.ts'; | |
| import { parseLexicon } from './frontend.ts'; | |
| import type { Voice } from './frontend.ts'; | |
| import { PiVoEngine } from './engine.ts'; | |
| import type { Backend, RenderStats } from './engine.ts'; | |
| import { createLiteBackend, createOrtBackend } from './backends.ts'; | |
| import type { OrtModule } from './backends.ts'; | |
| export interface BenchOptions { | |
| ortModule?: string; | |
| ortWasmPaths?: string; | |
| threads?: number; | |
| } | |
| export interface BenchSentence { | |
| text: string; | |
| voice: Voice; | |
| stem: string; | |
| } | |
| export interface BenchRequest { | |
| kind: string; | |
| opts: BenchOptions; | |
| sentences: BenchSentence[]; | |
| reps: number; | |
| refBase: string | null; | |
| baseUrl: string; | |
| } | |
| export interface BenchRow { | |
| stem: string; | |
| seconds: number; | |
| planMs: number; | |
| totalMs: number; | |
| firstMs: number; | |
| stats: RenderStats | null; | |
| parity: { corr: number; snr: number; sameLength: boolean } | null; | |
| } | |
| export type BenchResponse = | |
| | { ok: true; kind: string; name: string; initMs: number; rows: BenchRow[]; crossOriginIsolated: boolean; threads: number } | |
| | { ok: false; kind?: string; error: string }; | |
| const scope = self as unknown as { | |
| postMessage(message: BenchResponse): void; | |
| onmessage: ((event: MessageEvent<BenchRequest>) => void) | null; | |
| crossOriginIsolated: boolean; | |
| }; | |
| let assets: LoadedAssets | null = null; | |
| async function readWav(url: string): Promise<Float32Array | null> { | |
| const r = await fetch(url); | |
| if (!r.ok) return null; | |
| const v = new DataView(await r.arrayBuffer()); | |
| const n = (v.byteLength - 44) / 2; | |
| const out = new Float32Array(n); | |
| for (let i = 0; i < n; i++) out[i] = v.getInt16(44 + i * 2, true) / 32768; | |
| return out; | |
| } | |
| function snr(a: Float32Array, ref: Float32Array) { | |
| const n = Math.min(a.length, ref.length); | |
| let dot = 0; | |
| let aa = 0; | |
| let bb = 0; | |
| let err = 0; | |
| for (let i = 0; i < n; i++) { | |
| // compare after the same PCM16 quantization as the reference WAV | |
| const q = Math.round(Math.max(-1, Math.min(1, a[i])) * 32767) / 32768; | |
| dot += q * ref[i]; | |
| aa += q * q; | |
| bb += ref[i] * ref[i]; | |
| err += (q - ref[i]) ** 2; | |
| } | |
| return { corr: dot / Math.sqrt(aa * bb), snr: 10 * Math.log10(bb / Math.max(err, 1e-30)), sameLength: a.length === ref.length }; | |
| } | |
| async function makeBackend(kind: string, opts: BenchOptions, loaded: LoadedAssets): Promise<Backend<never>> { | |
| if (kind.startsWith('ort-')) { | |
| const ort = (await import(/* @vite-ignore */ opts.ortModule as string)) as OrtModule; | |
| ort.env.wasm.wasmPaths = opts.ortWasmPaths; | |
| ort.env.wasm.numThreads = opts.threads ?? 1; | |
| ort.env.wasm.simd = true; | |
| return (await createOrtBackend(ort, loaded.buffers, { ep: kind === 'ort-webgpu' ? 'webgpu' : 'wasm' })) as unknown as Backend<never>; | |
| } | |
| return createLiteBackend(loaded.buffers, { vocoder: kind.replace('lite-', '') as 'wasm' | 'js' | 'interpreted' }) as unknown as Backend<never>; | |
| } | |
| scope.onmessage = async (e) => { | |
| const { kind, opts, sentences, reps, refBase, baseUrl } = e.data; | |
| try { | |
| assets ??= await loadAssets(baseUrl); | |
| const t0 = performance.now(); | |
| const backend = await makeBackend(kind, opts, assets); | |
| const initMs = performance.now() - t0; | |
| const engine = new PiVoEngine(backend, parseLexicon(new TextDecoder().decode(assets.buffers.cmudict))); | |
| // warm-up (not timed) | |
| await engine.render(await engine.plan(sentences[0].text, { voice: 'Pi' })); | |
| const rows: BenchRow[] = []; | |
| for (const s of sentences) { | |
| for (let r = 0; r < reps; r++) { | |
| const tStart = performance.now(); | |
| const plan = await engine.plan(s.text, { voice: s.voice }); | |
| const tPlan = performance.now() - tStart; | |
| let first: number | null = null; | |
| let stats: RenderStats | null = null; | |
| const audio = await engine.render(plan, { | |
| onFrames: () => { | |
| first ??= performance.now() - tStart; | |
| }, | |
| onStats: (x) => (stats = x), | |
| }); | |
| const total = performance.now() - tStart; | |
| let parity: BenchRow['parity'] = null; | |
| if (refBase && r === 0) { | |
| const ref = await readWav(`${refBase}/${s.stem}.wav`).catch(() => null); | |
| if (ref) parity = snr(audio, ref); | |
| } | |
| rows.push({ stem: s.stem, seconds: audio.length / 16000, planMs: tPlan, totalMs: total, firstMs: first ?? total, stats, parity }); | |
| } | |
| } | |
| scope.postMessage({ ok: true, kind, name: backend.name, initMs, rows, crossOriginIsolated: scope.crossOriginIsolated, threads: opts.threads ?? 1 }); | |
| backend.release?.(); | |
| } catch (error) { | |
| scope.postMessage({ ok: false, kind, error: String((error as Error)?.stack || error) }); | |
| } | |
| }; | |