[Feat]Support voice clone for omnivoice in online serving & add seed parameter for reproducible (#3668)

Signed-off-by: sphinxkkkbc <binchengkang8@gmail.com>
Co-authored-by: sphinxkkkbc <binchengkang8@gmail.com>
This commit is contained in:
boatmanandsphinxkkkbc authored and GitHub committed 2026-05-29 07:42:08 +00:00
1 parent c69ed4037d
commit 4d9262a6eb
9 files changed
+158 -22

No files matched your search

@@ -271,6 +271,14 @@ python examples/offline_inference/text_to_speech/omnivoice/end2end.py \
--lang zh
```
### Seed for Reproducibility
```bash
python examples/offline_inference/text_to_speech/omnivoice/end2end.py \
--model k2-fsa/OmniVoice \
--text "Hello, this is a test." \
--seed 42
```
### Notes
- Stage 0 (Generator): Qwen3-0.6B with 32-step iterative unmasking.
- Stage 1 (Decoder): HiggsAudioV2 RVQ + DAC at 24 kHz.
@@ -80,6 +80,12 @@ def run_e2e():
default=600,
help="Stage initialization timeout in seconds",
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="Random seed for generation",
)
nullify_stage_engine_defaults(parser)
args = parser.parse_args()
@@ -122,7 +128,7 @@ def run_e2e():
if mm_processor_kwargs:
prompts["mm_processor_kwargs"] = mm_processor_kwargs
sampling_params_list = [OmniDiffusionSamplingParams()]
sampling_params_list = [OmniDiffusionSamplingParams(extra_args={"seed": args.seed})]
print(f"Generating speech for: {args.text}")
@@ -18,7 +18,7 @@ For the full list of supported architectures across all modalities, see
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
| OmniVoice | `k2-fsa/OmniVoice` | (offline only) | — | — | — |
| OmniVoice | `k2-fsa/OmniVoice` | ✓ | — | — | — |
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | ✓ (Base) | ✓ (PCM + WebSocket) | ✓ (presets + `/v1/audio/voices` upload) | ✓ (standard + FastRTC) |
| VoxCPM2 | `openbmb/VoxCPM2` | ✓ | ✓ (AudioWorklet via gradio) | — | ✓ |
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✓ (gated upstream) | ✓ | ✓ (presets) | ✓ |
@@ -321,16 +321,29 @@ vllm serve k2-fsa/OmniVoice --omni --port 8091 --trust-remote-code
### CLI client
```bash
cd examples/online_serving/text_to_speech/omnivoice
# Text-only (auto voice)
python speech_client.py --text "Hello, how are you?"
# Language hint
python speech_client.py --text "Bonjour, comment allez-vous?" --language French
# Voice cloning (reference audio + optional ref_text)
python speech_client.py \
--text "Bonjour, comment allez-vous?" \
--ref-audio /path/to/ref_audio.wav \
--ref-text "Bonjour, comment allez-vous?"
# Style instruction (voice design-style control)
python speech_client.py \
--text "Bonjour, comment allez-vous?" \
--language French \
--instructions "loud voice"
# Deterministic output with seed parameter
python speech_client.py --text "Hello, how are you?" --seed 42
```
The client supports `--api-base`, `--model`, `--text`, `--response-format`, `--language`, `--output`.
The client supports `--api-base`, `--model`, `--text`, `--response-format`, `--language`, `--voice`, `--ref-audio`, `--ref-text`, `--instructions`, `--seed`, and `--output`.
### Notes
- Voice cloning and voice design require offline inference; see the [offline OmniVoice section](../../offline_inference/text_to_speech/README.md#omnivoice).
---
## Qwen3-TTS
@@ -12,6 +12,8 @@ Examples:
"""
import argparse
import base64
import os
import httpx
@@ -19,6 +21,24 @@ DEFAULT_API_BASE = "http://localhost:8091"
DEFAULT_API_KEY = "EMPTY"
def encode_audio_to_base64(audio_path: str) -> str:
"""Encode a local audio file to a base64 data URL."""
if not os.path.exists(audio_path):
raise FileNotFoundError(f"Audio file not found: {audio_path}")
ext = audio_path.lower().rsplit(".", 1)[-1]
mime = {
"wav": "audio/wav",
"mp3": "audio/mpeg",
"flac": "audio/flac",
"ogg": "audio/ogg",
}.get(ext, "audio/wav")
with open(audio_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
return f"data:{mime};base64,{b64}"
def run_tts(args) -> None:
"""Generate speech via /v1/audio/speech API."""
payload = {
@@ -26,16 +46,36 @@ def run_tts(args) -> None:
"input": args.text,
"response_format": args.response_format,
}
if args.seed is not None:
payload["extra_params"] = {}
payload["extra_params"]["seed"] = args.seed
if args.voice:
payload["voice"] = args.voice
if args.language:
payload["language"] = args.language
if args.ref_audio:
ref = args.ref_audio
if ref.startswith(("http://", "https://", "data:")):
payload["ref_audio"] = ref
else:
payload["ref_audio"] = encode_audio_to_base64(ref)
if args.ref_text:
payload["ref_text"] = args.ref_text
if args.instructions:
payload["instructions"] = args.instructions
print(f"Model: {args.model}")
print(f"Text: {args.text}")
if args.seed:
print(f"Seed: {args.seed}")
if args.voice:
print(f"Voice: {args.voice}")
if args.language:
print(f"Language: {args.language}")
print("Generating audio...")
@@ -45,7 +85,6 @@ def run_tts(args) -> None:
"Content-Type": "application/json",
"Authorization": f"Bearer {args.api_key}",
}
with httpx.Client(timeout=300.0) as client:
response = client.post(api_url, json=payload, headers=headers)
@@ -86,6 +125,30 @@ def main():
choices=["wav", "mp3", "flac", "pcm", "aac", "opus"],
help="Audio format (default: wav)",
)
parser.add_argument(
"--ref-audio",
type=str,
default=None,
help="Reference audio for voice cloning (local path, URL, or data: URI)",
)
parser.add_argument(
"--ref-text",
type=str,
default=None,
help="Reference text for voice cloning",
)
parser.add_argument(
"--instructions",
type=str,
default=None,
help="Voice style/emotion instructions",
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="Random seed for generation, default: None for stochastic output)",
)
parser.add_argument("--output", "-o", default=None, help="Output file path")
args = parser.parse_args()
run_tts(args)