External LLM serving¶
VoiceHub can delegate the language-model portion of selected TTS models to vLLM or SGLang. Backend selection uses the same configuration-first pattern as the rest of the library:
from voicehub import AutoModelForTextToSpeech
model = AutoModelForTextToSpeech.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
model_type="qwen3tts",
llm_backend="vllm",
llm_backend_config={
"endpoint": "http://127.0.0.1:8091",
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
},
)
The integration is HTTP-only. VoiceHub does not import either engine and does not add one of their CUDA stacks to its environment.
Keep the engine in a separate environment¶
VoiceHub pins PyTorch 2.8 for its native runtime. Current engine releases own tightly coupled PyTorch, CUDA, Triton, FlashAttention, and compiler dependencies. For example, the current SGLang-Omni stack pins PyTorch 2.11, SGLang 0.5.12, CUDA 13 relay packages, and a narrow FlashAttention-4 range. The SGLang-Omni installation guide therefore recommends its prepared Docker image. vLLM and vLLM-Omni likewise select platform-specific compiled dependencies.
Run the engine in a container, virtual environment, or another host, then point VoiceHub at its HTTP endpoint. Do not install vLLM/SGLang into the VoiceHub environment merely to use this client.
This split has two useful properties:
- the engine controls its CUDA kernels, tensor parallelism, quantization, and scheduler without mutating the VoiceHub environment; and
- VoiceHub keeps its model-family input validation, output type, local codec
work where required, and
AutoModelForTextToSpeechAPI.
Two transport contracts¶
transport="auto" resolves to the only verified transport for a
model/backend pair.
| Transport | Engine route | Work performed by VoiceHub | Work performed by the engine |
|---|---|---|---|
tokens |
vLLM /v1/completions or SGLang /generate |
Prompt tokenization, model-specific token parsing, and codec decoding | Flat causal-LM token generation |
speech |
/v1/audio/speech |
Request normalization and PCM WAVE decoding | The complete tokenizer, model, codec, and vocoder pipeline |
Token transport is appropriate only when the language model is a standard single-stream causal LM. VoiceHub loads the tokenizer and codec on first use, but it does not load the local LM checkpoint.
Speech transport is for multi-stage or multi-codebook models. The external Omni server owns the complete synthesis pipeline, so the VoiceHub wrapper does not allocate native model weights. The current client requests one non-streaming WAVE response; engine batch, SSE, raw-streaming, and WebSocket interfaces are not exposed by this wrapper.
There is no silent fallback. An unverified backend, transport, checkpoint family, or native-only generation option raises a compatibility error before synthesis.
Capability matrix¶
The registry contains the pairings that have an explicit VoiceHub adapter:
| VoiceHub model type | vLLM | SGLang | Verified checkpoint family |
|---|---|---|---|
orpheustts |
Tokens | Tokens | Dense Llama Orpheus checkpoint |
llasa |
Tokens | Tokens | Dense Llama LLaSA checkpoint |
qwen3tts |
Speech (vLLM-Omni) | Speech (SGLang-Omni) | Base, CustomVoice, or VoiceDesign |
fishtts |
Speech (vLLM-Omni) | Speech (SGLang-Omni) | fishaudio/s2-pro |
mosstts |
Speech (vLLM-Omni) | Speech (SGLang-Omni) | Engine-specific supported MOSS pipeline |
cosyvoice |
Speech (vLLM-Omni) | — | FunAudioLLM/Fun-CosyVoice3-0.5B-2512 |
voxcpm |
Speech (vLLM-Omni) | — | openbmb/VoxCPM2 |
omnivoice |
Speech (vLLM-Omni) | — | k2-fsa/OmniVoice |
higgstts |
Speech (vLLM-Omni) | — | Higgs Audio v2 3B |
SGLang-Omni's Higgs Audio v3 pipeline is not compatible with VoiceHub's Higgs v2 wrapper. A blank cell means that VoiceHub intentionally rejects the pairing, even if an upstream engine later adds a model with a similar name.
Query the installed library instead of copying this table into application code:
from voicehub.llm_serving import list_llm_backend_support
for support in list_llm_backend_support():
print(
support.model_type,
support.backend.value,
support.default_transport.value,
support.checkpoint_family,
)
get_llm_backend_support(model_type, backend, transport="auto") resolves one
pairing without importing or contacting the engine.
Each support record also owns request-shape differences: default task types,
flat versus references-list reference audio, and named non-empty string
options understood by that exact engine pairing. Its speech_input_options,
speech_default_options, and speech_native_only_options properties expose the
resulting request contract. These fields are validated and returned by
support.to_dict() as JSON data. The wrapper and direct HTTP client consume the
same schema and do not contain model-name or extension-option branches.
Separately distributed integrations can register the same capability without editing VoiceHub's client:
from voicehub import (
LLMBackendSupport,
register_llm_backend_support,
)
register_llm_backend_support(
LLMBackendSupport(
model_type="auroratts",
backend="vllm",
transports=("speech",),
default_transport="speech",
engine="Aurora vLLM-Omni plugin",
checkpoint_family="acme/aurora-base",
task_type_without_reference="Generate",
task_type_with_reference="Clone",
task_type_aliases=(("clone", "Clone"),),
reference_format="references",
speech_string_options=("emotion_prompt",),
)
)
Register extensions once during process startup and remove temporary records in
tests with unregister_llm_backend_support(). A declared
speech_string_options entry becomes a recognized wrapper input and a
generation-default key automatically. It must be verified for that engine and
must not redefine a typed or request-owned field such as temperature or
reference_audio.
When a native architecture has no verified external-engine path, keep the
specific limitation beside that architecture as
metadata={"external_llm_backend_blocker": "..."} on its
ArchitectureSpec. The shared resolver reads this declaration lazily and uses
a generic fail-closed message when an architecture does not provide one.
Launch an engine¶
Install and launch these commands in the engine's environment. The commands follow the current upstream vLLM OpenAI server, vLLM-Omni Speech API, and SGLang-Omni TTS guides. Check the upstream model recipe when changing engine versions or checkpoints.
Flat token servers¶
Launch a standard vLLM server for Orpheus:
VoiceHub sends token IDs to /v1/completions, requests
return_token_ids=true, and disables special-token skipping. The current
vLLM completion protocol must return choices[0].token_ids.
The equivalent SGLang server is:
python -m sglang.launch_server \
--model-path canopylabs/orpheus-3b-0.1-ft \
--host 0.0.0.0 \
--port 30000 \
--skip-tokenizer-init
--skip-tokenizer-init makes the token-in/token-out boundary explicit.
VoiceHub posts input_ids to /generate and expects output_ids.
SGLang selects the model when the server starts, so VoiceHub does not send a
per-request model field on this route.
Replace the model path in both commands with
HKUSTAudio/Llasa-1B-Multilingual for LLaSA. The server checkpoint must
exactly match the checkpoint passed to VoiceHub; a different vocabulary or
special-token layout can produce invalid codec tokens.
Complete speech servers¶
For Qwen3-TTS CustomVoice with vLLM-Omni:
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--deploy-config vllm_omni/deploy/qwen3_tts.yaml \
--omni \
--port 8091 \
--trust-remote-code \
--enforce-eager
For Fish Speech S2 Pro:
The explicit Qwen deploy-config path is relative to a vLLM-Omni checkout. Use the matching upstream recipe or an absolute path when running elsewhere.
SGLang-Omni uses its model-specific pipeline YAML. Run these commands from the SGLang-Omni checkout, or replace each config with its absolute path:
sgl-omni serve \
--model-path Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice \
--config examples/configs/qwen3_tts_0_6b_customvoice.yaml \
--port 8000
sgl-omni serve \
--model-path fishaudio/s2-pro \
--config examples/configs/s2pro_tts.yaml \
--port 8000
For MOSS-TTS v1.5:
sgl-omni serve \
--model-path OpenMOSS-Team/MOSS-TTS-v1.5 \
--config examples/configs/moss_tts.yaml \
--port 8000
When a server fetches reference audio by URL, add only the required domains with the engine's media-domain allowlist. VoiceHub converts a local reference file to an inline audio data URL, so the remote process does not need access to the caller's filesystem.
Configure the client¶
Pass a mapping through llm_backend_config, or construct a typed runtime
configuration:
import os
from voicehub import AutoModelForTextToSpeech
from voicehub.llm_serving import LLMBackendConfig
backend = LLMBackendConfig(
backend="vllm",
endpoint="https://tts.internal.example",
transport="auto",
model="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
api_key=os.environ["VOICEHUB_TTS_API_KEY"],
timeout=300,
max_response_bytes=512 * 1024 * 1024,
)
model = AutoModelForTextToSpeech.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
model_type="qwen3tts",
llm_backend="vllm",
llm_backend_config=backend,
)
The configuration fields are:
| Field | Purpose |
|---|---|
backend |
native, vllm, or sglang |
endpoint |
Absolute HTTP(S) server base URL; required for an external backend |
transport |
auto, tokens, or speech |
model |
Model identifier expected by the server; defaults to the wrapper checkpoint |
api_key |
Runtime-only Bearer token |
headers |
Additional runtime-only HTTP headers |
timeout |
Positive request timeout in seconds |
extra_body |
Documented server-specific JSON fields not owned by VoiceHub |
max_response_bytes |
Hard upper bound for a response body |
Use the server origin, such as http://127.0.0.1:8091, as the endpoint.
VoiceHub appends the required route and also accepts a vLLM-style base ending
in /v1.
Security behavior¶
- Endpoint credentials, query strings, and fragments are rejected. Use
api_keyor oneAuthorizationheader, never both. - Header names and values reject newlines and transport-owned headers.
api_keyand header values are redacted fromrepr()andto_dict(). Backend configuration is kept on the live wrapper and is not written toconfig.json.extra_bodycannot replace request-owned fields such asinput,prompt,input_ids,stream, orresponse_format. Do not put credentials in it.- HTTP redirects fail closed. VoiceHub never forwards Bearer credentials or custom runtime headers to a redirect target.
- A local reference clip is read by the client, limited to 64 MiB, and sent as a data URL. Existing base64 audio data URLs are validated and subject to the same decoded-size limit; HTTP(S) reference URLs are also accepted.
- Use HTTPS and server-side authentication whenever traffic leaves a trusted host.
Sampling controls¶
The client maps sampling controls to each upstream protocol instead of assuming that the two OpenAI-shaped speech routes have identical schemas:
- vLLM-Omni receives
temperature,top_p, andtop_kinsideextra_params. Its current speech schema does not exposerepetition_penalty,duration_tokens,token_count, orstage_params, so VoiceHub rejects those call options. Its MOSS-SoundEffect adapter does acceptambient_sound. - SGLang-Omni receives
temperature,top_p,top_k,repetition_penalty,duration_tokens, andtoken_countas top-level fields. VoiceHub rejectsstage_paramsandnon_streaming_mode. - Remote seeds must be integers from
0through2**63 - 1. - Speech speed must be in the engine schema's
[0.25, 4.0]interval, and mode flags must be actual booleans.
Documented engine-version-specific extensions can be supplied through
LLMBackendConfig.extra_body. Explicit VoiceHub call options take precedence
when the client owns the corresponding protocol field.
Model examples¶
Qwen3-TTS through vLLM-Omni¶
CustomVoice uses a checkpoint speaker and optional style instruction:
from voicehub import AutoModelForTextToSpeech
checkpoint = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
qwen = AutoModelForTextToSpeech.from_pretrained(
checkpoint,
model_type="qwen3tts",
llm_backend="vllm",
llm_backend_config={
"endpoint": "http://127.0.0.1:8091",
"model": checkpoint,
},
)
output = qwen.generate(
"The serving engine owns the complete speech pipeline.",
mode="custom_voice",
speaker="ryan",
language="English",
instruct="Speak warmly and clearly.",
output_file="qwen-vllm.wav",
)
print(output.sample_rate, output.metadata)
Voice cloning requires the Base checkpoint and reference audio:
checkpoint = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
qwen_base = AutoModelForTextToSpeech.from_pretrained(
checkpoint,
model_type="qwen3tts",
llm_backend="sglang",
llm_backend_config={
"endpoint": "http://127.0.0.1:8000",
"model": checkpoint,
},
)
output = qwen_base.generate(
"This utterance uses the reference voice.",
mode="voice_clone",
speaker_audio_path="reference.wav",
reference_text="The exact transcript of reference.wav.",
language="English",
output_file="qwen-sglang-clone.wav",
)
VoiceDesign uses the matching VoiceDesign checkpoint,
mode="voice_design", and a non-empty instruct.
Fish Speech through SGLang-Omni¶
from voicehub import AutoModelForTextToSpeech
fish = AutoModelForTextToSpeech.from_pretrained(
"fishaudio/s2-pro",
model_type="fishtts",
llm_backend="sglang",
llm_backend_config={
"endpoint": "http://127.0.0.1:8000",
"model": "fishaudio/s2-pro",
},
)
output = fish.generate(
"External scheduling, one stable VoiceHub result.",
speaker_audio_path="reference.wav",
reference_text="The exact transcript of reference.wav.",
max_new_tokens=2048,
temperature=0.8,
output_file="fish-sglang.wav",
)
VoiceHub maps Fish reference audio to SGLang-Omni's references request
shape. The same call works with llm_backend="vllm" and a vLLM-Omni
endpoint; that adapter uses ref_audio and ref_text.
Orpheus through a flat token server¶
from voicehub import AutoModelForTextToSpeech
checkpoint = "canopylabs/orpheus-3b-0.1-ft"
orpheus = AutoModelForTextToSpeech.from_pretrained(
checkpoint,
model_type="orpheustts",
llm_backend="vllm",
llm_backend_config={
"endpoint": "http://127.0.0.1:8000",
"model": checkpoint,
},
)
output = orpheus.generate(
"The language model is remote, while SNAC decoding stays local.",
voice="tara",
max_new_tokens=1200,
temperature=0.6,
top_p=0.8,
output_file="orpheus-vllm.wav",
)
The first call downloads or opens Orpheus tokenizer/config files and the SNAC
codec, but skips the local Llama weights. Change the backend to sglang and
the endpoint to the SGLang server to keep the same VoiceHub generation API.
Unsupported custom generation¶
Do not route every model containing a Transformer through the token transport. Many TTS architectures require semantics that a stock flat-logit server cannot reproduce:
- OuteTTS needs its exact 64-token repetition window;
- NeuTTS needs checkpoint-specific RoPE behavior and minimum-token EOS masking;
- Vui, ConversationTTS, Zonos, Zonos2, CSM, Parler-TTS, and similar models generate multiple codebooks or run a hidden-state-conditioned depth decoder; and
- Chatterbox, GPT-SoVITS, XTTS, VibeVoice, Bark, and Dia use custom conditioning, CFG, semantic heads, or multiple generation stages.
VoiceHub rejects these token-server pairings. Qwen3-TTS, Fish Speech, and
other entries marked speech are supported only because the matching Omni
runtime implements their complete architecture-specific pipeline. A future
engine release does not become supported automatically; add and test a
capability record and request adapter first.
Lifecycle, optimization, and training limits¶
External backends are an inference mode:
- Select
llm_backendbefore callingload()or serving a request. - Only the eager wrapper-side inference strategy is supported.
- An external backend cannot be combined with VoiceHub's in-process
TTSOptimizationConfig. Configure compilation, quantization, attention kernels, and parallelism in the engine process. load_for_training()rejects an external wrapper. Fine-tune with a native VoiceHub wrapper, export a server-compatible checkpoint, launch the engine against that artifact, and create a new external wrapper.from_pretrained()cannot attach an external backend while restoring a local VoiceHub Trainer state. Point the engine at the exported fine-tuned checkpoint instead.- After a token-backed wrapper has loaded its local tokenizer/codec runtime, it cannot detach the remote LM. Create a fresh native wrapper to transition back to local inference.
- A token-backed wrapper does not own the LM weights and therefore cannot export a complete native pretrained model.
- Requests on one external wrapper may overlap so the server can apply continuous batching. Backend configuration cannot be replaced or cleared while those requests are active.
These constraints prevent duplicated weights, partially restored training graphs, and accidental mixing of server-side and in-process optimization policies.