fix(scanner): read recording.db, the format the recorder writes - #17
Merged
Conversation
`catalog scan` globbed `capture.db` and queried a `capture` table and an `events` table. openadapt-capture PR #28 replaced that bespoke format on 2026-07-17 with a SQLAlchemy database named `recording.db`. Nothing a user records today matched the glob, so the scanner found zero recordings on a machine full of them, and the catalog could not be populated at all. The rename alone would not have fixed it. The two formats share no table and no column: capture.started_at -> recording.timestamp capture.ended_at -> (gone; derive from the newest event) capture.screen_width -> recording.monitor_width capture.screen_height -> recording.monitor_height events -> action_event screenshots/*.png -> screenshot rows holding png_data blobs Pointed straight at a current recording, the old reader logged "no such table: capture" and registered an entry with the directory mtime as its date and None for duration, frames and events. So this reads the current schema: * `recording` for the start timestamp, task description, platform and display geometry, matching what `CaptureSession.load` selects; * `action_event` rows for the event count, which is what `events` counted; * `screenshot` rows for the frame count, since frames are blobs now, falling back to screenshots/*.png only when the database cannot answer; * the newest timestamp across the event tables for the duration, because there is no ended_at column. Legacy `capture.db` is not read. openadapt-capture's own `scripts/migrate_legacy_capture.py` states that current code cannot load it, and the screenshots workflow already treats it as unusable. A legacy directory now prints the conversion command instead of registering a shell with no date and no counts, and `_extract_recording_info` raises rather than returning one. Also fixes `catalog register`, which raised TypeError for every directory that got past the path check: `Recording` names the field `id` and `register_recording` takes `recording_id`, and TypeError is not in the except clause there. The tests read the two recordings openadapt-capture commits under examples/captures, which CI now checks out. Building a recording.db here to match this repository's idea of the schema would only prove the reader agrees with the test author, which is how the drift went unnoticed for six weeks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
📸 Generated Screenshots PreviewScreenshots have been generated. Download the artifacts to preview them. Generated Files:
|
abrichr
added a commit
that referenced
this pull request
Aug 28, 2026
`load_real_capture_data` opened `<recording>/capture.db` and ran `SELECT * FROM capture LIMIT 1`, then read `started_at`, `ended_at`, `screen_width` and `screen_height`. openadapt-capture PR #28 replaced that format on 2026-07-17 with `recording.db`, a SQLAlchemy database whose `recording` table shares no column with it: capture.started_at -> recording.timestamp capture.ended_at -> (gone; derive from the newest event) capture.screen_width -> recording.monitor_width capture.screen_height -> recording.monitor_height events -> action_event screenshots/*.png -> screenshot rows holding png_data blobs So the benchmark viewer raised FileNotFoundError on every recording the current recorder has produced. #17 fixed this in the catalog scanner and left this file alone, because the loader also requires an `episodes.json` that no committed recording carries. There is now one reader. `openadapt_viewer/recording_db.py` owns the filename, the column names, the derived end time and the refusal to read a legacy capture; the scanner and the benchmark loader both go through it. Adding a second, divergent translator here would have put format knowledge in two places in a repository that does not own the format. Legacy support is deliberately absent for the same reason: openadapt-capture ships `scripts/migrate_legacy_capture.py` and states that current code cannot load a legacy capture, so a `capture.db` directory now raises LegacyCaptureError carrying the conversion command, matching what the scanner prints. The run's `config` gains `frame_count`, `event_count` and `task_description`, counted from the database. They are what makes a loaded run checkable against its source file rather than only against itself. tests/test_real_data_loader.py loads the two recordings openadapt-capture commits under examples/captures and asserts the values the loader reports, queried from the same file. Twenty of its twenty-one tests fail without this change. It does not verify the episodes half, and says so in its own docstring. The loader's `episodes.json` dialect has string `steps`, an `episode_id` like `episode_001` and a `screenshots.key_frames` list. openadapt-ml's pipeline serialises `EpisodeExtractionResult`, whose `steps` are objects, whose `episode_id` is a UUID, and which has no `screenshots` key. No producer writes the dialect this parser reads, so the fixture used is the one already committed here, and every assertion that matters is made against recording.db instead. examples/capture.db goes with it: a zero-byte file, committed, named after the retired format and read by nothing. sqlite3.connect creates a database at any path it is handed, which is how a file like that appears. Both lookups here use is_file() before connecting.
abrichr
added a commit
that referenced
this pull request
Aug 28, 2026
The first pass verified everything against openadapt-viewer==0.2.0 from PyPI and documented two bugs that main has already fixed. #15 replaced the hardcoded DEFAULT_CAPTURE_PATH with $OPENADAPT_CAPTURE_RECORDING, and #17 and #19 moved the readers from the pre-#28 capture.db to recording.db. Reciting the wheel's behaviour also tripped tests/test_no_hardcoded_paths.py, which bans an absolute home path in any tracked file including documentation. Everything is now re-run against an editable install of this branch: the demo screenshot, the component signatures in docs/COMPONENTS.md, the benchmark invocation, and the offline behaviour with cdn.jsdelivr.net aborted in a headless browser. The legacy-capture path replaces the stale hardcoded-path bullet, because it is a live failure: LegacyCaptureError subclasses FileNotFoundError, so the fallback in generate_benchmark_html catches it, load_benchmark_data returns a run with zero tasks, and the CLI prints "Generated:". The migration command that recording_db raises never reaches the person who needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 28, 2026
* docs: rewrite the README around one worked example The old README opened with six bolded-lead feature bullets, then documented capabilities this package does not have. The audio-transcript section described a feature of openadapt-capture: the string "transcript" appears nowhere in openadapt-viewer's source or in the 0.2.0 wheel. The four README screenshots were produced by scripts/generate_readme_screenshots.py, which calls openadapt_capture.visualize.html.create_html, so they showed another package's output with captions pointing at a transcript panel that is not in the images. The synthetic-demo section told the reader to open synthetic_demo_viewer.html and linked SYNTHETIC_DEMOS_EXPLAINED.md; neither file exists in the repository. SEARCH_FUNCTIONALITY.md was linked at the root and lives under docs/. Everything in the new file was run against openadapt-viewer==0.2.0 installed from PyPI into an empty venv, and the pasted output is that run's output. The component signatures move to docs/COMPONENTS.md, read out of the installed package with inspect.signature rather than copied from the source tree. The screenshot is the real output of `openadapt-viewer demo`, regenerated by scripts/generate_demo_screenshot.py. Four behaviours that the old README's "works offline, no server required" claim covered up are now written down: the page fetches Alpine from jsdelivr and the task list does not render without it; `benchmark` with no --data resolves an absolute path on one developer's machine; the capture viewer emits repo-relative href/src for episode_timeline.css and .js; and __version__ still reports 0.1.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: describe main, not the released 0.2.0 The first pass verified everything against openadapt-viewer==0.2.0 from PyPI and documented two bugs that main has already fixed. #15 replaced the hardcoded DEFAULT_CAPTURE_PATH with $OPENADAPT_CAPTURE_RECORDING, and #17 and #19 moved the readers from the pre-#28 capture.db to recording.db. Reciting the wheel's behaviour also tripped tests/test_no_hardcoded_paths.py, which bans an absolute home path in any tracked file including documentation. Everything is now re-run against an editable install of this branch: the demo screenshot, the component signatures in docs/COMPONENTS.md, the benchmark invocation, and the offline behaviour with cdn.jsdelivr.net aborted in a headless browser. The legacy-capture path replaces the stale hardcoded-path bullet, because it is a live failure: LegacyCaptureError subclasses FileNotFoundError, so the fallback in generate_benchmark_html catches it, load_benchmark_data returns a run with zero tasks, and the CLI prints "Generated:". The migration command that recording_db raises never reaches the person who needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: drop two capability claims the generator does not implement The offline section said benchmark --standalone embeds Plotly. The flag is dead: cli.py passes it to generate_benchmark_html, which passes it into _generate_viewer_html, whose body never reads it. PageBuilder is built with include_alpine=True and no include_plotly, so the Plotly branch never runs and the two renderings are byte identical. Claiming an offline escape hatch inside the section about not being offline-safe is the worst place for it. dark_mode is stored in PageBuilder.__init__ and never read again. The sun button in the header is what switches the palette. Say that instead. Add the screenshot-path bullet: the benchmark viewer writes a real recording's screenshots as absolute local paths, so mailing that file loses the images. Only demo inlines them as data URIs. The demo's pass and fail values come from an unseeded random.random(), so 90.0% is not impossible, only unlikely. Soften the caption. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
openadapt-viewer catalog scanfound zero recordings on a machine full of them.The scanner globbed
capture.dband queried acapturetable and aneventstable. openadapt-capture PR #28 replaced that bespoke format on 2026-07-17 with a SQLAlchemy database namedrecording.db. Nothing the current recorder writes matched the glob.Against the two recordings openadapt-capture commits under
examples/captures, onmain:It was not a one-word fix
Renaming the glob would have registered directories with no date, no duration and no counts, which is the second line above. The formats share no table and no column:
capture.dbrecording.dbcapture.started_atrecording.timestampcapture.ended_atcapture.screen_width/screen_heightrecording.monitor_width/monitor_heighteventsaction_eventscreenshots/*.pngscreenshotrows holdingpng_datablobsWhat changed
scanner.pyreads the current schema:recordingfor the start timestamp, task description, platform and display geometry, matching whatCaptureSession.loadselectsaction_eventrows for the event count, which is whateventscountedscreenshotrows for the frame count, falling back toscreenshots/*.pngonly when the database cannot answerended_atcolumnIt reads with
sqlite3rather than importingCaptureSession. Indexing needs two row counts and a few scalars; installing openadapt-capture for them would pull mss, sounddevice, soundfile, matplotlib, sqlalchemy, alembic, numpy and the platform accessibility stack into an HTML generator. Anything that needs decoded frames should importCaptureSessioninstead of extending the scanner.cli.pyalso fixescatalog register, which raisedTypeErrorfor every directory that got past the path check.Recordingnames the fieldid,register_recordingtakesrecording_id, andTypeErroris not in theexceptclause there, so the command ended in a traceback.Legacy captures are not supported, on purpose
openadapt-capture's own
scripts/migrate_legacy_capture.pysays it plainly: "CaptureSession.loadreads onlyrecording.db. Legacy captures are therefore unloadable by current code." The migration exists, andscreenshots.ymlalready reports acapture.dbdirectory as unusable rather than missing.So a legacy directory now prints the conversion command and is skipped, and
_extract_recording_inforaises rather than returning a shell:A directory holding both is indexed from
recording.dbwith no warning, so converting in place works.Proof against the real recordings
Not a fixture written here.
openadapt-viewer catalog scan --capture-dir <openadapt-capture>/examples/captures, thencatalog list --json:[ {"id": "turn-off-nightshift", "created_at": 1767229200.0, "duration_seconds": 19.0, "frame_count": 20, "event_count": 20, "task_description": "Synthetic display-setting documentation fixture", "metadata": {"platform": "synthetic", "screen_width": 960, "screen_height": 540, "pixel_ratio": 1.0}}, {"id": "demo_new", "created_at": 1767225600.0, "duration_seconds": 13.0, "frame_count": 14, "event_count": 14, "task_description": "Synthetic calculator documentation fixture", "metadata": {"platform": "synthetic", "screen_width": 960, "screen_height": 540, "pixel_ratio": 1.0}} ]The counts match the table in that repository's
examples/captures/README.md: 20 frames and 20 actions forturn-off-nightshift, 14 and 14 fordemo_new.Tests
tests/test_scanner_recording_db.pyreads those two committed recordings. 17 of its 21 tests fail against the scanner onmainand all 21 pass here. Every expectation is queried from the recording.db the scanner read, written out in the test rather than shared with the scanner, so it asserts the catalog reports what the file says.test.ymlnow sparse-checks outopenadapt-capture/examples/capturesand points the suite at it, the wayscreenshots.ymlalready does. Without that the whole module would skip and verify nothing, so one test fails underGITHUB_ACTIONSwhen the recordings are absent. Tonight a PR with 197 passing tests shipped a real break because every test stubbed the boundary under test; a recording.db built here to match this repository's idea of the schema would have proved only that the reader agrees with the test author.tests/test_screenshot_generation.py::test_captures_existasserted oncapture.dbat the checkout root. Both halves were stale: the captures live underexamples/captures, and the file isrecording.db.Not fixed here
viewers/benchmark/real_data_loader.pyreads the same legacy schema. It also needs anepisodes.jsonthat no committed recording carries, so there is no real input to verify a fix against. Left alone rather than changed blind.🤖 Generated with Claude Code