Repository navigation
egocap: time-align kept episodes and estimate tracking time - #1457
gareth-morgan-nv wants to merge 38 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
NVdejung
left a comment
There was a problem hiding this comment.
🤖 Automated first-pass review (Claude, for Dejun) — I'll follow up personally where needed.
Summary — This PR adds recorder-side time alignment of kept episodes. Each take now stores per-source qualification and continuity evidence. There is a resampling aligner and a staged writer that never replaces an existing episode, a background worker in the backend that is on by default, a CLI, and synthetic tracking delays in the mock. The aligner and writer are careful and well tested. The blockers are in how the tracking latency is switched on and how it is labelled.
Must fix
station_config.py:140—_resolve_latencynow readsnumeric_sources[].latency.- The station template has always filled that key with 0.05 s (
ego_stations.example.yaml:87,:93). Its header called the value reference data "not consumed here", and the PR description calls it a placeholder. - After the upgrade, every station file copied from the template saves raw tracking
capture_timeas poll time − 50 ms, markedqualified, without anyone asking for it. - The key already means "capture-to-receive latency" in the dataset schema (
data_recording/config.yaml:69).recording_service.py:744writes the poll-based value back under that same key in the sessionconfig.yaml, so a reader that follows the schema would apply it a second time, or against receive time. - Fix: read the estimate from a new, explicit key (for example under
alignment:). Remove the template values or mark them as unmeasured. Don't write the estimate asnumeric_sources[].latency; the timing evidence already holdslatency_s.
- The station template has always filled that key with 0.05 s (
livekit_recording.py:515— LiveKit takes still record"tracking_time": "host_poll", and the comment at L514 says the same. Whenever a latency is applied, this contradicts the mergedevent_time.<source>.meaning: host_poll_minus_latency.tracking_semantics.recorded_timewas updated, but this key next to it was not. Fix: droptracking_timefrom the media evidence (tracking evidence now comes fromtracking_event_time), or derive it from the latency.
Suggestions
- Default index source (
station_config.py:155): as far as I can tell, the default can never align a take.- The default is the first station
video_sourcesentry, which is a CaptureServer camera. Those cameras never get anevent_timeentry, and every configured camera is a required source, so such a station refuses every take. - A LiveKit-only station gets no aligner unless
index_sourceis set. - LiveKit streams only qualify with
capture_monotonic_us, butLIVEKIT_PREVIEW.md:36, which the README now points to, documentsmonotonic_usas the default. - Consider turning alignment on only when
index_sourcenames a recordable stream, checked once at session open.
- The default is the first station
- Recovery (
auto_align.py:77,app.py:443):recover()runs synchronously at startup. It reads every manifest ever recorded and opens every unaligned kept episode.- It re-queues episodes refused for any reason other than qualification at every start, although the docstring (L80–81) says history is not retried. Run it in a thread and record refusals.
- Kept results from
recover_episode(recording_session.py:680) never go through_emit_result, so they are only aligned after a restart.
- Staging folders (
aligned_writer.py:271): a killed run leaves a hidden_aligned.staging-*folder holding a full copy of the episode, and nothing removes it. The analytics skip only folder names ending in_aligned(count_daily_trajectories.py:167), so they count that copy, both after a crash and while an alignment is running. Remove stale staging folders inrecover(), and have the analytics skip hidden folders. - Aligned copies can go out of date:
- The root
index_source(aligned_writer.py:112) is rewritten on every publish, which relabels episodes that were aligned on a different index. - The end-of-day move and rename tools (e.g.
eod_studio_session_move.py:85) update the raw group but not its_alignedsibling.
- The root
- Stale docs:
recording/README.md:130,:146andepisode_layout.py:185still sayconfig.yamlholds onlymeta.alignment.py:22and:69still describe aligning on receive time − latency, which no caller does any more.
- Shared process: alignment runs in the recorder's own process, next to a live take. It uses plain Python loops (
alignment.py:130) and holds every array of the episode in memory (aligned_writer.py:212). Please check this with a long take. The 30 s limit bounds the app's shutdown step, but the process likely still waits for the alignment thread to finish before it exits. - Nits:
- The
_digestcomment (recording_service.py:145) doesn't hold for the backend, which now sets frame types. A take left SEALED or CHECKED across the upgrade will be reported as recorded with a different configuration. write_aligned_episodeis exported and still replaces an existing episode (aligned_writer.py:226), which goes against the new AGENTS.md rule.align_episode(epochs=…)is only used by tests.- In the mock, jitter is also applied to a source with zero delay and then clamped at 0, which biases that source late (
mock_isaac_capture.py:763). - A bounded queue plus an unbounded backlog behaves exactly like one unbounded queue.
- The
- Internal names (no action needed): none found in added lines, file names or commit text, and the fixtures are synthetic. The scan does not cover the 12 binary
.npzfixtures; a pattern search over them found nothing.
Conflicts
- With
gmorgan/TimestampAlignment: the PR is 0 commits behind and a local 3-way merge has no conflicts, so no rebase is needed. - With open PRs:
#1437(8 shared files) and#1426(10 shared files) are stack parents by the same author, with no textual conflicts.- Qualification here depends on
#1437'stiming_evidence()shape, its"sensor_capture"basis string and its clock-mapping epochs. Rebase this PR and re-run its tests after every change to#1437. - The stack also includes
#1428and#1429, both titled as drafts, so this PR cannot reachmainbefore they do.
- Qualification here depends on
Change-by-change (59 files)
| File / area | What changed | Makes sense? |
|---|---|---|
AGENTS.md |
Rule: derived output lives apart from the raw session; an existing derived episode is refused | ✅ (exported write_aligned_episode still replaces) |
README.md |
Mock note: delay flags; plain mock cameras are kept but refused by alignment | |
backend/egocap_web/app.py |
Passes the station latency into ServiceConfig; starts AutoAligner, chains on_episode_result, recovers at startup, waits for queued alignments on shutdown |
recover(); "bounded so shutdown is too" only covers the app's shutdown step |
backend/egocap_web/auto_align.py (new) |
One worker, queue plus backlog, startup recovery, bounded close | |
backend/egocap_web/livekit_recording.py |
tracking_event_time(); continuity from the mapping epoch on each frame; event_time per stream |
tracking_time is now stale (must-fix 2) |
backend/egocap_web/recording_adapter.py |
Frame types on the tracking specs; continuity on samples; latency taken off the poll time | |
backend/egocap_web/recording_session.py |
Latency read per session and passed into conversion; tracking timing evidence on every take, merged with the media evidence | ✅ (imports the private _convert_raw_item) |
backend/egocap_web/station_config.py |
Resolves latency and alignment_index |
|
capture_server/launch.py, server.py, mock/mock_isaac_capture.py |
Delay and jitter flags in milliseconds; the mock reads controller and glove poses at their own delayed instants | ✅ (jitter nit) |
recording/egocap/recording/alignment.py (new) |
Valid range, nearest-frame walk, pose/linear/nearest resampling, continuity and qualification checks | epochs only used by tests |
recording/egocap/recording/aligned_writer.py (new) |
Writer, validation, staged publish with one rename, binding to the kept episode, qualification read from timing |
index_source |
recording/egocap/recording/episode_layout.py |
Manifest schema 4 with per-source continuity |
✅ (SESSION_CONFIG_FILE comment now stale) |
recording/egocap/recording/episode_store.py |
NumericSourceSpec.frame_type |
✅ |
recording/egocap/stations/ego_stations.example.yaml |
Header now says latency is read; adds an alignment: section |
|
recording/recording_service/models.py |
continuity on NumericSample and VideoSample |
✅ |
recording/recording_service/recording_service.py |
Continuity validated and recorded per take in the manifest; digest skips an unset frame type; session config.yaml always written with source declarations |
|
recording/tools/align_episode.py (new) |
CLI that aligns one kept episode | ✅ (usage line omits --manifest-episode) |
tests/backend/* (9 files) |
Aligner worker, recovery and shutdown; app startup/shutdown wiring; latency and continuity on samples; evidence merge; station config | ✅ |
tests/capture_server/* (3 files) |
Delay flags, mock delay and jitter, source-factory wiring | ✅ |
tests/recording/unit/*, tests/recording_service/* (9 files) |
Aligner, writer, publication, binding to the kept episode, reader contract, continuity, session config | ✅ skimmed, not read line by line |
tests/recording/fixtures/* (19 files) |
Synthetic raw inputs, expected trees and scenarios (JSON plus 12 binary .npz) |
✅ JSON spot-checked for names only; binary files not read |
Reviewed at c24fae9 · 59 files · 2026-11-09 13:23 PST
|
🤖 Agent-generated (Claude · claude-sonnet-5-5) Thanks for the review. Addressed in b3fdc2d and 61e516b. Must fix
Suggestions
Tests, lint and the full pre-commit run pass on the pushed head. Still not covered: a long-take measurement, a real station, and the mock cameras over LiveKit. |
|
🤖 Agent-generated (Claude · claude-sonnet-5-5) Correction to my earlier reply: it left out three items. They are fixed in 3fc413d.
Still open and stated in the description: no long-take timing or memory measurement, no real-station run, and the mock cameras over LiveKit. |
|
🤖 Agent-generated (Claude · claude-sonnet-5-5) Second correction: re-reading the review against the code turned up more gaps. Fixed in 1b80c17.
Not done: the long-take timing and memory measurement of the child-process aligner. Also still open, as stated in the description: a real-station run and the mock cameras over LiveKit. |
9c5d6e5 to
4465d5d
Compare
1b80c17 to
0379ba5
Compare
de2236d to
ae325e6
Compare
0379ba5 to
bd329af
Compare
43d2c12 to
0322a69
Compare
bd329af to
0298231
Compare
0298231 to
04acf46
Compare
0322a69 to
f9331b8
Compare
04acf46 to
5460857
Compare
f9331b8 to
53ba8e1
Compare
The aligned episode is written and validated in a staging session beside the aligned one and moved into place with renames, so a reader never sees a half-written episode. An interrupted run leaves only the staging tree (discarded next run) or the previous episode under a .replaced name (restored next run); a refused or failed write leaves the aligned session untouched. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… align a kept episode from it The timing attribute now records, per recorded source, what its saved time means and whether it is a qualified event time (a camera only when it is the sensor's capture event; the tracking sources are host times and are not). qualified_sources reads that and fails closed: only a literal True counts, and a clock mapping alone never qualifies a source. align_kept_episode takes the manifest's continuity evidence, derives qualification from the episode's own timing attribute, and publishes the aligned episode by staging and renames. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Reads the episode's manifest for its continuity evidence, derives qualification from the episode's own timing attribute, and publishes the aligned copy beside its session; an episode that lacks either is refused with the reason. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Covers success equal to the in-place writer, re-publication replacing the whole episode, refusals leaving the aligned session untouched, crashes at each of the two renames and their recovery, leftover staging and replaced trees, and failures after staging started. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…lize publication timing_evidence now keys each camera's event-time entry by the stream's own name and iterates the source's streams (it indexed attributes that belong to the feed). publish_aligned_episode holds an exclusive lock beside the aligned session for the whole publication, so concurrent runs queue instead of discarding each other's staging tree, and its docstring states the contract: a reader sees the previous episode, the new one or, between the two renames of a re-publication, none; never a partial one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…on evidence Covers malformed continuity refused before anything is counted (counts, queue and evidence unchanged), queued copies keeping it, the completeness check with every missing or malformed identity or break list and break rows beyond the written count, and qualified_sources reading only a literal True from the per-source event-time evidence (a clock mapping never qualifies a source), plus the camera and tracking event-time entries the LiveKit recorder writes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Covers concurrent publications of different episodes both landing complete, a second publisher waiting for the lock holder, a refused run creating nothing inside the aligned session, and the episode being absent but never partial between the two renames. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Covers align_kept_episode publishing beside the raw session or to an explicit session, the downsample passed through, refusals for missing or non-literal qualification evidence and for missing or broken continuity (nothing created, raw untouched), and the command's exit status and messages, its manifest-session, aligned-session and downsample options and its argument errors. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…gning align_kept_episode now takes the episode's manifest and refuses unless the episode directory carries that manifest's episode id, the take is finalized and kept, the written rows match the sealed counts, and the location is the manifest's: its routed slot and group stamp when routed, its own index otherwise. The align command can name the manifest's episode index separately for a routed episode. A matching integer in a file name is never taken as identity. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…rs use Builds a raw session with real videos and non-empty root attributes, writes the aligned tree, and checks what a reader of aligned data relies on: the root index source and carried-over attributes, a boolean valid_indices, strictly increasing valid_timestamps, one row per timestamp in every resampled field with no raw twin kept, and per-camera frame indices within the video's frame count. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
check_kept_episode took len() of a zarr array, which zarr 3 does not define, so every kept-episode alignment with a counted source raised TypeError. Use the shape, as the dataset reader does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Covers a manifest of another episode with the same index (and an unstamped episode) refused before any output, non-finalized and non-kept manifests, written counts that differ from the sealed counts, an unrouted index mismatch, an unreadable episode, routed success with a slot that differs from the recording index, routed refusals (wrong slot, missing or foreign route stamp, indexing by the recording index) and the command's --manifest-episode option. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…rison Rename test helpers, test names and a fixture key to state the expected output directly, and reword two alignment.py comments to match. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Drop the lock and the set-aside/recovery pair; an existing aligned episode is refused and a crashed run leaves only a staging tree that is discarded. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…latency A station's numeric_sources latency now reaches the recording config, and a source without a qualified event time but with a latency is aligned on receive time minus it; one with neither is refused. Tracking samples are stamped on the host clock, so their continuity is scoped to the capture connection and the aligner accepts that scope. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
… isolate staging, align kept episodes - Take a configured latency off the host poll time once, when a tracking sample is converted, and record the source's time as an estimate with the latency used; the aligner uses saved times unchanged. - Refuse a kept episode whose source with sealed rows is missing. - Build each aligned episode in its own staging directory. - Align each kept episode in the background when the station enables it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…ng evidence with any media - Close the background aligner after the recorder, so the last kept episode is queued, and bound the wait. - Bound the queue, recover unaligned kept episodes from every session whose evidence allows alignment, skip an already-aligned episode, and log under the isaaccapture hierarchy. - Record the tracking sources' timing evidence from the recording coordinator, whether or not the take has LiveKit video. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
--controller-delay-ms, --glove-delay-ms and --delay-jitter-ms make a source's content that old at the poll (seeded bounded jitter), so a configured latency can be checked against a known truth. Mock-only; default is no delay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…an episode recorded - Hold alignment work beyond the queue's room in a backlog the worker drains, so none is dropped. - A source takes part in alignment only if the episode recorded rows for it; an absent or empty optional source no longer vetoes the alignment or its recovery. - Correct the station example's latency comment and state the mock quickstart's limits. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
- Read the estimated tracking delay from alignment.tracking_latency_s only; a station file's numeric_sources latency is unused, and the estimate is no longer written under the dataset's latency key. The template ships no value. - Drop the stale tracking_time entry from the LiveKit timing evidence. - Hold aligner jobs in one unbounded queue; skip hidden staging folders in the daily analytics. - Do not jitter a mock source that has no delay. - Correct stale alignment, config and CLI documentation. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…aligned copies in step - Run each alignment as a low-priority child process of the command-line aligner, so it neither shares the recorder's interpreter nor outlives a shutdown; exit 1 is a refusal, 2 a retryable failure. - Scan for unfinished work on a thread at worker start, remember refusals in a hidden marker beside the aligned copy, and remove staging directories left by a run that was killed long ago. - Queue alignment for every kept episode, including one kept by recovering an interrupted take. - Refuse to publish an episode on a different index camera into an existing aligned session. - Move an aligned copy with its session and apply task metadata updates to it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…es out of the digest - Tell the aligner which cameras each session records when it opens, and queue no episodes (with one warning) while the configured index camera is not among them. - A source's frame type no longer enters the configuration digest, so setting one does not make a take left SEALED or CHECKED look like a different configuration. - Document that automatic alignment needs the capture-time stream basis. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…pture times; never replace an aligned episode - Alignment is on only when alignment.index_source is set (no default camera), and is skipped with one warning for a session whose index camera, or any recorded camera, lacks a sensor capture time. - write_aligned_episode refuses an existing episode like the staged publish; drop the unused epochs argument of align_episode; expose convert_raw_item instead of a private name. - Correct the recording README on what config.yaml carries. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
…ing times Every take records the tracking sources' timing evidence, with or without media, so the next take after a discarded or interrupted one has that evidence call; it still inherits nothing from the earlier take. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Gareth Morgan <gmorgan@nvidia.com>
5460857 to
087336e
Compare
53ba8e1 to
0a4625a
Compare
Description
Stacked on the egocap timing PR (#1459): this branch targets that PR's branch, so the diff shows only the recorder-side work.
Kept episodes are time-aligned in the background after a take is finalized. Each source is resampled onto one index camera's frame times and written as an aligned copy beside the raw session; the raw recording is never modified.
What changed
valid_indices,valid_timestamps,aligned_frame_indicesand<field>_interparrays, copied attributes and sidecars, and hard-linked videos. A source that cannot be aligned (single sample, backwards timestamps, no common range) is refused with the reason rather than skipped.alignment.tracking_latency_sin the station file) is taken off the host poll time once, when a sample is converted. The source's timing evidence records it as an estimate with the latency used. A source with no configured delay stays unqualified; zero is a valid delay. The station template ships no value, and the estimate is not written under the dataset'slatencykey. The aligner uses saved times unchanged.alignment.index_source(there is no default;alignment.enabled: falseturns it off). Each alignment runs as a low-priority child process of the command-line tool, so it does not share the recorder's interpreter and can be stopped. Alignment is skipped, with one logged warning, for a session whose index camera or any recorded camera lacks a sensor capture time (a LiveKit stream withcapture_monotonic_us). Failures are logged with the reason; a refusal leaves a hidden marker beside the aligned copy so it is not retried at every start (delete it to retry), while a retryable failure is. At startup the worker scans, on a thread, for kept episodes with no aligned copy whose evidence allows alignment, and removes staging directories left by a run killed more than a day ago. Shutdown drains the queue after the recorder closes, with a bounded wait, then stops any alignment still running; nothing partial is published.recording/tools/align_episode.pyaligns one kept episode by hand (exit 0 published, 1 refused, 2 retryable failure). An aligned session keeps one index camera; publishing an episode on a different one is refused. The end-of-day move and task-description tools keep a session's_alignedcopy in step.capture-server --mockaccepts--controller-delay-ms,--glove-delay-msand--delay-jitter-ms, which make a source's content that old at the poll (seeded, bounded jitter).Type of change
Testing
Run on the final head from
examples/egocap:tests/recording: 677 passed, 3 skipped; 100% line and branch coverage ofrecording/egocap.tests/backend: 740 passed.tests/capture_server: 801 passed.tests/recording_service: 92 passed, 100 skipped (the skipped tests needEGOCAP_REAL_EPISODEset to a recorded episode).ruff checkandruff format --check;SKIP=check-copyright-year pre-commit run --all-files.Known limitations / follow-ups
alignment.tracking_latency_sto a value measured on the station. Physical timing accuracy is not claimed.capture-server --mockcameras carry no such evidence, so automatic alignment refuses those takes and logs why; running the mock cameras through LiveKit is a follow-up.Checklist
SKIP=check-copyright-year pre-commit run --all-filesgit commit -s) per the DCOPR description drafted with Claude Sonnet 5.5.