pdftomusicxml / app.py
saitonakamura
Score preview: force currentColor black (staff lines visible on dark theme)
5147bb2
Raw
History Blame Contribute Delete
5.23 kB
"""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 "<p><em>Preview unavailable (verovio not installed).</em></p>"
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 "<p><em>Preview unavailable (could not parse MusicXML).</em></p>"
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 (
'<div style="overflow:auto; background:#ffffff; color:#000; '
f'padding:12px; border-radius:8px">{svg}</div>'
)
except Exception as exc: # pragma: no cover - preview is best-effort
return f"<p><em>Preview failed: {exc}</em></p>"
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()