Files
vllm-omni/tests/benchmarks/test_omniinteract_eval.py
T

821 lines
29 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import json
from dataclasses import asdict
from pathlib import Path
import pytest
from vllm_omni.benchmarks.data_modules.omniinteract_dataset import (
OmniInteractCase,
OmniInteractEvaluationOptions,
)
from vllm_omni.benchmarks.omniinteract import OmniInteractCaseResult
from vllm_omni.benchmarks.omniinteract_eval import (
AlignedWord,
TranscriptChunk,
_summarize,
build_slots,
evaluate_batch,
evaluate_case,
evaluation_inputs_fingerprint,
match_slots,
)
from vllm_omni.benchmarks.omniinteract_judge import (
CoreJudgment,
EarlyJudgment,
OmniInteractJudge,
PartialJudgment,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.benchmark]
class _FixedJudge:
def judge_early(
self,
slot: dict[str, object],
full_context: str,
actual_text: str,
) -> EarlyJudgment:
del slot, full_context, actual_text
return EarlyJudgment("neutral", 1.0, "ok", "{}", "llm_json")
def judge_core(
self,
slot: dict[str, object],
full_context: str,
actual_text: str,
future_answers: str,
) -> CoreJudgment:
del slot, full_context, actual_text, future_answers
return CoreJudgment(1.0, "answer", False, "ok", "{}", "llm_json")
def judge_interrupted_partial(
self,
slot: dict[str, object],
actual_text: str,
) -> PartialJudgment:
del slot, actual_text
return PartialJudgment(1.0, False, "ok", "{}", "llm_json")
class _GeneratedJudge(OmniInteractJudge):
def __init__(self, responses: list[str]) -> None:
super().__init__("http://judge", "judge-model")
self.responses = iter(responses)
def _generate(self, system_prompt: str, user_prompt: str) -> str:
del system_prompt, user_prompt
return next(self.responses)
def test_build_slots_and_split_word_aligned_chunk() -> None:
slots = build_slots(
[
{
"question_time": 0.0,
"answer_time": 2.0,
"question_text": "question",
"answer_text": "answer",
"question_type": "realtime",
},
{
"question_time": 5.0,
"answer_time": 6.0,
"question_text": "next",
"answer_text": "next answer",
"question_type": "proactive",
},
],
"multi_turn",
)
chunk = TranscriptChunk(
source_id=0,
text="okay answer",
start=0.5,
end=2.5,
aligned_words=(
AlignedWord("okay", 0.5, 0.9),
AlignedWord("answer", 2.1, 2.5),
),
)
matched, unmatched = match_slots(slots, [chunk])
assert not unmatched
assert matched[0].early_chunks[0].text == "okay"
assert matched[0].core_chunks[0].text == "answer"
assert matched[0].core_chunks[0].start == pytest.approx(2.1)
def test_judge_parses_json_wrapped_in_model_text() -> None:
judge = _GeneratedJudge(
[
'result: {"flag":"FP_Hallucination","score":1,"rationale":"early"}',
'```json\n{"score":0.8,"trigger_phrase":"answer","spoiler":false,"rationale":"good"}\n```',
'{"score":0.7,"hallucination":false,"rationale":"partial"}',
]
)
slot = {"question_text": "q", "gt_answer": "answer"}
early = judge.judge_early(slot, "context", "guess")
core = judge.judge_core(slot, "context", "answer", "(none)")
partial = judge.judge_interrupted_partial(slot, "answer")
assert early.category == "hallucination"
assert early.score == 0.0
assert core.score == pytest.approx(0.8)
assert core.trigger_phrase == "answer"
assert partial.score == pytest.approx(0.7)
def test_evaluate_case_persists_accuracy_artifact(tmp_path: Path) -> None:
annotation = tmp_path / "annotation.json"
annotation.write_text(
json.dumps(
[
{
"question_time": 0.0,
"answer_time": 1.0,
"question_text": "question",
"answer_text": "answer",
"question_type": "realtime",
}
]
)
)
output_dir = tmp_path / "case-output"
output_dir.mkdir()
(output_dir / "wav_transcript.json").write_text(
json.dumps(
{
"chunks": [
{
"timestamp": [1.0, 1.5],
"text": "answer",
"aligned_words": [{"text": "answer", "start": 1.0, "end": 1.5}],
}
]
}
)
)
video = tmp_path / "video.mp4"
video.touch()
case = OmniInteractCase("1q1a", "video.mp4", video, annotation, "multi_turn")
result = OmniInteractCaseResult(
subset="1q1a",
video=str(video),
output_dir=str(output_dir),
success=True,
eligible_for_official_eval=True,
)
artifact = tmp_path / "evaluation.json"
evaluation = evaluate_case(case, result, _FixedJudge(), artifact)
assert evaluation["timing_precision"] == "word-aligned"
assert evaluation["summary"]["IA_QTF1"] == pytest.approx(1.0)
paper = evaluation["summary"]["paper_metrics"]["exp_f1"]
assert paper["all_global"]["IA_QTF1"] == pytest.approx(1.0)
assert paper["realtime"]["IA_QTF1"] == pytest.approx(1.0)
assert json.loads(artifact.read_text())["status"] == "ok"
fingerprint = evaluation["inputs_fingerprint"]
assert fingerprint["annotation_sha256"]
assert fingerprint["transcript_sha256"]
assert fingerprint["protocol_source"]
def _realtime_eval_case(tmp_path: Path, *, transcript_text: str) -> tuple[OmniInteractCase, OmniInteractCaseResult]:
annotation = tmp_path / "annotation.json"
annotation.write_text(
json.dumps(
[
{
"question_time": 0.0,
"answer_time": 1.0,
"question_text": "question",
"answer_text": "answer",
"question_type": "realtime",
}
]
)
)
output_dir = tmp_path / "case-output"
output_dir.mkdir(exist_ok=True)
chunks: list[dict[str, object]] = []
if transcript_text:
chunks.append({"timestamp": [1.0, 1.5], "text": transcript_text})
(output_dir / "wav_transcript.json").write_text(json.dumps({"chunks": chunks}))
video = tmp_path / "video.mp4"
video.touch()
case = OmniInteractCase("1q1a", "video.mp4", video, annotation, "multi_turn")
result = OmniInteractCaseResult(
subset="1q1a",
video=str(video),
output_dir=str(output_dir),
success=True,
eligible_for_official_eval=True,
)
return case, result
def _eval_options(tmp_path: Path, *, skip_existing: bool) -> OmniInteractEvaluationOptions:
return OmniInteractEvaluationOptions(
judge_base_url="http://127.0.0.1:9",
judge_model="cached-judge",
judge_api_key="EMPTY",
judge_timeout_s=60.0,
judge_max_tokens=32,
workers=1,
output_dir=tmp_path / "evaluation",
skip_existing=skip_existing,
)
class _ConfiguredFixedJudge(_FixedJudge):
def __init__(
self,
base_url: str,
model: str,
*,
api_key: str = "EMPTY",
timeout_s: float = 60.0,
max_tokens: int = 512,
) -> None:
del api_key, timeout_s
self.base_url = base_url
self.model = model
self.max_tokens = max_tokens
def test_skip_existing_reuses_matching_fingerprint(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import vllm_omni.benchmarks.omniinteract_eval as eval_mod
case, result = _realtime_eval_case(tmp_path, transcript_text="answer")
options = _eval_options(tmp_path, skip_existing=False)
monkeypatch.setattr(eval_mod, "OmniInteractJudge", _ConfiguredFixedJudge)
first = evaluate_batch([case], [result], options)
assert first["summary"]["IA_QTF1"] == pytest.approx(1.0)
calls: list[str] = []
def _forbidden(
case: OmniInteractCase,
result: OmniInteractCaseResult,
judge: object,
output_path: Path,
config: object = None,
inputs_fingerprint: object = None,
) -> dict[str, object]:
del case, result, judge, output_path, config, inputs_fingerprint
calls.append("evaluate_case")
raise AssertionError("matching fingerprint must reuse the cached evaluation")
monkeypatch.setattr(eval_mod, "evaluate_case", _forbidden)
reused = evaluate_batch([case], [result], _eval_options(tmp_path, skip_existing=True))
assert calls == []
assert reused["summary"]["IA_QTF1"] == pytest.approx(1.0)
def test_skip_existing_rejects_stale_transcript(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import vllm_omni.benchmarks.omniinteract_eval as eval_mod
case, result = _realtime_eval_case(tmp_path, transcript_text="answer")
options = _eval_options(tmp_path, skip_existing=True)
fingerprint = evaluation_inputs_fingerprint(
annotation_path=case.annotation_path,
transcript_path=Path(result.output_dir) / "wav_transcript.json",
judge_model=options.judge_model,
judge_base_url=options.judge_base_url,
judge_max_tokens=options.judge_max_tokens,
)
sample_id = f"{case.subset}__{Path(result.output_dir).name}"
cache_path = options.output_dir / f"{sample_id}.unified_eval.json"
cache_path.parent.mkdir(parents=True)
cache_path.write_text(
json.dumps(
{
"status": "ok",
"sample_id": sample_id,
"inputs_fingerprint": asdict(fingerprint),
"summary": {
"IA_QTF1": 1.0,
"num_slots": 1,
"num_unmatched_chunks": 0,
"Global_TP": 1,
"Global_FP": 0,
"Global_FN": 0,
},
"slots": [],
}
)
)
(Path(result.output_dir) / "wav_transcript.json").write_text(json.dumps({"chunks": []}))
monkeypatch.setattr(eval_mod, "OmniInteractJudge", _ConfiguredFixedJudge)
fresh = evaluate_batch([case], [result], options)
assert fresh["summary"]["IA_QTF1"] == pytest.approx(0.0)
class _MissingCoreScoreJudge(_ConfiguredFixedJudge):
def judge_core(
self,
slot: dict[str, object],
full_context: str,
actual_text: str,
future_answers: str,
) -> CoreJudgment:
del slot, full_context, actual_text, future_answers
return CoreJudgment(0.0, "", False, "Could not evaluate", "{}", "llm_parse_failed")
def test_evaluate_batch_reports_missing_score_as_parse_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import vllm_omni.benchmarks.omniinteract_eval as eval_mod
case, result = _realtime_eval_case(tmp_path, transcript_text="answer")
options = _eval_options(tmp_path, skip_existing=False)
monkeypatch.setattr(eval_mod, "OmniInteractJudge", _MissingCoreScoreJudge)
evaluation = evaluate_batch([case], [result], options)
assert evaluation["status"] == "failed"
assert evaluation["evaluated"] == 0
assert evaluation["failed"] == 1
sample_id = f"{case.subset}__{Path(result.output_dir).name}"
cached = json.loads((options.output_dir / f"{sample_id}.unified_eval.json").read_text())
assert cached["status"] == "parse_failed"
class _CoreRationaleNumberJudge(OmniInteractJudge):
def __init__(
self,
base_url: str,
model: str,
*,
api_key: str = "EMPTY",
timeout_s: float = 60.0,
max_tokens: int = 512,
) -> None:
super().__init__(base_url, model, api_key=api_key, timeout_s=timeout_s, max_tokens=max_tokens)
def _generate(self, system_prompt: str, user_prompt: str) -> str:
del system_prompt, user_prompt
return '{"rationale":"Cannot score: 1 required reference is missing."}'
def test_evaluate_batch_rejects_core_rationale_number_as_parse_failure(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
import vllm_omni.benchmarks.omniinteract_eval as eval_mod
case, result = _realtime_eval_case(tmp_path, transcript_text="answer")
options = _eval_options(tmp_path, skip_existing=False)
monkeypatch.setattr(eval_mod, "OmniInteractJudge", _CoreRationaleNumberJudge)
evaluation = evaluate_batch([case], [result], options)
assert evaluation["status"] == "failed"
assert evaluation["evaluated"] == 0
assert evaluation["failed"] == 1
sample_id = f"{case.subset}__{Path(result.output_dir).name}"
cached = json.loads((options.output_dir / f"{sample_id}.unified_eval.json").read_text())
assert cached["status"] == "parse_failed"
assert cached["slots"][0]["stage_core"]["parse_source"] == "llm_parse_failed"
assert cached["slots"][0]["stage_core"]["S_core"] == 0.0
def test_skip_existing_rejects_parse_failed_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
import vllm_omni.benchmarks.omniinteract_eval as eval_mod
case, result = _realtime_eval_case(tmp_path, transcript_text="answer")
options = _eval_options(tmp_path, skip_existing=True)
fingerprint = evaluation_inputs_fingerprint(
annotation_path=case.annotation_path,
transcript_path=Path(result.output_dir) / "wav_transcript.json",
judge_model=options.judge_model,
judge_base_url=options.judge_base_url,
judge_max_tokens=options.judge_max_tokens,
)
sample_id = f"{case.subset}__{Path(result.output_dir).name}"
cache_path = options.output_dir / f"{sample_id}.unified_eval.json"
cache_path.parent.mkdir(parents=True)
cache_path.write_text(
json.dumps(
{
"status": "parse_failed",
"sample_id": sample_id,
"inputs_fingerprint": asdict(fingerprint),
"summary": {
"IA_QTF1": 0.0,
"num_slots": 1,
"num_unmatched_chunks": 0,
"Global_TP": 0,
"Global_FP": 0,
"Global_FN": 1,
},
"slots": [
{
"stage_core": {"parse_source": "llm_parse_failed", "S_core": 0.0},
"TP_n": 0.0,
"FP_delta": 0,
"FN_delta": 1,
}
],
}
)
)
monkeypatch.setattr(eval_mod, "OmniInteractJudge", _ConfiguredFixedJudge)
fresh = evaluate_batch([case], [result], options)
assert fresh["evaluated"] == 1
assert fresh["failed"] == 0
assert fresh["summary"]["IA_QTF1"] == pytest.approx(1.0)
assert json.loads(cache_path.read_text())["status"] == "ok"
def _slot_row(
*,
scene_type: str,
question_type: str,
tp_n: float,
fp: int = 0,
fn: int = 0,
nested_role: str | None = None,
nested_group_id: int | None = None,
sample_id: str = "s",
tp_core: float | None = None,
answer_start: float | None = 1.0,
) -> dict[str, object]:
core_tp = tp_n if tp_core is None else tp_core
return {
"sample_id": sample_id,
"scene_type": scene_type,
"question_type": question_type,
"nested_role": nested_role,
"nested_group_id": nested_group_id,
"TP_n": tp_n,
"TP_core": core_tp,
"FP_delta": fp,
"FN_delta": fn,
"is_interrupted": False,
"stage_core": {"answer_start": answer_start},
}
def test_paper_metrics_exclude_nested_from_realtime_and_proactive() -> None:
summary = _summarize(
[
_slot_row(scene_type="multi_turn", question_type="realtime", tp_n=1.0),
_slot_row(
scene_type="nested",
question_type="realtime",
tp_n=1.0,
nested_role="inner",
nested_group_id=1,
answer_start=0.5,
),
_slot_row(
scene_type="nested",
question_type="proactive",
tp_n=1.0,
nested_role="outer",
nested_group_id=1,
answer_start=1.5,
),
_slot_row(scene_type="multi_turn", question_type="proactive", tp_n=1.0),
_slot_row(scene_type="1QnA", question_type="step", tp_n=1.0, fn=0),
],
unmatched=0,
)
paper = summary["paper_metrics"]["exp_f1"]
assert paper["realtime"]["num_slots"] == 1
assert paper["proactive"]["num_slots"] == 1
assert paper["nested"]["num_slots"] == 2
assert paper["one_q1a_global"]["num_slots"] == 4
assert paper["one_qna"]["num_slots"] == 1
assert paper["all_global"]["num_slots"] == 5
# All Global includes 1QnA; 1Q1A Global does not.
assert paper["all_global"]["Global_TP"] == pytest.approx(5.0)
assert paper["one_q1a_global"]["Global_TP"] == pytest.approx(4.0)
assert summary["nested"]["NCCS"] == pytest.approx(1.0)
assert summary["nested"]["missing_q1_count"] == 0
assert summary["paper_metrics"]["exp_nested"]["missed_outer"] == 0
assert summary["paper_metrics"]["exp_nested"]["inner_IA_QTF1"] == pytest.approx(1.0)
assert summary["paper_metrics"]["exp_nested"]["outer_IA_QTF1"] == pytest.approx(1.0)
assert summary["scenario_case_counts"]["realtime"] == 1
assert summary["scenario_case_counts"]["proactive"] == 1
assert summary["scenario_case_counts"]["nested"] == 1
assert summary["scenario_case_counts"]["1qna"] == 1
def test_nccs_allows_missing_answer_start() -> None:
summary = _summarize(
[
_slot_row(
scene_type="nested",
question_type="realtime",
tp_n=0.8,
tp_core=0.8,
nested_role="inner",
nested_group_id=1,
answer_start=None,
),
_slot_row(
scene_type="nested",
question_type="proactive",
tp_n=0.8,
tp_core=0.8,
nested_role="outer",
nested_group_id=1,
answer_start=None,
),
],
unmatched=0,
)
assert summary["nested"]["success_pairs"] == 1
assert summary["nested"]["NCCS"] == pytest.approx(0.8)
def test_missed_outer_counts_zero_outer_core() -> None:
summary = _summarize(
[
_slot_row(
scene_type="nested",
question_type="realtime",
tp_n=1.0,
tp_core=1.0,
nested_role="inner",
nested_group_id=1,
answer_start=0.5,
),
_slot_row(
scene_type="nested",
question_type="proactive",
tp_n=0.0,
tp_core=0.0,
fn=1,
nested_role="outer",
nested_group_id=1,
answer_start=1.5,
),
],
unmatched=0,
)
nested = summary["paper_metrics"]["exp_nested"]
assert nested["missed_outer"] == 1
assert nested["NCCS"] == pytest.approx(0.0)
assert nested["inner_IA_QTF1"] == pytest.approx(1.0)
assert nested["outer_IA_QTF1"] == pytest.approx(0.0)
def test_cross_boundary_core_sets_effective_start_hint() -> None:
slots = build_slots(
[
{
"question_time": 0.0,
"answer_time": 2.0,
"question_text": "question",
"answer_text": "answer",
"question_type": "realtime",
}
],
"multi_turn",
)
chunk = TranscriptChunk(
source_id=0,
text="okay answer",
start=0.5,
end=2.5,
aligned_words=(
AlignedWord("okay", 0.5, 0.9),
AlignedWord("answer", 2.1, 2.5),
),
)
matched, unmatched = match_slots(slots, [chunk])
assert not unmatched
assert matched[0].core_chunks[0].effective_start_hint == pytest.approx(2.0)
class _NonNeutralEarlyJudge(_FixedJudge):
def judge_early(
self,
slot: dict[str, object],
full_context: str,
actual_text: str,
) -> EarlyJudgment:
del slot, full_context, actual_text
return EarlyJudgment("other", 1.0, "ignored", "{}", "llm_json")
def test_early_tp_ack_only_for_neutral(tmp_path: Path) -> None:
annotation = tmp_path / "annotation.json"
annotation.write_text(
json.dumps(
[
{
"question_time": 0.0,
"answer_time": 2.0,
"question_text": "question",
"answer_text": "answer",
"question_type": "realtime",
}
]
)
)
output_dir = tmp_path / "case-output"
output_dir.mkdir()
(output_dir / "wav_transcript.json").write_text(
json.dumps(
{
"chunks": [
{
"timestamp": [0.5, 0.9],
"text": "okay",
"aligned_words": [{"text": "okay", "start": 0.5, "end": 0.9}],
},
{
"timestamp": [2.1, 2.5],
"text": "answer",
"aligned_words": [{"text": "answer", "start": 2.1, "end": 2.5}],
},
]
}
)
)
video = tmp_path / "video.mp4"
video.touch()
case = OmniInteractCase("1q1a", "video.mp4", video, annotation, "multi_turn")
result = OmniInteractCaseResult(
subset="1q1a",
video=str(video),
output_dir=str(output_dir),
success=True,
eligible_for_official_eval=True,
)
evaluation = evaluate_case(case, result, _NonNeutralEarlyJudge(), tmp_path / "evaluation.json")
assert evaluation["slots"][0]["TP_ack"] == 0.0
assert evaluation["slots"][0]["stage_early"]["category"] == "other"
def test_paper_metrics_unknown_inner_does_not_underflow_realtime() -> None:
summary = _summarize(
[
_slot_row(scene_type="multi_turn", question_type="realtime", tp_n=0.5),
_slot_row(
scene_type="nested",
question_type="unknown",
tp_n=2.0,
nested_role="inner",
nested_group_id=1,
answer_start=0.5,
),
_slot_row(
scene_type="nested",
question_type="proactive",
tp_n=1.0,
nested_role="outer",
nested_group_id=1,
answer_start=1.5,
),
],
unmatched=0,
)
paper = summary["paper_metrics"]["exp_f1"]
assert paper["realtime"]["Global_TP"] == pytest.approx(0.5)
assert paper["realtime"]["num_slots"] == 1
assert paper["realtime"]["Precision"] >= 0.0
assert paper["one_q1a_global"]["Global_TP"] == pytest.approx(3.5)
def test_core_score_float_ignores_leading_counts() -> None:
from vllm_omni.benchmarks.omniinteract_judge import _score_float_from_text
assert _score_float_from_text("The answer covers 3 of the 5 key points, score: 0.6") == pytest.approx(0.6)
assert _score_float_from_text("score: 3") is None
assert _score_float_from_text("no score here 0.9") is None
judge = _GeneratedJudge(["The answer covers 3 of the 5 key points, score: 0.6"])
core = judge.judge_core({"question_text": "q", "gt_answer": "a"}, "ctx", "answer", "(none)")
assert core.score == pytest.approx(0.6)
assert core.parse_source == "llm_float_text"
def test_core_missing_json_score_does_not_use_rationale_number() -> None:
raw = '{"rationale":"Cannot score: 1 required reference is missing."}'
core = _GeneratedJudge([raw]).judge_core({"question_text": "q", "gt_answer": "a"}, "ctx", "answer", "(none)")
assert core.score == 0.0
assert core.parse_source == "llm_parse_failed"
def test_early_missing_score_and_unparsed_flag() -> None:
missing_score = _GeneratedJudge(['{"flag":"Neutral","rationale":"ok"}'])
early = missing_score.judge_early({"question_text": "q", "gt_answer": "a"}, "ctx", "hi")
assert early.category == "unparsed"
assert early.score == 0.0
assert early.parse_source == "llm_parse_failed"
garbage = _GeneratedJudge(["not json and not a known flag"])
unparsed = garbage.judge_early({"question_text": "q", "gt_answer": "a"}, "ctx", "hi")
assert unparsed.category == "unparsed"
assert unparsed.score == 0.0
assert unparsed.parse_source == "llm_parse_failed"
@pytest.mark.parametrize(
("path", "response"),
[
("early", '{"flag":"Neutral","rationale":"Could not evaluate this answer"}'),
("early", '{"flag":"Neutral","score":"invalid","rationale":"bad"}'),
("core", '{"rationale":"Could not evaluate this answer"}'),
("core", '{"rationale":"Cannot score: 1 required reference is missing."}'),
("core", '{"score":"invalid","trigger_phrase":"","spoiler":false,"rationale":"bad"}'),
("partial", '{"rationale":"Could not evaluate this answer"}'),
("partial", '{"score":"invalid","hallucination":false,"rationale":"bad"}'),
],
)
def test_missing_or_invalid_json_scores_are_parse_failures(path: str, response: str) -> None:
slot = {"question_text": "q", "gt_answer": "a"}
judge = _GeneratedJudge([response])
if path == "early":
judgment = judge.judge_early(slot, "ctx", "hi")
elif path == "core":
judgment = judge.judge_core(slot, "ctx", "answer", "(none)")
else:
judgment = judge.judge_interrupted_partial(slot, "answer")
assert judgment.score == 0.0
assert judgment.parse_source == "llm_parse_failed"
def test_explicit_zero_json_score_is_valid_llm_json() -> None:
slot = {"question_text": "q", "gt_answer": "a"}
early = _GeneratedJudge(['{"flag":"Neutral","score":0,"rationale":"zero"}']).judge_early(slot, "ctx", "hi")
core = _GeneratedJudge(['{"score":0.0,"trigger_phrase":"","spoiler":false,"rationale":"zero"}']).judge_core(
slot, "ctx", "answer", "(none)"
)
partial = _GeneratedJudge(['{"score":0,"hallucination":false,"rationale":"zero"}']).judge_interrupted_partial(
slot, "answer"
)
assert early.category == "neutral"
assert early.score == 0.0
assert early.parse_source == "llm_json"
assert core.score == 0.0
assert core.parse_source == "llm_json"
assert partial.score == 0.0
assert partial.parse_source == "llm_json"
@pytest.mark.parametrize("score_literal", ['"NaN"', '"Infinity"', '"-Infinity"', "NaN", "Infinity", "-Infinity"])
def test_nonfinite_json_scores_are_parse_failures(score_literal: str) -> None:
slot = {"question_text": "q", "gt_answer": "a"}
early = _GeneratedJudge([f'{{"flag":"Neutral","score":{score_literal},"rationale":"bad"}}']).judge_early(
slot, "ctx", "hi"
)
core = _GeneratedJudge(
[f'{{"score":{score_literal},"trigger_phrase":"answer","spoiler":false,"rationale":"bad"}}']
).judge_core(slot, "ctx", "answer", "(none)")
partial = _GeneratedJudge(
[f'{{"score":{score_literal},"hallucination":false,"rationale":"bad"}}']
).judge_interrupted_partial(slot, "answer")
assert early.score == 0.0
assert early.parse_source == "llm_parse_failed"
assert core.score == 0.0
assert core.parse_source == "llm_parse_failed"
assert core.trigger_phrase == ""
assert partial.score == 0.0
assert partial.parse_source == "llm_parse_failed"
def test_fingerprint_requires_evaluator_schema_version() -> None:
from vllm_omni.benchmarks.omniinteract_eval import EvaluationInputsFingerprint
payload = {
"annotation_sha256": "a" * 64,
"transcript_sha256": "b" * 64,
"judge_model": "m",
"judge_base_url": "http://127.0.0.1:9",
"judge_max_tokens": 32,
"protocol_source": "proto",
}
assert EvaluationInputsFingerprint.from_mapping(payload) is None
payload["evaluator_schema_version"] = 1
assert EvaluationInputsFingerprint.from_mapping(payload) is not None
payload["evaluator_schema_version"] = 999
matched = EvaluationInputsFingerprint.from_mapping(payload)
assert matched is not None
assert matched.evaluator_schema_version == 999
def test_summarize_counts_parse_sources() -> None:
rows = [
{
**_slot_row(scene_type="multi_turn", question_type="realtime", tp_n=1.0),
"stage_early": {"parse_source": "llm_json"},
"stage_core": {"parse_source": "llm_parse_failed", "trigger_fallback": True},
}
]
summary = _summarize(rows, unmatched=0)
assert summary["judge_parse"]["llm_json"] == 1
assert summary["judge_parse"]["llm_parse_failed"] == 1
assert summary["judge_parse"]["judge_calls"] == 2
assert summary["trigger_fallback_slots"] == 1