Styles
ci / checks (push) Successful in 39m31s

This commit is contained in:
khodowany
2026-09-08 22:07:31 -05:00
parent 6e7db6fd97
commit fa7485d404
28 changed files with 522 additions and 292 deletions
+81 -21
View File
@@ -6,6 +6,8 @@ import re
from pathlib import Path
from typing import Any
from generator.styles import canonical_style
WHOLE_TICKS = 16384
HALF_TICKS = WHOLE_TICKS // 2
QUARTER_TICKS = WHOLE_TICKS // 4
@@ -28,13 +30,6 @@ RENDERABLE_TICKS = (
REST_PITCH = {"letter": "b", "accidental": "n", "octave": 4}
FORBIDDEN_OUTPUT_FIELDS = {
"tab_positions",
"tab_frets",
"finger",
"fingers",
"fingering",
"left_hand_finger",
"right_hand_finger",
"chord_optional",
}
@@ -91,6 +86,35 @@ def score_field(document: dict[str, Any], metadata: dict[str, Any], *names: str,
return fallback
def canonicalize_score_metadata(metadata: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
output = sanitize_json(metadata)
if not isinstance(output, dict):
output = {}
style_source = output.get("style")
try:
output["style"] = canonical_style(style_source)
except ValueError as error:
location = f"{path}: " if path is not None else ""
raise CompactScoreError(f"{location}{error}") from error
return output
def normalize_full_score_document(document: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
output = copy.deepcopy(document)
output.pop("version", None)
metadata_value = output.get("metadata", {})
if metadata_value is not None and not isinstance(metadata_value, dict):
location = f"{path}: " if path is not None else ""
raise CompactScoreError(f"{location}metadata must be an object")
metadata = copy.deepcopy(metadata_value) if isinstance(metadata_value, dict) else {}
for key in ("style",):
if key in output and key not in metadata:
metadata[key] = output[key]
output.pop(key, None)
output["metadata"] = canonicalize_score_metadata(metadata, path)
return output
def parse_duration_token(text: str, label: str) -> int:
dotted = text.endswith(".")
base_text = text[:-1] if dotted else text
@@ -352,9 +376,31 @@ class CompactMeasureParser:
return notes, self.tuplets
def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str]]:
COMPACT_MEASURE_CONTROL_FIELDS = {
"notes",
"notation",
"section",
"section_label",
"key",
"key_signature",
"time",
"time_signature",
"id",
"voices",
}
GENERATED_MEASURE_FIELDS = {
"id",
"section_label",
"time_signature",
"key_signature",
"voices",
}
def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str], dict[str, Any]]:
if isinstance(value, str):
return value, {}
return value, {}, {}
if isinstance(value, dict):
text = value.get("notes", value.get("notation", ""))
if not isinstance(text, str):
@@ -371,16 +417,21 @@ def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str]]:
field = value.get(source)
if isinstance(field, str):
overrides[target] = field
return text, overrides
extra_fields = {
key: sanitize_json(extra_value)
for key, extra_value in value.items()
if key not in COMPACT_MEASURE_CONTROL_FIELDS
}
return text, overrides, extra_fields
raise CompactScoreError(f"measure {index + 1}: compact measure must be a string or object")
def normalize_score_document(document: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
if isinstance(document.get("staves"), list):
return document
return normalize_full_score_document(document, path)
measures_value = document.get("measures")
if not isinstance(measures_value, list):
return document
return normalize_full_score_document(document, path)
metadata_value = document.get("metadata", {})
metadata = metadata_value if isinstance(metadata_value, dict) else {}
title = score_field(document, metadata, "title", fallback=human_label(path.stem if path is not None else "score"))
@@ -401,18 +452,19 @@ def normalize_score_document(document: dict[str, Any], path: Path | None = None)
"time_signature": time_signature,
})
for key, value in document.items():
if key in {"title", "composer", "key", "key_signature", "time", "time_signature", "clef", "part", "name", "role", "measures", "metadata"}:
if key in {"version", "title", "composer", "key", "key_signature", "time", "time_signature", "clef", "part", "name", "role", "measures", "metadata"}:
continue
output_metadata[key] = sanitize_json(value)
output_metadata = canonicalize_score_metadata(output_metadata, path)
measures: list[dict[str, Any]] = []
for index, value in enumerate(measures_value):
text, overrides = compact_measure_text(value, index)
text, overrides, extra_fields = compact_measure_text(value, index)
measure_time = overrides.get("time_signature", time_signature)
measure_key = overrides.get("key_signature", key_signature)
parser = CompactMeasureParser(text, measure_number=index + 1, time_signature=measure_time)
notes, tuplets = parser.parse()
measures.append({
measure = {
"id": f"measure-{index + 1}",
"section_label": overrides.get("section_label", ""),
"time_signature": measure_time,
@@ -422,10 +474,11 @@ def normalize_score_document(document: dict[str, Any], path: Path | None = None)
"notes": notes,
"tuplets": tuplets,
}],
})
}
measure.update(extra_fields)
measures.append(measure)
return {
"version": int(document.get("version", 1)) if isinstance(document.get("version", 1), int) else 1,
"metadata": output_metadata,
"staves": [{
"id": "staff-source",
@@ -706,6 +759,14 @@ def copy_source_voice(measure: dict[str, Any], *, strip_lyrics: bool, include_op
}
def measure_notation_metadata(source: dict[str, Any]) -> dict[str, Any]:
return {
key: sanitize_json(value)
for key, value in source.items()
if key not in GENERATED_MEASURE_FIELDS
}
def make_measure(measure_id: str, source: dict[str, Any], notes: list[dict[str, Any]], *, section_label: str = "", tuplets: list[Any] | None = None) -> dict[str, Any]:
measure: dict[str, Any] = {
"id": measure_id,
@@ -719,15 +780,14 @@ def make_measure(measure_id: str, source: dict[str, Any], notes: list[dict[str,
}
if section_label:
measure["section_label"] = section_label
measure.update(measure_notation_metadata(source))
return measure
def make_arranged_document(source_document: dict[str, Any], staff: dict[str, Any], *, staff_id: str, staff_name: str, role: str, arrangement_part_type: str, measures: list[dict[str, Any]]) -> dict[str, Any]:
metadata = sanitize_json(source_document.get("metadata", {}))
if not isinstance(metadata, dict):
metadata = {}
source_metadata = source_document.get("metadata", {})
metadata = canonicalize_score_metadata(source_metadata if isinstance(source_metadata, dict) else {})
output: dict[str, Any] = {
"version": int(source_document.get("version", 1)) if isinstance(source_document.get("version", 1), int) else 1,
"metadata": metadata,
"staves": [{
"id": staff_id,