Levels shipped at 0–255 unless someone moved the sliders, and Bicycle
Race showed what that costs. Its ink is grey, not black — a scanned
engraving, ink at 2–95, paper at 163–255 — and with alpha = 255 − luminance
that greyness becomes transparency. No pixel in the exported bundle was
even fully opaque, and the downscale to the song's width blended every
stroke edge further. Nothing downstream can rescue it.
So detection proposes levels too, like it proposes cuts and skew.
Notation is two-tone, which makes Otsu's split the measurement wanted;
the points sit halfway from it to each end of the range, so the ramp
between them survives as antialiasing rather than going jagged. A page
already scanned bilevel has no interior split — Otsu degenerates to 0 —
and is left alone. Per page, with the median becoming the song's, so a
near-blank page cannot set them.
Scanned scores are black ink on white paper stored as 8-bit greyscale or
RGB, which costs several times what the same page costs as a bilevel
image. Across an 11-song corpus this is 27.6 MB to 7.7 MB; Engel's
bundle goes from 5997 KB to 2092 KB with byte-identical slices, since
only the archived copy changes.
Three approaches were measured and discarded first, which is worth
recording because two of them are the obvious ones. Converting RGB to
greyscale and re-encoding makes these files 20-86% LARGER: the source
JPEGs are already near 0.7 bits per pixel, so re-encoding adds
generation loss and spends more bits than the original did, and dropping
chroma recovers nothing because JPEG already subsamples it. Lossless
structural optimisation gains 0.1%, because images are 99% of every file
and there are no duplicates. Downsampling works but 300 DPI is print
resolution, and the PDF exists to be printed.
Two failure modes were found by looking at output rather than at byte
counts, and both are now refused:
- A scan at ~115 DPI came back with broken staff lines. Guarded on
resolution as the image is *placed on the page*, so a tiled scan with
126 small images still qualifies where a pixel count would reject it.
- Cover artwork was flattened to grey. Guarded on chroma: artwork
measures 44% off-grey against 3% for sensor tint on a greyscale scan.
The first threshold of 2% was a false positive that cost 685 KB on one
song for nothing; 10% sits in the gap with room either side.
Exposed as a button rather than a checkbox. It reports what it skipped
and why, and shows a before/after crop, because the failure it can
produce is obvious at a glance and invisible in a size figure. Off by
default: this is lossy on the copy kept for printing.
Re-engraving is a rescue path for the handful of systems a scan cannot
deliver, so the window is an editing surface rather than an automation
project. Three full-width rows - the scanned system, the render, the
form - because a system is wide and short and the job is comparing one
against the other bar by bar. The render is shown scaled to the scan's
staff height, which is what export does anyway, so it previews the real
thing.
A form rather than a text box. Key and time are slice-level, clef,
notes and lyrics per voice: every staff in a system carries the same key
signature, and Kaipaava proves it across five-staff and two-staff
systems alike. Notes and lyrics stay raw LilyPond, so slurs, dynamics,
tuplets and the laissezVibrer/repeatTie idiom for ties crossing into the
next slice all work untouched.
Notes are entered in \relative mode, referenced to the middle of each
clef's staff, so a part needs no octave marks at all in the common case.
The time signature is used for spacing and bar checks but not printed:
the printed score repeats the key at every system and the time only at
the first, so a re-engraved middle slice showing one would stand out.
Seeded from what can be known reliably. Voice count comes from counting
staves in the slice; key, time and clefs are inherited from the song,
because the slices being re-engraved are the illegible ones and reading
a key signature off them is exactly the measurement that fails. After
the first replacement in a song only the notes need typing.
Staff counting needed two corrections against the corpus: compare gaps
against line spacing rather than staff height, since adjacent staves can
sit closer together than one staff is tall; and require five lines in a
group, since Engel's 'uh______' lyric extenders are long horizontal runs
too and each counted as a staff. Kaipaava now reads 2,2,2,2,5 on page 1,
Ketun 6, Engel 4.
Also in this change:
- Title is required for export, every other metadata field optional,
enforced in bundle.write so the CLI and the editor both get it. Tempo
added; noteman already has a free-form column for it.
- The panel is a splitter rather than a fixed width, sections collapse
under bold grey disclosure headers, and it scrolls.
- A re-engraved slice is washed amber with an ENGRAVED badge, and
markers get badges too. Thin coloured text was invisible against a
scan.
Closes#31Closes#32Closes#33Closes#34
Markers are stored per (page, slot), parallel to the discard flags, so
adding or removing a cut keeps them aligned with their slices. On a
split they stay with the upper half: a marker sits on a printed symbol
and nothing can say which side that symbol landed on, so predictable
beats clever.
Jump targets are chosen by clicking the slice rather than from the
thumbnail strip the plan called for. Less code, and it reads the score
instead of a list of thumbnails - which is what you want when hunting
for the Coda sign. Any page; PageUp/PageDown while picking.
Export resolves (page, slot) to the bundle's array index, the only
cross-reference the format has. A jump whose target was discarded or
re-cut away is dropped rather than exported dangling, since noteman
would have nothing to resolve it to.
tests/test_markers.py covers the enum size - that is the coupling
between two repos - along with cut-edit alignment, index resolution,
the dangling-target drop, and round-trips through both the project file
and a real bundle.
Closes#28Closes#29Closes#30
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.
Everything the human decides, in normalised page coordinates so the
file is independent of DPI and of which renderer produced it. The
bundle will be generated from this, which is what makes re-export
possible without repeating human work.
Cuts are polylines from the start, two points being the ordinary
straight case, so the stepped cuts Engel needs are a data question
rather than a migration. Adding a cut splits a slice and copies its
discard flag to both halves; removing one merges them.
Boundary cuts belong here rather than in detection: detection emits
cuts only between systems, so a page would have exactly as many slices
as systems, with the header and footer inside the first and last.
Isolating and discarding them is a slicing decision.
Saves are write-then-rename, so a crash mid-save cannot destroy the
previous state. The PDF is hashed, not copied, so an edit underneath is
reported rather than silently re-cut.
Ketun joululaulu now yields 24 kept slices over 12 pages and Feliz
Navidad 20 over 4, with headers and footers discarded on every page.
Closes#10, #11, #13