"""FireRedAudio — unified audio understanding + generation Gradio demo. Wraps `inference.FireRedAudioInference` from the repo root verbatim, so every task uses the authors' own prompt templates and sampling parameters. Four tabs cover zero-shot TTS, instruct TTS, speech editing (semantic / acoustic), and audio understanding. The understanding tab exposes the full Qwen3 sampling controls (`temperature`, `top_p`, `top_k`, `min_p`, `repetition_penalty`, `do_sample`) with a think-preset toggle that swaps in Qwen3's recommended `(temperature, top_p)` for reasoning / non-reasoning modes. """ import argparse import gc import logging import os import random import sys import tempfile import threading import traceback from pathlib import Path import numpy as np import torch import torchaudio import gradio as gr # ----------------------------------------------------------------- path bootstrap REPO_ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(REPO_ROOT)) ASSETS = REPO_ROOT / "assets" / "examples" import inference as fra # noqa: E402 from fireredaudio.redae.decoder import ( # noqa: E402 PretrainedRedAEAudioDecoderV1, PretrainedRedAEDecoderConfig, ) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logger = logging.getLogger("demo") for _noisy in ("httpcore", "httpx", "asyncio", "urllib3", "uvicorn.access"): logging.getLogger(_noisy).setLevel(logging.WARNING) MAX_SEED = np.iinfo(np.int32).max # One inference lock: FireRedAudioInference holds a single model instance and # every task path mutates the same `_gen_config` when we override sampling. INFER_LOCK = threading.Lock() # Populated in main() ENGINE: "fra.FireRedAudioInference | None" = None # ---------------------------------------------------------------- weight loading def _build_engine(model_path: str, vae_decoder_path: str, device: str ) -> "fra.FireRedAudioInference": """Build FireRedAudioInference on `device` and load the RedAE decoder. We load the decoder ourselves rather than passing `vae_decoder_path=` so we can drop the .pt file from disk before the (much larger) backbone lands — matches the HF Space's disk-frugal ordering. """ logger.info("Loading RedAE decoder from %s", vae_decoder_path) vae_decoder = PretrainedRedAEAudioDecoderV1.from_config( PretrainedRedAEDecoderConfig() ) sd = torch.load(vae_decoder_path, weights_only=True, map_location="cpu", mmap=True)["model"] vae_decoder.load_state_dict( {k.removeprefix("decoder."): v for k, v in sd.items() if k.startswith("decoder.")}, strict=True, ) vae_decoder.eval() del sd gc.collect() logger.info("Loading FireRedAudio backbone from %s on %s", model_path, device) engine = fra.FireRedAudioInference(model_path=model_path, device=device) engine.vae_decoder = vae_decoder.to(engine.device) logger.info("Model ready.") return engine # ---------------------------------------------------------------------- utilities def _write_wav(audio: torch.Tensor) -> str: wav = audio.detach().float().cpu().reshape(1, -1).clamp(-1.0, 1.0) fd, path = tempfile.mkstemp(suffix=".wav") os.close(fd) torchaudio.save(path, wav, fra.GENERATION_SAMPLE_RATE) return path def _seed(seed: int, randomize: bool) -> int: seed = random.randint(0, MAX_SEED) if randomize else int(seed) % (MAX_SEED + 1) fra.set_seed(seed) return seed # ----------------------------------------------------------------------- handlers def clone_voice( reference_audio: str, reference_text: str, target_text: str, language: str = "zh", seed: int = 0, randomize_seed: bool = True, inference_cfg: float = 2.0, n_timesteps: int = 10, max_new_audio_steps: int = 150, ) -> tuple[str, int]: """Zero-shot voice cloning.""" if not reference_audio: raise gr.Error("Please provide a reference audio clip.") if not (reference_text or "").strip(): raise gr.Error("Please provide the transcript of the reference audio.") if not (target_text or "").strip(): raise gr.Error("Please provide the text to synthesise.") used = _seed(seed, randomize_seed) with INFER_LOCK: out = ENGINE.tts( prompt_text=reference_text.strip(), prompt_audio=reference_audio, target_text=target_text.strip(), language=language, n_timesteps=int(n_timesteps), inference_cfg=float(inference_cfg), max_new_audio_steps=int(max_new_audio_steps), ) return _write_wav(out.audio), used def design_voice( instruction: str, text: str, seed: int = 0, randomize_seed: bool = True, inference_cfg: float = 2.0, n_timesteps: int = 10, max_new_audio_steps: int = 150, max_new_text_tokens: int = 512, ) -> tuple[str, str, int]: """Instruct TTS: describe a voice, then speak text in it.""" if not (instruction or "").strip(): raise gr.Error("Please describe the voice you want.") if not (text or "").strip(): raise gr.Error("Please provide the text to synthesise.") used = _seed(seed, randomize_seed) with INFER_LOCK: out = ENGINE.voice_design( instruction=instruction.strip(), text=text.strip(), n_timesteps=int(n_timesteps), inference_cfg=float(inference_cfg), max_new_audio_steps=int(max_new_audio_steps), max_new_text_tokens=int(max_new_text_tokens), ) return _write_wav(out.audio), out.text or "", used def edit_speech( audio: str, instruction: str, edit_type: str = "semantic", seed: int = 0, randomize_seed: bool = True, inference_cfg: float = 2.0, n_timesteps: int = 10, max_new_audio_steps: int = 150, max_new_text_tokens: int = 512, ) -> tuple[str, str, int]: """Semantic / acoustic speech edit, keeping the original voice.""" if not audio: raise gr.Error("Please provide an audio clip to edit.") if not (instruction or "").strip(): raise gr.Error("Please provide an edit instruction.") used = _seed(seed, randomize_seed) with INFER_LOCK: out = ENGINE.edit( audio_path=audio, instruction=instruction.strip(), edit_type=edit_type, n_timesteps=int(n_timesteps), inference_cfg=float(inference_cfg), max_new_audio_steps=int(max_new_audio_steps), max_new_text_tokens=int(max_new_text_tokens), ) return _write_wav(out.audio), out.text or "", used # Qwen3-recommended sampling per mode: only (temperature, top_p) differ. Toggling # `enable_thinking` in the UI re-applies these — a manual tune is overwritten, # which is intended (predictable > sticky). _THINK_SAMPLING = {"temperature": 0.6, "top_p": 0.95} _NONTHINK_SAMPLING = {"temperature": 0.7, "top_p": 0.8} def _apply_think_preset(enable_thinking: bool): preset = _THINK_SAMPLING if enable_thinking else _NONTHINK_SAMPLING return gr.update(value=preset["temperature"]), gr.update(value=preset["top_p"]) def understand_audio( audio: str, question: str, task: str, enable_thinking: bool, max_new_tokens: int, do_sample: bool, temperature: float, top_p: float, top_k: int, min_p: float, repetition_penalty: float, ) -> tuple[str, str]: """ASR / free-form audio QA with full Qwen3 sampling exposed. The upstream engine.understand() builds its GenerationConfig from a fixed per-task dict, so we override `engine._gen_config` for the duration of the call to inject the sliders' values without patching the packaged code. """ if not audio: raise gr.Error("Please provide an audio clip.") prompt = fra.DEFAULT_ASR_PROMPT if task == "asr" else (question or "").strip() if not prompt: raise gr.Error("Please ask a question about the audio.") thinking = bool(enable_thinking) and task == "understand" # asr goes through beam search — sampling knobs would be silently ignored by # HF's GenerationConfig.validate() and just spam warnings. Only apply the # override for `understand`. overrides: dict = {} if task == "understand": overrides = { "do_sample": bool(do_sample), "temperature": float(temperature), "top_p": float(top_p), "top_k": int(top_k), "min_p": float(min_p), "repetition_penalty": float(repetition_penalty), } # Greedy: neutralise sampling knobs so GenerationConfig.validate() doesn't # warn about "non-neutral value X while do_sample=False". if not overrides["do_sample"]: overrides.update(temperature=1.0, top_p=1.0, top_k=0, min_p=0.0) max_new = fra.THINKING_MAX_NEW_TOKENS if thinking else int(max_new_tokens) with INFER_LOCK: original_gen_config = ENGINE._gen_config def _patched(t: str, mnt: int | None = None): cfg = original_gen_config(t, mnt) for k, v in overrides.items(): setattr(cfg, k, v) return cfg ENGINE._gen_config = _patched try: out = ENGINE.understand( audio_paths=audio, prompt=prompt, task=task, enable_thinking=thinking, max_new_tokens=max_new, ) finally: ENGINE._gen_config = original_gen_config return out.answer, out.reasoning or "" # --------------------------------------------------------------------------- UI _XHS_RED = "#ff2442" # Xiaohongshu brand red _XHS_RED_H = "#ff4d63" _FIRE = "#e8593c" # complementary FireRed accent _THEME = ( gr.themes.Base( primary_hue=gr.themes.colors.red, secondary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.slate, font=[ "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "sans-serif", ], ) .set( body_background_fill_dark="#0f1216", background_fill_primary_dark="#171b22", background_fill_secondary_dark="#1a1f2c", block_background_fill_dark="#171b22", border_color_primary_dark="#2a3140", block_border_color_dark="#2a3140", block_border_width="1px", block_border_width_dark="1px", body_text_color_dark="#e6ebf2", body_text_color_subdued_dark="#93a0b5", block_label_text_color_dark="#93a0b5", block_label_text_size="*text_sm", color_accent=_XHS_RED, border_color_accent_dark=_XHS_RED, button_primary_background_fill=_XHS_RED, button_primary_background_fill_dark=_XHS_RED, button_primary_background_fill_hover=_XHS_RED_H, button_primary_background_fill_hover_dark=_XHS_RED_H, button_primary_border_color=_XHS_RED, button_primary_border_color_dark=_XHS_RED, button_primary_border_color_hover=_XHS_RED_H, button_primary_border_color_hover_dark=_XHS_RED_H, button_primary_text_color="#ffffff", button_primary_text_color_dark="#ffffff", button_primary_shadow="none", button_primary_shadow_dark="none", button_large_text_size="*text_md", button_large_padding="12px 28px", input_background_fill_dark="#1a1f2c", input_border_color_dark="#2a3140", input_border_color_focus_dark=_XHS_RED, block_radius="12px", input_radius="10px", button_large_radius="10px", ) ) _CSS = f""" :root {{ color-scheme: dark; }} #col-container {{ margin: 0 auto; max-width: 1180px; }} /* header banner — XHS-red → FireRed gradient */ .app-header {{ background: linear-gradient(135deg, {_XHS_RED} 0%, {_FIRE} 100%); border: none; border-radius: 14px; padding: 22px 28px; margin-bottom: 6px; box-shadow: 0 8px 24px rgba(255, 36, 66, 0.15); display: flex; align-items: center; gap: 18px; }} .app-header .brand-logo {{ /* Transparent so the FireRedTeam avatar (already red-on-red) shows its own baked-in rounded shape; a white ring lifts it off the red gradient. */ width: 60px; height: 60px; border-radius: 14px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; box-shadow: 0 0 0 2px rgba(255,255,255,0.85), 0 4px 12px rgba(0,0,0,0.18); }} .app-header .brand-logo img {{ width: 100%; height: 100%; object-fit: cover; display: block; }} .app-header .brand-text h1 {{ font-size: 24px !important; font-weight: 800 !important; margin: 0 0 4px !important; color: #ffffff !important; letter-spacing: -0.3px; }} .app-header .brand-text p {{ margin: 0 !important; color: rgba(255,255,255,0.9) !important; font-size: 13.5px !important; }} .app-header .brand-text .xhs-chip {{ display: inline-block; margin-top: 8px; background: rgba(255,255,255,0.18); color: #ffffff; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; backdrop-filter: blur(6px); }} /* tab underline in XHS red */ .tab-nav button.selected {{ border-bottom-color: {_XHS_RED} !important; color: {_XHS_RED} !important; }} /* footer credit */ .app-footer {{ text-align: center; color: #93a0b5; font-size: 12px; padding: 16px 0 4px; }} .app-footer a {{ color: {_XHS_RED}; text-decoration: none; }} /* result / reasoning textboxes */ #think-box textarea {{ font-family: "SF Mono", "Consolas", "Monaco", monospace !important; font-size: 12.5px !important; line-height: 1.65 !important; color: #93a0b5 !important; font-style: italic; background: #141822 !important; min-height: 120px; }} #think-box .block-label {{ color: #b48a5d !important; }} #answer-box textarea {{ font-size: 14px !important; line-height: 1.7 !important; min-height: 220px; }} details summary {{ font-size: 13px !important; font-weight: 500 !important; }} """ _DARK_JS = "() => { document.documentElement.classList.add('dark'); }" _HEADER_HTML = """

🔥 FireRedAudio · Unified Audio Understanding & Generation

Zero-shot TTS · Instruct TTS · speech editing · ASR & audio QA — all in one 9B model.

🔴 小红书 · FireRedTeam
""" _FOOTER_HTML = """ """ def build_demo() -> gr.Blocks: # Use the FireRedTeam org avatar (square, red-on-white wordmark) rather than # `fireredaudio_logo.png`, which is a 16:9 feature-infographic banner and # renders as unreadable colour mush when squeezed into a header icon slot. logo_path = REPO_ROOT / "assets" / "fireredteam_avatar.png" # Gradio 6.0 moved theme / css / js out of Blocks() into launch(); pass them # there instead. Blocks() only keeps `title` and layout kwargs. with gr.Blocks(title="FireRedAudio · 小红书 FireRedTeam") as demo: with gr.Column(elem_id="col-container"): gr.HTML(_HEADER_HTML.format(logo_path=str(logo_path))) # ------------------------------------------------------ Zero-shot TTS with gr.Tab("🗣️ Zero-shot TTS"): gr.Markdown( "Give a few seconds of a voice plus its exact transcript, " "and read any new text in that voice (zero-shot TTS)." ) with gr.Row(): with gr.Column(): tts_ref_audio = gr.Audio(label="Reference audio", type="filepath", sources=["upload", "microphone"]) tts_ref_text = gr.Textbox( label="Reference transcript", placeholder="Exactly what is said in the reference audio", lines=2, ) tts_target = gr.Textbox( label="Text to speak", placeholder="What the cloned voice should say", lines=3, ) tts_lang = gr.Radio(["zh", "en"], value="zh", label="Language of the text") tts_btn = gr.Button("Generate speech", variant="primary") with gr.Column(): tts_out = gr.Audio(label="Generated speech", type="filepath") tts_seed_out = gr.Number(label="Seed used", precision=0, interactive=False) with gr.Accordion("Advanced settings", open=False): with gr.Row(): tts_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") tts_rand = gr.Checkbox(value=True, label="Randomize seed") with gr.Row(): tts_cfg = gr.Slider(1.0, 5.0, value=2.0, step=0.1, label="CFG scale") tts_steps = gr.Slider(4, 32, value=10, step=1, label="Flow-matching steps") tts_max_audio = gr.Slider( 24, 750, value=150, step=1, label="Max audio steps (160 ms each)", ) gr.Examples( examples=[ [str(ASSETS / "tts_zh_prompt.wav"), "同时,他强调微调要科学有序。", "安徽淮南秦师傅发现,停在小区的爱车右前驾驶窗玻璃被砸。", "zh"], ], inputs=[tts_ref_audio, tts_ref_text, tts_target, tts_lang], outputs=[tts_out, tts_seed_out], fn=clone_voice, cache_examples=False, label="Official examples", ) # -------------------------------------------------------- Instruct TTS with gr.Tab("🎨 Instruct TTS"): gr.Markdown( "Describe a voice in words — gender, age, accent, emotion, " "pacing — and the model invents it, then speaks your text." ) with gr.Row(): with gr.Column(): vd_instruction = gr.Textbox( label="Voice description", placeholder="A warm middle-aged male voice with a slight British accent, speaking slowly.", lines=4, ) vd_text = gr.Textbox(label="Text to speak", lines=3) vd_btn = gr.Button("Generate speech", variant="primary") with gr.Column(): vd_out = gr.Audio(label="Generated speech", type="filepath") vd_tags = gr.Textbox(label="Timbre tags chosen by the model", lines=3, interactive=False) vd_seed_out = gr.Number(label="Seed used", precision=0, interactive=False) with gr.Accordion("Advanced settings", open=False): with gr.Row(): vd_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") vd_rand = gr.Checkbox(value=True, label="Randomize seed") with gr.Row(): vd_cfg = gr.Slider(1.0, 5.0, value=2.0, step=0.1, label="CFG scale") vd_steps = gr.Slider(4, 32, value=10, step=1, label="Flow-matching steps") vd_max_audio = gr.Slider( 24, 750, value=150, step=1, label="Max audio steps (160 ms each)", ) vd_max_text = gr.Slider( 64, 1024, value=512, step=8, label="Max text tokens (timbre tags)", ) gr.Examples( examples=[ ["以女性高音区的清亮音色,表现出青年阶段的特质,音量略强,语速适中稍快,语调带有解释意味和急切的情感流露,确保语音流畅自然。", "是我请他来的,可他什么也不知道,他来只是想打听一下,你们厂是不是有旧锅炉?"], ["Incorporate an accent reminiscent of British English, perhaps " "with a regional flavor such as Cockney, and convey a sense of " "emotional vulnerability through a voice that reflects sadness " "and an overwhelming demeanor, punctuated by a tremor.", "I get these headaches, sharp pains through me head, milady. " "Everything seems to be on top of me and I can't stop crying."], ["Give it a dynamic tour as if you're a cheerful cartoon " "character with a mid-to-high pitch broadcasting lively, " "fast-paced thoughts.", "Hello, everybody. Welcome to the show. My name is Ryan " "Seacrest, and on behalf of the show, thank you for being here."], ], inputs=[vd_instruction, vd_text], outputs=[vd_out, vd_tags, vd_seed_out], fn=design_voice, cache_examples=False, label="Official examples", ) # ------------------------------------------------------- Speech editing with gr.Tab("✂️ Speech editing"): gr.Markdown( "Edit a recording with a text instruction while keeping the " "original voice. The model is trained on these exact instruction " "templates rather than free-form phrasing.\n\n" "**Semantic** — changes what is said:\n" "- `delete 'the words to remove'.`\n" "- `substitute 'old words' with 'new words'.`\n" "- `substitute the characters or words from index 8 to index 10 with 'new words'.`\n" "- `insert 'new words' after the character or word at index 8.`\n" "- `insert 'new words' before the character or word 'anchor'.`\n\n" "**Acoustic** — changes how it is said:\n" "- `shifts the pitch by N steps.` — N in −6…−1, 1…6\n" "- `adjusts the speed to X.` — X in 0.5…2.0\n" "- `adjusts the volume to X.` — X in 0.3…2.0" ) with gr.Row(): with gr.Column(): ed_audio = gr.Audio(label="Audio to edit", type="filepath", sources=["upload", "microphone"]) ed_instruction = gr.Textbox(label="Edit instruction", lines=2) ed_type = gr.Radio(["semantic", "acoustic"], value="semantic", label="Edit type") ed_btn = gr.Button("Apply edit", variant="primary") with gr.Column(): ed_out = gr.Audio(label="Edited speech", type="filepath") ed_text = gr.Textbox( label="Rewritten transcript (semantic edits)", info="Best-effort text the model wrote before rendering " "audio — substituted words sometimes show as a gap " "here even though the audio is correct.", lines=3, interactive=False, ) ed_seed_out = gr.Number(label="Seed used", precision=0, interactive=False) with gr.Accordion("Advanced settings", open=False): with gr.Row(): ed_seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") ed_rand = gr.Checkbox(value=True, label="Randomize seed") with gr.Row(): ed_cfg = gr.Slider(1.0, 5.0, value=2.0, step=0.1, label="CFG scale") ed_steps = gr.Slider(4, 32, value=10, step=1, label="Flow-matching steps") ed_max_audio = gr.Slider( 24, 750, value=150, step=1, label="Max audio steps (160 ms each)", ) ed_max_text = gr.Slider( 64, 1024, value=512, step=8, label="Max text tokens (rewritten transcript)", ) gr.Examples( examples=[ [str(ASSETS / "edit_semantic_zh_ref.wav"), "delete '比普通的茶叶要'.", "semantic"], [str(ASSETS / "edit_acoustic_zh_ref.wav"), "shifts the pitch by 3 steps.", "acoustic"], ], inputs=[ed_audio, ed_instruction, ed_type], outputs=[ed_out, ed_text, ed_seed_out], fn=edit_speech, cache_examples=False, label="Official examples", ) # ------------------------------------------------------- Understanding with gr.Tab("👂 Listen & understand"): gr.Markdown( "The same model transcribes speech (ASR) and answers open " "questions about any audio — speech, music or sound events." ) with gr.Row(): with gr.Column(): un_audio = gr.Audio(label="Audio", type="filepath", sources=["upload", "microphone"]) un_task = gr.Radio( ["asr", "understand"], value="asr", label="Task", info="asr = verbatim transcript (beam search) · " "understand = free-form QA", ) un_question = gr.Textbox( label="Question", value=fra.DEFAULT_ASR_PROMPT, lines=2, interactive=False, ) un_think = gr.Checkbox( value=False, visible=False, label="🧠 Enable thinking (understand only)", ) un_btn = gr.Button("Run", variant="primary") with gr.Column(): un_answer = gr.Textbox(label="Answer", lines=10, elem_id="answer-box") un_reasoning = gr.Textbox( label="🧠 Reasoning", lines=8, interactive=False, elem_id="think-box", ) with gr.Accordion("⚙️ Sampling parameters (understand only)", open=False): un_max_tokens = gr.Slider( 64, 4096, value=1024, step=8, label="max_new_tokens", ) un_do_sample = gr.Checkbox(value=True, label="do_sample") with gr.Row(): un_temp = gr.Slider( 0.0, 2.0, value=_NONTHINK_SAMPLING["temperature"], step=0.05, label="temperature", ) un_top_p = gr.Slider( 0.0, 1.0, value=_NONTHINK_SAMPLING["top_p"], step=0.05, label="top_p", ) with gr.Row(): un_top_k = gr.Slider(0, 200, value=20, step=1, label="top_k") un_min_p = gr.Slider(0.0, 1.0, value=0.0, step=0.01, label="min_p") un_rep_pen = gr.Slider( 0.5, 2.0, value=1.0, step=0.05, label="repetition_penalty", ) def _on_task_change(task: str): if task == "asr": return ( gr.update(value=fra.DEFAULT_ASR_PROMPT, interactive=False), gr.update(visible=False, value=False), ) return ( gr.update(value="Describe the audio in detail.", interactive=True), gr.update(visible=True), ) un_task.change(_on_task_change, [un_task], [un_question, un_think]) un_think.change(_apply_think_preset, [un_think], [un_temp, un_top_p]) gr.Examples( examples=[ [str(ASSETS / "asr_zh_fleurs.wav"), fra.DEFAULT_ASR_PROMPT, "asr"], [str(ASSETS / "assets_mmau_test.wav"), "What illness did Second speaker's friend suffer from?\n" "(A) Progressive arthritis (B) Progressive cancer " "(C) Acute pneumonia (D) Chronic heart disease", "understand"], ], inputs=[un_audio, un_question, un_task], outputs=[un_answer, un_reasoning], fn=lambda a, q, t: understand_audio( a, q, t, False, 1024, True, _NONTHINK_SAMPLING["temperature"], _NONTHINK_SAMPLING["top_p"], 20, 0.0, 1.0, ), cache_examples=False, label="Official examples", ) gr.HTML(_FOOTER_HTML) tts_btn.click( _handler_wrapper(clone_voice), [tts_ref_audio, tts_ref_text, tts_target, tts_lang, tts_seed, tts_rand, tts_cfg, tts_steps, tts_max_audio], [tts_out, tts_seed_out], ) vd_btn.click( _handler_wrapper(design_voice), [vd_instruction, vd_text, vd_seed, vd_rand, vd_cfg, vd_steps, vd_max_audio, vd_max_text], [vd_out, vd_tags, vd_seed_out], ) ed_btn.click( _handler_wrapper(edit_speech), [ed_audio, ed_instruction, ed_type, ed_seed, ed_rand, ed_cfg, ed_steps, ed_max_audio, ed_max_text], [ed_out, ed_text, ed_seed_out], ) un_btn.click( _handler_wrapper(understand_audio), [un_audio, un_question, un_task, un_think, un_max_tokens, un_do_sample, un_temp, un_top_p, un_top_k, un_min_p, un_rep_pen], [un_answer, un_reasoning], ) return demo def _handler_wrapper(fn): """Catches unexpected exceptions and surfaces them as gr.Error with a traceback in the log — a bare exception in a click handler just spins the UI forever with no feedback.""" def _wrapped(*args, **kwargs): try: return fn(*args, **kwargs) except gr.Error: raise except Exception as e: logger.exception("handler %s failed", fn.__name__) raise gr.Error(f"{type(e).__name__}: {e}\n\n{traceback.format_exc()}") return _wrapped # --------------------------------------------------------------------------- CLI def parse_args(): p = argparse.ArgumentParser(description="FireRedAudio Gradio Web Demo") p.add_argument( "--model_path", type=str, required=True, help="Directory holding the FireRedAudio checkpoint " "(config.json + safetensors shards).", ) p.add_argument( "--vae_decoder_path", type=str, required=True, help="Path to the RedAE decoder .pt file.", ) p.add_argument("--tokenizer_path", type=str, default=None, help="Defaults to --model_path.") p.add_argument("--processor_path", type=str, default=None, help="Defaults to --model_path.") p.add_argument("--device", type=str, default=None, help="cuda:N or cpu (defaults to cuda:0 if available).") p.add_argument("--host", type=str, default="0.0.0.0") p.add_argument("--port", type=int, default=7860) p.add_argument( "--root-path", dest="root_path", type=str, default="", help="Root path prefix when running behind a reverse proxy (e.g. /myapp).", ) p.add_argument( "--share", action="store_true", help="Launch a public gradio.live tunnel (requires internet).", ) return p.parse_args() def main(): global ENGINE args = parse_args() if args.device is None: args.device = "cuda:0" if torch.cuda.is_available() else "cpu" if args.device.startswith("cuda:"): torch.cuda.set_device(int(args.device.split(":")[1])) ENGINE = _build_engine( model_path=args.model_path, vae_decoder_path=args.vae_decoder_path, device=args.device, ) # Optional override paths — FireRedAudioInference already accepts them at # __init__, but we build the engine with defaults for a leaner signature; # honour explicit overrides by re-loading the tokenizer/processor here. if args.tokenizer_path: from transformers import AutoTokenizer ENGINE.tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_path) ENGINE.tokenizer.padding_side = "left" if args.processor_path: from fireredaudio.audio_encoder.processor import FireRedAudioProcessor ENGINE.processor = FireRedAudioProcessor.from_pretrained(args.processor_path) demo = build_demo() demo.queue(max_size=20).launch( server_name=args.host, server_port=args.port, root_path=args.root_path, share=args.share, show_error=True, allowed_paths=[str(REPO_ROOT / "assets")], theme=_THEME, css=_CSS, js=_DARK_JS, ) if __name__ == "__main__": main()