[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:
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)
|
||||
|
||||
Reference in new issue
Block a user