Ano-Nimus's picture
Upload folder using huggingface_hub
163a5a6 verified
Raw
History Blame Contribute Delete
10.2 kB
import os
from threading import Thread
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # noqa: E402 (must be imported before torch / transformers)
import av # noqa: E402
import gradio as gr # noqa: E402
import numpy as np # noqa: E402
import torch # noqa: E402
from transformers import ( # noqa: E402
AutoModelForImageTextToText,
AutoProcessor,
BitsAndBytesConfig,
TextIteratorStreamer,
)
MODEL_ID = "Qwen/Qwen3-VL-32B-Instruct"
HF_TOKEN = os.environ.get("HF_TOKEN", None)
TITLE = "Qwen3-VL-32B-Instruct — Vision & Video Chat"
SUBTITLE = "Ask questions about images and videos on ZeroGPU (4-bit, NF4)"
DESCRIPTION = (
"A multimodal chat demo for Qwen3-VL-32B-Instruct running on Hugging Face "
"ZeroGPU. Upload one or more images, or a short video, and ask questions about them."
)
SYSTEM_PROMPT = (
"You are Qwen3-VL-32B-Instruct, a highly capable multimodal assistant. "
"Be clear, accurate, and helpful."
)
PLACEHOLDER = (
"Upload images or a video and ask anything about them — or just type a question."
)
MAX_NEW_TOKENS = 8192
DEFAULT_MAX_NEW_TOKENS = 2048
MAX_HISTORY_MESSAGES = 10
MAX_IMAGES_PER_MESSAGE = 3
MAX_VIDEO_FRAMES = 32
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
VIDEO_EXTS = {".mp4", ".webm", ".mov", ".mkv", ".avi", ".m4v", ".ogv"}
# ---------------------------------------------------------------------------
# Model loading (ZeroGPU: module scope, bnb 4-bit, device_map="cuda")
# ---------------------------------------------------------------------------
BNB_CONFIG = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
processor = AutoProcessor.from_pretrained(MODEL_ID, token=HF_TOKEN)
# Cap per-image pixel budget so single images stay fast on shared ZeroGPU queues.
processor.image_processor.size = {"shortest_edge": 65536, "longest_edge": 1_600_000}
model = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
token=HF_TOKEN,
quantization_config=BNB_CONFIG,
device_map="cuda",
dtype=torch.bfloat16,
attn_implementation="sdpa",
low_cpu_mem_usage=True,
)
model.eval()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def file_kind(path: str):
ext = os.path.splitext(path)[1].lower()
if ext in VIDEO_EXTS:
return "video"
if ext in IMAGE_EXTS:
return "image"
return None
def sample_video_frames(path: str, max_frames: int = MAX_VIDEO_FRAMES) -> np.ndarray:
"""Decode a video into a uniformly-sampled stack of RGB frames (T, H, W, 3)."""
container = av.open(path)
stream = container.streams.video[0]
frames = [frame.to_ndarray(format="rgb24") for frame in container.decode(stream)]
container.close()
if not frames:
raise ValueError("Could not decode any frames from this video.")
if len(frames) > max_frames:
indices = np.linspace(0, len(frames) - 1, max_frames, dtype=int)
frames = [frames[i] for i in indices]
return np.stack(frames)
def append_files_to_content(paths, items, allow_video, note_video):
images = 0
for path in paths:
if not path or not os.path.exists(path):
continue
kind = file_kind(path)
if kind == "image":
if images < MAX_IMAGES_PER_MESSAGE:
items.append({"type": "image", "image": path})
images += 1
elif kind == "video":
if allow_video:
items.append({"type": "video", "video": path})
elif note_video:
items.append({"type": "text", "text": "[a video was shared in this message]"})
def build_messages(history, message, system_prompt):
"""Convert Gradio multimodal history + message into transformers messages."""
messages = []
if system_prompt.strip():
messages.append({"role": "system", "content": system_prompt.strip()})
history = history[-MAX_HISTORY_MESSAGES:]
for i, msg in enumerate(history):
role = msg.get("role", "user")
content = msg.get("content", "")
if isinstance(content, str):
if content.strip():
messages.append({"role": role, "content": content.strip()})
continue
items = []
is_last_user = role == "user" and i == len(history) - 1
for item in content:
if item.get("type") == "text":
text = item.get("text", "")
if text.strip():
items.append({"type": "text", "text": text.strip()})
paths = [item.get("file") for item in content if item.get("type") == "file"]
append_files_to_content(paths, items, allow_video=is_last_user, note_video=True)
if not items:
items = [{"type": "text", "text": "(empty message)"}]
messages.append({"role": role, "content": items})
items = []
paths = [f if isinstance(f, str) else f.get("path") for f in message.get("files") or []]
append_files_to_content(paths, items, allow_video=True, note_video=False)
text = (message.get("text") or "").strip()
if text:
items.append({"type": "text", "text": text})
elif not items:
items = [{"type": "text", "text": "What do you see here?"}]
messages.append({"role": "user", "content": items})
return messages
def format_output(text: str) -> str:
text = text.replace("<|im_end|>", "")
return text
def estimate_duration(
message,
history,
system_prompt,
temperature,
max_new_tokens,
top_p,
top_k,
):
del history, system_prompt, temperature, top_p, top_k
base = 90 + int(max_new_tokens / 40)
for f in message.get("files") or []:
path = f if isinstance(f, str) else f.get("path")
kind = file_kind(path)
if kind == "video":
base += 40
elif kind == "image":
base += 15
return min(360, max(90, base))
# ---------------------------------------------------------------------------
# ZeroGPU chat handler
# ---------------------------------------------------------------------------
@spaces.GPU(duration=estimate_duration, size="large")
def chat(
message: dict,
history: list,
system_prompt: str,
temperature: float,
max_new_tokens: int,
top_p: float,
top_k: int,
):
"""Chat with Qwen3-VL-32B-Instruct over images and videos. Streams the answer."""
messages = build_messages(history, message, system_prompt)
for msg in messages:
if isinstance(msg["content"], list):
for item in msg["content"]:
if item.get("type") == "video":
item["video"] = sample_video_frames(item["video"])
inputs = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
).to(model.device)
streamer = TextIteratorStreamer(
processor.tokenizer,
timeout=300.0,
skip_prompt=True,
skip_special_tokens=True,
)
generation_kwargs = dict(
**inputs,
streamer=streamer,
max_new_tokens=max_new_tokens,
do_sample=temperature > 0,
temperature=max(temperature, 1e-5),
top_p=top_p,
top_k=top_k,
use_cache=True,
)
worker = Thread(target=model.generate, kwargs=generation_kwargs)
worker.start()
output = ""
for chunk in streamer:
output += chunk
yield format_output(output)
# ---------------------------------------------------------------------------
# Gradio UI
# ---------------------------------------------------------------------------
CSS = """
.gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
.title h1 { text-align: center; margin-bottom: 0.2rem !important; }
.subtitle p, .meta p { text-align: center; }
.meta p { font-size: 0.95rem; color: #6b7280; margin-top: 0.35rem !important; }
.duplicate-button { margin: 0 auto 14px auto !important; }
"""
chatbot = gr.Chatbot(
height=620,
placeholder=PLACEHOLDER,
avatar_images=(None, "https://huggingface.co/front/assets/huggingface_logo-noborder.svg"),
)
with gr.Blocks() as demo:
gr.Markdown(f"# {TITLE}", elem_classes="title")
gr.Markdown(SUBTITLE, elem_classes="subtitle")
gr.Markdown(
f"{DESCRIPTION} Model: [{MODEL_ID}](https://huggingface.co/{MODEL_ID})",
elem_classes="meta",
)
gr.DuplicateButton("Duplicate Space", elem_classes="duplicate-button")
gr.ChatInterface(
fn=chat,
chatbot=chatbot,
fill_height=True,
multimodal=True,
additional_inputs_accordion=gr.Accordion("⚙️ Parameters", open=False, render=False),
additional_inputs=[
gr.Textbox(value=SYSTEM_PROMPT, label="System prompt", lines=3, render=False),
gr.Slider(minimum=0.0, maximum=1.2, step=0.05, value=0.7, label="Temperature", render=False),
gr.Slider(
minimum=256,
maximum=MAX_NEW_TOKENS,
step=256,
value=DEFAULT_MAX_NEW_TOKENS,
label="Max new tokens",
render=False,
),
gr.Slider(minimum=0.1, maximum=1.0, step=0.05, value=0.9, label="Top-p", render=False),
gr.Slider(minimum=1, maximum=100, step=1, value=20, label="Top-k", render=False),
],
examples=[
[{"text": "What animal is on the candy?", "files": ["examples/candy.jpg"]}],
[{"text": "Solve the geometry problem in this image.", "files": ["examples/math.jpg"]}],
[{"text": "How many porcelain jars were discovered in the niches located in the primary chamber of the tomb?", "files": ["examples/sample_video.mp4"]}],
["Explain the benefits of multimodal language models."],
],
cache_examples=False,
)
if __name__ == "__main__":
demo.launch(theme="soft", css=CSS)