# services.yaml — canonical catalog of inference services on irv-ml1
#
# THIS IS A FIRST-CLASS CONTRACT. External consumers depend on the
# shape and contents of this file. See CATALOG-CONTRACT.md alongside
# for: schema reference, change-management policy, sync workflow for
# downstream consumers, and known consumers list.
#
# Drives form generators that auto-render UIs against the inference
# services. Each entry produces one form (from `fields:`) and one
# response renderer (dispatched on `response.type`). The field-type
# vocabulary and response-type vocabulary are closed sets — see the
# JSON Schema at services.schema.json or the contract doc.
#
# `catalog_version`: bump when the SCHEMA changes (a field type added,
# a required field removed, etc). Per-service `version:` bumps when a
# specific service's parameter shape changes. Both let downstream
# consumers detect drift.

catalog_version: 2

services:
  - id: kokoro
    name: Kokoro 82M TTS
    description: >
      Lowest-latency English TTS (82M params, fp16). ~300ms TTFA, 35–100x realtime,
      60+ built-in voices in 8 languages. Voice mixing via inline weights.
      OpenAI-compatible /v1/audio/speech.
    category: tts
    version: 3
    host: irv-ml1
    lifecycle:
      stack: kokoro
      vram_gb: 2
      gpu_device_id: null
    endpoint: http://10.100.79.3:8193/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: hexgrad/Kokoro-82M
      revision: null
      image: ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4-master
    fields:
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: select
        label: Voice
        source_url: http://10.100.79.3:8193/v1/audio/voices
        source_jsonpath: $.voices[*]
        default: af_heart
        description: >
          60+ built-in voices. Default `af_heart` is the only A-rated voice per
          upstream VOICES.md (also the kokoro-fastapi container's own default).
          Custom blends: af_bella(2)+af_aoede(1) syntax for weighted mixing.
          Persistent custom voices via playbooks/blend-kokoro-voice.yaml.
      - name: response_format
        type: select
        options: [wav, mp3, opus, flac, pcm]
        default: wav
      - name: speed
        type: slider
        label: Speed
        required: false
        min: 0.25
        max: 4.0
        step: 0.05
        default: 1.0
        description: Playback speed multiplier. 1.0 is natural.
      - name: volume_multiplier
        type: slider
        label: Volume
        required: false
        min: 0.5
        max: 2.0
        step: 0.05
        default: 1.0
        description: >
          Output gain. Useful for level-matching against music beds.
          Upstream is unbounded; UI bounds (0.5–2.0) are a sensible default —
          override the slider client-side if you need extremes.
      - name: lang_code
        type: text
        label: Language code
        required: false
        description: >
          Optional language hint (e.g. "a" for American English). When unset,
          kokoro infers from the first letter of the voice name. Set when text
          contains code-switching kokoro guesses wrong on.
      - name: stream
        type: bool
        default: false
        description: Phrase-by-phrase streaming via chunked HTTP.
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: false
      deterministic: true
      notes: >
        No seed parameter. Model fully deterministic (no sampling); identical
        params always produce identical bytes. Image tag is mutable; pin to
        digest for v3 reproducibility audit.
    estimated_latency:
      cold_start_s: 2
      warm_per_unit: "~300ms TTFA, 35–100x realtime"
    license: Apache-2.0
    notes: |
      Voice mixing: voice="name1(w1)+name2(w2)+..." normalizes weights.
      Custom voices persist at /worktank/kokoro/user_voices (bind-mounted).
      v2 (2026-05-11): added speed, volume_multiplier, lang_code per
      kokoro's OpenAPI surface.

  - id: kokoro-captioned
    name: Kokoro Captioned Speech
    description: >
      Kokoro TTS with word-level timestamps returned alongside the audio.
      For subtitle generation and video sync. Same model as `kokoro`;
      separate catalog entry because the wire shape is a JSON envelope
      carrying base64-encoded audio plus a structured timestamps array.
    category: tts
    version: 2
    status: ready
    host: irv-ml1
    endpoint: http://10.100.79.3:8193/dev/captioned_speech
    method: POST
    content_type: application/json
    model:
      id: hexgrad/Kokoro-82M
      revision: null
      image: ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4-master
    fields:
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: select
        label: Voice
        source_url: http://10.100.79.3:8193/v1/audio/voices
        source_jsonpath: $.voices[*]
        default: af_heart
      - name: response_format
        type: select
        options: [wav, mp3, opus, flac, pcm]
        default: wav
      - name: speed
        type: slider
        label: Speed
        required: false
        min: 0.25
        max: 4.0
        step: 0.05
        default: 1.0
      - name: lang_code
        type: text
        label: Language code
        required: false
    response:
      # Stays in the closed type vocabulary: from a renderer-dispatch
      # standpoint this IS audio. The audio_field/audio_format_field/
      # timestamps_field decomposition tells consumers how to extract
      # those parts from the JSON envelope wire shape — added to the
      # catalog schema in 2026-05 specifically to support response
      # shapes like this one without extending the type vocab.
      type: audio
      audio_field: audio
      audio_format_field: audio_format
      timestamps_field: timestamps
    reproducibility:
      seedable: false
      deterministic: true
      notes: >
        Same determinism story as kokoro proper. Verified wire shape
        (2026-05-11 against live API):
          {
            "audio": "<base64-encoded bytes in response_format>",
            "audio_format": "audio/wav" (or matching response_format),
            "timestamps": [{"word": str, "start_time": float, "end_time": float}, ...]
          }
        Consumer base64-decodes `audio` to play; `timestamps` drives
        subtitle/karaoke UI. The response decomposition fields above
        encode this so the renderer doesn't need per-service-id branches.
    estimated_latency:
      cold_start_s: 2
      warm_per_unit: "~same as kokoro proper, plus minor overhead for timestamp emission"
    license: Apache-2.0
    notes: |
      `return_timestamps` and `stream` upstream params deliberately
      omitted from the catalog: timestamps must be on for this endpoint
      to be meaningful, and streaming + JSON-with-base64 don't compose.
      `download_format` / `return_download_link` skipped — same as kokoro
      proper.

      status: experimental until the consumer's audio-with-timestamps
      renderer ships. Once present, flip to status: ready.

  - id: chatterbox
    name: Chatterbox TTS
    description: >
      Resemble AI's low-latency English TTS (Chatterbox-Turbo, 350M, ~75ms TTFB,
      6× realtime). 28 built-in predefined voices + zero-shot cloning from a
      5–30s reference. Inline paralinguistic tags. (Resemble's exaggeration /
      cfg_weight emotion knobs are no-ops on the Turbo checkpoint — omitted.)
    category: tts
    version: 2
    status: ready
    host: irv-ml1
    lifecycle:
      stack: chatterbox
      vram_gb: 4
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8196/tts
    method: POST
    content_type: application/json
    model:
      id: ResembleAI/chatterbox-turbo
      revision: null
      image: devnen/Chatterbox-TTS-Server:latest
    section_groups:
      - id: basic
        label: Text & voice
      - id: sampling
        label: Expression & sampling
        hint: Turbo honors temperature / seed / speed_factor. (exaggeration / cfg_weight are Turbo no-ops — removed.)
      - id: advanced
        label: Advanced
    fields:
      - name: text
        type: textarea
        label: Text (with optional [tags])
        section: basic
        required: true
        max_length: 5000
        description: >
          Inline paralinguistic tags honored by Turbo: [laugh] [chuckle] [sigh]
          [gasp] [cough] [clear throat] [sniff] [groan] [shush]. Best results
          when a physical tag is paired with surrounding emotional context.
      - name: voice_mode
        type: select
        label: Voice mode
        section: basic
        options: [predefined, clone]
        default: predefined
        description: >
          `predefined` -> a built-in voice (predefined_voice_id below).
          `clone` -> a reference clip (reference_audio_filename). predefined is
          the out-of-box default; the empty/"undefined" case is avoided by
          defaulting the voice below.
      - name: predefined_voice_id
        type: select
        label: Voice (built-in)
        section: basic
        optional: true
        default: "Emily.wav"
        source_url: http://10.100.79.3:8196/get_predefined_voices
        source_jsonpath: $[*].filename
        description: >
          Required when voice_mode=predefined. 28 built-in voices staged in the
          devnen image (Abigail, Adrian, Alexander, Alice, Austin, Axel, Connor,
          Cora, Elena, Eli, Emily, Everett, Gabriel, Gianna, Henry, Ian, Jade,
          Jeremiah, Jordan, Julian, Layla, Leonardo, Michael, Miles, Olivia,
          Ryan, Taylor, Thomas — each <name>.wav). Default Emily.wav is the
          server's own default_voice_id. Verified live via /get_predefined_voices.
      - name: reference_audio_filename
        type: select
        label: Voice (clone reference)
        section: basic
        optional: true
        source_url: http://10.100.79.3:8196/get_reference_files
        source_jsonpath: $[*]
        description: >
          Required when voice_mode=clone. 5–30s clean WAV (16 kHz+ mono) under
          /worktank/chatterbox/reference_audio/; upload via the server's
          /upload_reference. Match the clip's language to `language` to avoid
          accent transfer.
      # exaggeration / cfg_weight intentionally removed: Chatterbox-Turbo IGNORES
      # both. Proven 2026-06-04 — same text/voice/seed across cfg 0.0/0.5/0.9 gave
      # byte-identical audio, and the model logs "CFG, min_p and exaggeration are
      # not supported by Turbo version and will be ignored." They were dead UI
      # sliders. They'd only do something on a non-turbo Chatterbox checkpoint.
      - name: temperature
        type: slider
        section: sampling
        min: 0.05
        max: 2.0
        step: 0.05
        default: 0.8
        description: Sampling temperature; lower = steadier. Server + Resemble default 0.8.
      - name: speed_factor
        type: slider
        section: sampling
        min: 0.5
        max: 2.0
        step: 0.05
        default: 1.0
        description: Post-hoc playback speed. Server default 1.0.
      - name: seed
        type: number
        section: sampling
        required: false
        default: 0
        description: 0 = random; a fixed integer repeats the same take.
      - name: output_format
        type: select
        section: basic
        options: [wav, opus, mp3]
        default: wav
        description: 24 kHz. Live-verified enum (wav/opus/mp3).
      - name: language
        type: text
        section: advanced
        required: false
        default: en
        description: >
          Language override. Base Turbo is English; the multilingual variant
          (23 languages, via the stack .env) honors other codes. Leave `en`.
      - name: split_text
        type: bool
        section: advanced
        default: true
        description: Auto-split long text into chunks.
      - name: chunk_size
        type: slider
        section: advanced
        min: 100
        max: 1000
        step: 10
        default: 120
        description: Target chunk length in chars when splitting (server default 120).
    response:
      type: audio
      mime_from_field: output_format
    reproducibility:
      seedable: true
      deterministic: false
      seed_field: seed
      notes: >
        /tts exposes `seed` (0=random); a fixed seed + identical params repeats a
        take. Temperature-sampled → not guaranteed byte-exact, and Resemble's
        PerTh watermark is applied unconditionally. (Prior catalog claimed no
        seed support — corrected against the live OpenAPI 2026-06-01.)
    estimated_latency:
      cold_start_s: 3
      warm_per_unit: "~75ms TTFB, 6× realtime"
    license: MIT
    notes: |
      Routes to the devnen wrapper's /tts (control surface: temperature /
      speed_factor / seed / voice_mode) rather than the thin OpenAI
      /v1/audio/speech. exaggeration / cfg_weight were DROPPED 2026-06-04 — the
      Turbo checkpoint ignores them (proven byte-identical across cfg values; the
      model warns they're "not supported by Turbo version and will be ignored"),
      so they were dead UI sliders. Same wrapper author as the `dia` stack;
      identical predefined/clone voice model. Defaults from the server's
      generation_defaults (temperature 0.8, speed 1.0, seed 0), read live
      2026-06-01. Python 3.10 only (wrapper hardcoding); multilingual (23-language)
      variant available via the stack .env.

  - id: chatterbox-fast
    name: Chatterbox Fast (streaming)
    description: >
      Custom streaming front-end for Chatterbox-Turbo: an adaptive buffer-ratchet
      chunker emits the first sentence immediately (~0.5s time-to-first-audio vs
      ~5s one-shot), then ratchets chunk size up on the model's realtime headroom.
      Single-stream interactive. Same Turbo voice + inline paralinguistic tags as
      `chatterbox`; the win is sub-second start for streaming consumers.
    category: tts
    version: 1
    status: ready
    host: irv-ml1
    lifecycle:
      stack: chatterbox-fast
      vram_gb: 6
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8197/tts
    method: POST
    content_type: application/json
    streamable: true
    model:
      id: ResembleAI/chatterbox-turbo
      revision: null
      image: local/chatterbox-fast:v1
    section_groups:
      - id: basic
        label: Text & voice
      - id: sampling
        label: Sampling
    fields:
      - name: text
        type: textarea
        label: Text (with optional [tags])
        section: basic
        required: true
        max_length: 5000
        description: >
          Inline paralinguistic tags honored by Turbo ([laugh] [whispers] [sigh]
          etc.). Adaptive-chunking splits on sentence boundaries; a very long
          comma-less sentence may show a faint join seam.
      - name: voice
        type: select
        label: Voice
        section: basic
        default: glados_25s
        source_url: http://10.100.79.3:8197/voices
        source_jsonpath: $.voices[*]
        description: >
          Predefined voice (a *.wav stem under /worktank/chatterbox/reference_audio,
          shared with the chatterbox stack). Live-enumerated via /voices. Clone
          references are passable as an absolute path via the API (not listed here).
      - name: temperature
        type: slider
        section: sampling
        min: 0.05
        max: 2.0
        step: 0.05
        default: 0.8
        description: Sampling temperature; lower = steadier. Turbo default 0.8.
      - name: top_p
        type: slider
        section: sampling
        min: 0.0
        max: 1.0
        step: 0.05
        default: 0.95
        description: Nucleus sampling cutoff. Spike-validated default 0.95.
      - name: top_k
        type: number
        section: sampling
        required: false
        default: 1000
        description: Top-k sampling cutoff. Spike-validated default 1000.
      - name: repetition_penalty
        type: slider
        section: sampling
        min: 1.0
        max: 2.0
        step: 0.05
        default: 1.2
        description: Penalize token repetition. Spike-validated default 1.2.
      - name: seed
        type: number
        section: sampling
        required: false
        default: 0
        description: >
          0 = random. A fixed seed repeats a one-shot take (verified byte-exact).
          Streaming output is NOT reproducible — adaptive-chunk boundaries depend
          on the live-measured realtime factor.
      - name: format
        type: select
        section: basic
        options: [pcm, wav]
        default: pcm
        description: >
          pcm = raw s16le @ 24 kHz (lowest latency, for API consumers); wav adds a
          header. The streaming-audition UI forces wav for the browser <audio>.
    response:
      type: audio
      mime_from_field: format
    reproducibility:
      seedable: true
      deterministic: false
      seed_field: seed
      notes: >
        One-shot (non-streaming) output is reproducible for a fixed seed + params
        (verified byte-identical 2026-06-02). STREAMING output is NOT — adaptive-
        chunk boundaries depend on live-measured RTF (wall-clock), so splits vary
        run to run; streamable auditions are ephemeral anyway (no Asset). PerTh
        watermark is applied unconditionally (Resemble policy).
    estimated_latency:
      cold_start_s: 16
      warm_per_unit: "~0.5s TTFB streaming, ~3.9× realtime (A6000)"
    license: MIT
    notes: |
      Streaming front-end built in-house (eshpfi stacks/chatterbox-fast/): FastAPI
      + ChatterboxTurboTTS + an adaptive buffer-ratchet scheduler. Deployed beside
      the buffered `chatterbox` (:8196), which is unchanged. Defaults sourced from
      the 2026-06-02 spike (temperature 0.8 / top_p 0.95 / top_k 1000 /
      repetition_penalty 1.2). exaggeration / cfg_weight are deliberately omitted —
      Turbo ignores them. streamable:true routes the UI to an ephemeral audition
      (progressive <audio>, no Asset/library entry); re-run on `chatterbox` to keep
      output. Browser audition verified 2026-06-02 (progressive playback confirmed
      end-to-end) — promoted to ready.

  - id: index-tts
    name: IndexTTS-2
    description: >
      Bilibili's emotion-controllable zero-shot TTS. Disentangled emotion control
      (timbre from one reference, emotion from another). 22050 Hz fixed output.
    category: tts
    version: 1
    status: down
    host: irv-ml1
    lifecycle:
      stack: index-tts
      vram_gb: 6
      gpu_device_id: null
    endpoint: http://10.100.79.3:8192/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: IndexTeam/IndexTTS-2
      revision: null
      image: local/index-tts:v2
    fields:
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: select
        label: Speaker Voice
        description: <name>.wav in /worktank/index-tts/voices/. 5–30s clean clips.
      - name: response_format
        type: select
        options: [wav]
        default: wav
        description: 22050 Hz PCM_16 mono only; no negotiation.
      - name: stream
        type: bool
        default: false
        description: >
          Segment-level streaming (~120 tokens). Streaming WAV uses placeholder
          data-length (0xFFFFFFFF); browsers fine, strict parsers may complain.
      - name: emotion_voice
        type: select
        label: Emotion Reference Voice
        optional: true
        description: <name>.wav in /worktank/index-tts/emotions/.
      - name: emotion_vector
        type: json
        label: Emotion Vector
        optional: true
        description: >
          8 floats [happy, angry, sad, afraid, disgusted, melancholic, surprised, calm],
          each 0.0–1.0.
      - name: emotion_text
        type: textarea
        label: Emotion Description (free text)
        optional: true
      - name: emotion_alpha
        type: slider
        min: 0.0
        max: 1.0
        default: 1.0
        label: Emotion Strength
    response:
      type: audio
      mime: audio/wav
    reproducibility:
      seedable: false
      deterministic: true
      notes: >
        No seed. 22050 Hz hardcoded — resample in caller if 24/48 kHz needed.
        Precedence if multiple emotion sources: emotion_voice > vector > text.
    estimated_latency:
      cold_start_s: 5
      warm_per_unit: "segment-latency streaming"
    license: "Custom Bilibili (free at small scale; commercial tier 100M MAU)"
    notes: |
      Three-way mutual-exclusion among emotion_voice / emotion_vector / emotion_text;
      precedence as above. UI should expose this as a single picker.

  - id: omnivoice
    name: OmniVoice
    description: >
      k2-fsa zero-shot, massively-multilingual (600+ language) voice-cloning TTS
      (diffusion-LM, RTF ~0.025). Apache-2.0. Behind our own FastAPI wrapper
      (stacks/omnivoice/app.py); voices are the reused chatterbox reference clips.
    category: tts
    version: 2
    status: ready
    host: irv-ml1
    lifecycle:
      stack: omnivoice
      vram_gb: 6
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8199/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: k2-fsa/OmniVoice
      revision: null
      image: local/omnivoice:latest
    fields:
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      # Voice source — at least one of voice (clone) / instruct (design) is required.
      - name: voice
        type: select
        label: Speaker Voice (clone)
        optional: true
        source_url: http://10.100.79.3:8199/v1/audio/voices
        source_jsonpath: $.voices[*]
        description: >
          Zero-shot clone target — a reference clip in /worktank/omnivoice/voices/
          (reused chatterbox voices; 33 at deploy). Omit to design a voice via
          instruct instead. Live list at /v1/audio/voices.
      - name: instruct
        type: text
        label: Voice Design (instruct)
        optional: true
        source_url: http://10.100.79.3:8199/v1/audio/instruct-items
        source_jsonpath: $.instruct_items[*]
        description: >
          Voice DESIGN — a comma-separated list of CONTROLLED attribute tags (not
          free prose), e.g. "british accent, elderly, male, low pitch". Valid tags
          (gender/age/pitch/accent/whisper) at /v1/audio/instruct-items. Use instead
          of, or together with, a clone voice.
      - name: language
        type: select
        label: Language
        optional: true
        default: Auto
        source_url: http://10.100.79.3:8199/v1/audio/languages
        source_jsonpath: $.languages[*]
        description: "Auto-detects when left as Auto; 600+ languages supported."
      - name: speed
        type: slider
        label: Speed
        optional: true
        min: 0.5
        max: 1.5
        default: 1.0
        description: "1.0 = normal; >1 faster, <1 slower. Ignored if duration is set."
      - name: duration
        type: number
        label: Duration (seconds)
        optional: true
        description: "Fixed output length in seconds; overrides speed when set."
      - name: num_step
        type: slider
        label: Inference Steps
        optional: true
        min: 4
        max: 64
        default: 32
        description: "Diffusion steps. Lower = faster, higher = better quality."
      - name: guidance_scale
        type: slider
        label: Guidance Scale (CFG)
        optional: true
        min: 0.0
        max: 4.0
        default: 2.0
      - name: denoise
        type: bool
        label: Denoise
        optional: true
        default: true
      - name: preprocess_prompt
        type: bool
        label: Preprocess Prompt
        optional: true
        default: true
        description: "Silence-trim + punctuate the reference (clone mode)."
      - name: postprocess_output
        type: bool
        label: Postprocess Output
        optional: true
        default: true
        description: "Remove long silences from the generated audio."
      - name: generation_overrides
        type: json
        label: Advanced (GenerationConfig)
        optional: true
        description: >
          Expert OmniVoiceGenerationConfig overrides as a JSON object — keys:
          t_shift (0.1), layer_penalty_factor (5.0), position_temperature (5.0),
          class_temperature (0.0), audio_chunk_duration (15.0),
          audio_chunk_threshold (30.0). Unknown keys ignored.
      - name: response_format
        type: select
        options: [wav]
        default: wav
        description: 24000 Hz PCM_16 mono only; no negotiation.
    response:
      type: audio
      mime: audio/wav
    reproducibility:
      seedable: false
      deterministic: false
      notes: >
        Diffusion-LM, temperature/denoise sampled — not byte-exact, no seed exposed.
        Output 24000 Hz PCM_16 mono. Voice = a cloned reference clip (clone prompt
        precomputed per voice at startup; Whisper auto-transcribes the reference).
    estimated_latency:
      cold_start_s: 600
      warm_per_unit: "full-utterance (no streaming)"
    license: "Apache-2.0"
    notes: |
      Two voice sources, combinable: voice (clone a staged reference clip) and/or
      instruct (free-text voice DESIGN); at least one required. Full generation
      surface exposed — language (600+), speed, duration, num_step, guidance_scale,
      denoise, preprocess/postprocess — with expert GenerationConfig knobs (t_shift,
      layer/position/class temperature, audio_chunk_*) via the generation_overrides
      JSON field. No streaming. Voices reused from chatterbox /refs.

  - id: qwen3-tts
    name: Qwen3-TTS 1.7B
    description: >
      Alibaba's open English-first TTS (Apache 2.0). 10 languages, 97ms TTFB,
      instruction-driven emotion, voice cloning.
    category: tts
    version: 2
    status: down
    host: irv-ml1
    lifecycle:
      stack: qwen3-tts
      vram_gb: 16
      gpu_device_id: null
    endpoint: http://10.100.79.3:8191/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: Qwen/Qwen3-TTS-12Hz-1.7B
      revision: null
      image: local/qwen3-tts:v2
    fields:
      - name: model
        type: select
        options: [Qwen/Qwen3-TTS-12Hz-1.7B, Qwen/Qwen3-TTS-12Hz-0.6B-Base]
        default: Qwen/Qwen3-TTS-12Hz-1.7B
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: select
        label: Voice
        source_url: http://10.100.79.3:8191/v1/voices
        source_jsonpath: $.voices[*].id
        default: vivian
        options:
          - vivian
          - serena
          - uncle_fu
          - ryan
          - aiden
          - ono_anna
          - sohee
          - eric
          - dylan
          - alloy
          - echo
          - fable
          - nova
          - onyx
          - shimmer
        description: >
          Wrapper ships 9 Qwen presets (vivian/serena/uncle_fu/ryan/aiden/
          ono_anna/sohee/eric/dylan) + 6 OpenAI-compat aliases (alloy/echo/
          fable/nova/onyx/shimmer) — verified live 2026-05-12 against
          /v1/voices. Default `vivian` matches the wrapper's OpenAPI default
          (case-normalized; the Qwen3-TTS blog cites "Vivian" with caps).
          For cloning: clone:<name> where <name> is a profile dir under
          /worktank/qwen3-tts/voices/profiles/. Create via /voice-studio web
          UI or manually (meta.json + reference.wav). The 17-preset list
          cited in the Qwen3-TTS upstream blog (Cherry/Ethan/Chelsie/...)
          differs from what this wrapper exposes — the wrapper is the
          source of truth.
      - name: instructions
        type: textarea
        label: Emotion/Style Instructions
        optional: true
        description: >
          Natural-language directive (e.g. "speak with cold contempt").
          English instructions verified working ~2026-04.
      - name: response_format
        type: select
        options: [wav, mp3, pcm]
        default: wav
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: false
      deterministic: true
    estimated_latency:
      cold_start_s: 5
      warm_per_unit: "~97ms TTFB"
    license: Apache-2.0
    notes: |
      Voice cloning shape differs from CosyVoice: profile-based, not voice-id-based.

  - id: csm-expressiva
    name: CSM Expressiva (whisper)
    description: >
      Experimental LoRA fine-tune of Sesame's CSM-1B (Conversational Speech Model)
      on Expresso's whispering voice — WHISPER-style speech only, trained speaker
      = 4. Custom thin server (no upstream CSM server image exists). Autoregressive,
      so ~RTF 1.8 on the A6000 — good for offline/clip generation, not live
      streaming. License cc-by-nc-4.0 (non-commercial). No output watermark.
    category: tts
    version: 1
    status: experimental
    host: irv-ml1
    lifecycle:
      stack: csm-expressiva
      vram_gb: 4
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8198/tts
    method: POST
    content_type: application/json
    model:
      id: senstella/csm-expressiva-1b
      revision: null
      image: local/csm-expressiva:v1
    section_groups:
      - id: basic
        label: Text
      - id: sampling
        label: Sampling
    fields:
      - name: text
        type: textarea
        label: Text
        section: basic
        required: true
        max_length: 2000
        description: >
          Text to synthesize as a whisper. CSM is conversational — short
          utterances work best. No inline paralinguistic tag support.
      - name: speaker
        type: number
        section: basic
        required: false
        default: 4
        description: >
          Speaker id. The fine-tune only trained speaker 4 (Expresso whisper);
          leave at 4 — other ids fall back to off-distribution base-CSM behavior.
      - name: max_audio_length_ms
        type: number
        section: sampling
        required: false
        default: 20000
        description: Hard cap on generated audio length (ms); generation stops early on EOS.
      - name: temperature
        type: slider
        section: sampling
        min: 0.05
        max: 1.5
        step: 0.05
        default: 0.8
        description: Sampling temperature. Repo (csm-mlx) example default 0.8.
      - name: topk
        type: number
        section: sampling
        required: false
        default: 50
        description: Top-k sampling cutoff. Repo example default 50.
    response:
      type: audio
      mime: audio/wav
    reproducibility:
      seedable: false
      deterministic: false
      notes: >
        No seed field exposed; temperature-sampled, so takes vary run to run.
    estimated_latency:
      cold_start_s: 30
      warm_per_unit: "~RTF 1.8 (≈9s compute per ≈5s of audio on the A6000)"
    license: cc-by-nc-4.0
    notes: |
      Whisper-only experimental fine-tune. Custom thin FastAPI wrapper around the
      vendored Sesame csm generator (watermark + silentcipher stripped; ungated
      Llama-3.2 tokenizer mirror). Runs eager — CSM has no torch.compile hot path,
      so the compiled image gained nothing. NOT behind the LiteLLM gateway (custom
      /tts schema, not OpenAI /v1/audio/speech).

  - id: cosyvoice
    name: CosyVoice 3 (Multilingual)
    description: >
      FunAudioLLM's multilingual expressive TTS. 18+ Chinese dialects + 8 other langs.
      English prosody not ElevenLabs-grade — use Qwen3-TTS for English.
    category: tts
    version: 1
    status: down
    host: irv-ml1
    lifecycle:
      stack: cosyvoice
      vram_gb: 4
      gpu_device_id: null
    endpoint: http://10.100.79.3:8190/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: FunAudioLLM/Fun-CosyVoice3-0.5B-2512
      revision: null
      image: neosun/cosyvoice:v1.3.2
    fields:
      - name: model
        type: select
        options: [cosyvoice-v3, cosyvoice-v2]
        default: cosyvoice-v3
      - name: input
        type: textarea
        label: Text (with optional XML emotion tags)
        required: true
        max_length: 5000
        description: >
          For English: use XML tags <angry>, <sad>, <surprised>, <fast>, <whisper>, etc.
          NOT the instruct field — English instruct values get vocalized literally
          (upstream bug).
      - name: voice
        type: select
        label: Voice (cloned only — no presets)
        description: >
          Create via POST /v1/voices/create (multipart with reference audio ≤30s).
          Reference must be 16kHz mono ≤30s; longer = AssertionError.
      - name: response_format
        type: select
        options: [wav]
        default: wav
      - name: speed
        type: slider
        min: 0.5
        max: 2.0
        default: 1.0
    response:
      type: audio
      mime: audio/wav
    reproducibility:
      seedable: false
      deterministic: true
      notes: >
        Reference audio MUST be ≤30s (16kHz mono). instruct field broken for English.
    estimated_latency:
      cold_start_s: 5
      warm_per_unit: "~150ms TTFB streaming"
    license: Apache-2.0
    notes: |
      GOTCHA: instruct field is Chinese-context only — UI should hide it for English
      or surface a strong warning.

  - id: fish-s2
    name: Fish Audio S2-Pro
    description: >
      Fishaudio's richest-paralinguistic English TTS (15,000+ inline tags).
      Trained 10M+ hours, dual-AR. Released March 2026. Heavy: ~240s compile
      warmup on cold start, ~realtime throughput once warm.
    category: tts
    version: 6
    host: irv-ml1
    lifecycle:
      stack: fish-s2
      vram_gb: 24
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8195/v1/tts
    method: POST
    content_type: application/json
    model:
      id: fishaudio/s2-pro
      revision: null
      image: local/fish-s2:v1
    section_groups:
      - id: basic
        label: Text & output
      - id: sampling
        label: Sampling & latency
        hint: fish-speech ServeTTSRequest defaults. Lower temperature/top_p for steadier reads; latency=balanced for lower latency.
      - id: advanced
        label: Advanced & voice cloning
    fields:
      - name: text
        type: textarea
        label: Text (with optional [tags])
        section: basic
        required: true
        max_length: 5000
        description: >
          Inline tags: [laugh] [whispers] [super happy] [sigh] [excited]
          [heavy breathing] [angry] [sleepy] [crying] [surprise] ... (15,000+).
          Upstream best-practice: physical tags like [whispers] feel flat
          without emotional context — pair with an emotion tag. Example:
          `[excited] Welcome back! [whisper] I have a secret. [laugh]
          Just kidding.`
      - name: format
        type: select
        section: basic
        options: [wav, mp3, opus]
        default: wav
        description: >
          Output container (upstream also accepts raw `pcm`, omitted here).
          wav + mp3 verified live 2026-06-01.
      - name: temperature
        type: slider
        section: sampling
        min: 0.1
        max: 1.0
        step: 0.05
        default: 0.8
        description: Sampling temperature; lower = steadier. fish-speech default 0.8.
      - name: top_p
        type: slider
        section: sampling
        min: 0.1
        max: 1.0
        step: 0.05
        default: 0.8
        description: Nucleus sampling cutoff. fish-speech default 0.8.
      - name: repetition_penalty
        type: slider
        section: sampling
        min: 0.9
        max: 2.0
        step: 0.05
        default: 1.1
        description: >
          Penalizes repeats; raise if you hear stutters/loops. fish-speech
          default 1.1.
      - name: latency
        type: select
        section: sampling
        options: [normal, balanced]
        default: normal
        description: >
          `balanced` trades a little quality for lower latency — useful given
          S2-Pro's ~realtime throughput. fish-speech default `normal`.
      - name: seed
        type: number
        section: sampling
        required: false
        description: Omit for a random seed; a fixed integer repeats a take.
      - name: reference_id
        type: select
        label: Voice
        section: basic
        optional: true
        default: "Emily"
        options:
          - Abigail
          - Adrian
          - Alexander
          - Alice
          - Austin
          - Axel
          - Beatrice
          - Eleanor
          - Imogen
          - Connor
          - Cora
          - Elena
          - Eli
          - Emily
          - Everett
          - Gabriel
          - Gianna
          - glados
          - Henry
          - Ian
          - Jade
          - Jeremiah
          - Jordan
          - Julian
          - Layla
          - Leonardo
          - Michael
          - Miles
          - Olivia
          - Ryan
          - Taylor
          - Thomas
        description: >
          Voice = a staged clone reference picked by name (THE working voice
          path on this build; verified live 2026-06-01). 32 voices staged in
          /worktank/fish-s2/references/: 28 from the dia library + glados + 3
          British-female VCTK voices (Beatrice/Eleanor/Imogen). Default Emily.
          Subtle British (Southern-England) female accents: Imogen (VCTK p225),
          Eleanor (p228), Beatrice (p229) — consenting VCTK volunteers (CC BY
          4.0), NOT modeled on or representing any public figure. Other female:
          Abigail, Alice, Cora, Elena, Emily, Gianna, Jade, Layla, Olivia,
          glados. Resolves against the DIRECTORY references/<name>/ (audio +
          a same-basename <name>.lab transcript inside it) — NOT a flat
          references/<name>.wav, which fish-speech ignores (empty/flat-only
          dirs silently collapse every voice to the model default; root-caused
          + fixed 2026-06-01). Blank = model default/random speaker. To add:
          create references/<name>/ with <name>.wav (clean 5–15s) + <name>.lab
          (transcript) and add the name here; the deploy playbook's normalize
          step also mirrors any flat <name>.wav into this layout. (No /voices
          API → static list; a list-endpoint is the durable fix — see notes.)
      - name: references
        type: json
        label: Custom clone (inline base64)
        section: advanced
        optional: true
        togglable: true
        description: >
          Power-use: zero-shot clone from a reference NOT in the staged library
          — array of {audio: <base64-encoded WAV bytes>, text: "transcript"}.
          IMPORTANT: `audio` must be base64 bytes, NOT a filesystem path (a path
          returns HTTP 500 on this build). For staged voices use `reference_id`.
      - name: max_new_tokens
        type: number
        section: advanced
        required: false
        default: 1024
        description: Max audio tokens per chunk. fish-speech default 1024.
      - name: chunk_length
        type: slider
        section: advanced
        min: 100
        max: 1000
        step: 10
        default: 200
        description: Target chunk length in chars when splitting. fish-speech default 200.
      - name: normalize
        type: bool
        section: advanced
        default: true
        description: Text normalization (numbers, punctuation). fish-speech default on.
      - name: use_memory_cache
        type: select
        section: advanced
        options: ["off", "on"]
        default: "off"
        description: >
          `on` caches the reference encoding to speed up repeated calls with the
          same voice. fish-speech default off.
    response:
      type: audio
      mime_from_field: format
    reproducibility:
      seedable: true
      deterministic: false
      seed_field: seed
      notes: >
        /v1/tts exposes `seed` (omit = random); a fixed seed repeats a take.
        Temperature/top_p sampled → not guaranteed byte-exact. (Prior catalog
        claimed no seed support — corrected against the upstream ServeTTSRequest
        schema + a live generation 2026-06-01.)
    estimated_latency:
      cold_start_s: 240
      warm_per_unit: "~realtime throughput on the A6000 (heavy). latency=balanced trades quality for lower latency."
    license: "Research-only (Fishaudio terms; non-commercial)"
    license_warning: |
      Research/internal only. Not clear for commercial use. Same flag as
      Voxtral and SAO — UI must surface when output destined for products.
    notes: |
      NOT OpenAI-compatible: only /v1/tts (no /v1/audio/voices, /v1/models, or
      /openapi.json). Voice discovery is manual (host filesystem). Full control
      surface (temperature/top_p/repetition_penalty/latency/seed/format + the
      cloning fields) sourced from fish-speech's ServeTTSRequest schema and
      verified live 2026-06-01; prior catalog exposed only text + references.
      HEAVY: ~240s torch.compile warmup measured on cold start; budget the wait.

  - id: kyutai-tts
    name: Kyutai TTS 1.6B EN/FR
    description: >
      Kyutai's bilingual streaming TTS (1.6B, 2.5M hours). Heritage from Mimi codec
      + Moshi dialogue framework. OpenAI-compat HTTP wrapper over Rust streaming core.
    category: tts
    version: 2
    status: down
    host: irv-ml1
    lifecycle:
      stack: kyutai-tts
      vram_gb: 6
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8198/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: kyutai/tts-1.6b-en_fr
      revision: null
      image: local/kyutai-tts:v1
    fields:
      - name: model
        type: select
        options: [tts-1.6b-en_fr]
        default: tts-1.6b-en_fr
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: text
        label: Voice (HF-relative path)
        default: unmute-prod-website/default_voice.wav
        description: >
          Path relative to the `kyutai/tts-voices` HF repo, cached on
          irv-ml1 at /worktank/kyutai-tts/hf_cache. NillPointer wrapper
          does NOT expose /v1/audio/voices (404; only /health + POST
          /v1/audio/speech are wired) — voices are specified by path.
          Default `unmute-prod-website/default_voice.wav` is upstream's
          named-default. Categories available locally (count):
            vctk/* (212, CC BY 4.0 — commercial-safe)
            ears/* (306)
            voice-donations/* (291, mixed)
            expresso/* (103, CC BY-NC — research-only)
            cml-tts/* (70)
            unmute-prod-website/* (6, incl. default)
            alba-mackenna/* (4)
          For commercial paths, prefer a vctk/* voice (e.g.,
          `vctk/p225_023_mic1.wav`). Full repo:
          https://huggingface.co/kyutai/tts-voices
      - name: response_format
        type: select
        options: [wav, mp3, pcm]
        default: wav
      - name: stream
        type: bool
        default: false
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: false
      deterministic: true
      notes: >
        Wrapper adds Python overhead vs Kyutai's bare 220ms claim.
        Voice list is filesystem-discovered, not API-exposed — the
        NillPointer wrapper only wires /health + /v1/audio/speech.
    estimated_latency:
      cold_start_s: 4
      warm_per_unit: "~220–400ms TTFB (with wrapper)"
    license: "TBD (Kyutai — verify)"

  - id: vibevoice
    name: VibeVoice 1.5B (Long-form)
    description: >
      Microsoft's diffusion-based long-form multi-speaker TTS. Multi-minute scripts
      with speaker switching. Not for low-latency single-line use.
    category: tts
    version: 3
    status: down
    host: irv-ml1
    lifecycle:
      stack: vibevoice
      vram_gb: 20
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8194/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: microsoft/VibeVoice-1.5B
      revision: null
      image: local/vibevoice:v1
    fields:
      - name: model
        type: select
        options: [tts-1, tts-1-hd, vibevoice]
        default: tts-1
        description: >
          OpenAI-compatibility aliases all map to VibeVoice internally.
          Wire default is tts-1 (verified against /openapi.json).
      - name: input
        type: textarea
        label: "Text (or Speaker N: ... script)"
        required: true
        description: >
          Single-speaker: plain text. Multi-speaker: "Speaker 0: ...\nSpeaker 1: ..."
          via /v1/vibevoice/generate (extended endpoint).
      - name: voice
        type: select
        label: Voice
        default: en-Alice_woman
        description: >
          Default `en-Alice_woman` matches the upstream model card's stated
          default voice. Foot-gun: Alice has been observed to insert
          intro-BGM for openers like "Welcome to ..." — flip to
          en-Maya_woman if BGM artifacts are unwanted.
          OpenAI presets (compat aliases): alloy, echo, fable, onyx, nova, shimmer.
          VibeVoice presets: en-Alice_woman, en-Carter_man, en-Frank_man,
          en-Mary_woman_bgm, en-Maya_woman, in-Samuel_man, zh-Anchen_man_bgm,
          zh-Bowen_man, zh-Xinran_woman. The `_bgm`-suffixed voices were
          trained with background music intentionally — avoid for clean speech.
          Custom: drop WAV/MP3/FLAC/M4A into /worktank/vibevoice/voices/,
          restart container. Voice cloning training NOT released by Microsoft.
        options:
          - alloy
          - echo
          - fable
          - onyx
          - nova
          - shimmer
          - en-Alice_woman
          - en-Carter_man
          - en-Frank_man
          - en-Mary_woman_bgm
          - en-Maya_woman
          - in-Samuel_man
          - zh-Anchen_man_bgm
          - zh-Bowen_man
          - zh-Xinran_woman
      - name: response_format
        type: select
        options: [wav, mp3, opus, flac, pcm]
        default: mp3
        description: >
          Wire default is mp3 (verified /openapi.json). All five values
          confirmed by exercising the endpoint 2026-05-11; aac and m4a
          fail with 500 despite OpenAPI not enforcing an enum, so excluded.
      - name: stream
        type: bool
        default: false
        description: Single-shot endpoint doesn't stream; segment-level on multi-speaker.
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: false
      deterministic: true
    estimated_latency:
      cold_start_s: 8
      warm_per_unit: "30–60s first generation; segment-based after"
    license: MIT
    notes: |
      flash_attention_2 default; sdpa fallback.
      7B variant (rsxdalv/VibeVoice-Large, ~18GB) needs int8 for <10GB VRAM.

  - id: voxtral
    name: Voxtral 4B TTS
    description: >
      Mistral AI's 4B multilingual streaming TTS (CC BY-NC — research/internal only).
      8 languages, 70ms model latency, 9.7× realtime. Served via vLLM-Omni.
    category: tts
    version: 2
    status: down
    host: irv-ml1
    lifecycle:
      stack: voxtral
      vram_gb: 12
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8201/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: mistralai/Voxtral-4B-TTS-2603
      revision: null
      image: vllm/vllm-omni:v0.18.0
    fields:
      - name: model
        type: select
        options: [mistralai/Voxtral-4B-TTS-2603]
        default: mistralai/Voxtral-4B-TTS-2603
      - name: input
        type: textarea
        label: Text
        required: true
        max_length: 5000
      - name: voice
        type: select
        label: Voice
        source_url: http://10.100.79.3:8201/v1/audio/voices
        default: neutral_female
        options:
          - neutral_female
          - neutral_male
          - casual_female
          - casual_male
          - cheerful_female
          - ar_male
          - de_female
          - de_male
          - es_female
          - es_male
          - fr_female
          - fr_male
          - hi_female
          - hi_male
          - it_female
          - it_male
          - nl_female
          - nl_male
          - pt_female
          - pt_male
        description: >
          Native Voxtral presets — vLLM-Omni does NOT translate OpenAI
          aliases (verified 2026-05-12 live against /v1/audio/voices). Naming
          shape is `<register>_<gender>` for English (neutral_*, casual_*,
          cheerful_*) and `<lang>_<gender>` for non-English. Default
          `neutral_female` matches Mistral docs' stated default. The full
          list above was captured live from the running container; uploaded
          user voices appear in /v1/audio/voices under `uploaded_voices`.
      - name: response_format
        type: select
        options: [wav, mp3, pcm]
        default: wav
      - name: stream
        type: bool
        default: false
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: false
      deterministic: true
      notes: vLLM-Omni v0.18.0+ required (first Voxtral-aware release, 2026-03-29).
    estimated_latency:
      cold_start_s: 6
      warm_per_unit: "~70ms model latency, 9.7× realtime"
    license: "CC BY-NC (Mistral) — non-commercial only"
    license_warning: |
      NOT clear for commercial use. UI must surface this when output destined
      for products. Use Kokoro/Chatterbox/Fish/IndexTTS/Qwen3/CosyVoice for
      commercial paths.

  - id: parakeet
    name: Parakeet TDT (ASR)
    description: >
      NVIDIA Parakeet-TDT 0.6B (int8 ONNX, ~400MB) via sherpa-onnx.
      Transcription only.
    category: asr
    version: 1
    host: irv-ml1
    lifecycle:
      stack: parakeet
      vram_gb: 2
      gpu_device_id: null
    endpoint: http://10.100.79.3:8765/transcribe
    method: POST
    content_type: multipart/form-data
    model:
      id: nvidia/parakeet-tdt-v2-en-int8
      revision: null
      image: local/parakeet:sherpa-onnx-v2
    fields:
      - name: file
        type: file
        label: Audio File
        required: true
        accepted_types: [audio/wav, audio/mp3, audio/flac, audio/ogg]
    response:
      type: text
      output_field: text
    reproducibility:
      seedable: false
      deterministic: true
    estimated_latency:
      cold_start_s: 1
      warm_per_unit: "~realtime"
    license: "Proprietary (NVIDIA model) + Apache-2.0 (sherpa-onnx)"
    notes: |
      Aliased at /v1/audio/transcriptions for OpenAI compat.
      v3 model (25 languages) available via env-only change.

  - id: stable-audio-open
    name: Stable Audio Open 1.0 (SFX)
    description: >
      Stability AI's text-to-audio diffusion. SFX/foley/ambience only — NOT music.
      Max 47s clips. Seeded, fully deterministic.
    category: sfx
    version: 2
    host: irv-ml1
    lifecycle:
      stack: stable-audio-open
      vram_gb: 6
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8211/v1/audio/sfx
    method: POST
    content_type: application/json
    model:
      id: stabilityai/stable-audio-open-1.0
      revision: null
      image: local/stable-audio-open:v1
    fields:
      - name: prompt
        type: textarea
        label: Prompt
        required: true
        description: >
          Description of the audio to generate. Per diffusers Tips section,
          descriptive multi-element prompts produce better results than terse
          ones — `"melodic techno with a fast beat and synths, 128 BPM"` is
          stronger than `"techno"`. Other canonical examples from the model
          card: `"128 BPM tech house drum loop"`, `"the sound of a hammer
          hitting a wooden surface"`.
      - name: negative_prompt
        type: textarea
        label: Negative Prompt
        default: "low quality, average quality"
        description: >
          Official diffusers-docs recommendation. `"low quality, average
          quality"` (lowercase, comma-separated) per the Stable Audio
          pipeline's Tips section — was `"Low quality."` in v0, fixed in v1.
      - name: duration
        type: slider
        min: 0.5
        max: 47.0
        default: 10.0
        label: Duration (seconds)
      - name: steps
        type: slider
        min: 10
        max: 300
        default: 100
        label: Diffusion Steps
      - name: cfg_scale
        type: slider
        min: 0.0
        max: 20.0
        default: 7.0
        label: CFG Scale
      - name: seed
        type: number
        label: Seed
        optional: true
    response:
      type: audio
      mime: audio/wav
    reproducibility:
      seedable: true
      seed_field: seed
      deterministic: true
      notes: >
        Identical seed+prompt+steps+cfg = bit-identical bytes. Wrapper serializes
        concurrent requests via asyncio.Lock (StableAudioPipeline not reentrant).
    estimated_latency:
      cold_start_s: 3
      warm_per_unit: "~30–60s per 10s clip"
    license: "Stability AI Community (non-commercial)"
    license_warning: |
      Non-commercial only. Same flag as Voxtral for commercial paths.

  - id: ace-step
    name: ACE-Step 1.5 (Music)
    description: >
      Apache-2.0 hybrid diffusion+LLM music generation. Multi-minute lyric-aware
      songs with vocals + instrumentation.
    category: music
    version: 5
    host: irv-ml1
    lifecycle:
      stack: ace-step
      vram_gb: 16
      gpu_device_id: 1
    endpoint: http://10.100.79.3:8210/generate
    method: POST
    content_type: application/json
    model:
      id: ace-step/ACE-Step
      revision: main
      image: local/ace-step:v1
    section_groups:
      - id: basic
        label: Basic
        hint: "Prompt + lyrics + duration. The 80% case."
      - id: generation
        label: Generation
        hint: "Sampler steps, guidance, scheduler, seed."
      - id: conditioning
        label: Conditioning
        hint: "ERG flags, OSS schedule, guidance shape."
      - id: a2a
        label: "Audio → Audio"
        hint: "Generate from a reference clip."
      - id: lora
        label: LoRA
        hint: "Adapter weights."
      - id: output
        label: Output
        hint: "Format and device."
    fields:
      - name: prompt
        type: textarea
        label: Musical Prompt
        required: true
        section: basic
        description: >
          Style/mood/instrumentation as a comma-separated tag string.
          The upstream Gradio UI ships pre-filled with: `"funk, pop, soul,
          rock, melodic, guitar, drums, bass, keyboard, percussion, 105 BPM,
          energetic, upbeat, groovy, vibrant, dynamic"` — same shape works
          well here. Shorter examples: `"uplifting pop with synth leads"`,
          `"lo-fi hip-hop beat with vinyl crackle"`.
      - name: lyrics
        type: textarea
        label: Lyrics
        required: true
        default: ""
        section: basic
        description: >
          Required by the upstream Pydantic model (`lyrics: str`) but empty
          string is a valid value (instrumental). Default '' so the inference
          client always sends a string.
      - name: audio_duration
        type: slider
        min: 5.0
        max: 240.0
        default: 30.0
        label: Duration (seconds)
        section: basic
        description: >
          Upstream caps at 240s (the model's training horizon). Lower bound
          5s is our choice — upstream uses -1 as a "random duration" sentinel
          which is hostile UX for a slider. Default 30s also kept (upstream
          uses -1; explicit 30 is the better first-time-user experience).
      - name: infer_step
        type: number
        default: 60
        label: Inference Steps
        section: generation
        description: >
          Upstream Gradio default is 60 (matches benchmark numbers in the
          README). Lower values (20-30) are useful for "preview" passes;
          higher (80-100) marginal returns.
      - name: guidance_scale
        type: slider
        min: 0.0
        max: 30.0
        default: 15.0
        section: generation
      - name: scheduler_type
        type: select
        options: [euler, heun, pingpong]
        default: euler
        section: generation
        description: >
          Flow-matching scheduler. `euler` is the canonical default;
          `heun` trades 2x compute for higher-order accuracy; `pingpong`
          is a specialized noise-bouncing variant.
      - name: cfg_type
        type: select
        options: [apg, cfg, cfg_star]
        default: apg
        section: generation
        description: >
          Classifier-free guidance variant. Upstream Gradio default is `apg`
          (adaptive projected guidance); `cfg` is the standard SD-style
          formulation; `cfg_star` is an advanced alternative. See the
          upstream pipeline source for trade-offs.
      - name: omega_scale
        type: slider
        min: -10.0
        max: 30.0
        default: 10.0
        section: generation
        description: >
          Upstream technically allows [-100, 100] but values that wide are
          unusable as a slider. UI-bounded to [-10, 30] which covers the
          typical zone with headroom. Hit the API directly for extremes.
      - name: actual_seeds
        type: json
        label: Seeds (empty = random)
        default: []
        section: generation
        description: >
          Empty list = wrapper sends empty string to pipeline = pipeline
          picks a random seed per batch element. Explicit seeds (e.g. [42]
          or [42, 137, 9999]) for reproducibility.

          REPRODUCIBILITY GAP (queued for follow-up): the pipeline returns
          the chosen seed in its result dict, but our wrapper currently
          throws it away. Assets generated with the default `[]` cannot
          currently be regenerated. Workaround: set actual_seeds explicitly
          when reproducibility matters. Wrapper enhancement to surface
          random-resolved seeds via X-Actual-Seeds header + a catalog
          schema field for header→accessory capture is the planned fix.
      - name: guidance_interval
        type: slider
        min: 0.0
        max: 1.0
        default: 0.5
        section: conditioning
      - name: guidance_interval_decay
        type: slider
        min: 0.0
        max: 1.0
        default: 0.0
        section: conditioning
        description: >
          Upstream Gradio default is 0.0 (no decay). Catalog v4 had this at
          1.0 (full decay) — wrong; produced under-conditioned outputs.
      - name: min_guidance_scale
        type: slider
        min: 0.0
        max: 20.0
        default: 3.0
        section: conditioning
        description: >
          Upstream technically allows up to 200; UI-bounded to 20 (covers
          the typical zone). Hit the API directly for extremes.
      - name: use_erg_tag
        type: bool
        default: true
        section: conditioning
      - name: use_erg_lyric
        type: bool
        default: false
        section: conditioning
      - name: use_erg_diffusion
        type: bool
        default: true
        section: conditioning
      - name: oss_steps
        type: json
        default: []
        section: conditioning
      - name: guidance_scale_text
        type: slider
        min: 0.0
        max: 10.0
        default: 0.0
        section: conditioning
      - name: guidance_scale_lyric
        type: slider
        min: 0.0
        max: 10.0
        default: 0.0
        section: conditioning
      - name: audio2audio_enable
        type: bool
        default: false
        section: a2a
      - name: ref_audio_strength
        type: slider
        min: 0.0
        max: 1.0
        default: 0.5
        section: a2a
      - name: ref_audio_input
        type: text
        label: Reference Audio Path
        optional: true
        section: a2a
      - name: lora_name_or_path
        type: text
        label: LoRA Repo/Path
        optional: true
        section: lora
      - name: lora_weight
        type: slider
        min: -3.0
        max: 3.0
        default: 1.0
        section: lora
        description: >
          Negative weights are legitimate (apply the LoRA in inverse).
          Upstream Gradio range adopted verbatim.
      - name: audio_format
        type: select
        options: [wav, mp3, flac]
        default: wav
        section: output
      - name: bf16
        type: bool
        default: true
        section: output
      - name: torch_compile
        type: bool
        default: false
        section: output
      - name: device_id
        type: number
        default: 0
        label: GPU Device Index
        section: output
      - name: checkpoint_path
        type: text
        label: Checkpoint Path
        required: true
        default: /app/checkpoints
        section: output
        description: >
          Required by the upstream wrapper (`infer-api.py:29`, no default in
          the Pydantic model). The container always mounts checkpoints at
          /app/checkpoints, so the catalog default works out-of-the-box.
          Only override if you've mounted an alternate checkpoint path.
          Wrapper-side cleanup queued — once the upstream model defaults this,
          the catalog field will become optional or be dropped entirely.
    response:
      # As of wrapper version that ships with image local/ace-step:v1
      # post 2026-05-11, /generate streams audio bytes inline with
      # Content-Type set from the audio_format request field. The
      # in-container output_path is exposed via X-Output-Path header
      # for log correlation but is no longer load-bearing.
      type: audio
      mime_from_field: audio_format
    reproducibility:
      seedable: true
      deterministic: true
      notes: >
        actual_seeds parameter exposed; identical seeds + params = identical audio.
        Local infer-api.py patches upstream's broken 24-arg pipeline signature
        (v2: was 18 in upstream — caused crashes with audio_duration in `format`
        slot) AND inline-streams the generated audio bytes (v4: was returning a
        JSON path reference to a file inside the container, which was
        unreachable from outside).

        REPRODUCIBILITY GAP (v5): default `actual_seeds: []` triggers random
        seed selection inside the pipeline. The chosen seed IS available in
        the pipeline's return dict (`actual_seeds` key) but our wrapper
        doesn't capture or surface it — so default-defaulted assets cannot be
        regenerated bit-exact. Set actual_seeds explicitly when reproducibility
        is required. Wrapper enhancement to surface chosen seeds via response
        header + a catalog schema for header→accessory capture is queued.
    estimated_latency:
      cold_start_s: 30
      warm_per_unit: "~10–60s depending on audio_duration + infer_step"
    license: Apache-2.0
    notes: |
      27-field surface — UI must do progressive disclosure (basic/advanced).
      Gradio UI fallback: docker exec -it ace-step python3 acestep/gui.py.

  - id: comfyui
    name: ComfyUI (workflow engine — catalog-deferred)
    description: >
      Node-based diffusion workflow engine. Native API is workflow JSON DAG, not
      form fields. Catalog-deferred until per-asset-type workflow templates are
      defined and a wrapper maps form inputs to template node parameters.
    category: image
    status: catalog-deferred
    version: 1
    host: irv-ml1
    endpoint: http://10.100.79.3:8188
    method: WebSocket + POST
    content_type: application/json
    model:
      id: "various (SD, SDXL, Flux, ControlNet, LoRA, upscalers)"
      revision: null
      image: mmartial/comfyui-nvidia-docker:ubuntu24_cuda12.8-20260312
    response:
      type: image
      mime: image/png
    reproducibility:
      seedable: true
      deterministic: true
      notes: >
        Seed is a workflow node parameter. Reproducibility requires persisting
        full workflow JSON + seed alongside the asset.
    license: GPL-3.0
    notes: |
      To bring into the catalog: define workflow templates per asset-type
      (portrait, landscape, sfx-thumbnail, ...), build a wrapper that does
      form-field-to-node-parameter substitution, submit via POST /prompt, poll
      /history. Until then, expose ComfyUI as an external link in the UI.
      User state at /worktank/comfyui/basedir/.

  # Dia2 runs as TWO fixed-model instances: the devnen wrapper is
  # single-model and IGNORES the OpenAI `model` field (serves whatever
  # checkpoint is loaded), so the only way to offer both Dia2 models to
  # asset-engine as real per-request choices is one endpoint per model.
  # Both live in the `dia` stack (one compose, two services); `compose up`
  # starts both. The legacy Dia 1.6B entry was retired 2026-05-31 in favor
  # of these — drop the old `dia` tile when re-vendoring.
  - id: dia2-2b
    name: Dia2 2B (High Quality)
    description: >
      Nari Labs' Dia2 dialogue TTS, 2B high-quality checkpoint — multi-speaker
      turn-taking in one pass with [S1]/[S2] speaker tags and nonverbals
      (laughs)/(coughs)/(sighs). 24 kHz. Served via devnen/Dia-TTS-Server.
    category: tts
    version: 2
    status: ready
    host: irv-ml1
    lifecycle:
      stack: dia
      vram_gb: 8
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8200/tts
    method: POST
    content_type: application/json
    model:
      id: nari-labs/Dia2-2B
      revision: null
      image: local/dia:v2
    section_groups:
      - id: basic
        label: Text & voice
      - id: sampling
        label: Sampling
        hint: Wrapper-canonical Dia2 defaults; lower temperature/cfg for steadier output.
      - id: advanced
        label: Advanced
    fields:
      - name: text
        type: textarea
        label: Text ([S1]/[S2] dialogue + nonverbals)
        section: basic
        required: true
        max_length: 5000
        default: "[S1] Hello Dia2!"  # canonical: nari-labs/dia2 README usage example
        description: >
          [S1]/[S2] tags mark speaker turns; nonverbals like (laughs),
          (coughs), (sighs), (clears throat) go inline.
      - name: voice_mode
        type: select
        label: Voice mode
        section: basic
        options: [clone, single_s1, single_s2, dialogue]
        default: clone
        description: >
          Defaults to `clone` -> a stable predefined voice (clone_reference_filename
          below, default Abigail.wav). The unconditioned modes (single_s1/s2,
          dialogue) sample a RANDOM speaker each call (gender varies) — pick one
          only if you want that. NOTE: voice_mode=clone REQUIRES a real
          clone_reference_filename; an empty/"undefined" value 404s.
      - name: clone_reference_filename
        type: select
        label: Voice (clone reference)
        section: basic
        optional: true
        default: "Abigail.wav"
        source_url: http://10.100.79.3:8200/get_reference_files
        source_jsonpath: $[*]
        description: >
          Required when voice_mode=clone. Lists the reference_audio library —
          43 curated voices (singles + [S1]/[S2] dialogue pairs like
          Abigail_Taylor.wav) staged there from the image's /app/voices, plus
          any you upload. Pins voice/gender deterministically.
      - name: transcript
        type: textarea
        label: Reference transcript
        section: advanced
        optional: true
        description: >
          Optional transcript of a custom clone reference (overrides its local
          .txt sidecar). Leave blank for predefined voices.
      - name: output_format
        type: select
        section: basic
        options: [opus, wav]
        default: opus
        description: 24 kHz (Dia2).
      - name: cfg_scale
        type: slider
        section: sampling
        min: 1.0
        max: 5.0
        step: 0.1
        default: 3.0
        description: Classifier-free guidance scale (wrapper default 3.0).
      - name: temperature
        type: slider
        section: sampling
        min: 0.1
        max: 1.5
        step: 0.05
        default: 1.3
        description: Sampling temperature; lower = steadier (wrapper default 1.3).
      - name: top_p
        type: slider
        section: sampling
        min: 0.1
        max: 1.0
        step: 0.01
        default: 0.95
      - name: cfg_filter_top_k
        type: number
        section: sampling
        required: false
        default: 35
        description: Top-k filter for CFG guidance (1–100; wrapper default 35).
      - name: speed_factor
        type: slider
        section: sampling
        min: 0.5
        max: 2.0
        step: 0.01
        default: 0.94
        description: Playback speed (wrapper canonical default 0.94).
      - name: seed
        type: number
        section: sampling
        required: false
        default: -1
        description: -1 = random; a fixed integer repeats the same take.
      - name: split_text
        type: bool
        section: advanced
        default: true
        description: Auto-split long text into chunks.
      - name: chunk_size
        type: slider
        section: advanced
        min: 100
        max: 1000
        step: 10
        default: 300
        description: Target chunk length in chars when splitting (wrapper default 300).
      - name: max_tokens
        type: number
        section: advanced
        required: false
        description: Max audio tokens per chunk; omit to use the model's internal default.
    response:
      type: audio
      mime_from_field: output_format
    reproducibility:
      seedable: true
      deterministic: false
      notes: >
        Per the dia2 README, voices vary per generation unless anchored — set
        voice_mode=clone + a clone_reference_filename for a stable voice/gender.
        seed (-1=random) repeats a take but doesn't choose the voice;
        temperature-sampled, not byte-exact; raw bytes -> no seed_field. All
        field defaults are the wrapper's CustomTTSRequest Pydantic blessed values.
    estimated_latency:
      cold_start_s: 30
      warm_per_unit: "dialogue one-pass; ~realtime on the 3090"
    license: "Apache-2.0 (Dia2 weights); MIT (devnen wrapper)"
    notes: |
      Routes to the wrapper's richer /tts endpoint (full control surface:
      cfg_scale/temperature/top_p/cfg_filter_top_k/voice_mode/clone) rather than
      the OpenAI /v1/audio/speech. One of two fixed-model instances in the `dia`
      stack; sibling dia2-1b (:8202). `docker compose up` starts BOTH (~8 GB
      here; budget both if gating the whole stack). Image local/dia:v2 (see
      stacks/dia/dia2-image/). Verified end-to-end 2026-05-31.

  - id: dia2-1b
    name: Dia2 1B (Streaming)
    description: >
      Nari Labs' Dia2 dialogue TTS, 1B streaming checkpoint — same [S1]/[S2]
      dialogue + nonverbals as dia2-2b, lower latency / lighter weight. 24 kHz.
      Served via devnen/Dia-TTS-Server.
    category: tts
    version: 2
    status: ready
    host: irv-ml1
    lifecycle:
      stack: dia
      vram_gb: 5
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8202/tts
    method: POST
    content_type: application/json
    model:
      id: nari-labs/Dia2-1B
      revision: null
      image: local/dia:v2
    section_groups:
      - id: basic
        label: Text & voice
      - id: sampling
        label: Sampling
        hint: Wrapper-canonical Dia2 defaults; lower temperature/cfg for steadier output.
      - id: advanced
        label: Advanced
    fields:
      - name: text
        type: textarea
        label: Text ([S1]/[S2] dialogue + nonverbals)
        section: basic
        required: true
        max_length: 5000
        default: "[S1] Hello Dia2!"  # canonical: nari-labs/dia2 README usage example
        description: >
          [S1]/[S2] tags mark speaker turns; nonverbals like (laughs),
          (coughs), (sighs), (clears throat) go inline.
      - name: voice_mode
        type: select
        label: Voice mode
        section: basic
        options: [clone, single_s1, single_s2, dialogue]
        default: clone
        description: >
          Defaults to `clone` -> a stable predefined voice (clone_reference_filename
          below, default Abigail.wav). The unconditioned modes (single_s1/s2,
          dialogue) sample a RANDOM speaker each call (gender varies) — pick one
          only if you want that. NOTE: voice_mode=clone REQUIRES a real
          clone_reference_filename; an empty/"undefined" value 404s.
      - name: clone_reference_filename
        type: select
        label: Voice (clone reference)
        section: basic
        optional: true
        default: "Abigail.wav"
        source_url: http://10.100.79.3:8202/get_reference_files
        source_jsonpath: $[*]
        description: >
          Required when voice_mode=clone. Lists the reference_audio library —
          43 curated voices (singles + [S1]/[S2] dialogue pairs like
          Abigail_Taylor.wav) staged there from the image's /app/voices, plus
          any you upload. Pins voice/gender deterministically.
      - name: transcript
        type: textarea
        label: Reference transcript
        section: advanced
        optional: true
        description: >
          Optional transcript of a custom clone reference (overrides its local
          .txt sidecar). Leave blank for predefined voices.
      - name: output_format
        type: select
        section: basic
        options: [opus, wav]
        default: opus
        description: 24 kHz (Dia2).
      - name: cfg_scale
        type: slider
        section: sampling
        min: 1.0
        max: 5.0
        step: 0.1
        default: 3.0
        description: Classifier-free guidance scale (wrapper default 3.0).
      - name: temperature
        type: slider
        section: sampling
        min: 0.1
        max: 1.5
        step: 0.05
        default: 1.3
        description: Sampling temperature; lower = steadier (wrapper default 1.3).
      - name: top_p
        type: slider
        section: sampling
        min: 0.1
        max: 1.0
        step: 0.01
        default: 0.95
      - name: cfg_filter_top_k
        type: number
        section: sampling
        required: false
        default: 35
        description: Top-k filter for CFG guidance (1–100; wrapper default 35).
      - name: speed_factor
        type: slider
        section: sampling
        min: 0.5
        max: 2.0
        step: 0.01
        default: 0.94
        description: Playback speed (wrapper canonical default 0.94).
      - name: seed
        type: number
        section: sampling
        required: false
        default: -1
        description: -1 = random; a fixed integer repeats the same take.
      - name: split_text
        type: bool
        section: advanced
        default: true
        description: Auto-split long text into chunks.
      - name: chunk_size
        type: slider
        section: advanced
        min: 100
        max: 1000
        step: 10
        default: 300
        description: Target chunk length in chars when splitting (wrapper default 300).
      - name: max_tokens
        type: number
        section: advanced
        required: false
        description: Max audio tokens per chunk; omit to use the model's internal default.
    response:
      type: audio
      mime_from_field: output_format
    reproducibility:
      seedable: true
      deterministic: false
      notes: >
        Same story as dia2-2b: voices vary per generation unless anchored
        (voice_mode=clone + clone_reference_filename); temperature-sampled,
        not byte-exact; raw bytes -> no seed_field. Defaults are the wrapper's
        CustomTTSRequest Pydantic blessed values.
    estimated_latency:
      cold_start_s: 30
      warm_per_unit: "streaming; lower latency than dia2-2b"
    license: "Apache-2.0 (Dia2 weights); MIT (devnen wrapper)"
    notes: |
      Routes to the wrapper's richer /tts endpoint (same control surface as
      dia2-2b). Sibling of dia2-2b in the same `dia` stack (see its notes for
      the single-model-per-endpoint rationale + both-start-together caveat).
      Image local/dia:v2. Verified end-to-end 2026-05-31.

  - id: zonos
    name: Zonos-v0.1 TTS
    description: >
      Zyphra's expressive multilingual open-weight TTS (Apache-2.0) — 44 kHz,
      zero-shot voice cloning from a 10–30s clip, and an 8-axis emotion
      vector plus pitch/rate/fmax conditioning. First genuinely seedable TTS
      in the fleet: the adapter returns the seed so regenerate/fork can pin it.
    category: tts
    version: 1
    status: down
    host: irv-ml1
    lifecycle:
      stack: zonos
      # Adapter alone (~6 GB). NOTE: the zonos stack currently ships TWO
      # services (the Gradio eval UI + this adapter); if orchestration does a
      # whole-stack `compose up` it loads the model twice (~12 GB). Target the
      # zonos-api service, or drop the Gradio service at promotion.
      vram_gb: 6
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8203/v1/audio/speech
    method: POST
    content_type: application/json
    model:
      id: Zyphra/Zonos-v0.1-transformer
      revision: null
      image: local/zonos-api:v1
    section_groups:
      - id: text
        label: Text
      - id: voice
        label: Voice
      - id: expression
        label: Expression
        hint: Conditioning knobs; leave emotion blank to keep it unconditional.
      - id: generation
        label: Generation
    fields:
      - name: input
        type: textarea
        label: Text to synthesize
        section: text
        required: true
        max_length: 500
        default: "Zonos uses eSpeak for text to phoneme conversion!"  # canonical: upstream gradio_interface.py default
        description: eSpeak-phonemized. Upstream Gradio caps the box at ~500 chars.
      - name: language
        type: select
        label: Language code
        section: text
        default: en-us
        # eSpeak code list is large; these are the documented multilingual
        # set. The adapter accepts any code in zonos.supported_language_codes.
        options: [en-us, ja, cmn, fr-fr, de]
        description: Any eSpeak code Zonos supports; 400 on an unknown code.
      - name: voice
        type: select
        label: Clone voice (optional)
        section: voice
        optional: true
        source_url: http://10.100.79.3:8203/v1/audio/voices
        source_jsonpath: $.voices[*]
        description: >
          Reference clip filename under /worktank/zonos/voices/ (10–30s clean
          WAV). Omit for Zonos's default unconditioned voice.
      - name: response_format
        type: select
        section: generation
        options: [wav, flac]
        default: wav
        description: 44.1 kHz. Both lossless (libsndfile); no mp3/opus in v1.
      - name: emotion
        type: json
        label: Emotion vector (optional)
        section: expression
        optional: true
        description: >
          8 floats 0.0–1.0 [happy, sad, disgust, fear, surprise, anger, other,
          neutral]. Omit to leave emotion unconditional (the Gradio default;
          upstream warns the sliders are finicky and can destabilize output).
      - name: speaking_rate
        type: slider
        section: expression
        min: 5.0
        max: 30.0
        step: 0.5
        default: 15.0
      - name: pitch_std
        type: slider
        label: Pitch std
        section: expression
        min: 0.0
        max: 300.0
        step: 1
        default: 45.0
      - name: fmax
        type: slider
        label: Fmax (Hz)
        section: expression
        min: 0
        max: 24000
        step: 1
        default: 24000
      - name: cfg_scale
        type: slider
        label: CFG scale
        section: generation
        min: 1.0
        max: 5.0
        step: 0.1
        default: 2.0
      - name: seed
        type: number
        section: generation
        optional: true
        description: Omit for a random seed; the seed used is returned in the envelope.
    response:
      # JSON envelope (like kokoro-captioned) so the seed rides back with the
      # audio. Adapter returns: {audio: <base64 in response_format>,
      # audio_format: "audio/wav"|"audio/flac", seed: <int>}.
      type: audio
      audio_field: audio
      audio_format_field: audio_format
    reproducibility:
      seedable: true
      deterministic: true
      seed_field: seed
      notes: >
        torch.manual_seed(seed); the adapter echoes the seed it used in the
        envelope's `seed` key. Same seed + params + GPU → byte-stable in
        practice; cross-GPU (3090 vs A6000) may differ at bf16. Wire shape
        is defined by the adapter (stacks/zonos/adapter/server.py) — verify
        against the live endpoint at deploy per CATALOG-CONTRACT.
    estimated_latency:
      cold_start_s: 30
      warm_per_unit: "~2x realtime on the 3090 (transformer variant)"
    license: Apache-2.0
    notes: |
      Served via a thin OpenAI-ish FastAPI adapter (stacks/zonos/adapter/) in
      front of the Zonos Python SDK — upstream ships only Gradio + SDK, no REST.
      Transformer variant only (mamba-ssm absent → no hybrid). Fields read from
      upstream gradio_interface.py (2026-05-31), the CATALOG-CONTRACT blessed
      source for defaults/ranges. Adapter not yet deployed/verified — flip to
      ready (or experimental) after the first successful generation through 8203.

  - id: zonos-gateway
    name: Zonos Gateway (expressive)
    description: >
      OpenAI-compatible streaming facade over the Zonos engine (kept stock),
      exposing Zonos's full expressive control surface: emotion directions
      (happy / sad / angry / surprised) plus a valence/arousal axis pair,
      classifier-free-guidance on emotion, accurate-vs-expressive mode,
      speaking-rate conditioning, quality-metric targets, and the full
      sampling stack — all reachable from named presets (neutral / warm /
      excited / sad / intense / whisper) that seed the dials before explicit
      overrides win. Streams s16le PCM (or a WAV wrapper) from
      /v1/audio/speech. The LiteLLM `ext-tts` alias points at this gateway.
    category: tts
    version: 1
    status: experimental
    host: irv-ml1
    lifecycle:
      stack: zonos-gateway
      vram_gb: 16
      gpu_device_id: 0
    endpoint: http://10.100.79.3:8890/v1/audio/speech
    method: POST
    content_type: application/json
    streamable: true
    model:
      id: Zyphra/ZONOS2
      revision: null
      image: local/zonos-gateway:0.1.0
    section_groups:
      - id: basic
        label: Text & voice
      - id: expression
        label: Expression
        hint: Emotion conditioning. A preset seeds these; explicit dials win.
      - id: prosody
        label: Prosody
        hint: Speaking-rate conditioning. Leave the enable toggles off for the model's native pacing.
      - id: quality
        label: Quality target
        hint: Advanced — raw metric targets (LUFS, silence, bandlimit) Zonos buckets internally.
      - id: sampling
        label: Sampling
      - id: output
        label: Output
    fields:
      - name: input
        type: textarea
        label: Text to synthesize
        section: basic
        required: true
        max_length: 5000
        description: >
          Text to speak. OpenAI-style `input` field; the gateway streams the
          synthesized audio back.
      - name: voice
        type: select
        label: Voice
        section: basic
        default: Cora
        source_url: http://10.100.79.3:8890/v1/voices
        source_jsonpath: $.voices[*].name
        description: >
          Predefined Zonos voice. Live-enumerated from /v1/voices so the list
          auto-syncs with the deployed voice pack (Cora is the default).
      - name: preset
        type: select
        label: Expressive preset
        section: expression
        required: false
        options: [neutral, warm, excited, sad, intense, whisper]
        default: neutral
        description: >
          Named expressive preset applied before explicit dials; any explicit
          emotion/prosody/quality dial you set overrides the preset's value.
      - name: emotion_enabled
        type: bool
        label: Enable emotion conditioning
        section: expression
        required: false
        default: false
        description: >
          Turn emotion conditioning on. Required for the emotion_* dials to
          bite — a preset that sets emotion turns this on for you.
      - name: emotion_valence
        type: slider
        label: Valence
        section: expression
        min: -1.0
        max: 1.0
        step: 0.05
        default: 0.0
        description: Pleasantness axis. -1 negative, +1 positive.
      - name: emotion_arousal
        type: slider
        label: Arousal
        section: expression
        min: -1.0
        max: 1.0
        step: 0.05
        default: 0.0
        description: Energy/activation axis. -1 calm, +1 excited.
      - name: emotion_strength
        type: slider
        label: Emotion strength
        section: expression
        min: 0.0
        max: 2.0
        step: 0.05
        default: 1.0
        description: Overall scale on the emotion direction. 1.0 = as specified.
      - name: emotion_cfg_scale
        type: slider
        label: Emotion CFG scale
        section: expression
        min: 1.0
        max: 3.0
        step: 0.1
        default: 1.0
        description: >
          Classifier-free-guidance on emotion. 1.0 = off; >1 amplifies
          expression.
      - name: emotion_sliders
        type: json
        label: Per-emotion weights (advanced)
        section: expression
        optional: true
        description: >
          Advanced — per-emotion weight dict {happy|sad|angry|surprised: -1..1};
          higher = stronger. Overrides the coarse valence/arousal directions
          with explicit per-emotion control. Omit to use valence/arousal.
      - name: accurate_mode
        type: bool
        label: Accurate mode
        section: expression
        required: false
        default: true
        description: >
          true = faithful to the reference voice; false = more
          expressive/looser.
      - name: speaking_rate_enabled
        type: bool
        label: Enable speaking-rate conditioning
        section: prosody
        required: false
        default: false
        description: >
          Turn speaking-rate conditioning on. Required for speed /
          speaking_rate / speaking_rate_bucket to take effect.
      - name: speed
        type: slider
        label: Speed (OpenAI-style)
        section: prosody
        min: 0.25
        max: 4.0
        step: 0.05
        optional: true
        description: >
          OpenAI-style rate multiplier. Mapped to speaking_rate when no
          explicit speaking_rate is given; auto-enables speaking-rate
          conditioning. Omit to leave pacing native.
      - name: speaking_rate
        type: slider
        label: Speaking rate (native)
        section: prosody
        min: 0.25
        max: 4.0
        step: 0.05
        optional: true
        description: >
          Native speaking-rate multiplier. Overrides speed if both are sent.
          Omit to leave pacing native.
      - name: speaking_rate_bucket
        type: slider
        label: Speaking-rate bucket
        section: prosody
        min: 0
        max: 7
        step: 1
        optional: true
        description: >
          Words/sec bucket index 0..7 (0 = 0-8 wps … 7 = 40+ wps). Coarser than
          speaking_rate. Omit to leave pacing native.
      - name: quality_enabled
        type: bool
        label: Enable quality-target conditioning
        section: quality
        required: false
        default: true
        description: >
          Advanced — turn quality-target conditioning on (on by default in
          Zonos). Gates quality_values.
      - name: quality_values
        type: json
        label: Quality metric targets (advanced)
        section: quality
        optional: true
        description: >
          Advanced — raw metric targets Zonos buckets internally, e.g.
          {lufs: -23, trailing_silence_s: 0.1}. Keys: lufs, estimated_snr,
          max_pause, estimated_bandlimit_hz, leading_silence_s,
          trailing_silence_s. Omit for Zonos's defaults.
      - name: temperature
        type: slider
        section: sampling
        min: 0.0
        max: 2.0
        step: 0.05
        default: 1.15
        description: Sampling temperature. Higher = more varied. Zonos default 1.15.
      - name: top_p
        type: slider
        label: Top-p
        section: sampling
        min: 0.0
        max: 1.0
        step: 0.05
        default: 0.0
        description: Nucleus sampling cutoff. 0.0 = off (Zonos default).
      - name: min_p
        type: slider
        label: Min-p
        section: sampling
        min: 0.0
        max: 1.0
        step: 0.01
        default: 0.18
        description: Min-p sampling floor. Zonos default 0.18.
      - name: topk
        type: number
        label: Top-k
        section: sampling
        required: false
        default: 106
        description: Top-k sampling cutoff. Zonos default 106.
      - name: seed
        type: number
        section: sampling
        optional: true
        description: >
          RNG seed for reproducible sampling. Omit for a random seed. Pins the
          sampler only; emotion/quality conditioning still varies subtly.
      - name: max_tokens
        type: number
        label: Max audio tokens
        section: sampling
        required: false
        max: 6144
        description: >
          Cap on generated audio tokens (upper bound; Zonos stops at
          end-of-speech). Omit to let Zonos decide.
      - name: response_format
        type: select
        label: Response format
        section: output
        options: [pcm, wav]
        default: pcm
        description: >
          pcm = raw s16le stream (lowest latency, for API consumers); wav adds
          a header. The stream-audition UI forces wav for the browser <audio>.
    response:
      type: audio
      mime_from_field: response_format
    reproducibility:
      seedable: true
      deterministic: false
      seed_field: seed
      notes: >
        Temperature-sampled; seed pins the sampler but emotion/quality
        conditioning still varies subtly run-to-run.
    estimated_latency:
      cold_start_s: 3
      warm_per_unit: "streaming; first audio in a couple seconds warm, then near-realtime on the 3090"
    license: Apache-2.0
    notes: |
      OpenAI-compatible streaming gateway (local/zonos-gateway:0.1.0) fronting a
      stock Zonos engine on the 3090 (irv-ml1 device 0). The LiteLLM `ext-tts`
      alias routes here. Fields mirror the gateway's /v1/dials schema (24 params;
      the CATALOG-CONTRACT blessed source for defaults/ranges) and /v1/voices.
      Deliberately omits repetition_window / repetition_penalty / codebooks — the
      wrapper rejects them and they are the "70s of silence" footgun. Presets seed
      the dials before explicit overrides win. New service (experimental) — flip to
      ready after the first verified generation + browser audition through 8890.

# Reproducibility audit — answers per service: (a) seedable, (b) model
# deterministic without seed, (c) image tag mutable (security/reproducibility risk).
reproducibility_audit:
  - service: kokoro
    seedable: false
    model_deterministic: true
    image_tag_mutable: true
    notes: ":v0.2.4-master is mutable; pin to digest for v3."
  - service: kokoro-captioned
    seedable: false
    model_deterministic: true
    image_tag_mutable: true
    notes: "Same image as kokoro proper; same mutability story. Response carries timestamps."
  - service: chatterbox
    seedable: true
    model_deterministic: false
    image_tag_mutable: true
    notes: "/tts exposes seed (0=random); temperature-sampled, not byte-exact. PerTh watermark unconditional (Resemble policy). image :latest is mutable — pin a digest/SHA for true repro."
  - service: chatterbox-fast
    seedable: true
    model_deterministic: false
    image_tag_mutable: true
    notes: "/tts exposes seed (0=random) — one-shot byte-exact for fixed seed+params (verified 2026-06-02); STREAMING non-reproducible (timing-dependent chunk boundaries), auditions ephemeral. PerTh watermark unconditional. image local/chatterbox-fast:v1 is tag-pinned + mutable — pin a digest for true repro."
  - service: csm-expressiva
    seedable: false
    model_deterministic: false
    image_tag_mutable: true
    notes: "No seed field exposed; temperature-sampled, takes vary run to run. No watermark (stripped). image local/csm-expressiva:v1 is tag-pinned + mutable — pin a digest for true repro. Runs eager (CSM has no torch.compile hot path)."
  - service: index-tts
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
    notes: "22050 Hz hardcoded — caller must resample."
  - service: omnivoice
    seedable: false
    model_deterministic: false
    image_tag_mutable: true
    notes: "Diffusion-LM, temperature/denoise sampled — not byte-exact, no seed exposed. 24000 Hz PCM_16 mono. image local/omnivoice:latest is mutable — pin a digest for true repro. Voices = reused chatterbox /refs clones (clone prompt precomputed per voice at startup)."
  - service: qwen3-tts
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
  - service: cosyvoice
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
    notes: "instruct field broken for English; XML tags only."
  - service: fish-s2
    seedable: true
    model_deterministic: false
    image_tag_mutable: false
    notes: "/v1/tts exposes seed (omit=random); temperature/top_p sampled, not byte-exact. Research-only license — non-commercial."
  - service: kyutai-tts
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
  - service: vibevoice
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
    notes: "Voice cloning training not released."
  - service: voxtral
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
    notes: "CC BY-NC — non-commercial."
  - service: parakeet
    seedable: false
    model_deterministic: true
    image_tag_mutable: false
  - service: stable-audio-open
    seedable: true
    model_deterministic: true
    image_tag_mutable: false
    notes: "Wrapper serializes concurrent requests (StableAudioPipeline not reentrant)."
  - service: ace-step
    seedable: true
    model_deterministic: true
    image_tag_mutable: false
    notes: "Local infer-api.py patches upstream's broken pipeline signature."
  - service: comfyui
    seedable: true
    model_deterministic: true
    image_tag_mutable: false
    notes: "Reproducibility requires persisting full workflow JSON + seed."
  - service: dia2-2b
    seedable: true
    model_deterministic: false
    image_tag_mutable: false
    notes: "Dia2 2B, temperature-sampled (seed gives consistent prosody, not byte-exact). local/dia:v2 (derived; upstream wrapper SHA not pinned)."
  - service: dia2-1b
    seedable: true
    model_deterministic: false
    image_tag_mutable: false
    notes: "Dia2 1B streaming; same determinism story as dia2-2b. local/dia:v2."
  - service: zonos
    seedable: true
    model_deterministic: true
    image_tag_mutable: false
    notes: "Adapter echoes the seed used (reproducibility.seed_field=seed). Byte-stable same-GPU; bf16 may drift cross-GPU. local/zonos-api:v1 built FROM local/zonos (pin ZONOS_SHA for true repro)."
  - service: zonos-gateway
    seedable: true
    model_deterministic: false
    image_tag_mutable: true
    notes: "Seed pins the sampler (reproducibility.seed_field=seed) but emotion/quality conditioning still varies subtly run-to-run — not byte-exact. Streaming (s16le PCM / WAV). Distinct from the `zonos` adapter: this is the OpenAI-compatible gateway on :8890 behind the LiteLLM `ext-tts` alias. image local/zonos-gateway:0.1.0 is tag-pinned + mutable — pin a digest for true repro."
