"""Runnable check for markers: model, cut edits, and export resolution.""" from __future__ import annotations import json import sys import zipfile from pathlib import Path import numpy as np import pymupdf sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from noteman_slicer import bundle # noqa: E402 from noteman_slicer.bundle import song_json # noqa: E402 from noteman_slicer.detect import detect_page # noqa: E402 from noteman_slicer.pdf import open_source, page_raster # noqa: E402 from noteman_slicer.project import ( # noqa: E402 JUMP_TYPES, MARKER_TYPES, Cut, Marker, Project, default_path, ) W, H = 1200, 1600 def _scan_pdf(path: Path) -> None: art = np.full((H, W), 255, np.uint8) for top in (300, 800): art[top : top + 200, 100:104] = 0 for staff in (top, top + 140): for i in range(5): art[staff + i * 15 : staff + i * 15 + 2, 110:1100] = 0 pix = pymupdf.Pixmap(pymupdf.csGRAY, W, H, bytearray(art.tobytes()), False) doc = pymupdf.open() doc.new_page(width=595, height=842).insert_image(pymupdf.Rect(0, 0, 595, 842), pixmap=pix) doc.save(path) def main() -> int: tmp = Path(__file__).with_name("_tmp") tmp.mkdir(exist_ok=True) pdf = tmp / "scan.pdf" _scan_pdf(pdf) # noteman's enum, verbatim — this is real coupling between two repos. assert len(MARKER_TYPES) == 14, MARKER_TYPES assert len(JUMP_TYPES) == 6 assert "generic_jump" in JUMP_TYPES and "segno" not in JUMP_TYPES source = open_source(pdf) gray = page_raster(source, 0) project = Project.from_detection(pdf, [detect_page(gray)], [gray.shape[0]]) page = project.pages[0] assert len(page.markers) == page.slice_count kept = project.kept_slices() assert len(kept) == 2, kept (_, first), (_, second) = kept page.markers[first].append(Marker("rehearsal_letter", label="A")) page.markers[second].append(Marker("coda")) page.markers[first].append(Marker("to_coda", destination=(0, second))) # Cut edits keep markers aligned with their slices. before = list(page.markers[first]) index = page.add_cut(Cut.straight(0.95)) assert len(page.markers) == page.slice_count assert page.markers[first] == before, "markers must not move when a later slice splits" page.remove_cut(index) assert len(page.markers) == page.slice_count # Export resolves (page, slot) to the slice's index in the bundle. names = [f"{i + 1:03}.webp" for i in range(len(project.kept_slices()))] project.metadata.update({"title": "Test song", "tempo": "92", "composer": ""}) payload = song_json(project, names) # Tempo is a number, not a string; empty fields are absent, not "". assert payload["tempo"] == 92, payload["tempo"] assert "composer" not in payload project.metadata["tempo"] = "Andante" assert "tempo" not in song_json(project, names), "words are not a tempo" project.metadata["tempo"] = "92" slices = payload["slices"] assert [s["file"] for s in slices] == names assert slices[0]["markers"][0] == {"type": "rehearsal_letter", "label": "A"} assert slices[1]["markers"][0] == {"type": "coda"} assert slices[0]["markers"][1] == {"type": "to_coda", "destination": 1} # A jump whose target got discarded is dropped, not exported dangling. project.pages[0].discards[second] = True dropped = song_json(project, ["001.webp"]) assert all(m["type"] != "to_coda" for m in dropped["slices"][0].get("markers", [])) project.pages[0].discards[second] = False # Round-trip through the project file. saved = project.save() reloaded = Project.load(saved) assert reloaded.pages[0].markers[first][0].label == "A" assert reloaded.pages[0].markers[first][1].destination == (0, second) assert reloaded.pages[0].markers[second][0].type == "coda" # And through a real bundle. reloaded.metadata["title"] = "Test song" out = bundle.write(reloaded, source, tmp / "song.zip") with zipfile.ZipFile(out) as zf: meta = json.loads(zf.read("song.json")) assert meta["slices"][0]["markers"][1]["destination"] == 1, meta["slices"] source.close() for f in (pdf, out, saved, default_path(pdf)): f.unlink(missing_ok=True) tmp.rmdir() print("ok") return 0 if __name__ == "__main__": sys.exit(main())