diff --git a/stacks/dia/.env.example b/stacks/dia/.env.example index 102d2bf..291d690 100644 --- a/stacks/dia/.env.example +++ b/stacks/dia/.env.example @@ -1,56 +1,41 @@ -# Dia / Dia2 stack tunables. Copy to `.env` on irv-ml1 before deploying. +# Dia2 stack tunables. Copy to `.env` on irv-ml1 before deploying. +# Two fixed-model instances (dia2-2b, dia2-1b) from the dia2-capable image. -# ── build pin ──────────────────────────────────────────────────────── -# SHA of devnen/Dia-TTS-Server. Use the FULL 40-char SHA — docker -# buildx's git source resolver doesn't accept short hashes. `main` -# works but is NOT reproducible; pin a SHA before you rely on this in -# anger (same churn lesson as chatterbox, whose `main` once broke on an -# upstream path restructure). Latest commits: -# https://github.com/devnen/Dia-TTS-Server/commits/main -DIA_SHA=main - -# Local image tag — bump when you change build context to force a -# fresh layer build. -DIA_TAG=v1 +# ── image ──────────────────────────────────────────────────────────── +# Built in two stages (see README + dia2-image/Dockerfile): +# upstream devnen wrapper -> local/dia:v1 +# + dia2 package & its missing deps -> local/dia:v2 (this tag) +# Bump when you change the derived build context. +DIA_TAG=v2 # ── network ────────────────────────────────────────────────────────── -# Host port. Container listens on 8003 internally. -# Reserved on irv-ml1: 8188 ComfyUI, 8190 CosyVoice, 8191 Qwen3-TTS, -# 8192 IndexTTS-2, 8193 Kokoro, 8194 VibeVoice, 8195 Fish-S2, -# 8196 Chatterbox, 8197 Voxtral, 8198 Kyutai, 8199 Zonos, 8765 Parakeet. -# 8200 picked here. -DIA_PORT=8200 +# Each container listens on 8003 internally. Reserved on irv-ml1: +# 8188 ComfyUI, 8190 CosyVoice, 8191 Qwen3-TTS, 8192 IndexTTS-2, +# 8193 Kokoro, 8194 VibeVoice, 8195 Fish-S2, 8196 Chatterbox, +# 8197 Voxtral, 8198 Kyutai, 8199 Zonos(gradio), 8200 Dia2-2B, +# 8201 csm, 8202 Dia2-1B, 8203 zonos-api, 8765 Parakeet. +DIA2_2B_PORT=8200 +DIA2_1B_PORT=8202 -# Bind address. 0.0.0.0 exposes on all interfaces (incl. WG tunnel +# Bind address. 0.0.0.0 exposes on all interfaces (incl. the WG tunnel # interface 10.100.79.3); 127.0.0.1 restricts to local-only. DIA_BIND=0.0.0.0 # ── runtime / GPU ──────────────────────────────────────────────────── -# Devices visible inside the container. "0" pins to the RTX 3090 -# (24 GB) — Dia at BF16 (~7 GB) fits comfortably. Use "1" for the -# A6000 if you'd rather keep the 3090 free. -DIA_GPU_DEVICES=0 +# Both Dia2 models are small enough to share the RTX 3090 (device 0). +# Set either to "1" to move it to the A6000 if the 3090 gets crowded. +DIA2_2B_GPU=0 +DIA2_1B_GPU=0 -# Model selection is config.yaml-driven in the wrapper (model.repo_id), -# not a plain env var. Default in-image model is Dia 1.6B. To pin a -# different default, mount a host config.yaml (see commented volume in -# compose.yaml) setting one of: -# nari-labs/Dia-1.6B — original, single/dual-speaker dialogue -# Dia2-1B — streaming, lower latency -# Dia2-2B — highest quality -# Or just switch live from the Web UI at http://:8200/ . +# ── per-instance config (pins model.repo_id) ───────────────────────── +# conf-convention path (lkraven-owned, no sudo). Each file is a full +# config.yaml with model.repo_id set to that instance's Dia2 model. +DIA2_2B_CONFIG=/opt/docker/conf/dia2-2b/config.yaml +DIA2_1B_CONFIG=/opt/docker/conf/dia2-1b/config.yaml -# ── persistent storage on the host ─────────────────────────────────── -# Reference audio dir for voice cloning / speaker prompts. Drop short -# reference WAVs here; the wrapper picks them up by filename. Included -# in restic (cloned voices need the original reference to recreate). +# ── persistent storage on the host (shared by both instances) ──────── +# Reference audio for cloning / speaker prompts (included in restic). DIA_REFERENCE_DIR=/worktank/dia/reference_audio - -# HuggingFace cache — Dia/Dia2 checkpoints land here on first start. -# Bind-mounted so they survive container recreate. Excluded from restic -# (regenerable from HF). +# HuggingFace cache — Dia2 checkpoints land here on first start. Shared +# cache is fine (keyed per repo). Excluded from restic (regenerable). DIA_CACHE_DIR=/worktank/dia/cache - -# Optional: host config.yaml to pin a default model + generation -# defaults. Leave commented in compose.yaml unless you want it. -# DIA_CONFIG=/worktank/dia/config.yaml diff --git a/stacks/dia/compose.yaml b/stacks/dia/compose.yaml index f14a625..e08a9da 100644 --- a/stacks/dia/compose.yaml +++ b/stacks/dia/compose.yaml @@ -1,67 +1,81 @@ -# Dia / Dia2 (Nari Labs' dialogue-focused TTS) served via -# devnen/Dia-TTS-Server — the same OpenAI-compat wrapper author as our -# chatterbox stack, with multi-model support for the Dia 2 family -# (Dia 1.6B / Dia2-1B / Dia2-2B), switchable from the Web UI. +# Dia2 (Nari Labs' dialogue-focused TTS) served via devnen/Dia-TTS-Server. # -# Why this stack exists alongside the other TTS: -# * DIALOGUE scenes with nonverbal cues — Dia generates multi-speaker -# turn-taking in a single pass with inline [S1]/[S2] speaker tags -# and nonverbals like (laughs), (coughs), (sighs). Purpose-built for -# the character-dialogue case skaldsong's storytelling hits, which -# the single-speaker engines (Fish/Index/Chatterbox) don't own. -# * Dia2 (released 2025-11-19) adds realtime streaming + a 2B -# high-quality checkpoint. -# * Apache-2.0 model weights; MIT wrapper; OpenAI-compat -# /v1/audio/speech so skaldsong can target it by base-URL. -# * ~7 GB VRAM at BF16 SafeTensors. +# Dialogue scenes none of the other bench TTS own cleanly: multi-speaker +# turn-taking in one pass with inline [S1]/[S2] tags + nonverbals +# (laughs)/(coughs)/(sighs). OpenAI-compat POST /v1/audio/speech. # -# Image is built locally from the upstream wrapper via docker buildx -# git-context (no source vendored on the host) — same pattern as -# chatterbox/fish-s2. +# This stack runs TWO fixed-model instances from the dia2-capable image +# (local/dia:v2 — built in two stages: the upstream devnen wrapper image +# local/dia:v1, then dia2-image/Dockerfile layers in the dia2 package + +# its missing runtime deps; see README). The wrapper serves whatever model +# is loaded and IGNORES per-request model selection, so the only way to +# offer multiple Dia2 models to asset-engine as real choices is one fixed +# instance per model — which is exactly this: +# * dia2-2b — Dia2 2B, highest quality (:8200, the primary dia port) +# * dia2-1b — Dia2 1B, streaming/faster (:8202) +# Each pins ONE model via its mounted config.yaml. Legacy Dia 1.6B retired +# (operator decision 2026-05-31). # # All tunables live in .env — edit that, not this file. services: - dia: + dia2-2b: image: local/dia:${DIA_TAG} - build: - # Single Dockerfile at repo root; GPU via NVIDIA Container Toolkit. - context: https://github.com/devnen/Dia-TTS-Server.git#${DIA_SHA} - dockerfile: Dockerfile - container_name: dia + container_name: dia2-2b restart: unless-stopped runtime: nvidia ports: - - "${DIA_BIND:-0.0.0.0}:${DIA_PORT}:8003" + - "${DIA_BIND:-0.0.0.0}:${DIA2_2B_PORT}:8003" environment: - - NVIDIA_VISIBLE_DEVICES=${DIA_GPU_DEVICES:-0} + - NVIDIA_VISIBLE_DEVICES=${DIA2_2B_GPU:-0} - NVIDIA_DRIVER_CAPABILITIES=compute,utility - # Speeds the first-boot HF download of the checkpoint. - HF_HUB_ENABLE_HF_TRANSFER=1 - HF_HOME=/app/hf_cache volumes: - ${DIA_REFERENCE_DIR}:/app/reference_audio - ${DIA_CACHE_DIR}:/app/hf_cache - # Model selection lives in the wrapper's config.yaml (model.repo_id): - # mount a host config to pin a default of Dia2-1B / Dia2-2B instead - # of the in-image default (Dia 1.6B). Otherwise switch live in the - # Web UI (selection may not survive a container recreate). - # - ${DIA_CONFIG}:/app/config.yaml + # Pins model.repo_id=nari-labs/Dia2-2B (highest quality). + - ${DIA2_2B_CONFIG}:/app/config.yaml healthcheck: - # devnen's Dia server exposes GET /health (liveness) plus - # /api/model-status (download/load progress) and /api/model-info. - # /health is the simple liveness probe; start_period covers the - # first-boot model pull. python urllib (image has no curl), bound - # to 127.0.0.1 (uvicorn is IPv4-only). test: ["CMD-SHELL", "python3 -c \"import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8003/health', timeout=5); sys.exit(0)\""] interval: 30s timeout: 10s retries: 3 - # First boot pulls the checkpoint (~6-10 GB) — generous deadline. + # First boot pulls the Dia2-2B checkpoint — generous deadline. start_period: 600s labels: - homepage.group=AI Systems - - homepage.name=Dia / Dia2 + - homepage.name=Dia2 2B (HQ) - homepage.icon=mdi-account-voice - - homepage.description=Dialogue TTS — multi-speaker turn-taking + nonverbals (irv-ml1) - - homepage.href=http://10.100.79.3:${DIA_PORT} + - homepage.description=Dialogue TTS — Dia2 2B, highest quality (irv-ml1) + - homepage.href=http://10.100.79.3:${DIA2_2B_PORT} + + dia2-1b: + image: local/dia:${DIA_TAG} + container_name: dia2-1b + restart: unless-stopped + runtime: nvidia + ports: + - "${DIA_BIND:-0.0.0.0}:${DIA2_1B_PORT}:8003" + environment: + - NVIDIA_VISIBLE_DEVICES=${DIA2_1B_GPU:-0} + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - HF_HUB_ENABLE_HF_TRANSFER=1 + - HF_HOME=/app/hf_cache + volumes: + - ${DIA_REFERENCE_DIR}:/app/reference_audio + - ${DIA_CACHE_DIR}:/app/hf_cache + # Pins model.repo_id=nari-labs/Dia2-1B (streaming, lower latency). + - ${DIA2_1B_CONFIG}:/app/config.yaml + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8003/health', timeout=5); sys.exit(0)\""] + interval: 30s + timeout: 10s + retries: 3 + start_period: 600s + labels: + - homepage.group=AI Systems + - homepage.name=Dia2 1B (Streaming) + - homepage.icon=mdi-account-voice + - homepage.description=Dialogue TTS — Dia2 1B, streaming/faster (irv-ml1) + - homepage.href=http://10.100.79.3:${DIA2_1B_PORT} diff --git a/stacks/dia/dia2-image/Dockerfile b/stacks/dia/dia2-image/Dockerfile new file mode 100644 index 0000000..cbe24fd --- /dev/null +++ b/stacks/dia/dia2-image/Dockerfile @@ -0,0 +1,31 @@ +# dia2-capable image, layered on the upstream devnen wrapper image +# (local/dia:, which ships the wrapper + the dia1 package only). +# +# Why this exists: the upstream image does NOT bundle the dia2 package or +# its runtime deps. The wrapper tries to `git clone nari-labs/dia2 && +# pip install` it at RUNTIME on first Dia2 load — which fails (no `git`, +# and would be lost on recreate). We provision Dia2 at build time instead. +# +# Two upstream packaging quirks, both handled below: +# 1. dia2's pyproject build backend yields an empty "UNKNOWN" wheel under +# the base image's old setuptools (59.6 < the required 70) and rejects +# editable installs. dia2 is pure-python (packages=["dia2"]), so we +# copy the package straight into site-packages — the wrapper only needs +# `from dia2 import ...` to succeed, not pip-tracked metadata. +# 2. A bare `pip install dia2_src` would also drag torch>=2.8 + pin +# safetensors==0.5.3. The base ALREADY satisfies dia2 (torch 2.12, +# numpy 2.2, safetensors 0.7) — so we install ONLY the genuinely +# missing runtime deps and leave the heavy/pinned stack untouched. +# (transformers/sphn/whisper-timestamped verified as the minimal set +# that makes the wrapper's `from dia2 import Dia2, ...` import succeed.) +ARG DIA_BASE=local/dia:v1 +FROM ${DIA_BASE} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://github.com/nari-labs/dia2.git /opt/dia2_src \ + && cp -r /opt/dia2_src/dia2 "$(python3 -c 'import site; print(site.getsitepackages()[0])')/" \ + && rm -rf /opt/dia2_src \ + && pip install --no-cache-dir "transformers>=4.55.3" "sphn>=0.2.0" "whisper-timestamped>=1.14.2"