feat: resolve language aliases in the VoiceClone voice field
OpenAI-compatible clients may send a route alias such as FR, FR_F or FR_M as the voice. Map it (case-insensitively) to a configured <LANG>_<M/F>_* speaker instead of falling back to the default English voice; a bare language code prefers a female voice. Regenerated patches/openai_server.patch from source so it applies cleanly to both the pinned upstream commit and upstream main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
62a2ad9e7c
commit
ceba2bf27a
@ -242,7 +242,7 @@ The streaming service on port `8023` uses the same generated `config/voices.json
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `model` | string | `tts-1` | Kept for OpenAI compatibility |
|
| `model` | string | `tts-1` | Kept for OpenAI compatibility |
|
||||||
| `input` | string | required | Text to synthesize |
|
| `input` | string | required | Text to synthesize |
|
||||||
| `voice` | string | first configured voice | Voice ID from the selected service |
|
| `voice` | string | first configured voice | Voice ID from the selected service. VoiceClone also accepts a language alias (`FR`, `FR_F`, `fr_m`) that resolves to the first matching `<LANG>_<M/F>_...` voice; a bare language code prefers a female voice |
|
||||||
| `response_format` | string | `wav` | `wav`, `pcm`, `mp3`, or `zip` (for timestamps) |
|
| `response_format` | string | `wav` | `wav`, `pcm`, `mp3`, or `zip` (for timestamps) |
|
||||||
| `speed` | float | 1.0 | Scales audio tempo via ffmpeg |
|
| `speed` | float | 1.0 | Scales audio tempo via ffmpeg |
|
||||||
| `language` | string | voice config | Per-request override for VoiceDesign/CustomVoice |
|
| `language` | string | voice config | Per-request override for VoiceDesign/CustomVoice |
|
||||||
@ -378,6 +378,10 @@ The first request after container startup can be slower because CUDA graph captu
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v6.11 — 2026-09-21
|
||||||
|
**Feature: Language Aliases for VoiceClone Voices**
|
||||||
|
- OpenAI-compatible clients that send a route alias such as `FR`, `FR_F` or `FR_M` in the `voice` field now get a configured speaker in that language (case-insensitive) instead of silently falling back to the default English voice. Unknown aliases still return the default voice or a 400 as before.
|
||||||
|
|
||||||
### v6.10.1 — 2026-09-21
|
### v6.10.1 — 2026-09-21
|
||||||
**Fix: Fresh `docker build` Crashes on Model Load**
|
**Fix: Fresh `docker build` Crashes on Model Load**
|
||||||
- **Root Cause:** The Dockerfile cloned upstream `faster-qwen3-tts` from `main`. Upstream v0.4.0 (2026-08-25) moved to Transformers 5 (`qwen-tts-hf`), but `qwen-asr` pins `transformers==4.57.6` and downgraded it in the next build step, so `Qwen3TTSModel.from_pretrained` failed on startup. Images built before that date were unaffected.
|
- **Root Cause:** The Dockerfile cloned upstream `faster-qwen3-tts` from `main`. Upstream v0.4.0 (2026-08-25) moved to Transformers 5 (`qwen-tts-hf`), but `qwen-asr` pins `transformers==4.57.6` and downgraded it in the next build step, so `Qwen3TTSModel.from_pretrained` failed on startup. Images built before that date were unaffected.
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
diff --git a/examples/openai_server.py b/examples/openai_server.py
|
diff --git a/examples/openai_server.py b/examples/openai_server.py
|
||||||
index 2199e14..05151ce 100644
|
index 2199e14..ca5eaf9 100644
|
||||||
--- a/examples/openai_server.py
|
--- a/examples/openai_server.py
|
||||||
+++ b/examples/openai_server.py
|
+++ b/examples/openai_server.py
|
||||||
@@ -36,11 +36,13 @@ API usage:
|
@@ -36,11 +36,13 @@ API usage:
|
||||||
@ -147,10 +147,12 @@ index 2199e14..05151ce 100644
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -145,6 +257,21 @@ def _to_mp3_bytes(pcm: np.ndarray, sample_rate: int) -> bytes:
|
@@ -144,9 +256,39 @@ def _to_mp3_bytes(pcm: np.ndarray, sample_rate: int) -> bytes:
|
||||||
|
|
||||||
|
|
||||||
def resolve_voice(voice_name: str) -> dict:
|
def resolve_voice(voice_name: str) -> dict:
|
||||||
"""Return voice config dict or fall back to default, else raise 400."""
|
- """Return voice config dict or fall back to default, else raise 400."""
|
||||||
|
+ """Return voice config dict, accepting a bare language code as an alias."""
|
||||||
+ global voices, last_voices_mtime
|
+ global voices, last_voices_mtime
|
||||||
+ voice_name = voice_name.strip()
|
+ voice_name = voice_name.strip()
|
||||||
+
|
+
|
||||||
@ -168,8 +170,25 @@ index 2199e14..05151ce 100644
|
|||||||
+
|
+
|
||||||
if voice_name in voices:
|
if voice_name in voices:
|
||||||
return voices[voice_name]
|
return voices[voice_name]
|
||||||
|
+ # OpenAI-compatible clients sometimes send a route alias (e.g. ``FR``,
|
||||||
|
+ # ``FR_F`` or ``FR_M``) in the voice field. Resolve it to a configured
|
||||||
|
+ # speaker instead of silently switching to the default English voice.
|
||||||
|
+ alias = re.fullmatch(r"([A-Z]{2})(?:_([MF]))?", voice_name, re.IGNORECASE)
|
||||||
|
+ if alias:
|
||||||
|
+ prefix = alias.group(1).upper() + "_"
|
||||||
|
+ if alias.group(2):
|
||||||
|
+ prefix += alias.group(2).upper() + "_"
|
||||||
|
+ candidates = [name for name in voices if name.upper().startswith(prefix)]
|
||||||
|
+ if candidates:
|
||||||
|
+ # For a language-only alias prefer a female voice, then registry order.
|
||||||
|
+ selected = next((name for name in candidates
|
||||||
|
+ if not alias.group(2) and name.upper().startswith(prefix + "F_")), candidates[0])
|
||||||
|
+ logger.warning("Voice %r is a language alias; using configured voice %r", voice_name, selected)
|
||||||
|
+ return voices[selected]
|
||||||
if default_voice and default_voice in voices:
|
if default_voice and default_voice in voices:
|
||||||
@@ -168,7 +295,47 @@ def resolve_voice(voice_name: str) -> dict:
|
logger.warning(
|
||||||
|
"Voice %r not configured; falling back to default voice %r",
|
||||||
|
@@ -168,7 +310,47 @@ def resolve_voice(voice_name: str) -> dict:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@ -218,7 +237,7 @@ index 2199e14..05151ce 100644
|
|||||||
"""
|
"""
|
||||||
Run generate_voice_clone_streaming in a background thread and yield
|
Run generate_voice_clone_streaming in a background thread and yield
|
||||||
raw PCM bytes for each chunk as they arrive.
|
raw PCM bytes for each chunk as they arrive.
|
||||||
@@ -177,21 +344,65 @@ async def _stream_chunks(voice_cfg: dict, text: str) -> AsyncGenerator[bytes, No
|
@@ -177,21 +359,65 @@ async def _stream_chunks(voice_cfg: dict, text: str) -> AsyncGenerator[bytes, No
|
||||||
_DONE = object()
|
_DONE = object()
|
||||||
|
|
||||||
def producer():
|
def producer():
|
||||||
@ -294,7 +313,7 @@ index 2199e14..05151ce 100644
|
|||||||
|
|
||||||
thread = threading.Thread(target=producer, daemon=True)
|
thread = threading.Thread(target=producer, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -203,7 +414,7 @@ async def _stream_chunks(voice_cfg: dict, text: str) -> AsyncGenerator[bytes, No
|
@@ -203,7 +429,7 @@ async def _stream_chunks(voice_cfg: dict, text: str) -> AsyncGenerator[bytes, No
|
||||||
break
|
break
|
||||||
if isinstance(item, Exception):
|
if isinstance(item, Exception):
|
||||||
raise item
|
raise item
|
||||||
@ -303,7 +322,7 @@ index 2199e14..05151ce 100644
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -220,7 +431,8 @@ async def health():
|
@@ -220,7 +446,8 @@ async def health():
|
||||||
async def create_speech(req: SpeechRequest):
|
async def create_speech(req: SpeechRequest):
|
||||||
if tts_model is None:
|
if tts_model is None:
|
||||||
raise HTTPException(status_code=503, detail="Model not loaded")
|
raise HTTPException(status_code=503, detail="Model not loaded")
|
||||||
@ -313,7 +332,7 @@ index 2199e14..05151ce 100644
|
|||||||
raise HTTPException(status_code=400, detail="'input' text is empty")
|
raise HTTPException(status_code=400, detail="'input' text is empty")
|
||||||
|
|
||||||
voice_cfg = resolve_voice(req.voice)
|
voice_cfg = resolve_voice(req.voice)
|
||||||
@@ -230,36 +442,84 @@ async def create_speech(req: SpeechRequest):
|
@@ -230,36 +457,84 @@ async def create_speech(req: SpeechRequest):
|
||||||
"wav": "audio/wav",
|
"wav": "audio/wav",
|
||||||
"pcm": "audio/pcm",
|
"pcm": "audio/pcm",
|
||||||
"mp3": "audio/mpeg",
|
"mp3": "audio/mpeg",
|
||||||
@ -411,7 +430,7 @@ index 2199e14..05151ce 100644
|
|||||||
yield raw_chunk
|
yield raw_chunk
|
||||||
|
|
||||||
return StreamingResponse(audio_stream(), media_type=content_type)
|
return StreamingResponse(audio_stream(), media_type=content_type)
|
||||||
@@ -306,16 +566,21 @@ def _parse_args():
|
@@ -306,16 +581,21 @@ def _parse_args():
|
||||||
p.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
|
p.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
|
||||||
p.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
|
p.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
|
||||||
p.add_argument("--device", default="cuda", help="Torch device (default: cuda)")
|
p.add_argument("--device", default="cuda", help="Torch device (default: cuda)")
|
||||||
@ -434,7 +453,7 @@ index 2199e14..05151ce 100644
|
|||||||
with open(args.voices) as f:
|
with open(args.voices) as f:
|
||||||
voices = json.load(f)
|
voices = json.load(f)
|
||||||
default_voice = next(iter(voices))
|
default_voice = next(iter(voices))
|
||||||
@@ -344,6 +609,7 @@ def main():
|
@@ -344,6 +624,7 @@ def main():
|
||||||
args.model,
|
args.model,
|
||||||
device=args.device,
|
device=args.device,
|
||||||
dtype=torch.bfloat16,
|
dtype=torch.bfloat16,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user