Spaces:
Sleeping
Sleeping
| """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() | |