Speech API

The UI, native API and OpenAI-compatible speech API share port 7860, one FIFO inference queue and the same local Qwen implementation. Discover the exact HTTP request schemas at /openapi.json. The browser API guide is /docs.

Live text input and audio output

Connect to ws://localhost:7860/tts/live to send arriving text and receive audio from one ongoing cloned utterance. It supports full-reference ICL with the saved reference transcript, or speaker-only conditioning. Control messages are JSON; audio frames are raw PCM16LE at 24 kHz, mono. This native protocol is documented in the live speech API guide, with a Python client. It shares the existing FIFO inference queue and operator model policy.

OpenAI-compatible clients

curl http://localhost:7860/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{"model":"qwen3-tts","input":"Good morning from Hangry Labs.","voice":"Ryan","response_format":"mp3"}' \
  --output speech.mp3
from openai import OpenAI

client = OpenAI(base_url="http://localhost:7860/v1", api_key="local")
with client.audio.speech.with_streaming_response.create(
    model="qwen3-tts", voice="Ryan", input="Good morning from Hangry Labs.",
    response_format="mp3",
) as response:
    response.stream_to_file("speech.mp3")

GET /v1/models lists the five actual checkpoints. GET /v1/models/{id} also accepts the routing aliases qwen3-tts, faster-qwen3-tts, tts-1, tts-1-hd and gpt-4o-mini-tts. These aliases select Qwen workflows, not OpenAI models. With an alias, saved voices use the Clone workflow default and built-in speakers use the Generate workflow default from System. Both initially select 0.6B models; delivery instructions automatically select 1.7B CustomVoice when needed. An explicit Qwen checkpoint must match the requested workflow and supported instructions.

GET /v1/audio/voices lists native speakers, compatibility aliases and saved profile ids. OpenAI voice aliases such as alloy map to documented Qwen speakers; they do not reproduce OpenAI voices. A saved voice works as either its id, unique name or {"id":"saved-id"}. instructions directs delivery, including experimental Base delivery with an audio-backed saved voice.

Create and delete saved voices

POST /v1/audio/voices and DELETE /v1/audio/voices/{voice_id} follow the OpenAI voice resource shapes. They use the same saved library as the UI and native API. Creation returns id, object: "audio.voice", created_at, name and type. Deletion requires the exact saved id and returns {"id":"saved-id","deleted":true,"object":"audio.voice.deleted"}; unknown or previously deleted ids return 404. Built-in speakers cannot be deleted. Existing names return 409; creation never silently replaces a recording. Use the native API for explicitly confirmed replacement or metadata editing.

For a reference-based voice, upload a consent recording first, then send its local id with the sample. Both requests use multipart form data:

curl http://localhost:7860/v1/audio/voice_consents \
  -F 'name=Speaker approval' -F 'language=en-US' \
  -F 'recording=@consent.wav'

curl http://localhost:7860/v1/audio/voices \
  -F 'name=My narrator' -F 'consent=cons_RETURNED_ID' \
  -F 'audio_sample=@reference.wav' -F 'ref_text=The exact reference words.'

The optional type defaults to audio_sample. ref_text and language are local extensions; without reference words, synthesis uses identity-only cloning. Names support up to 256 characters. Audio uploads share the native limits: 10 MiB per file and two minutes, with supported formats normalized to mono float WAV. No model inference is needed to register a recording.

type="prompt" accepts JSON or multipart name, prompt and optional script_hint. It generates a reference with the configured Qwen VoiceDesign model and publishes it only after successful generation:

curl http://localhost:7860/v1/audio/voices \
  -H 'Content-Type: application/json' \
  -d '{"type":"prompt","name":"Warm narrator","prompt":"A calm, warm adult storyteller."}'

Prompts and script hints support up to 2000 characters. Without a hint, the server uses a short fixed English sample. Set the language extension and provide an appropriate hint for another speech language. model="auto" uses the configured design checkpoint; the explicit Qwen VoiceDesign id is also accepted. OpenAI model revision names are not Qwen checkpoints and return 400. The response includes type: "prompt" and the stored prompt, without preview audio. In this Qwen implementation, prompt-created voices also work immediately with /v1/audio/speech; this extends OpenAI’s Live-only prompt-voice behavior.

Consent resources support POST/GET /v1/audio/voice_consents and GET/POST/DELETE /v1/audio/voice_consents/{consent_id}. Creation takes name, a BCP 47 language tag and a recording file. Updates take JSON {"name":"New label"}. Listing uses limit (1..100, default 20) and an after cursor, with data, has_more, first_id and last_id. Consent objects use audio.voice_consent; deletion returns that same object type with deleted: true. Voice and consent deletion are independent: neither automatically deletes the other resource. Recordings and metadata stay in the local product volume. This service stores the supplied consent record; it does not verify the speaker’s identity or match the spoken consent phrase. The native voice library does not require this OpenAI consent workflow. Existing /v1 API-key authentication covers these routes.

The reference contracts are OpenAI’s voice creation, voice deletion and consent resources.

Speech output and controls

Formats: MP3 (default), WAV, FLAC, Opus in Ogg, AAC in ADTS, Ogg Vorbis, and raw PCM. PCM is 24 kHz, mono, signed 16-bit little endian, with no header. WAV and FLAC complete before delivery; other formats use one progressive encoder. OpenAI stream_format="audio" is supported. OpenAI speech SSE events are not implemented. input is always plain text; use native routes for markup. speed is a pitch-preserving audio effect, from 0.25 to 4.0.

Optional extensions include language, seed, randomize_seed, voice_profile, normalize_loudness, protect_long_audio, temperature, top_k, repetition_penalty, chunk_size, decoder_context_frames, max_new_tokens, non_streaming_mode and xvec_only. They use the same validation and controls as native requests. Unknown harmless OpenAI fields are ignored. Unsupported formats, voices, models and conditioning combinations return OpenAI-shaped error objects. Set QWEN_STUDIO_API_KEY to require a Bearer key on /v1 routes. This does not authenticate the browser, native API or System routes; use a trusted bind address or an authenticated reverse proxy for the whole Studio.

Browser requests that change state must come from the same service host. This blocks cross-site form submissions to the local service. Direct API clients without an Origin header remain supported; MCP uses its own configured host/origin validation.

Native synthesis

curl http://localhost:7860/tts/generate \
  -H 'Content-Type: application/json' \
  -d '{"text":"Hello again.","voice":"Ryan","seed":42,"output_format":"wav"}' \
  --output speech.wav

POST /tts/generate and /tts/convert return complete binary audio. Use POST /tts/stream for a progressive response and select stream_format from MP3, PCM, Opus, AAC, FLAC or Ogg. All three Qwen model families produce incremental codec audio. Text and multi-turn SSML/SSML-H can stream within each speech unit. Internal boundary compaction retains up to 600 ms of audio at a unit edge; it does not wait for the next unit to finish. Explicit pauses and outer recording edges retain the complete-response semantics.

Speed/prosody use a continuous filter per speech unit. Optional loudness normalization runs once over the assembled stream. These filters and compressed codecs add buffering, so browser playback latency differs from model/SSE TTFA. One encoder owns the entire response. MP3 uses bounded input probing to avoid waiting for the whole short recording. If MP3 MediaSource is unavailable, the browser collects the stream and plays the completed recording instead.

POST /tts/events returns native SSE progress and base64 WAV chunks, followed by a done event containing metrics and committed profile ids. This is separate from the OpenAI protocol. Cancelling/disconnecting stops queued work and asks active generation to stop at its next yield. An in-flight complete model call finishes before releasing its GPU lease.

All native routes accept input_type="text" (default), "ssml" or "ssml-h". See the processor profile. instruct, control and instructions are equivalent names for a default delivery/voice description. Sampling controls are temperature, top_k, repetition_penalty, seed, max_new_tokens, chunk_size and non_streaming_mode. The last field controls Qwen text input, independently of response streaming. The native schemas list the supported controls; unknown native fields fail clearly.

decoder_context_frames controls how many preceding codec frames are reused when reconstructing streamed audio. It accepts integers from 1 to 200 and defaults to 25 in every voice mode. Larger windows may improve continuity but require more decoder work and memory. It is independent of chunk size, sampling and text conditioning, and has no effect on complete waveform generation. Use it on native streaming routes or as an OpenAI extension with a progressive format such as PCM or MP3; OpenAI WAV/FLAC responses use complete generation.

For uploaded cloning, send multipart payload JSON and reference_audio to /tts/generate-upload or /tts/stream-upload. Set mode="clone", ref_text to the exact reference words, or xvec_only=true for identity-only conditioning. Do not send server filesystem paths or remote audio URLs. For saved references, set voice_profile to a saved id or unique name; the saved transcript is reused.

Long text

protect_long_audio=true is the default. The planner counts words in spaced scripts and characters in CJK scripts, preferring sentence endings, then clauses, then whitespace, with a hard 400-character ceiling. Default planning targets 50 units with an 80-unit ceiling; smaller codec budgets reduce those limits. Whitespace is collapsed and an uninterrupted oversized word is split at the character ceiling. These heuristics do not guarantee speech duration. With protection disabled, each speech unit must fit 1000 characters.

Native source input can contain up to 50000 characters, including markup; OpenAI input is limited to 4096. Both use the processor limits of 6000 spoken characters, 64 compiled speech units and 300 seconds of final audio. The audio limit is enforced during execution, including pauses and effects, so an overlong stream can fail after some audio has already been delivered.

Each split is a fresh model call with the chosen voice/reference and a seed incremented by speech-unit index. Previous chunks are not supplied as narration context. Clone calls reuse the original reference; VoiceDesign repeats the description for each chunk. New SSML-H voice definitions are bootstrapped before dialogue synthesis starts. One FIFO GPU lease covers the whole request.

Internal padding is compacted and adjacent speech receives a 120 ms handoff; an explicit SSML break replaces that gap. Outer recording edges are preserved. Streaming still retains audio for the completed result and takes; it is not a constant-memory long-form session.

max_new_tokens applies per model call, not to the final joined recording. The default 360 codec frames represent roughly 30 seconds of raw speech; the maximum 600 represents roughly 50 seconds, before effects. Exhausting the token or model-context budget is reported as incomplete generation. Complete native responses may retry the affected unit once in up to three smaller sections before publishing audio; metrics.retries records this recovery. A failed retry returns HTTP 422. Streaming never regenerates already-emitted speech: SSE reports an error and binary streams terminate unsuccessfully. There is no automatic continuation from partial model context.

Inspect the full plan before inference with POST /tts/plan; POST /tts/metrics reports text and plan counts. The legacy /studio/generate and /studio/generate/stream routes, used by the dedicated Design/Clone workflows, accept at most 1000 text characters in one model call and do not use this automatic splitting. Audio output streaming requires the complete input request first; incremental text input and bidirectional sessions are not exposed.

Voices and saved scripts

GET/POST /tts/voice-profiles lists or explicitly saves references. POST takes multipart name, reference_audio, optional ref_text, language, description, JSON tags, JSON recipe, and profile_type (clone, reference or design). Names support up to 256 characters and are unique ignoring case. GET /tts/voice-profiles?q=warm searches names, descriptions, tags and languages.

Optional original_audio, processed_audio and design_audio uploads retain the creation recording, processed alternative and source cloning reference. Audio is stored as mono float WAV without changing its sample rate. Each upload must fit the reference limits: 10 MB and two minutes. The selected reference_audio is what synthesis uses. portrait accepts PNG/JPEG/WebP up to 5 MB and 25 megapixels, normalized to a 100×100 lossless WebP.

PUT metadata to /tts/voice-profiles/{id} to edit it. Omitted fields, audio and creation assets remain intact. Replacing reference_audio requires replace_audio=true; send the replacement creation assets alongside it. Unspecified processed/source recordings are cleared on audio replacement to avoid retaining an unrelated creation history. Portraits persist unless replaced or explicitly removed with clear_portrait=true. POST replace=true also allows explicit replacement by an existing unique name, preserving its profile id.

Responses include audio_url, optional original_url, processed_url, source_url and portrait_url, plus recipe and timestamps. GET these URLs to retrieve assets; DELETE /tts/voice-profiles/{id} removes the profile and assets. Old saved voices and /studio/voices remain compatible. Recipes record creation settings; matching a seed alone does not guarantee identical audio across model or runtime changes.

GET/POST /tts/dialogue-scripts, GET/PUT/DELETE /tts/dialogue-scripts/{id}, and GET its /download child manage SSML-H scripts. Scripts contain a name, document, optional description, tags and folder. Script storage validates markup without requiring every referenced voice to be installed yet. Synthesis resolves all voices before taking the GPU lease.

POST/DELETE /tts/dialogue-script-folders[/{id}] creates/deletes folders. PATCH /tts/dialogue-scripts/{id}/folder moves a script. Deleting a folder leaves its scripts ungrouped. POST /tts/dialogue-workspaces and PUT /tts/dialogue-workspaces/{id} explicitly store a script, settings, locked takes and optional output/processed audio. They take multipart metadata, takes, output and processed; the browser sends this contract when saving. Stored audio is validated and canonicalized to WAV, with a 20 MB per-file and 40 MB total upload limit. Script/workspace data stays in the product volume.

Magic

Magic compiles the same SSML-H plan as native synthesis. It adds take editing:

Route Request Response
POST /tts/magic/take JSON payload speech request and zero-based speech_index One WAV take; never commits document profiles
POST /tts/magic/render Multipart payload, ordered JSON speech_ids, JSON retained id/seed records, matching takes files Generated/unlocked takes plus assembled output
POST /tts/magic/assemble-upload Multipart JSON options timeline, take_metadata, takes Assemble existing takes without inference

Render returns application/vnd.hangrylabs.magic-takes: a four-byte little endian JSON manifest length, UTF-8 manifest, then audio payloads. Manifest version 1 describes each take/output offset and length relative to the audio payload, seed, format and committed profiles. The packaged magic-takes.js client implements this shared Hangry Labs contract. Speech ids correspond to compiled units; keep Magic turns within 400 characters to avoid automatic splits. Locked takes are reused byte-for-byte at WAV precision. Source editing invalidates the browser’s corresponding take. Assembly preserves explicit positive/zero pauses and adds a 120 ms handoff only where no pause was specified.

Discovery and utilities

Operator model defaults live at GET /system/settings under model_policy. PUT /system/settings accepts either asr_url, model_policy, or both, preserving other settings. A policy contains enabled and all three workflows keys: custom, clone, design, each with a compatible full model id and a keep_loaded boolean. Disabling enabled releases weights after each request; it preserves per-workflow choices. Changes apply without a container restart. GET /studio/status lists actual resident_models, requested pins and individual residency_errors. Its gpu_memory reports this process’s PyTorch allocated and reserved MiB once CUDA is initialized, independently of device-wide telemetry. A saved policy may need more VRAM than the GPU has available; inspect status after applying it. GET /studio/capabilities exposes workflow defaults. Explicit request model ids override defaults; interoperable aliases and auto use them. CustomVoice delivery direction automatically selects a capable 1.7B model when the configured 0.6B default cannot accept instructions.

Routes Purpose
GET /tts/ping, /tts/status, /tts/activity Version, model and queue status
GET /tts/defaults, /tts/formats, /tts/stream-formats Current defaults and encoding contracts
GET /tts/languages, /tts/voices, /tts/speakers Qwen inventory and saved profiles
GET /tts/ssml/capabilities Exact supported markup and processor limits
POST /tts/transcribe-upload reference_audio forwarded to the configured ASR service
POST /tts/postprocess-upload Audio plus JSON options: speed, pitch in semitones, linear volume, normalization, output format, and finishing controls below
POST /tts/purge Unload all models in this Studio

Supply a unique X-Qwen-Request-ID (1 to 80 letters, digits or hyphens) with /tts/generate, /tts/stream, /tts/magic/take or /tts/magic/render, then poll GET /tts/activity?request_id=... for that request’s phase, title and detail. Completed progressive requests also expose metrics and committed profile ids. Unknown or expired ids return an empty object. Progress is retained in memory for up to an hour, bounded to the most recent 128 requests. Omitting the query retains the model/queue status response.

Finishing accepts multipart audio and JSON options, up to 40 MB and 300 seconds. method="ffmpeg" provides noise cleanup, EQ, dynamics and optional loudness normalization. ffmpeg-shape and signalsmith add independent pitch_semitones (-6 to 6) and speed_factor (0.75 to 1.25). WAV previews use float samples at Qwen’s native 24 kHz rate.

Advanced controls include high_pass_hz, warmth_db/warmth_hz, mud_db/mud_hz, presence_db/presence_hz, air_db/air_hz, compression_ratio, compression_threshold_dbfs, compression_attack_ms, compression_release_ms, deesser_strength_percent, saturation_blend_percent, room_percent, target_loudness_lufs and peak_ceiling_dbtp. The schema exposes exact bounds; the air band tops out at 11 kHz for the 24 kHz signal. Loudness normalization remains opt-in. preset records clean, studio or custom; clients send numeric settings explicitly. Legacy speed, pitch, volume and output_format remain supported. Originals and processed previews remain separate, and preview processing does not load TTS weights.

The original /studio/* multipart/JSON/SSE contracts remain available for single-model workflows. Native speech requests may provide ref_audio as a container path within the configured MCP input directory. It is mutually exclusive with an upload or voice_profile. POST /tts/transcribe accepts JSON {"audio_location":"reference.wav"} from that same directory. Native routes do not fetch URL references; bounded HTTP inputs belong to advanced MCP tools. No local ASR or forced alignment is bundled. Only clone permitted speakers and use recordings you are authorized to process.

GET /tts/ping and /tts/status include version, build_id, build_date and image_version. Unspecified development build metadata is reported as unknown; runtime does not infer a Git revision from the host filesystem.

Queue admission

GPU inference remains serialized. QWEN_STUDIO_QUEUE_SIZE sets pending capacity from 0 to 256, default 8. The synthesis pipeline admits at most this value plus one request, including requests still finishing CPU audio work. Zero permits one request at a time, including its finishing stage. Native and OpenAI generation reserve admission before voice lookup and plan compilation, which run off the HTTP event loop while earlier requests synthesize. Reference files are prepared before the GPU lease. Model tokenization, reference encoding and waveform decoding still run under that lease. Admission happens before creating a generation thread or opening a response stream. A full queue returns HTTP 429 with Retry-After: 1; /v1 uses an OpenAI error envelope. Cancellation removes queued work. Status includes queue capacity, active/waiting counts, total synthesis inflight and cumulative rejected admissions. Full-response per-unit effects run on CPU workers with at most two unfinished units per request and two simultaneous effect operations per service. The final model call releases the GPU before assembly and whole-recording normalization. Streaming releases it before the final filter drain; bounded stream buffers still apply backpressure during inference. HTTP encoding already runs separately from GPU inference.

Native generation metrics add preparation_ms, gpu_lease_ms and postprocess_ms. Preparation measures off-loop voice lookup and compilation. The GPU lease includes model preparation and synthesis; the CPU tail starts after release and excludes HTTP encoding. These intervals can overlap work from other requests. queue_ms measures time until GPU acquisition, including reference file preparation; total_ms starts after planning. Complete-response ttfa_ms remains time to assembled audio, rather than a streaming latency claim.