CoolFace
Apppublic

OpenMOSS-Team/MOSS-VL-Instruct-0708-Demo

sourceHugging Faceupdated 2mo agoView on Hugging Face
2likes
app.py661 linesDownload Raw Back to root
1"""MOSS-VL-Instruct Space — multi-turn media chat demo.2 3Full-width chat log with a chat/raw toggle; the input bar sits directly under4it: attach button INSIDE the input area (right side), pending media thumbnails5in a row just above the text (click a thumbnail to open the full-resolution6file), round send button beside the bar. Each turn sends whatever is staged:7media + text, media alone (default prompt), or text alone (no images sent).8TRUE MULTI-TURN: the model sees the whole conversation — earlier turns and9their media — via its offline_batch_generate session-state API (the state is10a plain message list held in gr.State and re-encoded on every stateless11ZeroGPU call). The 🗑 button resets the conversation. analyze_image /12analyze_video / analyze_text remain exposed as single-turn API/MCP tools.13 14MOCK mode (MOSS_DEMO_MOCK=1): no torch / spaces imports; canned responses for15local UI work.16"""17 18import inspect19import os20import time21 22os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")23 24MOCK = os.getenv("MOSS_DEMO_MOCK") == "1"25 26import gradio as gr27 28MODEL_ID = "OpenMOSS-Team/MOSS-VL-Instruct-0708"29 30if not MOCK:31    import spaces32    import torch33    from transformers import AutoModelForCausalLM, AutoProcessor34 35    processor = AutoProcessor.from_pretrained(36        MODEL_ID,37        trust_remote_code=True,38        frame_extract_num_threads=1,39    )40 41    # transformers 4.5x fast image processors pass `interpolation=` into42    # _preprocess, but this model's remote-code MossVLImageProcessorFast still43    # names the parameter `resample` (same torchvision InterpolationMode type,44    # only the name is stale) -> TypeError on every image request. Bridge the45    # rename here; this no-ops once the model repo updates the signature.46    _ip_cls = type(processor.image_processor)47    if "resample" in inspect.signature(_ip_cls._preprocess).parameters:48        _orig_preprocess = _ip_cls._preprocess49 50        def _compat_preprocess(self, images, *args, **kwargs):51            if "interpolation" in kwargs and "resample" not in kwargs:52                kwargs["resample"] = kwargs.pop("interpolation")53            return _orig_preprocess(self, images, *args, **kwargs)54 55        _ip_cls._preprocess = _compat_preprocess56 57    model = AutoModelForCausalLM.from_pretrained(58        MODEL_ID,59        trust_remote_code=True,60        torch_dtype=torch.bfloat16,61        attn_implementation="sdpa",62    ).to("cuda")63else:64    class _MockSpaces:65        """Effect-free stand-in for the spaces module in MOCK mode."""66 67        @staticmethod68        def GPU(*args, **kwargs):69            if args and callable(args[0]):70                return args[0]71            return lambda fn: fn72 73    spaces = _MockSpaces()74 75 76@spaces.GPU(duration=120)77def analyze_image(78    image: str,79    prompt: str,80    max_new_tokens: int,81    temperature: float,82    top_p: float,83    do_sample: bool,84) -> str:85    """Analyze an image with a text prompt.86 87    Args:88        image: Path of the input image to analyze.89        prompt: Text instruction or question about the image.90        max_new_tokens: Maximum number of tokens to generate.91        temperature: Sampling temperature (1.0 = greedy when do_sample=False).92        top_p: Nucleus sampling probability threshold.93        do_sample: Whether to use sampling instead of greedy decoding.94    """95    if image is None:96        return "Please upload an image."97    if not prompt.strip():98        prompt = "Describe this image in detail."99    if MOCK:100        time.sleep(1.2)101        return (102            f"[MOCK] Scripted image analysis for {os.path.basename(image)} — prompt: {prompt!r}.\n\n"103            + "This is a long mock paragraph to exercise scrolling in the response view. " * 12104        )105 106    return model.offline_image_generate(107        processor,108        prompt=prompt,109        image=image,110        max_new_tokens=int(max_new_tokens),111        temperature=float(temperature),112        top_p=float(top_p),113        do_sample=do_sample,114    )115 116 117@spaces.GPU(duration=180)118def analyze_video(119    video: str,120    prompt: str,121    max_new_tokens: int,122    temperature: float,123    top_p: float,124    video_fps: float,125    max_frames: int,126    do_sample: bool,127) -> str:128    """Analyze a video with a text prompt.129 130    Args:131        video: Path of the input video file to analyze.132        prompt: Text instruction or question about the video.133        max_new_tokens: Maximum number of tokens to generate.134        temperature: Sampling temperature (1.0 = greedy when do_sample=False).135        top_p: Nucleus sampling probability threshold.136        video_fps: Frames per second to sample from the video.137        max_frames: Maximum number of frames to extract.138        do_sample: Whether to use sampling instead of greedy decoding.139    """140    if video is None:141        return "Please upload a video."142    if not prompt.strip():143        prompt = "Describe this video in detail."144    if MOCK:145        time.sleep(1.5)146        return (147            f"[MOCK] Scripted video analysis for {os.path.basename(video)} — prompt: {prompt!r}.\n\n"148            + "This is a long mock paragraph to exercise scrolling in the response view. " * 12149        )150 151    return model.offline_video_generate(152        processor,153        prompt=prompt,154        video=video,155        max_new_tokens=int(max_new_tokens),156        temperature=float(temperature),157        top_p=float(top_p),158        video_fps=float(video_fps),159        max_frames=int(max_frames),160        do_sample=do_sample,161    )162 163 164@spaces.GPU(duration=90)165def analyze_text(166    prompt: str,167    max_new_tokens: int,168    temperature: float,169    top_p: float,170    do_sample: bool,171) -> str:172    """Answer a text-only prompt (no media attached).173 174    Args:175        prompt: The text instruction or question.176        max_new_tokens: Maximum number of tokens to generate.177        temperature: Sampling temperature (1.0 = greedy when do_sample=False).178        top_p: Nucleus sampling probability threshold.179        do_sample: Whether to use sampling instead of greedy decoding.180    """181    if not prompt.strip():182        return "Please enter a prompt."183    if MOCK:184        time.sleep(0.8)185        return f"[MOCK] Scripted text answer — prompt: {prompt!r}."186 187    query = {188        "prompt": prompt,189        "images": [],190        "videos": [],191        "generate_kwargs": {192            "max_new_tokens": int(max_new_tokens),193            "temperature": float(temperature),194            "top_p": float(top_p),195            "repetition_penalty": 1.0,196            "do_sample": bool(do_sample),197        },198    }199    return model._offline_generate_one_with_processor_overrides(processor, query)200 201 202@spaces.GPU(duration=180)203def chat_generate(session: list, prompt: str, images: list, videos: list,204                  max_new_tokens: int, temperature: float, top_p: float,205                  do_sample: bool, video_fps: float, max_frames: int) -> dict:206    """One multi-turn chat step: prior session messages + this turn -> reply.207 208    Built on the model's offline_batch_generate session API. `session` is a209    plain list of message dicts (media referenced by file path), so it210    round-trips through gr.State across stateless ZeroGPU calls; every turn211    re-encodes the full context including earlier media.212 213    Args:214        session: Message history from the previous call's "session" output ([] to start).215        prompt: This turn's text instruction (may be empty when media is attached).216        images: Image file paths attached to this turn (usually 0 or 1).217        videos: Video file paths attached to this turn (usually 0 or 1).218        max_new_tokens: Maximum number of tokens to generate.219        temperature: Sampling temperature (1.0 = greedy when do_sample=False).220        top_p: Nucleus sampling probability threshold.221        do_sample: Whether to use sampling instead of greedy decoding.222        video_fps: Frames per second to sample from attached videos.223        max_frames: Maximum frames to extract from attached videos.224 225    Returns:226        {"text": assistant reply, "session": updated message history to pass next turn}.227    """228    session = list(session or [])229    if MOCK:230        time.sleep(1.0)231        n_prev = sum(1 for m in session if isinstance(m, dict) and m.get("role") == "assistant")232        media = (list(images or []) + list(videos or []))233        media_note = f" with {os.path.basename(media[0])}" if media else ""234        text = (f"[MOCK] Turn {n_prev + 1} reply{media_note} — prompt: {prompt!r}. "235                f"History carried: {len(session)} messages.")236        content = [{"type": "image", "image": m} for m in (images or [])]237        content += [{"type": "video", "video": m} for m in (videos or [])]238        content.append({"type": "text", "text": prompt})239        return {"text": text, "session": session + [240            {"role": "user", "content": content},241            {"role": "assistant", "content": text},242        ]}243 244    query = {245        "prompt": prompt,246        "images": list(images or []),247        "videos": list(videos or []),248        "generate_kwargs": {249            "max_new_tokens": int(max_new_tokens),250            "temperature": float(temperature),251            "top_p": float(top_p),252            "repetition_penalty": 1.0,253            "do_sample": bool(do_sample),254        },255    }256    if videos:257        query["media_kwargs"] = {"video_fps": float(video_fps), "max_frames": int(max_frames)}258    out = model.offline_batch_generate(processor, [query], session_states=[session])259    return {"text": out["results"][0]["text"], "session": out["session_states"][0]}260 261 262# --- UI glue ---263 264IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}265 266MOCK_BADGE = "\n\n`MOCK mode — scripted model responses`"267 268# UI language packs, same switcher pattern as the live demo: radio choices are269# (label, value) pairs so switching language never fires change events.270LANGS = {271    "en": {272        "btn": "中文",273        "title": "# MOSS-VL-Instruct-0708\n\n"274                 "An 11B multimodal vision-language model for image and video understanding. "275                 "Attach an image or video (or none) and ask anything, turn by turn.",276        "view_chat": "💬 Chat",277        "view_raw": "{ } Raw",278        "ph": "Ask anything — attach an image/video for this turn, or none…",279        "adv": "Advanced Settings",280        "max_tokens": "Max New Tokens",281        "temperature": "Temperature",282        "top_p": "Top-p",283        "do_sample": "Do Sample",284        "fps": "Video FPS (video only)",285        "max_frames": "Max Frames (video only)",286    },287    "zh": {288        "btn": "English",289        "title": "# MOSS-VL-Instruct-0708\n\n"290                 "110 亿参数的多模态视觉语言模型,支持图片与视频理解。"291                 "每一轮可附带图片/视频(或不带媒体),随意提问。",292        "view_chat": "💬 对话",293        "view_raw": "{ } 原始",294        "ph": "随意提问——本轮可附带图片/视频,也可不带…",295        "adv": "高级设置",296        "max_tokens": "最大生成长度",297        "temperature": "温度",298        "top_p": "Top-p",299        "do_sample": "启用采样",300        "fps": "视频采样帧率(仅视频)",301        "max_frames": "最大帧数(仅视频)",302    },303}304 305 306def switch_lang(lang):307    lang = "zh" if lang == "en" else "en"308    L = LANGS[lang]309    return (310        lang,311        gr.update(value=L["btn"]),312        L["title"] + (MOCK_BADGE if MOCK else ""),313        gr.update(choices=[(L["view_chat"], "chat"), (L["view_raw"], "raw")]),314        gr.update(placeholder=L["ph"]),315        gr.update(label=L["adv"]),316        gr.update(label=L["max_tokens"]),317        gr.update(label=L["temperature"]),318        gr.update(label=L["top_p"]),319        gr.update(label=L["do_sample"]),320        gr.update(label=L["fps"]),321        gr.update(label=L["max_frames"]),322    )323 324 325def classify_media(path):326    return "image" if os.path.splitext(path)[1].lower() in IMAGE_EXTS else "video"327 328 329def _file_path(value):330    """Normalize a file entry (str | dict | FileData) to a path or None."""331    if isinstance(value, dict):332        value = value.get("path") or value.get("name")333    path = getattr(value, "path", value)334    return path or None335 336 337def run_analyze(msg, history, events, session,338                max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames):339    """One MULTI-TURN chat turn: send whatever is staged — media + text, media340    alone (default prompt), or text alone (no images sent). The model sees the341    whole session (earlier turns and their media) via the session-state API."""342    msg = msg or {}343    text = (msg.get("text") or "").strip()344    files = [p for p in (_file_path(f) for f in (msg.get("files") or [])) if p]345    if not text and not files:346        gr.Warning("Type a question or attach an image/video · 请输入问题或添加图片/视频")347        yield (gr.skip(),) * 4348        return349 350    media = files[0] if files else None351    kind = classify_media(media) if media else "text"352    prompt = text or f"Describe this {kind} in detail."353    session = list(session or [])354 355    history = list(history or [])356    events = list(events or [])357    if media:358        history.append({"role": "user", "content": {"path": media}})359    if text:360        history.append({"role": "user", "content": text})361    history.append({"role": "assistant", "content": "⏳ Analyzing · 分析中…"})362    params = {363        "max_new_tokens": int(max_new_tokens),364        "temperature": float(temperature),365        "top_p": float(top_p),366        "do_sample": bool(do_sample),367    }368    if kind == "video":369        params.update(video_fps=float(video_fps), max_frames=int(max_frames))370    events.append({"event": "request", "kind": kind,371                   "media": os.path.basename(media) if media else None,372                   "prompt": prompt, "session_msgs": len(session), "params": params})373    yield history, events, {"text": "", "files": []}, gr.skip()374 375    t0 = time.monotonic()376    try:377        out = chat_generate(378            session, prompt,379            [media] if kind == "image" else [],380            [media] if kind == "video" else [],381            max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames,382        )383    except Exception as exc:384        history[-1] = {"role": "assistant", "content": f"⚠️ {type(exc).__name__}: {exc}"}385        events.append({"event": "error", "message": f"{type(exc).__name__}: {exc}"})386        yield history, events, gr.skip(), gr.skip()387        return388    history[-1] = {"role": "assistant", "content": out["text"]}389    events.append({"event": "response", "elapsed_s": round(time.monotonic() - t0, 2), "text": out["text"]})390    yield history, events, gr.skip(), out["session"]391 392 393def clear_chat():394    return [], [], {"text": "", "files": []}, []395 396 397def toggle_view(choice):398    chat = choice == "chat"399    return gr.update(visible=chat), gr.update(visible=not chat)400 401 402CSS = """403#col-container { max-width: 1250px; margin: 0 auto; --console-h: 46px; --stage-h: 640px; }404#links-row { align-items: center; }405#links-md { flex-grow: 1; }406#lang-btn { flex-grow: 0 !important; min-width: 84px; }407/* full-width chat stage with a fixed height — content scrolls inside */408#message-stage {409  flex-grow: 0 !important; height: var(--stage-h); min-height: var(--stage-h); max-height: var(--stage-h);410  display: flex; flex-direction: column;411}412#message-stage > *, #message-stage .styler {413  flex-grow: 1 !important; display: flex; flex-direction: column; min-height: 0;414}415#chatbot, #raw-json {416  flex-grow: 1 !important; min-height: 0 !important; height: auto !important; max-height: none !important;417}418/* segmented chat/raw toggle: two half-width buttons, centered text, no radio419   dot — black = off, accent yellow = on (same as the live demo) */420#view-toggle { flex-grow: 0 !important; flex-shrink: 0 !important; min-height: fit-content; }421#view-toggle .wrap { display: flex; flex-direction: row; gap: 0; width: 100%; }422#view-toggle label {423  flex: 1 1 50%; justify-content: center; text-align: center; margin: 0;424  padding: var(--spacing-sm) 0; cursor: pointer; border-radius: 0;425  background: #141414; color: #9aa0a6; border: 1px solid var(--border-color-primary);426  transition: background 0.15s, color 0.15s;427}428#view-toggle label:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }429#view-toggle label:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }430#view-toggle label.selected { background: var(--color-accent); color: #111; font-weight: 600; }431#view-toggle input[type="radio"] { display: none; }432/* input line INSIDE the message block: one divider line separates it from433   the chat — no second box, no gap */434#session-console {435  flex-grow: 0 !important; align-items: center; gap: 2px; margin: 0;436  border-top: 1px solid var(--border-color-primary);437  padding: 4px 8px; min-height: var(--console-h);438}439#session-console .form {440  flex: 1 1 auto; border: none !important; background: transparent !important; box-shadow: none !important;441}442/* the multimodal input renders bare — the surrounding block provides the box */443#prompt-box { background: transparent !important; border: none !important; box-shadow: none !important; }444#prompt-box .full-container { padding: 0 !important; }445/* the floating pending strip sits above the input line — nothing may clip it */446#prompt-box, #prompt-box .full-container, #prompt-box .input-container,447#session-console, #session-console .form { overflow: visible !important; }448#prompt-box .input-wrapper {449  position: relative; min-height: 38px;450  border: none; border-radius: 0; background: transparent;451  padding: 0 var(--spacing-xs);452}453/* pending media live in a floating strip at the BOTTOM OF THE MESSAGE BLOCK454   (just above the input bar), not inside the bar */455#prompt-box .thumbnails {456  position: absolute; bottom: calc(100% + 8px); left: 0; margin: 0;457  padding: 5px 8px; z-index: 30; width: auto; max-width: 100%;458  background: rgba(20, 20, 20, 0.55); backdrop-filter: blur(3px);459  border-radius: var(--radius-md);460}461#prompt-box .input-row { align-items: center; }462#prompt-box .input-row textarea {463  order: 1; padding: 6px 8px; line-height: 20px; resize: none;464  max-height: 34px; overflow-y: auto; scrollbar-width: none;465}466#prompt-box .input-row textarea::-webkit-scrollbar { display: none; width: 0; }467#prompt-box .input-row button { order: 2; }468/* pending thumbnails: whole tile opens the full-res file; the delete control469   shrinks to a corner badge so it doesn't swallow the click */470#prompt-box .thumbnails img { cursor: zoom-in; }471#prompt-box .thumbnail-wrapper .delete-button {472  inset: auto; top: -4px; right: -4px; width: 18px; height: 18px;473  border-radius: 50%; opacity: 1; padding: 0;474}475#prompt-box .thumbnail-wrapper .delete-button svg { width: 10px; height: 10px; }476/* Citrus dark mode leaves example chips near-black on dark background —477   follow the theme's body text color (white on black / black on white) */478.dark .gallery-item { color: var(--body-text-color) !important; }479/* send + clear-chat: flat line-form icon buttons, same look as the attach480   icon inside the input line */481#send-btn, #clear-chat-btn {482  width: 36px; min-width: 36px !important;483  height: 36px !important; min-height: 36px !important;484  border-radius: var(--radius-md); padding: 0; font-size: 1rem; flex-grow: 0 !important;485  background: transparent !important; border: none !important; box-shadow: none !important;486  color: var(--body-text-color) !important;487}488#send-btn:hover, #clear-chat-btn:hover { background: var(--button-secondary-background-fill) !important; }489/* scrollbars: no rail is drawn; the thumb is invisible until the pointer490   enters the rail strip at the scrollable edge (.sb-hot, set by the page491   script) or grabs the thumb itself. scrollbar-color drives modern492   Chrome/Firefox; the ::-webkit rules cover older WebKit. */493* { scrollbar-width: thin; scrollbar-color: transparent transparent; }494.sb-hot { scrollbar-color: rgba(128, 128, 128, 0.55) transparent !important; }495::-webkit-scrollbar { width: 10px; height: 10px; background: transparent !important; }496::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent !important; }497::-webkit-scrollbar-button { display: none !important; width: 0 !important; height: 0 !important; }498::-webkit-scrollbar-thumb { background: transparent; border-radius: 5px; }499.sb-hot::-webkit-scrollbar-thumb,500::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { background: rgba(128, 128, 128, 0.6) !important; }501"""502 503# Page JS: (1) keep the raw event view pinned to the bottom while it grows,504# unless the user has scrolled up to read; (2) clicking a pending thumbnail505# opens the full-resolution file (the corner ✕ still deletes).506PAGE_JS = """507() => {508  if (!window.__mossSbHot) {509    window.__mossSbHot = true;510    let hot = null;511    document.addEventListener('mousemove', (e) => {512      let el = e.target instanceof Element ? e.target : null;513      let found = null;514      while (el && el !== document.documentElement) {515        const vs = el.scrollHeight > el.clientHeight + 4;516        const hs = el.scrollWidth > el.clientWidth + 4;517        const cs = vs || hs ? getComputedStyle(el) : null;518        if (cs && /(auto|scroll)/.test(cs.overflowY + cs.overflowX)) {519          const r = el.getBoundingClientRect();520          if ((vs && r.right - e.clientX <= 18) || (hs && r.bottom - e.clientY <= 18)) found = el;521          break;522        }523        el = el.parentElement;524      }525      if (found !== hot) {526        if (hot) hot.classList.remove('sb-hot');527        if (found) found.classList.add('sb-hot');528        hot = found;529      }530    }, {passive: true});531  }532  if (window.__mossInstructInit) return;533  window.__mossInstructInit = true;534  document.addEventListener('click', (e) => {535    if (e.target.closest('button')) return;536    const img = e.target.closest('#prompt-box .thumbnails img');537    if (img && img.src) window.open(img.src, '_blank');538  }, true);539  const timer = setInterval(() => {540    const rawRoot = document.querySelector('#raw-json');541    if (!rawRoot) return;542    clearInterval(timer);543    const obs = new MutationObserver(() => {544      if (rawRoot.offsetParent === null) return;545      const sc = [...rawRoot.querySelectorAll('*')].find(e => e.scrollHeight > e.clientHeight + 8);546      if (!sc) return;547      if (sc.scrollHeight - sc.scrollTop - sc.clientHeight < 240) sc.scrollTop = sc.scrollHeight;548    });549    obs.observe(rawRoot, {childList: true, subtree: true, characterData: true});550  }, 500);551}552"""553 554with gr.Blocks(title="MOSS-VL-Instruct Demo") as demo:555    EN = LANGS["en"]556    with gr.Column(elem_id="col-container"):557        title_md = gr.Markdown(EN["title"] + (MOCK_BADGE if MOCK else ""))558        with gr.Row(elem_id="links-row"):559            gr.Markdown(560                "[Model Card](https://huggingface.co/OpenMOSS-Team/MOSS-VL-Instruct-0708) | "561                "[GitHub](https://github.com/OpenMOSS/MOSS-VL)",562                elem_id="links-md",563            )564            lang_btn = gr.Button(EN["btn"], size="sm", scale=0, elem_id="lang-btn")565 566        with gr.Group(elem_id="message-stage"):567            view_toggle = gr.Radio(568                [(EN["view_chat"], "chat"), (EN["view_raw"], "raw")],569                value="chat",570                show_label=False,571                container=False,572                elem_id="view-toggle",573            )574            chatbot = gr.Chatbot(575                height=560,576                show_label=False,577                buttons=["copy"],578                autoscroll=True,579                elem_id="chatbot",580            )581            raw_json = gr.JSON(582                value=[], show_label=False, visible=False, height=560, elem_id="raw-json"583            )584            # input line lives INSIDE the same block, separated from the chat585            # by a single divider line; send/clear are flat line-form icons586            # matching the attach icon587            with gr.Row(elem_id="session-console"):588                prompt_box = gr.MultimodalTextbox(589                    scale=1,590                    show_label=False,591                    container=False,592                    file_types=["image", "video"],593                    file_count="single",594                    submit_btn=False,595                    placeholder=EN["ph"],596                    elem_id="prompt-box",597                )598                send_btn = gr.Button("➤", variant="secondary", scale=0, elem_id="send-btn")599                clear_chat_btn = gr.Button("🗑︎", variant="secondary", scale=0, elem_id="clear-chat-btn")600 601        with gr.Accordion(EN["adv"], open=False) as adv_acc:602            with gr.Row():603                max_new_tokens = gr.Slider(64, 2048, value=512, step=64, label=EN["max_tokens"])604                temperature = gr.Slider(0.1, 2.0, value=1.0, step=0.1, label=EN["temperature"])605                top_p = gr.Slider(0.1, 1.0, value=1.0, step=0.05, label=EN["top_p"])606                do_sample = gr.Checkbox(label=EN["do_sample"], value=False)607            with gr.Row():608                video_fps = gr.Slider(0.5, 4.0, value=1.0, step=0.5, label=EN["fps"])609                max_frames = gr.Slider(8, 256, value=64, step=8, label=EN["max_frames"])610 611        gr.Examples(612            examples=[613                [{"text": "Extract the store name, waiter name, bill number, number of people, items purchased with their quantities and amounts, total amount, and print time from this receipt. Output in JSON format.", "files": ["example_bill.png"]}],614                [{"text": "Describe this image in detail.", "files": ["astronaut.jpg"]}],615                [{"text": "What species of bird is this? Describe its appearance and habitat.", "files": ["bird_kingfisher.jpg"]}],616                [{"text": "Describe what happens in this video.", "files": ["example_video.mp4"]}],617            ],618            inputs=[prompt_box],619            label="Examples",620        )621 622    ui_lang = gr.State("en")623    chat_session = gr.State([])  # model-side multi-turn message history624 625    lang_btn.click(626        switch_lang,627        [ui_lang],628        [ui_lang, lang_btn, title_md, view_toggle, prompt_box,629         adv_acc, max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames],630        api_visibility="private",631    )632    view_toggle.change(toggle_view, [view_toggle], [chatbot, raw_json], api_visibility="private")633 634    analyze_inputs = [prompt_box, chatbot, raw_json, chat_session,635                      max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames]636    analyze_outputs = [chatbot, raw_json, prompt_box, chat_session]637    send_btn.click(638        run_analyze, analyze_inputs, analyze_outputs,639        api_visibility="private", show_progress="hidden",640    )641    prompt_box.submit(642        run_analyze, analyze_inputs, analyze_outputs,643        api_visibility="private", show_progress="hidden",644    )645    clear_chat_btn.click(646        clear_chat, None, [chatbot, raw_json, prompt_box, chat_session],647        api_visibility="private",648    )649 650    gr.api(analyze_image, api_name="analyze_image")651    gr.api(analyze_video, api_name="analyze_video")652    gr.api(analyze_text, api_name="analyze_text")653    gr.api(chat_generate, api_name="chat")654    demo.load(None, None, None, js=PAGE_JS, api_visibility="private")655 656 657if __name__ == "__main__":658    # gradio 6.x: theme/css/mcp_server are launch() parameters659    demo.queue(max_size=32)660    demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)661