"""Hugging Face Space (Gradio) wrapper around the pdftomusic-mine pipeline. Thin web front-end only: it copies the uploaded PDF to a temp dir, calls the existing `pdftomusic_mine.pipeline.transcribe`, and returns the generated MusicXML + MIDI for download plus an optional in-page score preview rendered with Verovio (no MuseScore dependency). The transcription logic is untouched. Run locally: python app.py On HF Spaces: the package is installed from GitHub via requirements.txt. """ from __future__ import annotations import os import shutil import subprocess import sys import tempfile from pathlib import Path import gradio as gr # The pdftomusic-mine package lives in a PRIVATE GitHub repo, so it isn't in # requirements.txt (HF's build-time secret substitution is unreliable). Install it # here at runtime using the GH_TOKEN Space secret, which IS reliably available at # runtime. --no-deps because its deps (pymupdf, music21) come from requirements.txt. # Local dev where the package is already importable skips straight past this. _REPO = "github.com/saitonakamura/drumpdf2musicxml.git" def _ensure_pdftomusic() -> None: try: import pdftomusic_mine # noqa: F401 return except ImportError: pass token = os.environ.get("GH_TOKEN") if not token: raise RuntimeError( "pdftomusic_mine is not installed and the GH_TOKEN secret is unset. " "Add GH_TOKEN (a GitHub PAT with read access to the private repo) in the " "Space's Settings -> Secrets." ) url = f"git+https://{token}@{_REPO}" subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-deps", url]) _ensure_pdftomusic() from pdftomusic_mine.pipeline import transcribe # noqa: E402 # Verovio is optional: if it isn't installed the app still works, just without # the inline preview. (pure-Python, no MuseScore / xvfb needed.) try: import verovio _VEROVIO = verovio.toolkit() except Exception: # pragma: no cover - preview is best-effort _VEROVIO = None def _render_preview(musicxml: Path) -> str: """Render the first page of the score to an inline SVG, or a note if we can't.""" if _VEROVIO is None: return "

Preview unavailable (verovio not installed).

" try: # Load from a string, not the path: verovio's C++ file reader can choke on # non-ASCII paths (our temp dir mirrors the uploaded filename, e.g. CJK). xml_text = musicxml.read_text(encoding="utf-8") _VEROVIO.setOptions({"pageWidth": 2000, "scale": 40, "adjustPageHeight": True}) if not _VEROVIO.loadData(xml_text): return "

Preview unavailable (could not parse MusicXML).

" svg = _VEROVIO.renderToSVG(1) # Verovio fills noteheads black but strokes staff lines/stems with # `currentColor`, which on HF's dark theme is light. Pin a white card AND # color:#000 so currentColor resolves to black — full black-on-white score. return ( '
{svg}
' ) except Exception as exc: # pragma: no cover - preview is best-effort return f"

Preview failed: {exc}

" def convert(pdf_file: str | None, max_measures: int | None): if not pdf_file: raise gr.Error("Please upload a PDF first.") src = Path(pdf_file) # Work in a fresh temp dir so the output stem matches the uploaded filename. workdir = Path(tempfile.mkdtemp(prefix="pdftomusic_")) local_pdf = workdir / src.name shutil.copy(src, local_pdf) out_dir = workdir / "out" mm = int(max_measures) if max_measures and max_measures > 0 else None result = transcribe(local_pdf, out_dir, max_measures=mm) tempo = result["tempo"] info = ( f"pages={result['pages']} measures={result['measures']}" + (f" tempo={tempo} BPM" if tempo else "") ) musicxml = Path(result["musicxml"]) midi = Path(result["midi"]) return info, [str(musicxml), str(midi)], _render_preview(musicxml) with gr.Blocks(title="pdftomusic-mine") as demo: gr.Markdown( "# pdftomusic-mine\n" "Convert a notation-software **drum PDF** (MuseScore / Sibelius / Finale / " "Dorico) to **MusicXML + MIDI** by parsing the embedded SMuFL music font — " "no image OCR." ) with gr.Row(): with gr.Column(): pdf_in = gr.File(label="Drum-notation PDF", file_types=[".pdf"], type="filepath") max_in = gr.Number( label="Max measures (0 = all)", value=0, precision=0, info="Limit to the first N measures for a quick check.", ) run = gr.Button("Convert", variant="primary") with gr.Column(): info_out = gr.Textbox(label="Result", interactive=False) files_out = gr.Files(label="Downloads (.musicxml + .mid)") preview_out = gr.HTML(label="Score preview") run.click(convert, inputs=[pdf_in, max_in], outputs=[info_out, files_out, preview_out]) if __name__ == "__main__": demo.launch()