# Fish Audio S2-Pro — the most expressive open-source TTS as of
# 2026-04. 4B params, dual-AR architecture (Slow AR for semantic
# codebook + Fast AR for 9 residual codebooks), trained on 10M+ hours
# across 80+ languages. ~150 ms streaming TTFB on warm GPU.
#
# Why this stack alongside the existing TTS:
#   * Headline feature: 15,000+ paralinguistic / emotion tags via
#     natural language, e.g. [laugh] [whispers] [super happy] [sigh].
#     Chatterbox Turbo only has 9 fixed tags — Fish's vocabulary is
#     dramatically richer for any emotive use case.
#   * 91.61% paralinguistic win rate on EmergentTTS-Eval — currently
#     the leader on that benchmark.
#   * Voice cloning + native multi-speaker / multi-turn generation.
#   * MIT-style license (weights, training code, inference engine all
#     open).
#
# Image is built locally from upstream's repo via docker buildx
# git-context. Upstream ships a compose with `--profile server` for
# the API path; we adapt that to our `restart: unless-stopped`
# convention + bind-mount layout.
#
# All tunables live in .env — edit that, not this file.

services:
  fish-s2:
    image: local/fish-s2:${FISH_S2_TAG}
    build:
      context: https://github.com/fishaudio/fish-speech.git#${FISH_S2_SHA}
      # The REAL production Dockerfile is at docker/Dockerfile (per
      # upstream's compose.base.yml). The repo also ships a
      # `dockerfile.dev` at root which is a thin
      # `FROM ghcr.io/fishaudio/fish-speech:${VERSION}` wrapper meant
      # for dev iteration on top of a private base image — that path
      # 403s on anonymous pulls. Build from source via docker/Dockerfile
      # instead.
      dockerfile: docker/Dockerfile
      # Multi-stage Dockerfile — `server` stage installs start_server.sh
      # and exposes the API. The default last stage is `webui` (gradio
      # only), which crashloops silently because start_server.sh isn't
      # there. Target=server is what upstream's compose.yml does.
      target: server
      args:
        # Build args mirror upstream compose.base.yml defaults.
        # CUDA_VER 12.9 + UV_EXTRA cu129 = the CUDA 12.9 PyTorch wheels.
        # irv-ml1's driver (595.58.03 / CUDA 13.2 capable) is
        # backward-compatible with 12.9-built images.
        BACKEND: cuda
        CUDA_VER: "12.9.0"
        UV_EXTRA: cu129
        UV_VERSION: "0.8.15"
    container_name: fish-s2
    restart: unless-stopped
    runtime: nvidia
    ports:
      - "${FISH_S2_BIND:-0.0.0.0}:${FISH_S2_PORT}:8080"
    environment:
      - NVIDIA_VISIBLE_DEVICES=${FISH_S2_GPU_DEVICES:-1}
      - BACKEND=cuda
      # COMPILE=1 enables torch.compile — upstream claims ~10× speedup
      # on the autoregressive forward, at the cost of ~60 s warmup the
      # first time each input shape is seen. Worth the speedup; turn
      # off via .env if you hit a torch.compile bug on a future model
      # checkpoint.
      - COMPILE=${FISH_S2_COMPILE:-1}
      - API_PORT=8080
      # Model selection. Override Fish's Dockerfile defaults so we can
      # swap variants via .env without rebuilding. Both checkpoints are
      # pre-pulled by the deploy playbook into bind-mounted checkpoints/.
      #   s2-pro   — 4B class, ~17 GB VRAM, slow but highest quality
      #   s1-mini  — lighter, ~3-5× smaller model.pth, much faster
      - LLAMA_CHECKPOINT_PATH=checkpoints/${FISH_S2_MODEL:-s2-pro}
      - DECODER_CHECKPOINT_PATH=checkpoints/${FISH_S2_MODEL:-s2-pro}/codec.pth
      # Hugging Face cache for model weights — first start pulls
      # fishaudio/s2-pro (~9 GB BF16) into this dir.
      - HF_HOME=/app/hf_cache
    volumes:
      # Model checkpoints (auto-downloaded on first run, then cached).
      - ${FISH_S2_CHECKPOINT_DIR}:/app/checkpoints
      # Reference audio for voice cloning — drop ~5–15 s WAV clips here.
      - ${FISH_S2_REFERENCE_DIR}:/app/references
      # Persistent HF cache so model re-pull only happens on first deploy.
      - ${FISH_S2_CACHE_DIR}:/app/hf_cache
    # Pass --half to start_server.sh → enables fp16 inference on the
    # LLM half. Speeds up the autoregressive loop (the dominant cost
    # in TTFB). Also enables the streaming path's faster total time.
    # build_compile_args() echoes unknown args back to the exec line.
    command: ["--half"]
    healthcheck:
      # Fish ships /v1/health on the API server. python urllib instead
      # of curl because the upstream image is python-based and may not
      # carry curl. 127.0.0.1 explicit to dodge the IPv6-first
      # localhost trap we hit on chatterbox + news-digest.
      test: ["CMD-SHELL", "python3 -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/v1/health', timeout=5).status==200 else 1)\""]
      interval: 30s
      timeout: 10s
      retries: 3
      # First boot: torch.compile warmup + first-pull HF download +
      # checkpoint load. Generous deadline.
      start_period: 900s
    labels:
      - homepage.group=AI - Dormant
      - homepage.name=Fish Audio S2-Pro
      - homepage.icon=mdi-fish
      - homepage.description=Most expressive open-source TTS — 15k+ paralinguistic tags, voice cloning, 80+ languages (irv-ml1)
      - homepage.href=http://10.100.79.3:${FISH_S2_PORT}
