cajpany's picture
Upload folder using huggingface_hub
fc9b7e2 verified
Raw
History Blame Contribute Delete
1.87 kB
"""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)