feat: containerize VoxCPM2 vLLM-Omni deployment
This commit is contained in:
7
.containerignore
Normal file
7
.containerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
runtime/
|
||||
reference-audio/
|
||||
*.wav
|
||||
*.opus
|
||||
*.pcm
|
||||
*.safetensors
|
||||
resposta.headers
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
hf-cache/
|
||||
runtime/
|
||||
voices/
|
||||
reference-audio/
|
||||
*.wav
|
||||
*.opus
|
||||
*.pcm
|
||||
*.safetensors
|
||||
resposta.headers
|
||||
16
Containerfile
Normal file
16
Containerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM vllm/vllm-omni:v0.28.0
|
||||
|
||||
RUN python -m pip install --no-cache-dir \
|
||||
voxcpm \
|
||||
soundfile \
|
||||
httpx \
|
||||
ninja
|
||||
|
||||
COPY config/voxcpm2-a10-container.yaml /opt/config/voxcpm2.yaml
|
||||
|
||||
RUN mkdir -p /opt/vllm-omni/examples/online_serving/text_to_speech/voxcpm2
|
||||
|
||||
COPY tools/precompute_custom_voice.py \
|
||||
/opt/vllm-omni/examples/online_serving/text_to_speech/voxcpm2/precompute_custom_voice.py
|
||||
|
||||
CMD ["vllm", "serve", "openbmb/VoxCPM2", "--omni", "--deploy-config", "/opt/config/voxcpm2.yaml", "--host", "0.0.0.0", "--port", "8000"]
|
||||
23
config/voxcpm2-a10-container.yaml
Normal file
23
config/voxcpm2-a10-container.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
async_chunk: false
|
||||
dtype: bfloat16
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
devices: "0"
|
||||
max_num_seqs: 2
|
||||
gpu_memory_utilization: 0.9
|
||||
kv_cache_memory_bytes: 6442450944
|
||||
enforce_eager: true
|
||||
trust_remote_code: true
|
||||
enable_prefix_caching: false
|
||||
max_num_batched_tokens: 4096
|
||||
max_model_len: 4096
|
||||
custom_voice_dir: /opt/voices
|
||||
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 4096
|
||||
seed: 42
|
||||
repetition_penalty: 1.0
|
||||
@@ -2,4 +2,9 @@
|
||||
name = "voxcpm-vllm-poc"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"ninja>=1.13.2",
|
||||
"soundfile>=0.14.0",
|
||||
"voxcpm>=2.0.3",
|
||||
]
|
||||
|
||||
128
tools/precompute_custom_voice.py
Normal file
128
tools/precompute_custom_voice.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Pre-compute VoxCPM2 custom voice profiles.
|
||||
|
||||
The generated directory can be passed to the server via
|
||||
``custom_voice_dir`` in ``vllm_omni/deploy/voxcpm2.yaml``. Requests can then
|
||||
use ``/v1/audio/speech`` with ``voice="<name>"`` and no per-request ref_audio.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from safetensors.torch import save_file
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni.utils.custom_voice_io import safe_voice_stem # noqa: E402
|
||||
|
||||
MANIFEST_NAME = "custom_voice_manifest.json"
|
||||
|
||||
|
||||
def _load_tts(model: str, device: torch.device):
|
||||
from vllm_omni.model_executor.models.voxcpm2.voxcpm2_import_utils import import_voxcpm2_core
|
||||
|
||||
VoxCPM = import_voxcpm2_core()
|
||||
native = VoxCPM.from_pretrained(model, load_denoiser=False, optimize=False)
|
||||
return native.tts_model.to(device).eval()
|
||||
|
||||
|
||||
def _load_manifest(output_dir: Path, model: str) -> dict[str, Any]:
|
||||
path = output_dir / MANIFEST_NAME
|
||||
if path.exists():
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"model_type": "voxcpm2",
|
||||
"model": model,
|
||||
"voices": {},
|
||||
}
|
||||
|
||||
|
||||
def _write_voice(
|
||||
*,
|
||||
model: str,
|
||||
output_dir: Path,
|
||||
voice_name: str,
|
||||
ref_audio: str,
|
||||
prompt_text: str | None,
|
||||
mode: str,
|
||||
speaker_description: str | None,
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
if mode in ("continuation", "ref_continuation") and not prompt_text:
|
||||
raise ValueError("--prompt-text is required for continuation/ref_continuation modes")
|
||||
|
||||
tts = _load_tts(model, device)
|
||||
tensors: dict[str, torch.Tensor] = {}
|
||||
with torch.inference_mode():
|
||||
if mode in ("reference", "ref_continuation"):
|
||||
tensors["ref_audio_feat"] = tts._encode_wav(ref_audio, padding_mode="right").float().cpu().contiguous()
|
||||
if mode in ("continuation", "ref_continuation"):
|
||||
tensors["audio_feat"] = tts._encode_wav(ref_audio, padding_mode="left").float().cpu().contiguous()
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
filename = f"{safe_voice_stem(voice_name)}.safetensors"
|
||||
save_file(tensors, str(output_dir / filename))
|
||||
|
||||
manifest = _load_manifest(output_dir, model)
|
||||
entry: dict[str, Any] = {
|
||||
"name": voice_name,
|
||||
"file": filename,
|
||||
"mode": mode,
|
||||
}
|
||||
if "ref_audio_feat" in tensors:
|
||||
entry["ref_audio_feat_len"] = int(tensors["ref_audio_feat"].shape[0])
|
||||
if "audio_feat" in tensors:
|
||||
entry["audio_feat_len"] = int(tensors["audio_feat"].shape[0])
|
||||
if prompt_text:
|
||||
entry["prompt_text"] = prompt_text
|
||||
if speaker_description:
|
||||
entry["speaker_description"] = speaker_description
|
||||
|
||||
manifest.setdefault("voices", {})[voice_name] = entry
|
||||
(output_dir / MANIFEST_NAME).write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"Wrote {output_dir / filename}")
|
||||
print(f"Updated {output_dir / MANIFEST_NAME}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Pre-compute VoxCPM2 custom voice profile")
|
||||
parser.add_argument("--model", default="openbmb/VoxCPM2", help="VoxCPM2 model path or Hugging Face ID")
|
||||
parser.add_argument("--voice-name", required=True)
|
||||
parser.add_argument("--ref-audio", required=True)
|
||||
parser.add_argument(
|
||||
"--prompt-text",
|
||||
default=None,
|
||||
help="Transcript of ref audio for continuation/ref_continuation modes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["reference", "continuation", "ref_continuation"],
|
||||
default="reference",
|
||||
)
|
||||
parser.add_argument("--speaker-description", default=None)
|
||||
parser.add_argument("--output-dir", required=True)
|
||||
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
||||
args = parser.parse_args()
|
||||
|
||||
_write_voice(
|
||||
model=args.model,
|
||||
output_dir=Path(args.output_dir),
|
||||
voice_name=args.voice_name,
|
||||
ref_audio=args.ref_audio,
|
||||
prompt_text=args.prompt_text,
|
||||
mode=args.mode,
|
||||
speaker_description=args.speaker_description,
|
||||
device=torch.device(args.device),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user