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, transpose_document 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 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="vocal", 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_chord_practice(source_path: Path, source_root: Path, output_dir: Path) -> int: return generate_key_variants(source_path, source_root, output_dir, 'chord') def generate_melody_practice(source_path: Path, source_root: Path, output_dir: Path) -> int: return generate_key_variants(source_path, source_root, output_dir, 'melody') def generate_key_variants(source_path: Path, source_root: Path, output_dir: Path, practice_type: str) -> int: base_document = practice_document(load_musicxml(source_path)) relative = source_path.relative_to(source_root) lesson_group = relative.parent.name exercise_id = slugify(source_path.stem) count = 0 for key in TARGET_KEYS: try: variant = transpose_guitar_practice(base_document, key) if practice_type == 'chord' else transpose_document(base_document, key) except ValueError as error: raise ValueError(f'{source_path}, target key {key}: {error}') from error write_json(output_dir / practice_type / lesson_group / exercise_id / key_file_name(key), variant) count += 1 return count def generate_practice_scores(source_dir: Path, output_dir: Path) -> int: if not source_dir.exists(): return 0 sources = sorted(source_dir.rglob('*.xml')) destinations: dict[Path, Path] = {} for path in sources: parts = path.relative_to(source_dir).parts if len(parts) != 3 or parts[0] not in ('chord', 'melody'): raise ValueError(f'{path}: practice source must be stored as practice///.xml') destination = Path(parts[0]) / parts[1] / slugify(path.stem) if destination in destinations: raise ValueError(f'{path}: practice output path collides with {destinations[destination]}: {destination}') destinations[destination] = path count = 0 for destination, path in destinations.items(): practice_type = destination.parts[0] count += generate_key_variants(path, source_dir / practice_type, output_dir, practice_type) return count