The bundle is documented as a standalone format rather than as a note
between two programs: producers and consumers are generic, the marker
vocabulary is defined musically rather than by what a viewer does with
it, and image properties are stated as guarantees with the reasoning
where it is not obvious. Anything that reads scores can implement it
without knowing this tool exists.
Taking that view changed the substance in three places. Marker types now
carry their musical meaning rather than a UI mapping. The rule against
re-importing became a statement about identity - indices mean something
only within one bundle, so two bundles of a piece are independent
documents. And forward-compatibility rules were added, which a protocol
needs and a handover note did not: ignore unknown fields and marker
types, refuse an unknown version.
Tempo is now an integer, beats per minute, exported as a JSON number and
omitted when blank; the editor accepts digits only. A figure can drive a
metronome or a click track where a verbal marking cannot, and readers do
not agree on what Andante means. This needs the consumer's column
changed from free-form text, which the format document flags.
Every figure in the document comes from a real export.
Opening an exported song starts a fresh session from detection instead
of resuming: cuts, discards and metadata do not carry over, so a re-cut
never inherits decisions that have already shipped. --resume overrides
it on edit, export and project.
This reverses what was agreed in planning and written into docs/spec.md
and CONTEXT.md, which promised resume-across-sessions and re-export.
Both are corrected. The cost is deliberate and worth stating: changing
the width cap or adding the SVG renderer later now means re-cutting each
song by hand rather than regenerating every bundle from its project
file.
Export records the flag in bundle.write, so no caller can forget it.
Also removed --refit and the Auto-fit buttons, which were added without
being asked for and whose only purpose - migrating projects made before
the content rectangle was proposed - disappears once exported projects
start fresh. Reset now restores detection's proposal rather than the
whole page: clearing to full width would undo the thing the rectangle
exists for, so one button covers it.
open_project() replaces four copies of load-or-detect across the CLI
and the editor.
Both scan problems reported from testing came from the same place: the
content rectangle defaulted to the whole page, so the mechanism meant
to handle margin junk never engaged. Ketun joululaulu has a vertical
scan streak down the right margin and Engel a shadow, and because trim
is tight and per slice, either one sets that slice's width, which sets
the song's widest slice, which scales the whole song down.
Detection can propose it. Staff lines are long horizontal runs; scan
shadows, spine darkening and glass streaks are vertical, so a wide flat
opening keeps one and erases the other. Three corrections were needed
against the corpus:
- Search only rows inside detected systems. A horizontal artefact above
or below the music is itself a long horizontal run reaching the paper
edge, which put Engel's left bound at 0.
- Take a percentile of the staff-line extents, not the maximum. Where
an artefact touches a staff line the two merge into one component: on
Ketun p2 the merged line ends at 1575px against 1544px on the clean
page.
- Take the left bound from the brackets too. A bracket sits left of
every staff line, so a staff-line bound crops it off — visible
immediately when comparing exported slices.
Anchors are now a dataclass carrying their left edge rather than a
(top, bottom) tuple.
Engel now drops 12-14% of page width and its music fills 1920px instead
of leaving the shadow's dead space; Ketun drops 12%.
Existing projects keep their saved rectangle; the editor's new Auto-fit
and Auto-fit all buttons re-propose it without disturbing cuts.
Page 1 of Engel (Bosse/Partitura) has a boxed VERSE 1 label in the left
margin at the same rows as the preceding system's bass lyric line — ink
on both sides of the page throughout the band, so no horizontal line
separates them. The label belongs to system 2, the lyrics to system 1.
A cut is therefore a polyline spanning the page, two points being the
ordinary straight case. A slice bounded by a non-straight cut is not
rectangular: its image is the region's bounding box with everything
outside made transparent, which composites invisibly on the viewer's
sheet. Masking paints transparency, never white.
The handoff was written as a message to relay information; several
decisions lived only there. Split into permanent homes:
- docs/spec.md — scope, geometry model, pipeline order, detection,
levels, editor, project file, markers, bundle format, noteman's
changes, reference values
- ADR 0002 — raster only in release 1; SVG slices measured at 40x WebP
naive, 2.6x with a bounding-box cull, deferred on risk not size
- ADR 0003 — lossless WebP with levels and 16-level alpha; every lossy
option and alternative format measured larger for line art
- ADR 0004 — detection proposes, the human disposes; no unattended mode
- ADR 0005 — PyMuPDF for all PDF access, accepting AGPL
- ADR 0006 — systems are found by vertical brackets, not row-darkness
gaps, which miscount every page of a 6-voice score
Also from testing against the hardest score in the repertoire: scanned
PDFs carry their scan as an embedded image and must be extracted at
native resolution rather than re-rendered at 600 DPI, and per-page
deskew is mandatory (skew varies -2.6 to +1.2 within one PDF).