Add CSV notes export - #19
Closed
emlcpfx wants to merge 8 commits into
Closed
Conversation
Introduce the first automated test infrastructure for FrameDeck. There were previously no tests; CI only ran compileall. - tests/conftest.py forces Qt's offscreen platform plugin and a single shared QApplication so widgets can be constructed, rendered and pixel-probed with no display server. FRAMEDECK_PROFILE_ROOT is redirected to a temp dir so tests do not write into the user's Documents folder. - tests/helpers.py provides render_widget_to_image / probe_pixel plus synthetic media generators (solid MP4 via PyAV, numbered PNG sequence, flat EXR via OIIO) so later tests need no external assets. - tests/test_smoke.py verifies the harness end to end: offscreen render + pixel probe, FrameDeck package imports, and each media generator produces a decodable/readable file. 10 tests pass locally. - requirements-dev.txt pins pytest and pypdf. - pytest.ini sets testpaths. - ci.yml gains a headless pytest job (installs Qt offscreen system libs plus runtime + dev requirements). Also adds doc/PLAN-reviewapp-parity.md: the staged plan for porting reviewapp features into FrameDeck across separate PRs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177bi2WkPjrGytrarheKFDc Claude-Session-Id: 45a11a50-85fe-4349-b853-e1db9cd20276
The Sketch annotation store had undo (create/move/erase) but no redo. Add a snapshot-based redo that reapplies the most recently undone action. - widgets/annotations.py: add redo_history; route the four action-record sites through _record_action(), which appends to the undo stack and clears redo (a fresh edit invalidates redo, standard semantics). undo() now snapshots the full pre-undo stroke state onto the redo stack; redo() restores it in place (mutating the existing dict so held references stay valid) and re-pushes the action onto the undo stack. - widgets/viewer.py: redo_strokes() mirrors undo_strokes(). - widgets/__init__.py: "Redo Note" edit-menu action bound to Ctrl+Shift+Z and Ctrl+Y. Tests: tests/test_annotation_redo.py covers create redo, multi-step undo-then-redo ordering, redo-stack invalidation on a new edit, erase undo/redo, and empty-stack no-op. 5 tests pass (15 with the harness suite). Depends on the test harness (PR D-Mad#7); branched on top of it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177bi2WkPjrGytrarheKFDc Claude-Session-Id: 45a11a50-85fe-4349-b853-e1db9cd20276
FrameDeck kept annotations only in memory, so pencil/text notes were lost when switching sources or closing the app. Persist them to JSON sidecars so they survive a reload -- the first piece of stronger session/project state, and the storage foundation the comment sidebar (planned) will build on. - widgets/annotations.py: Sketch.serialize() returns a JSON-safe snapshot (frame -> strokes, empty frames omitted); deserialize() restores it, converting list coordinates/colours back to tuples and resetting undo/redo history. - widgets/notestore.py: read/write sidecars under the FrameDeck profile dir (<profile>/framedeck/notes/<stem>_<hash>.fdnotes.json), keyed by a hash of the absolute source path. Empty notes remove a stale sidecar; unreadable/foreign files load as empty. - widgets/__init__.py: openMedia saves the outgoing source's notes before the viewer is cleared and loads the incoming source's notes after it opens; closeEvent saves on exit. All hooks are defensive (a notes error is logged, never breaks media loading). Tests: tests/test_notes_persistence.py covers JSON serialize/deserialize round-trip, history reset on load, sidecar save/load round-trip, empty-removes- sidecar, missing-clears-sketch, and deterministic profile-scoped paths. 6 tests pass (21 with the branch suite). Depends on the test harness (PR D-Mad#7) and annotation redo (PR D-Mad#9, for the redo stack reset in deserialize). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177bi2WkPjrGytrarheKFDc Claude-Session-Id: 45a11a50-85fe-4349-b853-e1db9cd20276
FrameDeck showed frame numbers only. Add a timecode readout to the review toolbar and a reusable conversion module (the CSV/PDF exports will use it). - utils/timecode.py: non-drop, frame-count based conversion. Frames are counted at the nominal integer rate (23.976 counts at 24), which is what editorial tools display and makes HH:MM:SS:FF <-> frame an exact round trip. A wall-clock conversion drifts on fractional rates and cannot be inverted reliably, so it is deliberately not used. Unusable rates degrade to a frame label (f0042) instead of raising. - widgets/__init__.py: a TC status label in the review toolbar showing "TC | HH:MM:SS:FF F <frame>", refreshed on every frame change and reset when media is removed. The timeline is 1-based, so the 0-based timecode index subtracts VL_START_FRAME. Tests: tests/test_timecode.py covers zero/second boundaries, fractional rates counting at nominal, H:M:S:F composition, exact round trips across 6 rates x 6 frames, negative frames, bad-rate fallback, and malformed-input rejection. 48 tests pass (58 with the harness suite). Depends on the test harness (PR D-Mad#7). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177bi2WkPjrGytrarheKFDc Claude-Session-Id: 45a11a50-85fe-4349-b853-e1db9cd20276
Foundation for the Frame.io-style review workflow: per-frame text comments that can optionally be pinned to a point on the image. This is the model, rendering, persistence and navigation layer; the comment sidebar UI follows separately. - widgets/annotations.py: Sketch gains a comments store (frame -> comments) with add/get/delete/toggle-done, commented_frames() and comment_count(). A comment holds text, a timestamp, a done flag and optional normalized x/y. draw_comment_pins() renders numbered markers for the current frame's pinned comments (numbering matches list order; done comments use a distinct fill). annotated_frames() now returns the union of stroke and comment frames, and clear()/clear_all() cover both. - widgets/notestore.py: sidecars persist comments alongside strokes. A sketch holding only comments is still written; sidecars predating comments (no "comments" key) still load. - widgets/__init__.py: jump_to_annotation() with "[" / "]" shortcuts and Edit menu actions, seeking to the previous/next annotated frame. Annotations are keyed by the player's local frame while the timeline is global during playlist playback, so the jump maps local -> timeline explicitly. - constants: COMMENT_PIN_RADIUS and the pin colours. Tests: tests/test_comments.py covers comment CRUD, blank-text rejection, done-toggling, the stroke/comment frame union, per-frame and global clear, pin rendering (pixel-probing the marker fill, and the distinct done colour), unpinned comments drawing nothing, sidecar round-trip, a comment-only sketch still writing a sidecar, legacy sidecars without a comments key, and a missing sidecar clearing stale comments. 12 tests pass (33 with the branch suite). Depends on annotation persistence (PR D-Mad#10), which is on PR D-Mad#9 -> PR D-Mad#7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177bi2WkPjrGytrarheKFDc Claude-Session-Id: 45a11a50-85fe-4349-b853-e1db9cd20276
Turns the comment model into a usable review workflow: a Comments panel
listing every note grouped by frame, and a viewer tool that pins a note to
a point on the frame.
- widgets/commentpanel.py: frame-grouped comment tree. Click a row to seek,
tick to mark done, delete the selection, add a frame-level note. The panel
is a pure view over Sketch.comments and owns no state, so it cannot drift
from the markers drawn on the frame. Pin numbers in the list match the
numbers inside the on-frame markers.
- Viewer gains a Comment tool. Clicking the frame emits comment_requested
with the normalized hit point; the window prompts for text and pins it.
- View > Comments Panel (Ctrl+M) toggles the sidebar, which opens to a
usable width the first time it is shown.
- Sketch.mousePressEvent now returns early for the comment tool. A
{type: comment} stroke would render as nothing and be silently dropped
by erase(), which only re-appends the stroke types it recognizes.
Note toggling a comment done restyles its row in place. Rebuilding the tree
from inside itemChanged destroys the item Qt is still emitting the signal
for, which is a use-after-free that takes the process down; the tests caught
it as an intermittent access violation.
15 tests, including the in-place-restyle regression guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TexnzYbmCjjTDB8zzZuUPb
File > Export Notes as CSV... writes every comment and drawing to a flat CSV, one row per note: frame, timecode, type, content, color, x, y, done, timestamp. - utils/notescsv.py is pure (no Qt), so the row building is tested on its own. Rows are ordered by frame ascending; within a frame the comments come first, then the drawings in the order they were made. - Coordinates are the normalized 0-1 image-space values the annotations are already stored in, so a note stays meaningful regardless of the resolution it was made at. Blank means not applicable: a scribble has no text, a frame-level comment has no pin. - Text annotations carry their words into the content column; pencil strokes anchor on their first point, shapes on their start corner. - An unusable frame rate degrades the timecode to a frame label (f0042) rather than failing the export. Bad metadata must not cost a supervisor their notes. - The file is opened with an empty newline argument, else the csv module emits CRLF twice per row on Windows and every other line of the exported file comes out blank. 14 tests, including comma and quote escaping, malformed colours, a pencil with no points, and the blank-line regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TexnzYbmCjjTDB8zzZuUPb
Owner
|
Thank you, Eric. The CSV feature commit has been preserved with your authorship and rebased cleanly onto the current comment-sidebar implementation in #22. That replacement also adds Excel-friendly Unicode output and malformed-coordinate hardening. Closing this stacked/conflicting branch in favor of #22. |
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.
File > Export Notes as CSV...writes every comment and drawing to a flat CSV - one row per note - so a supervisor can take their notes out of FrameDeck into a spreadsheet, a tracker import, or an email.Columns:
frame, timecode, type, content, color, x, y, done, timestampDesign notes
utils/notescsv.pyis pure - no Qt, no window - so the row building is tested directly rather than through the UI.contentcolumn. Pencil strokes anchor on their first point (where the reviewer started drawing - the thing they were pointing at); shapes anchor on their start corner.Two failure modes it handles
Bad frame-rate metadata must not cost someone their notes. An unusable fps degrades the timecode column to a frame label (
f0042) instead of raising. Exporting notes is the last thing a reviewer does before handing off; it is the worst possible moment to fail.Windows CRLF doubling. The csv module writes its own line terminator, so the file must be opened with an empty
newlineargument - otherwise every other line of the exported file is blank when opened on Windows.test_written_csv_has_no_blank_linespins it.Tests
14 new tests, 110 on the branch, all passing headless: ordering, pin/no-pin, done state, timecode mapping, the fps fallback, comma and quote escaping, malformed colours, a pencil with no points, and an empty sketch (header only, no rows).
Verified end to end through a real
MainWindow: the menu action is present and the written file parses back throughcsv.DictReaderwith the notes intact.Depends on #12 (timecode) and #17 (comments), both of which are in this branch base.