Spaces:
Sleeping
Sleeping
File size: 4,283 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 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 | #!/usr/bin/env python3
"""Fail closed when a NotePilot v2 debug bundle is not portable by itself."""
import base64
import json
import sys
from pathlib import Path
def fail(message):
print(f'FAIL: {message}', file=sys.stderr)
raise SystemExit(1)
def require(mapping, keys, label):
for key in keys:
if key not in mapping:
fail(f'missing {label}.{key}')
def main() -> int:
if len(sys.argv) != 2:
print('usage: validate_debug_bundle.py bundle.json', file=sys.stderr)
return 2
try:
bundle = json.loads(Path(sys.argv[1]).read_text())
except Exception as exc:
fail(f'invalid JSON: {exc}')
if bundle.get('schema') != 'notepilot.debug-bundle/v2': fail('unexpected schema')
require(bundle, ('meta', 'events', 'audio', 'clocks', 'diagnostics', 'analysis', 'schemaDocumentation'), 'bundle')
meta = bundle['meta']
require(meta, ('score', 'inputMode', 'userAgent', 'scoreEvents', 'latency'), 'meta')
if not meta['scoreEvents'].get('solo') or 'accompaniment' not in meta['scoreEvents']:
fail('incomplete score events')
if meta.get('micEngine') == 'follower' and not meta.get('follower', {}).get('options'):
fail('missing follower options')
if not any(e.get('type') == 'mic.gateCalibration' for e in bundle.get('events', [])): fail('missing calibration history')
events = bundle['events']
if not events or any(not isinstance(e.get('sessionTimeMs'), (int, float)) for e in events): fail('events lack numeric canonical sessionTimeMs')
if any(events[i]['sessionTimeMs'] > events[i + 1]['sessionTimeMs'] for i in range(len(events) - 1)):
fail('event timestamps are not monotonic')
for kind, description in (('blind.pitch', 'raw blind pitch'), ('mic.gate', 'gate history'), ('follow.frame', 'full follower frames')):
if not any(e.get('type') == kind for e in events): fail(f'missing {description}')
if not any(e.get('type') in ('tracker', 'follow.advance', 'skip', 'follow.resync') for e in events): fail('no tracker decisions')
if not isinstance(bundle['schemaDocumentation'], dict) or not bundle['schemaDocumentation'].get('sal'): fail('missing salience documentation')
if bundle['clocks'].get('master') != 'sessionTimeMs': fail('missing master-clock documentation')
timeline = bundle['diagnostics'].get('timeline')
if not isinstance(timeline, list) or not timeline: fail('missing graph-ready timeline')
required_row = ('tMs', 'db', 'gateOpen', 'onsetValue', 'blindMidi', 'expectedMidi', 'posterior', 'wrongProbability')
if any(any(k not in row for k in required_row) for row in timeline): fail('timeline rows are incomplete')
if any(timeline[i]['tMs'] > timeline[i + 1]['tMs'] for i in range(len(timeline) - 1)): fail('timeline timestamps are not monotonic')
if not isinstance(bundle['analysis'].get('attempts'), list): fail('missing attempt summary')
for key in ('falseAdvances', 'autoSkips', 'resyncs', 'hostRejections', 'missedAttempts'):
if not isinstance(bundle['analysis'].get(key), list): fail(f'missing analysis.{key}')
mic = bundle['audio'].get('microphone', {})
require(mic, ('codec', 'sampleRate', 'channels', 'durationMs', 'captureStartSessionMs', 'captureEndSessionMs', 'base64', 'bytes'), 'audio.microphone')
if not mic.get('base64'): fail('missing embedded microphone audio')
try:
raw = base64.b64decode(mic['base64'], validate=True)
except Exception as exc:
fail(f'invalid microphone base64: {exc}')
if len(raw) != mic.get('bytes'): fail('microphone byte count does not match base64')
if not raw.startswith(b'\x1aE\xdf\xa3'): fail('microphone payload is not WebM/EBML')
output = bundle['audio'].get('digitalOutput', {})
if output.get('base64'):
try: base64.b64decode(output['base64'], validate=True)
except Exception as exc: fail(f'invalid output base64: {exc}')
elif output.get('kind') != 'EXACT_EVENT_MANIFEST':
fail('digital output must be embedded audio or EXACT_EVENT_MANIFEST')
print(f'PASS: {len(events)} events; {len(timeline)} frame rows; mic WebM {len(raw)} bytes; {len(bundle["analysis"]["attempts"])} attempts')
return 0
if __name__ == '__main__':
raise SystemExit(main())
|