Spaces:
Running on Zero
Running on Zero
| """ | |
| 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() | |