mesm-tts: ten tiny English voices (ONNX + pdn WebAssembly engine), inference, samples, benchmarks
931612f Download web/demo.html from mesmertech/mesm-tts: direct link, hf CLI and curl.
- Browser
- Download file 5.63 kB
-
https://huggingface.co/mesmertech/mesm-tts/resolve/main/web/demo.html
- Command line
-
hf download hf://mesmertech/mesm-tts/web/demo.html
-
curl -L -o demo.html https://huggingface.co/mesmertech/mesm-tts/resolve/main/web/demo.html
5.63 kB
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>mesm-tts: ten voices in this tab</title> | |
| <!-- All ten voices on the pdn WebAssembly engine, with the mesmer.tools TTS Generator's worker (tts-worker.mjs: streaming, natural | |
| sentence joins, per-voice level). Serve the REPOSITORY ROOT over HTTP (module workers do not load from file://), e.g. | |
| python -m http.server 8000 then open http://localhost:8000/web/demo.html | |
| Hosted version: https://mesmer.tools/free-tools/tts-generator --> | |
| <style> | |
| body { font: 15px/1.45 system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; color: #222; } | |
| textarea { width: 100%; font: inherit; padding: .6rem; box-sizing: border-box; } | |
| .voices { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0; } | |
| .voices button { border: 1px solid #bbb; background: #fff; border-radius: 999px; padding: .35rem .8rem; cursor: pointer; font: inherit; } | |
| .voices button[aria-pressed="true"] { background: #222; color: #fff; border-color: #222; } | |
| .voices small { opacity: .65; margin-left: .3rem; } | |
| .row { display: flex; gap: .8rem; align-items: center; flex-wrap: wrap; } | |
| #status { font-variant-numeric: tabular-nums; color: #555; } | |
| </style> | |
| <h1>mesm-tts</h1> | |
| <p>Ten English voices, each one file of 2.3 to 6.9 MB, synthesised on this device in WebAssembly. Nothing is sent anywhere.</p> | |
| <textarea id="text" rows="4">The lighthouse keeper climbed the stairs one last time. Below him, the harbor lights blinked awake, one by one, and the evening ferry sounded its horn.</textarea> | |
| <div class="voices" id="voices"></div> | |
| <div class="row"> | |
| <button id="speak">Speak</button> <button id="stop">Stop</button> | |
| <label>pace <input id="speed" type="range" min="0.7" max="1.4" step="0.05" value="1"></label> | |
| <a id="wav" download hidden>download WAV</a> | |
| </div> | |
| <p id="status">loading the voice list…</p> | |
| <script type="module"> | |
| import { createPlayer } from "./v2/player.mjs"; | |
| const $ = (id) => document.getElementById(id); | |
| const { voices } = await (await fetch(new URL("../voices.json", import.meta.url))).json(); | |
| let worker = null, voiceId = null, player = null, id = 0, ready = null, pcm = []; | |
| async function load(v) { | |
| worker?.terminate(); voiceId = v; | |
| for (const b of $("voices").children) b.setAttribute("aria-pressed", String(b.dataset.id === v)); | |
| const s = voices[v], t0 = performance.now(); | |
| worker = new Worker(new URL("./tts-worker.mjs", import.meta.url), { type: "module" }); | |
| ready = (async () => { | |
| let msg; | |
| if (s.engine === "paradee") msg = { op: "load", kind: "paradee", model: s.default_model, gainDb: s.gain_db }; | |
| else { | |
| $("status").textContent = `downloading ${s.name} (${(s.files[1].bytes / 1e6).toFixed(1)} MB)…`; | |
| const bytes = new Uint8Array(await (await fetch(new URL(`../${s.pdn}`, import.meta.url))).arrayBuffer()); | |
| msg = { op: "load", kind: "pv", model: bytes, join: s.join, gainDb: s.gain_db }; | |
| } | |
| await new Promise((res, rej) => { | |
| worker.onmessage = ({ data: m }) => m.op === "loaded" ? res(m) : m.op === "error" && rej(new Error(m.message)); | |
| worker.postMessage({ ...msg, text: $("text").value }, msg.model instanceof Uint8Array ? [msg.model.buffer] : []); | |
| }).then((m) => ($("status").textContent = `${s.name} ready in ${(performance.now() - t0).toFixed(0)} ms (pdn ${m.build}${m.int8 ? ", int8" : ""})`)); | |
| worker.onmessage = onMessage; | |
| })().catch((e) => ($("status").textContent = e.message)); | |
| return ready; | |
| } | |
| function onMessage({ data: m }) { | |
| if (m.id !== id) return; | |
| if (m.op === "chunk") { pcm.push(m.pcm); if (m.samples.length) player.push(m.samples); } | |
| else if (m.op === "skipped") $("status").textContent = `skipped a sentence: ${m.message}`; | |
| else if (m.op === "error") $("status").textContent = m.message; | |
| else if (m.op === "done") { | |
| player.end(); | |
| $("status").textContent = `${m.audio_s.toFixed(1)} s of speech made in ${m.synth_ms.toFixed(0)} ms ` + | |
| `(${(m.audio_s * 1000 / m.synth_ms).toFixed(0)}x real time, 1 thread); first audio after ${m.first_ms.toFixed(0)} ms; engine memory ${m.memory_mb.toFixed(0)} MB`; | |
| const n = pcm.reduce((a, p) => a + p.length, 0), b = new DataView(new ArrayBuffer(44 + 2 * n)); | |
| const w = (o, s) => [...s].forEach((c, i) => b.setUint8(o + i, c.charCodeAt(0))); | |
| w(0, "RIFF"); b.setUint32(4, 36 + 2 * n, true); w(8, "WAVEfmt "); b.setUint32(16, 16, true); b.setUint16(20, 1, true); b.setUint16(22, 1, true); | |
| b.setUint32(24, 24000, true); b.setUint32(28, 48000, true); b.setUint16(32, 2, true); b.setUint16(34, 16, true); w(36, "data"); b.setUint32(40, 2 * n, true); | |
| let o = 44; for (const p of pcm) for (const x of p) { b.setInt16(o, x, true); o += 2; } | |
| const a = $("wav"); URL.revokeObjectURL(a.href); a.href = URL.createObjectURL(new Blob([b], { type: "audio/wav" })); | |
| a.download = `mesm-tts-${voiceId}.wav`; a.hidden = false; | |
| } | |
| } | |
| for (const [v, s] of Object.entries(voices)) { | |
| const b = document.createElement("button"); b.dataset.id = v; | |
| b.innerHTML = `${s.name}<small>${s.accent.toUpperCase()} · ${s.tag}</small>`; | |
| b.onclick = () => load(v); $("voices").append(b); | |
| } | |
| load("heart"); | |
| $("speak").onclick = async () => { | |
| player ??= await createPlayer(); await player.resume(); await ready; // the AudioContext needs a user gesture | |
| id++; pcm = []; player.reset(); $("wav").hidden = true; | |
| worker.postMessage({ op: "speak", id, text: $("text").value, speed: +$("speed").value }); | |
| }; | |
| $("stop").onclick = () => { id++; worker?.postMessage({ op: "stop" }); player?.reset(); }; | |
| </script> | |