|
9 | 9 | import logging |
10 | 10 | import sys |
11 | 11 | from collections import Counter |
12 | | -from dataclasses import dataclass |
| 12 | +from dataclasses import dataclass, replace |
13 | 13 | from pathlib import Path |
14 | 14 |
|
15 | 15 | import click |
16 | 16 |
|
17 | 17 | from kern import KernReader |
18 | | -from kernsheet import KernSheet, review_names, score_findings |
| 18 | +from kernsheet import KernScore, KernSheet, review_names, score_findings |
19 | 19 | from kernsheet.reviews import Finding |
20 | 20 | from utils import log_uncaught_exceptions, print_histogram |
21 | 21 |
|
@@ -218,44 +218,56 @@ def edit( |
218 | 218 | def detect(ctx: ClickContext, prefix: str, write: bool, width: int) -> None: |
219 | 219 | """Generate a layout via ClassicalStaffer for every catalog score that has none. |
220 | 220 |
|
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. |
225 | 227 | """ |
226 | 228 | from kernsheet import ClassicalStaffer |
227 | 229 |
|
228 | 230 | ks = ctx.kern_sheet |
229 | 231 | 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()) |
240 | 247 | ok = failed = 0 |
241 | | - for _, score in todo: |
| 248 | + for pdf_path, scores in todo.items(): |
242 | 249 | 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) |
244 | 257 | print( |
245 | 258 | f" {score.id}: {result.page_count}p " |
246 | 259 | f"{result.system_count}sys {result.staff_count}staves" |
| 260 | + + (" (shared pdf)" if i else "") |
247 | 261 | + ("" if write else " (dry-run)") |
248 | 262 | ) |
249 | 263 | if write: |
250 | 264 | 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) |
257 | 269 | 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).") |
259 | 271 |
|
260 | 272 |
|
261 | 273 | @click.command() |
|
0 commit comments