"""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 Hub id (``dataset_path``): resolved with ``snapshot_download`` to the repo's ``qa.json``, which reads the hub cache and therefore also works under ``HF_HUB_OFFLINE`` 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`` (Hub ids, resolved to the repo's ``qa.json`` through the 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``. 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, ) # Hub id mode (works offline once the repo is in the HF hub cache) 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 io import json import logging import math import os import shutil import tarfile import wave 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__) # Official MiniCPM ``get_video_frame_audio_segments`` default (env ``MAX_NUM_FRAMES``). _MINICPM_MAX_NUM_FRAMES = int(os.environ.get("MAX_NUM_FRAMES", "64")) _MINICPM_AUDIO_SR = 16000 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 _extract_daily_omni_videos_tar(tar_path: Path, cache_key: str) -> Path: """Extract ``Videos.tar`` once into the shared media cache and return the ``Videos`` root. Cached under ``HF_HOME`` / ``vllm_omni/daily_omni_media/``. Reuses a previous extraction when the tarball fingerprint matches. """ safe = cache_key.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" 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 (cache_key=%s)", tar_path, cache_key) 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 media ready at %s", videos_dir) return videos_dir 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}}/``). 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 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)." ) return _extract_daily_omni_videos_tar(tar_path, rid) def _resolve_hf_cache_snapshot(path: Path) -> Path: """Map a HF hub cache dir (``datasets--org--name``) to its checked-out snapshot dir. Any other directory is returned unchanged. """ snapshots = path / "snapshots" if not snapshots.is_dir(): return path ref = path / "refs" / "main" if ref.is_file(): try: target = snapshots / ref.read_text(encoding="utf-8").strip() except OSError: target = snapshots if target.is_dir(): return target revisions = [p for p in snapshots.iterdir() if p.is_dir()] if revisions: return max(revisions, key=lambda p: p.stat().st_mtime) return path #: QA metadata pulled by :func:`ensure_daily_omni_hub_root`. ``Videos.tar`` is deliberately left #: out: it is ~60GB and only :func:`ensure_daily_omni_hub_videos_dir` needs it, lazily, once a #: request actually asks for media. _DAILY_OMNI_QA_ALLOW_PATTERNS = ["qa.json", "QA.json"] def ensure_daily_omni_hub_root(repo_id: str) -> Path: """Resolve a Daily-Omni Hub id to its snapshot directory, downloading QA metadata if needed. Mirrors :func:`...seed_tts_dataset.resolve_seed_tts_root`: ``snapshot_download`` resolves the repo through the **hub cache**, so an air-gapped run (``HF_HUB_OFFLINE=1``) keeps working as long as the dataset was pre-staged there. ``datasets.load_dataset`` cannot do this — it only consults the separate *datasets* cache, which a ``hf download`` never populates. Raises: ImportError: if ``huggingface_hub`` is not installed. ValueError: if ``repo_id`` is empty. """ rid = (repo_id or "").strip() if not rid: raise ValueError("repo_id is required to download Daily-Omni from Hugging Face") try: from huggingface_hub import snapshot_download except ImportError as e: raise ImportError( "Install huggingface_hub to load Daily-Omni from the Hub, or pass a local " "--daily-omni-qa-json / --daily-omni-video-dir mirror." ) from e cache = snapshot_download( repo_id=rid, repo_type="dataset", allow_patterns=_DAILY_OMNI_QA_ALLOW_PATTERNS, ) root = _resolve_hf_cache_snapshot(Path(cache).resolve()) logger.info("Daily-Omni Hub snapshot ready at %s (repo=%s)", root, rid) return root def _daily_omni_qa_load_error( repo_id: str | None, snapshot_error: Exception | None, datasets_error: Exception, ) -> RuntimeError: """Compose an actionable error after both QA-metadata sources failed. Offline CI hits this when the dataset is missing from the hub cache *or* staged under a different repo id, so the message names both attempts and the flags that bypass them. """ parts = [f"Could not load Daily-Omni QA metadata for {repo_id!r}."] if snapshot_error is not None: parts.append(f"huggingface_hub snapshot_download failed: {type(snapshot_error).__name__}: {snapshot_error}") parts.append(f"datasets.load_dataset failed: {type(datasets_error).__name__}: {datasets_error}") parts.append( "For offline runs, pre-stage the dataset in the HF hub cache " "(`hf download --repo-type dataset `) and make sure the repo id matches, " "or pass --daily-omni-qa-json / --daily-omni-video-dir pointing at a local mirror." ) return RuntimeError(" ".join(parts)) def resolve_daily_omni_local_root(dataset_path: str | None) -> Path | None: """Return the local Daily-Omni mirror root for ``dataset_path``, or ``None`` if not local. Accepts a plain directory (``qa.json`` + ``Videos/`` or ``Videos.tar``, i.e. the layout of the Hub repo) as well as a HuggingFace hub cache directory (``datasets--liarliar--Daily-Omni``), which is resolved to its snapshot. Hub ids such as ``liarliar/Daily-Omni`` return ``None``. """ raw = (dataset_path or "").strip() if not raw: return None path = Path(raw).expanduser() if not path.is_dir(): return None return _resolve_hf_cache_snapshot(path.resolve()) def daily_omni_local_qa_json(root: Path) -> Path | None: """Locate ``qa.json`` inside a local Daily-Omni mirror (``None`` when absent).""" for name in ("qa.json", "QA.json"): candidate = root / name if candidate.is_file(): return candidate return None def daily_omni_local_videos_dir(root: Path) -> Path | None: """Return the ``Videos`` root of a local mirror, extracting ``Videos.tar`` when needed.""" for name in ("Videos", "videos"): candidate = root / name if candidate.is_dir(): return candidate for name in ("Videos.tar", "videos.tar"): tar_path = root / name if tar_path.is_file(): return _extract_daily_omni_videos_tar(tar_path, f"local__{root.name}") return None 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"] # How multimodal parts are packed into OpenAI chat messages: # - ``qwen``: one ``video_url`` + one ``audio_url`` (upstream Daily-Omni / Qwen protocol). # - ``minicpm-interleave``: 1fps ``[image_i, audio_i, ...]`` matching OpenBMB MiniCPM-o # ``get_video_frame_audio_segments`` + interleaved omni contents (paper ~80% recipe). DailyOmniPackMode = Literal["qwen", "minicpm-interleave"] # ``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." ) # MiniCPM-o ``get_sys_prompt(mode="omni")`` without ref-audio is an empty system string. MINICPM_OMNI_SYSTEM_TEXT = "" def _uniform_sample_indices(n: int, k: int) -> list[int]: """Evenly pick ``k`` indices from ``0..n-1`` (MiniCPM ``uniform_sample`` on index lists).""" if k <= 0: return [] if n <= k: return list(range(n)) # Match numpy linspace(..., dtype=int) used upstream. try: import numpy as np return [int(i) for i in np.linspace(0, n - 1, k, dtype=int)] except ImportError: if k == 1: return [0] return [int(round(i * (n - 1) / (k - 1))) for i in range(k)] def _probe_video_frames_and_fps(video_path: Path) -> tuple[int, float]: """Frame count and average FPS, replacing ``len(vr)`` / ``vr.get_avg_fps()``. PyAV is used instead of ``decord`` because ``decord`` ships x86_64-only Linux wheels and is unmaintained, while ``av`` is already a first-class requirement and has aarch64 wheels (Ascend hosts). """ import av with av.open(str(video_path)) as container: stream = container.streams.video[0] avg_fps = float(stream.average_rate) if stream.average_rate else 0.0 num_frames = stream.frames or 0 if num_frames <= 0: # Containers such as raw webm leave ``frames`` at 0; derive it from duration. duration: float | None = None if stream.duration is not None and stream.time_base is not None: duration = float(stream.duration * stream.time_base) elif container.duration is not None: duration = container.duration / av.time_base if duration and avg_fps: num_frames = int(math.floor(duration * avg_fps)) if num_frames <= 0: # Last resort for streams with no usable metadata at all. stream.thread_type = "AUTO" num_frames = sum(1 for _ in container.decode(stream)) if num_frames <= 0: raise ValueError(f"No decodable video frames in {video_path}") return num_frames, avg_fps or 1.0 def _decode_video_frames_rgb(video_path: Path, frame_idx: list[int]) -> list[Any]: """Decode presentation-order ``frame_idx`` into RGB ``PIL.Image``s. Mirrors ``decord.VideoReader.get_batch(frame_idx)``: one image per requested index, in the requested order, duplicates repeated. Decoding is a single forward pass (no seeking) so the mapping from index to picture matches decord's indexing. """ import av wanted = sorted({int(i) for i in frame_idx}) if not wanted: return [] decoded: dict[int, Any] = {} with av.open(str(video_path)) as container: stream = container.streams.video[0] stream.thread_type = "AUTO" cursor = 0 for position, frame in enumerate(container.decode(stream)): if wanted[cursor] > position: continue image = frame.to_image() # ``<=`` also absorbs indices that overshoot the real frame count. while cursor < len(wanted) and wanted[cursor] <= position: decoded[wanted[cursor]] = image cursor += 1 if cursor >= len(wanted): break if not decoded: raise ValueError(f"Decoded no frames from {video_path} for indices {wanted[:8]}") # Metadata may promise more frames than the stream actually decodes; clamp to the last one. last_image = decoded[max(decoded)] return [decoded.get(int(i), last_image) for i in frame_idx] def _numpy_to_wav_bytes(audio_np: Any, sample_rate: int = _MINICPM_AUDIO_SR) -> bytes: """Encode a mono float/int numpy waveform as 16-bit PCM WAV bytes.""" import numpy as np arr = np.asarray(audio_np) if arr.ndim > 1: arr = arr.reshape(-1) if arr.dtype != np.int16: arr = np.clip(arr.astype(np.float64), -1.0, 1.0) arr = (arr * 32767.0).astype(np.int16) buf = io.BytesIO() with wave.open(buf, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) wf.setframerate(sample_rate) wf.writeframes(arr.tobytes()) return buf.getvalue() def _pil_to_jpeg_bytes(image: Any, quality: int = 85) -> bytes: buf = io.BytesIO() image.save(buf, format="JPEG", quality=quality) return buf.getvalue() @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 Hub id (``dataset_path``): the repo's ``qa.json`` via ``snapshot_download`` (hub-cache backed, offline-safe); ``datasets.load_dataset`` only as a parquet fallback 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; resolved through the hub cache, so a pre-staged repo also loads with ``HF_HUB_OFFLINE=1``. 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``). pack_mode: Multimodal packing recipe: ``qwen`` — one ``video_url`` + optional ``audio_url`` (default; Daily-Omni/Qwen); ``minicpm-interleave`` — MiniCPM-o paper recipe: 1fps frames interleaved with matching 1s audio segments as ``image_url``/``audio_url`` pairs (required to approach OpenBMB ~80% Daily-Omni; needs local video+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,...``). For ``minicpm-interleave``, embeds JPEG/WAV segment data URLs instead of the raw MP4. 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", pack_mode: DailyOmniPackMode = "qwen", 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}") if pack_mode not in ("qwen", "minicpm-interleave"): raise ValueError(f"pack_mode must be 'qwen' or 'minicpm-interleave', got {pack_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.pack_mode: DailyOmniPackMode = pack_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] = {} #: Cache of MiniCPM 1fps interleaved expansions: video_id -> content parts. self._minicpm_interleave_cache: dict[str, list[dict[str, Any]]] = {} # 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, pack_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, pack_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 for a Hub id, preferring the repo's ``qa.json`` over ``datasets``. The Hub repo ships ``qa.json`` (1,197 rows) next to ``Videos.tar``, so the primary path is :func:`ensure_daily_omni_hub_root` → :meth:`_load_from_local_json`: ``snapshot_download`` reads the hub cache and therefore works under ``HF_HUB_OFFLINE``, the same contract as Seed-TTS. ``datasets.load_dataset`` remains only as a fallback for mirrors that publish parquet rows instead of ``qa.json``; it consults the *datasets* cache rather than the hub cache and streams shards over HTTP, so it cannot serve air-gapped runs. 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. """ snapshot_error: Exception | None = None try: root = ensure_daily_omni_hub_root(self.dataset_path) except Exception as e: # noqa: BLE001 - any failure just falls through to `datasets` snapshot_error = e else: qa_json = daily_omni_local_qa_json(root) if qa_json is not None: self.qa_json_path = qa_json logger.info("Loading Daily-Omni QA metadata from Hub snapshot: %s", qa_json) self._load_from_local_json() return logger.info( "Hub snapshot %s ships no qa.json; falling back to `datasets.load_dataset`", root, ) if load_dataset is None: raise _daily_omni_qa_load_error( self.dataset_path, snapshot_error, ImportError("datasets library is not installed (pip install datasets)"), ) 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 try: ds = load_dataset(self.dataset_path, **load_kw) except Exception as e: raise _daily_omni_qa_load_error(self.dataset_path, snapshot_error, e) from e 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). system_text = self._system_text_for_pack_mode() prompt_len = len(tokenizer.encode(f"{system_text}\n{user_text}" if system_text else 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 multimodal OpenAI content parts and request extras for the active modes.""" if self.pack_mode == "minicpm-interleave": return self._compose_minicpm_interleaved_multimodal(video_id, video_url) # Qwen / 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" def _compose_minicpm_interleaved_multimodal( self, video_id: str, video_url: str | None, ) -> tuple[list[dict[str, Any]] | None, dict[str, Any] | None, Literal["first", "last"]]: """Pack media like MiniCPM-o official omni chat: 1fps ``[frame_i, audio_i, ...]``. OpenBMB's HF / utils path expands ``video_url`` with ``use_audio=True`` into interleaved PIL frames and 1s audio ndarrays. vLLM-Omni does not implement that expansion, so the bench client builds ordered ``image_url`` / ``audio_url`` parts instead. MiniCPM-o 5.0's OpenAI chat template preserves content-part order, so placeholders stay time-aligned. ``max_slice_nums=1`` and ``use_image_id=False`` mirror MiniCPM-o's omni path (``modeling_minicpmo.py`` passes both for interleaved frames): slicing every frame with the config default of 9 would blow past ``max_model_len``, and ``N`` prefixes would label each frame as a separate picture instead of a video timeline. """ del video_url # Hub HTTP URLs are not used; local extract is required. extra: dict[str, Any] = { "mm_processor_kwargs": { "use_audio_in_video": False, "max_slice_nums": 1, "use_image_id": False, } } mode = self.input_mode if mode == "audio": # Fall back to whole-WAV for audio-only ablations. a = self._get_audio_content(video_id) return ([a] if a else None), extra, "first" parts = self._get_minicpm_interleaved_parts(video_id, include_audio=(mode == "all")) if not parts: return None, None, "first" return parts, extra, "first" def _get_minicpm_interleaved_parts( self, video_id: str, *, include_audio: bool, ) -> list[dict[str, Any]] | None: """Return cached or freshly extracted MiniCPM-style interleaved OpenAI content parts.""" cache_key = f"{video_id}|audio={int(include_audio)}|inline={int(self.inline_local_video)}" cached = self._minicpm_interleave_cache.get(cache_key) if cached is not None: return cached self._lazy_ensure_hub_media_dir() video_path = self._resolve_local_video_path(video_id) if video_path is None: logger.warning( "MiniCPM interleave pack requires local video under video_dir=%s for video_id=%r", self.video_dir, video_id, ) return None audio_path = self._resolve_local_audio_path(video_id) if include_audio else None if include_audio and audio_path is None: logger.warning( "MiniCPM interleave pack (input_mode=all) missing WAV for video_id=%r under %s", video_id, self.video_dir, ) return None try: frames, audio_segments = self._extract_minicpm_frame_audio_segments( video_path, audio_path=audio_path, include_audio=include_audio, ) except Exception: logger.exception( "Failed MiniCPM 1fps extract for video_id=%r path=%s", video_id, video_path, ) return None if not frames: logger.warning("No frames extracted for video_id=%r", video_id) return None if include_audio and len(audio_segments) != len(frames): logger.warning( "Frame/audio length mismatch for video_id=%r: frames=%d audios=%d", video_id, len(frames), len(audio_segments), ) n = min(len(frames), len(audio_segments)) frames = frames[:n] audio_segments = audio_segments[:n] cache_root: Path | None = None if not self.inline_local_video: assert self.video_dir is not None cache_root = self.video_dir / ".minicpm_daily_omni_interleave" / video_id cache_root.mkdir(parents=True, exist_ok=True) parts: list[dict[str, Any]] = [] for i, frame in enumerate(frames): jpeg = _pil_to_jpeg_bytes(frame) if self.inline_local_video: b64 = base64.b64encode(jpeg).decode("ascii") parts.append( { "type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}, } ) else: assert cache_root is not None frame_path = cache_root / f"frame_{i:04d}.jpg" if not frame_path.is_file() or frame_path.stat().st_size == 0: frame_path.write_bytes(jpeg) parts.append( { "type": "image_url", "image_url": {"url": frame_path.expanduser().resolve().as_uri()}, } ) if include_audio: wav = _numpy_to_wav_bytes(audio_segments[i]) if self.inline_local_video: b64 = base64.b64encode(wav).decode("ascii") parts.append( { "type": "audio_url", "audio_url": {"url": f"data:audio/wav;base64,{b64}"}, } ) else: assert cache_root is not None audio_seg_path = cache_root / f"audio_{i:04d}.wav" if not audio_seg_path.is_file() or audio_seg_path.stat().st_size == 0: audio_seg_path.write_bytes(wav) parts.append( { "type": "audio_url", "audio_url": {"url": audio_seg_path.expanduser().resolve().as_uri()}, } ) # File URLs are tiny; cache them so repeated QA on the same video skips re-extract. # Inline base64 must not be retained: ~700 Daily-Omni videos would pin GBs of RSS. if not self.inline_local_video: self._minicpm_interleave_cache[cache_key] = parts logger.debug( "MiniCPM interleave packed video_id=%r frames=%d include_audio=%s parts=%d", video_id, len(frames), include_audio, len(parts), ) return parts @staticmethod def _extract_minicpm_frame_audio_segments( video_path: Path, *, audio_path: Path | None, include_audio: bool, max_num_frames: int = _MINICPM_MAX_NUM_FRAMES, ) -> tuple[list[Any], list[Any]]: """Port of MiniCPM ``get_video_frame_audio_segments`` (stack_frames=1, 1fps).""" import numpy as np from vllm.multimodal.media.audio import load_audio num_video_frames, avg_fps = _probe_video_frames_and_fps(video_path) duration = num_video_frames / avg_fps num_seconds = max(1, int(math.ceil(duration))) second_timestamps = list(range(num_seconds)) if duration > max_num_frames: timestamps = [round(i * 0.1, 1) for i in range(int(duration / 0.1))] frame_idx = [min(int(ts * avg_fps), num_video_frames - 1) for ts in timestamps] pick = _uniform_sample_indices(len(frame_idx), max_num_frames) frame_idx = [frame_idx[i] for i in pick] timestamps = [timestamps[i] for i in pick] else: frame_idx = [min(int(i * avg_fps), num_video_frames - 1) for i in range(num_seconds)] timestamps = second_timestamps frames = _decode_video_frames_rgb(video_path, frame_idx) audio_segments: list[Any] = [] if include_audio: load_path = str(audio_path) if audio_path is not None else str(video_path) audio_np, sr = load_audio(load_path, sr=_MINICPM_AUDIO_SR, mono=True) for i, start_time in enumerate(timestamps): end_time = timestamps[i + 1] if i < len(timestamps) - 1 else duration start_sample = int(start_time * sr) end_sample = int(end_time * sr) segment = audio_np[start_sample:end_sample] if i == len(timestamps) - 1 and len(segment) < 1600: segment = np.concatenate([segment, np.zeros(1600 - len(segment), dtype=segment.dtype)]) if len(segment) == 0: segment = np.zeros(1600, dtype=np.float32) audio_segments.append(segment) return frames, audio_segments def _system_text_for_pack_mode(self) -> str: if self.pack_mode == "minicpm-interleave": return MINICPM_OMNI_SYSTEM_TEXT return DAILY_OMNI_SYSTEM_TEXT @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``.""" 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}] system_text = self._system_text_for_pack_mode() messages: list[dict[str, Any]] = [] if system_text: messages.append({"role": "system", "content": [{"type": "text", "text": system_text}]}) messages.append({"role": "user", "content": user_content}) return messages 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"pack_mode={self.pack_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, }