aboutsummaryrefslogtreecommitdiff
path: root/app/converter/converter.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/converter/converter.py')
-rw-r--r--app/converter/converter.py35
1 files changed, 28 insertions, 7 deletions
diff --git a/app/converter/converter.py b/app/converter/converter.py
index 48d4987..b356448 100644
--- a/app/converter/converter.py
+++ b/app/converter/converter.py
@@ -24,6 +24,7 @@ from .clients import (
MODEL_SIZE,
VOICE_MODE_CLONE,
VOICE_MODE_CUSTOM,
+ VOICE_MODE_DESIGN,
VOICE_MODES,
AudioCppTTSClient,
FasterTTSClient,
@@ -100,19 +101,23 @@ def setup_directories() -> None:
def voice_mode_for(backend: str, voice: Optional[str] = None,
- clone: Optional[str] = None) -> str:
+ clone: Optional[str] = None,
+ instructions: Optional[str] = None) -> str:
"""The voice mode a run with these options would use.
Mirrors the choice ``audiobook.convert`` makes from the same inputs
(faster always clones; audiocpp clones through a server-side voice;
- qwen clones only with a reference .wav), so the hub can run the
- pre-flight overwrite checks against exactly the output names the
- conversion will produce.
+ qwen designs with instructions, clones only with a reference .wav, and
+ uses a built-in speaker otherwise), so the hub can run the pre-flight
+ overwrite checks against exactly the output names the conversion will
+ produce.
"""
if backend == BACKEND_FASTER:
return VOICE_MODE_CLONE
if backend == BACKEND_AUDIOCPP:
return VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM
+ if (instructions or "").strip():
+ return VOICE_MODE_DESIGN
return VOICE_MODE_CLONE if clone else VOICE_MODE_CUSTOM
@@ -245,6 +250,9 @@ class AudiobookConverter:
request_options=self.request_options,
api_url=api_url, quiet=quiet)
else:
+ # Qwen: the voice mode picks the request shape (built-in
+ # speaker, clone from a reference .wav, or a designed voice);
+ # instructions describe the voice in design mode.
self.tts = QwenTTSClient(
chunks_dir=CHUNKS_FOLDER,
voice_mode=voice_mode,
@@ -252,6 +260,7 @@ class AudiobookConverter:
voice_clone_ref_text=voice_clone_ref_text,
skip_transcription=skip_transcription,
language=self.language,
+ instructions=self.instructions,
api_url=api_url,
quiet=quiet,
)
@@ -281,6 +290,12 @@ class AudiobookConverter:
f"Unknown voice mode: {self.voice_mode!r} "
f"(expected one of {VOICE_MODES})"
)
+ if self.backend == BACKEND_QWEN and self.voice_mode == VOICE_MODE_DESIGN \
+ and not (self.instructions or "").strip():
+ raise ValueError(
+ "Voice Design mode requires a voice description. "
+ "Use --instructions \"...\" to describe the voice to synthesize with."
+ )
if self.voice_mode == VOICE_MODE_CLONE and self.backend == BACKEND_QWEN:
if not self.voice_clone_ref_audio:
raise ValueError(
@@ -336,6 +351,10 @@ class AudiobookConverter:
narrator = "designed"
else:
narrator = speaker_display_name()
+ elif voice_mode == VOICE_MODE_DESIGN:
+ # Qwen's VoiceDesign model: the voice is described by an
+ # instruction and has no speaker name.
+ narrator = "designed"
elif voice_mode == VOICE_MODE_CLONE:
narrator = Path(voice_clone_ref_audio).stem
else:
@@ -722,9 +741,7 @@ class AudiobookConverter:
else:
tts_client = getattr(self, "tts", None)
api_url = (getattr(tts_client, "api_url", None)
- or (config.CLONE_API_URL
- if self.voice_mode == VOICE_MODE_CLONE
- else config.QWEN_API_URL))
+ or config.QWEN_API_URL)
self._say(f"Qwen API endpoint: {api_url}")
self._say(f"Voice mode: {self.voice_mode}")
self._say(f"Model size: {MODEL_SIZE} (always)")
@@ -734,6 +751,10 @@ class AudiobookConverter:
elif self.voice_mode == VOICE_MODE_CLONE:
self._say(f"Reference audio: {Path(self.voice_clone_ref_audio).name}")
self._say(f"Language: {self.language}")
+ elif self.voice_mode == VOICE_MODE_DESIGN:
+ self._say("Backend: qwen-tts (voice from --instructions description)")
+ self._say(f"Instruction: {self.instructions}")
+ self._say(f"Language: {self.language}")
self._say(f"Output format: {self.output_format}")
if self.single_file and self.output_format != "m4b":
self._say("Chapter mode: single file (--single-file)")