feat: add OmniVoice TTS container, upgrade whisper model, tune qwentts tone
Этот коммит содержится в:
Исполняемый файл
+22
@@ -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"
|
||||
Исполняемый файл
@@ -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
|
||||
@@ -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
|
||||
|
||||
Ссылка в новой задаче
Block a user