Skip to content

fix(lerobot): stop flooring a fractional source fps - #461

Merged
kstonekuan merged 3 commits into
Hebbian-Robotics:mainfrom
chiruu12:fix/458-fractional-fps
Sep 7, 2026
Merged

fix(lerobot): stop flooring a fractional source fps#461
kstonekuan merged 3 commits into
Hebbian-Robotics:mainfrom
chiruu12:fix/458-fractional-fps

Conversation

@chiruu12

@chiruu12 chiruu12 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #458

meta/info.json is allowed a fractional fps and _parse_dataset_information stores it as
one, but the single call site floored it with int(source_archive.fps). A corpus
declaring 29.97 was converted as if it were 29.

The keyframe interval is the primary half. _transcode_mp4_to_h264 computes
round(gop_seconds * frames_per_second), which is 30 at the declared rate and 29 at the
floored one, so the file carried a 29 frame GOP while provenance/v1 stamped
gop_seconds as 1. That field is recorded as what was actually used and nothing checks
it, which is what #376 is open about. This was one of the ways it could already be false.

The time axis is the visible half. Log times are frame_index * 1e9 / frames_per_second,
so the whole episode stretches:

frame     1: source    0.0334s   written    0.0345s   drift +0.0011s
frame    30: source    1.0010s   written    1.0345s   drift +0.0335s
frame   900: source   30.0300s   written   31.0345s   drift +1.0045s
frame  5400: source  180.1802s   written  186.2069s   drift +6.0267s

Both consumers already took a float, so the fix is the int() at the call site and the
annotation that forced it.

CONVERTER_VERSION moves to v8, and the reason is resume rather than bytes.
_episode_identity_matches compares dataset, revision, episode index, camera keys,
converter version and gop_seconds. It never looks at fps, so an episode delivered before
this fix with the stretched time axis still matches on every field and the resume path
would hand it back as completed work. The bump is what makes those episodes fail identity
and be re-converted. This differs from the bumps in #390 and #403: those moved every
episode's content id, this one changes bytes only for fractional-fps sources, and an
integer-fps corpus produces identical output either way.

Tests, in the order they were asked for:

  • the declared fps reaches conversion untruncated
  • the log times come from the declared rate, with the floored value asserted absent
  • the rate the transcoder receives yields a keyframe interval of 30 rather than 29
  • an episode stamped with the previous converter version is refused by reuse

All four fail on the commit before the fix. The corpus helper now takes an fps so the
end-to-end import runs at 29.97 rather than the integer 30 every existing fixture uses,
which is why this survived: nine fixtures, all at 30, and int(30.0) is 30.

tests/test_lerobot_converter.py tests/test_lerobot_metadata_refusals.py tests/test_lerobot_export.py: 107 passed. ruff check, ruff format --check and
ty check are clean.

Copilot AI lite review requested due to automatic review settings September 7, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@kstonekuan kstonekuan 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.

Both halves land and both are held, which is the thing I most wanted to see here. They fix different problems: dropping the truncation fixes new conversions, the version bump is what stops resume handing back the episodes already written with a stretched time axis. A PR that did only the first would have looked complete and left every existing delivery broken.

The mutation sweep says neither can be quietly undone:

truncation restored at the call site   FAILED test_fractional_fps_reaches_conversion_untruncated
                                       FAILED test_fractional_fps_reaches_the_transcoder_for_the_keyframe_interval
                                       FAILED test_fractional_fps_sets_the_log_times_from_the_declared_rate
version bump reverted, fix kept        FAILED test_converter_version_bumped_with_the_label_support
                                       FAILED test_reuse_refuses_an_episode_written_before_the_fps_fix
log time truncated at use instead      FAILED test_fractional_fps_sets_the_log_times_from_the_declared_rate

The middle one is the one that matters. Reverting v8 to v7 while keeping the fix turns two tests red, so the bump is pinned behavior rather than a number someone can tidy away next time they read the constant. The third confirms the rate reaches the division rather than being truncated one layer down.

Validated: full gate clean, 1636 passed / 6 skipped.

Two things done well beyond the ask. test_fractional_fps_reaches_the_transcoder_for_the_keyframe_interval asserts 30 and then asserts it differs from the floored 29, so it fails if the two ever coincide rather than passing on a number that happens to be right. And the CONVERTER_VERSION comment explains the bump as a resume-invalidation rather than as a byte change, which is the distinction I raised on the issue: integer-fps corpora produce identical output either way, so a reader comparing content ids would otherwise conclude the bump was unnecessary.

Merging.

Worth noting for #376, which you touched in a test docstring: this closes one specific way gop_seconds could already be false in provenance. It does not close the general case, which is still that the field is stamped as "actually used" and never verified.

@kstonekuan
kstonekuan merged commit 1c8f16e into Hebbian-Robotics:main Sep 7, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A fractional fps in meta/info.json is floored before conversion, stretching the episode's time axis

3 participants