Skip to content

[feat] eval: input ergonomics + Evaluator features + bug fixes - #1392

Merged
SolitaryThinker merged 2 commits into
mainfrom
shao/eval-improvements
May 26, 2026
Merged

[feat] eval: input ergonomics + Evaluator features + bug fixes#1392
SolitaryThinker merged 2 commits into
mainfrom
shao/eval-improvements

Conversation

@shaoxiongduan

Copy link
Copy Markdown
Collaborator

Purpose

Bundle of eval-framework improvements layered on top of #1380's FVD consolidation. Three themes:

  1. Input ergonomicssamples_from + as_video collapse the 5–10 LoC of manual sample-list assembly into a single call. The pool's _decode simplifies to one rule (Video instances → populate .frames); the worker unwraps Video → .frames once before metric dispatch. The canonical FVD example (examples/inference/eval/eval_fvd.py) drops from ~85 LoC to ~30.

  2. Evaluator featuresEvaluator(skip_missing_deps=True) keeps the run alive when a metric's optional deps aren't importable (covers declared deps AND lazy transitive imports from setup() or compute(), e.g. vbench's hard decord import, torchcodec's libnvrtc.so.13 requirement). Evaluator.evaluate(samples, metrics=[...]) filters dispatch to a subset of registered metrics per call — useful for keeping one long-lived Evaluator alive across structurally different evaluations (LPIPS on paired corpus + FVD on unequal-cardinality corpus) without burning model loads.

  3. Bug fixes + supporting changes — PyAV decoder fallback in load_video (decord → PyAV → torchvision; torchvision 0.20+ removed read_video, decord has no aarch64 wheels); decord factored into [eval-fast-decode] so [eval] installs on aarch64; FVD accumulate refactored to streaming references (role="reference" tagging + paired-input handling), cache_mode kwarg, cache write moved from accumulate to finalize, merge_from folds both buffers for multi-GPU correctness; worker role-skip rule (per-sample metrics ignore role-tagged samples); audio metric skip messages enumerate the actually-missing keys; audio extraction sugar (extract_audio_track, samples_from(extract_audio=True)) so V2A model outputs feed audio metrics without manual ffmpeg.

Net: +893 / −244 LoC across 16 files.

Changes

Input layer (NEW)

  • fastvideo/eval/io/inputs.py (NEW) — samples_from + as_video. Cardinality-inferred shape: equal |gen| == |ref| zips into paired samples, unequal cardinality role-tags the unmatched references. Symmetric video/reference/audio/reference_audio kwargs; text_prompt(s)/fps/auxiliary_info for vbench-style attachments; extras= catch-all for exotic per-sample keys (physics_iq scenario/view, etc.).
  • fastvideo/eval/io/audio.py (NEW) — extract_audio_track via PyAV. Idempotent on output .wav; NoAudioStreamError for videos with no audio stream.
  • fastvideo/eval/io/__init__.py, fastvideo/eval/__init__.py — re-exports.

FVD streaming refactor

  • fastvideo/eval/metrics/common/fvd/metric.py_real_features: ndarray_real_buf: list[ndarray]; accumulate routes on sample["role"] AND pulls sample["reference"] from paired samples; merge_from folds both buffers; finalize prefers streamed over cached, writes cache on cache-miss; new cache_mode={"off","read","read_write"} kwarg.

Evaluator + worker

  • fastvideo/eval/evaluator.pyEvaluator(skip_missing_deps=False) + evaluate(samples, *, metrics=None). _resolve_metric_names(skip_missing_deps=...) filters explicit names. Only filtered set metrics reset their accumulators (state survives across calls for non-filtered set metrics).
  • fastvideo/eval/worker.pyEvalWorker(skip_missing_deps=False). Catches ImportError/ModuleNotFoundError at setup(), broad Exception at compute()/accumulate() under skip mode (drops the metric for the rest of the run). Role-skip rule for per-sample metrics on role="reference" samples. Video → .frames unwrap before dispatch. metrics= filter threaded through.

Pool simplification

  • fastvideo/eval/pool.py_decode collapsed to one rule: Video instances → populate .frames. Dropped the path-string-under-key and 5-D-tensor squeeze back-compat branches; users wrap paths via as_video() or use samples_from().

Misc

  • fastvideo/eval/io/video.py — PyAV decoder inserted between decord (optional now) and torchvision (read_video removed in 0.20+).
  • pyproject.tomldecord[eval-fast-decode] optional extra. [eval] / [eval-full] now install on aarch64.
  • fastvideo/eval/metrics/audio/{clap_score,desync,imagebind_score,kl_divergence}/metric.py_skip messages enumerate the actually-missing keys (e.g. "missing 'audio'" instead of the ambiguous "missing 'audio' or 'text_prompt'").
  • fastvideo/eval/README.md — Public API section adds samples_from/as_video + metrics= filter; FVD section rewritten to use samples_from; Install table notes [eval-fast-decode] is opt-in for x86_64.
  • examples/inference/eval/eval_fvd.py — rewrite to use samples_from (drops ~55 LoC of manual list assembly).

Test Plan

pre-commit run --files \
  fastvideo/eval/__init__.py \
  fastvideo/eval/io/__init__.py \
  fastvideo/eval/io/audio.py \
  fastvideo/eval/io/inputs.py \
  fastvideo/eval/io/video.py \
  fastvideo/eval/evaluator.py \
  fastvideo/eval/worker.py \
  fastvideo/eval/pool.py \
  fastvideo/eval/metrics/common/fvd/metric.py \
  fastvideo/eval/metrics/audio/clap_score/metric.py \
  fastvideo/eval/metrics/audio/desync/metric.py \
  fastvideo/eval/metrics/audio/imagebind_score/metric.py \
  fastvideo/eval/metrics/audio/kl_divergence/metric.py \
  examples/inference/eval/eval_fvd.py \
  fastvideo/eval/README.md \
  pyproject.toml

Integration testing was done locally (test files not in this PR per current scope):

  • Identical-set FVD invariant verified across N ∈ {4, 8, 16} and num_gpus ∈ {1, 2, 3} on a duplicated LTX2-Distilled-generated sample. FVD = 0.0000 in every config, confirming merge_from correctness across workers.
  • All-metrics integration via Evaluator(metrics=list_metrics(), skip_missing_deps=True) + samples_from(extract_audio=True) on the same identical-set inputs: perfect-match metrics hit their bounds (PSNR=100, SSIM=1, LPIPS=0, FAD=0); per-sample metrics missing required keys produced clean skipped MetricResults; metrics with missing optional deps dropped with one warning per skip.

Test Results

Pre-commit
yapf.....................................................................Passed
ruff (legacy alias)......................................................Passed
codespell................................................................Passed
PyMarkdown...............................................................Passed
mypy.....................................................................Passed
Check for spaces in all filenames........................................Passed
Multi-GPU sweep (5 configs, GB200)
N    GPUs thr  | construct  evaluate  total   | peak GPU (GB)        | invariants
 4   1    4    |  47.2s    200.6s    247.8s   | [40.1]               | OK
 4   2    4    |  85.9s    113.3s    199.1s   | [40.2, 40.1]         | OK
 4   3    4    | 123.3s    143.1s    266.4s   | [40.2, 40.1, 40.1]   | OK
 8   1    4    |  42.2s    371.1s    413.3s   | [40.2]               | OK
 8   2    4    |  92.9s    219.9s    312.8s   | [40.2, 40.1]         | OK

Eval-step scaling: ~85% efficiency at 2 GPUs (1.77× speedup on N=4). 3 GPUs on N=4 = 1.40× (4 samples don't divide evenly across 3 workers, tail straggler dominates). Per-device peak memory is ~40 GB regardless of num_gpus; multi-GPU costs aggregate memory but not per-device pressure.

Checklist

  • I ran pre-commit on all changed files and fixed all issues
  • No tests included in this PR — kept local while the new input API settles; a follow-up can add integration tests once the surface is stable
  • I updated documentation (fastvideo/eval/README.md Public API + Install + FVD sections; common.fvd module docstring)
  • I considered GPU memory impact (per-device peak unchanged from before the PR; verified via multi-GPU sweep)

Bundle of eval-framework improvements layered on top of #1380's FVD work.

Input ergonomics:
- ``samples_from`` + ``as_video`` (``eval/io/inputs.py``): pure helpers
  that turn path-style inputs into the canonical samples list.
  Cardinality-inferred shape — equal ``|gen| == |ref|`` zips into paired
  samples, unequal cardinality role-tags the unmatched references as
  set-style trailing samples. Symmetric ``video`` / ``audio`` /
  ``reference`` / ``reference_audio`` kwargs; ``text_prompt(s)``, ``fps``,
  ``auxiliary_info`` for vbench-style attachments; ``extras=`` catch-all
  for exotic metric inputs (physics_iq ``scenario``, etc.).
- ``examples/inference/eval/eval_fvd.py``: rewrite to use samples_from
  (replaces ~65 LoC of manual sample-list assembly with 3 lines).
- ``pool.py``: collapse ``_decode`` to one rule — populate
  ``Video.frames`` on first decode. Path-string-under-key and 5-D
  back-compat branches deleted; ``samples_from`` always emits ``Video``.
- ``worker.py``: unwrap ``Video → .frames`` once before metric dispatch,
  so per-sample tensor metrics (PSNR/SSIM/LPIPS) never see the wrapper.

FVD streaming refactor:
- ``common.fvd.accumulate``: routes on ``sample["role"]`` (``"reference"``
  → real buffer; else → generated buffer, also pulls ``sample["reference"]``
  for paired inputs). ``_real_buf: list[ndarray]`` replaces the one-shot
  ndarray. ``merge_from`` folds both buffers across workers (multi-GPU
  correctness). New ``cache_mode={"off","read","read_write"}`` kwarg;
  cache write moved from ``accumulate`` (silent foot-gun) to ``finalize``.
- ``worker.py``: role-skip rule — per-sample metrics skip
  ``sample["role"] == "reference"`` so the streaming FVD coexists
  cleanly with LPIPS / PSNR / SSIM / vbench in one Evaluator.

Evaluator features:
- ``Evaluator(skip_missing_deps=True)``: drops metrics whose declared
  or transitively-imported deps aren't available, with a warning per
  skipped metric (covers vbench's hard ``decord`` import, torchcodec's
  ``libnvrtc`` requirement, etc.). Strict mode (default) re-raises.
- ``Evaluator.evaluate(metrics=[...])``: per-call subset filter. Keeps
  one long-lived Evaluator alive across structurally-different
  evaluations — e.g. LPIPS on a paired corpus, FVD on an unequal
  corpus — without burning model loads. Only filtered set-metrics
  reset their accumulators (state for the others survives across calls).

Audio:
- ``eval/io/audio.py`` (NEW): ``extract_audio_track`` via PyAV.
  Idempotent on the output ``.wav`` so parallel workers / repeated
  runs hit a cache.
- ``samples_from(audio=, reference_audio=, extract_audio=)``: explicit
  audio attachment + auto-extract sugar. Auto-extract uses a thread
  pool over PyAV (ffmpeg releases the GIL during decode).
- Audio metric ``_skip`` messages: enumerate the actually-missing key(s)
  instead of ambiguous ``"missing 'X' or 'Y'"`` text.

Other:
- ``eval/io/video.py``: PyAV fallback in the ``load_video`` decoder
  chain — decord (often unavailable) → PyAV → torchvision (read_video
  removed in 0.20+).
- ``pyproject.toml``: factor ``decord`` into ``[eval-fast-decode]`` so
  ``[eval]`` / ``[eval-full]`` install on aarch64 (decord has no aarch64
  wheels).
@mergify mergify Bot added type: feat New feature or capability scope: inference Inference pipeline, serving, CLI labels May 25, 2026
@mergify

mergify Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🔴 PR merge requirements

Waiting for

  • check-success=fastcheck-passed
  • check-success=full-suite-passed
This rule is failing.
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
  • #approved-reviews-by>=1
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the evaluation pipeline to introduce a more flexible, path-based API via samples_from and create_evaluator. Key changes include adding a PyAV-based video decoder as a fallback to decord, implementing automatic audio extraction for video-to-audio metrics, and enhancing the FVD metric with better caching and streaming support. Feedback identifies critical issues in the new IO utilities: the PyAV audio extractor fails to flush the resampler, which can lead to truncated audio; the temporary directory usage for audio extraction lacks cleanup and misses caching opportunities; and the video loader incorrectly assumes the first stream is always video, which could cause crashes on certain file formats.

Comment on lines +91 to +99
for frame in container.decode(in_stream):
frames_out = resampler.resample(frame) if resampler is not None else [frame]
for f in frames_out:
# PyAV needs pts cleared so the encoder reassigns.
f.pts = None
for packet in out_stream.encode(f):
out.mux(packet)
for packet in out_stream.encode(): # flush
out.mux(packet)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The AudioResampler in PyAV can buffer samples, especially when resampling to a different rate. To ensure all audio samples are written to the output file, you must flush the resampler by calling resample(None) after the main decode loop. Without this, the resulting .wav file might be slightly shorter than the source audio.

Additionally, it is safer to create the resampler if either the rate or the format differs from the input, as the pcm_s16le codec typically expects s16 (signed 16-bit) input, while many source videos use fltp (floating point planar).

Suggested change
for frame in container.decode(in_stream):
frames_out = resampler.resample(frame) if resampler is not None else [frame]
for f in frames_out:
# PyAV needs pts cleared so the encoder reassigns.
f.pts = None
for packet in out_stream.encode(f):
out.mux(packet)
for packet in out_stream.encode(): # flush
out.mux(packet)
for frame in container.decode(in_stream):
frames_out = resampler.resample(frame) if resampler is not None else [frame]
for f in frames_out:
# PyAV needs pts cleared so the encoder reassigns.
f.pts = None
for packet in out_stream.encode(f):
out.mux(packet)
# Flush resampler to get remaining buffered frames
if resampler is not None:
for f in resampler.resample(None):
f.pts = None
for packet in out_stream.encode(f):
out.mux(packet)
for packet in out_stream.encode(): # flush encoder
out.mux(packet)

Comment on lines +275 to +278
if isinstance(extract_audio, str | Path):
out_dir: Path = Path(extract_audio)
else:
out_dir = Path(tempfile.mkdtemp(prefix="fv_extracted_audio_"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using tempfile.mkdtemp() without an explicit cleanup mechanism will lead to a leak of directories in the system's temporary folder over time. Since this repository already manages a central cache via get_cache_dir(), it would be more efficient and maintainable to use a stable subdirectory within that cache.

This also enables the idempotency check in extract_audio_track to work across different sessions, significantly speeding up repeated evaluations on the same dataset.

Suggested change
if isinstance(extract_audio, str | Path):
out_dir: Path = Path(extract_audio)
else:
out_dir = Path(tempfile.mkdtemp(prefix="fv_extracted_audio_"))
if isinstance(extract_audio, str | Path):
out_dir: Path = Path(extract_audio)
else:
from fastvideo.eval.models import get_cache_dir
out_dir = get_cache_dir() / "extracted_audio"

Comment on lines +89 to +95
container = av.open(path)
try:
frames = [frame.to_ndarray(format="rgb24") for frame in container.decode(video=0)]
finally:
container.close()
arr = np.stack(frames) # (T, H, W, C) uint8
return torch.from_numpy(arr).permute(0, 3, 1, 2).float() / 255.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In _load_with_pyav, using container.decode(video=0) assumes that the first stream in the container is the video stream. This is not always true (e.g., files with an initial audio or data stream), and attempting to decode a non-video stream as rgb24 will raise an error. It is safer to explicitly select the first video stream.

Also, adding a check for empty frame lists prevents a ValueError when calling np.stack() on corrupted or empty video files.

Suggested change
container = av.open(path)
try:
frames = [frame.to_ndarray(format="rgb24") for frame in container.decode(video=0)]
finally:
container.close()
arr = np.stack(frames) # (T, H, W, C) uint8
return torch.from_numpy(arr).permute(0, 3, 1, 2).float() / 255.0
container = av.open(path)
try:
if not container.streams.video:
raise ValueError(f"No video stream found in {path}")
frames = [frame.to_ndarray(format="rgb24") for frame in container.decode(container.streams.video[0])]
finally:
container.close()
if not frames:
raise ValueError(f"Video file {path} contains no decodable frames.")
arr = np.stack(frames) # (T, H, W, C) uint8
return torch.from_numpy(arr).permute(0, 3, 1, 2).float() / 255.0

@SolitaryThinker

Copy link
Copy Markdown
Collaborator

Hi @shaoxiongduan — automated review from Gob, one of @SolitaryThinker's AI reviewers. Findings aren't all human-verified; ping @SolitaryThinker if anything looks off.

TL;DR

High-quality ergonomics PR with real wins (samples_from collapses the manual sample-list ceremony; Evaluator(skip_missing_deps=) and evaluate(metrics=) are well-designed and well-documented; FVD streaming refactor preserves #1380's contract). Two S2s worth a follow-up: the broad except Exception in EvalWorker.evaluate() swallows real errors like MemoryError/AssertionError when skip_missing_deps=True, and the pool's _decode simplification is a silent breaking change for anyone passing raw path strings under sample keys (no migration shim or runtime check). Zero new tests for +893 LoC is also S2 — the checklist disclosure is honest, but a concrete follow-up issue/PR would lock in the commitment.

This PR is stacked on top of #1380 (approved). No rebase action needed pre-merge.

Verdict: approve-with-followup

  • S0: 0 S1: 0 S2: 3 (3 surfaced) S3: not shown; see review.md

Findings

S2 — Broad except Exception in worker.evaluate() can swallow non-dep errors

Where: fastvideo/eval/worker.py:134-144

The compute/accumulate-time exception handler catches Exception broadly, gated on skip_missing_deps=True. Because MemoryError, AssertionError, RuntimeError (including most CUDA OOM manifestations), and programmer-introduced KeyError/AttributeError are all Exception-derived, they will be silently caught when a user runs with skip_missing_deps=True. The user sees a single logger.warning with the exception type+message; the metric is dropped for the remainder of the Evaluator's life.

The narrow setup()-time catch at worker.py:68 (except (ImportError, ModuleNotFoundError)) is correctly scoped and not the issue.

Why it matters: skip_missing_deps=True is a reasonable default for "score every metric that works in my venv" workflows like your all-metrics sweep. But it conflates two semantically distinct failure modes: (a) "metric's optional dep isn't installed" (legitimate skip) and (b) "metric exploded for a real reason — OOM, bad sample, programming bug" (should surface loudly). With (b) silently degraded to a warning, a user running a multi-day benchmark could end up with FVD silently dropped after the first OOM and only a warning in the log. Re-running with the same config has the same outcome.

Suggested fix (in priority order):

  1. Narrow the catch to (ImportError, ModuleNotFoundError) for the common lazy-import case, keep a broader catch separately but exclude (MemoryError, RecursionError, SystemError) and ideally AssertionError.
  2. Or: keep the broad catch but use logger.exception(...) instead of logger.warning(...) so the traceback is preserved and introspectable.
  3. Or: introduce a separate drop_on_runtime_error flag defaulting to False, distinct from skip_missing_deps.

S2 — Pool _decode simplification is a silent breaking change

Where: fastvideo/eval/pool.py:138-152

The pool's _decode is collapsed to one rule: Video instances get their .frames populated; everything else passes through unchanged. The previous behavior decoded raw path strings under any sample key and accepted 5-D tensors (squeezed to 4-D). Both back-compat branches are removed without a deprecation shim, runtime check, or migration warning.

Why it matters: Any downstream script passing samples=[{"video": "/path/to/clip.mp4", ...}] (raw strings, no as_video() wrap) will now silently flow the string through to the metric, which crashes with a cryptic AttributeError (str has no .frames/.dim()) several layers deep. The PR body discloses the change, and the README/example use the new API throughout — but there's no clean migration error for in-the-wild callers.

Suggested fix: Add a runtime guard in _decode (or _consumer_loop) that raises a clean TypeError for raw-string video/reference keys:

for key in ("video", "reference"):
    v = sample.get(key)
    if isinstance(v, str | Path):
        raise TypeError(
            f"sample[{key!r}] is a raw path string. Wrap it with "
            f"`fastvideo.eval.as_video(...)` or build the samples "
            f"list with `samples_from(...)`."
        )

Cost is one isinstance per sample; the win is a clean migration error instead of an AttributeError deep in metric code. Alternative: re-add a one-release deprecation shim that decodes string paths but warns once per process.

S2 — Zero new tests for +893 LoC of new public API

Where: PR-wide; the [ ] No tests included in this PR checklist item.

The PR adds inputs.py (333 LoC, samples_from/as_video), audio.py (104 LoC, extract_audio_track), the FVD streaming refactor (+115/-66), and two new public Evaluator features (skip_missing_deps=, evaluate(metrics=)). Zero new test files.

The follow-up commitment in the body is appreciated and reasonable for a "settling ergonomics" PR. To lock it in, consider filing the follow-up explicitly (issue or [ ] follow-up checkbox referencing a tracking issue) with a concrete minimum target:

  1. test_samples_from_cardinality.py — equal/unequal cardinality, role-tagging, audio+video combos.
  2. test_evaluator_metrics_filter.py — set-metric state survival across multiple evaluate(metrics=...) calls; unknown-name validation.
  3. test_evalworker_skip_missing_deps.py — mock a metric whose setup() raises ImportError; verify skip+warn.
  4. test_pool_decode_breaking_change.py — if the Finding Dataloader issue. #2 fix lands, verify the migration TypeError.

These four would cover the critical paths in ~300 LoC.


Strengths (acknowledged): the samples_from cardinality-driven shape is genuinely elegant, the Evaluator(skip_missing_deps=) scoping is correct (off by default, group selectors always silent-skip), the evaluate(metrics=[...]) state-survival semantic is well-documented, the FVD cache_mode default preserves #1380's contract, and the PR is unusually transparent (single human commit, no AI trailers, explicit checklist on the test gap, multi-GPU sweep evidence in the body). Nice work.

— Gob (@SolitaryThinker's AI reviewer). Full review archived locally.

The compute/accumulate-time handler in EvalWorker.evaluate() caught
Exception broadly under skip_missing_deps=True, silently dropping
metrics on MemoryError, AssertionError, CUDA OOMs, and programmer bugs.
Narrow to (ImportError, ModuleNotFoundError, FileNotFoundError) to match
the documented intent (lazy-imported deps + missing checkpoints), and
log via logger.exception so the traceback is preserved when a drop does
happen.
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

Hi @shaoxiongduan — automated review from Gob, one of @SolitaryThinker's AI reviewers. Findings aren't all human-verified; ping @SolitaryThinker if anything looks off.

TL;DR

The address commit fixes the worker skip_missing_deps=True broad-catch issue: MemoryError, AssertionError, and other non-missing-dependency runtime failures now propagate instead of being dropped. Two prior S2s remain open (pool raw-string migration behavior and no tests for the new public eval API), and the prior approval is stale because it was submitted on 37e75fd476 before the current bf189133a4 head.

Verdict: approve-with-followup

Severity tally: S0: 0 S1: 0 S2: 3 open (2 persistent + stale approval gate) S3: table only

Prior findings status at bf18913

ID Severity Prior finding Status Notes
F1 S2 Broad except Exception in EvalWorker.evaluate() could swallow MemoryError/AssertionError under skip_missing_deps=True Fixed in fastvideo/eval/worker.py: setup still catches (ImportError, ModuleNotFoundError), and compute/accumulate now catch only (ImportError, ModuleNotFoundError, FileNotFoundError) with logger.exception(...).
F2 S2 Pool _decode simplification is a silent breaking change for raw-string path callers Still open; pool.py still only materializes Video instances and passes raw strings through without a migration TypeError or shim.
F3 S2 Zero new tests for the new public eval API and FVD streaming changes Still open; the delta from 37e75fd476 to bf189133a4 adds no test files.
S3a S3 Torchvision 0.20+ read_video fallback can raise AttributeError on the rare no-av/no-decord path Not touched by this address commit.
S3b S3 [test] extra has redundant av Not touched by this address commit.

Still open

S2 — Pool _decode simplification is still a silent breaking change

fastvideo/eval/pool.py remains unchanged from the prior review: _decode() materializes only Video values and lets every other value pass through unchanged. A raw path string under video or reference can still reach metric code and fail with a downstream AttributeError; please add either a clean migration TypeError or a short deprecation shim.

S2 — Test coverage is still missing for the new public eval API

The address commit only touches fastvideo/eval/worker.py; no test_*.py files were added in the delta. The prior minimum coverage ask still stands: samples_from cardinality/role behavior, Evaluator.evaluate(metrics=...) state survival, skip_missing_deps behavior, and the pool migration error if F2 is fixed.

Pre-merge gate

S2 — Prior approval is stale on this head

The prior approval from SolitaryThinker was submitted on 37e75fd476 at 2026-05-26T20:04:04Z. The current PR head is bf189133a4c6e3c2560854df75b97319f39f447c, with the PR updated after that approval, so the approval is bound to obsolete code and does not cover the address commit. Re-request review on current HEAD before merge.

— Gob (@SolitaryThinker's AI reviewer). Full review archived locally.

@SolitaryThinker
SolitaryThinker merged commit 3668279 into main May 26, 2026
19 of 23 checks passed
@SolitaryThinker
SolitaryThinker deleted the shao/eval-improvements branch May 26, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: inference Inference pipeline, serving, CLI type: feat New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants