stacks/index-tts: own FastAPI wrapper for IndexTTS-2 + deploy playbook

Adds a third TTS to the irv-ml1 fleet. IndexTTS-2 is Bilibili's
emotion-controllable zero-shot TTS (paper 2506.21619). Distinguishing
capability vs the existing two: timbre and emotion are disentangled —
clone a voice's timbre from one reference and the emotion from a
different reference, OR set emotion via 8-vector, OR derive it from a
text description. Neither CosyVoice 3 nor Qwen3-TTS-1.7B-Base does
this cleanly in English.

Wrapper is owned end-to-end (~150 lines in app.py) — the only existing
FastAPI fork (csllpr/index-tts-fastapi) targets v1 and is a dormant
single-commit repo. Upstream IndexTTS-2 ships only a Gradio webui.

Layout follows the qwen3-tts pattern:
  stacks/index-tts/
    Dockerfile           — CUDA 12.8 base, IndexTTS pinned to a SHA
    app.py               — FastAPI: POST /v1/audio/speech + /v1/voices
    entrypoint.sh        — one-time HF snapshot_download of the weights
    compose.yaml         — env-driven, GPU pinning support, bind mounts
    .env.example         — port 8192, fp16, paths
    README.md            — API examples + comparison vs the other TTS
  playbooks/deploy-index-tts.yaml  — elway playbook for irv-ml1

Voice and emotion libraries are flat host dirs of WAVs, bind-mounted.
Drop a new <name>.wav and /v1/voices picks it up immediately.

License caveat: IndexTTS-2 weights ship under a custom Bilibili
license (free at our scale, not OSI-open). README documents it.
This commit is contained in:
vh
2026-04-25 10:38:14 -07:00
parent 1f14c6d959
commit b75f020cc9
7 changed files with 604 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# Deploy IndexTTS-2 (https://github.com/index-tts/index-tts) to irv-ml1
# behind our own FastAPI wrapper (stacks/index-tts/app.py).
#
# Builds the image locally from the Dockerfile in stacks/index-tts/
# (which clones the upstream IndexTTS-2 repo at a pinned SHA inside
# the build), stages app.py + entrypoint.sh + compose + .env under
# /opt/docker/compose/index-tts/, brings it up, waits for /healthz,
# and verifies the API surface.
#
# First run is slow: ~5-10 min for the docker build (CUDA torch + the
# IndexTTS pinned-deps tail) plus ~5-7 GB model download from HF on
# first container start (entrypoint.sh handles that). The healthz wait
# below allows up to 15 min total.
#
# Usage:
# scripts/elway irv-ml1 --playbook playbooks/deploy-index-tts.yaml
#
# Idempotent — every step is creates-/when-gated; rerun is safe.
vars:
compose_dir: /opt/docker/compose/index-tts
cache_dir: /worktank/index-tts/cache
voices_dir: /worktank/index-tts/voices
emotions_dir: /worktank/index-tts/emotions
host_port: "8192"
steps:
# ── host-side dirs ──────────────────────────────────────────────────
- name: Ensure /worktank/index-tts root exists (one-time, sudo)
shell: mkdir -p /worktank/index-tts
sudo: true
creates: /worktank/index-tts
- name: Chown /worktank/index-tts to lkraven
shell: chown lkraven:lkraven /worktank/index-tts
sudo: true
when: '[ "$(stat -c %U /worktank/index-tts)" != lkraven ]'
- name: Ensure cache dir exists
shell: mkdir -p {{ cache_dir }}
creates: "{{ cache_dir }}"
- name: Ensure voices dir exists
shell: mkdir -p {{ voices_dir }}
creates: "{{ voices_dir }}"
- name: Ensure emotions dir exists
shell: mkdir -p {{ emotions_dir }}
creates: "{{ emotions_dir }}"
- name: Ensure compose dir exists
shell: mkdir -p {{ compose_dir }}
creates: "{{ compose_dir }}"
# ── deploy build context (compose, env, dockerfile, app, entrypoint) ──
- name: Upload compose.yaml
upload:
src: stacks/index-tts/compose.yaml
dest: "{{ compose_dir }}/compose.yaml"
mode: "0644"
- name: Upload Dockerfile
upload:
src: stacks/index-tts/Dockerfile
dest: "{{ compose_dir }}/Dockerfile"
mode: "0644"
- name: Upload app.py
upload:
src: stacks/index-tts/app.py
dest: "{{ compose_dir }}/app.py"
mode: "0644"
- name: Upload entrypoint.sh
upload:
src: stacks/index-tts/entrypoint.sh
dest: "{{ compose_dir }}/entrypoint.sh"
mode: "0755"
- name: Seed .env from template (only if absent)
upload:
src: stacks/index-tts/.env.example
dest: "{{ compose_dir }}/.env"
mode: "0644"
when: "[ ! -f {{ compose_dir }}/.env ]"
# ── build + bring up ────────────────────────────────────────────────
- name: docker compose build (~5-10 min first time; cached after)
shell: cd {{ compose_dir }} && docker compose build
- name: docker compose up -d
shell: cd {{ compose_dir }} && docker compose up -d
- name: Wait for /healthz to respond (allow ~15 min for model download)
shell: |
for i in $(seq 1 180); do
curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/healthz && exit 0
sleep 5
done
exit 1
changed_when: "false"
verify:
- name: /healthz returns 200
shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/healthz
changed_when: "false"
- name: /v1/voices returns a JSON object with 'voices' and 'emotions' keys
shell: curl -sf http://localhost:{{ host_port }}/v1/voices | grep -q '"voices"'
changed_when: "false"
- name: Container is running
shell: docker inspect index-tts --format '{{.State.Status}}' | grep -q running
changed_when: "false"
+53
View File
@@ -0,0 +1,53 @@
# IndexTTS-2 stack tunables. Copy to `.env` on irv-ml1 before deploying.
# ── build pin ────────────────────────────────────────────────────────
# SHA of index-tts/index-tts to build from. Bump + rebuild when you want
# upstream wrapper updates.
INDEX_TTS_SHA=830f6f8f94a51fea23ab1d639027a86200075a4e
# Local image tag — bump when you change build context (Dockerfile,
# app.py, entrypoint.sh) to force a fresh layer build.
INDEX_TTS_TAG=v1
# ── network ──────────────────────────────────────────────────────────
# Host port. Container listens on 8000 internally.
# Reserved on irv-ml1: 8188 ComfyUI, 8190 CosyVoice, 8191 Qwen3-TTS,
# 8765 Parakeet. 8192 is open.
INDEX_TTS_PORT=8192
# Bind address. 0.0.0.0 exposes on all interfaces (incl. WG tunnel
# interface 10.100.79.3); 127.0.0.1 restricts to local-only.
INDEX_TTS_BIND=0.0.0.0
# ── runtime / GPU ────────────────────────────────────────────────────
# bf16/fp16 vs fp32. Empty = fp32. "1" = fp16. IndexTTS-2 README
# recommends fp16; ~6-10 GB VRAM at fp16, double at fp32.
INDEX_TTS_FP16=1
# GPU pinning. Empty = let IndexTTS auto-pick (cuda:0). Set to "cuda:1"
# to pin to the second GPU (irv-ml1's RTX 3090 vs RTX A6000).
INDEX_TTS_DEVICE=
# Devices visible inside the container. Either "all" (both GPUs) or a
# comma-separated list of indices (e.g. "1" to expose only the second
# card). The DEVICE setting above further restricts within those.
INDEX_TTS_GPU_DEVICES=all
# Logging level for the wrapper itself (IndexTTS internals are noisier
# regardless).
INDEX_TTS_LOG_LEVEL=INFO
# ── persistent storage on the host ───────────────────────────────────
# Model weights (~5-7 GB after first run). Bind-mounted so model state
# survives container recreate. Excluded from restic (regenerable from HF).
INDEX_TTS_CACHE_DIR=/worktank/index-tts/cache
# Voice library — flat dir of <name>.wav files (timbre references).
# Cloned voices need the original reference audio to recreate; included
# in restic.
INDEX_TTS_VOICES_DIR=/worktank/index-tts/voices
# Emotion library — flat dir of <name>.wav files (emotion references,
# typically short clips with strong affect). Optional — without any
# entries here you can still use emotion_vector or emotion_text.
INDEX_TTS_EMOTIONS_DIR=/worktank/index-tts/emotions
+57
View File
@@ -0,0 +1,57 @@
# syntax=docker/dockerfile:1.6
#
# IndexTTS-2 served behind our own thin FastAPI wrapper (~150 lines in
# app.py). Upstream ships only a Gradio webui; the existing csllpr
# FastAPI fork is for v1 and dormant. We own the wrapper end-to-end.
ARG CUDA_BASE=nvidia/cuda:12.8.0-cudnn-runtime-ubuntu22.04
FROM ${CUDA_BASE}
ENV DEBIAN_FRONTEND=noninteractive \
PIP_ROOT_USER_ACTION=ignore \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:${PATH}"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3.10 python3.10-venv python3-pip \
git ffmpeg libsndfile1 \
ca-certificates wget \
&& rm -rf /var/lib/apt/lists/* \
&& python3.10 -m venv /opt/venv
# Pinned IndexTTS-2 SHA — bump in stack .env (INDEX_TTS_SHA build arg)
# when you want upstream updates.
ARG INDEX_TTS_SHA=830f6f8f94a51fea23ab1d639027a86200075a4e
RUN git clone https://github.com/index-tts/index-tts.git /opt/index-tts \
&& cd /opt/index-tts && git checkout ${INDEX_TTS_SHA}
# CUDA 12.8 torch wheels (per IndexTTS pyproject.toml's tool.uv.index
# pin — same versions, same source).
RUN pip install --no-cache-dir \
torch==2.8.0 torchaudio==2.8.0 \
--index-url https://download.pytorch.org/whl/cu128
# IndexTTS package + its long pinned-deps tail (numpy 1.26.2, transformers
# 4.52.1, sentencepiece, librosa, descript-audiotools, jieba/g2p-en/cn2an,
# WeTextProcessing, modelscope, accelerate, safetensors, etc.).
# BigVGAN is vendored under indextts/ so it resolves without a pip dep.
RUN cd /opt/index-tts && pip install --no-cache-dir -e .
# Wrapper deps (FastAPI stack + soundfile for in-memory WAV encoding).
RUN pip install --no-cache-dir \
fastapi 'uvicorn[standard]' python-multipart soundfile
WORKDIR /app
COPY app.py /app/app.py
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8000
# wget is in the base image (added above); curl is not.
HEALTHCHECK --interval=30s --timeout=10s --start-period=600s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8000/healthz || exit 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
+134
View File
@@ -0,0 +1,134 @@
# IndexTTS-2
Bilibili's emotion-controllable zero-shot TTS
([paper](https://arxiv.org/abs/2506.21619),
[code](https://github.com/index-tts/index-tts),
[weights](https://huggingface.co/IndexTeam/IndexTTS-2)) served behind
our own thin FastAPI wrapper.
## Why this stack exists alongside the other two TTS
| | CosyVoice 3 | Qwen3-TTS-1.7B-Base | **IndexTTS-2** |
|---|---|---|---|
| Voice cloning | ✅ | ✅ (`-Base` variant only) | ✅ |
| English quality | medium (Chinese-leaning) | high (English-first) | medium (better than CosyVoice) |
| Emotion control | `instruct` mode is Chinese-only | inline tags | **explicit: audio / 8-vector / text** |
| Duration control | implicit | implicit | **explicit token-count mode** |
| License | Apache 2.0 | Apache 2.0 | custom (Bilibili — free at our scale) |
| Wrapper | bare CosyVoice CLI | groxaxo upstream FastAPI | ours, in this dir |
The differentiator is **disentangled emotion**. IndexTTS-2 lets you
clone a voice's timbre from one reference and the emotion from a
different reference — or skip emotion-audio entirely and supply an
8-vector or a text description. Neither of the other two does this
cleanly in English.
## API
OpenAI-compat-ish:
```bash
# List available voices + emotions
curl http://10.100.79.3:8192/v1/voices
# Basic synthesis (uses speaker WAV's natural emotion)
curl -X POST http://10.100.79.3:8192/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"input": "I have all the time in the world.",
"voice": "glados"
}' > glados.wav
# Same speaker, emotion taken from a separate reference WAV
curl -X POST http://10.100.79.3:8192/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"input": "I have all the time in the world.",
"voice": "glados",
"emotion_voice": "menacing",
"emotion_alpha": 0.9
}' > glados-menacing.wav
# Same speaker, emotion as 8-vector
# Order: happy, angry, sad, afraid, disgusted, melancholic, surprised, calm
curl -X POST http://10.100.79.3:8192/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"input": "I have all the time in the world.",
"voice": "glados",
"emotion_vector": [0, 0.7, 0, 0, 0.2, 0, 0, 0]
}' > glados-angry.wav
# Same speaker, emotion derived from text by bundled QwenEmotion model
curl -X POST http://10.100.79.3:8192/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{
"input": "I have all the time in the world.",
"voice": "glados",
"emotion_text": "she said with quiet menace"
}' > glados-menacing.wav
# Health
curl http://10.100.79.3:8192/healthz
```
Output is always WAV (PCM_16, 22050 Hz — IndexTTS-2's native rate).
`response_format` other than `wav` is rejected.
## Voice library
Flat dirs on the host (bind-mounted; survives container recreates):
```
/worktank/index-tts/voices/<name>.wav # timbre references
/worktank/index-tts/emotions/<name>.wav # emotion references
```
Drop a new WAV in either dir and `/v1/voices` picks it up immediately —
no restart. Use clean reference clips, 5-30 s each, single speaker.
Cloned voices live under restic; cache (model weights) is excluded.
## Deploy
```bash
scripts/elway irv-ml1 --playbook playbooks/deploy-index-tts.yaml
```
First build: ~5-10 min for the docker image (CUDA torch + IndexTTS
deps), plus ~5-7 GB model download on first container start. Subsequent
starts: ~30 s warmup.
## Switching GPUs
irv-ml1 has an RTX 3090 (cuda:0) + RTX A6000 (cuda:1). Default is
auto-pick (cuda:0). To pin to the A6000 alongside Qwen3-TTS-on-3090:
```bash
ssh irv-ml1 '
cd /opt/docker/compose/index-tts
sed -i "s|^INDEX_TTS_DEVICE=.*|INDEX_TTS_DEVICE=cuda:1|" .env
docker compose up -d
'
```
## Gotchas
- **License** — IndexTeam/IndexTTS-2 ships under a custom Bilibili
license, not Apache/MIT. Free at our scale (the commercial tier kicks
in at 100M MAU / RMB 1B revenue). Restricts using outputs to train
other AI models. Read `INDEX_MODEL_LICENSE` in the HF repo before
using outputs anywhere external.
- **Sample rate** — 22050 Hz is hardcoded upstream. If you need 24 kHz
or 48 kHz, resample in the caller.
- **Emotion-source precedence** — if multiple emotion controls are
specified in one request, the first non-empty one wins in this order:
`emotion_voice` > `emotion_vector` > `emotion_text`. The others are
silently ignored.
- **Model download** — happens in the entrypoint on first start; the
config.yaml file in the cache dir is the gate. To force a re-download,
delete that file and recreate the container.
- **HF cache pinning** — `infer_v2.py` pins `HF_HUB_CACHE` at import
time to `./checkpoints/hf_cache`. The wrapper sets this env var
before importing, so auxiliary HF assets (MaskGCT, campplus, BigVGAN,
w2v-bert) land alongside the IndexTTS-2 weights and are excluded
from restic together.
+154
View File
@@ -0,0 +1,154 @@
"""IndexTTS-2 — minimal FastAPI wrapper.
Upstream (https://github.com/index-tts/index-tts) ships only a Gradio
webui; the only existing FastAPI fork (csllpr/index-tts-fastapi) targets
v1 and is dormant. We own this wrapper end-to-end.
API surface:
POST /v1/audio/speech OpenAI-compat-ish, see SpeechRequest
GET /v1/voices list speakers + emotion references
GET /healthz liveness for compose healthcheck
Voice library is a flat directory of WAVs (one file per voice). Emotion
references live in a parallel directory. Both are bind-mounted from the
host so cloned voices survive container recreates.
Emotion control is opt-in and mutually exclusive (audio > vector > text):
* emotion_voice — name of a WAV in the emotions dir, used as a SECOND
reference whose timbre is ignored but emotion is
transferred onto the speaker.
* emotion_vector — 8-float [happy, angry, sad, afraid, disgusted,
melancholic, surprised, calm].
* emotion_text — free text; bundled QwenEmotion model derives the
vector ("she said excitedly" → joy spike).
Without any of these the speaker WAV's natural emotion is reused.
"""
from __future__ import annotations
import io
import logging
import os
from pathlib import Path
from typing import List, Optional
# IndexTTS pins HF_HUB_CACHE at import time (./checkpoints/hf_cache by
# default, see infer_v2.py:4). Override BEFORE the indextts import or
# downloads land in the wrong tree.
os.environ.setdefault(
"HF_HUB_CACHE",
os.environ.get("INDEX_TTS_HF_CACHE", "/app/checkpoints/hf_cache"),
)
import soundfile as sf # noqa: E402
from fastapi import FastAPI, HTTPException # noqa: E402
from fastapi.responses import Response # noqa: E402
from pydantic import BaseModel, Field # noqa: E402
from indextts.infer_v2 import IndexTTS2 # noqa: E402
# ── config from env ──────────────────────────────────────────────────
MODEL_DIR = os.environ.get("INDEX_TTS_MODEL_DIR", "/app/checkpoints")
CFG_PATH = os.environ.get("INDEX_TTS_CFG", f"{MODEL_DIR}/config.yaml")
VOICES_DIR = Path(os.environ.get("INDEX_TTS_VOICES_DIR", "/app/voices"))
EMOTIONS_DIR = Path(os.environ.get("INDEX_TTS_EMOTIONS_DIR", "/app/emotions"))
USE_FP16 = os.environ.get("INDEX_TTS_FP16", "1") == "1"
DEVICE = os.environ.get("INDEX_TTS_DEVICE") or None # "cuda:0", "cuda:1", or None=auto
VOICES_DIR.mkdir(parents=True, exist_ok=True)
EMOTIONS_DIR.mkdir(parents=True, exist_ok=True)
logging.basicConfig(level=os.environ.get("INDEX_TTS_LOG_LEVEL", "INFO"))
log = logging.getLogger("index-tts")
log.info(
"Loading IndexTTS2 from %s (device=%s, fp16=%s)",
MODEL_DIR, DEVICE or "auto", USE_FP16,
)
tts = IndexTTS2(
cfg_path=CFG_PATH,
model_dir=MODEL_DIR,
use_fp16=USE_FP16,
device=DEVICE,
)
log.info("IndexTTS2 ready")
app = FastAPI(title="index-tts", version="0.1.0")
class SpeechRequest(BaseModel):
model: Optional[str] = "index-tts-2" # accepted but ignored
input: str = Field(..., description="Text to synthesize")
voice: str = Field(..., description="<name>.wav must exist in voices dir")
response_format: str = Field("wav", description="wav (only)")
# ── emotion (all optional, mutually exclusive) ──
emotion_voice: Optional[str] = Field(
None, description="<name>.wav in emotions dir, used as emotion ref"
)
emotion_vector: Optional[List[float]] = Field(
None,
description="8 floats: happy, angry, sad, afraid, disgusted, melancholic, surprised, calm",
)
emotion_text: Optional[str] = Field(
None, description="Text describing emotion; QwenEmotion derives vector"
)
emotion_alpha: float = Field(1.0, ge=0.0, le=1.0)
def _resolve(name: str, root: Path) -> Path:
p = root / f"{name}.wav"
if not p.is_file():
raise HTTPException(status_code=404, detail=f"not found: {root.name}/{name}.wav")
return p
@app.get("/healthz")
def healthz() -> dict:
return {"status": "ok"}
@app.get("/v1/voices")
def list_voices() -> dict:
return {
"voices": sorted(p.stem for p in VOICES_DIR.glob("*.wav")),
"emotions": sorted(p.stem for p in EMOTIONS_DIR.glob("*.wav")),
}
@app.post("/v1/audio/speech")
def synthesize(req: SpeechRequest) -> Response:
if req.response_format != "wav":
raise HTTPException(status_code=400, detail="only response_format=wav is supported")
spk = str(_resolve(req.voice, VOICES_DIR))
# First emotion source set wins.
emo_path = None
emo_vector = None
use_emo_text = False
emo_text = None
if req.emotion_voice:
emo_path = str(_resolve(req.emotion_voice, EMOTIONS_DIR))
elif req.emotion_vector is not None:
if len(req.emotion_vector) != 8:
raise HTTPException(status_code=400, detail="emotion_vector must have 8 elements")
emo_vector = list(req.emotion_vector)
elif req.emotion_text:
use_emo_text = True
emo_text = req.emotion_text
sr, audio = tts.infer(
spk_audio_prompt=spk,
text=req.input,
output_path=None, # in-memory return: (sr, np_int16)
emo_audio_prompt=emo_path,
emo_alpha=req.emotion_alpha,
emo_vector=emo_vector,
use_emo_text=use_emo_text,
emo_text=emo_text,
verbose=False,
)
buf = io.BytesIO()
sf.write(buf, audio, sr, format="WAV", subtype="PCM_16")
return Response(content=buf.getvalue(), media_type="audio/wav")
+63
View File
@@ -0,0 +1,63 @@
# IndexTTS-2 — Bilibili's emotion-controllable zero-shot TTS, served
# behind our own thin FastAPI wrapper (stacks/index-tts/app.py).
#
# Why this stack exists alongside qwen3-tts and cosyvoice:
# IndexTTS-2 disentangles timbre from emotion — emotion can be cloned
# from a separate audio reference, set via 8-vector, or derived from
# free text. Neither qwen3-tts nor cosyvoice expose this cleanly in
# English. See stacks/index-tts/README.md for the full rationale.
#
# Build: image is local, built from the Dockerfile in this dir. Pinned
# upstream SHA lives in .env as INDEX_TTS_SHA so rebuilds are
# reproducible.
#
# Model: ~5-7 GB IndexTTS-2 weights download on first start via the
# entrypoint, persisted under ${INDEX_TTS_CACHE_DIR}.
#
# License note: weights carry a custom Bilibili license (free at our
# scale, but not OSI-open). The wrapper code is ours, MIT-by-default.
#
# All tunables live in .env — edit that, not this file.
services:
index-tts:
image: local/index-tts:${INDEX_TTS_TAG}
build:
context: .
dockerfile: Dockerfile
args:
INDEX_TTS_SHA: ${INDEX_TTS_SHA}
container_name: index-tts
restart: unless-stopped
runtime: nvidia
ports:
- "${INDEX_TTS_BIND:-0.0.0.0}:${INDEX_TTS_PORT}:8000"
environment:
- NVIDIA_VISIBLE_DEVICES=${INDEX_TTS_GPU_DEVICES:-all}
- INDEX_TTS_MODEL_DIR=/app/checkpoints
- INDEX_TTS_VOICES_DIR=/app/voices
- INDEX_TTS_EMOTIONS_DIR=/app/emotions
- INDEX_TTS_FP16=${INDEX_TTS_FP16:-1}
- INDEX_TTS_DEVICE=${INDEX_TTS_DEVICE:-}
- INDEX_TTS_LOG_LEVEL=${INDEX_TTS_LOG_LEVEL:-INFO}
volumes:
- ${INDEX_TTS_CACHE_DIR}:/app/checkpoints
- ${INDEX_TTS_VOICES_DIR}:/app/voices
- ${INDEX_TTS_EMOTIONS_DIR}:/app/emotions
healthcheck:
# Match Dockerfile's healthcheck. Compose-level entry overrides the
# image-level one if anything ever needs tweaking per-deploy.
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:8000/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# First boot: ~5-7 GB HF download + IndexTTS-2 import (touches several
# auxiliary HF repos for MaskGCT, campplus, BigVGAN, w2v-bert) + initial
# CUDA warmup. Generous deadline to ride that out.
start_period: 600s
labels:
- homepage.group=AI Systems
- homepage.name=IndexTTS-2
- homepage.icon=mdi-account-music
- homepage.description=Emotion-controllable TTS w/ voice cloning (irv-ml1)
- homepage.href=http://10.100.79.3:${INDEX_TTS_PORT}
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# entrypoint.sh — fetch IndexTTS-2 weights on first run, then exec server.
#
# IndexTTS2() expects checkpoint files pre-staged under model_dir; if
# they're missing, the constructor sys.exit(1)s. Pull them via
# huggingface_hub.snapshot_download on first start. ~5-7 GB; subsequent
# starts skip the download because config.yaml is the gate.
set -e
MODEL_DIR="${INDEX_TTS_MODEL_DIR:-/app/checkpoints}"
mkdir -p "${MODEL_DIR}"
if [ ! -f "${MODEL_DIR}/config.yaml" ]; then
echo "[index-tts] downloading IndexTTS-2 weights to ${MODEL_DIR} (~5-7 GB, one-time)"
python3 - <<EOF
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="IndexTeam/IndexTTS-2",
local_dir="${MODEL_DIR}",
)
EOF
echo "[index-tts] download complete"
fi
exec "$@"