Spaces:
Running on Zero
Running on Zero
| """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() | |
| 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) |