| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| """ |
| Gradio demo for OmniVoice. |
| |
| Supports voice cloning and voice design. |
| |
| Usage: |
| omnivoice-demo --model /path/to/checkpoint --port 8000 |
| """ |
|
|
| import argparse |
| import logging |
| from typing import Any, Dict |
|
|
| import gradio as gr |
| import numpy as np |
| import torch |
|
|
| from omnivoice import OmniVoice, OmniVoiceGenerationConfig |
| from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name |
|
|
|
|
| def get_best_device(): |
| """Auto-detect the best available device: CUDA > MPS > CPU.""" |
| if torch.cuda.is_available(): |
| return "cuda" |
| if torch.backends.mps.is_available(): |
| return "mps" |
| return "cpu" |
|
|
|
|
| |
| |
| |
| _ALL_LANGUAGES = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES) |
|
|
|
|
| |
| |
| |
| |
| |
| _CATEGORIES = { |
| "Gender / 性别": ["Male / 男", "Female / 女"], |
| "Age / 年龄": [ |
| "Child / 儿童", |
| "Teenager / 少年", |
| "Young Adult / 青年", |
| "Middle-aged / 中年", |
| "Elderly / 老年", |
| ], |
| "Pitch / 音调": [ |
| "Very Low Pitch / 极低音调", |
| "Low Pitch / 低音调", |
| "Moderate Pitch / 中音调", |
| "High Pitch / 高音调", |
| "Very High Pitch / 极高音调", |
| ], |
| "Style / 风格": ["Whisper / 耳语"], |
| "English Accent / 英文口音": [ |
| "American Accent / 美式口音", |
| "Australian Accent / 澳大利亚口音", |
| "British Accent / 英国口音", |
| "Chinese Accent / 中国口音", |
| "Canadian Accent / 加拿大口音", |
| "Indian Accent / 印度口音", |
| "Korean Accent / 韩国口音", |
| "Portuguese Accent / 葡萄牙口音", |
| "Russian Accent / 俄罗斯口音", |
| "Japanese Accent / 日本口音", |
| ], |
| "Chinese Dialect / 中文方言": [ |
| "Henan Dialect / 河南话", |
| "Shaanxi Dialect / 陕西话", |
| "Sichuan Dialect / 四川话", |
| "Guizhou Dialect / 贵州话", |
| "Yunnan Dialect / 云南话", |
| "Guilin Dialect / 桂林话", |
| "Jinan Dialect / 济南话", |
| "Shijiazhuang Dialect / 石家庄话", |
| "Gansu Dialect / 甘肃话", |
| "Ningxia Dialect / 宁夏话", |
| "Qingdao Dialect / 青岛话", |
| "Northeast Dialect / 东北话", |
| ], |
| } |
|
|
| _ATTR_INFO = { |
| "English Accent / 英文口音": "Only effective for English speech.", |
| "Chinese Dialect / 中文方言": "Only effective for Chinese speech.", |
| } |
|
|
| |
| |
| |
|
|
|
|
| def build_parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser( |
| prog="omnivoice-demo", |
| description="Launch a Gradio demo for OmniVoice.", |
| formatter_class=argparse.RawTextHelpFormatter, |
| ) |
| parser.add_argument( |
| "--model", |
| default="k2-fsa/OmniVoice", |
| help="Model checkpoint path or HuggingFace repo id.", |
| ) |
| parser.add_argument( |
| "--device", default=None, help="Device to use. Auto-detected if not specified." |
| ) |
| parser.add_argument("--ip", default="0.0.0.0", help="Server IP (default: 0.0.0.0).") |
| parser.add_argument( |
| "--port", type=int, default=7860, help="Server port (default: 7860)." |
| ) |
| parser.add_argument( |
| "--root-path", |
| default=None, |
| help="Root path for reverse proxy.", |
| ) |
| parser.add_argument( |
| "--share", action="store_true", default=False, help="Create public link." |
| ) |
| parser.add_argument( |
| "--no-asr", |
| action="store_true", |
| default=False, |
| help="Skip loading Whisper ASR model. Reference text auto-transcription" |
| " will be unavailable.", |
| ) |
| parser.add_argument( |
| "--asr-model", |
| default="openai/whisper-large-v3-turbo", |
| help="ASR model path or HuggingFace repo id" |
| " (default: openai/whisper-large-v3-turbo).", |
| ) |
| return parser |
|
|
|
|
| |
| |
| |
|
|
|
|
| def build_demo( |
| model: OmniVoice, |
| checkpoint: str, |
| generate_fn=None, |
| ) -> gr.Blocks: |
|
|
| sampling_rate = model.sampling_rate |
|
|
| |
| def _gen_core( |
| text, |
| language, |
| ref_audio, |
| instruct, |
| num_step, |
| guidance_scale, |
| denoise, |
| speed, |
| duration, |
| preprocess_prompt, |
| postprocess_output, |
| mode, |
| ref_text=None, |
| ): |
| if not text or not text.strip(): |
| return None, "Please enter the text to synthesize." |
|
|
| gen_config = OmniVoiceGenerationConfig( |
| num_step=int(num_step or 32), |
| guidance_scale=float(guidance_scale) if guidance_scale is not None else 2.0, |
| denoise=bool(denoise) if denoise is not None else True, |
| preprocess_prompt=bool(preprocess_prompt), |
| postprocess_output=bool(postprocess_output), |
| ) |
|
|
| lang = language if (language and language != "Auto") else None |
|
|
| kw: Dict[str, Any] = dict( |
| text=text.strip(), language=lang, generation_config=gen_config |
| ) |
|
|
| if speed is not None and float(speed) != 1.0: |
| kw["speed"] = float(speed) |
| if duration is not None and float(duration) > 0: |
| kw["duration"] = float(duration) |
|
|
| if mode == "clone": |
| if not ref_audio: |
| return None, "Please upload a reference audio." |
| kw["voice_clone_prompt"] = model.create_voice_clone_prompt( |
| ref_audio=ref_audio, |
| ref_text=ref_text, |
| ) |
|
|
| if instruct and instruct.strip(): |
| kw["instruct"] = instruct.strip() |
|
|
| try: |
| audio = model.generate(**kw) |
| except Exception as e: |
| return None, f"Error: {type(e).__name__}: {e}" |
|
|
| waveform = (audio[0] * 32767).astype(np.int16) |
| return (sampling_rate, waveform), "Done." |
|
|
| |
| _gen = generate_fn if generate_fn is not None else _gen_core |
|
|
| |
| |
| |
| theme = gr.themes.Soft( |
| font=["Inter", "Arial", "sans-serif"], |
| ) |
| css = """ |
| .gradio-container {max-width: 100% !important; font-size: 16px !important;} |
| .gradio-container h1 {font-size: 1.5em !important;} |
| .gradio-container .prose {font-size: 1.1em !important;} |
| .compact-audio audio {height: 60px !important;} |
| .compact-audio .waveform {min-height: 80px !important;} |
| """ |
|
|
| |
| def _lang_dropdown(label="Language (optional) / 语种 (可选)", value="Auto"): |
| return gr.Dropdown( |
| label=label, |
| choices=_ALL_LANGUAGES, |
| value=value, |
| allow_custom_value=False, |
| interactive=True, |
| info="Keep as Auto to auto-detect the language.", |
| ) |
|
|
| |
| def _gen_settings(): |
| with gr.Accordion("Generation Settings (optional)", open=False): |
| sp = gr.Slider( |
| 0.5, |
| 1.5, |
| value=1.0, |
| step=0.05, |
| label="Speed", |
| info="1.0 = normal. >1 faster, <1 slower. Ignored if Duration is set.", |
| ) |
| du = gr.Number( |
| value=None, |
| label="Duration (seconds)", |
| info=( |
| "Leave empty to use speed." |
| " Set a fixed duration to override speed." |
| ), |
| ) |
| ns = gr.Slider( |
| 4, |
| 64, |
| value=32, |
| step=1, |
| label="Inference Steps", |
| info="Default: 32. Lower = faster, higher = better quality.", |
| ) |
| dn = gr.Checkbox( |
| label="Denoise", |
| value=True, |
| info="Default: enabled. Uncheck to disable denoising.", |
| ) |
| gs = gr.Slider( |
| 0.0, |
| 4.0, |
| value=2.0, |
| step=0.1, |
| label="Guidance Scale (CFG)", |
| info="Default: 2.0.", |
| ) |
| pp = gr.Checkbox( |
| label="Preprocess Prompt", |
| value=True, |
| info="apply silence removal and trimming to the reference " |
| "audio, add punctuation in the end of reference text (if not already)", |
| ) |
| po = gr.Checkbox( |
| label="Postprocess Output", |
| value=True, |
| info="Remove long silences from generated audio.", |
| ) |
| return ns, gs, dn, sp, du, pp, po |
|
|
| with gr.Blocks(theme=theme, css=css, title="OmniVoice Demo") as demo: |
| gr.Markdown( |
| """ |
| # OmniVoice Demo |
| |
| State-of-the-art text-to-speech model for **600+ languages**, supporting: |
| |
| - **Voice Clone** — Clone any voice from a reference audio |
| - **Voice Design** — Create custom voices with speaker attributes |
| |
| Built with [OmniVoice](https://github.com/k2-fsa/OmniVoice) |
| by Xiaomi AI Lab Next-gen Kaldi team. |
| """ |
| ) |
|
|
| with gr.Tabs(): |
| |
| |
| |
| with gr.TabItem("Voice Clone"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| vc_text = gr.Textbox( |
| label="Text to Synthesize / 待合成文本", |
| lines=4, |
| placeholder="Enter the text you want to synthesize...", |
| ) |
| vc_ref_audio = gr.Audio( |
| label="Reference Audio / 参考音频", |
| type="filepath", |
| elem_classes="compact-audio", |
| ) |
| gr.Markdown( |
| "<span style='font-size:0.85em;color:#888;'>" |
| "Recommended: 3–10 seconds audio. " |
| "</span>" |
| ) |
| vc_ref_text = gr.Textbox( |
| label=("Reference Text (optional)" " / 参考音频文本(可选)"), |
| lines=2, |
| placeholder="Transcript of the reference audio. Leave empty" |
| " to auto-transcribe via ASR models.", |
| ) |
| vc_lang = _lang_dropdown("Language (optional) / 语种 (可选)") |
| with gr.Accordion("Instruct (optional)", open=False): |
| vc_instruct = gr.Textbox(label="Instruct", lines=2) |
| ( |
| vc_ns, |
| vc_gs, |
| vc_dn, |
| vc_sp, |
| vc_du, |
| vc_pp, |
| vc_po, |
| ) = _gen_settings() |
| vc_btn = gr.Button("Generate / 生成", variant="primary") |
| with gr.Column(scale=1): |
| vc_audio = gr.Audio( |
| label="Output Audio / 合成结果", |
| type="numpy", |
| ) |
| vc_status = gr.Textbox(label="Status / 状态", lines=2) |
|
|
| def _clone_fn( |
| text, lang, ref_aud, ref_text, instruct, ns, gs, dn, sp, du, pp, po |
| ): |
| return _gen( |
| text, |
| lang, |
| ref_aud, |
| instruct, |
| ns, |
| gs, |
| dn, |
| sp, |
| du, |
| pp, |
| po, |
| mode="clone", |
| ref_text=ref_text or None, |
| ) |
|
|
| vc_btn.click( |
| _clone_fn, |
| inputs=[ |
| vc_text, |
| vc_lang, |
| vc_ref_audio, |
| vc_ref_text, |
| vc_instruct, |
| vc_ns, |
| vc_gs, |
| vc_dn, |
| vc_sp, |
| vc_du, |
| vc_pp, |
| vc_po, |
| ], |
| outputs=[vc_audio, vc_status], |
| ) |
|
|
| |
| |
| |
| with gr.TabItem("Voice Design"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| vd_text = gr.Textbox( |
| label="Text to Synthesize / 待合成文本", |
| lines=4, |
| placeholder="Enter the text you want to synthesize...", |
| ) |
| vd_lang = _lang_dropdown() |
|
|
| _AUTO = "Auto" |
| vd_groups = [] |
| for _cat, _choices in _CATEGORIES.items(): |
| vd_groups.append( |
| gr.Dropdown( |
| label=_cat, |
| choices=[_AUTO] + _choices, |
| value=_AUTO, |
| info=_ATTR_INFO.get(_cat), |
| ) |
| ) |
|
|
| ( |
| vd_ns, |
| vd_gs, |
| vd_dn, |
| vd_sp, |
| vd_du, |
| vd_pp, |
| vd_po, |
| ) = _gen_settings() |
| vd_btn = gr.Button("Generate / 生成", variant="primary") |
| with gr.Column(scale=1): |
| vd_audio = gr.Audio( |
| label="Output Audio / 合成结果", |
| type="numpy", |
| ) |
| vd_status = gr.Textbox(label="Status / 状态", lines=2) |
|
|
| def _build_instruct(groups): |
| """Extract instruct text from UI dropdowns. |
| |
| Language unification and validation is handled by |
| _resolve_instruct inside _preprocess_all. |
| """ |
| selected = [g for g in groups if g and g != "Auto"] |
| if not selected: |
| return None |
| parts = [] |
| for v in selected: |
| if " / " in v: |
| en, zh = v.split(" / ", 1) |
| |
| if "Dialect" in v.split(" / ")[0]: |
| parts.append(zh.strip()) |
| else: |
| parts.append(en.strip()) |
| else: |
| parts.append(v) |
| return ", ".join(parts) |
|
|
| def _design_fn(text, lang, ns, gs, dn, sp, du, pp, po, *groups): |
| return _gen( |
| text, |
| lang, |
| None, |
| _build_instruct(groups), |
| ns, |
| gs, |
| dn, |
| sp, |
| du, |
| pp, |
| po, |
| mode="design", |
| ) |
|
|
| vd_btn.click( |
| _design_fn, |
| inputs=[ |
| vd_text, |
| vd_lang, |
| vd_ns, |
| vd_gs, |
| vd_dn, |
| vd_sp, |
| vd_du, |
| vd_pp, |
| vd_po, |
| ] |
| + vd_groups, |
| outputs=[vd_audio, vd_status], |
| ) |
|
|
| return demo |
|
|
|
|
| |
| |
| |
|
|
|
|
| def main(argv=None) -> int: |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s %(name)s %(levelname)s: %(message)s", |
| ) |
| parser = build_parser() |
| args = parser.parse_args(argv) |
|
|
| device = args.device or get_best_device() |
|
|
| checkpoint = args.model |
| if not checkpoint: |
| parser.print_help() |
| return 0 |
| logging.info(f"Loading model from {checkpoint}, device={device} ...") |
| model = OmniVoice.from_pretrained( |
| checkpoint, |
| device_map=device, |
| dtype=torch.float16, |
| load_asr=not args.no_asr, |
| asr_model_name=args.asr_model, |
| ) |
| print("Model loaded.") |
|
|
| demo = build_demo(model, checkpoint) |
|
|
| demo.queue().launch( |
| server_name=args.ip, |
| server_port=args.port, |
| share=args.share, |
| root_path=args.root_path, |
| ) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|