audio8-voice-clone / src /inference.py
Utkarsh65447's picture
fix: ZeroGPU spaces.GPU decorator + mount keep demo
188aafb verified
Raw History Blame Contribute Delete
6.54 kB
"""
src/inference.py - Audio8 0.1b inference wrapper (free-tier: ZeroGPU / CPU fallback)
Single model load, BF16 on CUDA, float32 on CPU. 1.7GB total.
"""
import os
import time
import tempfile
from pathlib import Path
from typing import Tuple, Optional
import torch
import soundfile as sf
import librosa
# ZeroGPU support for HF Spaces free tier
try:
import spaces
HAS_SPACES = True
except ImportError:
HAS_SPACES = False
spaces = None
def _gpu(func):
"""Wrap with spaces.GPU if available (ZeroGPU), else no-op."""
if HAS_SPACES:
return spaces.GPU(duration=90)(func)
return func
MODEL_ID = "Audio8/Audio8-TTS-Preview-0.1b"
# Global singleton
_processor = None
_model = None
_device = None
_dtype = None
_sample_rate = None
_model_loaded = False
_load_error: Optional[str] = None
def get_device() -> str:
if torch.cuda.is_available():
return "cuda"
return "cpu"
def get_dtype(device: str):
if device == "cuda":
# bfloat16 halves VRAM vs float32, fits free 16GB
return torch.bfloat16
return torch.float32
def is_model_loaded() -> bool:
return _model_loaded and _model is not None
def get_model_info() -> dict:
return {
"model_id": MODEL_ID,
"device": _device or get_device(),
"dtype": str(_dtype) if _dtype else str(get_dtype(get_device())),
"sample_rate": _sample_rate or 44100,
"loaded": is_model_loaded(),
"error": _load_error,
"cuda_available": torch.cuda.is_available(),
}
def load_model():
"""Load processor + model once. Thread-safe singleton."""
global _processor, _model, _device, _dtype, _sample_rate, _model_loaded, _load_error
if is_model_loaded():
return _processor, _model
_device = get_device()
_dtype = get_dtype(_device)
print(f"[inference] Loading {MODEL_ID} on {_device} ({_dtype}) ...")
t0 = time.time()
try:
from transformers import AutoProcessor, AutoModel
_processor = AutoProcessor.from_pretrained(
MODEL_ID, trust_remote_code=True
)
_model = AutoModel.from_pretrained(
MODEL_ID, trust_remote_code=True, dtype=_dtype
).eval().to(_device)
_sample_rate = getattr(_model.config, "codec_sample_rate", 44100)
_model_loaded = True
_load_error = None
print(f"[inference] Loaded in {time.time()-t0:.1f}s | sr={_sample_rate} | device={_device}")
# Warmup compile check (optional)
# torch.compile not used on free CPU to avoid high RAM
except Exception as e:
_load_error = str(e)
print(f"[inference] Load failed: {e}")
raise
return _processor, _model
def validate_reference_audio(path: str) -> Tuple[bool, str]:
"""Check duration 3-20s, exists, <10MB."""
if not path or not os.path.exists(path):
return False, "Reference audio file not found."
try:
size_mb = os.path.getsize(path) / (1024 * 1024)
if size_mb > 10:
return False, f"File too large ({size_mb:.1f}MB > 10MB)."
dur = librosa.get_duration(path=path)
if dur < 1.0:
return False, f"Audio too short ({dur:.1f}s). Use 3-15s for best clone."
if dur > 20:
return False, f"Audio too long ({dur:.1f}s). Trim to <20s."
if dur < 3:
# Warning, not blocking
return True, f"Warning: short audio ({dur:.1f}s) may reduce similarity. 5-15s recommended."
return True, "OK"
except Exception as e:
return False, f"Cannot read audio: {e}"
@_gpu
@torch.inference_mode()
def synthesize(
reference_audio_path: str,
reference_text: str,
text: str,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: int = 50,
max_new_tokens: int = 512,
) -> Tuple[str, str]:
"""
Returns (output_wav_path, info_message)
Raises on failure.
"""
if not reference_audio_path:
raise ValueError("Please upload a reference audio (3-15s).")
if not reference_text or not reference_text.strip():
raise ValueError("Reference transcript is required and must match the audio exactly.")
if not text or not text.strip():
raise ValueError("Please enter text to synthesize.")
if len(text) > 800:
raise ValueError(f"Text too long ({len(text)} chars). Limit 800 characters for free tier.")
ok, msg = validate_reference_audio(reference_audio_path)
if not ok:
raise ValueError(msg)
processor, model = load_model()
device = _device
# Processor expects lists
inputs = processor(
text=[text.strip()],
reference_audio=[reference_audio_path],
reference_text=[reference_text.strip()],
return_tensors="pt",
)
inputs = {k: v.to(device) for k, v in inputs.items()}
t0 = time.time()
output = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
do_sample=True,
return_dict_in_generate=True,
)
waveforms, waveform_lengths = model.decode_audio(output.codes)
gen_time = time.time() - t0
# Save temp wav
tmp = tempfile.mktemp(suffix=".wav")
audio = waveforms[0, : int(waveform_lengths[0])].float().cpu().numpy()
sf.write(tmp, audio, _sample_rate)
dur = len(audio) / _sample_rate
info = f"Generated {dur:.1f}s in {gen_time:.1f}s on {device} | temp={temperature} top_p={top_p} | sr={_sample_rate}"
if msg.startswith("Warning"):
info = msg + " | " + info
return tmp, info
def synthesize_no_clone(
text: str,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: int = 50,
max_new_tokens: int = 512,
) -> Tuple[str, str]:
"""TTS without voice cloning (no reference)."""
if not text or not text.strip():
raise ValueError("Please enter text.")
processor, model = load_model()
device = _device
inputs = processor(text=[text.strip()], return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}
with torch.inference_mode():
out = model.generate(**inputs, max_new_tokens=max_new_tokens, temperature=temperature, top_p=top_p, top_k=top_k, do_sample=True, return_dict_in_generate=True)
wavs, lens = model.decode_audio(out.codes)
tmp = tempfile.mktemp(suffix=".wav")
audio = wavs[0, : int(lens[0])].float().cpu().numpy()
sf.write(tmp, audio, _sample_rate)
return tmp, f"Generated without cloning on {device}"