voxcpm / inference.py
ahmad4raza's picture
Upload inference.py with huggingface_hub
2e864b6 verified
Raw
History Blame Contribute Delete
9.07 kB
#!/usr/bin/env python3
"""
Generate audio for each input text with the Priyanka LoRA and full-finetune checkpoints.
Examples:
python inference.py --text "नमस्ते, आप कैसे हैं?" --text "This is a second sample."
python inference.py --text-file texts.txt --output-dir outputs/priyanka_inference
python inference.py \
--text "This is voice cloning." \
--prompt-audio examples/reference_speaker.wav \
--prompt-text "Reference speaker transcript."
"""
from __future__ import annotations
import argparse
import gc
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
import soundfile as sf
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
from voxcpm.core import VoxCPM # noqa: E402
from voxcpm.model.voxcpm import LoRAConfig # noqa: E402
CHECKPOINTS_ROOT = ROOT / "checkpoints"
DEFAULT_CHECKPOINTS = (
{
"name": "03_priyanka_lora_step_0000999",
"kind": "lora",
"path": CHECKPOINTS_ROOT / "03_priyanka_lora" / "step_0000999",
},
{
"name": "04_priyanka_full_step_0000500",
"kind": "full",
"path": CHECKPOINTS_ROOT / "04_priyanka_full" / "step_0000500",
},
)
@dataclass(frozen=True)
class CheckpointSpec:
name: str
kind: str
path: Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate WAV files for every provided text using two Priyanka VoxCPM checkpoints."
)
parser.add_argument(
"--text",
action="append",
default=[],
help="Text to synthesize. Can be passed multiple times.",
)
parser.add_argument(
"--text-file",
type=Path,
default=None,
help="UTF-8 text file with one synthesis text per non-empty line.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=ROOT / "outputs" / "priyanka_inference",
help="Directory where generated WAV files and manifest.json are saved.",
)
parser.add_argument(
"--prompt-audio",
type=Path,
default=None,
help="Optional reference/prompt WAV path. Must be used with --prompt-text.",
)
parser.add_argument(
"--prompt-text",
default=None,
help="Transcript for --prompt-audio.",
)
parser.add_argument(
"--reference-audio",
type=Path,
default=None,
help="Optional VoxCPM2 reference WAV path for voice cloning.",
)
parser.add_argument("--cfg-value", type=float, default=2.0, help="CFG scale.")
parser.add_argument(
"--inference-timesteps",
type=int,
default=10,
help="Number of diffusion inference steps.",
)
parser.add_argument("--max-len", type=int, default=600, help="Maximum generation length.")
parser.add_argument("--normalize", action="store_true", help="Enable text normalization.")
parser.add_argument(
"--device",
default=None,
help="Runtime device, for example auto, cuda, cuda:0, mps, or cpu.",
)
parser.add_argument(
"--no-optimize",
action="store_true",
help="Disable VoxCPM warmup/optimization.",
)
parser.add_argument(
"--denoise",
action="store_true",
help="Run denoiser for prompt/reference audio. The denoiser is loaded only when this is set.",
)
return parser.parse_args()
def read_texts(args: argparse.Namespace) -> list[str]:
texts = [text.strip() for text in args.text if text and text.strip()]
if args.text_file:
if not args.text_file.exists():
raise FileNotFoundError(f"--text-file does not exist: {args.text_file}")
file_texts = [
line.strip()
for line in args.text_file.read_text(encoding="utf-8").splitlines()
if line.strip()
]
texts.extend(file_texts)
if not texts:
raise ValueError("Provide at least one input with --text or --text-file.")
return texts
def validate_audio_args(args: argparse.Namespace) -> None:
if (args.prompt_audio is None) != (args.prompt_text is None):
raise ValueError("--prompt-audio and --prompt-text must be provided together.")
for path_arg in (args.prompt_audio, args.reference_audio):
if path_arg is not None and not path_arg.exists():
raise FileNotFoundError(f"Audio file does not exist: {path_arg}")
def load_lora_model(ckpt_dir: Path, args: argparse.Namespace) -> VoxCPM:
lora_config_path = ckpt_dir / "lora_config.json"
if not lora_config_path.exists():
raise FileNotFoundError(f"Missing LoRA config: {lora_config_path}")
with lora_config_path.open("r", encoding="utf-8") as f:
lora_info = json.load(f)
base_model = lora_info.get("base_model")
if not base_model:
raise ValueError(f"'base_model' is missing in {lora_config_path}")
lora_cfg = LoRAConfig(**lora_info.get("lora_config", {}))
return VoxCPM.from_pretrained(
hf_model_id=base_model,
load_denoiser=args.denoise,
optimize=not args.no_optimize,
device=args.device,
lora_config=lora_cfg,
lora_weights_path=str(ckpt_dir),
)
def load_full_model(ckpt_dir: Path, args: argparse.Namespace) -> VoxCPM:
return VoxCPM.from_pretrained(
hf_model_id=str(ckpt_dir),
load_denoiser=args.denoise,
optimize=not args.no_optimize,
device=args.device,
)
def load_model(spec: CheckpointSpec, args: argparse.Namespace) -> VoxCPM:
if not spec.path.exists():
raise FileNotFoundError(f"Checkpoint does not exist: {spec.path}")
if spec.kind == "lora":
return load_lora_model(spec.path, args)
if spec.kind == "full":
return load_full_model(spec.path, args)
raise ValueError(f"Unsupported checkpoint kind: {spec.kind}")
def slugify(value: str, max_len: int = 48) -> str:
value = re.sub(r"\s+", "_", value.strip().lower())
value = re.sub(r"[^0-9a-zA-Z_\-]+", "", value)
value = value.strip("_-")
return (value[:max_len].strip("_-") or "text")
def output_path(output_dir: Path, spec: CheckpointSpec, index: int, text: str) -> Path:
text_slug = slugify(text)
return output_dir / f"text_{index:03d}_{spec.name}_{text_slug}.wav"
def release_model(model: VoxCPM) -> None:
del model
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
except Exception:
pass
def main() -> None:
args = parse_args()
texts = read_texts(args)
validate_audio_args(args)
args.output_dir.mkdir(parents=True, exist_ok=True)
specs = [CheckpointSpec(**item) for item in DEFAULT_CHECKPOINTS]
manifest = {
"output_dir": str(args.output_dir),
"texts": texts,
"checkpoints": [],
"files": [],
}
print(f"Generating {len(texts)} text(s) with {len(specs)} checkpoint(s).", file=sys.stderr)
for spec in specs:
print(f"\nLoading {spec.name}: {spec.path}", file=sys.stderr)
model = load_model(spec, args)
sample_rate = model.tts_model.sample_rate
manifest["checkpoints"].append(
{"name": spec.name, "kind": spec.kind, "path": str(spec.path), "sample_rate": sample_rate}
)
try:
for index, text in enumerate(texts, start=1):
wav_path = output_path(args.output_dir, spec, index, text)
print(f"[{spec.name}] text {index}/{len(texts)} -> {wav_path}", file=sys.stderr)
audio_np = model.generate(
text=text,
prompt_wav_path=str(args.prompt_audio) if args.prompt_audio else None,
prompt_text=args.prompt_text,
reference_wav_path=str(args.reference_audio) if args.reference_audio else None,
cfg_value=args.cfg_value,
inference_timesteps=args.inference_timesteps,
max_len=args.max_len,
normalize=args.normalize,
denoise=args.denoise,
)
sf.write(str(wav_path), audio_np, sample_rate)
manifest["files"].append(
{
"checkpoint": spec.name,
"text_index": index,
"text": text,
"path": str(wav_path),
"duration_seconds": len(audio_np) / sample_rate,
}
)
finally:
release_model(model)
manifest_path = args.output_dir / "manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\nDone. Wrote {len(manifest['files'])} WAV file(s).", file=sys.stderr)
print(f"Manifest: {manifest_path}", file=sys.stderr)
if __name__ == "__main__":
main()