Files
product-vamp/generator/resources.py
T
khodowany ef62af892b Cleanup
2026-10-04 22:29:06 -05:00

83 lines
3.5 KiB
Python

"""Content-addressed, non-executable catalog compilation.
Only validated production XML is compiled here. The cache contains no song,
role, candidate, reference, or fitted musical result. A checksum and decoder
version protect stale/corrupt payloads; no pickle or executable serialization.
"""
from __future__ import annotations
import dataclasses
from fractions import Fraction as F
import functools
import hashlib
import json
import os
from pathlib import Path
import tempfile
from generator import rhythm, vocabulary
from generator.scales import library as scales
from generator.solo import sequences
VERSION = 'catalog-json-1'
def identity(directory):
directory=Path(directory)
return {name:vocabulary.fingerprint([(p.relative_to(directory/name).as_posix(),
hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted((directory/name).rglob('*.xml'))])
for name in ('scales','sequences','rhythms')}
def encode(libraries):
scale,sequence,rhythms=libraries
return {'scales':[dataclasses.asdict(p) for p in scale.patterns],
'sequences':[dataclasses.asdict(p) for values in sequence.paths.values() for p in values],
'rhythms':[dataclasses.asdict(p) for values in rhythms.snippets.values() for p in values]}
def decode(value):
scale=scales.Library(tuple(scales.Pattern(p['family'],p['mode'],tuple(p['intervals'])) for p in value['scales']))
sequence=sequences.Library(tuple(sequences.Sequence(p['id'],tuple(p['steps'])) for p in value['sequences']))
snippets=[]
for p in value['rhythms']:
bars=[]
for b in p['bars']:
events=tuple(rhythm.Event(F(e['onset']),F(e['duration']),e['sounding'],e['written_type'],e['dots'],
tuple(tuple(v) for v in e['beams']),e['continuation'],F(e['ratio'])) for e in b['events'])
bars.append(rhythm.Bar(events,F(b['length'])))
snippets.append(rhythm.Snippet(p['id'],p['meter'],tuple(bars)))
return scale,sequence,rhythm.Library(tuple(snippets))
def load(directory, fingerprints=None, cache_dir=None):
directory=Path(directory)
fingerprints=fingerprints or identity(directory)
# Code edits invalidate compiled importer semantics, but do not reseed music.
root=Path(__file__).parent
code=[root/n for n in ('resources.py','rhythm.py','musicxml.py','model.py','pitch.py','harmony.py','solo/sequences.py','scales/library.py')]
key=vocabulary.fingerprint({'version':VERSION,'inputs':fingerprints,
'decoder':[(p.relative_to(root).as_posix(),hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted(code)]})
return _load(str(directory.resolve()),key,str(Path(cache_dir).resolve()) if cache_dir else '')
@functools.lru_cache(maxsize=4)
def _load(directory,key,cache_dir):
path=Path(cache_dir)/'_resources'/(key+'.json') if cache_dir else None
if path and path.is_file() and not path.is_symlink():
try:
entry=json.loads(path.read_text())
if entry['key']==key and vocabulary.fingerprint(entry['value'])==entry['digest']:
return decode(entry['value'])
except (ValueError,KeyError,TypeError,OverflowError):
pass
root=Path(directory)
libraries=(scales.load_library(root/'scales'),sequences.load_library(root/'sequences'),rhythm.load_library(root/'rhythms'))
if path:
value=json.loads(json.dumps(encode(libraries),default=str))
path.parent.mkdir(parents=True,exist_ok=True)
with tempfile.NamedTemporaryFile('w',dir=path.parent,delete=False) as out:
json.dump({'key':key,'digest':vocabulary.fingerprint(value),'value':value},out,separators=(',',':'))
temporary=out.name
os.replace(temporary,path)
return libraries