File size: 3,420 Bytes
11330a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/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())