Skip to content

Commit 990f574

Browse files
anselm67claude
andcommitted
perf(kernsheet): detect each shared PDF once, clone the layout
detect looped per-score, so a PDF shared by N entries (an all-in-one edition) was rasterised and cv2-detected N times to produce N layouts differing only in the embedded id. Group layout-less scores by source pdf_path, detect each unique PDF once, and clone the result to every score (replace(base, id=...)); rebuild_images once per PDF since png paths are keyed by pdf stem + page. Same outputs, no redundant passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015eptQttDEavSJVC2qzSq41
1 parent 4611bd4 commit 990f574

1 file changed

Lines changed: 37 additions & 25 deletions

File tree

src/cli/kernsheet.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
import logging
1010
import sys
1111
from collections import Counter
12-
from dataclasses import dataclass
12+
from dataclasses import dataclass, replace
1313
from pathlib import Path
1414

1515
import click
1616

1717
from kern import KernReader
18-
from kernsheet import KernSheet, review_names, score_findings
18+
from kernsheet import KernScore, KernSheet, review_names, score_findings
1919
from kernsheet.reviews import Finding
2020
from utils import log_uncaught_exceptions, print_histogram
2121

@@ -218,44 +218,56 @@ def edit(
218218
def detect(ctx: ClickContext, prefix: str, write: bool, width: int) -> None:
219219
"""Generate a layout via ClassicalStaffer for every catalog score that has none.
220220
221-
Runs the cv2 projection-profile detector on each PDF and writes an UN-validated
222-
Score under layout/ for review in the editor. Only scores with no existing layout
223-
(and a usable pdf + write target) are touched; pass PREFIX to restrict to entries
224-
whose key starts with it. Dry-run by default; pass -w to write.
221+
Runs the cv2 projection-profile detector once per source PDF (a PDF shared by
222+
several entries — an all-in-one edition — is detected once and the layout cloned
223+
to each score) and writes an UN-validated Score under layout/ for review in the
224+
editor. Only scores with no existing layout (and a usable pdf + write target) are
225+
touched; pass PREFIX to restrict to entries whose key starts with it. Dry-run by
226+
default; pass -w to write.
225227
"""
226228
from kernsheet import ClassicalStaffer
227229

228230
ks = ctx.kern_sheet
229231
staffer = ClassicalStaffer(width=width)
230-
todo = [
231-
(key, score)
232-
for key, entry in ks.catalog.entries.items()
233-
for score in entry.scores
234-
if (not prefix or key.startswith(prefix))
235-
and not ks.layout_path(score).is_file()
236-
and score.json_path
237-
and score.pdf_path
238-
and ks.pdf_path(score).is_file()
239-
]
232+
# Group layout-less scores by source pdf: a pdf shared by many entries yields
233+
# identical geometry for every score, so detect it once and clone the result
234+
# (only the embedded id differs) instead of re-running detection per score.
235+
todo: dict[Path, list[KernScore]] = {}
236+
for key, entry in ks.catalog.entries.items():
237+
for score in entry.scores:
238+
if (
239+
(not prefix or key.startswith(prefix))
240+
and not ks.layout_path(score).is_file()
241+
and score.json_path
242+
and score.pdf_path
243+
and ks.pdf_path(score).is_file()
244+
):
245+
todo.setdefault(ks.pdf_path(score), []).append(score)
246+
candidates = sum(len(scores) for scores in todo.values())
240247
ok = failed = 0
241-
for _, score in todo:
248+
for pdf_path, scores in todo.items():
242249
try:
243-
result = staffer.detect(ks.pdf_path(score), score.id)
250+
base = staffer.detect(pdf_path, scores[0].id)
251+
except Exception as e:
252+
failed += len(scores)
253+
logging.error(f"detect {pdf_path}: {e}")
254+
continue
255+
for i, score in enumerate(scores):
256+
result = base if i == 0 else replace(base, id=score.id)
244257
print(
245258
f" {score.id}: {result.page_count}p "
246259
f"{result.system_count}sys {result.staff_count}staves"
260+
+ (" (shared pdf)" if i else "")
247261
+ ("" if write else " (dry-run)")
248262
)
249263
if write:
250264
ks.save_score(score.id, result)
251-
ks.rebuild_images(score, result)
252-
except Exception as e:
253-
failed += 1
254-
logging.error(f"detect {score.id}: {e}")
255-
continue
256-
ok += 1
265+
ok += 1
266+
# png paths are keyed by pdf stem + page, so a shared pdf renders once.
267+
if write:
268+
ks.rebuild_images(scores[0], base)
257269
verb = "written" if write else "detected (dry-run; pass -w to write)"
258-
print(f"\n{ok} score(s) {verb}, {failed} failed, of {len(todo)} candidate(s).")
270+
print(f"\n{ok} score(s) {verb}, {failed} failed, of {candidates} candidate(s).")
259271

260272

261273
@click.command()

0 commit comments

Comments
 (0)