feat(dia): two-instance Dia2 deployment (dia2-1b + dia2-2b)

The devnen wrapper is single-model and ignores the OpenAI model field, so
offering both Dia2 models to asset-engine as real per-request choices means
one fixed-model instance per model. Rework the dia stack to run two services
from a dia2-capable image:
  * dia2-2b (:8200, best quality), dia2-1b (:8202, streaming) — both GPU 0
  * each pins its model via a mounted /opt/docker/conf/dia2-*/config.yaml
Retire the legacy Dia 1.6B service.

New dia2-image/Dockerfile builds local/dia:v2 = upstream devnen wrapper +
the dia2 package (copied into site-packages; its pyproject build backend
yields an empty UNKNOWN wheel under the base's old setuptools) + the three
missing runtime deps (transformers/sphn/whisper-timestamped); torch 2.12 /
numpy 2.2 in the base already satisfy Dia2. Both instances verified
end-to-end (HTTP 200, Ogg/Opus 24 kHz).
This commit is contained in:
2026-05-31 14:41:11 -07:00
parent 02b26f45a6
commit 3139e81e8a
3 changed files with 115 additions and 85 deletions
+29 -44
View File
@@ -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://<host>: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
+55 -41
View File
@@ -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}
+31
View File
@@ -0,0 +1,31 @@
# dia2-capable image, layered on the upstream devnen wrapper image
# (local/dia:<base>, 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"