from __future__ import annotations
import json
from pathlib import Path
_WORKLET_SOURCE = (Path(__file__).parent / "web" / "PCMPlayerWorklet.js").read_text()
TTS_PLAYER_TEMPLATE = """
"""
TTS_PLAYER_CSS = """
.audex-player {
display: grid;
gap: 0.75rem;
padding: 1rem;
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-lg);
}
.audex-player-status {
color: var(--body-text-color-subdued);
}
.audex-player-audio {
width: 100%;
}
.audex-player-download {
color: var(--link-text-color);
font-weight: 600;
}
"""
TTS_PLAYER_JS = """
const WORKLET_SOURCE = __WORKLET_SOURCE__;
const status = element.querySelector(".audex-player-status");
const enable = element.querySelector(".audex-player-enable");
const audio = element.querySelector(".audex-player-audio");
const download = element.querySelector(".audex-player-download");
let context = null;
let player = null;
let chunks = [];
let wavUrl = null;
let lastSequence = -1;
const ensurePlayer = async () => {
if (context) {
await context.resume();
return;
}
context = new AudioContext();
const blob = new Blob([WORKLET_SOURCE], { type: "text/javascript" });
const moduleUrl = URL.createObjectURL(blob);
await context.audioWorklet.addModule(moduleUrl);
URL.revokeObjectURL(moduleUrl);
player = new AudioWorkletNode(
context,
"audex-pcm-player",
{ outputChannelCount: [1] },
);
player.connect(context.destination);
player.port.onmessage = (event) => {
if (event.data.type === "underrun") {
status.textContent = "Buffering generated speech…";
}
};
await context.resume();
};
const decodePcm = (encoded) => {
const binary = atob(encoded);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return new Float32Array(bytes.buffer);
};
const resample = (input, sourceRate, targetRate) => {
if (sourceRate === targetRate) return input;
const output = new Float32Array(Math.max(1, Math.round(input.length * targetRate / sourceRate)));
const ratio = sourceRate / targetRate;
for (let index = 0; index < output.length; index += 1) {
const position = index * ratio;
const left = Math.floor(position);
const right = Math.min(left + 1, input.length - 1);
const fraction = position - left;
output[index] = input[left] * (1 - fraction) + input[right] * fraction;
}
return output;
};
const buildWav = (parts, sampleRate) => {
const length = parts.reduce((total, part) => total + part.length, 0);
const buffer = new ArrayBuffer(44 + length * 2);
const view = new DataView(buffer);
const write = (offset, text) => {
for (let index = 0; index < text.length; index += 1) {
view.setUint8(offset + index, text.charCodeAt(index));
}
};
write(0, "RIFF");
view.setUint32(4, 36 + length * 2, true);
write(8, "WAVE");
write(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
write(36, "data");
view.setUint32(40, length * 2, true);
let offset = 44;
for (const part of parts) {
for (const sample of part) {
const clipped = Math.max(-1, Math.min(1, sample));
view.setInt16(offset, clipped < 0 ? clipped * 32768 : clipped * 32767, true);
offset += 2;
}
}
return new Blob([buffer], { type: "audio/wav" });
};
const reset = async () => {
chunks = [];
lastSequence = -1;
audio.removeAttribute("src");
audio.load();
download.hidden = true;
if (wavUrl) URL.revokeObjectURL(wavUrl);
wavUrl = null;
await ensurePlayer();
player.port.postMessage({ type: "reset" });
status.textContent = "Waiting for speech tokens…";
};
const consume = async () => {
const value = props.value || {};
if (value.sequence === lastSequence) return;
lastSequence = value.sequence;
if (value.reset) await reset();
if (value.pcm) {
await ensurePlayer();
const pcm = decodePcm(value.pcm);
chunks.push(pcm.slice());
const playback = resample(pcm, value.sample_rate || 16000, context.sampleRate);
player.port.postMessage({ type: "audio", samples: playback.buffer }, [playback.buffer]);
status.textContent = `Streaming ${value.token_count || 0} speech tokens…`;
}
if (value.done && chunks.length) {
const wav = buildWav(chunks, value.sample_rate || 16000);
wavUrl = URL.createObjectURL(wav);
audio.src = wavUrl;
download.href = wavUrl;
download.hidden = false;
status.textContent = `Complete · ${value.token_count || 0} speech tokens`;
}
};
const runButton = document.getElementById("audex-run");
if (runButton) runButton.addEventListener("click", () => ensurePlayer());
enable.addEventListener("click", async () => {
await ensurePlayer();
status.textContent = "Playback enabled";
});
const stopButton = document.getElementById("audex-stop");
if (stopButton) {
stopButton.addEventListener("click", () => {
if (player) player.port.postMessage({ type: "reset" });
status.textContent = "Stopped";
});
}
watch("value", consume);
consume();
""".replace("__WORKLET_SOURCE__", json.dumps(_WORKLET_SOURCE))
def player_value(
sequence: int,
*,
pcm: str | None = None,
token_count: int = 0,
done: bool = False,
reset: bool = False,
) -> dict[str, object]:
return {
"sequence": sequence,
"sample_rate": 16000,
"pcm": pcm,
"token_count": token_count,
"done": done,
"reset": reset,
}