File size: 3,752 Bytes
125bae1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env python3
"""Egystormy Tools — portable local MuScriptor transcription pack.

Usage:
    runtime/python transcribe.py <input_audio> <output.mid>

Fully offline and self-contained: the interpreter is ./runtime, all
third-party libraries live in ./libs, and the model weights in ./weights.
No Python installation, no setup, no network, no account.

Design note — no beat detection, on purpose: this entry point uses
``TranscriptionModel.transcribe`` + ``events_to_midi_bytes`` directly and
never calls the beat-grid path (``transcribe_and_postprocess`` /
``detect_tempo``). MuScriptor's beat detection hard-imports
beat_this -> torchaudio at call time, and a mismatched torchaudio build
kills the whole transcription. Skipping it costs nothing that matters
here: note timings stay true (seconds-accurate); the MIDI simply carries
no tempo map. (In this pack torch is 2.15.0+cpu while the newest
torchaudio on the CPU index is 2.11.1+cpu — no matched 2.15 pair exists,
which is exactly the hazard avoided.)
"""

import os
import sys
import time
from pathlib import Path

PACK = Path(__file__).resolve().parent
sys.path.insert(0, str(PACK / "libs"))
# Hard-offline: never let a library try to phone home.
os.environ.setdefault("HF_HUB_OFFLINE", "1")
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")

T0 = time.perf_counter()


def stamp(label: str) -> None:
    print(f"[pack] {label}: {time.perf_counter() - T0:.2f}s elapsed", flush=True)


def main() -> int:
    if len(sys.argv) != 3:
        print("usage: runtime/python transcribe.py <input_audio> <output.mid>",
              file=sys.stderr)
        return 2
    src = Path(sys.argv[1])
    dst = Path(sys.argv[2])
    weights = PACK / "weights" / "model.safetensors"
    if not src.exists():
        print(f"input audio not found: {src}", file=sys.stderr)
        return 2
    if not weights.exists():
        print(f"weights not found: {weights}", file=sys.stderr)
        return 2

    from muscriptor import TranscriptionModel
    from muscriptor.events import NoteStartEvent, ProgressEvent

    stamp("imports done")

    # libsndfile cannot decode m4a/AAC and some other containers; if an
    # ffmpeg happens to be available, transcode those to a temp WAV first.
    # (mp3/wav/flac/ogg decode natively and never reach this branch.)
    audio_arg = src
    tmp_wav = None
    if src.suffix.lower() in {".m4a", ".aac", ".opus", ".wma"}:
        import shutil
        import subprocess
        import tempfile

        ffmpeg = shutil.which("ffmpeg")
        if ffmpeg:
            fd, tmp_wav = tempfile.mkstemp(suffix=".wav")
            os.close(fd)
            subprocess.run(
                [ffmpeg, "-y", "-v", "error", "-i", str(src), tmp_wav],
                check=True,
            )
            audio_arg = Path(tmp_wav)

    model = TranscriptionModel.load_model(weights_path=weights, device="cpu")
    stamp("model loaded (cold start)")

    t_transcribe = time.perf_counter()
    events = []
    notes = 0
    for ev in model.transcribe(str(audio_arg)):
        if isinstance(ev, ProgressEvent):
            print(f"[pack] progress: chunk {ev.completed}/{ev.total}", flush=True)
        else:
            events.append(ev)
            if isinstance(ev, NoteStartEvent):
                notes += 1

    midi_bytes = model.events_to_midi_bytes(iter(events), beat_grid=None)
    dst.parent.mkdir(parents=True, exist_ok=True)
    dst.write_bytes(midi_bytes)
    if tmp_wav:
        os.unlink(tmp_wav)

    print(f"[pack] transcribe wall time: {time.perf_counter() - t_transcribe:.2f}s",
          flush=True)
    print(f"DONE notes={notes} output={dst} bytes={len(midi_bytes)}", flush=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())