mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-21 06:39:09 -04:00
New `sheet-music-ocr review` command that sends MusicXML output to an LLM (Claude or OpenAI, configurable via --provider flag) for reviewing and fixing common OCR errors like incorrect pitches, rhythms, key signatures, and garbled lyrics. Uses httpx for direct API calls. https://claude.ai/code/session_017GqUbuRDT58toRaxMtfRmf
124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
"""CLI tool for converting scanned sheet music to MusicXML.
|
|
|
|
Usage:
|
|
sheet-music-ocr convert scan.pdf
|
|
sheet-music-ocr convert scan.png -o output.mxml
|
|
sheet-music-ocr review output.mxml --provider claude
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
import typer
|
|
|
|
from python.sheet_music_ocr.audiveris import AudiverisError, run_audiveris
|
|
from python.sheet_music_ocr.review import LLMProvider, ReviewError, review_mxml
|
|
|
|
SUPPORTED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".tif"}
|
|
|
|
app = typer.Typer(help="Convert scanned sheet music to MusicXML using Audiveris.")
|
|
|
|
|
|
def extract_mxml_from_mxl(mxl_path: Path, output_path: Path) -> Path:
|
|
"""Extract the MusicXML file from an .mxl archive.
|
|
|
|
An .mxl file is a ZIP archive containing one or more .xml MusicXML files.
|
|
|
|
Args:
|
|
mxl_path: Path to the .mxl file.
|
|
output_path: Path where the extracted .mxml file should be written.
|
|
|
|
Returns:
|
|
The output path.
|
|
|
|
Raises:
|
|
FileNotFoundError: If no MusicXML file is found inside the archive.
|
|
"""
|
|
with zipfile.ZipFile(mxl_path, "r") as zf:
|
|
xml_names = [n for n in zf.namelist() if n.endswith(".xml") and not n.startswith("META-INF")]
|
|
if not xml_names:
|
|
msg = f"No MusicXML (.xml) file found inside {mxl_path}"
|
|
raise FileNotFoundError(msg)
|
|
with zf.open(xml_names[0]) as src, output_path.open("wb") as dst:
|
|
dst.write(src.read())
|
|
return output_path
|
|
|
|
|
|
@app.command()
|
|
def convert(
|
|
input_file: Annotated[Path, typer.Argument(help="Path to sheet music scan (PDF, PNG, JPG, TIFF).")],
|
|
output: Annotated[
|
|
Path | None,
|
|
typer.Option("--output", "-o", help="Output .mxml file path. Defaults to <input_stem>.mxml."),
|
|
] = None,
|
|
) -> None:
|
|
"""Convert a scanned sheet music file to MusicXML."""
|
|
if not input_file.exists():
|
|
typer.echo(f"Error: {input_file} does not exist.", err=True)
|
|
raise typer.Exit(code=1)
|
|
|
|
if input_file.suffix.lower() not in SUPPORTED_EXTENSIONS:
|
|
typer.echo(
|
|
f"Error: Unsupported format '{input_file.suffix}'. Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}",
|
|
err=True,
|
|
)
|
|
raise typer.Exit(code=1)
|
|
|
|
output_path = output or input_file.with_suffix(".mxml")
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
try:
|
|
mxl_path = run_audiveris(input_file, Path(tmpdir))
|
|
except AudiverisError as e:
|
|
typer.echo(f"Error: {e}", err=True)
|
|
raise typer.Exit(code=1) from e
|
|
|
|
try:
|
|
extract_mxml_from_mxl(mxl_path, output_path)
|
|
except FileNotFoundError as e:
|
|
typer.echo(f"Error: {e}", err=True)
|
|
raise typer.Exit(code=1) from e
|
|
|
|
typer.echo(f"Written: {output_path}")
|
|
|
|
|
|
@app.command()
|
|
def review(
|
|
input_file: Annotated[Path, typer.Argument(help="Path to MusicXML (.mxml) file to review.")],
|
|
output: Annotated[
|
|
Path | None,
|
|
typer.Option("--output", "-o", help="Output path for corrected .mxml. Defaults to overwriting input."),
|
|
] = None,
|
|
provider: Annotated[
|
|
LLMProvider,
|
|
typer.Option("--provider", "-p", help="LLM provider to use."),
|
|
] = LLMProvider.CLAUDE,
|
|
) -> None:
|
|
"""Review and fix a MusicXML file using an LLM."""
|
|
if not input_file.exists():
|
|
typer.echo(f"Error: {input_file} does not exist.", err=True)
|
|
raise typer.Exit(code=1)
|
|
|
|
if input_file.suffix.lower() != ".mxml":
|
|
typer.echo("Error: Input file must be a .mxml file.", err=True)
|
|
raise typer.Exit(code=1)
|
|
|
|
output_path = output or input_file
|
|
|
|
try:
|
|
corrected = review_mxml(input_file, provider)
|
|
except ReviewError as e:
|
|
typer.echo(f"Error: {e}", err=True)
|
|
raise typer.Exit(code=1) from e
|
|
|
|
output_path.write_text(corrected, encoding="utf-8")
|
|
typer.echo(f"Reviewed: {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|