mesm-tts / web /demo.html
mesmertech's picture
mesm-tts: ten tiny English voices (ONNX + pdn WebAssembly engine), inference, samples, benchmarks
931612f
Raw History Blame Contribute Delete
5.63 kB
<!doctype html>
<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>