feat(tts): move TTS backends under containers/tts/, add MOSS-TTS

Этот коммит содержится в:
2026-09-13 15:19:51 +02:00
родитель 753c0b15f2
Коммит a5716f4dad
24 изменённых файлов: 297 добавлений и 0 удалений
Просмотреть файл
Просмотреть файл
Просмотреть файл
Исполняемый файл
+22
Просмотреть файл
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
### Build the mosstts image. USAGE: ./build-container.sh [cpu|rocm] (default: cpu)
set -euo pipefail
DEVICE="${1:-cpu}"
case "$DEVICE" in
cpu|rocm) ;;
*)
echo "Usage: $0 [cpu|rocm]" >&2
exit 1
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
podman build \
--build-arg DEVICE="$DEVICE" \
-t "localhost/mosstts:$DEVICE" \
-t localhost/mosstts:latest \
-f "$SCRIPT_DIR/mosstts.Containerfile" \
"$SCRIPT_DIR"
Исполняемый файл
+20
Просмотреть файл
@@ -0,0 +1,20 @@
#!/bin/bash
# Starts the FastAPI server; model weights download on first request into /app/models
set -e
# wait for GPU: torch.cuda.is_available() caches its first (possibly premature) result
if [ -e /dev/kfd ]; then
echo "ROCm device detected, waiting for GPU to become available..."
for i in $(seq 1 15); do
if python3 -c "import sys, torch; sys.exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
echo "GPU is available."
break
fi
sleep 1
done
fi
echo "=== MOSS-TTS Server ==="
echo
exec uvicorn server:app --host 0.0.0.0 --port 8000 --app-dir /app "$@"
+47
Просмотреть файл
@@ -0,0 +1,47 @@
### MOSS-TTS Container (https://github.com/OpenMOSS/MOSS-TTS) — build with ./build-container.sh [cpu|rocm]
ARG DEVICE=cpu
ARG BASE_IMAGE_CPU=python:3.12-slim
ARG BASE_IMAGE_ROCM=docker.io/rocm/pytorch-nightly
FROM ${BASE_IMAGE_CPU} AS base-cpu
FROM ${BASE_IMAGE_ROCM} AS base-rocm
FROM base-${DEVICE}
ARG DEVICE
USER root
EXPOSE 8000
# ffmpeg/libsndfile: needed at runtime for reference-audio format handling
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg curl \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# CPU-only PyTorch (ROCm base image already ships its own torch/torchaudio build)
RUN if [ "$DEVICE" = "cpu" ]; then \
pip install --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cpu; \
fi
# Core MOSS-TTS runtime deps (from upstream pyproject.toml), installed directly
# instead of cloning the repo: AutoModel/AutoProcessor with trust_remote_code
# pull the actual modeling code from the Hugging Face model repo at runtime.
RUN pip install --no-cache-dir \
safetensors numpy orjson tqdm PyYAML einops scipy librosa tiktoken \
psutil packaging "transformers>=5.0.0" accelerate torchcodec \
fastapi uvicorn python-multipart soundfile
# Models download at runtime here — mount as a volume to persist them
RUN mkdir -p /app/models
ENV HF_HOME=/app/models
# Copy entrypoint / server script
COPY entrypoint.sh /app/entrypoint.sh
COPY server.py /app/server.py
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []
+13
Просмотреть файл
@@ -0,0 +1,13 @@
mosstts.example.com {
import gateway_error
request_body {
max_size 100MB
}
reverse_proxy mosstts:8000
log {
output file /var/log/caddy/mosstts_access.log
}
}
+46
Просмотреть файл
@@ -0,0 +1,46 @@
[Unit]
Description=MOSS-TTS Server
After=network-online.target
Wants=network-online.target
[Container]
Image=localhost/mosstts:latest
ContainerName=mosstts
Network=internal.network
# CPU-only container
#Memory=8g
#CPUs=4
# ROCm
AddDevice=/dev/kfd
AddDevice=/dev/dri/renderD128
PodmanArgs=--group-add=keep-groups --ipc=host --pids-limit=-1 --security-opt label=disable
SecurityLabelType=container_runtime_t
# HTTP API
PublishPort=8003:8000
# Persist downloaded model weights across restarts (adjust host path as needed)
Volume=/srv/containers/mosstts/models:/app/models:Z
# Default model: OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 (4B, 48kHz stereo).
# Other checkpoints usable via MOSSTTS_MODEL (same generate API unless noted):
# OpenMOSS-Team/MOSS-TTS-v1.5 8B, 24kHz, the flagship model
# OpenMOSS-Team/MOSS-TTSD-v1.0 8B, same arch as v1.5, earlier release
# OpenMOSS-Team/MOSS-TTS-Nano 0.1B, CPU-friendly, API not verified
# OpenMOSS-Team/MOSS-TTS-Realtime 1.7B, multi-turn streaming voice-agent
# model with a DIFFERENT generation API;
# NOT compatible with this server as-is
#Environment=MOSSTTS_MODEL=OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5
#Environment=MOSSTTS_ATTN_IMPL=sdpa
#Environment=MOSSTTS_MAX_NEW_TOKENS=4096
[Service]
Restart=on-failure
RestartSec=5
TimeoutStartSec=300
[Install]
WantedBy=default.target
+149
Просмотреть файл
@@ -0,0 +1,149 @@
"""Minimal FastAPI server exposing MOSS-TTS generation and voice cloning. https://github.com/OpenMOSS/MOSS-TTS
Other checkpoints usable via MOSSTTS_MODEL (same build_user_message/generate/decode
API unless noted):
- OpenMOSS-Team/MOSS-TTS-v1.5 8B, 24kHz, the flagship model
- OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5 4B, 48kHz stereo (default here)
- OpenMOSS-Team/MOSS-TTSD-v1.0 8B, same arch as v1.5, earlier release
- OpenMOSS-Team/MOSS-TTS-Nano 0.1B, CPU-friendly (as few as 4 cores),
streaming-oriented; API not verified
- OpenMOSS-Team/MOSS-TTS-Realtime 1.7B, multi-turn/streaming voice-agent
model with a different generation API
(not build_user_message/generate/decode);
NOT compatible with this server as-is
"""
import io
import os
import tempfile
import soundfile as sf
import torch
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from transformers import AutoModel, AutoProcessor
MODEL_NAME = os.environ.get("MOSSTTS_MODEL", "OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5")
ATTN_IMPLEMENTATION = os.environ.get("MOSSTTS_ATTN_IMPL", "sdpa")
MAX_NEW_TOKENS = int(os.environ.get("MOSSTTS_MAX_NEW_TOKENS", "4096"))
_forced_device = os.environ.get("MOSSTTS_DEVICE", "").strip()
if _forced_device:
DEVICE = _forced_device
elif torch.cuda.is_available(): # ROCm builds expose the GPU via the CUDA API too
DEVICE = "cuda"
else:
DEVICE = "cpu"
# bfloat16 is not well supported on CPU; use full precision there.
DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
if DEVICE == "cuda":
# Match the backends enabled in the upstream quickstart; avoids a broken
# cuDNN SDP kernel on some GPU combos (same class of issue as chatterbox).
torch.backends.cuda.enable_cudnn_sdp(False)
torch.backends.cuda.enable_flash_sdp(True)
torch.backends.cuda.enable_mem_efficient_sdp(True)
torch.backends.cuda.enable_math_sdp(True)
app = FastAPI(title="MOSS-TTS server")
model = None
processor = None
@app.on_event("startup")
def load_model():
global model, processor
print(f"Loading MOSS-TTS processor/model {MODEL_NAME} on {DEVICE}...")
processor = AutoProcessor.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
)
processor.audio_tokenizer = processor.audio_tokenizer.to(DEVICE)
model = AutoModel.from_pretrained(
MODEL_NAME,
trust_remote_code=True,
attn_implementation=ATTN_IMPLEMENTATION,
dtype=DTYPE,
).to(DEVICE)
model.eval()
print("MOSS-TTS model loaded.")
@app.get("/health")
def health():
return {"status": "ok", "model": MODEL_NAME, "device": DEVICE}
def _run_generation(text: str, language: str, reference: list[str] | None) -> tuple:
if model is None or processor is None:
raise HTTPException(status_code=503, detail="Model not loaded yet")
message_kwargs = {"text": text}
if language:
message_kwargs["language"] = language
if reference:
message_kwargs["reference"] = reference
conversation = [processor.build_user_message(**message_kwargs)]
batch = processor([conversation], mode="generation")
input_ids = batch["input_ids"].to(DEVICE)
attention_mask = batch["attention_mask"].to(DEVICE)
with torch.no_grad():
outputs = model.generate(
input_ids=input_ids,
attention_mask=attention_mask,
max_new_tokens=MAX_NEW_TOKENS,
)
decoded = processor.decode(outputs)[0]
audio = decoded.audio_codes_list[0]
if audio.ndim > 1:
audio = audio.squeeze(0)
return audio.detach().cpu().to(torch.float32).numpy(), processor.model_config.sampling_rate
class SpeechRequest(BaseModel):
text: str
language: str = ""
@app.post("/speech")
def speech(req: SpeechRequest):
audio, sr = _run_generation(req.text, req.language, reference=None)
buf = io.BytesIO()
sf.write(buf, audio, sr, format="WAV")
buf.seek(0)
return StreamingResponse(buf, media_type="audio/wav")
@app.post("/speech/clone")
def speech_clone(
text: str = Form(...),
language: str = Form(""),
ref_audio: UploadFile = File(...),
):
"""Clone a voice from ref_audio and synthesize text with it."""
ref_bytes = ref_audio.file.read()
suffix = os.path.splitext(ref_audio.filename or "")[1] or ".wav"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(ref_bytes)
tmp_path = tmp.name
try:
audio, sr = _run_generation(text, language, reference=[tmp_path])
finally:
os.remove(tmp_path)
buf = io.BytesIO()
sf.write(buf, audio, sr, format="WAV")
buf.seek(0)
return StreamingResponse(buf, media_type="audio/wav")
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл
Просмотреть файл