Files
vllm-omni/vllm_omni/benchmarks/duplex/omni_duplex_eval_dataset.py
T

194 lines
7.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Dataset and split normalization for Omni-DuplexEval.
The loader accepts a Hugging Face dataset id, a JSON/JSONL manifest, or an
already materialized iterable. Media is deliberately resolved at use time so
the benchmark remains usable in air-gapped environments.
"""
from __future__ import annotations
import json
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import Any
DEFAULT_DATASET = "Hothan/Omni-DuplexEval"
RTD_SPLITS = frozenset(
{
"RTD_world_knowledge",
"RTD_counting",
"RTD_fine_grained_movement",
"RTD_interaction_relation",
"RTD_OCR",
"RTD_Omni",
}
)
PR_SPLITS = frozenset({"PR_correction", "PR_event_reminder", "PR_post_event_reminder"})
_TASK_ALIASES = {
"correction": "correction",
"pr_correction": "correction",
"event_reminder": "proactive_reminder",
"pr_event_reminder": "proactive_reminder",
"proactive_reminder": "proactive_reminder",
"post_event_reminder": "post_event_reminder",
"pr_post_event_reminder": "post_event_reminder",
}
def canonical_task_type(value: str) -> str:
key = str(value).strip().lower().replace("-", "_").replace(" ", "_")
if key not in _TASK_ALIASES:
raise ValueError(f"unsupported Omni-DuplexEval task type: {value!r}")
return _TASK_ALIASES[key]
def task_type_for_split(split: str) -> str:
"""Match the task routing used by the official HF batch evaluator."""
key = str(split).strip().lower()
if "correction" in key:
return "correction"
if "post_event" in key:
return "post_event_reminder"
if key.startswith("pr"):
return "proactive_reminder"
raise ValueError(f"cannot infer proactive-reminder task from split {split!r}")
def family_for_split(split: str, task_type: str | None = None) -> str:
if split in RTD_SPLITS or str(split).upper().startswith("RTD"):
return "rtd"
if split in PR_SPLITS or str(split).upper().startswith("PR"):
return "pr"
if task_type:
return "rtd" if str(task_type).lower().startswith("rtd") else "pr"
raise ValueError(f"cannot infer benchmark family from split {split!r}")
def _value(row: dict[str, Any], *keys: str, default: Any = None) -> Any:
for key in keys:
if key in row and row[key] is not None:
return row[key]
return default
def _float(value: Any, default: float | None = None) -> float | None:
try:
return None if value is None else float(value)
except (TypeError, ValueError):
return default
@dataclass(frozen=True)
class DuplexSample:
id: str
split: str
family: str
task_type: str | None
video: Any
question_audio: Any = None
question_text: str = ""
answer1: str = ""
answer2: str = ""
reminder1: Any = None
reminder2: Any = None
video_duration: float | None = None
video_type: str = ""
raw: dict[str, Any] | None = None
@classmethod
def from_row(cls, row: dict[str, Any], *, split: str | None = None, media_root: Path | None = None) -> DuplexSample:
chosen_split = str(split or _value(row, "split", "subset", "config", default=""))
task_value = _value(row, "task_type", "task", "type")
family = str(_value(row, "family", default="") or family_for_split(chosen_split, task_value))
task = None
if family == "pr":
task = canonical_task_type(str(task_value)) if task_value else task_type_for_split(chosen_split)
sample_id = str(_value(row, "id", "sample_id", "uid", "name", default=""))
video = _value(row, "video", "video_path", "video_file")
audio = _value(row, "question_audio", "audio", "question_wav")
if media_root:
def resolve(value: Any) -> Any:
if not isinstance(value, str):
return value
candidate = Path(value).expanduser()
return str(candidate if candidate.is_absolute() else media_root / candidate)
video, audio = resolve(video), resolve(audio)
return cls(
id=sample_id,
split=chosen_split,
family=family,
task_type=task,
video=video,
question_audio=audio,
question_text=str(_value(row, "question_text", "question", "instruction", default="") or ""),
answer1=str(_value(row, "answer1", "answer", "ground_answer", default="") or ""),
answer2=str(_value(row, "answer2", default="") or ""),
reminder1=_value(row, "reminder1", "reminder_1"),
reminder2=_value(row, "reminder2", "reminder_2"),
video_duration=_float(_value(row, "video_duration", "duration")),
video_type=str(_value(row, "video_type", default="") or ""),
raw=dict(row),
)
def _read_manifest(path: Path) -> list[dict[str, Any]]:
if path.suffix.lower() == ".jsonl":
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
payload = payload.get("data", payload.get("samples", [payload]))
if not isinstance(payload, list):
raise ValueError("manifest must contain a JSON list")
return payload
def load_samples(
dataset: str | Path | Iterable[dict[str, Any]] = DEFAULT_DATASET,
*,
split: str | None = None,
family: str = "all",
media_root: str | Path | None = None,
limit: int | None = None,
ids: Iterable[str] | None = None,
) -> list[DuplexSample]:
if isinstance(dataset, (str, Path)) and Path(str(dataset)).exists():
rows = _read_manifest(Path(str(dataset)))
elif not isinstance(dataset, (str, Path)):
rows = list(dataset)
else:
try:
from datasets import load_dataset
except ImportError as exc:
raise RuntimeError("datasets is required for Hugging Face loading; use a local manifest instead") from exc
kwargs: dict[str, Any] = {}
if split and split != "all":
kwargs["split"] = split
loaded = load_dataset(str(dataset), **kwargs)
if isinstance(loaded, dict):
rows = []
for name, table in loaded.items():
rows.extend({**dict(row), "split": name} for row in table)
else:
rows = [dict(row) for row in loaded]
wanted = set(str(item) for item in ids) if ids else None
root = Path(media_root).expanduser() if media_root else None
result = []
for row in rows:
sample = DuplexSample.from_row(row, split=None if split in (None, "all") else split, media_root=root)
if split and split != "all" and sample.split != split:
continue
if family != "all" and sample.family != family:
continue
if wanted is not None and sample.id not in wanted:
continue
result.append(sample)
if limit is not None:
result = result[: max(0, limit)]
return result