Skip to content

fix(checks): include lead-in gap in keyframe_interval max_keyframe_gap_s when stream starts mid-GOP #699

Description

@shobhitagnihotri69

Summary

In src/hflow/checks.py, _keyframe_interval_value computes max_keyframe_gap_s by appending the stream tail timestamp (stamps_ns[-1]), but omits the stream head (stamps_ns[0]):

https://github.com/Hebbian-Robotics/hflow/blob/main/src/hflow/checks.py#L2255-L2261

When a camera stream starts mid-GOP (first_frame_is_keyframe == 0), the initial span of frames before the first keyframe is never measured, causing max_keyframe_gap_s to severely under-report the maximum seek/decode gap. In extreme cases where the only keyframe occurs at the end of the recording, it reports max_keyframe_gap_s = 0.0.


Problem & Invariant Violation

The documented invariant for keyframe_interval states:

"A keyframe is where a decoder can start, so the longest gap between them bounds both random access and frame-accurate cutting without re-encoding. Measured over true log time rather than the remuxed MP4's synthesized constant-rate clock, so a recording gap legitimately widens the reported gap -- a cut across that gap really does land somewhere else." (checks.py:2276-2280)

The check aims to find the longest span of footage without a keyframe across the stream, which is why it appends stamps_ns[-1] to measure the trailing gap from the last keyframe to the end of the stream.

However, because stamps_ns[0] is omitted:

  1. If a 10-second recording starts mid-GOP with its first keyframe at t = 7.0s and subsequent keyframes at t = 8.0s and 10.0s:
    • The first 7 seconds have no keyframe — random access and cutting anywhere in [0.0s, 7.0s] have a 7-second gap.
    • But max_keyframe_gap_s only evaluates gaps between keyframes and the tail (diff([7.0s, 8.0s, 10.0s])), reporting 2.0s instead of 7.0s.
  2. If a stream has only one keyframe located at the final frame (t = 5.0s), diff([5.0s, 5.0s]) = [0], returning max_keyframe_gap_s = 0.0s (asserting a perfect zero seek gap when the entire video had no keyframe prior to the end).
  3. Curation queries filtering on seekable footage (e.g. SELECT episode_id FROM episodes WHERE "/cam/max_keyframe_gap_s" <= 1.5) silently admit footage with multi-second lead-in dropouts.

Root Cause

In src/hflow/checks.py:2255-2261:

    if name == "max_keyframe_gap_s":
        if not inter.keyframe_indices:
            raise ValueError(f"{name!r}: fact named for a keyframe-less camera")
        keyframe_stamps_ns = stamps_ns[list(inter.keyframe_indices)]
        gaps_ns = np.diff(np.append(keyframe_stamps_ns, stamps_ns[-1]))  # <-- bounds tail, but omits stamps_ns[0]
        positive_gaps_ns = gaps_ns[gaps_ns > 0]
        return float(np.max(positive_gaps_ns) / 1e9) if len(positive_gaps_ns) else 0.0 

Minimal Reproduction

Run this standalone script (mirroring src/hflow/checks.py:2255-2261):

import numpy as np

# Camera stream lasting 5.0 seconds (t=0.0s to 5.0s)
stamps_ns = np.array([0, 1_000_000_000, 2_000_000_000, 3_000_000_000, 4_000_000_000, 5_000_000_000])

# Scenario A: Camera starts mid-GOP; first keyframe arrives at t=4.0s, stream ends at t=5.0s
keyframe_indices = [4]

# Current hflow implementation (checks.py:2258-2261):
keyframe_stamps_ns = stamps_ns[list(keyframe_indices)]
gaps_ns = np.diff(np.append(keyframe_stamps_ns, stamps_ns[-1]))
positive_gaps_ns = gaps_ns[gaps_ns > 0]
reported = float(np.max(positive_gaps_ns) / 1e9) if len(positive_gaps_ns) else 0.0

print(f"Scenario A - Reported: {reported:.1f}s | Expected: 4.0s (gap from t=0.0s to t=4.0s)")

# Scenario B: Extreme case where the only keyframe is the final frame (t=5.0s)
keyframe_stamps_ns_b = stamps_ns[[5]]
gaps_ns_b = np.diff(np.append(keyframe_stamps_ns_b, stamps_ns[-1]))
positive_gaps_ns_b = gaps_ns_b[gaps_ns_b > 0]
reported_b = float(np.max(positive_gaps_ns_b) / 1e9) if len(positive_gaps_ns_b) else 0.0

print(f"Scenario B - Reported: {reported_b:.1f}s | Expected: 5.0s (gap from t=0.0s to t=5.0s)")

Activity

  1. kstonekuan commented on Oct 7, 2026

    @kstonekuan
    Contributor

    Thanks, but leaving the lead-in out is deliberate. max_keyframe_gap_s bounds the seek cost between decodable points. Frames before the first keyframe have no keyframe to decode from at all, which is a different defect, and first_frame_is_keyframe = 0 already records it. The trailing span after the last keyframe is decodable footage with a seek cost, so it counts. Folding the lead-in into the gap would mix the two facts.

  2. shobhitagnihotri69 commented on Oct 7, 2026

    @shobhitagnihotri69
    ContributorAuthor

    Thanks for the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions