multimodalart's picture
multimodalart HF Staff
Upload app.py with huggingface_hub
cff04e7 verified
Raw
History Blame Contribute Delete
5.34 kB
"""TimeLens2-2B video temporal grounding demo."""
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces # noqa: E402 MUST come before any CUDA-touching import
import torch
from pathlib import Path
import gradio as gr
from transformers import AutoModelForImageTextToText, AutoProcessor
from qwen_vl_utils import process_vision_info
MODEL_ID = "MCG-NJU/TimeLens2-2B"
processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModelForImageTextToText.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
attn_implementation="sdpa",
).to("cuda").eval()
@spaces.GPU(duration=30)
def ground(
video_path: str,
query: str,
max_new_tokens: int = 512,
) -> str:
"""Given a video and a natural-language query, locate the time spans where the query is relevant.
Args:
video_path: Path or URL of the input video file.
query: Natural-language description of the event to locate.
max_new_tokens: Maximum tokens the model may generate.
Returns:
JSON array of [start, end] time pairs in seconds, e.g. [[1.5, 3.2], [10.0, 12.5]].
"""
if not video_path or not query.strip():
return "Please provide both a video and a query."
prompt = (
f'Given the query: "{query}", return ALL time spans (in seconds) '
"where the query is relevant.\n"
"Output format MUST be a JSON array of [start, end] pairs.\n"
)
if not video_path.startswith(("http://", "https://", "file://")):
video_uri = Path(video_path).resolve().as_uri()
else:
video_uri = video_path
messages = [
{
"role": "user",
"content": [
{
"type": "video",
"video": video_uri,
"fps": 2.0,
"min_pixels": 32 * 32,
"max_pixels": 480 * 480,
"total_pixels": 128000 * 32 * 32,
},
{"type": "text", "text": prompt},
],
}
]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
images, videos, video_kwargs = process_vision_info(
messages,
image_patch_size=16,
return_video_kwargs=True,
return_video_metadata=True,
)
if videos is not None:
videos, video_metadatas = zip(*videos)
videos, video_metadatas = list(videos), list(video_metadatas)
else:
video_metadatas = None
inputs = processor(
text=text,
images=images,
videos=videos,
video_metadata=video_metadatas,
do_resize=False,
return_tensors="pt",
**video_kwargs,
).to(model.device)
with torch.inference_mode():
output_ids = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.01,
top_p=0.001,
top_k=1,
repetition_penalty=1.0,
)
trimmed = [out[len(inp):] for inp, out in zip(inputs.input_ids, output_ids)]
response = processor.batch_decode(
trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)
return response[0].strip()
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks() as demo:
gr.Markdown(
"# 🎬 TimeLens2-2B — Video Temporal Grounding\n"
"Upload a video and describe an event. The model returns the time spans "
"(in seconds) where that event occurs.\n\n"
"Model: [MCG-NJU/TimeLens2-2B](https://huggingface.co/MCG-NJU/TimeLens2-2B) — "
"a compact 2B Qwen3-VL fine-tune that achieves SOTA at the 2B scale on seven "
"temporal-grounding benchmarks (average mIoU 44.5), even surpassing TimeLens-8B."
)
with gr.Column(elem_id="col-container"):
with gr.Row():
video_in = gr.Video(label="Input video", sources=["upload"])
query_in = gr.Textbox(
label="Query",
placeholder="e.g. A person opens the refrigerator",
lines=2,
scale=2,
)
run_btn = gr.Button("Ground", variant="primary")
output = gr.Textbox(
label="Predicted time spans (JSON)",
lines=4,
interactive=False,
)
with gr.Accordion("Advanced settings", open=False):
max_tokens = gr.Slider(
label="Max new tokens",
minimum=64,
maximum=4096,
value=512,
step=64,
)
gr.Examples(
examples=[
["examples/barista_frothing.mp4", "milk is being frothed"],
["examples/cat_laser.mp4", "a cat chases a laser pointer"],
["examples/slicing_veggie.mp4", "vegetables are being sliced"],
],
inputs=[video_in, query_in],
outputs=output,
fn=ground,
cache_examples=True,
cache_mode="lazy",
)
run_btn.click(
fn=ground,
inputs=[video_in, query_in, max_tokens],
outputs=output,
api_name="ground",
)
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)