| |
| """ |
| 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 |
| from voxcpm.model.voxcpm import LoRAConfig |
|
|
|
|
| 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() |
|
|