Spaces:
Running on Zero
Running on Zero
File size: 8,763 Bytes
8d6ad36 1e7dd80 8d6ad36 1e7dd80 8d6ad36 1e7dd80 8d6ad36 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """Spaces adapter; the vendored WaveGen inference implementations are unchanged."""
from __future__ import annotations
import math
import hashlib
import os
from pathlib import Path
import re
import tempfile
import numpy as np
import soundfile as sf
MODEL_REPO = "LeeSangHoon/WaveGen"
MODEL_REVISION = "da53c835a39272c05e01526fe3c10b2b9d4ce60b"
MAX_SECONDS = 30
MAX_FILE_BYTES = 20 * 1024 * 1024
MAX_TEXT_CHARS = 2000
MODEL_CHOICES = [("WaveGen V-Spec (Recommended)", "V-Spec"),
("WaveGen-Flash (4 Steps)", "Flash")]
ASSETS = {
"V-Spec": ("WaveGen_VSpec/model_1400000.safetensors",
"WaveGen_VSpec/WaveGen_VSpec.yaml", "WaveGen_VSpec/vocab.txt"),
"Flash": ("WaveGen_Flash/model.safetensors",
"WaveGen_Flash/WaveGen_Flash.yaml", "WaveGen_Flash/vocab.txt"),
}
def _number(value, name, low, high, *, integer=False):
if (isinstance(value, bool) or not isinstance(value, (int, float))
or not math.isfinite(value) or not low <= value <= high
or (integer and value != int(value))):
raise ValueError(f"{name} must be {'an integer' if integer else 'a number'} between {low} and {high}.")
return int(value) if integer else float(value)
def validate_request(ref_audio, ref_text, text, model_variant="V-Spec", seed=0,
speed=1.0, mel_temperature=0.0):
"""Bound decoded input and mirror native UTF-8 duration prediction on CPU."""
if model_variant not in ASSETS:
raise ValueError("Select WaveGen V-Spec or WaveGen-Flash.")
for value, name in ((ref_text, "Reference transcript"), (text, "Text to generate")):
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{name} is required.")
if len(value) > MAX_TEXT_CHARS:
raise ValueError(f"{name} must be at most {MAX_TEXT_CHARS:,} characters.")
seed = _number(seed, "Seed", 0, 2**32 - 1, integer=True)
speed = _number(speed, "Speed", 0.5, 2)
mel_temperature = _number(mel_temperature, "Mel temperature", 0, 1)
if not isinstance(ref_audio, (str, os.PathLike)) or not Path(ref_audio).is_file():
raise ValueError("Upload or record reference speech first.")
path = Path(ref_audio)
if path.stat().st_size > MAX_FILE_BYTES:
raise ValueError("Reference audio must be at most 20 MB.")
try:
info = sf.info(str(path))
if not 0 < info.duration <= MAX_SECONDS:
raise ValueError("Reference speech must be nonempty and at most 30 seconds.")
if not 8000 <= info.samplerate <= 192000 or info.channels not in (1, 2):
raise ValueError("Use mono or stereo reference speech at 8–192 kHz.")
audio, _ = sf.read(str(path), dtype="float32", always_2d=True)
except (RuntimeError, OSError) as exc:
raise ValueError("Could not decode the reference audio. Try a WAV file.") from exc
mono = audio.mean(axis=1)
if not np.isfinite(audio).all() or not len(mono) or np.sqrt(np.mean(mono**2)) <= 1e-8:
raise ValueError("Reference audio must contain audible, finite speech.")
# torchaudio.Resample rounds the output length up, then the native loader
# truncates/pads the reference to a whole 480-sample waveform patch.
resampled_length = (info.frames * 24000 + info.samplerate - 1) // info.samplerate
prompt_length = max(resampled_length // 480 * 480, 480)
duration_ref_text = ref_text + (" " if len(ref_text[-1].encode("utf-8")) == 1 else "")
total_length = prompt_length + int(
prompt_length / len(duration_ref_text.encode("utf-8")) * len(text.encode("utf-8")) / speed
)
if total_length <= prompt_length:
raise ValueError("Predicted speech is empty. Enter more text or reduce speed.")
if total_length > MAX_SECONDS * 24000:
raise ValueError("Reference plus predicted speech exceeds 30 seconds. Shorten the text or reference audio.")
return dict(ref_audio=str(path), ref_text=ref_text, text=text, model_variant=model_variant,
seed=seed, speed=speed, mel_temperature=mel_temperature,
predicted_seconds=total_length / 24000)
def model_description(model_variant):
if model_variant == "Flash":
return "**WaveGen-Flash:** 4 steps · CFG 0 · config dropout · final Mel head skipped."
return "**WaveGen V-Spec:** 50 steps · Prompt Anchor · Scheduled K3 (CFG 1.25) · config dropout · final Mel head skipped."
def load_models(*, model_dir=None, repo_id=MODEL_REPO, revision=MODEL_REVISION):
"""Call after importing spaces, before Gradio launch / GPU worker creation.
CPU loading avoids the native loader's CUDA/BF16 device probes. ZeroGPU
captures startup model.to('cuda'); actual CUDA checks belong in generate().
"""
import torch
from wavegen_v_spec.infer.utils_infer import WaveGen
from wavegen_flash import WaveGenFlash
torch.set_num_threads(1)
factories = {"V-Spec": WaveGen, "Flash": WaveGenFlash}
engines = {}
resolved_revision = revision
if model_dir is None and not re.fullmatch(r"[0-9a-fA-F]{40}", revision):
from huggingface_hub import model_info
resolved_revision = model_info(repo_id, revision=revision).sha
if not resolved_revision or not re.fullmatch(r"[0-9a-fA-F]{40}", resolved_revision):
raise RuntimeError("The model revision did not resolve to an immutable commit.")
if model_dir is None:
from huggingface_hub import hf_hub_download
# On Linux, the normal Hub snapshot cache uses symlinks to extensionless
# blobs. The native loader resolves paths before checking the format, so
# request actual named files instead of passing snapshot symlinks to it.
repo_key = hashlib.sha256(repo_id.encode("utf-8")).hexdigest()[:16]
download_root = Path(tempfile.gettempdir()) / "wavegen-hub" / repo_key / resolved_revision
for variant, (checkpoint, config, vocabulary) in ASSETS.items():
print(f"Loading {variant} for inference", flush=True)
options = dict(device="cpu", dtype="fp32")
if variant == "V-Spec":
options["use_ema"] = True
if model_dir is not None:
root = Path(model_dir)
engine = factories[variant].from_checkpoint(
checkpoint=root / checkpoint, vocab_file=root / vocabulary,
config=root / config, **options)
else:
checkpoint_path, config_path, vocabulary_path = [hf_hub_download(
repo_id=repo_id, filename=filename, revision=resolved_revision,
local_dir=download_root,
) for filename in (checkpoint, config, vocabulary)]
engine = factories[variant].from_checkpoint(
checkpoint=checkpoint_path, config=config_path,
vocab_file=vocabulary_path, **options)
engine.hub_repo_id = repo_id
engine.hub_revision = resolved_revision
if engine.sample_rate != 24000:
raise ValueError("The demo requires the 24 kHz model bundles.")
engine.model.requires_grad_(False).to("cuda")
engine.device = torch.device("cuda")
engine.dtype = torch.bfloat16
engines[variant] = engine
return engines
def synthesize_request(engines, request):
variant = request["model_variant"]
engine = engines[variant]
options = {key: request[key] for key in
("ref_audio", "ref_text", "text", "seed", "speed", "mel_temperature")}
options["compute_final_mel"] = False
if variant == "V-Spec":
options.update(steps=50, cfg_schedule="scheduled_k3", cfg_strength=1.25,
inference_method="Prompt_xt_Anchor", inference_dropout=None,
max_duration_frames=1500)
else:
options["max_duration_seconds"] = MAX_SECONDS
return engine.synthesize(**options)
def save_audio(sample_rate, waveform, output_dir):
"""Preserve native levels: return PCM16 WAV, not a Gradio numpy-audio tuple."""
waveform = np.asarray(waveform)
if sample_rate != 24000 or waveform.ndim != 1 or not waveform.size or not np.isfinite(waveform).all():
raise RuntimeError("The model returned invalid audio.")
if waveform.size > MAX_SECONDS * sample_rate:
raise RuntimeError("The generated audio exceeds the demo duration limit.")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(prefix="wavegen-", suffix=".wav", dir=output_dir, delete=False) as f:
path = Path(f.name)
try:
sf.write(str(path), waveform, sample_rate, subtype="PCM_16")
except Exception:
path.unlink(missing_ok=True)
raise
return str(path)
|