modelbeast/server/operators/voice_clone_local/run.py
type-two 110b2298b7 audio/video/text operator suite: stt_local, lipsync_local, voice_clone_local, wan_video, llm_local
- stt_local: Whisper large-v3-turbo MLX, transcript + timestamped json
- lipsync_local: Rhubarb visemes (CPU lane), dialog-guided
- voice_clone_local: Chatterbox on MPS (needs setuptools<81 for perth/pkg_resources)
- wan_video: Wan 2.2 TI2V-5B t2v/i2v via resident ComfyUI, frames->mp4
- llm_local: Qwen3-30B-A3B-4bit dialogue writer (primary-only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:26:51 +10:00

45 lines
1.3 KiB
Python

import argparse
import json
import os
import sys
import time
from pathlib import Path
os.environ.setdefault("TQDM_DISABLE", "1") # keep sampling bars out of job logs
ap = argparse.ArgumentParser()
ap.add_argument("--input", action="append", default=[])
ap.add_argument("--outdir", required=True)
ap.add_argument("--params", default="{}")
a = ap.parse_args()
p = json.loads(a.params)
text = (p.get("text") or "").strip()
if not text:
print("ERROR: text is required")
sys.exit(1)
if not a.input:
print("ERROR: needs a reference voice WAV as input")
sys.exit(1)
import torch # noqa: E402
import torchaudio # noqa: E402
from chatterbox.tts import ChatterboxTTS # noqa: E402
device = "mps" if torch.backends.mps.is_available() else "cpu"
t0 = time.time()
model = ChatterboxTTS.from_pretrained(device=device)
print(f"model loaded on {device} in {time.time() - t0:.1f}s", flush=True)
t1 = time.time()
wav = model.generate(
text,
audio_prompt_path=a.input[0],
exaggeration=float(p.get("exaggeration", 0.5)),
cfg_weight=float(p.get("cfg_weight", 0.5)),
)
out = Path(a.outdir) / f"clone_{Path(a.input[0]).stem}.wav"
torchaudio.save(str(out), wav, model.sr)
dur = wav.shape[-1] / model.sr
print(f"done in {time.time() - t1:.1f}s: {out.name} ({dur:.1f}s)", flush=True)