Pushkar02-n's picture
OmniVoice CPU Space
a3003cb
Raw
History Blame Contribute Delete
12.3 kB
"""
OmniVoice on CPU — Hugging Face Space
=====================================
A CPU-only Gradio front end for k2-fsa/OmniVoice (0.6B zero-shot TTS).
Design notes for CPU:
- float32 everywhere (fp16 CPU kernels are slow or unsupported).
- Model is loaded once at startup and reused across requests.
- Torch thread count is pinned to the vCPUs the Space actually has.
- Diffusion steps default to 16 instead of 32 (roughly halves compute).
- Text length is capped so a single request cannot hog the worker.
"""
import os
import time
import tempfile
import threading
# Must be set before torch is imported to take effect on some builds.
_CPU_COUNT = os.cpu_count() or 2
os.environ.setdefault("OMP_NUM_THREADS", str(_CPU_COUNT))
os.environ.setdefault("MKL_NUM_THREADS", str(_CPU_COUNT))
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
import gradio as gr
import numpy as np
import soundfile as sf
import torch
from omnivoice import OmniVoice
# --------------------------------------------------------------------------
# Configuration
# --------------------------------------------------------------------------
MODEL_ID = os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice")
SAMPLE_RATE = 24000
MAX_CHARS = int(os.environ.get("MAX_CHARS", "300"))
MAX_REF_SECONDS = 12.0
torch.set_num_threads(_CPU_COUNT)
torch.set_grad_enabled(False)
_model = None
_model_lock = threading.Lock()
def get_model():
"""Load the model once, on first use, and reuse it afterwards."""
global _model
if _model is None:
with _model_lock:
if _model is None:
print(f"Loading {MODEL_ID} on CPU with {_CPU_COUNT} threads...")
t0 = time.time()
_model = OmniVoice.from_pretrained(
MODEL_ID,
device_map="cpu",
dtype=torch.float32,
)
print(f"Model ready in {time.time() - t0:.1f}s")
return _model
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
def _write_wav(audio: np.ndarray) -> str:
path = tempfile.mktemp(suffix=".wav")
sf.write(path, audio, SAMPLE_RATE)
return path
def _check_text(text: str) -> str:
text = (text or "").strip()
if not text:
raise gr.Error("Enter some text to speak.")
if len(text) > MAX_CHARS:
raise gr.Error(
f"Text is {len(text)} characters. Keep it under {MAX_CHARS} "
"so CPU generation finishes in a reasonable time."
)
return text
def _check_ref_audio(path: str) -> str:
if not path:
raise gr.Error("Upload or record a reference clip first.")
info = sf.info(path)
if info.duration > MAX_REF_SECONDS:
raise gr.Error(
f"Reference clip is {info.duration:.1f}s. Trim it to "
f"{MAX_REF_SECONDS:.0f}s or less — long references slow CPU "
"inference and degrade cloning quality."
)
return path
def _report(elapsed: float, audio: np.ndarray, steps: int) -> str:
duration = len(audio) / SAMPLE_RATE
rtf = elapsed / duration if duration else 0.0
return (
f"Generated {duration:.1f}s of audio in {elapsed:.1f}s "
f"(RTF {rtf:.2f}) at {steps} diffusion steps, {_CPU_COUNT} CPU threads."
)
# --------------------------------------------------------------------------
# Generation modes
# --------------------------------------------------------------------------
def clone_voice(text, ref_audio, ref_text, steps, speed, progress=gr.Progress()):
text = _check_text(text)
ref_audio = _check_ref_audio(ref_audio)
progress(0.1, desc="Loading model")
model = get_model()
if not (ref_text or "").strip():
progress(0.2, desc="Transcribing reference with Whisper (slow on CPU)")
progress(0.35, desc="Generating speech")
t0 = time.time()
audio = model.generate(
text=text,
ref_audio=ref_audio,
ref_text=(ref_text or "").strip() or None,
num_step=int(steps),
speed=float(speed),
)[0]
elapsed = time.time() - t0
return _write_wav(audio), _report(elapsed, audio, int(steps))
def design_voice(text, gender, age, pitch, accent, extra, steps, speed,
progress=gr.Progress()):
text = _check_text(text)
attributes = [a for a in (gender, age, pitch, accent) if a and a != "any"]
if (extra or "").strip():
attributes.append(extra.strip())
if not attributes:
raise gr.Error("Pick at least one voice attribute, or use the Auto voice tab.")
instruct = ", ".join(attributes)
progress(0.1, desc="Loading model")
model = get_model()
progress(0.35, desc="Generating speech")
t0 = time.time()
audio = model.generate(
text=text,
instruct=instruct,
num_step=int(steps),
speed=float(speed),
)[0]
elapsed = time.time() - t0
return _write_wav(audio), f"Voice: {instruct}\n" + _report(elapsed, audio, int(steps))
def auto_voice(text, steps, speed, progress=gr.Progress()):
text = _check_text(text)
progress(0.1, desc="Loading model")
model = get_model()
progress(0.35, desc="Generating speech")
t0 = time.time()
audio = model.generate(
text=text,
num_step=int(steps),
speed=float(speed),
)[0]
elapsed = time.time() - t0
return _write_wav(audio), _report(elapsed, audio, int(steps))
# --------------------------------------------------------------------------
# Interface
# --------------------------------------------------------------------------
CSS = """
.gradio-container { max-width: 1040px !important; }
#header h1 { margin-bottom: 0.15rem; font-weight: 650; letter-spacing: -0.01em; }
#header p { margin-top: 0; opacity: 0.72; }
#speed-note { font-size: 0.85rem; opacity: 0.7; }
footer { visibility: hidden; }
"""
def settings_row():
"""Shared generation controls. Returns (steps, speed)."""
with gr.Row():
steps = gr.Slider(
8, 32, value=16, step=8,
label="Diffusion steps",
info="Fewer steps = faster, slightly rougher. 16 is a good CPU default.",
)
speed = gr.Slider(
0.7, 1.4, value=1.0, step=0.05,
label="Speaking rate",
info="Above 1.0 speaks faster and shortens the audio.",
)
return steps, speed
with gr.Blocks(title="OmniVoice on CPU", css=CSS, theme=gr.themes.Soft()) as demo:
gr.Markdown(
"# OmniVoice on CPU\n"
"Zero-shot text to speech in 600+ languages, running on a CPU Space. "
"Generation takes roughly as long as the clip itself — a 10 second "
"line lands in about 10 to 25 seconds. The first request also pays a "
"one-time model load.",
elem_id="header",
)
with gr.Tabs():
# ---------------- Voice cloning ----------------
with gr.Tab("Clone a voice"):
with gr.Row():
with gr.Column(scale=3):
clone_text = gr.Textbox(
label="Text to speak",
placeholder="Type what the cloned voice should say.",
lines=3,
max_lines=6,
)
clone_ref_audio = gr.Audio(
label="Reference clip (3–10 seconds)",
type="filepath",
sources=["upload", "microphone"],
)
clone_ref_text = gr.Textbox(
label="What the reference clip says",
placeholder="Leave blank to transcribe it automatically — "
"much slower on CPU.",
lines=2,
)
clone_steps, clone_speed = settings_row()
clone_btn = gr.Button("Generate speech", variant="primary")
with gr.Column(scale=2):
clone_out = gr.Audio(label="Result", type="filepath")
clone_status = gr.Textbox(label="Timing", lines=3, interactive=False)
clone_btn.click(
clone_voice,
inputs=[clone_text, clone_ref_audio, clone_ref_text,
clone_steps, clone_speed],
outputs=[clone_out, clone_status],
)
# ---------------- Voice design ----------------
with gr.Tab("Design a voice"):
with gr.Row():
with gr.Column(scale=3):
design_text = gr.Textbox(
label="Text to speak",
placeholder="Type what the designed voice should say.",
lines=3,
max_lines=6,
)
with gr.Row():
d_gender = gr.Dropdown(
["any", "male", "female"], value="female", label="Gender")
d_age = gr.Dropdown(
["any", "child", "young", "middle-aged", "elderly"],
value="any", label="Age")
with gr.Row():
d_pitch = gr.Dropdown(
["any", "very low pitch", "low pitch", "medium pitch",
"high pitch", "very high pitch"],
value="any", label="Pitch")
d_accent = gr.Dropdown(
["any", "american accent", "british accent",
"australian accent", "indian accent"],
value="any", label="Accent")
d_extra = gr.Textbox(
label="Other attributes",
placeholder="e.g. whisper, 四川话",
)
design_steps, design_speed = settings_row()
design_btn = gr.Button("Generate speech", variant="primary")
with gr.Column(scale=2):
design_out = gr.Audio(label="Result", type="filepath")
design_status = gr.Textbox(label="Timing", lines=3, interactive=False)
gr.Markdown(
"Voice design was trained on Chinese and English only. It "
"generalizes to other languages but results get unpredictable "
"for low-resource ones."
)
design_btn.click(
design_voice,
inputs=[design_text, d_gender, d_age, d_pitch, d_accent,
d_extra, design_steps, design_speed],
outputs=[design_out, design_status],
)
# ---------------- Auto voice ----------------
with gr.Tab("Auto voice"):
with gr.Row():
with gr.Column(scale=3):
auto_text = gr.Textbox(
label="Text to speak",
placeholder="The model picks a voice for you.",
lines=3,
max_lines=6,
)
auto_steps, auto_speed = settings_row()
auto_btn = gr.Button("Generate speech", variant="primary")
with gr.Column(scale=2):
auto_out = gr.Audio(label="Result", type="filepath")
auto_status = gr.Textbox(label="Timing", lines=3, interactive=False)
auto_btn.click(
auto_voice,
inputs=[auto_text, auto_steps, auto_speed],
outputs=[auto_out, auto_status],
)
gr.Markdown(
"Inline controls you can use in any text box: `[laughter]`, `[sigh]`, "
"`[question-en]`, `[surprise-ah]` and other non-verbal tags; CMU "
"phonemes in brackets for English (`[B EY1 S]`); pinyin with tone "
"numbers for Chinese (`打ZHE2`).\n\n"
"Do not clone anyone's voice without their permission."
)
if __name__ == "__main__":
# Queue with a single worker: CPU inference is not something to run
# concurrently on 2 vCPUs.
demo.queue(max_size=12, default_concurrency_limit=1).launch()