OpenMOSS-Team/MOSS-VL-Instruct-0708-Demo
2
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 