Files
product-vamp/generator/engraving/pipeline.py
T
khodowany bc7baea7c4
ci / checks (push) Failing after 23m22s
Cleanup
2026-10-08 02:13:45 -05:00

108 lines
5.5 KiB
Python

from __future__ import annotations
import concurrent.futures
import json
import multiprocessing
import pathlib
import time
import generator.engraving.cache
import generator.engraving.fonts
import generator.engraving.types
import generator.engraving.validation
import generator.engraving.worker
import generator.validation
def engrave_tree(root: pathlib.Path, fonts: pathlib.Path, *, cache_dir: pathlib.Path | None = None, jobs: int = 1, progress: bool = False) -> set[str]:
if type(jobs) is not int or not 1 <= jobs <= 16:
raise ValueError('engraving jobs must be between 1 and 16')
if cache_dir and (cache_dir.resolve().is_relative_to(root.resolve()) or root.resolve().is_relative_to(cache_dir.resolve())):
raise ValueError('engraving cache and generated assets must not overlap')
started = time.monotonic()
paths = sorted(root.rglob('*.json'))
validated_pages = set()
if not paths:
return validated_pages
family, style = generator.engraving.fonts.Fonts(fonts), generator.engraving.types.Style()
fingerprint = family.fingerprint(style)
renderer = generator.engraving.cache.renderer_fingerprint(family, style)
if cache_dir:
cache_dir = generator.engraving.cache.renderer_namespace(cache_dir, renderer)
if progress:
state = 'warm namespace' if cache_dir.is_dir() else 'cold namespace'
print(f'Engraving cache enabled: {cache_dir} ({state}; renderer {renderer})', flush=True)
elif progress:
print('Engraving cache disabled', flush=True)
totals = generator.engraving.worker.GroupResult()
missing, rejected, completed = 0, 0, 0
groups = {}
for path in paths:
try:
document = json.loads(path.read_text(encoding='utf-8'))
generator.validation.validate_document(document)
score_key = generator.engraving.cache.score_fingerprint(document, renderer)
destination = path.with_suffix('.pages')
generator.engraving.worker.clear_pages(destination)
entry = cache_dir / 'fullscore' / score_key if cache_dir else None
present = entry is not None and (entry.exists() or entry.is_symlink())
pages = generator.engraving.cache.restore_pages(entry, destination, validated_pages) if entry else None
if pages is not None:
try:
generator.engraving.worker.validate_cached_views(document, pages)
except (ValueError, TypeError, KeyError):
pages = None
if pages is None:
rejected += int(present)
missing += int(entry is not None and not present)
notation = generator.engraving.cache.notation_fingerprint(document, renderer)
groups.setdefault(notation, []).append((path, score_key))
else:
generator.engraving.worker.write_bundle(path, document, pages, fingerprint)
totals.fullscore += 1
completed += 1
except ValueError as error:
raise generator.engraving.types.EngravingError(f'{path.relative_to(root)}: {error}') from error
prepare_time = time.monotonic() - started
if progress:
print(f'Engraving grouped: {len(paths)} scores, {len(groups)} notation bodies, {totals.fullscore} fullscore hits', flush=True)
validation_time = 0.0
def finish(result):
nonlocal completed, validation_time
started = time.monotonic()
for path, score_key, pages in result.scores:
destination = path.with_suffix('.pages')
for page in pages:
generator.engraving.validation.check_page(destination / page['path'], validated_pages, expected_sha256=page['sha256'])
validation_time += time.monotonic() - started
if cache_dir:
started = time.monotonic()
stored = set()
for path, score_key, pages in result.scores:
if score_key not in stored:
generator.engraving.cache.store_pages(cache_dir / 'fullscore' / score_key, path.with_suffix('.pages'), pages)
stored.add(score_key)
result.timings['cache'] += time.monotonic() - started
for field in ('fullscore', 'bodyhits', 'bodies', 'renderedpages', 'rejected'):
setattr(totals, field, getattr(totals, field) + getattr(result, field))
for phase, elapsed in result.timings.items():
totals.timings[phase] += elapsed
completed += len(result.scores)
if progress:
print(f'Engraving: {completed}/{len(paths)} scores, fullscore {totals.fullscore}, bodyhits {totals.bodyhits}, bodies {totals.bodies}, renderedpages {totals.renderedpages}', flush=True)
jobs = min(jobs, len(groups))
if jobs <= 1:
for notation, group in groups.items():
finish(generator.engraving.worker.engrave_group(notation, group, root, fonts, cache_dir, fingerprint, progress))
else:
with concurrent.futures.ProcessPoolExecutor(max_workers=jobs, mp_context=multiprocessing.get_context('spawn')) as pool:
futures = [pool.submit(generator.engraving.worker.engrave_group, notation, group, root, fonts, cache_dir, fingerprint, progress) for notation, group in groups.items()]
for future in concurrent.futures.as_completed(futures):
finish(future.result())
if progress:
print(f'Engraving complete: {len(paths)} scores, {totals.fullscore} reused, {missing} missing, {rejected} rejected; fullscore {totals.fullscore}, bodyhits {totals.bodyhits}, bodies {totals.bodies}, renderedpages {totals.renderedpages}, rejected bodies {totals.rejected}', flush=True)
phases = {'prepare': prepare_time, **totals.timings, 'validation': validation_time}
print('Engraving timings: ' + ', '.join(f'{phase} {elapsed:.3f}s' for phase, elapsed in phases.items()) + f'; wall {time.monotonic() - started:.3f}s (worker phases summed)', flush=True)
return validated_pages