aboutsummaryrefslogtreecommitdiff
path: root/converter
diff options
context:
space:
mode:
Diffstat (limited to 'converter')
-rw-r--r--converter/audio.py2
-rw-r--r--converter/config.py46
-rw-r--r--converter/converter.py16
-rw-r--r--converter/tts.py44
4 files changed, 94 insertions, 14 deletions
diff --git a/converter/audio.py b/converter/audio.py
index ebcac21..4fd1721 100644
--- a/converter/audio.py
+++ b/converter/audio.py
@@ -282,7 +282,7 @@ def _collect_chunk_files(total_chunks: int,
def combine_chunks(total_chunks: int, output_path: Path,
chunk_results: Optional[Dict[int, Optional[Path]]] = None,
- speed: float = 1.0, output_format: str = "mp3",
+ speed: float = 1.0, output_format: str = config.AUDIO_FORMAT,
intermediate: bool = False,
meta: Optional[TrackMeta] = None,
cover: Optional[Path] = None) -> bool:
diff --git a/converter/config.py b/converter/config.py
index ec84986..f795520 100644
--- a/converter/config.py
+++ b/converter/config.py
@@ -7,7 +7,7 @@ be run from any working directory.
from pathlib import Path
-# Project root (directory containing audiobook_converter.py)
+# Project root (directory containing audiobook.py)
BASE_DIR = Path(__file__).resolve().parent.parent
# =============================================================================
@@ -19,6 +19,48 @@ VOICE_MODE_CLONE = "voice_clone"
VOICE_MODES = (VOICE_MODE_CUSTOM, VOICE_MODE_CLONE)
# =============================================================================
+# TTS LANGUAGE SETTINGS
+# =============================================================================
+# Languages understood by the Qwen3-TTS API. The Gradio demo silently falls
+# back to "Auto" for unrecognized values, so languages are validated
+# client-side (see converter.tts.normalize_language) before reaching the API.
+# Display names must match the demo dropdown exactly.
+
+TTS_LANGUAGES = (
+ "Auto",
+ "Chinese",
+ "English",
+ "German",
+ "Italian",
+ "Portuguese",
+ "Spanish",
+ "Japanese",
+ "Korean",
+ "French",
+ "Russian",
+)
+
+# Short aliases accepted on the command line (ISO 639-1 codes and common
+# shorthands), mapped to the display names above.
+TTS_LANGUAGE_ALIASES = {
+ "zh": "Chinese",
+ "en": "English",
+ "de": "German",
+ "it": "Italian",
+ "pt": "Portuguese",
+ "es": "Spanish",
+ "ja": "Japanese",
+ "ko": "Korean",
+ "fr": "French",
+ "ru": "Russian",
+ "zh-cn": "Chinese",
+ "zh-tw": "Chinese",
+ "pt-br": "Portuguese",
+ "en-us": "English",
+ "en-gb": "English",
+}
+
+# =============================================================================
# QWEN API CONFIGURATION
# =============================================================================
@@ -82,7 +124,7 @@ HEARTBEAT_INTERVAL_SECONDS = 30 # Print "still working" this often during a chu
# AUDIO OUTPUT SETTINGS
# =============================================================================
-AUDIO_FORMAT = "mp3" # Default output container
+AUDIO_FORMAT = "m4b" # Default output container
AUDIO_FORMATS = ("mp3", "m4b", "ogg", "flac")
AUDIO_BITRATE = "128k"
diff --git a/converter/converter.py b/converter/converter.py
index 6d09e09..29d3bc4 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -13,7 +13,7 @@ from typing import Dict, List, Optional, Tuple
from . import audio, chunking, config, cover, extractors
from .audio import TrackMeta
-from .tts import QwenTTSClient
+from .tts import QwenTTSClient, normalize_language
logger = logging.getLogger(__name__)
@@ -88,11 +88,16 @@ class AudiobookConverter:
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"):
+ speed: float = 1.0, single_file: bool = False, output_format: str = config.AUDIO_FORMAT,
+ language: Optional[str] = None):
if speed <= 0:
raise ValueError(f"Speed must be a positive number, got {speed}")
if output_format not in config.AUDIO_FORMATS:
raise ValueError(f"Unsupported output format: {output_format}")
+ if language is None:
+ language = (config.VOICE_CLONE_LANGUAGE if voice_mode == config.VOICE_MODE_CLONE
+ else config.CUSTOM_VOICE_LANGUAGE)
+ self.language = normalize_language(language)
self.voice_mode = voice_mode
self.voice_clone_ref_audio = voice_clone_ref_audio
self.speed = speed
@@ -104,6 +109,7 @@ class AudiobookConverter:
voice_clone_ref_audio=voice_clone_ref_audio,
voice_clone_ref_text=voice_clone_ref_text,
skip_transcription=skip_transcription,
+ language=self.language,
)
def _validate_configuration(self) -> None:
@@ -117,7 +123,7 @@ class AudiobookConverter:
if not self.voice_clone_ref_audio:
raise ValueError(
"Voice Clone mode requires a reference audio file. "
- "Use --voice-sample <path> to specify it."
+ "Use --clone <path> to specify it."
)
if not Path(self.voice_clone_ref_audio).exists():
@@ -366,10 +372,10 @@ class AudiobookConverter:
print("Model size: 1.7B (always)")
if self.voice_mode == config.VOICE_MODE_CUSTOM:
print(f"Speaker: {config.CUSTOM_VOICE_SPEAKER}")
- print(f"Language: {config.CUSTOM_VOICE_LANGUAGE}")
+ print(f"Language: {self.language}")
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"Language: {self.language}")
print(f"Output format: {self.output_format}")
if self.single_file and self.output_format != "m4b":
print("Chapter mode: single file (--single-file)")
diff --git a/converter/tts.py b/converter/tts.py
index 83c7330..db5de9b 100644
--- a/converter/tts.py
+++ b/converter/tts.py
@@ -15,11 +15,38 @@ from . import config
logger = logging.getLogger(__name__)
+def normalize_language(value: Optional[str]) -> str:
+ """Normalize a user-provided language name to a Qwen3-TTS display name.
+
+ Accepts the display names in config.TTS_LANGUAGES case-insensitively as
+ well as the short aliases in config.TTS_LANGUAGE_ALIASES (ISO 639-1 codes
+ and common shorthands). Raises ValueError for anything else, since the
+ Qwen3-TTS demo silently falls back to "Auto" for unrecognized languages.
+ """
+ if value is None:
+ raise ValueError("Language must not be None")
+ candidate = value.strip()
+ if not candidate:
+ raise ValueError("Language must not be empty")
+ for name in config.TTS_LANGUAGES:
+ if candidate.lower() == name.lower():
+ return name
+ alias = config.TTS_LANGUAGE_ALIASES.get(candidate.lower())
+ if alias:
+ return alias
+ raise ValueError(
+ f"Unknown language: {value!r}. Expected one of "
+ f"{', '.join(config.TTS_LANGUAGES)} (or an alias: "
+ f"{', '.join(sorted(config.TTS_LANGUAGE_ALIASES))})."
+ )
+
+
class QwenTTSClient:
"""Generates audio chunks through a Qwen3-TTS Gradio server."""
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):
+ voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False,
+ language: Optional[str] = None):
if voice_mode not in config.VOICE_MODES:
raise ValueError(
f"Unknown voice mode: {voice_mode!r} (expected one of {config.VOICE_MODES})"
@@ -28,6 +55,11 @@ class QwenTTSClient:
self.voice_clone_ref_audio = voice_clone_ref_audio
self.voice_clone_ref_text = (voice_clone_ref_text or "").strip()
self.skip_transcription = skip_transcription
+ if language is None:
+ language = (config.VOICE_CLONE_LANGUAGE if voice_mode == config.VOICE_MODE_CLONE
+ else config.CUSTOM_VOICE_LANGUAGE)
+ # Validate before connecting so bad values fail fast without a server.
+ self.language = normalize_language(language)
self.client = None
self.api_info: Dict[str, Any] = {}
self.clone_client = None
@@ -70,7 +102,7 @@ class QwenTTSClient:
self.voice_clone_ref_text = self.transcribe_audio(self.voice_clone_ref_audio) or ""
if not self.voice_clone_ref_text:
print("[WARNING] No reference text available; using x-vector-only clone mode (lower quality).")
- print(' Pass --voice-sample-text "..." for higher-quality in-context cloning.')
+ print(' Pass --transcription "..." for higher-quality in-context cloning.')
else:
print(f"[OK] Reference text: {self.voice_clone_ref_text[:100]}...")
@@ -258,7 +290,7 @@ class QwenTTSClient:
if custom_api == "/run_instruct":
payload = dict(
text=text,
- lang_disp=config.CUSTOM_VOICE_LANGUAGE,
+ lang_disp=self.language,
spk_disp=config.SPEAKER_DISPLAY_NAMES.get(
config.CUSTOM_VOICE_SPEAKER.lower(), config.CUSTOM_VOICE_SPEAKER),
instruct=config.CUSTOM_VOICE_INSTRUCT,
@@ -266,7 +298,7 @@ class QwenTTSClient:
else:
payload = dict(
text=text,
- language=config.CUSTOM_VOICE_LANGUAGE,
+ language=self.language,
speaker=config.CUSTOM_VOICE_SPEAKER,
instruct=config.CUSTOM_VOICE_INSTRUCT,
)
@@ -305,14 +337,14 @@ class QwenTTSClient:
ref_txt=self.voice_clone_ref_text,
use_xvec=use_xvector,
text=text,
- lang_disp=config.VOICE_CLONE_LANGUAGE,
+ lang_disp=self.language,
)
else:
payload = dict(
ref_audio=self._ref_audio_payload(),
ref_text=self.voice_clone_ref_text,
target_text=text,
- language=config.VOICE_CLONE_LANGUAGE,
+ language=self.language,
use_xvector_only=use_xvector,
)
optional_params = {