complete restructure

This commit is contained in:
2026-08-19 13:16:43 -04:00
parent d7b6f28cbd
commit f87e1d51ef
60 changed files with 8176 additions and 5143 deletions
+48 -13
View File
@@ -3,9 +3,12 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from io import BytesIO
import numpy as np
# kokoro-tts and soundfile ship no type stubs upstream.
import soundfile as sf # type: ignore[import-untyped]
from kokoro_tts import ( # type: ignore[import-untyped]
Kokoro,
@@ -13,14 +16,31 @@ from kokoro_tts import ( # type: ignore[import-untyped]
process_chunk_sequential,
)
from vibe_bot.config import TTS_SPEED, TTS_VOICE
logger = logging.getLogger(__name__)
# Default voice settings
DEFAULT_VOICE = "af_sarah"
DEFAULT_SPEED = 1.0
# Default voice settings (single source of truth: vibe_bot.config).
DEFAULT_VOICE = TTS_VOICE
DEFAULT_SPEED = TTS_SPEED
DEFAULT_LANG = "en-us"
@dataclass
class AudioResult:
"""Audio output from a TTS generation.
Attributes:
audio: The encoded audio (MP3) as a seekable BytesIO.
partial: True if one or more text chunks failed to produce audio.
failed_chunks: Number of text chunks that failed.
"""
audio: BytesIO
partial: bool
failed_chunks: int
class TTSEngine:
"""Text-to-speech engine wrapper around Kokoro TTS."""
@@ -43,13 +63,14 @@ class TTSEngine:
voice: str = DEFAULT_VOICE,
speed: float = DEFAULT_SPEED,
lang: str = DEFAULT_LANG,
) -> BytesIO:
"""Convert text to audio and return as BytesIO (MP3 format)."""
) -> AudioResult:
"""Convert text to audio and return an AudioResult (MP3 in .audio)."""
all_samples: list[np.ndarray] = []
sample_rate: int | None = None
failed_chunks = 0
chunks: list[str] = list(chunk_text(text))
logger.info("Split text into %d chunks", len(chunks))
logger.debug("Split text into %d chunks", len(chunks))
for i, chunk in enumerate(chunks):
try:
@@ -60,15 +81,21 @@ class TTSEngine:
speed,
lang,
)
if samples is not None:
if sample_rate is None:
sample_rate = sr
all_samples.append(np.asarray(samples))
logger.info("Processed chunk %d/%d", i + 1, len(chunks))
except Exception:
logger.exception("Error processing chunk %d", i + 1)
failed_chunks += 1
continue
if samples is None:
logger.warning("Chunk %d/%d produced no audio", i + 1, len(chunks))
failed_chunks += 1
continue
if sample_rate is None:
sample_rate = sr
all_samples.append(np.asarray(samples))
logger.debug("Processed chunk %d/%d", i + 1, len(chunks))
if not all_samples:
msg = "No audio samples generated - text may be invalid or too long"
raise ValueError(msg)
@@ -85,9 +112,17 @@ class TTSEngine:
)
buffer.seek(0)
logger.info(
partial = failed_chunks > 0
if partial:
logger.warning(
"TTS produced partial audio: %d of %d chunks failed",
failed_chunks,
len(chunks),
)
logger.debug(
"Generated MP3 audio: %d samples at %dHz",
len(combined),
sample_rate or 0,
)
return buffer
return AudioResult(audio=buffer, partial=partial, failed_chunks=failed_chunks)