from __future__ import annotations from pathlib import Path from generator.score import copy_source_voices, make_arranged_document, make_measure, reidentify, slugify, source_staff, write_json from generator.musicxml import load_musicxml from generator.practice.guitar import transpose_guitar_practice from generator.transposition import TARGET_KEYS def practice_document(document: dict) -> dict: staff = source_staff(document) source_measures = staff.get("measures") if not isinstance(source_measures, list): raise ValueError("practice source must contain measures") measures: list[dict] = [] for index, measure in enumerate(source_measure for source_measure in source_measures if isinstance(source_measure, dict)): voices = copy_source_voices(measure, strip_lyrics=True) output_measure = make_measure( f"measure-{index + 1}", measure, [], section_label=str(measure.get("section_label", "")), ) output_measure['voices'] = voices for chord in output_measure['harmonies']: chord.pop('chord_optional', None) measures.append(output_measure) tuplets = reidentify(measures, staff.get('tuplets', [])) result = make_arranged_document( document, staff, staff_id="staff-arranged-practice", staff_name="Practice", role="guitar", arrangement_part_type="practice", measures=measures, tuplets=tuplets, octave_shifts=staff.get('octave_shifts', []), ) result['metadata'].setdefault('authored_key_signature', document['metadata']['key_signature']) return result def key_file_name(key: str) -> str: return slugify(key.replace("#", "sharp")) + ".json" def generate_key_variants(source_path: Path, output_dir: Path) -> int: base_document = practice_document(load_musicxml(source_path)) count = 0 for key in TARGET_KEYS: try: variant = transpose_guitar_practice(base_document, key) except ValueError as error: raise ValueError(f'{source_path}, target key {key}: {error}') from error write_json(output_dir / source_path.stem / key_file_name(key), variant) count += 1 return count def generate_practice_scores(source_dir: Path, output_dir: Path) -> int: if not source_dir.is_dir(): raise ValueError(f'practice source directory not found: {source_dir}') sources = sorted(source_dir.glob('*.xml')) for path in source_dir.rglob('*.xml'): parts = path.relative_to(source_dir).parts if len(parts) != 1 and (len(parts) != 2 or parts[0] != 'songs'): raise ValueError(f'{path}: practice sources belong directly under the data directory') for path in sources: if len(path.stem) != 3 or not path.stem.isascii() or not path.stem.isdecimal() or int(path.stem) == 0: raise ValueError(f'{path}: practice sources require a three-digit positive filename such as 001.xml') return sum(generate_key_variants(path, output_dir) for path in sources)