Image-Text-to-Text
Transformers
Safetensors
qwen3_5
vllm
video
multimodal
reinforcement-learning
temporal-grounding
object-tracking
video-segmentation
visual-question-answering
spatial-reasoning
qwen3.5
conversational
Instructions to use OraRL/Video-ORA-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use OraRL/Video-ORA-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="OraRL/Video-ORA-4B") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("OraRL/Video-ORA-4B") model = AutoModelForMultimodalLM.from_pretrained("OraRL/Video-ORA-4B", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use OraRL/Video-ORA-4B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "OraRL/Video-ORA-4B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/OraRL/Video-ORA-4B
- SGLang
How to use OraRL/Video-ORA-4B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "OraRL/Video-ORA-4B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "OraRL/Video-ORA-4B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use OraRL/Video-ORA-4B with Docker Model Runner:
docker model run hf.co/OraRL/Video-ORA-4B
| #!/usr/bin/env python3 | |
| """ | |
| Segmentation prompt evaluation with vLLM. | |
| This is the inference stage only. It reads OneThinker-eval style records, | |
| asks the model for SAM2 prompts (`boxes`, `positive_points`, `negative_points`, | |
| and optional `time`), and writes `predicted_answer_norm` for the SAM2 | |
| post-processing stage. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterable, List, Optional | |
| os.environ.setdefault("VLLM_USE_V1", "1") | |
| os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") | |
| os.environ.setdefault("FORCE_QWENVL_VIDEO_READER", "decord") | |
| os.environ.setdefault("DECORD_EOF_RETRY_MAX", "20480") | |
| _PATCH_DIRS = ( | |
| Path(__file__).resolve().parents[1], | |
| Path(__file__).resolve().parents[2] / "Eval-onethink", | |
| ) | |
| if os.environ.get("FORCE_QWENVL_VIDEO_READER") == "decord": | |
| for _patch_dir in _PATCH_DIRS: | |
| if not (_patch_dir / "qwenvl_decord_patch.py").is_file(): | |
| continue | |
| sys.path.insert(0, str(_patch_dir)) | |
| try: | |
| import qwenvl_decord_patch # noqa: F401 | |
| except Exception as exc: | |
| print(f"[warn] failed to import qwenvl_decord_patch: {exc}", file=sys.stderr) | |
| break | |
| import torch # noqa: E402 | |
| from qwen_vl_utils import process_vision_info # noqa: E402 | |
| from tqdm import tqdm # noqa: E402 | |
| from transformers import AutoProcessor # noqa: E402 | |
| from vllm import LLM, SamplingParams # noqa: E402 | |
| # Prompts — single source of truth in eval/task/eval_prompt.py | |
| # (training tails match data/joint/sft_joint_all.jsonl seg_image / seg_video). | |
| _EVAL_TASK_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| if _EVAL_TASK_DIR not in sys.path: | |
| sys.path.insert(0, _EVAL_TASK_DIR) | |
| from canonical_data import ( # noqa: E402 | |
| load_json_records, | |
| repository_relative_output_path, | |
| ) | |
| from eval_prompt import ( # noqa: E402 | |
| TRAIN_SEG_IMAGE_TAIL, | |
| TRAIN_SEG_VIDEO_TAIL, | |
| ) | |
| ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE) | |
| def extract_answer(text: Optional[str]) -> str: | |
| if not isinstance(text, str): | |
| return "" | |
| match = ANSWER_RE.search(text) | |
| return match.group(1).strip() if match else text.strip() | |
| def load_json_or_jsonl(path: Path) -> List[Dict[str, Any]]: | |
| if path.suffix == ".jsonl": | |
| return load_json_records(path) | |
| with path.open("r", encoding="utf-8") as f: | |
| payload = json.load(f) | |
| if isinstance(payload, dict) and isinstance(payload.get("results"), list): | |
| return payload["results"] | |
| if isinstance(payload, list): | |
| return payload | |
| raise ValueError(f"Unsupported input JSON format: {path}") | |
| def resolve_dataset_path(bench_dir: str, dataset: str) -> Path: | |
| raw = Path(dataset) | |
| if raw.is_file(): | |
| return raw | |
| base = Path(bench_dir) | |
| candidates = [base / dataset, base / f"{dataset}.json", base / f"{dataset}.jsonl"] | |
| for path in candidates: | |
| if path.is_file(): | |
| return path | |
| raise FileNotFoundError(f"Cannot find dataset {dataset!r} under {bench_dir}") | |
| def clean_question(problem: str) -> str: | |
| return re.sub(r"^\s*<(image|video)>\s*", "", problem or "", flags=re.IGNORECASE).strip() | |
| def build_prompt(example: Dict[str, Any], prompt_mode: str) -> List[Dict[str, Any]]: | |
| # Match the joint SFT training prompt verbatim: the question is followed | |
| # immediately by the training tail. | |
| question = clean_question(str(example.get("problem") or example.get("question") or "")) | |
| data_type = str(example.get("data_type") or "").strip().lower() | |
| tail = TRAIN_SEG_VIDEO_TAIL if data_type == "video" else TRAIN_SEG_IMAGE_TAIL | |
| text = f"{question}\n{tail}" | |
| return [{"role": "user", "content": [{"type": "text", "text": text}]}] | |
| def resolve_media_path(base_prefix: str, raw_path: str) -> tuple[str, str]: | |
| """Return `(absolute_path_for_inference, relative_path_for_output)`. | |
| OneThinker JSON often stores paths like `./Evaluation/Refcoco/...`, while | |
| local mirrors may place media directly under `Refcoco/...`. | |
| """ | |
| if os.path.isabs(raw_path): | |
| return raw_path, repository_relative_output_path(raw_path, base_prefix) | |
| rel = raw_path.lstrip("./") | |
| candidates = [rel] | |
| if rel.startswith("Evaluation/"): | |
| candidates.append(rel[len("Evaluation/"):]) | |
| for cand in candidates: | |
| full = os.path.join(base_prefix, cand) | |
| if os.path.exists(full): | |
| return full, f"./{cand}" | |
| return os.path.join(base_prefix, rel), f"./{rel}" | |
| def canonical_output_path(base_prefix: str, raw_path: str) -> str: | |
| return resolve_media_path(base_prefix, raw_path)[1] | |
| def build_content(example: Dict[str, Any], args: argparse.Namespace) -> List[Dict[str, Any]]: | |
| data_type = str(example.get("data_type") or "").strip().lower() | |
| media_path, _ = resolve_media_path(args.base_prefix, str(example.get("path") or "")) | |
| if data_type == "image": | |
| return [{ | |
| "type": "image", | |
| "image": media_path, | |
| "max_pixels": args.max_pixels_image, | |
| "min_pixels": args.min_pixels_image, | |
| }] | |
| if data_type == "video": | |
| item = { | |
| "type": "video", | |
| "video": media_path, | |
| "max_pixels": args.max_pixels_video, | |
| "max_frames": args.max_frames, | |
| "fps": args.fps, | |
| } | |
| if os.getenv("EVAL_VIDEO_ITEM_ONETHINKER", "0") != "1": | |
| item["min_pixels"] = args.min_pixels_video | |
| item["total_pixels"] = args.total_pixels_video | |
| return [item] | |
| raise ValueError(f"Unsupported data_type for segmentation: {data_type!r}") | |
| def prepare_vllm_input( | |
| messages: List[Dict[str, Any]], | |
| processor: Any, | |
| patch_size: Optional[int], | |
| enable_thinking: bool = False, | |
| ) -> Dict[str, Any]: | |
| omit_thinking_kw = os.getenv("EVAL_OMIT_ENABLE_THINKING_KW", "0") == "1" | |
| force_do_resize_false = os.getenv("EVAL_FORCE_DO_RESIZE_FALSE", "0") == "1" | |
| if omit_thinking_kw: | |
| # OneThinker eval_bench.py omits this kwarg entirely. Keep this | |
| # switch for paper-parity runs with OneThinker checkpoints. | |
| text = processor.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| ) | |
| else: | |
| text = processor.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| enable_thinking=enable_thinking, | |
| ) | |
| kwargs: Dict[str, Any] = {"return_video_kwargs": True, "return_video_metadata": True} | |
| if patch_size is None: | |
| patch_size = getattr(getattr(processor, "image_processor", None), "patch_size", None) | |
| if patch_size is not None: | |
| kwargs["image_patch_size"] = patch_size | |
| image_inputs, video_inputs, video_kwargs = process_vision_info(messages, **kwargs) | |
| video_kwargs = video_kwargs or {} | |
| if force_do_resize_false: | |
| # Diagnostic only. For Qwen3.5 video segmentation this can trigger | |
| # processor shape mismatches when decoded frames are not patch-aligned. | |
| video_kwargs["do_resize"] = False | |
| mm_data: Dict[str, Any] = {} | |
| if image_inputs is not None: | |
| mm_data["image"] = image_inputs | |
| if video_inputs is not None: | |
| mm_data["video"] = video_inputs | |
| return {"prompt": text, "multi_modal_data": mm_data, "mm_processor_kwargs": video_kwargs} | |
| def parse_json_answer(answer: str) -> Optional[Dict[str, Any]]: | |
| if not answer: | |
| return None | |
| try: | |
| obj = json.loads(answer) | |
| return obj if isinstance(obj, dict) else None | |
| except Exception: | |
| pass | |
| start = answer.find("{") | |
| if start < 0: | |
| return None | |
| depth = 0 | |
| for idx in range(start, len(answer)): | |
| if answer[idx] == "{": | |
| depth += 1 | |
| elif answer[idx] == "}": | |
| depth -= 1 | |
| if depth == 0: | |
| try: | |
| obj = json.loads(answer[start:idx + 1]) | |
| return obj if isinstance(obj, dict) else None | |
| except Exception: | |
| return None | |
| return None | |
| def _is_number_list(value: Any, length: int) -> bool: | |
| if not isinstance(value, list) or len(value) != length: | |
| return False | |
| try: | |
| for item in value: | |
| float(item) | |
| return True | |
| except Exception: | |
| return False | |
| def _is_point_list(value: Any) -> bool: | |
| if not isinstance(value, list) or len(value) < 1: | |
| return False | |
| return all(_is_number_list(point, 2) for point in value) | |
| def normalize_seg_prompt(obj: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: | |
| """Normalize common model variants to the SAM2 prompt schema. | |
| The prompt asks for one bounding box but uses the plural key "boxes". Some | |
| models naturally return a list-of-boxes (`[[x1, y1, x2, y2]]`), while the | |
| downstream OneThinker SAM2 script expects the single box as a flat list. | |
| """ | |
| if not isinstance(obj, dict): | |
| return obj | |
| out = dict(obj) | |
| boxes = out.get("boxes") | |
| if ( | |
| isinstance(boxes, list) | |
| and len(boxes) == 1 | |
| and _is_number_list(boxes[0], 4) | |
| ): | |
| out["boxes"] = boxes[0] | |
| return out | |
| def valid_seg_prompt(obj: Optional[Dict[str, Any]], data_type: str) -> bool: | |
| if not isinstance(obj, dict): | |
| return False | |
| boxes = obj.get("boxes") | |
| if not _is_number_list(boxes, 4): | |
| return False | |
| for key in ("positive_points", "negative_points"): | |
| if not _is_point_list(obj.get(key)): | |
| return False | |
| if data_type == "video": | |
| try: | |
| float(obj.get("time")) | |
| except Exception: | |
| return False | |
| return True | |
| def iter_filtered(data: Iterable[Dict[str, Any]], args: argparse.Namespace) -> List[Dict[str, Any]]: | |
| rows = [] | |
| missing_media = 0 | |
| for rec in data: | |
| if str(rec.get("problem_type", "")).strip().lower() != "segmentation": | |
| continue | |
| data_type = str(rec.get("data_type") or "").strip().lower() | |
| if data_type not in {"image", "video"}: | |
| continue | |
| if args.data_type != "all" and data_type != args.data_type: | |
| continue | |
| media_path, _ = resolve_media_path(args.base_prefix, str(rec.get("path") or "")) | |
| if args.skip_missing_media and not os.path.exists(media_path): | |
| missing_media += 1 | |
| continue | |
| rows.append(rec) | |
| if missing_media: | |
| print(f"[warn] skipped {missing_media} samples with missing media", file=sys.stderr) | |
| if args.max_samples is not None: | |
| rows = rows[:args.max_samples] | |
| if args.chunk > 1: | |
| rows = [r for i, r in enumerate(rows) if i % args.chunk == args.index] | |
| return rows | |
| def evaluate_dataset(dataset: str, args: argparse.Namespace, llm: LLM, processor: Any, sampling_params: SamplingParams) -> Dict[str, Any]: | |
| data_path = resolve_dataset_path(args.bench_dir, dataset) | |
| rows = iter_filtered(load_json_or_jsonl(data_path), args) | |
| outputs: List[Dict[str, Any]] = [] | |
| for start in tqdm(range(0, len(rows), args.batch_size), desc=f"{dataset}"): | |
| batch = rows[start:start + args.batch_size] | |
| inputs = [] | |
| for example in batch: | |
| messages = build_prompt(example, args.prompt_mode) | |
| user_msg = messages[-1] | |
| user_msg = dict(user_msg) | |
| user_msg["content"] = build_content(example, args) + user_msg["content"] | |
| run_messages = messages[:-1] + [user_msg] | |
| inputs.append(prepare_vllm_input(run_messages, processor, args.patch_size, args.enable_thinking)) | |
| generated = llm.generate(inputs, sampling_params=sampling_params) | |
| texts = [out.outputs[0].text for out in generated] | |
| for example, text in zip(batch, texts): | |
| answer = extract_answer(text) | |
| parsed = parse_json_answer(answer) | |
| parsed = normalize_seg_prompt(parsed) | |
| data_type = str(example.get("data_type") or "").strip().lower() | |
| sample = dict(example) | |
| sample["path"] = canonical_output_path(args.base_prefix, str(example.get("path") or "")) | |
| sample["output"] = text | |
| sample["prediction"] = answer | |
| sample["predicted_answer_norm"] = ( | |
| json.dumps(parsed, ensure_ascii=False) if isinstance(parsed, dict) else answer | |
| ) | |
| sample["parsed_prediction"] = parsed | |
| sample["parse_ok"] = valid_seg_prompt(parsed, data_type) | |
| outputs.append(sample) | |
| total = len(outputs) | |
| parsed = sum(1 for row in outputs if row.get("parse_ok")) | |
| by_type: Dict[str, Dict[str, Any]] = {} | |
| for data_type in ("image", "video"): | |
| part = [r for r in outputs if r.get("data_type") == data_type] | |
| if part: | |
| ok = sum(1 for r in part if r.get("parse_ok")) | |
| by_type[data_type] = { | |
| "num_samples": len(part), | |
| "parse_rate": round(ok / len(part) * 100.0, 2), | |
| } | |
| return { | |
| "dataset": dataset, | |
| "input_file": str(data_path), | |
| "results": outputs, | |
| "metrics": { | |
| "num_samples": total, | |
| "parse_ok": parsed, | |
| "parse_rate": round(parsed / total * 100.0, 2) if total else 0.0, | |
| "by_data_type": by_type, | |
| }, | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--model_path", required=True) | |
| parser.add_argument("--processor_path", default=None) | |
| parser.add_argument("--bench_dir", required=True) | |
| parser.add_argument("--datasets", required=True, help="Comma-separated dataset names or JSON/JSONL paths.") | |
| parser.add_argument("--output_dir", required=True) | |
| parser.add_argument("--base_prefix", default="") | |
| parser.add_argument("--data_type", choices=["all", "image", "video"], default="all") | |
| parser.add_argument("--prompt_mode", choices=["think", "no_think", "bare", "onethink_system", "train_seg"], default="no_think") | |
| parser.add_argument("--enable_thinking", action="store_true", default=False) | |
| parser.add_argument("--max_samples", type=int, default=None) | |
| parser.add_argument("--skip_missing_media", action="store_true") | |
| parser.add_argument("--chunk", type=int, default=1) | |
| parser.add_argument("--index", type=int, default=0) | |
| parser.add_argument("--batch_size", type=int, default=16) | |
| parser.add_argument("--temperature", type=float, default=0.0) | |
| parser.add_argument("--top_k", type=int, default=-1) | |
| parser.add_argument("--max_new_tokens", type=int, default=1024) | |
| parser.add_argument("--tensor_parallel_size", type=int, default=1) | |
| parser.add_argument("--gpu_memory_utilization", type=float, default=0.85) | |
| parser.add_argument("--max_model_len", type=int, default=32768) | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--max_pixels_image", type=int, default=1024 * 32 * 32) | |
| parser.add_argument("--min_pixels_image", type=int, default=4 * 32 * 32) | |
| parser.add_argument("--max_pixels_video", type=int, default=256 * 32 * 32) | |
| parser.add_argument("--min_pixels_video", type=int, default=4 * 32 * 32) | |
| parser.add_argument("--total_pixels_video", type=int, default=256 * 64 * 32 * 32) | |
| parser.add_argument("--max_frames", type=int, default=128) | |
| parser.add_argument("--fps", type=int, default=2) | |
| parser.add_argument("--patch_size", type=int, default=None) | |
| args = parser.parse_args() | |
| if args.chunk < 1 or not (0 <= args.index < args.chunk): | |
| raise ValueError("--chunk must be >=1 and --index must be in [0, chunk)") | |
| os.makedirs(args.output_dir, exist_ok=True) | |
| torch.manual_seed(args.seed) | |
| processor = AutoProcessor.from_pretrained(args.processor_path or args.model_path) | |
| llm = LLM( | |
| model=args.model_path, | |
| tensor_parallel_size=args.tensor_parallel_size, | |
| max_model_len=args.max_model_len, | |
| gpu_memory_utilization=args.gpu_memory_utilization, | |
| mm_encoder_tp_mode="data", | |
| seed=args.seed, | |
| ) | |
| sampling_params = SamplingParams( | |
| temperature=args.temperature, | |
| max_tokens=args.max_new_tokens, | |
| top_k=args.top_k, | |
| stop_token_ids=[], | |
| ) | |
| summary: Dict[str, Any] = {} | |
| for dataset in [x.strip() for x in args.datasets.split(",") if x.strip()]: | |
| payload = evaluate_dataset(dataset, args, llm, processor, sampling_params) | |
| dataset_key = Path(dataset).stem if Path(dataset).suffix else dataset | |
| suffix = f"_shard{args.index}" if args.chunk > 1 else "" | |
| out_path = Path(args.output_dir) / f"results_{dataset_key}{suffix}.json" | |
| with out_path.open("w", encoding="utf-8") as f: | |
| json.dump({"results": payload["results"], "metrics": payload["metrics"]}, f, ensure_ascii=False, indent=2) | |
| summary[dataset_key] = payload["metrics"] | |
| print( | |
| f"{dataset_key}: n={payload['metrics']['num_samples']} " | |
| f"parse={payload['metrics']['parse_rate']:.2f}% -> {out_path}" | |
| ) | |
| summary_name = f"summary_shard{args.index}.json" if args.chunk > 1 else "summary.json" | |
| with (Path(args.output_dir) / summary_name).open("w", encoding="utf-8") as f: | |
| json.dump(summary, f, ensure_ascii=False, indent=2) | |
| if __name__ == "__main__": | |
| main() | |