Spaces:
Running on Zero
Running on Zero
Commit ·
65be679
1
Parent(s): 7b97603
Single-stage layout: one image/video block, chat/raw response view, fixed equal heights.
Browse files- One media block (gr.File + scaled-to-fit previews) replaces the two tabs;
examples merged into one table (3 image rows + 1 video row).
- Right response block mirrors the live demo: segmented Chat/{ } Raw toggle,
chatbot + raw request/response JSON, pinned to a fixed 680px height that
matches the left column exactly; long text scrolls inside with transparent
hover-only scrollbars.
- analyze_image / analyze_video stay exposed as API/MCP tools; a MOCK mode
(MOSS_DEMO_MOCK=1) allows local UI work without torch/spaces.
- sdk bumped to gradio 6.15.1 (same as the realtime Space).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 🧠
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
short_description: Image and video understanding with MOSS-VL multimodal model
|
| 10 |
python_version: "3.12"
|
|
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.15.1
|
| 8 |
app_file: app.py
|
| 9 |
short_description: Image and video understanding with MOSS-VL multimodal model
|
| 10 |
python_version: "3.12"
|
app.py
CHANGED
|
@@ -1,30 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
|
| 3 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
import gradio as gr
|
| 8 |
-
from transformers import AutoModelForCausalLM, AutoProcessor
|
| 9 |
|
| 10 |
MODEL_ID = "OpenMOSS-Team/MOSS-VL-Instruct-0708"
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
@spaces.GPU(duration=120)
|
| 26 |
def analyze_image(
|
| 27 |
-
image,
|
| 28 |
prompt: str,
|
| 29 |
max_new_tokens: int,
|
| 30 |
temperature: float,
|
|
@@ -34,7 +60,7 @@ def analyze_image(
|
|
| 34 |
"""Analyze an image with a text prompt.
|
| 35 |
|
| 36 |
Args:
|
| 37 |
-
image:
|
| 38 |
prompt: Text instruction or question about the image.
|
| 39 |
max_new_tokens: Maximum number of tokens to generate.
|
| 40 |
temperature: Sampling temperature (1.0 = greedy when do_sample=False).
|
|
@@ -45,8 +71,14 @@ def analyze_image(
|
|
| 45 |
return "Please upload an image."
|
| 46 |
if not prompt.strip():
|
| 47 |
prompt = "Describe this image in detail."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
processor,
|
| 51 |
prompt=prompt,
|
| 52 |
image=image,
|
|
@@ -55,12 +87,11 @@ def analyze_image(
|
|
| 55 |
top_p=float(top_p),
|
| 56 |
do_sample=do_sample,
|
| 57 |
)
|
| 58 |
-
return result
|
| 59 |
|
| 60 |
|
| 61 |
@spaces.GPU(duration=180)
|
| 62 |
def analyze_video(
|
| 63 |
-
video,
|
| 64 |
prompt: str,
|
| 65 |
max_new_tokens: int,
|
| 66 |
temperature: float,
|
|
@@ -72,7 +103,7 @@ def analyze_video(
|
|
| 72 |
"""Analyze a video with a text prompt.
|
| 73 |
|
| 74 |
Args:
|
| 75 |
-
video:
|
| 76 |
prompt: Text instruction or question about the video.
|
| 77 |
max_new_tokens: Maximum number of tokens to generate.
|
| 78 |
temperature: Sampling temperature (1.0 = greedy when do_sample=False).
|
|
@@ -85,8 +116,14 @@ def analyze_video(
|
|
| 85 |
return "Please upload a video."
|
| 86 |
if not prompt.strip():
|
| 87 |
prompt = "Describe this video in detail."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
processor,
|
| 91 |
prompt=prompt,
|
| 92 |
video=video,
|
|
@@ -97,105 +134,253 @@ def analyze_video(
|
|
| 97 |
max_frames=int(max_frames),
|
| 98 |
do_sample=do_sample,
|
| 99 |
)
|
| 100 |
-
return result
|
| 101 |
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
.
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
"""
|
| 117 |
)
|
| 118 |
|
| 119 |
-
with gr.Tab("Image Understanding"):
|
| 120 |
-
with gr.Row():
|
| 121 |
-
with gr.Column(scale=1):
|
| 122 |
-
img_input = gr.Image(type="filepath", label="Input Image")
|
| 123 |
-
img_prompt = gr.Textbox(
|
| 124 |
-
label="Prompt",
|
| 125 |
-
placeholder="Describe this image in detail.",
|
| 126 |
-
value="Describe this image in detail.",
|
| 127 |
-
lines=2,
|
| 128 |
-
)
|
| 129 |
-
img_btn = gr.Button("Analyze Image", variant="primary")
|
| 130 |
-
with gr.Column(scale=1):
|
| 131 |
-
img_output = gr.Textbox(label="Response", lines=12, show_copy_button=True)
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
gr.Examples(
|
| 141 |
-
examples=[
|
| 142 |
-
["example_bill.png", "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.", 512, 1.0, 1.0, False],
|
| 143 |
-
["astronaut.jpg", "Describe this image in detail.", 256, 1.0, 1.0, False],
|
| 144 |
-
["bird_kingfisher.jpg", "What species of bird is this? Describe its appearance and habitat.", 256, 1.0, 1.0, False],
|
| 145 |
-
],
|
| 146 |
-
inputs=[img_input, img_prompt, img_max_tokens, img_temperature, img_top_p, img_do_sample],
|
| 147 |
-
outputs=img_output,
|
| 148 |
-
fn=analyze_image,
|
| 149 |
-
cache_examples=True,
|
| 150 |
-
cache_mode="lazy",
|
| 151 |
-
)
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
)
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
label="Prompt",
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
with gr.Accordion("Advanced Settings", open=False):
|
| 175 |
with gr.Row():
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 182 |
|
| 183 |
gr.Examples(
|
| 184 |
examples=[
|
| 185 |
-
["
|
|
|
|
|
|
|
|
|
|
| 186 |
],
|
| 187 |
-
inputs=[
|
| 188 |
-
|
| 189 |
-
fn=analyze_video,
|
| 190 |
-
cache_examples=True,
|
| 191 |
-
cache_mode="lazy",
|
| 192 |
)
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MOSS-VL-Instruct Space — single-stage media QA demo.
|
| 2 |
+
|
| 3 |
+
ONE media stage (image OR video upload) + prompt on the left; the response log
|
| 4 |
+
with a chat/raw toggle on the right — mirroring the MOSS-VL-Realtime live demo
|
| 5 |
+
styling. analyze_image / analyze_video stay exposed as API/MCP tools.
|
| 6 |
+
|
| 7 |
+
MOCK mode (MOSS_DEMO_MOCK=1): no torch / spaces imports; canned responses for
|
| 8 |
+
local UI work.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
import os
|
| 12 |
+
import time
|
| 13 |
|
| 14 |
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 15 |
|
| 16 |
+
MOCK = os.getenv("MOSS_DEMO_MOCK") == "1"
|
| 17 |
+
|
| 18 |
import gradio as gr
|
|
|
|
| 19 |
|
| 20 |
MODEL_ID = "OpenMOSS-Team/MOSS-VL-Instruct-0708"
|
| 21 |
|
| 22 |
+
if not MOCK:
|
| 23 |
+
import spaces
|
| 24 |
+
import torch
|
| 25 |
+
from transformers import AutoModelForCausalLM, AutoProcessor
|
| 26 |
+
|
| 27 |
+
processor = AutoProcessor.from_pretrained(
|
| 28 |
+
MODEL_ID,
|
| 29 |
+
trust_remote_code=True,
|
| 30 |
+
frame_extract_num_threads=1,
|
| 31 |
+
)
|
| 32 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 33 |
+
MODEL_ID,
|
| 34 |
+
trust_remote_code=True,
|
| 35 |
+
torch_dtype=torch.bfloat16,
|
| 36 |
+
attn_implementation="sdpa",
|
| 37 |
+
).to("cuda")
|
| 38 |
+
else:
|
| 39 |
+
class _MockSpaces:
|
| 40 |
+
"""Effect-free stand-in for the spaces module in MOCK mode."""
|
| 41 |
+
|
| 42 |
+
@staticmethod
|
| 43 |
+
def GPU(*args, **kwargs):
|
| 44 |
+
if args and callable(args[0]):
|
| 45 |
+
return args[0]
|
| 46 |
+
return lambda fn: fn
|
| 47 |
+
|
| 48 |
+
spaces = _MockSpaces()
|
| 49 |
|
| 50 |
|
| 51 |
@spaces.GPU(duration=120)
|
| 52 |
def analyze_image(
|
| 53 |
+
image: str,
|
| 54 |
prompt: str,
|
| 55 |
max_new_tokens: int,
|
| 56 |
temperature: float,
|
|
|
|
| 60 |
"""Analyze an image with a text prompt.
|
| 61 |
|
| 62 |
Args:
|
| 63 |
+
image: Path of the input image to analyze.
|
| 64 |
prompt: Text instruction or question about the image.
|
| 65 |
max_new_tokens: Maximum number of tokens to generate.
|
| 66 |
temperature: Sampling temperature (1.0 = greedy when do_sample=False).
|
|
|
|
| 71 |
return "Please upload an image."
|
| 72 |
if not prompt.strip():
|
| 73 |
prompt = "Describe this image in detail."
|
| 74 |
+
if MOCK:
|
| 75 |
+
time.sleep(1.2)
|
| 76 |
+
return (
|
| 77 |
+
f"[MOCK] Scripted image analysis for {os.path.basename(image)} — prompt: {prompt!r}.\n\n"
|
| 78 |
+
+ "This is a long mock paragraph to exercise scrolling in the response view. " * 12
|
| 79 |
+
)
|
| 80 |
|
| 81 |
+
return model.offline_image_generate(
|
| 82 |
processor,
|
| 83 |
prompt=prompt,
|
| 84 |
image=image,
|
|
|
|
| 87 |
top_p=float(top_p),
|
| 88 |
do_sample=do_sample,
|
| 89 |
)
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
@spaces.GPU(duration=180)
|
| 93 |
def analyze_video(
|
| 94 |
+
video: str,
|
| 95 |
prompt: str,
|
| 96 |
max_new_tokens: int,
|
| 97 |
temperature: float,
|
|
|
|
| 103 |
"""Analyze a video with a text prompt.
|
| 104 |
|
| 105 |
Args:
|
| 106 |
+
video: Path of the input video file to analyze.
|
| 107 |
prompt: Text instruction or question about the video.
|
| 108 |
max_new_tokens: Maximum number of tokens to generate.
|
| 109 |
temperature: Sampling temperature (1.0 = greedy when do_sample=False).
|
|
|
|
| 116 |
return "Please upload a video."
|
| 117 |
if not prompt.strip():
|
| 118 |
prompt = "Describe this video in detail."
|
| 119 |
+
if MOCK:
|
| 120 |
+
time.sleep(1.5)
|
| 121 |
+
return (
|
| 122 |
+
f"[MOCK] Scripted video analysis for {os.path.basename(video)} — prompt: {prompt!r}.\n\n"
|
| 123 |
+
+ "This is a long mock paragraph to exercise scrolling in the response view. " * 12
|
| 124 |
+
)
|
| 125 |
|
| 126 |
+
return model.offline_video_generate(
|
| 127 |
processor,
|
| 128 |
prompt=prompt,
|
| 129 |
video=video,
|
|
|
|
| 134 |
max_frames=int(max_frames),
|
| 135 |
do_sample=do_sample,
|
| 136 |
)
|
|
|
|
| 137 |
|
| 138 |
|
| 139 |
+
# --- UI glue ---
|
| 140 |
+
|
| 141 |
+
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def classify_media(path):
|
| 145 |
+
return "image" if os.path.splitext(path)[1].lower() in IMAGE_EXTS else "video"
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _file_path(value):
|
| 149 |
+
"""Normalize a gr.File value (str | list | dict | None) to a path or None."""
|
| 150 |
+
if isinstance(value, list):
|
| 151 |
+
value = value[0] if value else None
|
| 152 |
+
if isinstance(value, dict):
|
| 153 |
+
value = value.get("path") or value.get("name")
|
| 154 |
+
return value or None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def run_analyze(media, prompt_text, max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames):
|
| 158 |
+
path = _file_path(media)
|
| 159 |
+
if not path:
|
| 160 |
+
gr.Warning("Upload an image or video first · 请先上传图片或视频")
|
| 161 |
+
yield gr.skip(), gr.skip()
|
| 162 |
+
return
|
| 163 |
+
kind = classify_media(path)
|
| 164 |
+
prompt_text = (prompt_text or "").strip() or f"Describe this {kind} in detail."
|
| 165 |
+
|
| 166 |
+
history = [
|
| 167 |
+
{"role": "user", "content": {"path": path}},
|
| 168 |
+
{"role": "user", "content": prompt_text},
|
| 169 |
+
{"role": "assistant", "content": "⏳ Analyzing · 分析中…"},
|
| 170 |
+
]
|
| 171 |
+
params = {
|
| 172 |
+
"max_new_tokens": int(max_new_tokens),
|
| 173 |
+
"temperature": float(temperature),
|
| 174 |
+
"top_p": float(top_p),
|
| 175 |
+
"do_sample": bool(do_sample),
|
| 176 |
+
}
|
| 177 |
+
if kind == "video":
|
| 178 |
+
params.update(video_fps=float(video_fps), max_frames=int(max_frames))
|
| 179 |
+
events = [{"event": "request", "media": os.path.basename(path), "kind": kind, "prompt": prompt_text, "params": params}]
|
| 180 |
+
yield history, events
|
| 181 |
+
|
| 182 |
+
t0 = time.monotonic()
|
| 183 |
+
try:
|
| 184 |
+
if kind == "image":
|
| 185 |
+
result = analyze_image(path, prompt_text, max_new_tokens, temperature, top_p, do_sample)
|
| 186 |
+
else:
|
| 187 |
+
result = analyze_video(path, prompt_text, max_new_tokens, temperature, top_p, video_fps, max_frames, do_sample)
|
| 188 |
+
except Exception as exc:
|
| 189 |
+
history[-1] = {"role": "assistant", "content": f"⚠️ {type(exc).__name__}: {exc}"}
|
| 190 |
+
events.append({"event": "error", "message": f"{type(exc).__name__}: {exc}"})
|
| 191 |
+
yield history, events
|
| 192 |
+
return
|
| 193 |
+
history[-1] = {"role": "assistant", "content": result}
|
| 194 |
+
events.append({"event": "response", "elapsed_s": round(time.monotonic() - t0, 2), "text": result})
|
| 195 |
+
yield history, events
|
| 196 |
+
|
| 197 |
|
| 198 |
+
def on_media_change(value):
|
| 199 |
+
path = _file_path(value)
|
| 200 |
+
if not path:
|
| 201 |
+
return gr.update(visible=True), gr.update(visible=False, value=None), gr.update(visible=False, value=None)
|
| 202 |
+
kind = classify_media(path)
|
| 203 |
+
return (
|
| 204 |
+
gr.update(visible=False),
|
| 205 |
+
gr.update(visible=kind == "video", value=path if kind == "video" else None),
|
| 206 |
+
gr.update(visible=kind == "image", value=path if kind == "image" else None),
|
| 207 |
)
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
+
def clear_media():
|
| 211 |
+
return (
|
| 212 |
+
gr.update(visible=True, value=None),
|
| 213 |
+
gr.update(visible=False, value=None),
|
| 214 |
+
gr.update(visible=False, value=None),
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def toggle_view(choice):
|
| 219 |
+
chat = choice == "chat"
|
| 220 |
+
return gr.update(visible=chat), gr.update(visible=not chat)
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
+
CSS = """
|
| 224 |
+
#col-container { max-width: 1250px; margin: 0 auto; --console-h: 46px; --stage-h: 680px; }
|
| 225 |
+
/* FIXED overall height: the right response block is pinned to --stage-h and its
|
| 226 |
+
content scrolls inside; the left column stretches to match, with the media
|
| 227 |
+
widget absorbing the spare height above the prompt + action row */
|
| 228 |
+
#message-stage {
|
| 229 |
+
flex-grow: 0 !important; height: var(--stage-h); min-height: var(--stage-h); max-height: var(--stage-h);
|
| 230 |
+
display: flex; flex-direction: column;
|
| 231 |
+
}
|
| 232 |
+
#media-stage { flex-grow: 1 !important; display: flex; flex-direction: column; min-height: 0; }
|
| 233 |
+
#media-stage > *, #media-stage .styler,
|
| 234 |
+
#message-stage > *, #message-stage .styler {
|
| 235 |
+
flex-grow: 1 !important; display: flex; flex-direction: column; min-height: 0;
|
| 236 |
+
}
|
| 237 |
+
#stage-file, #preview-video, #preview-image { flex-grow: 1 !important; min-height: 0; }
|
| 238 |
+
/* chat/raw fill the fixed stage and scroll internally — never grow with content */
|
| 239 |
+
#chatbot, #raw-json {
|
| 240 |
+
flex-grow: 1 !important; min-height: 0 !important; height: auto !important; max-height: none !important;
|
| 241 |
+
}
|
| 242 |
+
#stage-file .boundedheight, #preview-video .boundedheight, #preview-image .boundedheight,
|
| 243 |
+
#stage-file [data-testid="file"], #preview-video video, #preview-image img {
|
| 244 |
+
height: 100% !important; max-height: none !important;
|
| 245 |
+
}
|
| 246 |
+
/* media scales to fit its fixed frame */
|
| 247 |
+
#preview-image img, #preview-video video { object-fit: contain; width: 100%; }
|
| 248 |
+
/* segmented chat/raw toggle: two half-width buttons, centered text, no radio
|
| 249 |
+
dot — black = off, accent yellow = on (same as the live demo) */
|
| 250 |
+
#view-toggle { flex-grow: 0 !important; flex-shrink: 0 !important; min-height: fit-content; }
|
| 251 |
+
#view-toggle .wrap { display: flex; flex-direction: row; gap: 0; width: 100%; }
|
| 252 |
+
#view-toggle label {
|
| 253 |
+
flex: 1 1 50%; justify-content: center; text-align: center; margin: 0;
|
| 254 |
+
padding: var(--spacing-sm) 0; cursor: pointer; border-radius: 0;
|
| 255 |
+
background: #141414; color: #9aa0a6; border: 1px solid var(--border-color-primary);
|
| 256 |
+
transition: background 0.15s, color 0.15s;
|
| 257 |
+
}
|
| 258 |
+
#view-toggle label:first-child { border-radius: var(--radius-md) 0 0 var(--radius-md); }
|
| 259 |
+
#view-toggle label:last-child { border-radius: 0 var(--radius-md) var(--radius-md) 0; }
|
| 260 |
+
#view-toggle label.selected { background: var(--color-accent); color: #111; font-weight: 600; }
|
| 261 |
+
#view-toggle input[type="radio"] { display: none; }
|
| 262 |
+
/* prompt + action row stay natural-sized at the column bottom — including the
|
| 263 |
+
.form element gradio wraps around the textbox, which is flex-grow:1 by
|
| 264 |
+
default and would otherwise eat the column's spare height */
|
| 265 |
+
#prompt-block, #action-row { flex-grow: 0 !important; }
|
| 266 |
+
#col-container .form:has(#prompt-block) { flex-grow: 0 !important; }
|
| 267 |
+
#action-row { margin-top: auto; }
|
| 268 |
+
#action-row button { height: var(--console-h); min-height: var(--console-h); white-space: nowrap; }
|
| 269 |
+
/* scrollbars: invisible until the pointer is over the thin scrollbar strip;
|
| 270 |
+
no track rail, no arrow buttons. (Firefox has no thumb-hover selector, so
|
| 271 |
+
it falls back to a thin transparent bar revealed on container hover.) */
|
| 272 |
+
@supports not selector(::-webkit-scrollbar) {
|
| 273 |
+
* { scrollbar-width: thin; scrollbar-color: transparent transparent; }
|
| 274 |
+
*:hover { scrollbar-color: rgba(128, 128, 128, 0.45) transparent; }
|
| 275 |
+
}
|
| 276 |
+
::-webkit-scrollbar { width: 8px; height: 8px; background: transparent !important; }
|
| 277 |
+
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent !important; }
|
| 278 |
+
::-webkit-scrollbar-button { display: none !important; width: 0 !important; height: 0 !important; }
|
| 279 |
+
::-webkit-scrollbar-thumb { background: transparent !important; border-radius: 4px; }
|
| 280 |
+
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { background: rgba(128, 128, 128, 0.55) !important; }
|
| 281 |
+
"""
|
| 282 |
+
|
| 283 |
+
with gr.Blocks(title="MOSS-VL-Instruct Demo") as demo:
|
| 284 |
+
with gr.Column(elem_id="col-container"):
|
| 285 |
+
gr.Markdown(
|
| 286 |
+
"# MOSS-VL-Instruct-0708\n\n"
|
| 287 |
+
"An 11B multimodal vision-language model for image and video understanding. "
|
| 288 |
+
"Upload an image or video and ask any question about it.\n\n"
|
| 289 |
+
"[Model Card](https://huggingface.co/OpenMOSS-Team/MOSS-VL-Instruct-0708) | "
|
| 290 |
+
"[GitHub](https://github.com/OpenMOSS/MOSS-VL)"
|
| 291 |
+
+ ("\n\n`MOCK mode — scripted model responses`" if MOCK else "")
|
| 292 |
)
|
| 293 |
|
| 294 |
+
with gr.Row(equal_height=True):
|
| 295 |
+
with gr.Column(scale=5):
|
| 296 |
+
with gr.Group(elem_id="media-stage"):
|
| 297 |
+
media_file = gr.File(
|
| 298 |
+
file_types=["image", "video"],
|
| 299 |
+
label="Image or video · 图片或视频",
|
| 300 |
+
height=430,
|
| 301 |
+
elem_id="stage-file",
|
| 302 |
+
)
|
| 303 |
+
preview_video = gr.Video(
|
| 304 |
+
visible=False, interactive=False, height=430, show_label=False,
|
| 305 |
+
autoplay=False, elem_id="preview-video",
|
| 306 |
+
)
|
| 307 |
+
preview_image = gr.Image(
|
| 308 |
+
visible=False, interactive=False, height=430, show_label=False,
|
| 309 |
+
elem_id="preview-image",
|
| 310 |
+
)
|
| 311 |
+
prompt = gr.Textbox(
|
| 312 |
label="Prompt",
|
| 313 |
+
lines=3,
|
| 314 |
+
placeholder="Describe this image/video in detail.",
|
| 315 |
+
elem_id="prompt-block",
|
| 316 |
)
|
| 317 |
+
with gr.Row(elem_id="action-row"):
|
| 318 |
+
clear_btn = gr.Button("Clear media · 清除媒体", variant="secondary")
|
| 319 |
+
analyze_btn = gr.Button("▶ Analyze · 分析", variant="primary")
|
| 320 |
+
|
| 321 |
+
with gr.Column(scale=6):
|
| 322 |
+
with gr.Group(elem_id="message-stage"):
|
| 323 |
+
view_toggle = gr.Radio(
|
| 324 |
+
[("💬 Chat", "chat"), ("{ } Raw", "raw")],
|
| 325 |
+
value="chat",
|
| 326 |
+
show_label=False,
|
| 327 |
+
container=False,
|
| 328 |
+
elem_id="view-toggle",
|
| 329 |
+
)
|
| 330 |
+
chatbot = gr.Chatbot(
|
| 331 |
+
height=560,
|
| 332 |
+
show_label=False,
|
| 333 |
+
buttons=["copy"],
|
| 334 |
+
autoscroll=True,
|
| 335 |
+
elem_id="chatbot",
|
| 336 |
+
)
|
| 337 |
+
raw_json = gr.JSON(
|
| 338 |
+
value=[], show_label=False, visible=False, height=560, elem_id="raw-json"
|
| 339 |
+
)
|
| 340 |
|
| 341 |
with gr.Accordion("Advanced Settings", open=False):
|
| 342 |
with gr.Row():
|
| 343 |
+
max_new_tokens = gr.Slider(64, 2048, value=512, step=64, label="Max New Tokens")
|
| 344 |
+
temperature = gr.Slider(0.1, 2.0, value=1.0, step=0.1, label="Temperature")
|
| 345 |
+
top_p = gr.Slider(0.1, 1.0, value=1.0, step=0.05, label="Top-p")
|
| 346 |
+
do_sample = gr.Checkbox(label="Do Sample", value=False)
|
| 347 |
+
with gr.Row():
|
| 348 |
+
video_fps = gr.Slider(0.5, 4.0, value=1.0, step=0.5, label="Video FPS (video only)")
|
| 349 |
+
max_frames = gr.Slider(8, 256, value=64, step=8, label="Max Frames (video only)")
|
| 350 |
|
| 351 |
gr.Examples(
|
| 352 |
examples=[
|
| 353 |
+
["example_bill.png", "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."],
|
| 354 |
+
["astronaut.jpg", "Describe this image in detail."],
|
| 355 |
+
["bird_kingfisher.jpg", "What species of bird is this? Describe its appearance and habitat."],
|
| 356 |
+
["example_video.mp4", "Describe what happens in this video."],
|
| 357 |
],
|
| 358 |
+
inputs=[media_file, prompt],
|
| 359 |
+
label="Examples",
|
|
|
|
|
|
|
|
|
|
| 360 |
)
|
| 361 |
|
| 362 |
+
media_file.change(
|
| 363 |
+
on_media_change, [media_file],
|
| 364 |
+
[media_file, preview_video, preview_image], api_name=False,
|
| 365 |
+
)
|
| 366 |
+
clear_btn.click(
|
| 367 |
+
clear_media, None,
|
| 368 |
+
[media_file, preview_video, preview_image], api_name=False,
|
| 369 |
+
)
|
| 370 |
+
view_toggle.change(toggle_view, [view_toggle], [chatbot, raw_json], api_name=False)
|
| 371 |
+
analyze_btn.click(
|
| 372 |
+
run_analyze,
|
| 373 |
+
inputs=[media_file, prompt, max_new_tokens, temperature, top_p, do_sample, video_fps, max_frames],
|
| 374 |
+
outputs=[chatbot, raw_json],
|
| 375 |
+
api_name=False,
|
| 376 |
+
show_progress="hidden",
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
gr.api(analyze_image, api_name="analyze_image")
|
| 380 |
+
gr.api(analyze_video, api_name="analyze_video")
|
| 381 |
+
|
| 382 |
|
| 383 |
+
if __name__ == "__main__":
|
| 384 |
+
# gradio 6.x: theme/css/mcp_server are launch() parameters
|
| 385 |
+
demo.queue(max_size=32)
|
| 386 |
+
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)
|