Download tests/record_expected.py from salgadev/jam-buddy: direct link, hf CLI and curl.
- Browser
- Download file 6.32 kB
-
https://huggingface.co/spaces/salgadev/jam-buddy/resolve/main/tests/record_expected.py
- Command line
-
hf download hf://spaces/salgadev/jam-buddy/tests/record_expected.py
-
curl -L -o record_expected.py https://huggingface.co/spaces/salgadev/jam-buddy/resolve/main/tests/record_expected.py
6.32 kB
| #!/usr/bin/env python3 | |
| """ | |
| Record the current script's output as a baseline YAML. | |
| Run this once to create the expected/ files. Re-run only when the user's | |
| contract changes (i.e., when they edit the .gp5 files and want the new | |
| expected counts). NOT part of the test suite itself. | |
| """ | |
| import os | |
| import sys | |
| import collections | |
| import subprocess | |
| import mido | |
| import yaml | |
| REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) | |
| GP5_DIR = os.path.join(REPO_ROOT, "gp5_songs") | |
| EXPECTED_DIR = os.path.join(os.path.dirname(__file__), "expected") | |
| SCRIPT = os.path.join(REPO_ROOT, "gp5_to_keyswitched_mid.py") | |
| NOTE_NAMES = { | |
| 17: "SusDown", 18: "SusUp", 20: "PalmDown", 23: "B0(SlideDown)", | |
| 24: "C1(SlideUp)", 26: "D1(Hammer)", 27: "D#1(SlideIn)", 91: "Bend", | |
| 9: "Harm", | |
| } | |
| def keyswitch_counts(midi_path): | |
| counts = collections.Counter() | |
| mids = mido.MidiFile(midi_path) | |
| if len(mids.tracks) < 2: | |
| return {} | |
| for msg in mids.tracks[1]: | |
| if hasattr(msg, "channel") and msg.type == "note_on" and msg.velocity > 0: | |
| counts[msg.note] += 1 | |
| return {NOTE_NAMES.get(n, str(n)): c for n, c in sorted(counts.items())} | |
| def conductor_facts(midi_path): | |
| mids = mido.MidiFile(midi_path) | |
| if not mids.tracks: | |
| return {} | |
| facts = {} | |
| for msg in mids.tracks[0]: | |
| if msg.type == "set_tempo": | |
| facts["tempo_bpm"] = round(60_000_000 / msg.tempo) | |
| elif msg.type == "time_signature": | |
| facts["numerator"] = msg.numerator | |
| facts["denominator"] = msg.denominator | |
| return facts | |
| def first_event_tick(midi_path): | |
| mid = mido.MidiFile(midi_path) | |
| if len(mid.tracks) < 2: | |
| return None | |
| abs_t = 0 | |
| for msg in mid.tracks[1]: | |
| abs_t += msg.time | |
| if hasattr(msg, "channel") and msg.type == "note_on" and msg.velocity > 0: | |
| return abs_t | |
| return None | |
| def last_event_tick(midi_path): | |
| mid = mido.MidiFile(midi_path) | |
| if len(mid.tracks) < 2: | |
| return None | |
| abs_t = 0 | |
| last = 0 | |
| for msg in mid.tracks[1]: | |
| abs_t += msg.time | |
| if hasattr(msg, "channel"): | |
| last = abs_t | |
| return last | |
| def track_is_bass(track_name): | |
| return "bass" in track_name.lower() | |
| def song_block(gp5_filename): | |
| """Run the script for each track in the song and return the expected data.""" | |
| gp5_path = os.path.join(GP5_DIR, gp5_filename) | |
| song_name = os.path.splitext(gp5_filename)[0] | |
| songs_subdir = os.path.join(REPO_ROOT, song_name) | |
| # First pass: run the script for each track to populate the output dir. | |
| # We always pass --only "<track>" to get a single output file per run. | |
| track_names = [] | |
| # We need to know the track names. Run the script once in verbose mode | |
| # and parse the printed output for the list of tracks. | |
| proc = subprocess.run( | |
| ["python", SCRIPT, gp5_path], | |
| capture_output=True, text=True, cwd=REPO_ROOT, | |
| ) | |
| # Lines like " Track Name [track:Track Name] channel N" | |
| for line in proc.stdout.splitlines(): | |
| line = line.strip() | |
| if not line or line.startswith("Skipped") or line.startswith("OK:"): | |
| continue | |
| if "No guitar" in line: | |
| continue | |
| if line.startswith("["): | |
| continue | |
| if "[track:" in line: | |
| name = line.split("[track:")[0].strip() | |
| channel = int(line.split("channel")[-1].strip()) | |
| track_names.append((name, channel)) | |
| tracks_expected = [] | |
| for idx, (name, channel) in enumerate(track_names): | |
| is_bass = track_is_bass(name) | |
| # Re-run for this specific track. If the same name appears more | |
| # than once, use --only-index to disambiguate. | |
| cmd = ["python", SCRIPT, gp5_path, "-o", name, "-q"] | |
| if is_bass: | |
| cmd.insert(4, "-b") | |
| same_name_count = sum(1 for n, _ in track_names if n == name) | |
| if same_name_count > 1: | |
| # 1-based index of this occurrence. | |
| only_index = sum(1 for n, _ in track_names[:idx] if n == name) + 1 | |
| cmd += ["--only-index", str(only_index)] | |
| try: | |
| subprocess.run(cmd, check=True, cwd=REPO_ROOT) | |
| except subprocess.CalledProcessError: | |
| # The run may have produced a multi-track file when | |
| # only-index wasn't applied. Skip this track. | |
| print(f" WARN: {name} (index {idx}) export failed, skipping", | |
| file=sys.stderr) | |
| continue | |
| # Sanity: the output file should exist. With --only-index, the | |
| # filename gets a " (N)" suffix when there are multiple matches. | |
| midi_path = os.path.join(songs_subdir, f"{name}_keyswitched.mid") | |
| candidate_paths = [midi_path] | |
| if same_name_count > 1: | |
| only_index = sum(1 for n, _ in track_names[:idx] if n == name) + 1 | |
| candidate_paths.insert(0, os.path.join( | |
| songs_subdir, f"{name} ({only_index})_keyswitched.mid")) | |
| actual_midi_path = None | |
| for p in candidate_paths: | |
| if os.path.exists(p): | |
| actual_midi_path = p | |
| break | |
| if actual_midi_path is None: | |
| print(f" WARN: expected output missing for {name} (index {idx})", | |
| file=sys.stderr) | |
| continue | |
| tracks_expected.append({ | |
| "name": name, | |
| "channel": channel, | |
| "include_bass": is_bass, | |
| "midi_path": f"{song_name}/{os.path.basename(actual_midi_path)}", | |
| "conductor": conductor_facts(actual_midi_path), | |
| "first_event_tick": first_event_tick(actual_midi_path), | |
| "last_event_tick": last_event_tick(actual_midi_path), | |
| "keyswitch_counts": keyswitch_counts(actual_midi_path), | |
| }) | |
| return { | |
| "source_gp5": gp5_filename, | |
| "songs_subdir": song_name, | |
| "tracks": tracks_expected, | |
| } | |
| def main(): | |
| gp5_files = sorted(f for f in os.listdir(GP5_DIR) if f.endswith(".gp5")) | |
| expected = {"songs": [song_block(gp5) for gp5 in gp5_files]} | |
| out_path = os.path.join(EXPECTED_DIR, "baseline.yaml") | |
| with open(out_path, "w") as f: | |
| yaml.dump(expected, f, sort_keys=False, default_flow_style=False) | |
| print(f"Wrote {out_path}") | |
| if __name__ == "__main__": | |
| main() | |