notepilot / scripts /plot_debug_bundle.py
jasonmdong's picture
Sync from GitHub via hub-sync
11330a4 verified
Raw History Blame Contribute Delete
3.42 kB
#!/usr/bin/env python3
"""Create four forensic plots using only one NotePilot debug bundle."""
import json
import sys
from pathlib import Path
def main():
if len(sys.argv) not in (2, 3):
print('usage: plot_debug_bundle.py bundle.json [output-directory]', file=sys.stderr); return 2
bundle = json.loads(Path(sys.argv[1]).read_text())
out = Path(sys.argv[2]) if len(sys.argv) == 3 else Path(sys.argv[1]).with_suffix('')
out.mkdir(parents=True, exist_ok=True)
rows = bundle['diagnostics']['timeline']
t = [r['tMs'] / 1000 for r in rows]
def vals(name): return [r.get(name) for r in rows]
events = bundle['events']
def marks(kind): return [e['sessionTimeMs'] / 1000 for e in events if e.get('type') == kind]
# Self-contained SVG avoids a plotting-package dependency in a fresh shell.
def graph(name, title, series, vertical=()):
width, height, pad = 1200, 460, 55
numeric = [v for _, values, _ in series for v in values if isinstance(v, (int, float))]
lo, hi = (min(numeric), max(numeric)) if numeric else (0, 1)
if lo == hi: lo -= 1; hi += 1
end = max(t) if t else 1
def xy(i, value): return (pad + (t[i] / end) * (width - 2 * pad), height - pad - ((value - lo) / (hi - lo)) * (height - 2 * pad))
paths = []
for label, values, color in series:
points = [f'{x:.2f},{y:.2f}' for i, v in enumerate(values) if isinstance(v, (int, float)) for x, y in [xy(i, v)]]
paths.append(f'<polyline fill="none" stroke="{color}" stroke-width="1.5" points="{" ".join(points)}"/><text x="{pad}" y="{22 + 16*len(paths)}" fill="{color}">{label}</text>')
lines = ''.join(f'<line x1="{pad+(x/end)*(width-2*pad):.2f}" x2="{pad+(x/end)*(width-2*pad):.2f}" y1="{pad}" y2="{height-pad}" stroke="#c44" opacity=".25"/>' for x in vertical)
svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}"><rect width="100%" height="100%" fill="white"/><text x="{pad}" y="20" font-size="16">{title}</text><line x1="{pad}" x2="{pad}" y1="{pad}" y2="{height-pad}" stroke="black"/><line x1="{pad}" x2="{width-pad}" y1="{height-pad}" y2="{height-pad}" stroke="black"/>{lines}{"".join(paths)}<text x="{pad}" y="{height-15}">session seconds (range 0–{end:.2f}); value range {lo:.3g}–{hi:.3g}</text></svg>'
(out / name).write_text(svg)
attempts = [e['sessionTimeMs']/1000 for e in events if e.get('physicalAttemptId') is not None]
graph('01-mic-level.svg', 'Microphone level / noise floor / attempts', [('mic dB', vals('db'), '#1769aa'), ('noise floor dB', vals('noiseFloorDb'), '#777')], attempts + marks('mic.gate'))
graph('02-pitch.svg', 'Blind YIN pitch and expected score', [('blind MIDI', vals('blindMidi'), '#1769aa'), ('expected MIDI', vals('expectedMidi'), '#e68613')], attempts)
graph('03-onset.svg', 'Onset feature and attack flags', [('onset ratio', vals('onsetValue'), '#6a3d9a')], [r['tMs']/1000 for r in rows if r.get('attack')])
graph('04-follower.svg', 'Follower posterior / WRONG / evidence', [('posterior', vals('posterior'), '#1769aa'), ('committed posterior', vals('committedPosterior'), '#e68613'), ('WRONG', vals('wrongProbability'), '#c44'), ('evidence nats', vals('evidenceNats'), '#6a3d9a')])
print(f'Wrote four plots to {out}'); return 0
if __name__ == '__main__': raise SystemExit(main())