Build Fix
ci / checks (push) Failing after 1h18m29s

This commit is contained in:
khodowany
2026-09-25 09:35:09 -05:00
parent 07559ad74c
commit 17326dbf56
34 changed files with 283 additions and 3723 deletions
+2
View File
@@ -57,6 +57,8 @@ def generate_assets(args: argparse.Namespace) -> None:
report('checking source paths')
for source in sorted(data_dir.rglob('*.xml')):
if source.is_relative_to(data_dir / 'rhythms'):
continue
if source.parent not in (data_dir / 'practice', data_dir / 'songs'):
raise ValueError(f'{source}: MusicXML sources belong directly under data/practice or data/songs')
source_paths(data_dir / 'songs')
+2 -4
View File
@@ -7,7 +7,6 @@ import tempfile
from pathlib import Path
APPLICATION_ID = 0x56414D50
SCHEMA_VERSION = 3
COMMON_COLUMNS = """
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 160),
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
@@ -24,8 +23,8 @@ def metadata(path: Path, root: Path) -> dict:
raise ValueError('score escapes the asset root')
text = path.read_text(encoding='utf-8')
document = json.loads(text)
if document.get('schema_version') != 3:
raise ValueError('expected a schema-v3 score bundle')
if not isinstance(document, dict) or not isinstance(document.get('music'), dict) or not isinstance(document.get('presentation'), dict):
raise ValueError('score bundle requires music and presentation objects')
music, presentation = document['music'], document['presentation']
if len(music['staves']) != 1 or (presentation['width'], presentation['height']) != (976, 484):
raise ValueError('expected one staff and 976x484 pages')
@@ -46,7 +45,6 @@ def populate(connection: sqlite3.Connection, root: Path) -> None:
raise ValueError(f'{root}: score asset directory not found')
connection.executescript(f'''
PRAGMA application_id = {APPLICATION_ID};
PRAGMA user_version = {SCHEMA_VERSION};
CREATE TABLE songs ({COMMON_COLUMNS}, part TEXT NOT NULL) STRICT;
CREATE TABLE practice_exercises ({COMMON_COLUMNS}, exercise_id TEXT NOT NULL, part TEXT NOT NULL) STRICT;
CREATE INDEX songs_menu ON songs(title COLLATE NOCASE, composer COLLATE NOCASE, part COLLATE NOCASE);
+2 -3
View File
@@ -17,7 +17,6 @@ from generator.engraving.types import EngravingError, Style
from generator.model import validate_document
ENGINE_NAME = 'Vamp Python'
ENGINE_VERSION = '1.0.0'
PAGE_WIDTH = 976
PAGE_HEIGHT = 484
@@ -70,8 +69,8 @@ def engrave_scores(paths: list[Path], root: Path, fonts: Path, cache_dir: Path |
else:
reused += 1
entries = [{**page, 'path': destination.name + '/' + page['path']} for page in pages]
music = {k: v for k, v in document.items() if k not in ('schema_version', 'playback')}
bundle = {'schema_version': 3, 'music': music, 'presentation': {'engine': ENGINE_NAME, 'engine_version': ENGINE_VERSION, 'style_sha256': fingerprint, 'width': PAGE_WIDTH, 'height': PAGE_HEIGHT, 'pages': entries}}
music = {k: v for k, v in document.items() if k != 'playback'}
bundle = {'music': music, 'presentation': {'engine': ENGINE_NAME, 'style_sha256': fingerprint, 'width': PAGE_WIDTH, 'height': PAGE_HEIGHT, 'pages': entries}}
if 'playback' in document:
bundle['playback'] = document['playback']
path.write_text(json.dumps(bundle, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
+1 -1
View File
@@ -22,7 +22,7 @@ def renderer_fingerprint(fonts, style) -> str:
return hashlib.sha256(json.dumps(renderer_identity(fonts, style), sort_keys=True).encode()).hexdigest()
def renderer_namespace(cache: Path, renderer: str) -> Path:
namespace = cache / 'v2' / renderer
namespace = cache / renderer
if namespace.parent.is_symlink() or namespace.is_symlink():
raise ValueError('engraving cache namespaces must not be symlinks')
return namespace
+7 -4
View File
@@ -4,7 +4,6 @@ import copy
from fractions import Fraction
from typing import Any
SCHEMA_VERSION = 3
MAX_INTEGER = (1 << 63) - 1
NOTE_TYPES = ('maxima', 'long', 'breve', 'whole', 'half', 'quarter', 'eighth', '16th', '32nd', '64th', '128th', '256th', '512th', '1024th')
TYPE_LENGTHS = {name: Fraction(32, 2 ** index) for index, name in enumerate(NOTE_TYPES)}
@@ -141,11 +140,15 @@ def make_event(identifier: str, onset: Fraction, duration: Fraction, kind: str,
return event
def validate_document(document: dict) -> None:
if document.get('schema_version') != SCHEMA_VERSION:
raise ValueError('unsupported score schema; regenerate score assets with the current generator')
if not isinstance(document, dict) or not isinstance(document.get('metadata'), dict):
raise ValueError('score requires a metadata object')
if any(not isinstance(document['metadata'].get(name), str) for name in ('title', 'composer', 'key_signature', 'time_signature')):
raise ValueError('score metadata requires title, composer, key_signature and time_signature strings')
staves = document.get('staves', [])
if len(staves) != 1:
if not isinstance(staves, list) or len(staves) != 1 or not isinstance(staves[0], dict):
raise ValueError('exactly one staff is currently supported')
if not isinstance(staves[0].get('measures'), list) or not staves[0]['measures']:
raise ValueError('score requires measures')
instruments = staves[0].get('instruments', [])
instrument_ids = {i['id'] for i in instruments}
if len(instruments) != len(instrument_ids) or any(not isinstance(i, str) or not i for i in instrument_ids):
+2 -2
View File
@@ -7,7 +7,7 @@ from pathlib import Path
from defusedxml import ElementTree
from generator import harmony
from generator.model import (ACCIDENTALS, SCHEMA_VERSION, TYPE_LENGTHS, alteration, fraction, key_from_fifths, key_label, meter_label, meter_length, pitch_number, rational, validate_document, written_for, written_length, )
from generator.model import (ACCIDENTALS, TYPE_LENGTHS, alteration, fraction, key_from_fifths, key_label, meter_label, meter_length, pitch_number, rational, validate_document, written_for, written_length, )
from generator.styles import canonical_style
class MusicXMLError(ValueError):
@@ -625,7 +625,7 @@ class Importer:
for p in n['pitches']:
p.pop('_absolute', None)
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
result = {'schema_version': SCHEMA_VERSION, 'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
result = {'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
result['staves'][0]['instruments'] = instruments
validate_document(result)
return result
+5 -5
View File
@@ -13,7 +13,7 @@ from generator.model import TYPE_LENGTHS, fraction, rational
from generator import pitch
SAMPLE_RATE = 48000
PROFILE = 'soft-struck-1'
PROFILE = 'soft-struck'
MAX_EVENTS = 1000000
class PlaybackUnavailable(ValueError):
@@ -145,7 +145,7 @@ def compile_timeline(document: dict) -> dict:
if len(events) > MAX_EVENTS:
raise PlaybackUnavailable('playback exceeds one million tone events')
events.sort(key=lambda e: (fraction(e['onset']), e['id']))
return {'version': 1, 'available': True, 'duration': rational(absolute + sum(insertions.values(), Fraction())), 'events': events}
return {'available': True, 'duration': rational(absolute + sum(insertions.values(), Fraction())), 'events': events}
def write_pcm(path: Path, values) -> str:
samples = array('h', (round(max(-1, min(1, sample)) * 32767) for sample in values))
@@ -193,13 +193,13 @@ def compile_tree(root: Path) -> None:
used[tone['id']] = tone
playback.update({'samples': list(used.values()), 'release': release, 'profile': PROFILE})
except PlaybackUnavailable as error:
playback = {'version': 1, 'available': False, 'reason': str(error)}
playback = {'available': False, 'reason': str(error)}
document['playback'] = playback
path.write_text(json.dumps(document, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
def validate_assets(playback: dict, root: Path, cache: dict | None = None) -> None:
if playback.get('version') != 1 or type(playback.get('available')) is not bool:
raise ValueError('invalid playback manifest version/availability')
if not isinstance(playback, dict) or type(playback.get('available')) is not bool:
raise ValueError('playback manifest requires boolean availability')
if not playback['available']:
if not isinstance(playback.get('reason'), str) or not playback['reason']:
raise ValueError('unavailable playback must explain why')
+8 -6
View File
@@ -6,19 +6,21 @@ import json
import re
from pathlib import Path
from generator.model import SCHEMA_VERSION, validate_document
from generator.model import validate_document
def load_json(path: Path) -> dict:
document = json.loads(path.read_text(encoding='utf-8'))
return validate_json(document, path)
def validate_json(document: dict, path: Path) -> dict:
if not isinstance(document, dict):
raise ValueError('expected a score object')
if 'music' in document:
from generator.engraving import ENGINE_NAME, ENGINE_VERSION, PAGE_HEIGHT, PAGE_WIDTH, check_page
from generator.engraving import ENGINE_NAME, PAGE_HEIGHT, PAGE_WIDTH, check_page
presentation = document.get('presentation', {})
if document.get('schema_version') != SCHEMA_VERSION or not isinstance(presentation, dict) or not isinstance(presentation.get('pages'), list) or not presentation['pages']:
if not isinstance(document['music'], dict) or not isinstance(presentation, dict) or not isinstance(presentation.get('pages'), list) or not presentation['pages']:
raise ValueError('invalid score bundle; regenerate score assets')
if (presentation.get('engine'), presentation.get('engine_version'), presentation.get('width'), presentation.get('height')) != (ENGINE_NAME, ENGINE_VERSION, PAGE_WIDTH, PAGE_HEIGHT):
if (presentation.get('engine'), presentation.get('width'), presentation.get('height')) != (ENGINE_NAME, PAGE_WIDTH, PAGE_HEIGHT):
raise ValueError('unsupported score presentation; regenerate score assets')
for page in presentation['pages']:
if not isinstance(page, dict) or not isinstance(page.get('path'), str) or not isinstance(page.get('sha256'), str):
@@ -33,7 +35,7 @@ def validate_json(document: dict, path: Path) -> dict:
if not re.fullmatch('[0-9a-f]{64}', page['sha256']) or hashlib.sha256(resolved.read_bytes()).hexdigest() != page['sha256']:
raise ValueError('score page checksum mismatch')
check_page(resolved)
document = {'schema_version': SCHEMA_VERSION, **document['music']}
document = document['music']
validate_document(document)
return document
@@ -117,7 +119,7 @@ def reidentify(measures: list[dict], source_tuplets: list[dict]) -> list[dict]:
return tuplets
def make_arranged_document(source_document: dict, staff: dict, *, staff_id: str, staff_name: str, role: str, arrangement_part_type: str, measures: list[dict], tuplets=None, octave_shifts=None) -> dict:
return {'schema_version': SCHEMA_VERSION, 'metadata': copy.deepcopy(source_document['metadata']), 'staves': [{'id': staff_id, 'name': staff_name, 'clef': copy.deepcopy(staff['clef']), 'role': role, 'arranged': True, 'arrangement_part_type': arrangement_part_type, 'measures': measures, 'tuplets': tuplets or [], 'octave_shifts': copy.deepcopy(octave_shifts or []), 'instruments': copy.deepcopy(staff.get('instruments', []))}]}
return {'metadata': copy.deepcopy(source_document['metadata']), 'staves': [{'id': staff_id, 'name': staff_name, 'clef': copy.deepcopy(staff['clef']), 'role': role, 'arranged': True, 'arrangement_part_type': arrangement_part_type, 'measures': measures, 'tuplets': tuplets or [], 'octave_shifts': copy.deepcopy(octave_shifts or []), 'instruments': copy.deepcopy(staff.get('instruments', []))}]}
def count_json_files(root: Path) -> int:
return sum(1 for p in root.rglob('*.json') if p.is_file())