aboutsummaryrefslogtreecommitdiff
path: root/app/converter/clients/qwen.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/converter/clients/qwen.py')
-rw-r--r--app/converter/clients/qwen.py54
1 files changed, 42 insertions, 12 deletions
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: