diff options
| author | historia <historiavg@proton.me> | 2026-08-26 21:22:40 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-26 21:22:40 -0400 |
| commit | 477ac3e827e3bdc9f14583fc3aa8db1fa2d27c52 (patch) | |
| tree | 1e21fa5af1d7a95ffc62fb03eed153056c38ae9e /app/converter/clients | |
| parent | 65c6f737f1545ef225768af897acd20f163a4fb4 (diff) | |
| download | tts-audiobook-generator-477ac3e827e3bdc9f14583fc3aa8db1fa2d27c52.tar.gz | |
feat: design model support for qwen-tts backend. remove unnecessary port split for qwen models
Diffstat (limited to 'app/converter/clients')
| -rw-r--r-- | app/converter/clients/__init__.py | 4 | ||||
| -rw-r--r-- | app/converter/clients/base.py | 10 | ||||
| -rw-r--r-- | app/converter/clients/qwen.py | 54 |
3 files changed, 50 insertions, 18 deletions
diff --git a/app/converter/clients/__init__.py b/app/converter/clients/__init__.py index e216011..6ae8850 100644 --- a/app/converter/clients/__init__.py +++ b/app/converter/clients/__init__.py @@ -15,7 +15,7 @@ BACKEND_AUDIOCPP = "audiocpp" BACKENDS = (BACKEND_AUDIOCPP, BACKEND_QWEN, BACKEND_FASTER) from .base import BaseTTSClient, ConversionCancelled, VOICE_MODE_CLONE, \ - VOICE_MODE_CUSTOM, VOICE_MODES, resolve_request_seed + VOICE_MODE_CUSTOM, VOICE_MODE_DESIGN, VOICE_MODES, resolve_request_seed from .languages import LANGUAGE_ISO_CODES, TTS_LANGUAGES, \ TTS_LANGUAGE_ALIASES, normalize_language from .speakers import QWEN3_TTS_SPEAKERS, SPEAKER_DISPLAY_NAMES, \ @@ -44,7 +44,7 @@ from .audiocpp import ( __all__ = [ # vocabulary "BACKEND_QWEN", "BACKEND_FASTER", "BACKEND_AUDIOCPP", "BACKENDS", - "VOICE_MODE_CUSTOM", "VOICE_MODE_CLONE", "VOICE_MODES", + "VOICE_MODE_CUSTOM", "VOICE_MODE_CLONE", "VOICE_MODE_DESIGN", "VOICE_MODES", # clients "BaseTTSClient", "ConversionCancelled", "resolve_request_seed", "QwenTTSClient", "FasterTTSClient", "AudioCppTTSClient", diff --git a/app/converter/clients/base.py b/app/converter/clients/base.py index a0f28cf..d0d57fb 100644 --- a/app/converter/clients/base.py +++ b/app/converter/clients/base.py @@ -17,12 +17,14 @@ class ConversionCancelled(Exception): """Raised when the run's cancel event is set (between requests).""" -# How a run supplies its voice: a built-in CustomVoice speaker, or by -# cloning a reference audio clip (the faster and audiocpp backends always -# clone server-side; only the Qwen client branches on this at request time). +# How a run supplies its voice: a built-in CustomVoice speaker, by cloning +# a reference audio clip (the faster and audiocpp backends always clone +# server-side; only the Qwen client branches on this at request time), or +# designed from an instruction (Qwen's VoiceDesign model). VOICE_MODE_CUSTOM = "custom_voice" VOICE_MODE_CLONE = "voice_clone" -VOICE_MODES = (VOICE_MODE_CUSTOM, VOICE_MODE_CLONE) +VOICE_MODE_DESIGN = "voice_design" +VOICE_MODES = (VOICE_MODE_CUSTOM, VOICE_MODE_CLONE, VOICE_MODE_DESIGN) def resolve_request_seed() -> int: diff --git a/app/converter/clients/qwen.py b/app/converter/clients/qwen.py index 354ee04..ed3149b 100644 --- a/app/converter/clients/qwen.py +++ b/app/converter/clients/qwen.py @@ -1,4 +1,4 @@ -"""Client for the qwen-tts Gradio demo servers (CustomVoice + Base).""" +"""Client for the qwen-tts Gradio demo servers (CustomVoice / Base / VoiceDesign).""" import io import logging @@ -12,14 +12,15 @@ from .. import config from ..audio import concat_audio_files from ..chunking import split_into_chunks from .base import (BaseTTSClient, ConversionCancelled, resolve_request_seed, - VOICE_MODE_CLONE, VOICE_MODE_CUSTOM, VOICE_MODES) + VOICE_MODE_CLONE, VOICE_MODE_CUSTOM, VOICE_MODE_DESIGN, + VOICE_MODES) from .languages import normalize_language from .speakers import speaker_display_name logger = logging.getLogger(__name__) -# Fixed model facts: both demos run the 1.7B model (the CustomVoice demo -# takes its full HuggingFace id), and the 12Hz codec outputs 24 kHz audio. +# Fixed model facts: the demos run the 1.7B model (each takes its full +# HuggingFace id), and the 12Hz codec outputs 24 kHz audio. MODEL_SIZE = "1.7B" CUSTOM_VOICE_MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice" @@ -31,7 +32,7 @@ class QwenTTSClient(BaseTTSClient): voice_mode: str = "custom_voice", voice_clone_ref_audio: Optional[str] = None, voice_clone_ref_text: Optional[str] = None, skip_transcription: bool = False, language: Optional[str] = None, api_url: Optional[str] = None, - quiet: bool = False): + instructions: Optional[str] = None, quiet: bool = False): super().__init__(chunks_dir, quiet=quiet) if voice_mode not in VOICE_MODES: raise ValueError( @@ -41,6 +42,11 @@ class QwenTTSClient(BaseTTSClient): 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 + # Voice design / style instruction (VoiceDesign mode): describes the + # voice to design. Defaults to the configured CustomVoice INSTRUCT so + # a run never sends an empty design prompt. + self.instructions = (instructions if instructions is not None + else config.INSTRUCT).strip() # api_url overrides the configured endpoint for the active voice mode # (used by the hub's "[remote]" backend entries and --api-url). self.api_url = (api_url or "").strip() or None @@ -66,25 +72,28 @@ class QwenTTSClient(BaseTTSClient): # ------------------------------------------------------------------ def _connect(self) -> None: - api_url = self.api_url or ( - config.CLONE_API_URL if self.voice_mode == VOICE_MODE_CLONE - else config.QWEN_API_URL) + # One demo server runs at a time on the configured port, hosting + # whichever model this run selected (CustomVoice / Base / VoiceDesign). + api_url = self.api_url or config.QWEN_API_URL try: if self.voice_mode == 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). + # Voice clone talks to the Base-model demo. self._init_client(api_url, clone=True) self._report(f"[OK] Connected to Voice Clone API at {api_url}") self._resolve_reference_text() else: self._init_client(api_url, clone=False) - self._report("[OK] Connected to Qwen API") + if self.voice_mode == VOICE_MODE_DESIGN: + self._report(f"[OK] Connected to Voice Design API at {api_url}") + else: + self._report("[OK] Connected to Qwen API") except Exception as exc: raise RuntimeError( f"Qwen API initialization failed at {api_url}: {exc}. " "Make sure the Qwen demo server is running and reachable, and that your " "installed Qwen3-TTS version matches this converter's API expectations " - "(voice clone requires the Base-model demo: Qwen/Qwen3-TTS-12Hz-1.7B-Base)." + "(voice clone requires the Base-model demo: Qwen/Qwen3-TTS-12Hz-1.7B-Base; " + "voice design requires: Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign)." ) from exc def _resolve_reference_text(self) -> None: @@ -226,6 +235,8 @@ class QwenTTSClient(BaseTTSClient): result = self._generate_custom_voice(text) elif self.voice_mode == VOICE_MODE_CLONE: result = self._generate_voice_clone(text) + elif self.voice_mode == VOICE_MODE_DESIGN: + result = self._generate_voice_design(text) else: raise ValueError(f"Unknown voice mode: {self.voice_mode}") @@ -276,6 +287,25 @@ class QwenTTSClient(BaseTTSClient): return self.client.predict(**payload, api_name=custom_api) + def _generate_voice_design(self, text: str) -> Tuple: + """Generate audio using VoiceDesign mode (described-voice model).""" + design_api = self._resolve_api_name("/run_voice_design") + # The demo's field is named "design"; older builds may call it + # "instruct" instead. + design_field = ("instruct" + if self._endpoint_accepts_param(design_api, "instruct") + and not self._endpoint_accepts_param(design_api, "design") + else "design") + payload = dict( + text=text, + lang_disp=self.language, + **{design_field: self.instructions}, + ) + if self._endpoint_accepts_param(design_api, "seed"): + payload["seed"] = self._seed + + return self.client.predict(**payload, api_name=design_api) + def _ref_audio_payload(self) -> Dict[str, Any]: """Gradio file payload for the reference audio (built once, reused).""" if self._ref_audio_filedata is None: |
