"""Daily-Omni Dataset loader for benchmark. Daily-Omni is an audio-visual reasoning benchmark with 684 videos and 1,197 multiple-choice QA pairs across 6 major task types. Dataset source: https://huggingface.co/datasets/liarliar/Daily-Omni Supports loading QA metadata from: - Local JSON file (``qa_json_path``): recommended for offline/air-gapped environments - HuggingFace datasets (``dataset_path``): legacy online mode Video/audio files normally come from extracted ``Videos.tar``. When ``--daily-omni-video-dir`` is not set, the first request that needs on-disk media downloads that archive from the Hugging Face dataset repo (``huggingface_hub``) and caches it under ``HF_HOME``. Why ``BenchmarkDataset`` instead of ``HuggingFaceDataset``? vLLM's ``HuggingFaceDataset`` is a thin wrapper whose ``__init__`` always ends by calling ``load_data()`` → ``datasets.load_dataset(...)`` with a required Hub id and split. That contract fits "Hub-only" benches, but Daily-Omni also needs **offline QA metadata** from a local ``qa.json`` without touching the network. Subclassing ``HuggingFaceDataset`` would mean fighting the parent constructor (fake ``dataset_path``, reordering ``load_data``, or duplicating half the parent) and would still imply ``datasets`` is always relevant. This class therefore inherits only ``BenchmarkDataset`` (minimal: ``dataset_path``, ``random_seed``, ``self.data``) and implements **two explicit loaders**: ``_load_from_local_json`` (default path for air-gapped runs) and ``_load_from_huggingface`` (optional legacy path for users who prefer ``datasets`` + Hub cache). The latter is **not** inheritance; it is the same Hub rows as before, factored into a helper so one class can serve both deployment modes without mandatory ``datasets`` when using ``qa_json_path``. Usage: from vllm_omni.benchmarks.data_modules.daily_omni_dataset import DailyOmniDataset # Local JSON mode (recommended) dataset = DailyOmniDataset( qa_json_path="/path/to/qa.json", video_dir="/path/to/Videos", random_seed=42, ) # HuggingFace mode (legacy, requires network) dataset = DailyOmniDataset( dataset_path="liarliar/Daily-Omni", dataset_split="train", random_seed=42, ) requests = dataset.sample( tokenizer=tokenizer, num_requests=100, output_len=256, ) """ import base64 import json import logging import os import shutil import tarfile from dataclasses import dataclass from pathlib import Path from typing import Any, Literal try: from vllm.benchmarks.datasets import BenchmarkDataset, SampleRequest except ImportError: # Fallback: if BenchmarkDataset not available, use base class from same module from vllm.benchmarks.datasets import HuggingFaceDataset as BenchmarkDataset from vllm.benchmarks.datasets import SampleRequest from vllm.tokenizers import TokenizerLike from vllm.tokenizers.hf import get_cached_tokenizer try: from datasets import load_dataset except ImportError: load_dataset = None logger = logging.getLogger(__name__) def _daily_omni_hf_cache_root() -> Path: return Path(os.environ.get("HF_HOME", Path.home() / ".cache" / "huggingface")).expanduser().resolve() def _daily_omni_tar_fingerprint(tar_path: Path) -> str: st = tar_path.stat() return f"v1:{st.st_size}:{int(st.st_mtime_ns)}" def _daily_omni_find_videos_root_in_extract(tmp: Path) -> Path: """Return directory whose children are ``video_id`` folders with ``*_video.mp4``.""" videos = tmp / "Videos" if videos.is_dir(): return videos for child in sorted(tmp.iterdir()): if child.is_dir() and not child.name.startswith("."): probe = child / f"{child.name}_video.mp4" if probe.is_file(): return tmp raise RuntimeError( f"Unrecognized layout after extracting Daily-Omni Videos.tar under {tmp} " "(expected top-level 'Videos/' or per-video_id subdirs)." ) def ensure_daily_omni_hub_videos_dir(repo_id: str) -> Path: """Download ``Videos.tar`` from the Hugging Face dataset repo and return the ``Videos`` root. The returned path matches ``--daily-omni-video-dir`` (directory containing ``{{video_id}}/``). Cached under ``HF_HOME`` / ``vllm_omni/daily_omni_media/``. Reuses extraction when the tarball fingerprint matches. Raises: ImportError: if ``huggingface_hub`` is not installed. FileNotFoundError / RuntimeError: if the archive is missing or malformed. """ rid = (repo_id or "").strip() if not rid: raise ValueError("repo_id is required to download Daily-Omni Videos.tar") try: from huggingface_hub import hf_hub_download except ImportError as e: raise ImportError( "Daily-Omni Hub media download requires huggingface_hub. " "Install it (e.g. with vLLM) or provide --daily-omni-video-dir with a local extract." ) from e safe = rid.replace("/", "__").replace("\\", "_") staging_root = _daily_omni_hf_cache_root() / "vllm_omni" / "daily_omni_media" / safe videos_dir = staging_root / "Videos" marker = staging_root / ".videos_extracted" tar_path: Path | None = None for fname in ("Videos.tar", "videos.tar"): try: tar_path = Path(hf_hub_download(repo_id=rid, filename=fname, repo_type="dataset")) break except Exception: continue if tar_path is None or not tar_path.is_file(): raise FileNotFoundError( f"Could not download Videos.tar from Hugging Face dataset {rid!r} (tried Videos.tar / videos.tar)." ) fp = _daily_omni_tar_fingerprint(tar_path) if marker.is_file() and videos_dir.is_dir(): try: if marker.read_text(encoding="utf-8").strip() == fp: next(videos_dir.iterdir()) logger.info("Reusing cached Daily-Omni Videos extract at %s", videos_dir) return videos_dir except (OSError, StopIteration): shutil.rmtree(videos_dir, ignore_errors=True) marker.unlink(missing_ok=True) staging_root.mkdir(parents=True, exist_ok=True) work = staging_root / "_extract_work" shutil.rmtree(work, ignore_errors=True) work.mkdir(parents=True) try: logger.info("Extracting Daily-Omni Videos.tar from %s (repo=%s)", tar_path, rid) with tarfile.open(tar_path, "r:*") as tf: tf.extractall(path=work, filter="data") found = _daily_omni_find_videos_root_in_extract(work) if videos_dir.exists(): shutil.rmtree(videos_dir, ignore_errors=True) shutil.move(str(found), str(videos_dir)) finally: shutil.rmtree(work, ignore_errors=True) marker.write_text(fp, encoding="utf-8") logger.info("Daily-Omni Hub media ready at %s", videos_dir) return videos_dir class _ListDatasetIterator: """Simple iterator wrapper around a list to mimic HuggingFace streaming dataset behavior.""" def __init__(self, data: list[dict[str, Any]]) -> None: self._data = data self._index = 0 def __iter__(self): self._index = 0 return self def __next__(self) -> dict[str, Any]: if self._index >= len(self._data): raise StopIteration item = self._data[self._index] self._index += 1 return item def __len__(self) -> int: return len(self._data) def __getitem__(self, idx: int | slice) -> dict[str, Any] | list[dict[str, Any]]: return self._data[idx] # Aligns with Lliar-liar/Daily-Omni CLI ``--input_mode`` (test_model/*/testmodel.py). DailyOmniInputMode = Literal["all", "visual", "audio"] # ``build_conversation()`` in Daily-Omni ``test_model/Qwen2.5-Omni/testmodel.py`` (verbatim). DAILY_OMNI_SYSTEM_TEXT = ( "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, " "capable of perceiving auditory and visual inputs, as well as generating text and speech." ) @dataclass class DailyOmniSampleRequest(SampleRequest): """``SampleRequest`` with Daily-Omni gold labels for post-run accuracy scoring.""" daily_omni_gold_answer: str = "" daily_omni_video_id: str = "" daily_omni_task_type: str = "" #: Official qa.json ``video_duration`` (e.g. ``30s``, ``60s``) for leaderboard-style breakdown. daily_omni_video_duration: str = "" #: Official ``video_category`` (YouTube-style category string) for per-category accuracy. daily_omni_video_category: str = "" #: Extra JSON fields merged into chat-completions ``extra_body`` (e.g. ``mm_processor_kwargs``). omni_extra_body: dict[str, Any] | None = None #: Full OpenAI ``messages`` (system + user) mirroring upstream Daily-Omni conversation. omni_chat_messages: list[dict[str, Any]] | None = None #: Used only when ``omni_chat_messages`` is None (non-Daily-Omni-style requests). omni_chat_mm_position: Literal["first", "last"] = "last" class DailyOmniDataset(BenchmarkDataset): """Daily-Omni audio-visual QA dataset for benchmarking. Inherits ``BenchmarkDataset`` only (not ``HuggingFaceDataset``): see module docstring for why Hub loading lives in ``_load_from_huggingface`` instead of subclassing the HF base class. The dataset includes: - 684 videos from daily life scenarios (available in Videos.tar) - 1,197 multiple-choice QA pairs in qa.json - 6 major task categories QA metadata can be loaded from: - Local JSON file (``qa_json_path``): recommended for offline/air-gapped environments - HuggingFace datasets (``dataset_path``): legacy online mode Video/audio files normally come from extracted ``Videos.tar``. When ``video_dir`` is not set, the first sample that needs on-disk media downloads that archive from the Hugging Face dataset repo (env ``VLLM_DAILY_OMNI_MEDIA_REPO`` overrides the repo id; else ``dataset_path`` or :data:`DEFAULT_HF_DATASET_ID`). Args: qa_json_path: Path to local qa.json file (offline mode, preferred). When provided, ``dataset_path`` and ``dataset_split`` are ignored. dataset_path: HuggingFace dataset path (e.g., "liarliar/Daily-Omni"). Used only if ``qa_json_path`` is not provided (legacy online mode). dataset_split: Dataset split to use (default: "train"). Used only in online mode. random_seed: Random seed for shuffling video_dir: Directory containing extracted video files (default: None; may be filled lazily from Hub — see above). input_mode: Which modalities to send, matching upstream Daily-Omni ``--input_mode``: ``all`` — video + WAV (default; official audio-visual protocol); ``visual`` — video only; ``audio`` — extracted WAV only (requires ``{video_id}/{video_id}_audio.wav`` under ``video_dir``). max_duration_seconds: Reserved for future ffprobe-based filtering; currently **not applied** when building requests (metadata ``video_duration`` is still passed through for eval). dataset_subset: Optional HuggingFace subset name (``load_dataset(..., name=...)``); used by bench ``--hf-subset`` / patch. no_stream: If True, load the Hub split non-streaming (matches bench ``--no-stream``). inline_local_video: If True, embed local MP4 as ``data:video/mp4;base64,...`` in requests so the API server does not need ``--allowed-local-media-path`` (large JSON; use for small runs). When ``input_mode`` is ``audio`` or ``all``, local WAV is embedded the same way (``data:audio/wav;base64,...``). trust_remote_code: Whether to trust remote code when loading HuggingFace dataset (online mode only). """ SUPPORTED_DATASET_PATHS: set[str] = { "liarliar/Daily-Omni", } #: Default Hub id for synthetic video URLs when ``qa_json_path`` is used (``dataset_path`` None). DEFAULT_HF_DATASET_ID = "liarliar/Daily-Omni" IS_MULTIMODAL = True DEFAULT_OUTPUT_LEN = 256 def __init__( self, qa_json_path: str | None = None, dataset_path: str | None = None, dataset_split: str = "train", random_seed: int = 0, video_dir: str | None = None, input_mode: DailyOmniInputMode = "all", inline_local_video: bool = False, trust_remote_code: bool = False, max_duration_seconds: float | None = None, dataset_subset: str | None = None, no_stream: bool = False, **kwargs, ) -> None: if input_mode not in ("all", "visual", "audio"): raise ValueError(f"input_mode must be 'all', 'visual', or 'audio', got {input_mode!r}") # Validate arguments: need either local JSON or HF path if qa_json_path is None and dataset_path is None: raise ValueError( "Either 'qa_json_path' (local JSON) or 'dataset_path' (HuggingFace) must be provided. " "For offline/air-gapped environments, download qa.json and use qa_json_path." ) # Store configuration self.qa_json_path = Path(qa_json_path) if qa_json_path else None self.dataset_path = dataset_path self.dataset_split = dataset_split self.dataset_subset = dataset_subset #: Match vLLM ``HuggingFaceDataset`` / bench CLI ``--no-stream``. self._hf_streaming = not no_stream self.video_dir = Path(video_dir) if video_dir else None self.inline_local_video = inline_local_video self.input_mode: DailyOmniInputMode = input_mode self.max_duration_seconds = max_duration_seconds self.trust_remote_code = trust_remote_code #: In-process cache of ffprobe durations only (no disk persistence). self._video_durations: dict[str, float] = {} # Initialize parent BenchmarkDataset super().__init__( dataset_path=dataset_path if qa_json_path is None else None, random_seed=random_seed, **kwargs, ) # Load data based on mode self.load_data() # Verify dataset info logger.info( "Loaded Daily-Omni dataset: mode=%s, source=%s, random_seed=%d, input_mode=%s, max_duration=%s", "local_json" if self.qa_json_path else "huggingface", str(self.qa_json_path) if self.qa_json_path else f"{dataset_path}/{dataset_split}", random_seed, input_mode, f"{max_duration_seconds}s" if max_duration_seconds else "unlimited", ) def load_data(self) -> None: """Populate ``self.data`` from either local JSON or the Hub. See module docstring: we do not subclass ``HuggingFaceDataset`` because Daily-Omni needs a first-class offline path; Hub loading is an optional branch implemented below. """ if self.qa_json_path is not None: self._load_from_local_json() else: self._load_from_huggingface() def _load_from_local_json(self) -> None: """Load QA data from local JSON file.""" if not self.qa_json_path.exists(): raise FileNotFoundError(f"QA JSON file not found: {self.qa_json_path}") with open(self.qa_json_path, encoding="utf-8") as f: data = json.load(f) # Support both list format and dict with "train"/"test" splits if isinstance(data, dict): # Try to get the requested split, fallback to first available split_data = data.get(self.dataset_split) if split_data is None: available = list(data.keys()) if available: logger.warning( "Split '%s' not found in %s, using '%s' instead", self.dataset_split, self.qa_json_path, available[0], ) split_data = data[available[0]] else: split_data = [] data = split_data if not isinstance(data, list): raise ValueError(f"Expected list of QA items in JSON, got {type(data).__name__}") # Shuffle if requested if not getattr(self, "disable_shuffle", False) and self.random_seed is not None: import random rng = random.Random(self.random_seed) shuffled = data[:] rng.shuffle(shuffled) data = shuffled # Create an iterator-like wrapper for compatibility self.data = _ListDatasetIterator(data) def _load_from_huggingface(self) -> None: """Load QA rows via ``datasets.load_dataset`` (legacy / convenience path). Kept for backward compatibility: callers can still pass ``dataset_path=liarliar/Daily-Omni`` and get the same parquet-backed rows as the Hub dataset card, with streaming (or non-streaming if ``no_stream=True``) and shuffle. This is intentionally **not** implemented by subclassing ``HuggingFaceDataset``: that base always runs Hub ``load_dataset`` from its constructor and expects a Hub id as the primary API; Daily-Omni instead chooses the source in ``load_data()`` (JSON vs Hub) while sharing one ``sample()`` / request-building implementation for both. """ if load_dataset is None: raise ImportError( "datasets library is required for HuggingFace mode. " "Install with: pip install datasets, or use local JSON mode instead." ) load_kw: dict[str, Any] = { "split": self.dataset_split, "streaming": self._hf_streaming, "trust_remote_code": self.trust_remote_code, } if self.dataset_subset is not None: load_kw["name"] = self.dataset_subset ds = load_dataset(self.dataset_path, **load_kw) if not getattr(self, "disable_shuffle", False): ds = ds.shuffle(seed=self.random_seed) self.data = ds def get_task_statistics(self) -> dict[str, int]: """Get distribution of task types in the dataset. Returns: Dict mapping task type to count """ stats: dict[str, int] = {} for item in self.data: row = self._coerce_row(item) fields = self._normalize_qa_fields(row) task_type = fields["task_type"] or "unknown" stats[task_type] = stats.get(task_type, 0) + 1 return stats @staticmethod def _coerce_row(item: Any) -> dict[str, Any]: """Turn a dataset row into a plain dict (Arrow / Mapping).""" if isinstance(item, dict): return item if hasattr(item, "as_py"): return dict(item.as_py()) # pyarrow Row try: return dict(item) except (TypeError, ValueError): return {k: item[k] for k in item} # type: ignore[misc] @staticmethod def _normalize_qa_fields(row: dict[str, Any]) -> dict[str, Any]: """Map official Daily-Omni qa.json / Hub schema to internal fields. Official fields (see liarliar/Daily-Omni ``qa.json``): ``Question``, ``Choice`` (list), ``Answer``, ``video_id``, ``Type``, ``video_duration`` (``30s`` / ``60s``), ``video_category``, plus other category columns. Legacy aliases (lowercase / older loaders) are still accepted. """ out: dict[str, Any] = {} out["question"] = str(row.get("Question") or row.get("question") or "").strip() vid = row.get("video_id") if row.get("video_id") is not None else row.get("video") out["video_id"] = str(vid).strip() if vid is not None else "" out["task_type"] = str(row.get("Type") or row.get("task_type") or row.get("type") or "").strip() vc = row.get("video_category") if row.get("video_category") is not None else row.get("videoCategory") out["video_category"] = str(vc).strip() if vc is not None else "" vd = row.get("video_duration") if row.get("video_duration") is not None else row.get("videoDuration") out["video_duration"] = str(vd).strip() if vd is not None else "" out["answer"] = str(row.get("Answer") or row.get("answer") or "").strip() vu = row.get("video_url") if row.get("video_url") is not None else row.get("Video_URL") out["video_url"] = str(vu).strip() if vu is not None and str(vu).strip() else None choice = row.get("Choice") if choice is None: choice = row.get("options") or row.get("choice") out["choice"] = choice return out def sample( self, tokenizer: TokenizerLike, num_requests: int, output_len: int | None = None, request_id_prefix: str = "", no_oversample: bool = False, **kwargs, ) -> list[SampleRequest]: """Sample requests from Daily-Omni dataset. Args: tokenizer: Tokenizer for computing prompt length num_requests: Number of requests to sample output_len: Target output length in tokens (default: 256) request_id_prefix: Prefix for request IDs no_oversample: If True, do not oversample if fewer examples available **kwargs: Additional arguments (ignored) Returns: List of SampleRequest objects with video URLs and prompts """ if output_len is None: output_len = self.DEFAULT_OUTPUT_LEN sampled_requests: list[SampleRequest] = [] ind = 0 cached_tokenizer = get_cached_tokenizer(tokenizer) # Iterate over shuffled dataset for item in self.data: if len(sampled_requests) >= num_requests: break request = self._create_sample_request( self._coerce_row(item), cached_tokenizer, output_len, request_id_prefix, ind ) if request: sampled_requests.append(request) ind += 1 logger.info("Created %d sample requests from Daily-Omni dataset", len(sampled_requests)) # Handle oversampling if needed self.maybe_oversample_requests(sampled_requests, num_requests, request_id_prefix, no_oversample) return sampled_requests def _create_sample_request( self, qa_item: dict[str, Any], tokenizer: TokenizerLike, output_len: int, request_id_prefix: str, index: int, ) -> SampleRequest | None: """Create a SampleRequest from a QA item. Args: qa_item: QA pair from the dataset tokenizer: Tokenizer output_len: Target output length request_id_prefix: Prefix for request ID index: Request index Returns: SampleRequest or None if invalid """ fields = self._normalize_qa_fields(qa_item) video_id = fields["video_id"] question = fields["question"] choice = fields["choice"] task_type = fields["task_type"] video_url = fields["video_url"] video_duration = fields.get("video_duration") or "" video_category = fields.get("video_category") or "" if not video_id and not video_url: logger.warning("Skipping item: no video_id / video_url") return None if not question: logger.warning("Skipping item: no question found") return None # Official layout after extracting Videos.tar (see Lliar-liar/Daily-Omni test_model): # {video_base_dir}/{video_id}/{video_id}_video.mp4 mm_payload, omni_extra, mm_pos = self._compose_daily_omni_multimodal(video_id, video_url) if not mm_payload: return None messages = self._build_daily_omni_openai_messages(mm_payload, question, choice) user_text = self._official_daily_omni_user_prompt(question, choice) # Text-only length estimate (same as before: no MM token count in bench). prompt_len = len(tokenizer.encode(f"{DAILY_OMNI_SYSTEM_TEXT}\n{user_text}")) return DailyOmniSampleRequest( prompt=user_text, prompt_len=prompt_len, expected_output_len=output_len, multi_modal_data=None, request_id=f"{request_id_prefix}{index}", daily_omni_gold_answer=fields["answer"], daily_omni_video_id=video_id, daily_omni_task_type=task_type, daily_omni_video_duration=video_duration, daily_omni_video_category=video_category, omni_extra_body=omni_extra, omni_chat_messages=messages, omni_chat_mm_position=mm_pos, ) @staticmethod def _official_video_relpath(video_id: str) -> str: """Relative path inside extracted ``Videos/`` per upstream Daily-Omni scripts.""" return f"{video_id}/{video_id}_video.mp4" @staticmethod def _official_audio_relpath(video_id: str) -> str: """Relative path for extracted WAV per upstream ``get_audio_path``.""" return f"{video_id}/{video_id}_audio.wav" def _resolve_local_video_path(self, video_id: str) -> Path | None: """Pick an existing file under ``video_dir`` (official layout + flat fallback).""" if not self.video_dir or not video_id: return None candidates = [ self.video_dir / self._official_video_relpath(video_id), self.video_dir / f"{video_id}.mp4", # flat layout (custom mirrors / outdated docs) ] seen: set[Path] = set() for p in candidates: rp = p.resolve() if rp in seen: continue seen.add(rp) if p.exists(): return p return None def _resolve_local_audio_path(self, video_id: str) -> Path | None: """Pick an existing WAV under ``video_dir`` (official layout + flat fallback).""" if not self.video_dir or not video_id: return None candidates = [ self.video_dir / self._official_audio_relpath(video_id), self.video_dir / f"{video_id}.wav", ] seen: set[Path] = set() for p in candidates: rp = p.resolve() if rp in seen: continue seen.add(rp) if p.exists(): return p return None def _local_file_to_video_url_payload(self, video_path: Path) -> dict[str, Any]: """Build OpenAI-style video_url part for a resolved local file. vLLM rejects ``file://`` unless the server was started with ``--allowed-local-media-path`` set to a directory that **contains** the file (typically the extracted ``Videos`` root). Use ``inline_local_video=True`` to send base64 data URLs instead (no server path allowlist; larger requests). """ path = video_path.expanduser().resolve() if self.inline_local_video: raw = path.read_bytes() b64 = base64.b64encode(raw).decode("ascii") return { "type": "video_url", "video_url": {"url": f"data:video/mp4;base64,{b64}"}, } return { "type": "video_url", "video_url": {"url": path.as_uri()}, } def _local_file_to_audio_url_payload(self, audio_path: Path) -> dict[str, Any]: """Build OpenAI-style ``audio_url`` part for a resolved local WAV file.""" path = audio_path.expanduser().resolve() if self.inline_local_video: raw = path.read_bytes() b64 = base64.b64encode(raw).decode("ascii") return { "type": "audio_url", "audio_url": {"url": f"data:audio/wav;base64,{b64}"}, } return { "type": "audio_url", "audio_url": {"url": path.as_uri()}, } def _lazy_ensure_hub_media_dir(self) -> None: """If ``video_dir`` was not configured, download and extract ``Videos.tar`` once from HF.""" if self.video_dir is not None: return repo = os.environ.get("VLLM_DAILY_OMNI_MEDIA_REPO", "").strip() if not repo: repo = (self.dataset_path or "").strip() if not repo: repo = self.DEFAULT_HF_DATASET_ID self.video_dir = ensure_daily_omni_hub_videos_dir(repo) def _get_video_content( self, video_id: str, video_url: str | None, ) -> dict[str, Any] | None: """Resolve video for OpenAI-style ``video_url`` content. Upstream uses ``get_video_path(video_id, base) -> base/video_id/video_id_video.mp4``. The Hub repo only publishes ``Videos.tar``; use ``--daily-omni-video-dir`` pointing at the extracted ``Videos`` folder (parent of per-``video_id`` subdirs). For ``file://`` URLs, start ``vllm serve`` with e.g. ``--allowed-local-media-path /same/path/as/daily-omni-video-dir``. """ if video_url: url = video_url if not url.startswith(("http://", "https://", "file://")): url = f"https://{url.lstrip('/')}" return {"type": "video_url", "video_url": {"url": url}} self._lazy_ensure_hub_media_dir() if self.video_dir and video_id: video_path = self._resolve_local_video_path(video_id) if video_path is not None: return self._local_file_to_video_url_payload(video_path) logger.warning( "Video not found under video_dir=%s for video_id=%r (expected %s or %s)", self.video_dir, video_id, self._official_video_relpath(video_id), f"{video_id}.mp4", ) if video_id: repo = self.dataset_path or self.DEFAULT_HF_DATASET_ID rel = self._official_video_relpath(video_id) hf_video_url = f"https://huggingface.co/datasets/{repo}/resolve/main/Videos/{rel}" logger.debug( "Using HF video URL (likely 404 — Hub ships Videos.tar only): %s", hf_video_url, ) return {"type": "video_url", "video_url": {"url": hf_video_url}} logger.error("Could not determine video source for video_id=%r", video_id) return None def _get_audio_content(self, video_id: str) -> dict[str, Any] | None: """Resolve extracted WAV for OpenAI-style ``audio_url`` (local files under ``video_dir``). Uses the same tree as video (``{video_id}/{video_id}_audio.wav``), including after lazy Hub ``Videos.tar`` extraction when ``video_dir`` was unset. """ self._lazy_ensure_hub_media_dir() if not self.video_dir or not video_id: logger.warning( "Daily-Omni input_mode %r requires --daily-omni-video-dir with %s", self.input_mode, self._official_audio_relpath(video_id), ) return None audio_path = self._resolve_local_audio_path(video_id) if audio_path is not None: return self._local_file_to_audio_url_payload(audio_path) logger.warning( "Audio not found under video_dir=%s for video_id=%r (expected %s or %s)", self.video_dir, video_id, self._official_audio_relpath(video_id), f"{video_id}.wav", ) return None def _compose_daily_omni_multimodal( self, video_id: str, video_url: str | None, ) -> tuple[dict[str, Any] | list[dict[str, Any]] | None, dict[str, Any] | None, Literal["first", "last"]]: """Build ``multi_modal_data`` and request extras for the active ``input_mode``. Mirrors upstream Daily-Omni: separate video + WAV with ``use_audio_in_video=False``. """ extra: dict[str, Any] = {"mm_processor_kwargs": {"use_audio_in_video": False}} mode = self.input_mode if mode == "visual": v = self._get_video_content(video_id, video_url) return v, extra, "last" if mode == "audio": a = self._get_audio_content(video_id) return a, extra, "first" v = self._get_video_content(video_id, video_url) a = self._get_audio_content(video_id) if not v or not a: return None, None, "first" return [v, a], extra, "first" @staticmethod def _media_desc_for_official_prompt(mode: DailyOmniInputMode) -> str: """``media_desc`` in upstream ``build_conversation``.""" if mode == "audio": return "given audio" if mode == "all": return "given video and audio together" return "given video" @staticmethod def _choices_repr_for_official_prompt(choice: Any) -> str: """Format ``Choice`` from qa.json for the model (one option per line when possible). Using ``str(list)`` embeds Python list brackets and quotes, which is poor for MCQ reading; lists/tuples are joined with newlines instead. Other shapes fall back to ``str(choice)`` for parity with exotic upstream payloads. """ if choice is None: return "" if isinstance(choice, (list, tuple)): lines = [str(x).strip() for x in choice if str(x).strip()] return "\n".join(lines) if isinstance(choice, dict): return "\n".join(f"{k}. {v}" for k, v in choice.items()) return str(choice) def _official_daily_omni_user_prompt(self, question: str, choice: Any) -> str: """User text block from Daily-Omni ``build_conversation`` (after media parts).""" task_prompt = self._media_desc_for_official_prompt(self.input_mode) choices = self._choices_repr_for_official_prompt(choice) # Single f-string with explicit newlines avoids accidental implicit concatenation # gluing sentences (e.g. ``...media_desc.Select...``) when editing. return ( "Your task is to accurately answer multiple-choice questions " f"based on the {task_prompt}.\n" "Select the single most accurate answer from the given choices.\n" f"Question: {question}\n" f"Choices: {choices}\n" "Your answer should be a capital letter representing your choice: " "A, B, C, or D. Don't generate any other text.\n" ) def _build_daily_omni_openai_messages( self, mm_payload: dict[str, Any] | list[dict[str, Any]], question: str, choice: Any, ) -> list[dict[str, Any]]: """Map upstream conversation to OpenAI Chat Completions ``messages`` (video_url / audio_url parts).""" user_text = self._official_daily_omni_user_prompt(question, choice) mm_list: list[dict[str, Any]] = mm_payload if isinstance(mm_payload, list) else [mm_payload] user_content: list[dict[str, Any]] = [*mm_list, {"type": "text", "text": user_text}] return [ {"role": "system", "content": [{"type": "text", "text": DAILY_OMNI_SYSTEM_TEXT}]}, {"role": "user", "content": user_content}, ] def sample_by_task_type( self, tokenizer: TokenizerLike, task_type: str, num_samples: int, output_len: int | None = None, request_id_prefix: str = "", **kwargs, ) -> list[SampleRequest]: """Sample requests filtered by task type. Args: tokenizer: Tokenizer task_type: Task type to filter by num_samples: Number of samples output_len: Target output length request_id_prefix: Prefix for request IDs **kwargs: Additional sampling arguments Returns: List of SampleRequest objects matching the task type """ if output_len is None: output_len = self.DEFAULT_OUTPUT_LEN filtered = [ item for item in self.data if self._normalize_qa_fields(self._coerce_row(item))["task_type"] == task_type ] available = len(filtered) if available < num_samples: logger.warning( "Only %d samples available for task type '%s', requested %d", available, task_type, num_samples, ) num_samples = available sampled_requests: list[SampleRequest] = [] cached_tokenizer = get_cached_tokenizer(tokenizer) for i, item in enumerate(filtered[:num_samples]): request = self._create_sample_request(item, cached_tokenizer, output_len, request_id_prefix, i) if request: sampled_requests.append(request) return sampled_requests def __repr__(self) -> str: return ( f"DailyOmniDataset(" f"dataset_path={self.dataset_path!r}, " f"dataset_split={self.dataset_split!r}, " f"video_dir={self.video_dir!r}, " f"input_mode={self.input_mode!r}, " f"inline_local_video={self.inline_local_video!r}, " f"max_duration_seconds={self.max_duration_seconds}, " f"random_seed={self.random_seed}" f")" ) def load_daily_omni_dataset( qa_json_path: str | None = None, dataset_path: str | None = None, dataset_split: str = "train", random_seed: int = 0, video_dir: str | None = None, input_mode: DailyOmniInputMode = "all", max_duration_seconds: float | None = None, dataset_subset: str | None = None, no_stream: bool = False, **kwargs, ) -> DailyOmniDataset: """Convenience function to load Daily-Omni dataset. Args: qa_json_path: Path to local qa.json file (recommended for offline/air-gapped environments). When provided, ``dataset_path`` is ignored. dataset_path: HuggingFace dataset path (default: liarliar/Daily-Omni). Used only if ``qa_json_path`` is not provided (legacy online mode). dataset_split: Dataset split to use (default: "train") random_seed: Random seed for shuffling video_dir: Directory containing extracted ``Videos/`` tree (MP4 and, for ``all``/``audio``, WAV) input_mode: ``visual`` | ``audio`` | ``all`` (same semantics as upstream Daily-Omni) max_duration_seconds: Maximum video duration in seconds (e.g., 30 for 30s subset, 60 for 60s subset); uses ffprobe on local files under ``video_dir`` (in-memory cache only for this process). **kwargs: Additional arguments passed to DailyOmniDataset Returns: DailyOmniDataset instance Example: >>> from vllm_omni.benchmarks.data_modules.daily_omni_dataset import load_daily_omni_dataset # Local JSON mode (recommended for offline) >>> dataset = load_daily_omni_dataset( ... qa_json_path="/path/to/qa.json", ... video_dir="/path/to/Daily-Omni/Videos", ... random_seed=42, ... max_duration_seconds=30, ... ) # HuggingFace mode (legacy online) >>> dataset = load_daily_omni_dataset( ... dataset_path="liarliar/Daily-Omni", ... video_dir="/path/to/Daily-Omni/Videos", ... random_seed=42, ... ) >>> requests = dataset.sample(tokenizer, num_requests=100) """ return DailyOmniDataset( qa_json_path=qa_json_path, dataset_path=dataset_path, dataset_split=dataset_split, random_seed=random_seed, video_dir=video_dir, input_mode=input_mode, max_duration_seconds=max_duration_seconds, dataset_subset=dataset_subset, no_stream=no_stream, **kwargs, ) def get_daily_omni_statistics( qa_json_path: str | None = None, dataset_path: str | None = DailyOmniDataset.DEFAULT_HF_DATASET_ID, dataset_split: str = "train", ) -> dict[str, Any]: """Get statistics about the Daily-Omni dataset. Args: qa_json_path: Path to local qa.json file (recommended for offline/air-gapped environments). When provided, ``dataset_path`` is ignored. dataset_path: HuggingFace dataset path. Defaults to ``DailyOmniDataset.DEFAULT_HF_DATASET_ID`` when ``qa_json_path`` is omitted. Pass ``None`` only together with ``qa_json_path``. dataset_split: Dataset split to use (default: "train") Returns: Statistics dict with task type distribution and other info Example: >>> from vllm_omni.benchmarks.data_modules.daily_omni_dataset import get_daily_omni_statistics # Local JSON mode >>> stats = get_daily_omni_statistics(qa_json_path="/path/to/qa.json") # HuggingFace mode >>> stats = get_daily_omni_statistics(dataset_path="liarliar/Daily-Omni") >>> print(f"Total QA pairs: {stats['total_qa_pairs']}") >>> print(f"Task distribution: {stats['task_distribution']}") """ dataset = DailyOmniDataset( qa_json_path=qa_json_path, dataset_path=dataset_path, dataset_split=dataset_split, ) task_stats = dataset.get_task_statistics() source = str(qa_json_path) if qa_json_path else f"{dataset_path}/{dataset_split}" return { "source": source, "total_qa_pairs": len(list(dataset.data)), "task_distribution": task_stats, }