"""Kokoro-backed TTS rendering.""" from __future__ import annotations import wave from pathlib import Path import numpy as np from .schema import Script SAMPLE_RATE = 24_000 KOKORO_REPO_ID = "hexgrad/Kokoro-82M" def render_script_wav(script: Script, output_path: Path) -> Path: """Render dialogue lines with Kokoro into one mono WAV. This is intentionally simple: no delivery FX, radio chain, ducking, or SFX yet. It proves that the locked voice roster can produce multi-voice audio. """ from kokoro import KPipeline script.validate() output_path.parent.mkdir(parents=True, exist_ok=True) cast_voice = {member.id: member.voice.value for member in script.cast} pipelines: dict[str, KPipeline] = {} with wave.open(str(output_path), "wb") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(SAMPLE_RATE) for scene in script.scenes: _write_silence(wav_file, 0.25) for line in scene.lines: voice = cast_voice[line.cast] lang = voice[0] pipeline = pipelines.get(lang) if pipeline is None: pipeline = KPipeline(lang_code=lang, repo_id=KOKORO_REPO_ID) pipelines[lang] = pipeline for result in pipeline(line.text, voice=voice, speed=1.0, split_pattern=r"\n+"): if result.audio is None: continue audio = np.clip(result.audio.numpy(), -1.0, 1.0) wav_file.writeframes((audio * 32767).astype(np.int16).tobytes()) _write_silence(wav_file, 0.22) return output_path def _write_silence(wav_file: wave.Wave_write, seconds: float) -> None: frames = int(SAMPLE_RATE * seconds) wav_file.writeframes(b"\x00\x00" * frames)