diff options
Diffstat (limited to 'converter/tts.py')
| -rw-r--r-- | converter/tts.py | 32 |
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 |
