Add design docs: glossary, ADR 0001, and project scaffolding

Records the outcome of the design review that reworked the original
handoff notes:

- CONTEXT.md — ubiquitous language for the slicer (slice, cut, discard,
  content rectangle, trim, song scale, bundle, project, jump source)
- docs/adr/0001 — the slicer owns all image processing and the bundle is
  the only channel to noteman; noteman's sharp pipeline goes away
- pyproject renamed to noteman-slicer
- .gitignore keeps scores and bundles out of the repo (copyright, size)
This commit is contained in:
Esa Kataja
2026-07-28 21:33:14 +03:00
parent ecd303322f
commit efc920fdc5
6 changed files with 192 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
.venv/
# Scores and their working state — copyright, and multi-MB
*.pdf
*.zip
*.slicer.json
+1
View File
@@ -0,0 +1 @@
3.13
+122
View File
@@ -0,0 +1,122 @@
# noteman-slicer — Context
A local, single-user tool that turns a score PDF into the ordered slice images
noteman consumes, plus the navigation markers that sit on them. It automates the
mechanical part of noteman's ingestion boundary; it does not replace GIMP for
erasing previous-owner annotations.
## Language
**Score source**:
The original PDF as distributed to the choir. Either **vector** (engraved by a
notation program) or **raster** (a flatbed scan of a printed page).
_Avoid_: original, input file
**Source type**:
Whether a score source is vector or raster. Determined once per PDF, not per
page; the slicer detects it and asks the user to confirm before routing.
_Avoid_: mode, format
**Slice**:
The atomic visual unit of a song — one *system*, one full line of music across
all voices, typically 412 bars with lyrics intact. Same definition as noteman's.
Structurally, a horizontal region of a page: a page begins as a single slice and
each cut splits one slice into two, so slices always tile the page with no gaps
and no overlap.
_Avoid_: segment, strip, row, band
**Discard**:
A flag on a slice meaning it produces no output. How headers, footers and blank
regions leave the song — they are slices that are simply not exported. Detection
pre-sets it on a page's top and bottom slice when they contain no system.
_Avoid_: delete, skip, exclude
**Slice image**:
The rendered artifact of a slice. From a raster source: lossless WebP, RGB pure
black, `alpha = 255 luminance`, width capped at 1920px — paper is transparency,
ink is alpha. From a vector source: SVG with text converted to paths. Both are
display-ready as produced; nothing downstream reprocesses them.
_Avoid_: PNG, page image, tile
**Marker**:
A semantic tag placed on a slice for navigation. Vocabulary is noteman's enum
verbatim (`rehearsal_letter`, `section_label`, `segno`, `coda`, `fine`,
`repeat_start`, `repeat_end`, `volta`, `to_coda`, `ds_al_coda`, `ds_al_fine`,
`dc_al_coda`, `dc_al_fine`, `generic_jump`) and is shared coupling between the
two repos.
**Project**:
The persistent state of slicing one song: the source PDF it points at, its cuts,
discards, content rectangle, levels, staff-height overrides, markers and
metadata. Autosaved beside the PDF; the bundle is generated from it, so any
export can be regenerated without repeating human work. One PDF, one song, one
project, one bundle — never a many-to-one in any direction.
_Avoid_: session, document, edit list
**Bundle**:
The single compressed file that is the *only* channel between the slicer and
noteman. There is no API call, no direct upload. Carries the slice images in
order, their markers, the original PDF and the song-level text metadata.
Rehearsal MIDI and MP3s are deliberately out of the first bundle and stay
noteman's upload chore until a later phase.
_Avoid_: export, package, upload
**Marker label**:
The free text some markers carry — `rehearsal_letter` ("C"), `section_label`
("CHORUS"), `volta` ("1."). All other types are bare tags.
**Jump source**:
A marker that sends the reader elsewhere — `to_coda`, `ds_al_coda`,
`ds_al_fine`, `dc_al_coda`, `dc_al_fine`, `generic_jump`. Every jump source
stores its **target slice explicitly**; none is resolved by type at read time.
Authoring the target costs one click on a slice already on screen, and in
exchange the bundle is self-describing — no "exactly one Coda per song" rule
living unenforced in two repos, and a score with two codas simply works.
_Avoid_: link, reference, pointer
**Cut**:
A horizontal line placed on a page that splits one slice into two. Straight at
first; a later polyline form handles pages where systems slant or interleave.
Placement is forgiving — anywhere inside the whitespace gap yields the same
output, because trim crops to ink afterwards.
_Avoid_: split, divider, break
**Content rectangle**:
The region of a page that holds music. Set per PDF, adjustable per page, applied
before cutting. Everything outside it — scan-edge bands, spine shadows, margin
page numbers — is dropped. Handles margin junk structurally rather than
case-by-case, because margin junk is by definition outside the music.
_Avoid_: crop box, mask, margin
**Detection**:
Any automatic proposal the slicer makes — skew angle, cut positions, source
type, ink bounds. Detection is always a *suggestion*: the human confirms or
modifies every result before it is committed. Source quality varies too widely
for unattended processing to be trustworthy.
_Avoid_: auto-slicing, automatic mode
**Trim**:
Cropping a slice tight to its ink on all four sides, per slice. Normalises away
the left-margin drift between scanned pages, and flattens the engraved indent of
the first system — correct here, since the printed header it made room for is
stripped anyway. The viewer supplies margins with CSS padding.
**Staff height**:
The distance between a system's top and bottom staff line. The invariant a
reader perceives as "the notes are the same size", and therefore what slices are
normalised against — not their width. Falls out of the same row-darkness profile
used for cut detection. Manually overridable, since it is detected on the same
speckled scans where detection already struggles.
**Song scale**:
Two steps, both per song, never per slice. First every slice is normalised to a
common staff height, so a rescanned page or a re-engraved slice sits at the same
note size as its neighbours. Then the whole song is scaled uniformly so its
widest slice lands at 1920px — a ceiling, never a target: a song that comes out
narrower stays narrower, because upscaling a scan past its real resolution buys
softness and bytes and no detail.
**Right pad**:
The transparent space added to the right of every slice narrower than the widest,
bringing all slices in a song to the same width. Keeps the music the same size
and flush left; a short system simply ends earlier.
@@ -0,0 +1,48 @@
# The slicer owns all image processing; the bundle is the only channel to noteman
The slicer was originally scoped to share the work with noteman: it would cut the
pages, upload them to `POST /api/admin/songs/:id/slices`, and noteman's sharp
pipeline would trim, downscale and convert ink to alpha on arrival. We decided
instead that the slicer produces finished, display-ready artifacts and hands them
over as a single zip; noteman does no image manipulation beyond what plain HTML
and CSS do at runtime, and there is no API between the two tools.
## Considered options
**Split the work (rejected).** Slicer cuts, noteman normalises. This was the
existing shape and needed no changes on either side. It failed on inspection:
noteman's `normalizeSlice` calls sharp's `removeAlpha()` before its luminance
pass, and `removeAlpha()` discards the alpha channel rather than compositing it
over white — so any transparency the slicer produced would flatten to black ink.
A polygon-masked slice would have arrived as a solid black rectangle. More
fundamentally, the split put trim, downscale and encoding rules in two repos and
two languages, where they must agree exactly and nothing checks that they do.
**Slicer produces final pixels, noteman stores them (chosen).**
**Direct POST with a bypass flag (rejected).** Keeps the API but adds a
content-sniff or flag to skip normalisation. Preserves the two-language
duplication it was meant to remove, and needs an admin token in a local script.
## Consequences
Several rules that read as arbitrary in the slicer's code are only explicable
from here:
- The slicer owns **trim, staff-height normalisation, the 1920px ceiling, right
padding, levels and the ink→alpha conversion.** Nothing downstream can rescue
a faint scan or rehabilitate a bad crop, so the slicer's output is final.
- **Scale is normalised on staff height, not width**, and derived once per song.
A per-slice factor would change note size line to line down a continuous
scroll; a width-based factor breaks the moment a slice comes from a different
source (a rescanned page, or a re-engraved replacement system).
- **noteman must delete its sharp pipeline** and accept a bundle instead. Until
that lands, the slicer produces artifacts nothing consumes.
- **The bundle is a versioned contract**, not an internal format. Two separately
maintained repos now agree on: the marker enum, slice ordering, and the shape
of `song.json`. That coupling is deliberately kept as small as possible —
which is why every jump source stores its target slice explicitly rather than
relying on an unwritten "exactly one Coda per song" resolution rule.
- **The split is reversible only in one direction.** Re-adding server-side
processing means re-deriving all of the above; going further the other way
(more processing in the slicer) is free.
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from slicer!")
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
[project]
name = "noteman-slicer"
version = "0.1.0"
description = "Cuts score PDFs into noteman's slice images and markers"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []