From 477ac3e827e3bdc9f14583fc3aa8db1fa2d27c52 Mon Sep 17 00:00:00 2001 From: historia Date: Wed, 26 Aug 2026 21:22:40 -0400 Subject: feat: design model support for qwen-tts backend. remove unnecessary port split for qwen models --- app/converter/clients/qwen.py | 54 +++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) (limited to 'app/converter/clients/qwen.py') 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: -- cgit v1.2.3