aboutsummaryrefslogtreecommitdiff
path: root/converter/tts.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-18 02:17:03 -0400
committerhistoria <historiavg@proton.me>2026-08-18 02:17:03 -0400
commit8760626f744d79a05904e692119ffa72fc0c4e54 (patch)
tree1679faa3af8a1dd2ff1a94504168831ccf82f0ad /converter/tts.py
parentd72d274bd9895bdf97d31d56690b7df07fa74ad4 (diff)
downloadtts-audiobook-generator-8760626f744d79a05904e692119ffa72fc0c4e54.tar.gz
fix(converter): escape ffmetadata titles, thread exact chunk paths
Diffstat (limited to 'converter/tts.py')
-rw-r--r--converter/tts.py32
1 files changed, 24 insertions, 8 deletions
diff --git a/converter/tts.py b/converter/tts.py
index c3a4b81..83c7330 100644
--- a/converter/tts.py
+++ b/converter/tts.py
@@ -20,6 +20,10 @@ class QwenTTSClient:
def __init__(self, voice_mode: str = "custom_voice", voice_clone_ref_audio: Optional[str] = None,
voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False):
+ if voice_mode not in config.VOICE_MODES:
+ raise ValueError(
+ f"Unknown voice mode: {voice_mode!r} (expected one of {config.VOICE_MODES})"
+ )
self.voice_mode = voice_mode
self.voice_clone_ref_audio = voice_clone_ref_audio
self.voice_clone_ref_text = (voice_clone_ref_text or "").strip()
@@ -36,9 +40,9 @@ class QwenTTSClient:
# ------------------------------------------------------------------
def _connect(self) -> None:
- api_url = config.VOICE_CLONE_API_URL if self.voice_mode == "voice_clone" else config.QWEN_API_URL
+ api_url = config.VOICE_CLONE_API_URL if self.voice_mode == config.VOICE_MODE_CLONE else config.QWEN_API_URL
try:
- if self.voice_mode == "voice_clone":
+ if self.voice_mode == config.VOICE_MODE_CLONE:
# Voice clone uses the Base-model demo, which is a separate server
# from the CustomVoice demo (that one only exposes /run_instruct).
self._init_client(config.VOICE_CLONE_API_URL, clone=True)
@@ -159,10 +163,10 @@ class QwenTTSClient:
def generate_chunk(self, text: str, chunk_num: int) -> Optional[str]:
"""Generate one audio chunk; returns its path in the chunks folder."""
try:
- if self.voice_mode == "custom_voice":
+ if self.voice_mode == config.VOICE_MODE_CUSTOM:
with self._chunk_heartbeat(chunk_num):
result = self._generate_custom_voice(text)
- elif self.voice_mode == "voice_clone":
+ elif self.voice_mode == config.VOICE_MODE_CLONE:
with self._chunk_heartbeat(chunk_num):
result = self._generate_voice_clone(text)
else:
@@ -180,6 +184,13 @@ class QwenTTSClient:
raise RuntimeError(f"Generated audio file not found: {audio_path}")
suffix = source.suffix or ".wav"
+ # Remove any stale chunk file for this index first so a retry or
+ # extension change can never leave two files matching chunk_NNNN.*
+ for stale in config.CHUNKS_FOLDER.glob(f"chunk_{chunk_num:04d}.*"):
+ try:
+ stale.unlink()
+ except OSError as exc:
+ logger.debug("Could not remove stale chunk file %s: %s", stale, exc)
output_path = config.CHUNKS_FOLDER / f"chunk_{chunk_num:04d}{suffix}"
shutil.copy2(source, output_path)
@@ -190,8 +201,12 @@ class QwenTTSClient:
logger.error("Qwen chunk processing failed for chunk %d: %s", chunk_num, exc)
return None
- def process_chunk_with_retry(self, chunk_num: int, text: str) -> bool:
- """Process a chunk with retry logic and rate limiting."""
+ def process_chunk_with_retry(self, chunk_num: int, text: str) -> Optional[Path]:
+ """Process a chunk with retry logic and rate limiting.
+
+ Returns the generated chunk file's path, or None when all attempts
+ failed.
+ """
# Small delay between chunks to avoid rate limiting (only if not first chunk)
if chunk_num > 1:
time.sleep(config.MIN_DELAY_BETWEEN_CHUNKS)
@@ -200,7 +215,7 @@ class QwenTTSClient:
try:
result = self.generate_chunk(text, chunk_num)
if result and Path(result).exists():
- return True
+ return Path(result)
logger.warning("Chunk %d attempt %d failed", chunk_num, attempt + 1)
except Exception as exc:
logger.warning("Chunk %d attempt %d error: %s", chunk_num, attempt + 1, exc)
@@ -211,7 +226,7 @@ class QwenTTSClient:
time.sleep(sleep_time)
logger.error("Chunk %d failed after %d attempts", chunk_num, config.MAX_RETRIES)
- return False
+ return None
@contextlib.contextmanager
def _chunk_heartbeat(self, chunk_num: int):
@@ -231,6 +246,7 @@ class QwenTTSClient:
yield
finally:
stop.set()
+ thread.join()
# ------------------------------------------------------------------
# API payloads