aboutsummaryrefslogtreecommitdiff
path: root/converter/converter.py
diff options
context:
space:
mode:
Diffstat (limited to 'converter/converter.py')
-rw-r--r--converter/converter.py106
1 files changed, 63 insertions, 43 deletions
diff --git a/converter/converter.py b/converter/converter.py
index 0454892..d5a1096 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -5,9 +5,10 @@ import re
import sys
import time
import traceback
+from collections import Counter
from datetime import datetime
from pathlib import Path
-from typing import Dict, Optional
+from typing import Dict, List, Optional
from . import audio, chunking, config, extractors
from .tts import QwenTTSClient
@@ -41,7 +42,7 @@ def setup_directories() -> None:
class AudiobookConverter:
"""Audiobook converter using the Qwen TTS API."""
- def __init__(self, voice_mode: str = "custom_voice", voice_clone_ref_audio: Optional[str] = None,
+ def __init__(self, voice_mode: str = config.VOICE_MODE_CUSTOM, voice_clone_ref_audio: Optional[str] = None,
voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False,
speed: float = 1.0, single_file: bool = False, output_format: str = "mp3"):
if speed <= 0:
@@ -63,7 +64,12 @@ class AudiobookConverter:
def _validate_configuration(self) -> None:
"""Validate configuration settings."""
- if self.voice_mode == "voice_clone":
+ if self.voice_mode not in config.VOICE_MODES:
+ raise ValueError(
+ f"Unknown voice mode: {self.voice_mode!r} "
+ f"(expected one of {config.VOICE_MODES})"
+ )
+ if self.voice_mode == config.VOICE_MODE_CLONE:
if not self.voice_clone_ref_audio:
raise ValueError(
"Voice Clone mode requires a reference audio file. "
@@ -117,7 +123,7 @@ class AudiobookConverter:
for index, section in enumerate(sections, 1):
chapter_name = f"{stem}_{index:02d}_{self._sanitize_filename(section.title)}"
output_path = config.AUDIOBOOKS_FOLDER / f"{chapter_name}.{self.output_format}"
- success = self._convert_text(section.text, output_path, start_time) and success
+ success = self._convert_text(section.text, output_path, time.time()) and success
return success
except Exception as exc:
@@ -139,7 +145,7 @@ class AudiobookConverter:
titles = []
for index, section in enumerate(sections, 1):
chapter_path = config.CHUNKS_FOLDER / f"chapter_{index:04d}.wav"
- if not self._convert_text(section.text, chapter_path, start_time,
+ if not self._convert_text(section.text, chapter_path, time.time(),
speed=1.0, output_format="wav"):
logger.warning("Skipping chapter %d (%s) due to conversion failure",
index, section.title)
@@ -152,7 +158,50 @@ class AudiobookConverter:
return False
output_path = config.AUDIOBOOKS_FOLDER / f"{stem}.{self.output_format}"
- return audio.combine_chapters_to_m4b(chapter_files, titles, output_path, speed=self.speed)
+ if not audio.combine_chapters_to_m4b(chapter_files, titles, output_path, speed=self.speed):
+ return False
+ duration = time.time() - start_time
+ logger.info("Conversion completed in %dm %ds: %s",
+ int(duration // 60), int(duration % 60), output_path)
+ print(f"[SUCCESS] Conversion completed in {int(duration // 60)}m {int(duration % 60)}s")
+ return True
+
+ def _synthesize_chunks(self, chunks: List[str]) -> Dict[int, Optional[Path]]:
+ """Synthesize chunks sequentially, preserving order and naming.
+
+ Returns a mapping of chunk number to the generated audio path, with
+ None for chunks that failed after retries.
+ """
+ total_chunks = len(chunks)
+ print(f"\n{'=' * 50}")
+ print(f"PROCESSING {total_chunks} CHUNKS")
+ print(f"{'=' * 50}")
+
+ results: Dict[int, Optional[Path]] = {}
+ for chunk_num, chunk_text in enumerate(chunks, 1):
+ try:
+ result = self.tts.process_chunk_with_retry(chunk_num, chunk_text)
+ results[chunk_num] = result
+
+ if result:
+ print(f"[OK] Chunk {chunk_num:3d}/{total_chunks} completed")
+ logger.info("+ Chunk %d/%d completed", chunk_num, total_chunks)
+ else:
+ print(f"[FAIL] Chunk {chunk_num:3d}/{total_chunks} FAILED")
+ logger.error("- Chunk %d/%d failed", chunk_num, total_chunks)
+
+ except Exception as exc:
+ results[chunk_num] = None
+ print(f"[ERROR] Chunk {chunk_num:3d}/{total_chunks} ERROR: {exc}")
+ logger.error("- Chunk %d/%d error: %s", chunk_num, total_chunks, exc)
+
+ successful_chunks = sum(1 for path in results.values() if path)
+ print(f"\n{'=' * 50}")
+ print("CHUNK PROCESSING COMPLETE")
+ print(f"Successful: {successful_chunks}/{total_chunks}")
+ print(f"{'=' * 50}")
+ logger.info("Qwen processing completed: %d/%d chunks", successful_chunks, total_chunks)
+ return results
def _convert_text(self, text: str, output_path: Path, start_time: float,
speed: Optional[float] = None,
@@ -184,36 +233,8 @@ class AudiobookConverter:
print(f"[INFO] Processing {total_chunks} chunks via Qwen API...")
print(f"[INFO] Estimated time: ~{total_chunks * 4} minutes (4 min per chunk)")
- print(f"\n{'=' * 50}")
- print(f"PROCESSING {total_chunks} CHUNKS")
- print(f"{'=' * 50}")
-
- # Process chunks sequentially to ensure correct order and naming:
- # chunks are named 1, 2, 3, 4... in order.
- results = {} # chunk_num -> success (bool)
- for chunk_num, chunk_text in enumerate(chunks, 1):
- try:
- result = self.tts.process_chunk_with_retry(chunk_num, chunk_text)
- results[chunk_num] = result
-
- if result:
- print(f"[OK] Chunk {chunk_num:3d}/{total_chunks} completed")
- logger.info("+ Chunk %d/%d completed", chunk_num, total_chunks)
- else:
- print(f"[FAIL] Chunk {chunk_num:3d}/{total_chunks} FAILED")
- logger.error("- Chunk %d/%d failed", chunk_num, total_chunks)
-
- except Exception as exc:
- results[chunk_num] = False
- print(f"[ERROR] Chunk {chunk_num:3d}/{total_chunks} ERROR: {exc}")
- logger.error("- Chunk %d/%d error: %s", chunk_num, total_chunks, exc)
-
- successful_chunks = sum(1 for v in results.values() if v)
- print(f"\n{'=' * 50}")
- print("CHUNK PROCESSING COMPLETE")
- print(f"Successful: {successful_chunks}/{total_chunks}")
- print(f"{'=' * 50}")
- logger.info("Qwen processing completed: %d/%d chunks", successful_chunks, total_chunks)
+ results = self._synthesize_chunks(chunks)
+ successful_chunks = sum(1 for path in results.values() if path)
if successful_chunks == 0:
logger.error("No chunks were successfully processed")
@@ -224,7 +245,7 @@ class AudiobookConverter:
successful_chunks, total_chunks)
# Combine chunks (only the successful ones)
- success = audio.combine_chunks(total_chunks, output_path, results,
+ success = audio.combine_chunks(total_chunks, output_path, chunk_results=results,
speed=speed, output_format=output_format)
if success:
@@ -245,7 +266,7 @@ class AudiobookConverter:
def run(self) -> bool:
"""Main conversion process. Returns True if all books converted."""
- 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
print("=" * 70)
print("QWEN-BASED AUDIOBOOK CONVERTER")
@@ -255,10 +276,10 @@ class AudiobookConverter:
print(f"Qwen API endpoint: {api_url}")
print(f"Voice mode: {self.voice_mode}")
print("Model size: 1.7B (always)")
- if self.voice_mode == "custom_voice":
+ if self.voice_mode == config.VOICE_MODE_CUSTOM:
print(f"Speaker: {config.CUSTOM_VOICE_SPEAKER}")
print(f"Language: {config.CUSTOM_VOICE_LANGUAGE}")
- elif self.voice_mode == "voice_clone":
+ elif self.voice_mode == config.VOICE_MODE_CLONE:
print(f"Reference audio: {Path(self.voice_clone_ref_audio).name}")
print(f"Language: {config.VOICE_CLONE_LANGUAGE}")
print(f"Output format: {self.output_format}")
@@ -292,9 +313,7 @@ class AudiobookConverter:
print(f"[INFO] Found {len(book_files)} books to convert")
# Avoid output collisions when two books share a stem (e.g. dune.txt + dune.epub).
- stem_counts: Dict[str, int] = {}
- for book_file in book_files:
- stem_counts[book_file.stem] = stem_counts.get(book_file.stem, 0) + 1
+ stem_counts: Dict[str, int] = Counter(book_file.stem for book_file in book_files)
# Convert each book
results = {}
@@ -307,6 +326,7 @@ class AudiobookConverter:
results[book_file.name] = success
except KeyboardInterrupt:
print("\n[WARNING] Conversion interrupted by user")
+ results[book_file.name] = False
break
except Exception as exc:
logger.error("Unexpected error: %s", exc)