Esa Kataja c00a00bb2a Propose the content rectangle from the staff lines
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.
2026-07-28 23:28:45 +03:00

noteman-slicer

Turns a score PDF into the ordered slice images noteman consumes, plus the navigation markers that sit on them.

A slice is one system — one full line of music across all voices, typically 412 bars with lyrics intact. noteman displays them as a continuous vertical scroll, so the slicer's job is to cut a printed page into systems, clean them up enough to read on a tablet, and tag them with the score's navigation symbols.

Status: design only. No code yet. The design is settled; see below.

How it works

Open a PDF, and the tool proposes cuts between systems, a skew correction, and a content rectangle. You correct all of it — source quality varies too much for unattended processing, so detection is an accelerator that nothing depends on being right. You mark the header and footer regions discarded, set black and white points until the paper disappears and the notes go solid, place the rehearsal letters and jump markers, fill in the title block, and export.

Out comes one zip: the slices in order, their markers, the original PDF, and the song metadata. That bundle is the only channel to noteman — there's no API between the two tools.

Erasing previous-owner pencil marks, chord letters and breath marks stays in GIMP. That's the irreducible manual part, and GIMP with a stylus is already good at it.

Installation

Not yet installable. When it is:

uv tool install --editable .

That puts a noteman-slicer command on PATH which runs from any directory — no venv to activate. Dependencies (PyMuPDF, PySide6, OpenCV, numpy) are all wheels; nothing needs a system package.

Documentation

CONTEXT.md Glossary. What a slice, cut, discard, bundle and song scale actually mean here. Start here.
docs/spec.md The specification: pipeline, geometry model, detection, editor, bundle format, and what noteman has to change.

Deferred work is tracked as issues and milestones on the Gitea repo, not in this tree.

Decisions that were expensive to reach, each with the evidence behind it:

ADR 0001 The slicer owns all image processing; the bundle is the only channel to noteman.
ADR 0002 Raster only in release 1 — measured SVG slice sizes and what they showed.
ADR 0003 Lossless WebP beats every lossy option and every alternative format here.
ADR 0004 No unattended mode: detection suggests, a human confirms.
ADR 0005 PyMuPDF for all PDF access, accepting AGPL.
ADR 0006 Systems are found by vertical brackets; row-darkness gaps get it wrong.
S
Description
No description provided
Readme
195 KiB
Languages
Python 100%