feat: add OmniVoice TTS container, upgrade whisper model, tune qwentts tone

This commit is contained in:
2026-09-10 20:43:33 +02:00
szülő 7a03174b60
commit 47d1f10e68
8 fájl változott, egészen pontosan 216 új sor hozzáadva és 4 régi sor törölve
+22
Fájl megtekintése
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
### Build the omnivoice 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/omnivoice:$DEVICE" \
-t localhost/omnivoice:latest \
-f "$SCRIPT_DIR/omnivoice.Containerfile" \
"$SCRIPT_DIR"
+8
Fájl megtekintése
@@ -0,0 +1,8 @@
#!/bin/bash
# Starts the FastAPI server; model weights download on first request into /app/models
set -e
echo "=== OmniVoice Server ==="
echo
exec uvicorn server:app --host 0.0.0.0 --port 8000 --app-dir /app "$@"
@@ -0,0 +1,42 @@
### OmniVoice Container (https://github.com/k2-fsa/OmniVoice) — 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
# sox/ffmpeg: needed at runtime for reference-audio format handling
RUN apt-get update \
&& apt-get install -y --no-install-recommends sox 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 build)
RUN if [ "$DEVICE" = "cpu" ]; then \
pip install --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cpu; \
fi
# Install omnivoice and a minimal API server
RUN pip install --no-cache-dir omnivoice 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 []
@@ -0,0 +1,13 @@
omnivoice.example.com {
import gateway_error
request_body {
max_size 100MB
}
reverse_proxy omnivoice:8000
log {
output file /var/log/caddy/omnivoice_access.log
}
}
@@ -0,0 +1,36 @@
[Unit]
Description=OmniVoice TTS Server
After=network-online.target
Wants=network-online.target
[Container]
Image=localhost/omnivoice:latest
ContainerName=omnivoice
# 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=8001:8000
# Persist downloaded model weights across restarts (adjust host path as needed)
Volume=/srv/containers/omnivoice/models:/app/models:Z
# Optional: override the default checkpoint
#Environment=OMNIVOICE_MODEL=k2-fsa/OmniVoice
[Service]
Restart=on-failure
RestartSec=5
TimeoutStartSec=300
[Install]
WantedBy=default.target
+91
Fájl megtekintése
@@ -0,0 +1,91 @@
"""Minimal FastAPI server exposing OmniVoice zero-shot voice cloning (/speech/clone only). https://github.com/k2-fsa/OmniVoice"""
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 omnivoice import OmniVoice
MODEL_NAME = os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice")
_forced_device = os.environ.get("OMNIVOICE_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:0"
elif getattr(torch, "xpu", None) is not None and torch.xpu.is_available():
DEVICE = "xpu"
else:
DEVICE = "cpu"
# float16 is unsupported on CPU and broken on some XPU/oneDNN SDPA builds.
DTYPE = torch.float16 if DEVICE.startswith("cuda") else torch.float32
# OmniVoice hardcodes fp16 for its internal ASR sub-model on cuda/xpu, which breaks there; keep it on CPU by default.
ASR_DEVICE = os.environ.get("OMNIVOICE_ASR_DEVICE", "cpu")
app = FastAPI(title="OmniVoice server")
model: OmniVoice | None = None
@app.on_event("startup")
def load_model():
global model
print(f"Loading OmniVoice model {MODEL_NAME} on {DEVICE}...")
model = OmniVoice.from_pretrained(
MODEL_NAME,
device_map=DEVICE,
dtype=DTYPE,
asr_device=ASR_DEVICE,
)
print(f"OmniVoice model loaded. ASR (ref_text auto-transcribe) on {ASR_DEVICE}.")
@app.get("/health")
def health():
return {
"status": "ok",
"model": MODEL_NAME,
"device": DEVICE,
}
@app.post("/speech/clone")
def speech_clone(
text: str = Form(...),
language: str = Form(""),
ref_text: str = Form(""),
instruct: str = Form(""),
speed: float = Form(1.0),
ref_audio: UploadFile = File(...),
):
"""Clone a voice from ref_audio (ref_text auto-transcribed via Whisper if omitted) and synthesize text with it."""
if model is None:
raise HTTPException(status_code=503, detail="Model not loaded yet")
ref_bytes = ref_audio.file.read()
suffix = os.path.splitext(ref_audio.filename or "")[1] or ".wav" # generate() needs a file path, not bytes
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(ref_bytes)
tmp_path = tmp.name
try:
audio = model.generate(
text=text,
language=language or None,
ref_audio=tmp_path,
ref_text=ref_text or None,
instruct=instruct or None,
speed=speed,
)
finally:
os.remove(tmp_path)
buf = io.BytesIO()
sf.write(buf, audio[0], model.sampling_rate, format="WAV")
buf.seek(0)
return StreamingResponse(buf, media_type="audio/wav")
@@ -36,7 +36,7 @@ Volume=/srv/containers/qwen-tts/models:/app/models:Z
# `instruct` (Qwen3-TTS has no numeric speed/style knob, only natural-language
# steering). Only applies to /speech (CustomVoice); /speech/clone has no
# instruct support.
#Environment=QWEN_TTS_DEFAULT_INSTRUCT=This is a voice message between two close friends on WhatsApp. Speak in a casual, warm, friendly tone, not formal or professional-sounding.
Environment=QWEN_TTS_DEFAULT_INSTRUCT=This is a voice message between two close friends on WhatsApp. Speak calmly and naturally at a steady, even pace, like ordinary spoken conversation. Keep the tone warm and casual but restrained: avoid dramatic emphasis, exaggerated excitement, or theatrical intonation. Use natural, evenly spaced pauses between sentences, not rushed or drawn out.
[Service]
@@ -15,11 +15,11 @@ PodmanArgs=--group-add=keep-groups --ipc=host
SecurityLabelType=container_runtime_t
# Whisper configuration
Environment=WHISPER_MODEL_NAME=small
Environment=WHISPER_MODEL_FILE=ggml-small.bin
Environment=WHISPER_MODEL_NAME=large-v3-turbo
Environment=WHISPER_MODEL_FILE=ggml-large-v3-turbo.bin
Environment=WHISPER_LANGUAGE=auto
Environment=WHISPER_TRANSLATE=false
Environment=WHISPER_DTW=large.v3
#Environment=WHISPER_DTW=large.v3
Environment=WHISPER_DETECT_LANGUAGE=false
Environment=HF_HOME=/app/models/.huggingface