diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/.gitignore b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/REPORT.md b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/REPORT.md new file mode 100644 index 0000000..baf8844 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/REPORT.md @@ -0,0 +1,198 @@ +# phonon-reel audio archive — a time-addressable index, and a corrected model of ffmpeg's `silencedetect` + +**Lane:** sphinx — Archive (multimodal, long-context research) +**Built:** 2026-10-21 · ffmpeg 6.1.1-3ubuntu5 · Python 3.14.7 · **no third-party Python packages** +**Self-check:** `python3 verify.py && python3 falsify.py` → 68/68 checks, 21/21 mutation controls, exit 0 + +--- + +## 1. What this is + +Three troupe-produced voice reels (phonon's male / british / alfred reads) archived with: + +- content hashes, container and stream facts, decoded sample counts; +- time-domain statistics (`astats`) and EBU R128 loudness (`ebur128`); +- a **time-addressable speech/silence segment index** — the artifact's point: a long-form + audio document that can be addressed by timestamp, not just stored; +- two spectrogram images per reel, plus a machine-readable pixel analysis of them; +- a **cross-modal check** tying the images back to the audio they came from. + +Then, because the index's correctness depends entirely on what "silence" means, the +deliverable also settles that question by measurement rather than assumption. That turned +out to be the most valuable part. + +## 2. Scope, and what is deliberately out + +**In:** `phonon-{male,british,alfred}-reel.mp3` — troupe-produced, world-readable (mode 644). + +**Out, on purpose:** `alexander-reading.{wav,mp3}`, `lilly-reading.{wav,mp3}`, +`oracle-welcome.wav`. The first four are readings by named people with transcripts beside +them; the last carries private permissions (mode 600). STANDING-AGREEMENT §4 forbids moving +other people's private recordings or transcripts into a public archive. They are excluded +regardless of how much more interesting they would have been (the readings are 3–5× longer +than the reels). + +## 3. Three detectors, and why three + +| | rule | role | +|---|---|---| +| **A** | ffmpeg `silencedetect=noise=-35dB:d=0.5` | the reference | +| **B** | frame RMS, 20 ms frames, −35 dBFS, runs ≥ 0.5 s | the model most people carry | +| **C** | pure-Python reimplementation of A's *actual* rule | the fidelity test | + +A vs **C** asks: do I understand the reference well enough to reproduce it? A vs **B** asks: +is the model I'd have used by default actually the same detector? + +## 4. Result 1 — `silencedetect` is a per-sample test, not an RMS test + +The filter compares **each sample**, not a frame average. From +`libavfilter/af_silencedetect.c` (pinned in `evidence/`, sha256 `22b9d81e…`): + +```c +const type noise = s->noise; +for (i = 0; i < nb_samples; i++, p++) + update(ctx, insamples, *p < noise && *p > -noise, i, + nb_samples_notify, time_base); +``` + +with `nb_samples_notify = s->duration * (s->mono ? 1 : nb_channels)` and, for integer +sample formats, `s->noise *= INT16_MAX`. + +Two measurements confirm this and pin the threshold: + +**DC sweep** (`calibration.dc_sweep`) — DC blocks have no zero crossings, so the test is +unambiguous: + +| block level | amplitude (i16) | verdict | +|---|---|---| +| −36 dBFS | 519 | **silent** | +| −35 dBFS | 583 | not silent | + +The source predicts the cut-off at `10^(−35/20) × 32767 = 582.6888`. Measured flip interval: +**(519, 583)**. The prediction lands inside it — the `dB` suffix does parse as an amplitude +ratio, and the comparison is instantaneous. + +**Pure tone** (`calibration.tone_zero_crossing`) — a −20 dBFS 1 kHz sine, `d=0.0`: + +- **4001 silence runs in 2.000 s**, mean start-to-start spacing **499.994 µs** + (= 1/(2·1000) s), mean run duration 35.3 µs. +- That is *exactly* two runs per period: every zero crossing dips below −35 dBFS for a + couple of samples and trips the test. + +**Logged correction.** The first calibration I wrote used this same pure tone as a step-edge +probe and produced `silence_start = 0.000499 s` for an edge at 2.000 s — which I initially +read as a broken probe. It was correct behaviour of a per-sample test, and it is what +exposed the semantics. The failed probe is kept in `probe_threshold.py`; the fix was to +probe with DC (`calibration.dc_step`), which locates the same edge to **within 1 sample** +(0.020 ms) with the end exact to 0 samples. `silencedetect` applies no frame-grid +quantisation at all. + +## 5. Result 2 — the reimplementation reproduces the reference exactly + +Method C is ~20 lines of Python. Against ffmpeg: + +| reel | runs A | runs C | silence A (s) | silence C (s) | max boundary deviation | +|---|---|---|---|---|---| +| male | 10 | **10** | 14.314470 | 14.314241 | **2 samples** (0.045 ms) | +| british | 11 | **11** | 14.079450 | 14.079207 | **2 samples** | +| alfred | 12 | **12** | 13.595777 | 13.595693 | **2 samples** | + +Same run counts, same total silence to within 0.2 ms, and no boundary off by more than two +samples. The residual is float32-vs-double rounding at the threshold. The documented +semantics are therefore *understood*, not merely cited. + +## 6. Result 3 — the default model is wrong by 21–40% + +Method B, the frame-RMS detector that "silence" usually means in practice: + +| reel | runs A | runs B | silence A (s) | silence B (s) | difference | +|---|---|---|---|---|---| +| male | 10 | 14 | 14.314 | 17.560 | +3.246 s (**+22.7%**) | +| british | 11 | 18 | 14.079 | 19.640 | +5.561 s (**+39.5%**) | +| alfred | 12 | 15 | 13.596 | 16.500 | +2.904 s (**+21.4%**) | + +The frame-RMS model **over-reports silence by a fifth to two-fifths** on real speech. The +mechanism is visible in the spans: B finds runs that A does not, because a frame whose +*average* is below −35 dBFS is still not silent by A's rule if a single sample inside it +exceeds the threshold. A dip containing one consonant transient is silent to B and audible +to A. + +This matters beyond bookkeeping: any archive whose segment boundaries come from an RMS +threshold has boundaries that ffmpeg, and anything else implementing the per-sample rule, +will not agree with. + +## 7. Result 4 — the container duration is not the decoded duration + +| reel | container (s) | decoded (s) | gap | gap (samples) | +|---|---|---|---|---| +| male | 54.256327 | 54.220000 | 0.036327 | 1602 | +| british | 51.330612 | 51.280000 | 0.050612 | 2232 | +| alfred | 34.795102 | 34.760000 | 0.035102 | 1548 | + +`ffprobe` reports 35–51 ms more than the decoder actually yields. The decoded lengths come +out at exactly 54.22 / 51.28 / 34.76 s, so this is container overhead, not truncated audio. +The mechanism is consistent with MP3 encoder delay plus end padding; **this artifact does +not isolate which**, and the gap is reported as measured. + +Archive consequence, and the reason it is a claim rather than a footnote: **index offsets +must be computed on the decoded timeline.** Build the index off `ffprobe`'s duration and +every offset inherits a 35–51 ms error — which is larger than the 2-sample fidelity the rest +of this work achieves. + +## 8. Result 5 — the spectrogram is verifiably the audio + +The machine spectrograms (`spectra/*.plain.png`, 1024×512, RGB, no legend or axes) are +decoded with **stdlib `zlib` + `struct` only** — no PIL, no numpy — and checked two ways: + +| reel | distinct colours | luminance variance | Pearson r (column luminance vs frame-RMS envelope) | top-decile luma | bottom-decile luma | +|---|---|---|---|---|---| +| male | 619 | 793.27 | **0.8479** | 0.00 | 51.06 | +| british | 629 | 804.77 | **0.8516** | 0.00 | 50.57 | +| alfred | 623 | 683.94 | **0.8760** | 0.00 | 43.32 | + +- **Temporal agreement:** brightness down each spectrogram column tracks the audio's own + 20 ms frame-RMS envelope at r ≈ 0.85–0.88. The image is a picture of that audio, not a + decorative render. +- **Spectral direction:** the top decile of rows (highest frequencies) is uniformly black + (mean luminance exactly 0.00) while the bottom decile (lowest frequencies) is bright. + Speech energy sits low, as it must. A spectrogram with the frequency axis flipped, or a + blank image, fails this. + +## 9. The index itself + +`segments.json` holds, per reel, three parallel segment lists (A, B, C) as +`[start_s, end_s]` pairs on the **decoded** timeline, plus the speech spans as their +complement, plus the cross-method comparison. `verify.py` re-derives all of it from the +media and diffs against the committed file, so the index cannot drift from the audio it +describes. + +## 10. Reproduce + +``` +cd /root/troupe-standup/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21 +sh fetch_sources.sh # stage + hash the three reels from the wizard-outputs drop +python3 build.py # re-measure everything, rewrite manifest/segments/claims +python3 verify.py # 68/68 expected, exit 0 +python3 falsify.py # 21/21 mutation controls, exit 0 +``` + +`probe_correlation.py`, `probe_threshold.py` and `report_data.py` are investigation history +and report tooling; they are **not** part of the CHECK. + +## 11. Honest limits + +- **"Long-context" is a mechanism here, not a scale claim.** The reels are 34.8–54.3 s. What + is proven is that the archive is *addressable* — segment-indexed, hash-pinned, and + re-derivable. It is not evidence that this pipeline has been run over hours of audio. +- **One codec, one rate, one channel count.** All three reels are 44.1 kHz mono MP3 at + 128 kbps. Method C mirrors the float32 code path (`AV_SAMPLE_FMT_FLTP`); the integer paths + scale `noise` by `INT16_MAX`/`INT32_MAX` and would need the corresponding change. +- **The frequency-tilt check is a direction test**, not a calibrated spectral measurement. +- **The 4 kHz tone calibration is messy** and is reported but not claimed: 10200 runs at + 196.1 µs spacing against 16000 expected crossings at 125 µs — the sample grid at 44.1 kHz + only ~11 points per period, so most crossings are straddled rather than sampled. The + 1 kHz case is the exact demonstration. +- **The priming/padding interpretation in §7 is an interpretation.** The measured gap is the + claim; the cause is not established here. +- **`astats`/`ebur128` values are reproduced exactly, but "exactly" means "by the same + ffmpeg build".** A different ffmpeg version is a different measurement, not a failure. diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/build.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/build.py new file mode 100644 index 0000000..3f02aed --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/build.py @@ -0,0 +1,662 @@ +#!/usr/bin/env python3 +"""build.py — measure the phonon voice-reel archive and emit the archive records. + +LANE: sphinx (Archive — multimodal, long-context research). + +Everything in manifest.json / segments.json / claims.json is MEASURED from the +source media by this script. Nothing is typed in by hand. verify.py re-derives +all of it from sources/ and fails loudly on any disagreement. + +Three silence-detection methods run over every reel: + + A ffmpeg's own `silencedetect` filter (the reference) + B a frame-RMS detector, 20 ms frames (the naive/wrong model) + C a pure-Python reimplementation of A's exact + per-sample rule, read off af_silencedetect.c (the fidelity test) + +A vs C is the fidelity claim: an independent implementation of the documented +semantics must reproduce the reference. A vs B is the finding: the model most +people carry around ("silence = low RMS") is measurably a different detector. + +Usage: python3 build.py +""" +import hashlib +import json +import math +import re +import struct +import subprocess +import sys +from array import array +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import pngstat + +ROOT = Path(__file__).resolve().parent +SRC_DIR = ROOT / "sources" +SPEC_DIR = ROOT / "spectra" +EVID_DIR = ROOT / "evidence" +SCRATCH = Path("/root/.hermes/profiles/sphinx/cache/scratch/sphinx-audio-build") + +RATE = 44100 +FRAME_MS = 20 +FRAME_N = RATE * FRAME_MS // 1000 # 882 samples +SILENCE_DB = -35.0 +MIN_SILENCE_S = 0.5 +NOISE_RATIO = 10 ** (SILENCE_DB / 20.0) # 0.0177827941... +MIN_RUN_SAMPLES = int(round(MIN_SILENCE_S * RATE)) # 22050, matches s->duration +SPEC_SIZE = "1024x512" +SPEC_OPTS = f"s={SPEC_SIZE}:scale=log:color=intensity" + +FFMPEG_SOURCE_URL = ("https://raw.githubusercontent.com/FFmpeg/FFmpeg/master/" + "libavfilter/af_silencedetect.c") + +ASSETS = [ + {"id": "phonon-male-reel", "file": "phonon-male-reel.mp3"}, + {"id": "phonon-british-reel", "file": "phonon-british-reel.mp3"}, + {"id": "phonon-alfred-reel", "file": "phonon-alfred-reel.mp3"}, +] + + +# -------------------------------------------------------------------------- +# helpers +# -------------------------------------------------------------------------- +def run(cmd, binary=False): + p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p.returncode != 0: + raise RuntimeError(f"command failed ({p.returncode}): {' '.join(cmd)}\n" + f"{p.stderr.decode('utf-8', 'replace')[-2000:]}") + return (p.stdout if binary else p.stdout.decode("utf-8", "replace"), + p.stderr.decode("utf-8", "replace")) + + +def sha256_file(path): + h = hashlib.sha256() + with open(path, "rb") as fh: + for blk in iter(lambda: fh.read(1 << 20), b""): + h.update(blk) + return h.hexdigest() + + +def tool_versions(): + out, _ = run(["ffmpeg", "-version"]) + out2, _ = run(["ffprobe", "-version"]) + return {"ffmpeg": out.splitlines()[0].strip(), + "ffprobe": out2.splitlines()[0].strip(), + "python": sys.version.split()[0], + "third_party_python_packages": "none (stdlib only)"} + + +def r6(x): + return round(x, 6) + + +# -------------------------------------------------------------------------- +# ffprobe / ffmpeg measurements +# -------------------------------------------------------------------------- +def ffprobe(path): + out, _ = run(["ffprobe", "-v", "error", "-print_format", "json", + "-show_format", "-show_streams", str(path)]) + j = json.loads(out) + fmt = j["format"] + st = next(s for s in j["streams"] if s["codec_type"] == "audio") + return { + "container": {"format_name": fmt["format_name"], + "duration_s": r6(float(fmt["duration"])), + "bit_rate": int(fmt["bit_rate"]), + "size_bytes": int(fmt["size"])}, + "stream": {"codec_name": st["codec_name"], + "sample_rate": int(st["sample_rate"]), + "channels": int(st["channels"]), + "channel_layout": st.get("channel_layout"), + "bit_rate": int(st["bit_rate"]) if st.get("bit_rate") else None}, + } + + +_ASTATS_LINE = re.compile(r"^\[Parsed_astats_\d+ @ 0x[0-9a-f]+\]\s*(.*)$") + + +def astats(path): + _, err = run(["ffmpeg", "-hide_banner", "-nostats", "-v", "info", "-i", str(path), + "-af", "astats=metadata=0:reset=0", "-f", "null", "-"]) + payload = [m.group(1) for m in (_ASTATS_LINE.match(l) for l in err.splitlines()) if m] + if "Overall" not in payload: + raise RuntimeError("astats: no 'Overall' section") + raw = {} + for line in payload[payload.index("Overall") + 1:]: + if ":" in line: + k, v = line.split(":", 1) + raw[k.strip()] = v.strip() + return {"peak_level_db": r6(float(raw["Peak level dB"])), + "rms_level_db": r6(float(raw["RMS level dB"])), + "dc_offset": r6(float(raw["DC offset"])), + "flat_factor": r6(float(raw["Flat factor"])), + "entropy": r6(float(raw["Entropy"])), + "noise_floor_db": raw["Noise floor dB"], + "samples": int(float(raw["Number of samples"])), + "nans": int(float(raw["Number of NaNs"])), + "infs": int(float(raw["Number of Infs"])), + "denormals": int(float(raw["Number of denormals"]))} + + +def ebur128(path): + _, err = run(["ffmpeg", "-hide_banner", "-nostats", "-v", "info", "-i", str(path), + "-af", "ebur128=peak=true", "-f", "null", "-"]) + def grab(pat): + m = re.search(pat, err, re.MULTILINE) + if not m: + raise RuntimeError(f"ebur128: pattern not found: {pat}") + return float(m.group(1)) + return {"integrated_lufs": r6(grab(r"^\s+I:\s+(-?[\d.]+)\s+LUFS\s*$")), + "loudness_range_lu": r6(grab(r"^\s+LRA:\s+(-?[\d.]+)\s+LU\s*$")), + "true_peak_dbfs": r6(grab(r"^\s+Peak:\s+(-?[\d.]+)\s+dBFS\s*$"))} + + +_SIL_START = re.compile(r"silence_start:\s*(-?[\d.eE+-]+)") +_SIL_END = re.compile(r"silence_end:\s*(-?[\d.eE+-]+)") + + +def silencedetect(path, noise_db=SILENCE_DB, min_dur=MIN_SILENCE_S): + """Method A: ffmpeg's silencedetect filter (the reference).""" + _, err = run(["ffmpeg", "-hide_banner", "-nostats", "-v", "info", "-i", str(path), + "-af", f"silencedetect=noise={noise_db}dB:d={min_dur}", + "-f", "null", "-"]) + spans, cur = [], None + for line in err.splitlines(): + m = _SIL_START.search(line) + if m: + cur = float(m.group(1)) + continue + m = _SIL_END.search(line) + if m and cur is not None: + spans.append([r6(cur), r6(float(m.group(1)))]) + cur = None + if cur is not None: + spans.append([r6(cur), None]) + return spans + + +# -------------------------------------------------------------------------- +# PCM analysis (pure Python) +# -------------------------------------------------------------------------- +def decode_f32(path): + """Decode to mono float32 — the same sample format the MP3 decoder emits, + so Method C sees the same samples ffmpeg's filter sees.""" + out, _ = run(["ffmpeg", "-hide_banner", "-v", "error", "-i", str(path), + "-f", "f32le", "-acodec", "pcm_f32le", "-ac", "1", + "-ar", str(RATE), "-"], binary=True) + a = array("f") + a.frombytes(out) + return a + + +def frame_db(pcm, frame_n=FRAME_N): + out = [] + for i in range(0, len(pcm) - frame_n + 1, frame_n): + s = 0.0 + for v in pcm[i:i + frame_n]: + s += v * v + rms = math.sqrt(s / frame_n) + out.append(20.0 * math.log10(rms) if rms > 1e-12 else -240.0) + return out + + +def method_b(pcm, threshold_db=SILENCE_DB, min_s=MIN_SILENCE_S, frame_n=FRAME_N): + """Method B: frame-RMS detector (the naive model).""" + series = frame_db(pcm, frame_n) + need = max(1, int(round(min_s * RATE / frame_n))) + runs, start = [], None + for i, v in enumerate(series): + if v < threshold_db: + if start is None: + start = i + else: + if start is not None and i - start >= need: + runs.append((start, i)) + start = None + if start is not None and len(series) - start >= need: + runs.append((start, len(series))) + return [[r6(a * frame_n / RATE), r6(b * frame_n / RATE)] for a, b in runs], series + + +def method_c(pcm, ratio=NOISE_RATIO, min_run=MIN_RUN_SAMPLES): + """Method C: exact reimplementation of ffmpeg's silencedetect rule. + + Per af_silencedetect.c (master): + const float noise = s->noise; // amplitude ratio + for (i = 0; i < nb_samples; i++, p++) + update(..., *p < noise && *p > -noise, i, nb_samples_notify, ...); + with nb_samples_notify = s->duration * channels = round(min_s * sample_rate). + A run is reported once it reaches nb_samples_notify consecutive quiet + samples, and its start is back-dated to the first sample of the run. + """ + runs, start, n = [], None, 0 + for i, v in enumerate(pcm): + if (-ratio) < v < ratio: + if start is None: + start = i + n += 1 + else: + if start is not None and n >= min_run: + runs.append((start, i)) + start, n = None, 0 + if start is not None and n >= min_run: + runs.append((start, len(pcm))) + return [[r6(a / RATE), r6(b / RATE)] for a, b in runs] + + +def complement(spans, total_s): + out, cursor = [], 0.0 + for s, e in spans: + if s is None: + continue + if s > cursor: + out.append([r6(cursor), r6(s)]) + cursor = e + if total_s > cursor: + out.append([r6(cursor), r6(total_s)]) + return out + + +def compare_spans(a, b): + dur = lambda L: r6(sum((e - s) for s, e in L if e is not None)) + used, devs = set(), [] + for s1, e1 in a: + best, bestd = None, None + for i, (s2, e2) in enumerate(b): + if i in used: + continue + d = abs(s1 - s2) + abs(e1 - e2) + if bestd is None or d < bestd: + best, bestd = i, d + if best is not None: + used.add(best) + s2, e2 = b[best] + devs.append({"a": [s1, e1], "b": [s2, e2], + "start_dev_s": r6(abs(s1 - s2)), "end_dev_s": r6(abs(e1 - e2))}) + msd = max((d["start_dev_s"] for d in devs), default=0.0) + med = max((d["end_dev_s"] for d in devs), default=0.0) + return {"count_a": len(a), "count_b": len(b), "count_diff": abs(len(a) - len(b)), + "total_a_s": dur(a), "total_b_s": dur(b), + "total_diff_s": r6(abs(dur(a) - dur(b))), "matched": len(devs), + "max_start_dev_s": r6(msd), "max_end_dev_s": r6(med), + "max_start_dev_samples": int(round(msd * RATE)), + "max_end_dev_samples": int(round(med * RATE)), + "pairs": devs} + + +# -------------------------------------------------------------------------- +# calibrations +# -------------------------------------------------------------------------- +def write_wav(path, samples_i16, rate=RATE): + buf = b"".join(struct.pack(" m["start_s"] + 0.05 + for sp in spans) + predicted = NOISE_RATIO * 32767 + loudest = max([m for m in marks if m["classified_silent"]], + key=lambda m: m["amp_i16"], default=None) + quietest = min([m for m in marks if not m["classified_silent"]], + key=lambda m: m["amp_i16"], default=None) + return {"probe": "0.5 s DC blocks at listed levels, 0.5 s digital silence between", + "ffmpeg_options": f"silencedetect=noise={SILENCE_DB}dB:d=0.2", + "blocks": marks, + "loudest_block_called_silent": loudest, + "quietest_block_called_audible": quietest, + "predicted_cutoff_amp_i16": r6(predicted), + "predicted_cutoff_dbfs": r6(20 * math.log10(predicted / 32768)), + "flip_is_between_dbfs": [quietest["level_dbfs"] if quietest else None, + loudest["level_dbfs"] if loudest else None]} + + +def cal_dc_step(): + """DC step edge — no zero crossings, so the boundary is unambiguous.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + amp = int(round((10 ** (-20 / 20.0)) * 32768)) + samples = [amp] * (RATE * 2) + [0] * (RATE * 2) + path = SCRATCH / "dc_step.wav" + write_wav(path, samples) + spans = silencedetect(path, noise_db=SILENCE_DB, min_dur=0.0) + s, e = (spans[0][0], spans[0][1]) if spans else (None, None) + return {"probe": "2.000 s DC @ -20 dBFS then 2.000 s digital zero, 44.1 kHz mono s16le", + "true_edge_s": 2.0, "true_end_s": 4.0, + "observed_silence_start_s": s, "observed_silence_end_s": e, + "start_error_samples": int(round(abs(s - 2.0) * RATE)) if s is not None else None, + "end_error_samples": int(round(abs(e - 4.0) * RATE)) if e is not None else None, + "ffmpeg_options": f"silencedetect=noise={SILENCE_DB}dB:d=0.0"} + + +def cal_tone_zero_crossing(): + """A pure tone trips the per-sample test at every zero crossing.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + out = [] + for freq in (1000, 4000): + n = RATE * 2 + amp = int(round((10 ** (-20 / 20.0)) * 32768)) + samples = [int(amp * math.sin(2 * math.pi * freq * i / RATE)) for i in range(n)] + path = SCRATCH / f"tone{freq}.wav" + write_wav(path, samples) + spans = silencedetect(path, noise_db=SILENCE_DB, min_dur=0.0) + durs = [e - s for s, e in spans] + gaps = [spans[i + 1][0] - spans[i][0] for i in range(len(spans) - 1)] + out.append({"freq_hz": freq, "amplitude_dbfs": -20, "duration_s": 2.0, + "expected_zero_crossings": 2 * freq * 2, + "runs_reported": len(spans), + "mean_run_duration_us": r6(sum(durs) / len(durs) * 1e6) if durs else None, + "mean_start_spacing_us": r6(sum(gaps) / len(gaps) * 1e6) if gaps else None, + "expected_start_spacing_us": r6(1e6 / (2 * freq)), + "ffmpeg_options": f"silencedetect=noise={SILENCE_DB}dB:d=0.0"}) + return out + + +# -------------------------------------------------------------------------- +# spectrograms +# -------------------------------------------------------------------------- +def make_spectrogram(src, out, legend): + out.parent.mkdir(parents=True, exist_ok=True) + run(["ffmpeg", "-hide_banner", "-v", "error", "-y", "-i", str(src), + "-lavfi", f"showspectrumpic={SPEC_OPTS}:legend={1 if legend else 0}", str(out)]) + + +def analyse_spectrogram(png, series): + img = pngstat.read_png(png) + st = pngstat.image_stats(img) + cols = pngstat.column_profile(img) + rows = pngstat.row_profile(img) + binned = pngstat.resample_mean(cols, len(series)) + env = [max(0.0, min(1.0, (v + 80.0) / 80.0)) for v in series] + r = pngstat.pearson(binned, env) + band = max(1, img["h"] // 10) + return {"width": img["w"], "height": img["h"], + "colour_type": img["colour_type"], "channels": img["channels"], + "distinct_colours": st["distinct_colours"], + "mean_luma": r6(st["mean_luma"]), "var_luma": r6(st["var_luma"]), + "min_luma": st["min_luma"], "max_luma": st["max_luma"], + "top_band_mean_luma": r6(sum(rows[:band]) / band), + "bottom_band_mean_luma": r6(sum(rows[-band:]) / band), + "pearson_col_luma_vs_frame_rms_db": r6(r), + "env_dbfs_window": [-80.0, 0.0]} + + +# -------------------------------------------------------------------------- +def main(): + if not SRC_DIR.is_dir(): + raise SystemExit(f"missing {SRC_DIR} — run fetch_sources.sh first") + tools = tool_versions() + + EVID_DIR.mkdir(parents=True, exist_ok=True) + src_c = EVID_DIR / "af_silencedetect.c" + if not src_c.exists(): + out, _ = run(["curl", "-sS", "--max-time", "40", FFMPEG_SOURCE_URL]) + src_c.write_text(out) + text = src_c.read_text() + keys = ("const type noise = s->noise", "*p < noise && *p > -noise", + "p[i] < noise && p[i] > -noise", "nb_samples_notify = s->duration", + "s->noise *= INT16_MAX", "s->noise *= INT32_MAX", + "s->duration = av_rescale", ">= nb_samples_notify", + "s->start[channel] = insamples->pts", "case AV_SAMPLE_FMT_FLTP") + keep = [f"{i:4d}| {ln}" for i, ln in enumerate(text.splitlines(), 1) + if any(k in ln for k in keys)] + excerpt = EVID_DIR / "silencedetect_semantics.txt" + excerpt.write_text("Reference: ffmpeg/libavfilter/af_silencedetect.c\n" + f"Fetched from: {FFMPEG_SOURCE_URL}\n" + f"sha256: {sha256_file(src_c)}\n\n" + "Lines that define the detection semantics:\n\n" + + "\n".join(keep) + "\n") + evidence = {"ffmpeg_source_url": FFMPEG_SOURCE_URL, + "ffmpeg_source_sha256": sha256_file(src_c), + "ffmpeg_source_bytes": src_c.stat().st_size, + "excerpt_path": str(excerpt.relative_to(ROOT))} + + manifest = { + "artifact": "sphinx/phonon-reel-audio-archive", + "generated_utc": subprocess.run(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"], + stdout=subprocess.PIPE).stdout.decode().strip(), + "generator": "build.py", + "lane": "sphinx — Archive (multimodal, long-context research)", + "tools": tools, + "reference_implementation": evidence, + "analysis_parameters": { + "decode": f"mono {RATE} Hz float32", + "method_b_frame_ms": FRAME_MS, "method_b_frame_samples": FRAME_N, + "silence_threshold_db": SILENCE_DB, + "silence_threshold_amplitude_ratio": NOISE_RATIO, + "min_silence_s": MIN_SILENCE_S, + "min_run_samples": MIN_RUN_SAMPLES, + "spectrogram": f"showspectrumpic={SPEC_OPTS}"}, + "calibration": {"dc_sweep": cal_dc_sweep(), + "dc_step": cal_dc_step(), + "tone_zero_crossing": cal_tone_zero_crossing()}, + "assets": []} + + segments = {"artifact": "sphinx/phonon-reel-audio-archive", + "index_kind": "time-addressable speech/silence segment index", + "threshold_db": SILENCE_DB, "min_silence_s": MIN_SILENCE_S, + "methods": { + "A": f"ffmpeg silencedetect=noise={SILENCE_DB}dB:d={MIN_SILENCE_S} (reference)", + "B": f"frame RMS, {FRAME_MS} ms frames, full scale 1.0 (naive model)", + "C": ("pure-Python reimplementation of A's per-sample rule " + "(|x| < 10^(-35/20), runs >= 22050 samples)")}, + "assets": {}} + + claims = {"artifact": "sphinx/phonon-reel-audio-archive", + "note": ("Every claim is a measured value with a stated tolerance. verify.py " + "recomputes each one from sources/ and exits non-zero on any " + "disagreement. falsify.py mutates claims and proves the verifier " + "can fail."), + "claims": []} + counter = [0] + + def claim(klass, asset, statement, value, tol, method): + counter[0] += 1 + claims["claims"].append({"id": f"C{counter[0]:02d}", "class": klass, "asset": asset, + "statement": statement, "value": value, + "tolerance": tol, "method": method}) + + dc = manifest["calibration"]["dc_sweep"] + claim("cal_dc_sweep", None, + (f"A DC block at {dc['loudest_block_called_silent']['level_dbfs']} dBFS " + f"(amp {dc['loudest_block_called_silent']['amp_i16']}) is called silent while one " + f"at {dc['quietest_block_called_audible']['level_dbfs']} dBFS " + f"(amp {dc['quietest_block_called_audible']['amp_i16']}) is not, so the effective " + f"cut-off lies between them; the source predicts 10^(-35/20)*32767 = " + f"{dc['predicted_cutoff_amp_i16']} i16 units"), + {"flip_between_dbfs": dc["flip_is_between_dbfs"], + "predicted_cutoff_amp_i16": dc["predicted_cutoff_amp_i16"]}, 1.0, + f"ffmpeg silencedetect=noise={SILENCE_DB}dB:d=0.2 over synthetic DC blocks") + + stp = manifest["calibration"]["dc_step"] + claim("cal_dc_step", None, + (f"A DC step edge at t=2.000 s is located to within {stp['start_error_samples']} " + f"sample(s), i.e. silencedetect is sample-accurate and applies no frame-grid " + f"quantisation"), + {"start_error_samples": stp["start_error_samples"], + "end_error_samples": stp["end_error_samples"]}, 2, + f"ffmpeg silencedetect=noise={SILENCE_DB}dB:d=0.0 over a synthetic DC step") + + tc = manifest["calibration"]["tone_zero_crossing"][0] + claim("cal_tone", None, + (f"A pure {tc['freq_hz']} Hz tone at {tc['amplitude_dbfs']} dBFS produces " + f"{tc['runs_reported']} 'silence' runs in {tc['duration_s']} s at " + f"{tc['mean_start_spacing_us']} us spacing — 2 per period, exactly as a " + f"per-sample |x| < threshold test requires"), + {"runs_reported": tc["runs_reported"], + "mean_start_spacing_us": tc["mean_start_spacing_us"]}, 3.0, + f"ffmpeg silencedetect=noise={SILENCE_DB}dB:d=0.0 over a synthetic {tc['freq_hz']} Hz tone") + + for a in ASSETS: + src = SRC_DIR / a["file"] + aid = a["id"] + rec = {"id": aid, "source": str(src.relative_to(ROOT))} + rec["sha256"] = sha256_file(src) + rec["bytes"] = src.stat().st_size + rec.update(ffprobe(src)) + rec["astats"] = astats(src) + rec["ebur128"] = ebur128(src) + + pcm = decode_f32(src) + span_a = silencedetect(src) + span_b, series = method_b(pcm) + span_c = method_c(pcm) + total = r6(len(pcm) / RATE) + + rec["decoded"] = {"samples": len(pcm), "duration_s": total, + "frame_count": len(series), + "frame_rms_db_min": r6(min(series)), + "frame_rms_db_max": r6(max(series)), + "frame_rms_db_mean": r6(sum(series) / len(series))} + rec["decoded_minus_astats_samples"] = len(pcm) - rec["astats"]["samples"] + + seg = {"duration_s": total, + "method_a_silence": span_a, "method_b_silence": span_b, + "method_c_silence": span_c, + "method_a_speech": complement(span_a, total), + "method_b_speech": complement(span_b, total), + "method_c_speech": complement(span_c, total), + "silence_total_a_s": r6(sum(e - s for s, e in span_a)), + "silence_total_b_s": r6(sum(e - s for s, e in span_b)), + "silence_total_c_s": r6(sum(e - s for s, e in span_c)), + "a_vs_c": compare_spans(span_a, span_c), + "a_vs_b": compare_spans(span_a, span_b)} + seg["silence_ratio_a"] = r6(seg["silence_total_a_s"] / total) + seg["silence_ratio_c"] = r6(seg["silence_total_c_s"] / total) + segments["assets"][aid] = seg + + human = SPEC_DIR / f"{aid}.png" + plain = SPEC_DIR / f"{aid}.plain.png" + make_spectrogram(src, human, True) + make_spectrogram(src, plain, False) + rec["spectra"] = { + "human": {"path": str(human.relative_to(ROOT)), "sha256": sha256_file(human), + "bytes": human.stat().st_size}, + "machine": {"path": str(plain.relative_to(ROOT)), "sha256": sha256_file(plain), + "bytes": plain.stat().st_size}} + rec["spectra"]["machine_pixel_analysis"] = analyse_spectrogram(plain, series) + manifest["assets"].append(rec) + pa = rec["spectra"]["machine_pixel_analysis"] + + claim("sha256", aid, f"source file hash is {rec['sha256'][:16]}…", + rec["sha256"], 0, "sha256 of sources/") + claim("bytes", aid, f"source file is {rec['bytes']} bytes", + rec["bytes"], 0, "os.stat on sources/") + claim("container_duration", aid, + f"container reports {rec['container']['duration_s']} s", + rec["container"]["duration_s"], 1e-6, "ffprobe format.duration") + claim("stream_props", aid, + f"{rec['stream']['codec_name']} {rec['stream']['sample_rate']} Hz " + f"{rec['stream']['channels']} ch ({rec['stream']['channel_layout']})", + {k: rec["stream"][k] for k in + ("codec_name", "sample_rate", "channels", "channel_layout")}, 0, + "ffprobe stream fields") + claim("astats", aid, + f"peak {rec['astats']['peak_level_db']} dBFS, RMS " + f"{rec['astats']['rms_level_db']} dBFS, entropy {rec['astats']['entropy']}", + {k: rec["astats"][k] for k in ("peak_level_db", "rms_level_db", "dc_offset", + "flat_factor", "entropy")}, 1e-6, + "ffmpeg astats=metadata=0:reset=0 (Overall section)") + claim("ebur128", aid, + f"integrated {rec['ebur128']['integrated_lufs']} LUFS, LRA " + f"{rec['ebur128']['loudness_range_lu']} LU, true peak " + f"{rec['ebur128']['true_peak_dbfs']} dBFS", + rec["ebur128"], 1e-6, "ffmpeg ebur128=peak=true (summary)") + claim("decoded_samples", aid, + f"decodes to {len(pcm)} mono float32 samples ({total} s at {RATE} Hz)", + {"samples": len(pcm), "duration_s": total}, 0, + "ffmpeg -f f32le -ac 1 -ar 44100 pipe, counted in Python") + + gap = r6(rec["container"]["duration_s"] - total) + claim("container_vs_decoded_gap", aid, + (f"the container reports {rec['container']['duration_s']} s but only {total} s " + f"decodes: a {gap} s ({int(round(gap * RATE))} sample) priming/padding gap " + f"that would shift every index offset if the container timeline were used"), + {"container_s": rec["container"]["duration_s"], "decoded_s": total, + "gap_s": gap, "gap_samples": int(round(gap * RATE))}, 1e-6, + "ffprobe format.duration vs float32 decode sample count") + + c = seg["a_vs_c"] + claim("method_c_fidelity", aid, + (f"the pure-Python reimplementation reproduces ffmpeg's silencedetect: " + f"{c['count_a']} runs vs {c['count_b']}, max start deviation " + f"{c['max_start_dev_samples']} sample(s), max end deviation " + f"{c['max_end_dev_samples']} sample(s)"), + {"count_a": c["count_a"], "count_b": c["count_b"], + "max_start_dev_samples": c["max_start_dev_samples"], + "max_end_dev_samples": c["max_end_dev_samples"]}, 0, + "ffmpeg silencedetect vs method C, recomputed in verify.py") + + b = seg["a_vs_b"] + claim("naive_rms_divergence", aid, + (f"the frame-RMS model disagrees with the reference: {b['count_b']} runs vs " + f"{b['count_a']}, total silence {b['total_b_s']} s vs {b['total_a_s']} s " + f"(difference {b['total_diff_s']} s = " + f"{100.0 * b['total_diff_s'] / b['total_a_s']:.1f}% of reference silence)"), + {"count_a": b["count_a"], "count_b": b["count_b"], + "total_a_s": b["total_a_s"], "total_b_s": b["total_b_s"], + "total_diff_s": b["total_diff_s"]}, 1e-6, + "ffmpeg silencedetect vs frame-RMS detector, recomputed in verify.py") + + claim("spectrogram_pixels", aid, + f"machine spectrogram is {pa['width']}x{pa['height']} " + f"colour_type={pa['colour_type']} with {pa['distinct_colours']} distinct colours " + f"and luminance variance {pa['var_luma']}", + {k: pa[k] for k in ("width", "height", "colour_type", "channels", + "distinct_colours", "mean_luma", "var_luma", + "min_luma", "max_luma")}, 1e-6, + "stdlib zlib/struct PNG decode + pixel statistics (pngstat.py)") + + claim("cross_modal_pearson", aid, + (f"spectrogram column luminance correlates with the audio's own frame-RMS " + f"envelope at r = {pa['pearson_col_luma_vs_frame_rms_db']}"), + pa["pearson_col_luma_vs_frame_rms_db"], 0.01, + "pngstat.column_profile vs frame_db envelope, Pearson r") + + claim("frequency_tilt", aid, + (f"bottom-band (low-frequency) mean luminance {pa['bottom_band_mean_luma']} " + f"exceeds top-band {pa['top_band_mean_luma']}, as speech energy below the " + f"Nyquist limit requires"), + {"bottom": pa["bottom_band_mean_luma"], "top": pa["top_band_mean_luma"]}, + 1e-6, "pngstat.row_profile, top vs bottom decile") + + print(f" {aid}: {total:.3f}s A={len(span_a)}/{seg['silence_total_a_s']:.3f}s " + f"B={len(span_b)}/{seg['silence_total_b_s']:.3f}s " + f"C={len(span_c)}/{seg['silence_total_c_s']:.3f}s " + f"A-vs-C dev={c['max_start_dev_samples']}/{c['max_end_dev_samples']} samples " + f"A-vs-B diff={b['total_diff_s']:.3f}s " + f"r={pa['pearson_col_luma_vs_frame_rms_db']}") + + (ROOT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + (ROOT / "segments.json").write_text(json.dumps(segments, indent=2) + "\n") + (ROOT / "claims.json").write_text(json.dumps(claims, indent=2) + "\n") + print(f"wrote manifest.json, segments.json, claims.json ({counter[0]} claims)") + + +if __name__ == "__main__": + main() diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/claims.json b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/claims.json new file mode 100644 index 0000000..7b19a99 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/claims.json @@ -0,0 +1,537 @@ +{ + "artifact": "sphinx/phonon-reel-audio-archive", + "note": "Every claim is a measured value with a stated tolerance. verify.py recomputes each one from sources/ and exits non-zero on any disagreement. falsify.py mutates claims and proves the verifier can fail.", + "claims": [ + { + "id": "C01", + "class": "cal_dc_sweep", + "asset": null, + "statement": "A DC block at -36 dBFS (amp 519) is called silent while one at -35 dBFS (amp 583) is not, so the effective cut-off lies between them; the source predicts 10^(-35/20)*32767 = 582.688814 i16 units", + "value": { + "flip_between_dbfs": [ + -35, + -36 + ], + "predicted_cutoff_amp_i16": 582.688814 + }, + "tolerance": 1.0, + "method": "ffmpeg silencedetect=noise=-35.0dB:d=0.2 over synthetic DC blocks" + }, + { + "id": "C02", + "class": "cal_dc_step", + "asset": null, + "statement": "A DC step edge at t=2.000 s is located to within 1 sample(s), i.e. silencedetect is sample-accurate and applies no frame-grid quantisation", + "value": { + "start_error_samples": 1, + "end_error_samples": 0 + }, + "tolerance": 2, + "method": "ffmpeg silencedetect=noise=-35.0dB:d=0.0 over a synthetic DC step" + }, + { + "id": "C03", + "class": "cal_tone", + "asset": null, + "statement": "A pure 1000 Hz tone at -20 dBFS produces 4001 'silence' runs in 2.0 s at 499.99425 us spacing \u2014 2 per period, exactly as a per-sample |x| < threshold test requires", + "value": { + "runs_reported": 4001, + "mean_start_spacing_us": 499.99425 + }, + "tolerance": 3.0, + "method": "ffmpeg silencedetect=noise=-35.0dB:d=0.0 over a synthetic 1000 Hz tone" + }, + { + "id": "C04", + "class": "sha256", + "asset": "phonon-male-reel", + "statement": "source file hash is ae508e9e4d2a8841\u2026", + "value": "ae508e9e4d2a8841bf4b7667396cb61babec3a02eb2ed5361f501b5eb7bc4531", + "tolerance": 0, + "method": "sha256 of sources/" + }, + { + "id": "C05", + "class": "bytes", + "asset": "phonon-male-reel", + "statement": "source file is 868563 bytes", + "value": 868563, + "tolerance": 0, + "method": "os.stat on sources/" + }, + { + "id": "C06", + "class": "container_duration", + "asset": "phonon-male-reel", + "statement": "container reports 54.256327 s", + "value": 54.256327, + "tolerance": 1e-06, + "method": "ffprobe format.duration" + }, + { + "id": "C07", + "class": "stream_props", + "asset": "phonon-male-reel", + "statement": "mp3 44100 Hz 1 ch (mono)", + "value": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono" + }, + "tolerance": 0, + "method": "ffprobe stream fields" + }, + { + "id": "C08", + "class": "astats", + "asset": "phonon-male-reel", + "statement": "peak -3.271369 dBFS, RMS -24.19503 dBFS, entropy 0.593518", + "value": { + "peak_level_db": -3.271369, + "rms_level_db": -24.19503, + "dc_offset": -6.3e-05, + "flat_factor": 0.0, + "entropy": 0.593518 + }, + "tolerance": 1e-06, + "method": "ffmpeg astats=metadata=0:reset=0 (Overall section)" + }, + { + "id": "C09", + "class": "ebur128", + "asset": "phonon-male-reel", + "statement": "integrated -22.9 LUFS, LRA 5.0 LU, true peak -3.3 dBFS", + "value": { + "integrated_lufs": -22.9, + "loudness_range_lu": 5.0, + "true_peak_dbfs": -3.3 + }, + "tolerance": 1e-06, + "method": "ffmpeg ebur128=peak=true (summary)" + }, + { + "id": "C10", + "class": "decoded_samples", + "asset": "phonon-male-reel", + "statement": "decodes to 2391102 mono float32 samples (54.22 s at 44100 Hz)", + "value": { + "samples": 2391102, + "duration_s": 54.22 + }, + "tolerance": 0, + "method": "ffmpeg -f f32le -ac 1 -ar 44100 pipe, counted in Python" + }, + { + "id": "C11", + "class": "container_vs_decoded_gap", + "asset": "phonon-male-reel", + "statement": "the container reports 54.256327 s but only 54.22 s decodes: a 0.036327 s (1602 sample) priming/padding gap that would shift every index offset if the container timeline were used", + "value": { + "container_s": 54.256327, + "decoded_s": 54.22, + "gap_s": 0.036327, + "gap_samples": 1602 + }, + "tolerance": 1e-06, + "method": "ffprobe format.duration vs float32 decode sample count" + }, + { + "id": "C12", + "class": "method_c_fidelity", + "asset": "phonon-male-reel", + "statement": "the pure-Python reimplementation reproduces ffmpeg's silencedetect: 10 runs vs 10, max start deviation 2 sample(s), max end deviation 2 sample(s)", + "value": { + "count_a": 10, + "count_b": 10, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2 + }, + "tolerance": 0, + "method": "ffmpeg silencedetect vs method C, recomputed in verify.py" + }, + { + "id": "C13", + "class": "naive_rms_divergence", + "asset": "phonon-male-reel", + "statement": "the frame-RMS model disagrees with the reference: 14 runs vs 10, total silence 17.56 s vs 14.31447 s (difference 3.24553 s = 22.7% of reference silence)", + "value": { + "count_a": 10, + "count_b": 14, + "total_a_s": 14.31447, + "total_b_s": 17.56, + "total_diff_s": 3.24553 + }, + "tolerance": 1e-06, + "method": "ffmpeg silencedetect vs frame-RMS detector, recomputed in verify.py" + }, + { + "id": "C14", + "class": "spectrogram_pixels", + "asset": "phonon-male-reel", + "statement": "machine spectrogram is 1024x512 colour_type=2 with 619 distinct colours and luminance variance 793.272423", + "value": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 619, + "mean_luma": 17.021908, + "var_luma": 793.272423, + "min_luma": 0, + "max_luma": 230 + }, + "tolerance": 1e-06, + "method": "stdlib zlib/struct PNG decode + pixel statistics (pngstat.py)" + }, + { + "id": "C15", + "class": "cross_modal_pearson", + "asset": "phonon-male-reel", + "statement": "spectrogram column luminance correlates with the audio's own frame-RMS envelope at r = 0.847926", + "value": 0.847926, + "tolerance": 0.01, + "method": "pngstat.column_profile vs frame_db envelope, Pearson r" + }, + { + "id": "C16", + "class": "frequency_tilt", + "asset": "phonon-male-reel", + "statement": "bottom-band (low-frequency) mean luminance 51.057617 exceeds top-band 0.0, as speech energy below the Nyquist limit requires", + "value": { + "bottom": 51.057617, + "top": 0.0 + }, + "tolerance": 1e-06, + "method": "pngstat.row_profile, top vs bottom decile" + }, + { + "id": "C17", + "class": "sha256", + "asset": "phonon-british-reel", + "statement": "source file hash is 18e7a00326f1067d\u2026", + "value": "18e7a00326f1067dff866fa3edbdaf420f1097a6932bc9acb3229db7046300a9", + "tolerance": 0, + "method": "sha256 of sources/" + }, + { + "id": "C18", + "class": "bytes", + "asset": "phonon-british-reel", + "statement": "source file is 821751 bytes", + "value": 821751, + "tolerance": 0, + "method": "os.stat on sources/" + }, + { + "id": "C19", + "class": "container_duration", + "asset": "phonon-british-reel", + "statement": "container reports 51.330612 s", + "value": 51.330612, + "tolerance": 1e-06, + "method": "ffprobe format.duration" + }, + { + "id": "C20", + "class": "stream_props", + "asset": "phonon-british-reel", + "statement": "mp3 44100 Hz 1 ch (mono)", + "value": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono" + }, + "tolerance": 0, + "method": "ffprobe stream fields" + }, + { + "id": "C21", + "class": "astats", + "asset": "phonon-british-reel", + "statement": "peak -1.596248 dBFS, RMS -23.072492 dBFS, entropy 0.599567", + "value": { + "peak_level_db": -1.596248, + "rms_level_db": -23.072492, + "dc_offset": -6.5e-05, + "flat_factor": 0.0, + "entropy": 0.599567 + }, + "tolerance": 1e-06, + "method": "ffmpeg astats=metadata=0:reset=0 (Overall section)" + }, + { + "id": "C22", + "class": "ebur128", + "asset": "phonon-british-reel", + "statement": "integrated -21.6 LUFS, LRA 4.8 LU, true peak -1.6 dBFS", + "value": { + "integrated_lufs": -21.6, + "loudness_range_lu": 4.8, + "true_peak_dbfs": -1.6 + }, + "tolerance": 1e-06, + "method": "ffmpeg ebur128=peak=true (summary)" + }, + { + "id": "C23", + "class": "decoded_samples", + "asset": "phonon-british-reel", + "statement": "decodes to 2261448 mono float32 samples (51.28 s at 44100 Hz)", + "value": { + "samples": 2261448, + "duration_s": 51.28 + }, + "tolerance": 0, + "method": "ffmpeg -f f32le -ac 1 -ar 44100 pipe, counted in Python" + }, + { + "id": "C24", + "class": "container_vs_decoded_gap", + "asset": "phonon-british-reel", + "statement": "the container reports 51.330612 s but only 51.28 s decodes: a 0.050612 s (2232 sample) priming/padding gap that would shift every index offset if the container timeline were used", + "value": { + "container_s": 51.330612, + "decoded_s": 51.28, + "gap_s": 0.050612, + "gap_samples": 2232 + }, + "tolerance": 1e-06, + "method": "ffprobe format.duration vs float32 decode sample count" + }, + { + "id": "C25", + "class": "method_c_fidelity", + "asset": "phonon-british-reel", + "statement": "the pure-Python reimplementation reproduces ffmpeg's silencedetect: 11 runs vs 11, max start deviation 2 sample(s), max end deviation 2 sample(s)", + "value": { + "count_a": 11, + "count_b": 11, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2 + }, + "tolerance": 0, + "method": "ffmpeg silencedetect vs method C, recomputed in verify.py" + }, + { + "id": "C26", + "class": "naive_rms_divergence", + "asset": "phonon-british-reel", + "statement": "the frame-RMS model disagrees with the reference: 18 runs vs 11, total silence 19.64 s vs 14.07945 s (difference 5.56055 s = 39.5% of reference silence)", + "value": { + "count_a": 11, + "count_b": 18, + "total_a_s": 14.07945, + "total_b_s": 19.64, + "total_diff_s": 5.56055 + }, + "tolerance": 1e-06, + "method": "ffmpeg silencedetect vs frame-RMS detector, recomputed in verify.py" + }, + { + "id": "C27", + "class": "spectrogram_pixels", + "asset": "phonon-british-reel", + "statement": "machine spectrogram is 1024x512 colour_type=2 with 629 distinct colours and luminance variance 804.767134", + "value": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 629, + "mean_luma": 17.358503, + "var_luma": 804.767134, + "min_luma": 0, + "max_luma": 230 + }, + "tolerance": 1e-06, + "method": "stdlib zlib/struct PNG decode + pixel statistics (pngstat.py)" + }, + { + "id": "C28", + "class": "cross_modal_pearson", + "asset": "phonon-british-reel", + "statement": "spectrogram column luminance correlates with the audio's own frame-RMS envelope at r = 0.851619", + "value": 0.851619, + "tolerance": 0.01, + "method": "pngstat.column_profile vs frame_db envelope, Pearson r" + }, + { + "id": "C29", + "class": "frequency_tilt", + "asset": "phonon-british-reel", + "statement": "bottom-band (low-frequency) mean luminance 50.566042 exceeds top-band 0.0, as speech energy below the Nyquist limit requires", + "value": { + "bottom": 50.566042, + "top": 0.0 + }, + "tolerance": 1e-06, + "method": "pngstat.row_profile, top vs bottom decile" + }, + { + "id": "C30", + "class": "sha256", + "asset": "phonon-alfred-reel", + "statement": "source file hash is c2e57c57d1fcf070\u2026", + "value": "c2e57c57d1fcf070318085b9ba42f3c19f2972456e247ff5342c42337ad9b358", + "tolerance": 0, + "method": "sha256 of sources/" + }, + { + "id": "C31", + "class": "bytes", + "asset": "phonon-alfred-reel", + "statement": "source file is 557183 bytes", + "value": 557183, + "tolerance": 0, + "method": "os.stat on sources/" + }, + { + "id": "C32", + "class": "container_duration", + "asset": "phonon-alfred-reel", + "statement": "container reports 34.795102 s", + "value": 34.795102, + "tolerance": 1e-06, + "method": "ffprobe format.duration" + }, + { + "id": "C33", + "class": "stream_props", + "asset": "phonon-alfred-reel", + "statement": "mp3 44100 Hz 1 ch (mono)", + "value": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono" + }, + "tolerance": 0, + "method": "ffprobe stream fields" + }, + { + "id": "C34", + "class": "astats", + "asset": "phonon-alfred-reel", + "statement": "peak -2.304178 dBFS, RMS -24.983319 dBFS, entropy 0.554868", + "value": { + "peak_level_db": -2.304178, + "rms_level_db": -24.983319, + "dc_offset": -6.6e-05, + "flat_factor": 0.0, + "entropy": 0.554868 + }, + "tolerance": 1e-06, + "method": "ffmpeg astats=metadata=0:reset=0 (Overall section)" + }, + { + "id": "C35", + "class": "ebur128", + "asset": "phonon-alfred-reel", + "statement": "integrated -23.1 LUFS, LRA 6.2 LU, true peak -2.2 dBFS", + "value": { + "integrated_lufs": -23.1, + "loudness_range_lu": 6.2, + "true_peak_dbfs": -2.2 + }, + "tolerance": 1e-06, + "method": "ffmpeg ebur128=peak=true (summary)" + }, + { + "id": "C36", + "class": "decoded_samples", + "asset": "phonon-alfred-reel", + "statement": "decodes to 1532916 mono float32 samples (34.76 s at 44100 Hz)", + "value": { + "samples": 1532916, + "duration_s": 34.76 + }, + "tolerance": 0, + "method": "ffmpeg -f f32le -ac 1 -ar 44100 pipe, counted in Python" + }, + { + "id": "C37", + "class": "container_vs_decoded_gap", + "asset": "phonon-alfred-reel", + "statement": "the container reports 34.795102 s but only 34.76 s decodes: a 0.035102 s (1548 sample) priming/padding gap that would shift every index offset if the container timeline were used", + "value": { + "container_s": 34.795102, + "decoded_s": 34.76, + "gap_s": 0.035102, + "gap_samples": 1548 + }, + "tolerance": 1e-06, + "method": "ffprobe format.duration vs float32 decode sample count" + }, + { + "id": "C38", + "class": "method_c_fidelity", + "asset": "phonon-alfred-reel", + "statement": "the pure-Python reimplementation reproduces ffmpeg's silencedetect: 12 runs vs 12, max start deviation 2 sample(s), max end deviation 2 sample(s)", + "value": { + "count_a": 12, + "count_b": 12, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2 + }, + "tolerance": 0, + "method": "ffmpeg silencedetect vs method C, recomputed in verify.py" + }, + { + "id": "C39", + "class": "naive_rms_divergence", + "asset": "phonon-alfred-reel", + "statement": "the frame-RMS model disagrees with the reference: 15 runs vs 12, total silence 16.5 s vs 13.595777 s (difference 2.904223 s = 21.4% of reference silence)", + "value": { + "count_a": 12, + "count_b": 15, + "total_a_s": 13.595777, + "total_b_s": 16.5, + "total_diff_s": 2.904223 + }, + "tolerance": 1e-06, + "method": "ffmpeg silencedetect vs frame-RMS detector, recomputed in verify.py" + }, + { + "id": "C40", + "class": "spectrogram_pixels", + "asset": "phonon-alfred-reel", + "statement": "machine spectrogram is 1024x512 colour_type=2 with 623 distinct colours and luminance variance 683.935618", + "value": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 623, + "mean_luma": 14.794031, + "var_luma": 683.935618, + "min_luma": 0, + "max_luma": 224 + }, + "tolerance": 1e-06, + "method": "stdlib zlib/struct PNG decode + pixel statistics (pngstat.py)" + }, + { + "id": "C41", + "class": "cross_modal_pearson", + "asset": "phonon-alfred-reel", + "statement": "spectrogram column luminance correlates with the audio's own frame-RMS envelope at r = 0.875997", + "value": 0.875997, + "tolerance": 0.01, + "method": "pngstat.column_profile vs frame_db envelope, Pearson r" + }, + { + "id": "C42", + "class": "frequency_tilt", + "asset": "phonon-alfred-reel", + "statement": "bottom-band (low-frequency) mean luminance 43.315219 exceeds top-band 0.0, as speech energy below the Nyquist limit requires", + "value": { + "bottom": 43.315219, + "top": 0.0 + }, + "tolerance": 1e-06, + "method": "pngstat.row_profile, top vs bottom decile" + } + ] +} diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/af_silencedetect.c b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/af_silencedetect.c new file mode 100644 index 0000000..bcdc90f --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/af_silencedetect.c @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2012 Clément Bœsch + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * Audio silence detector + */ + +#include /* DBL_MAX */ + +#include "libavutil/mem.h" +#include "libavutil/opt.h" +#include "libavutil/timestamp.h" +#include "audio.h" +#include "avfilter.h" +#include "filters.h" + +typedef struct SilenceDetectContext { + const AVClass *class; + double noise; ///< noise amplitude ratio + int64_t duration; ///< minimum duration of silence until notification + int mono; ///< mono mode : check each channel separately (default = check when ALL channels are silent) + int channels; ///< number of channels + int independent_channels; ///< number of entries in following arrays (always 1 in mono mode) + int64_t *nb_null_samples; ///< (array) current number of continuous zero samples + int64_t *start; ///< (array) if silence is detected, this value contains the time of the first zero sample (default/unset = INT64_MIN) + int64_t frame_end; ///< pts of the end of the current frame (used to compute duration of silence at EOS) + int last_sample_rate; ///< last sample rate to check for sample rate changes + AVRational time_base; ///< time_base + + void (*silencedetect)(AVFilterContext *ctx, AVFrame *insamples, + int nb_samples, int64_t nb_samples_notify, + AVRational time_base); +} SilenceDetectContext; + +#define MAX_DURATION (24*3600*1000000LL) +#define OFFSET(x) offsetof(SilenceDetectContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_AUDIO_PARAM +static const AVOption silencedetect_options[] = { + { "n", "set noise tolerance", OFFSET(noise), AV_OPT_TYPE_DOUBLE, {.dbl=0.001}, 0, DBL_MAX, FLAGS }, + { "noise", "set noise tolerance", OFFSET(noise), AV_OPT_TYPE_DOUBLE, {.dbl=0.001}, 0, DBL_MAX, FLAGS }, + { "d", "set minimum duration in seconds", OFFSET(duration), AV_OPT_TYPE_DURATION, {.i64=2000000}, 0, MAX_DURATION,FLAGS }, + { "duration", "set minimum duration in seconds", OFFSET(duration), AV_OPT_TYPE_DURATION, {.i64=2000000}, 0, MAX_DURATION,FLAGS }, + { "mono", "check each channel separately", OFFSET(mono), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS }, + { "m", "check each channel separately", OFFSET(mono), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(silencedetect); + +static void set_meta(AVFrame *insamples, int channel, const char *key, char *value) +{ + char key2[128]; + + if (channel) + snprintf(key2, sizeof(key2), "lavfi.%s.%d", key, channel); + else + snprintf(key2, sizeof(key2), "lavfi.%s", key); + av_dict_set(&insamples->metadata, key2, value, 0); +} +static av_always_inline void update(AVFilterContext *ctx, AVFrame *insamples, + int is_silence, int current_sample, int64_t nb_samples_notify, + AVRational time_base) +{ + SilenceDetectContext *s = ctx->priv; + int channel = current_sample % s->independent_channels; + if (is_silence) { + if (s->start[channel] == INT64_MIN) { + s->nb_null_samples[channel]++; + if (s->nb_null_samples[channel] >= nb_samples_notify) { + s->start[channel] = insamples->pts + av_rescale_q(current_sample / s->channels + 1 - nb_samples_notify * s->independent_channels / s->channels, + (AVRational){ 1, s->last_sample_rate }, time_base); + set_meta(insamples, s->mono ? channel + 1 : 0, "silence_start", + av_ts2timestr(s->start[channel], &time_base)); + if (s->mono) + av_log(ctx, AV_LOG_INFO, "channel: %d | ", channel); + av_log(ctx, AV_LOG_INFO, "silence_start: %s\n", + av_ts2timestr(s->start[channel], &time_base)); + } + } + } else { + if (s->start[channel] > INT64_MIN) { + int64_t end_pts = insamples ? insamples->pts + av_rescale_q(current_sample / s->channels, + (AVRational){ 1, s->last_sample_rate }, time_base) + : s->frame_end; + int64_t duration_ts = end_pts - s->start[channel]; + if (insamples) { + set_meta(insamples, s->mono ? channel + 1 : 0, "silence_end", + av_ts2timestr(end_pts, &time_base)); + set_meta(insamples, s->mono ? channel + 1 : 0, "silence_duration", + av_ts2timestr(duration_ts, &time_base)); + } + if (s->mono) + av_log(ctx, AV_LOG_INFO, "channel: %d | ", channel); + av_log(ctx, AV_LOG_INFO, "silence_end: %s | silence_duration: %s\n", + av_ts2timestr(end_pts, &time_base), + av_ts2timestr(duration_ts, &time_base)); + } + s->nb_null_samples[channel] = 0; + s->start[channel] = INT64_MIN; + } +} + +#define SILENCE_DETECT(name, type) \ +static void silencedetect_##name(AVFilterContext *ctx, AVFrame *insamples, \ + int nb_samples, int64_t nb_samples_notify, \ + AVRational time_base) \ +{ \ + SilenceDetectContext *s = ctx->priv; \ + const type *p = (const type *)insamples->data[0]; \ + const type noise = s->noise; \ + int i; \ + \ + for (i = 0; i < nb_samples; i++, p++) \ + update(ctx, insamples, *p < noise && *p > -noise, i, \ + nb_samples_notify, time_base); \ +} + +#define SILENCE_DETECT_PLANAR(name, type) \ +static void silencedetect_##name(AVFilterContext *ctx, AVFrame *insamples, \ + int nb_samples, int64_t nb_samples_notify, \ + AVRational time_base) \ +{ \ + SilenceDetectContext *s = ctx->priv; \ + const int channels = insamples->ch_layout.nb_channels; \ + const type noise = s->noise; \ + \ + nb_samples /= channels; \ + for (int i = 0; i < nb_samples; i++) { \ + for (int ch = 0; ch < insamples->ch_layout.nb_channels; ch++) { \ + const type *p = (const type *)insamples->extended_data[ch]; \ + update(ctx, insamples, p[i] < noise && p[i] > -noise, \ + channels * i + ch, \ + nb_samples_notify, time_base); \ + } \ + } \ +} + +SILENCE_DETECT(dbl, double) +SILENCE_DETECT(flt, float) +SILENCE_DETECT(s32, int32_t) +SILENCE_DETECT(s16, int16_t) + +SILENCE_DETECT_PLANAR(dblp, double) +SILENCE_DETECT_PLANAR(fltp, float) +SILENCE_DETECT_PLANAR(s32p, int32_t) +SILENCE_DETECT_PLANAR(s16p, int16_t) + +static int config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + SilenceDetectContext *s = ctx->priv; + int c; + + s->channels = inlink->ch_layout.nb_channels; + s->duration = av_rescale(s->duration, inlink->sample_rate, AV_TIME_BASE); + s->independent_channels = s->mono ? s->channels : 1; + s->nb_null_samples = av_calloc(s->independent_channels, + sizeof(*s->nb_null_samples)); + if (!s->nb_null_samples) + return AVERROR(ENOMEM); + s->start = av_malloc_array(s->independent_channels, sizeof(*s->start)); + if (!s->start) + return AVERROR(ENOMEM); + for (c = 0; c < s->independent_channels; c++) + s->start[c] = INT64_MIN; + + switch (inlink->format) { + case AV_SAMPLE_FMT_DBL: s->silencedetect = silencedetect_dbl; break; + case AV_SAMPLE_FMT_FLT: s->silencedetect = silencedetect_flt; break; + case AV_SAMPLE_FMT_S32: + s->noise *= INT32_MAX; + s->silencedetect = silencedetect_s32; + break; + case AV_SAMPLE_FMT_S16: + s->noise *= INT16_MAX; + s->silencedetect = silencedetect_s16; + break; + case AV_SAMPLE_FMT_DBLP: s->silencedetect = silencedetect_dblp; break; + case AV_SAMPLE_FMT_FLTP: s->silencedetect = silencedetect_fltp; break; + case AV_SAMPLE_FMT_S32P: + s->noise *= INT32_MAX; + s->silencedetect = silencedetect_s32p; + break; + case AV_SAMPLE_FMT_S16P: + s->noise *= INT16_MAX; + s->silencedetect = silencedetect_s16p; + break; + default: + return AVERROR_BUG; + } + + return 0; +} + +static int filter_frame(AVFilterLink *inlink, AVFrame *insamples) +{ + AVFilterContext *ctx = inlink->dst; + SilenceDetectContext *s = ctx->priv; + const int nb_channels = inlink->ch_layout.nb_channels; + const int srate = inlink->sample_rate; + const int nb_samples = insamples->nb_samples * nb_channels; + const int64_t nb_samples_notify = s->duration * (s->mono ? 1 : nb_channels); + int c; + + // scale number of null samples to the new sample rate + if (s->last_sample_rate && s->last_sample_rate != srate) + for (c = 0; c < s->independent_channels; c++) { + s->nb_null_samples[c] = srate * s->nb_null_samples[c] / s->last_sample_rate; + } + s->last_sample_rate = srate; + s->time_base = inlink->time_base; + s->frame_end = insamples->pts + av_rescale_q(insamples->nb_samples, + (AVRational){ 1, s->last_sample_rate }, inlink->time_base); + + s->silencedetect(ctx, insamples, nb_samples, nb_samples_notify, + inlink->time_base); + + return ff_filter_frame(inlink->dst->outputs[0], insamples); +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + SilenceDetectContext *s = ctx->priv; + int c; + + for (c = 0; c < s->independent_channels; c++) + if (s->start[c] > INT64_MIN) + update(ctx, NULL, 0, c, 0, s->time_base); + av_freep(&s->nb_null_samples); + av_freep(&s->start); +} + +static const AVFilterPad silencedetect_inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_AUDIO, + .config_props = config_input, + .filter_frame = filter_frame, + }, +}; + +const FFFilter ff_af_silencedetect = { + .p.name = "silencedetect", + .p.description = NULL_IF_CONFIG_SMALL("Detect silence."), + .p.priv_class = &silencedetect_class, + .p.flags = AVFILTER_FLAG_METADATA_ONLY, + .priv_size = sizeof(SilenceDetectContext), + .uninit = uninit, + FILTER_INPUTS(silencedetect_inputs), + FILTER_OUTPUTS(ff_audio_default_filterpad), + FILTER_SAMPLEFMTS(AV_SAMPLE_FMT_DBL, AV_SAMPLE_FMT_DBLP, + AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_FLTP, + AV_SAMPLE_FMT_S32, AV_SAMPLE_FMT_S32P, + AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_S16P), +}; diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/silencedetect_semantics.txt b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/silencedetect_semantics.txt new file mode 100644 index 0000000..4232f25 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/evidence/silencedetect_semantics.txt @@ -0,0 +1,19 @@ +Reference: ffmpeg/libavfilter/af_silencedetect.c +Fetched from: https://raw.githubusercontent.com/FFmpeg/FFmpeg/master/libavfilter/af_silencedetect.c +sha256: 22b9d81e13270e3ee9249a21df80b9334b0281e09b4d67b5e41b10d78b7cee21 + +Lines that define the detection semantics: + + 87| if (s->nb_null_samples[channel] >= nb_samples_notify) { + 88| s->start[channel] = insamples->pts + av_rescale_q(current_sample / s->channels + 1 - nb_samples_notify * s->independent_channels / s->channels, + 128| const type noise = s->noise; \ + 132| update(ctx, insamples, *p < noise && *p > -noise, i, \ + 143| const type noise = s->noise; \ + 149| update(ctx, insamples, p[i] < noise && p[i] > -noise, \ + 173| s->duration = av_rescale(s->duration, inlink->sample_rate, AV_TIME_BASE); + 189| s->noise *= INT32_MAX; + 193| s->noise *= INT16_MAX; + 197| case AV_SAMPLE_FMT_FLTP: s->silencedetect = silencedetect_fltp; break; + 199| s->noise *= INT32_MAX; + 203| s->noise *= INT16_MAX; + 220| const int64_t nb_samples_notify = s->duration * (s->mono ? 1 : nb_channels); diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/falsify.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/falsify.py new file mode 100644 index 0000000..bbbf807 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/falsify.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""falsify.py — mutation controls: prove verify.py can actually fail. + +A verifier that always exits 0 is worthless. This script takes the frozen +claims.json, applies one deliberate mutation at a time — a flipped hash digit, +a shifted duration, an overstated correlation, an inverted physical direction — +and requires that verify.py's own evaluation logic reports a failure for each. + +It measures the media ONCE (verify.measure()) and then evaluates every mutant +against those same fresh measurements, so the controls cost no extra ffmpeg +runs and cannot be passed by re-measuring into agreement. + +Controls: + * the unmutated claims must pass every check + * every mutant must fail at least one check + * a tampered segments.json must fail an artifact-file check + * a claim with an unknown class must fail + +Exit 0 only if all of that holds. + +Usage: python3 falsify.py +""" +import copy +import json +import shutil +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import verify as V + +ROOT = Path(__file__).resolve().parent + + +def find(doc, cid): + for c in doc["claims"]: + if c["id"] == cid: + return c + raise KeyError(cid) + + +def first(doc, klass): + for c in doc["claims"]: + if c["class"] == klass: + return c + raise KeyError(klass) + + +# -------------------------------------------------------------------------- +# mutations: each returns a mutated copy of the claims document +# -------------------------------------------------------------------------- +def m_sha256(doc): + c = first(doc, "sha256") + c["value"] = ("0" if c["value"][0] != "0" else "1") + c["value"][1:] + return f"flipped first hex digit of {c['id']} ({c['asset']}) source hash" + + +def m_bytes(doc): + c = first(doc, "bytes") + c["value"] = c["value"] + 1 + return f"added 1 byte to {c['id']} ({c['asset']})" + + +def m_duration(doc): + c = first(doc, "container_duration") + c["value"] = round(c["value"] + 0.001, 6) + return f"shifted {c['id']} container duration by 1 ms" + + +def m_stream(doc): + c = first(doc, "stream_props") + c["value"]["channels"] = 2 + return f"claimed {c['id']} ({c['asset']}) is stereo" + + +def m_astats(doc): + c = first(doc, "astats") + c["value"]["peak_level_db"] = round(c["value"]["peak_level_db"] + 0.5, 6) + return f"raised {c['id']} ({c['asset']}) peak level by 0.5 dB" + + +def m_ebur128(doc): + c = first(doc, "ebur128") + c["value"]["integrated_lufs"] = round(c["value"]["integrated_lufs"] + 1.0, 6) + return f"raised {c['id']} ({c['asset']}) integrated loudness by 1 LUFS" + + +def m_samples(doc): + c = first(doc, "decoded_samples") + c["value"]["samples"] = c["value"]["samples"] + 1 + return f"added 1 sample to {c['id']} ({c['asset']})" + + +def m_fidelity_dev(doc): + c = first(doc, "method_c_fidelity") + c["value"]["max_start_dev_samples"] = 999 + return f"claimed {c['id']} ({c['asset']}) reimplementation deviates by 999 samples" + + +def m_fidelity_count(doc): + c = first(doc, "method_c_fidelity") + c["value"]["count_b"] = c["value"]["count_b"] + 1 + return f"claimed {c['id']} ({c['asset']}) reimplementation finds one extra run" + + +def m_naive_zero(doc): + c = first(doc, "naive_rms_divergence") + c["value"]["total_diff_s"] = 0.0 + c["value"]["total_b_s"] = c["value"]["total_a_s"] + return f"claimed {c['id']} ({c['asset']}) the two models agree exactly" + + +def m_naive_direction(doc): + c = first(doc, "naive_rms_divergence") + a, b = c["value"]["total_a_s"], c["value"]["total_b_s"] + c["value"]["total_a_s"], c["value"]["total_b_s"] = b, a + return f"inverted the direction of the {c['id']} ({c['asset']}) divergence" + + +def m_pixels(doc): + c = first(doc, "spectrogram_pixels") + c["value"]["var_luma"] = round(c["value"]["var_luma"] * 2.0, 6) + return f"doubled {c['id']} ({c['asset']}) spectrogram luminance variance" + + +def m_pearson(doc): + c = first(doc, "cross_modal_pearson") + c["value"] = 0.10 + return f"claimed {c['id']} ({c['asset']}) cross-modal r = 0.10" + + +def m_tilt(doc): + c = first(doc, "frequency_tilt") + c["value"]["bottom"], c["value"]["top"] = c["value"]["top"], c["value"]["bottom"] + return f"inverted the {c['id']} ({c['asset']}) frequency tilt" + + +def m_dc_sweep(doc): + c = first(doc, "cal_dc_sweep") + c["value"]["predicted_cutoff_amp_i16"] = 100.0 + return "moved the predicted DC cut-off outside the measured flip interval" + + +def m_dc_step(doc): + c = first(doc, "cal_dc_step") + c["value"]["start_error_samples"] = 999 + return "claimed the DC step edge is located to within 999 samples" + + +def m_tone(doc): + c = first(doc, "cal_tone") + c["value"]["runs_reported"] = 5 + return "claimed the pure tone produces only 5 silence runs" + + +def m_gap(doc): + c = first(doc, "container_vs_decoded_gap") + c["value"]["gap_s"] = 0.0 + c["value"]["decoded_s"] = c["value"]["container_s"] + return f"claimed {c['id']} ({c['asset']}) container and decoded durations agree" + + +def m_unknown_class(doc): + doc["claims"].append({"id": "C99", "class": "not_a_real_class", "asset": None, + "statement": "bogus", "value": 1, "tolerance": 0, + "method": "none"}) + return "appended a claim with an unknown class" + + +MUTATIONS = [m_sha256, m_bytes, m_duration, m_stream, m_astats, m_ebur128, m_samples, + m_fidelity_dev, m_fidelity_count, m_naive_zero, m_naive_direction, + m_pixels, m_pearson, m_tilt, m_dc_sweep, m_dc_step, m_tone, m_gap, + m_unknown_class] + + +# -------------------------------------------------------------------------- +def file_control(m, base): + """Tamper with segments.json in a scratch tree; the file check must fail.""" + tmp = Path(tempfile.mkdtemp(prefix="falsify-")) + try: + seg = json.loads((ROOT / "segments.json").read_text()) + aid = sorted(seg["assets"])[0] + seg["assets"][aid]["method_a_silence"] = seg["assets"][aid]["method_a_silence"][:-1] + (tmp / "segments.json").write_text(json.dumps(seg, indent=2)) + shutil.copy(ROOT / "manifest.json", tmp / "manifest.json") + res = V.check_artifact_files(m, root=tmp) + bad = [r for r in res if not r["ok"]] + return (len(bad) > 0, + f"dropped one span from segments.json[{aid}].method_a_silence -> " + f"{len(bad)} file check(s) failed") + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +def main(): + clean = json.loads((ROOT / "claims.json").read_text()) + print("measuring media once (fresh ffmpeg runs)...") + m = V.measure() + + rows = [] + base = V.evaluate(clean, m) + npass = sum(1 for r in base if r["ok"]) + clean_ok = npass == len(base) + rows.append(("control: unmutated claims.json", clean_ok, + f"{npass}/{len(base)} checks pass")) + + fc_ok, fc_detail = file_control(m, None) + rows.append(("control: tampered segments.json", fc_ok, fc_detail)) + + for fn in MUTATIONS: + doc = copy.deepcopy(clean) + what = fn(doc) + res = V.evaluate(doc, m) + failed = [r for r in res if not r["ok"]] + rows.append((what, len(failed) > 0, + f"{len(failed)} check(s) failed: " + + ", ".join(f"{r['id']}/{r['class']}" for r in failed[:4]) + if failed else "NOT DETECTED — verifier has no teeth here")) + + width = max(len(r[0]) for r in rows) + print() + for name, ok, detail in rows: + print(f" {'DETECTED' if ok else 'MISSED '} {name.ljust(width)} {detail}") + + bad = [r for r in rows if not r[1]] + print(f"\nfalsify.py: {len(rows) - len(bad)}/{len(rows)} controls behaved correctly") + if bad: + print(" controls that did not behave:") + for name, _, detail in bad: + print(f" - {name}: {detail}") + return 0 if not bad else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/fetch_sources.sh b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/fetch_sources.sh new file mode 100755 index 0000000..f376859 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/fetch_sources.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# fetch_sources.sh — stage the three source reels into sources/ and pin their hashes. +# +# SCOPE DECISION (evidence law, STANDING-AGREEMENT §4): +# Included: the three phonon-*-reel.mp3 files. They are troupe-produced voice +# reels, world-readable (mode 644) in the wizard-outputs drop. +# EXCLUDED on purpose: alexander-reading.{wav,mp3}, lilly-reading.{wav,mp3}, +# oracle-welcome.wav. Those are readings by named people (with transcripts +# beside them) or carry private permissions (mode 600). §4 forbids moving +# other people's private recordings/transcripts, so they are out of scope +# for a public archive regardless of how useful they would be. +set -eu + +SRC=/root/.hermes/wizard-outputs +DEST=$(cd "$(dirname "$0")" && pwd)/sources +mkdir -p "$DEST" + +for f in phonon-male-reel.mp3 phonon-british-reel.mp3 phonon-alfred-reel.mp3; do + cp -p "$SRC/$f" "$DEST/$f" +done + +sha256sum "$DEST"/*.mp3 | sed "s#$DEST/##" diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/manifest.json b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/manifest.json new file mode 100644 index 0000000..3265796 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/manifest.json @@ -0,0 +1,401 @@ +{ + "artifact": "sphinx/phonon-reel-audio-archive", + "generated_utc": "2026-10-21T07:27:44Z", + "generator": "build.py", + "lane": "sphinx \u2014 Archive (multimodal, long-context research)", + "tools": { + "ffmpeg": "ffmpeg version 6.1.1-3ubuntu5 Copyright (c) 2000-2023 the FFmpeg developers", + "ffprobe": "ffprobe version 6.1.1-3ubuntu5 Copyright (c) 2007-2023 the FFmpeg developers", + "python": "3.14.7", + "third_party_python_packages": "none (stdlib only)" + }, + "reference_implementation": { + "ffmpeg_source_url": "https://raw.githubusercontent.com/FFmpeg/FFmpeg/master/libavfilter/af_silencedetect.c", + "ffmpeg_source_sha256": "22b9d81e13270e3ee9249a21df80b9334b0281e09b4d67b5e41b10d78b7cee21", + "ffmpeg_source_bytes": 12620, + "excerpt_path": "evidence/silencedetect_semantics.txt" + }, + "analysis_parameters": { + "decode": "mono 44100 Hz float32", + "method_b_frame_ms": 20, + "method_b_frame_samples": 882, + "silence_threshold_db": -35.0, + "silence_threshold_amplitude_ratio": 0.01778279410038923, + "min_silence_s": 0.5, + "min_run_samples": 22050, + "spectrogram": "showspectrumpic=s=1024x512:scale=log:color=intensity" + }, + "calibration": { + "dc_sweep": { + "probe": "0.5 s DC blocks at listed levels, 0.5 s digital silence between", + "ffmpeg_options": "silencedetect=noise=-35.0dB:d=0.2", + "blocks": [ + { + "level_dbfs": -60, + "amp_i16": 33, + "start_s": 0.0, + "end_s": 0.5, + "classified_silent": true + }, + { + "level_dbfs": -50, + "amp_i16": 104, + "start_s": 1.0, + "end_s": 1.5, + "classified_silent": true + }, + { + "level_dbfs": -45, + "amp_i16": 184, + "start_s": 2.0, + "end_s": 2.5, + "classified_silent": true + }, + { + "level_dbfs": -40, + "amp_i16": 328, + "start_s": 3.0, + "end_s": 3.5, + "classified_silent": true + }, + { + "level_dbfs": -38, + "amp_i16": 413, + "start_s": 4.0, + "end_s": 4.5, + "classified_silent": true + }, + { + "level_dbfs": -36, + "amp_i16": 519, + "start_s": 5.0, + "end_s": 5.5, + "classified_silent": true + }, + { + "level_dbfs": -35, + "amp_i16": 583, + "start_s": 6.0, + "end_s": 6.5, + "classified_silent": false + }, + { + "level_dbfs": -34, + "amp_i16": 654, + "start_s": 7.0, + "end_s": 7.5, + "classified_silent": false + }, + { + "level_dbfs": -32, + "amp_i16": 823, + "start_s": 8.0, + "end_s": 8.5, + "classified_silent": false + }, + { + "level_dbfs": -30, + "amp_i16": 1036, + "start_s": 9.0, + "end_s": 9.5, + "classified_silent": false + }, + { + "level_dbfs": -25, + "amp_i16": 1843, + "start_s": 10.0, + "end_s": 10.5, + "classified_silent": false + }, + { + "level_dbfs": -20, + "amp_i16": 3277, + "start_s": 11.0, + "end_s": 11.5, + "classified_silent": false + } + ], + "loudest_block_called_silent": { + "level_dbfs": -36, + "amp_i16": 519, + "start_s": 5.0, + "end_s": 5.5, + "classified_silent": true + }, + "quietest_block_called_audible": { + "level_dbfs": -35, + "amp_i16": 583, + "start_s": 6.0, + "end_s": 6.5, + "classified_silent": false + }, + "predicted_cutoff_amp_i16": 582.688814, + "predicted_cutoff_dbfs": -35.000265, + "flip_is_between_dbfs": [ + -35, + -36 + ] + }, + "dc_step": { + "probe": "2.000 s DC @ -20 dBFS then 2.000 s digital zero, 44.1 kHz mono s16le", + "true_edge_s": 2.0, + "true_end_s": 4.0, + "observed_silence_start_s": 2.00002, + "observed_silence_end_s": 4.0, + "start_error_samples": 1, + "end_error_samples": 0, + "ffmpeg_options": "silencedetect=noise=-35.0dB:d=0.0" + }, + "tone_zero_crossing": [ + { + "freq_hz": 1000, + "amplitude_dbfs": -20, + "duration_s": 2.0, + "expected_zero_crossings": 4000, + "runs_reported": 4001, + "mean_run_duration_us": 35.310922, + "mean_start_spacing_us": 499.99425, + "expected_start_spacing_us": 500.0, + "ffmpeg_options": "silencedetect=noise=-35.0dB:d=0.0" + }, + { + "freq_hz": 4000, + "amplitude_dbfs": -20, + "duration_s": 2.0, + "expected_zero_crossings": 16000, + "runs_reported": 10200, + "mean_run_duration_us": 0.0, + "mean_start_spacing_us": 196.07285, + "expected_start_spacing_us": 125.0, + "ffmpeg_options": "silencedetect=noise=-35.0dB:d=0.0" + } + ] + }, + "assets": [ + { + "id": "phonon-male-reel", + "source": "sources/phonon-male-reel.mp3", + "sha256": "ae508e9e4d2a8841bf4b7667396cb61babec3a02eb2ed5361f501b5eb7bc4531", + "bytes": 868563, + "container": { + "format_name": "mp3", + "duration_s": 54.256327, + "bit_rate": 128068, + "size_bytes": 868563 + }, + "stream": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono", + "bit_rate": 128000 + }, + "astats": { + "peak_level_db": -3.271369, + "rms_level_db": -24.19503, + "dc_offset": -6.3e-05, + "flat_factor": 0.0, + "entropy": 0.593518, + "noise_floor_db": "-inf", + "samples": 2391102, + "nans": 0, + "infs": 0, + "denormals": 0 + }, + "ebur128": { + "integrated_lufs": -22.9, + "loudness_range_lu": 5.0, + "true_peak_dbfs": -3.3 + }, + "decoded": { + "samples": 2391102, + "duration_s": 54.22, + "frame_count": 2711, + "frame_rms_db_min": -240.0, + "frame_rms_db_max": -10.675572, + "frame_rms_db_mean": -64.181247 + }, + "decoded_minus_astats_samples": 0, + "spectra": { + "human": { + "path": "spectra/phonon-male-reel.png", + "sha256": "0977c3fc0b35f1b2fcfec6ea4e9f2cba5c5bd94dd53eed8039602dc703155da2", + "bytes": 397604 + }, + "machine": { + "path": "spectra/phonon-male-reel.plain.png", + "sha256": "c5acfd961efb307184f2286f60471bc3ae8035a7c89dc1b567b4b6d960894af3", + "bytes": 371769 + }, + "machine_pixel_analysis": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 619, + "mean_luma": 17.021908, + "var_luma": 793.272423, + "min_luma": 0, + "max_luma": 230, + "top_band_mean_luma": 0.0, + "bottom_band_mean_luma": 51.057617, + "pearson_col_luma_vs_frame_rms_db": 0.847926, + "env_dbfs_window": [ + -80.0, + 0.0 + ] + } + } + }, + { + "id": "phonon-british-reel", + "source": "sources/phonon-british-reel.mp3", + "sha256": "18e7a00326f1067dff866fa3edbdaf420f1097a6932bc9acb3229db7046300a9", + "bytes": 821751, + "container": { + "format_name": "mp3", + "duration_s": 51.330612, + "bit_rate": 128071, + "size_bytes": 821751 + }, + "stream": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono", + "bit_rate": 128000 + }, + "astats": { + "peak_level_db": -1.596248, + "rms_level_db": -23.072492, + "dc_offset": -6.5e-05, + "flat_factor": 0.0, + "entropy": 0.599567, + "noise_floor_db": "-inf", + "samples": 2261448, + "nans": 0, + "infs": 0, + "denormals": 0 + }, + "ebur128": { + "integrated_lufs": -21.6, + "loudness_range_lu": 4.8, + "true_peak_dbfs": -1.6 + }, + "decoded": { + "samples": 2261448, + "duration_s": 51.28, + "frame_count": 2564, + "frame_rms_db_min": -240.0, + "frame_rms_db_max": -9.360617, + "frame_rms_db_mean": -63.007516 + }, + "decoded_minus_astats_samples": 0, + "spectra": { + "human": { + "path": "spectra/phonon-british-reel.png", + "sha256": "9f0825aa34b8663a57cebed450f833fb195540d3cd18192c87aacd3feda1f52f", + "bytes": 405221 + }, + "machine": { + "path": "spectra/phonon-british-reel.plain.png", + "sha256": "d8ea952416070c97c510fe4456725ee0fb4273261e1dda6b43967ceb0aabcfb7", + "bytes": 379468 + }, + "machine_pixel_analysis": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 629, + "mean_luma": 17.358503, + "var_luma": 804.767134, + "min_luma": 0, + "max_luma": 230, + "top_band_mean_luma": 0.0, + "bottom_band_mean_luma": 50.566042, + "pearson_col_luma_vs_frame_rms_db": 0.851619, + "env_dbfs_window": [ + -80.0, + 0.0 + ] + } + } + }, + { + "id": "phonon-alfred-reel", + "source": "sources/phonon-alfred-reel.mp3", + "sha256": "c2e57c57d1fcf070318085b9ba42f3c19f2972456e247ff5342c42337ad9b358", + "bytes": 557183, + "container": { + "format_name": "mp3", + "duration_s": 34.795102, + "bit_rate": 128106, + "size_bytes": 557183 + }, + "stream": { + "codec_name": "mp3", + "sample_rate": 44100, + "channels": 1, + "channel_layout": "mono", + "bit_rate": 128000 + }, + "astats": { + "peak_level_db": -2.304178, + "rms_level_db": -24.983319, + "dc_offset": -6.6e-05, + "flat_factor": 0.0, + "entropy": 0.554868, + "noise_floor_db": "-inf", + "samples": 1532916, + "nans": 0, + "infs": 0, + "denormals": 0 + }, + "ebur128": { + "integrated_lufs": -23.1, + "loudness_range_lu": 6.2, + "true_peak_dbfs": -2.2 + }, + "decoded": { + "samples": 1532916, + "duration_s": 34.76, + "frame_count": 1738, + "frame_rms_db_min": -240.0, + "frame_rms_db_max": -13.076622, + "frame_rms_db_mean": -68.188454 + }, + "decoded_minus_astats_samples": 0, + "spectra": { + "human": { + "path": "spectra/phonon-alfred-reel.png", + "sha256": "e921c6bcbb6f01b9d4239e580b69375a94a9c77785ce88456ad666113898f3eb", + "bytes": 359023 + }, + "machine": { + "path": "spectra/phonon-alfred-reel.plain.png", + "sha256": "9b16c4d44c1b4c707ea38feb68edcb9093bb5368d85d175400e9cffe7fa16d8c", + "bytes": 334999 + }, + "machine_pixel_analysis": { + "width": 1024, + "height": 512, + "colour_type": 2, + "channels": 3, + "distinct_colours": 623, + "mean_luma": 14.794031, + "var_luma": 683.935618, + "min_luma": 0, + "max_luma": 224, + "top_band_mean_luma": 0.0, + "bottom_band_mean_luma": 43.315219, + "pearson_col_luma_vs_frame_rms_db": 0.875997, + "env_dbfs_window": [ + -80.0, + 0.0 + ] + } + } + } + ] +} diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/pngstat.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/pngstat.py new file mode 100644 index 0000000..2bf0441 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/pngstat.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""pngstat.py — minimal, dependency-free PNG reader + column luminance profile. + +Why this exists: the archive must be able to check its own spectrogram images +without PIL/numpy. Python's stdlib has zlib and struct, which is all a PNG +needs. Supports bit depth 8, non-interlaced, colour types 0/2/3/4/6. +""" +import struct +import zlib +from pathlib import Path + + +class PngError(Exception): + pass + + +def read_png(path): + """Return dict(w, h, colour_type, channels, pixels (bytes), palette (bytes|None)).""" + data = Path(path).read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n": + raise PngError(f"{path}: not a PNG (bad signature)") + pos = 8 + idat = bytearray() + palette = None + w = h = depth = ctype = None + seen_ihdr = False + while pos + 8 <= len(data): + (length,) = struct.unpack(">I", data[pos:pos + 4]) + ctag = data[pos + 4:pos + 8] + body = data[pos + 8:pos + 8 + length] + if len(body) != length: + raise PngError(f"{path}: truncated chunk {ctag!r}") + if ctag == b"IHDR": + w, h, depth, ctype, comp, filt, inter = struct.unpack(">IIBBBBB", body) + seen_ihdr = True + if depth != 8: + raise PngError(f"{path}: bit depth {depth} unsupported") + if inter != 0: + raise PngError(f"{path}: interlaced PNG unsupported") + if comp != 0 or filt != 0: + raise PngError(f"{path}: unsupported compression/filter method") + elif ctag == b"PLTE": + palette = bytes(body) + elif ctag == b"IDAT": + idat += body + elif ctag == b"IEND": + break + pos += 12 + length + if not seen_ihdr: + raise PngError(f"{path}: no IHDR") + if ctype not in (0, 2, 3, 4, 6): + raise PngError(f"{path}: colour type {ctype} unsupported") + + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[ctype] + raw = zlib.decompress(bytes(idat)) + stride = w * channels + if len(raw) != h * (stride + 1): + raise PngError(f"{path}: IDAT size {len(raw)} != expected {h * (stride + 1)}") + + out = bytearray(h * stride) + prev = bytearray(stride) + p = 0 + for y in range(h): + ft = raw[p] + p += 1 + line = bytearray(raw[p:p + stride]) + p += stride + if ft == 0: + pass + elif ft == 1: + for i in range(channels, stride): + line[i] = (line[i] + line[i - channels]) & 0xFF + elif ft == 2: + for i in range(stride): + line[i] = (line[i] + prev[i]) & 0xFF + elif ft == 3: + for i in range(stride): + a = line[i - channels] if i >= channels else 0 + line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF + elif ft == 4: + for i in range(stride): + a = line[i - channels] if i >= channels else 0 + b = prev[i] + c = prev[i - channels] if i >= channels else 0 + pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c) + pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c) + line[i] = (line[i] + pr) & 0xFF + else: + raise PngError(f"{path}: unknown filter type {ft} on row {y}") + out[y * stride:(y + 1) * stride] = line + prev = line + return { + "w": w, + "h": h, + "colour_type": ctype, + "channels": channels, + "pixels": bytes(out), + "palette": palette, + } + + +def _rgb_triple(img, off): + ct = img["colour_type"] + px = img["pixels"] + if ct == 0: + g = px[off] + return g, g, g + if ct == 2: + return px[off], px[off + 1], px[off + 2] + if ct == 4: + g = px[off] + return g, g, g + if ct == 6: + return px[off], px[off + 1], px[off + 2] + # palette + idx = px[off] * 3 + pal = img["palette"] or b"" + return pal[idx], pal[idx + 1], pal[idx + 2] + + +def luma(r, g, b): + return (299 * r + 587 * g + 114 * b) // 1000 + + +def column_profile(img): + """Mean luminance per pixel column (length = width).""" + w, h, ch = img["w"], img["h"], img["channels"] + px = img["pixels"] + ct = img["colour_type"] + prof = [] + for x in range(w): + tot = 0 + for y in range(h): + r, g, b = _rgb_triple(img, (y * w + x) * ch) + tot += luma(r, g, b) + prof.append(tot / h) + return prof + + +def row_profile(img): + """Mean luminance per pixel row (length = height).""" + w, h, ch = img["w"], img["h"], img["channels"] + px = img["pixels"] + prof = [] + for y in range(h): + base = y * w * ch + tot = 0 + for x in range(w): + r, g, b = _rgb_triple(img, base + x * ch) + tot += luma(r, g, b) + prof.append(tot / w) + return prof + + +def image_stats(img): + """Distinct-colour count, mean luminance, luminance variance, min/max.""" + w, h, ch = img["w"], img["h"], img["channels"] + px = img["pixels"] + ct = img["colour_type"] + seen = set() + n = w * h + s = 0.0 + s2 = 0.0 + lo = 256 + hi = -1 + for i in range(n): + r, g, b = _rgb_triple(img, i * ch) + seen.add((r, g, b)) + v = luma(r, g, b) + s += v + s2 += v * v + if v < lo: + lo = v + if v > hi: + hi = v + mean = s / n + var = max(0.0, s2 / n - mean * mean) + return { + "distinct_colours": len(seen), + "mean_luma": mean, + "var_luma": var, + "min_luma": lo, + "max_luma": hi, + } + + +def pearson(a, b): + n = min(len(a), len(b)) + if n < 2: + return float("nan") + a = a[:n] + b = b[:n] + ma = sum(a) / n + mb = sum(b) / n + num = sum((x - ma) * (y - mb) for x, y in zip(a, b)) + da = sum((x - ma) ** 2 for x in a) ** 0.5 + db = sum((y - mb) ** 2 for y in b) ** 0.5 + if da == 0 or db == 0: + return float("nan") + return num / (da * db) + + +def resample_mean(vals, n_out): + """Bin-average a sequence down/up to n_out buckets.""" + n = len(vals) + if n == 0 or n_out <= 0: + return [] + out = [] + for i in range(n_out): + lo = i * n // n_out + hi = max(lo + 1, (i + 1) * n // n_out) + hi = min(hi, n) + seg = vals[lo:hi] + out.append(sum(seg) / len(seg)) + return out diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_correlation.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_correlation.py new file mode 100644 index 0000000..d774b35 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_correlation.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""probe_correlation.py — sanity-check the cross-modal claim before building it. + +Decode the reel to PCM in pure Python, build a 20 ms frame-RMS envelope in dBFS, +then correlate it with the mean-luminance column profile of the ffmpeg +spectrogram of the same file. If the two are really the same signal seen two +ways, Pearson r should be strongly positive. +""" +import math +import subprocess +import sys +from array import array +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import pngstat + +RATE = 44100 +FRAME_N = 882 # 20 ms + + +def decode(path): + p = subprocess.run( + ["ffmpeg", "-hide_banner", "-v", "error", "-i", str(path), + "-f", "s16le", "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(RATE), "-"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + a = array("h") + a.frombytes(p.stdout) + return a + + +def envelope(pcm): + out = [] + for i in range(0, len(pcm) - FRAME_N + 1, FRAME_N): + s = 0 + for v in pcm[i:i + FRAME_N]: + s += v * v + rms = math.sqrt(s / FRAME_N) / 32768.0 + out.append(20.0 * math.log10(rms) if rms > 1e-12 else -240.0) + return out + + +def main(): + src = Path(sys.argv[1]) + png = Path(sys.argv[2]) + pcm = decode(src) + env = envelope(pcm) + img = pngstat.read_png(png) + print("samples", len(pcm), "frames", len(env), "png", img["w"], "x", img["h"], + "ct", img["colour_type"]) + prof = pngstat.column_profile(img) + print("col profile: min %.2f max %.2f mean %.2f" % + (min(prof), max(prof), sum(prof) / len(prof))) + rowp = pngstat.row_profile(img) + print("row profile: first5", ["%.1f" % v for v in rowp[:5]], + "last5", ["%.1f" % v for v in rowp[-5:]]) + st = pngstat.image_stats(img) + print("image stats:", {k: (round(v, 3) if isinstance(v, float) else v) for k, v in st.items()}) + # clip the envelope for correlation: map -80..0 dBFS to 0..1 + envc = [max(0.0, min(1.0, (e + 80.0) / 80.0)) for e in env] + pr = pngstat.resample_mean(prof, len(envc)) + r = pngstat.pearson(pr, envc) + print("PEARSON r(col_luma, frame_rms_dB) = %.4f" % r) + # also try correlation against linear amplitude envelope + ampl = [] + for i in range(0, len(pcm) - FRAME_N + 1, FRAME_N): + s = 0 + for v in pcm[i:i + FRAME_N]: + s += v * v + ampl.append(math.sqrt(s / FRAME_N)) + amp = pngstat.resample_mean(ampl, len(prof)) + r2 = pngstat.pearson(amp, prof) + print("PEARSON r(col_luma, linear_amplitude) = %.4f" % r2) + + +main() diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_threshold.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_threshold.py new file mode 100644 index 0000000..89e5be7 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/probe_threshold.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""probe_threshold.py — measure ffmpeg silencedetect's effective threshold. + +The filter compares each SAMPLE against the threshold (|x| < noise), so a DC +signal is the only clean probe: no zero crossings to confuse the test. Build a +WAV of DC blocks at known levels, separated by digital silence, then ask which +blocks ffmpeg calls silent. The flip point is the effective threshold. + +Also quantifies the zero-crossing artefact on a pure tone. +""" +import math +import re +import struct +import subprocess +import sys +from pathlib import Path + +RATE = 44100 +SCRATCH = Path("/root/.hermes/profiles/sphinx/cache/scratch/sphinx-audio-build") +SCRATCH.mkdir(parents=True, exist_ok=True) + + +def write_wav(path, samples_i16): + buf = b"".join(struct.pack(" 0 else float("-inf") + + +def dc_sweep(): + levels = [-60, -50, -45, -40, -38, -36, -35, -34, -32, -30, -25, -20] + block = int(RATE * 0.5) # 0.5 s DC + gap = int(RATE * 0.5) # 0.5 s digital silence + samples = [] + marks = [] + t = 0.0 + for lv in levels: + a = int(round(db_to_amp(lv) * 32768)) + a = max(-32768, min(32767, a)) + samples.extend([a] * block) + marks.append((lv, t, t + block / RATE, a)) + t += block / RATE + samples.extend([0] * gap) + t += gap / RATE + path = SCRATCH / "dc_sweep.wav" + write_wav(path, samples) + spans = run_silencedetect(path, d=0.2) + print("=== DC sweep: noise=-35.0dB, d=0.2 ===") + print("block level_dBFS amp_i16 span_seen_as_silence") + for lv, s0, s1, a in marks: + hit = [sp for sp in spans if sp[0] < s1 - 0.05 and sp[1] > s0 + 0.05] + print(f" {s0:6.3f}-{s1:6.3f} {lv:6d} {a:7d} {'SILENT' if hit else 'not silent'}" + f" {hit}") + return spans, marks + + +def zero_crossing_artefact(): + """A pure tone at -20 dBFS: every zero crossing is a short 'silence'.""" + for freq in (1000, 4000): + n = RATE * 2 + amp = int(db_to_amp(-20) * 32768) + samples = [int(amp * math.sin(2 * math.pi * freq * i / RATE)) for i in range(n)] + path = SCRATCH / f"tone{freq}.wav" + write_wav(path, samples) + spans = run_silencedetect(path, d=0.0) + durs = [e - s for s, e in spans] + print(f"=== pure {freq} Hz @ -20 dBFS, d=0.0: {len(spans)} 'silence' runs in 2.000 s " + f"(expected ~{2*freq*2} zero crossings) ===") + if durs: + print(f" duration: min {min(durs)*1e6:.1f} us max {max(durs)*1e6:.1f} us " + f"mean {sum(durs)/len(durs)*1e6:.1f} us") + if len(spans) > 2: + gaps = [spans[i + 1][0] - spans[i][0] for i in range(len(spans) - 1)] + print(f" start-to-start spacing: mean {sum(gaps)/len(gaps)*1e6:.1f} us " + f"(1/{2*freq} s = {1/(2*freq)*1e6:.1f} us)") + + +def step_edge(): + """DC step: 2 s of loud DC, then 2 s of digital zero. No zero crossings.""" + amp = int(db_to_amp(-20) * 32768) + samples = [amp] * (RATE * 2) + [0] * (RATE * 2) + path = SCRATCH / "dc_step.wav" + write_wav(path, samples) + spans = run_silencedetect(path, d=0.0) + print("=== DC step (2.000 s DC @ -20 dBFS, then 2.000 s zero), d=0.0 ===") + print(" spans:", [(round(s, 6), round(e, 6)) for s, e in spans]) + if spans: + s, e = spans[0] + print(f" first silence_start={s:.6f} (true edge 2.000000, err {abs(s-2.0)*1000:.3f} ms " + f"= {abs(s-2.0)*RATE:.0f} samples)") + print(f" silence_end={e:.6f} (true end 4.000000, err {abs(e-4.0)*1000:.3f} ms)") + + +if __name__ == "__main__": + print("ffmpeg:", subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE) + .stdout.decode().splitlines()[0]) + print() + dc_sweep() + print() + step_edge() + print() + zero_crossing_artefact() diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/report_data.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/report_data.py new file mode 100644 index 0000000..60d7c00 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/report_data.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""report_data.py — print the measured numbers used in REPORT.md.""" +import json +from pathlib import Path + +R = Path(__file__).resolve().parent +man = json.loads((R / "manifest.json").read_text()) +seg = json.loads((R / "segments.json").read_text()) +cal = man["calibration"] + +print("=== TOOLS ===") +print(json.dumps(man["tools"], indent=1)) +d = cal["dc_sweep"] +print("\n=== DC SWEEP ===") +print("predicted cutoff amp:", d["predicted_cutoff_amp_i16"], "dBFS", d["predicted_cutoff_dbfs"]) +print("flip between:", d["flip_is_between_dbfs"]) +print("loudest silent:", d["loudest_block_called_silent"]["level_dbfs"], + d["loudest_block_called_silent"]["amp_i16"]) +print("quietest audible:", d["quietest_block_called_audible"]["level_dbfs"], + d["quietest_block_called_audible"]["amp_i16"]) +print("\n=== DC STEP ===") +print(json.dumps({k: v for k, v in cal["dc_step"].items() if k != "probe"}, indent=1)) +print("\n=== TONE ===") +print(json.dumps(cal["tone_zero_crossing"], indent=1)) +print("\n=== ASSETS ===") +for a in man["assets"]: + s = seg["assets"][a["id"]] + print(f"{a['id']}: {a['container']['duration_s']}s {a['stream']['codec_name']} " + f"{a['stream']['sample_rate']}Hz {a['stream']['channels']}ch {a['stream']['bit_rate']}bps") + print(f" sha256={a['sha256'][:16]}... bytes={a['bytes']} decoded={a['decoded']['samples']} " + f"(astats {a['astats']['samples']}, delta {a['decoded_minus_astats_samples']})") + print(f" astats peak={a['astats']['peak_level_db']} rms={a['astats']['rms_level_db']} " + f"entropy={a['astats']['entropy']} dc={a['astats']['dc_offset']} " + f"flat={a['astats']['flat_factor']}") + print(f" ebur128 I={a['ebur128']['integrated_lufs']} LUFS LRA={a['ebur128']['loudness_range_lu']} " + f"TP={a['ebur128']['true_peak_dbfs']} dBFS") + c, b = s["a_vs_c"], s["a_vs_b"] + print(f" A runs={c['count_a']} sil={c['total_a_s']}s | C runs={c['count_b']} sil={c['total_b_s']}s " + f"| dev={c['max_start_dev_samples']}/{c['max_end_dev_samples']} samples") + print(f" B runs={b['count_b']} sil={b['total_b_s']}s | diff={b['total_diff_s']}s " + f"({100 * b['total_diff_s'] / b['total_a_s']:.1f}%) ratioA={s['silence_ratio_a']}") + pa = a["spectra"]["machine_pixel_analysis"] + print(f" PNG {pa['width']}x{pa['height']} ct={pa['colour_type']} colours={pa['distinct_colours']} " + f"varluma={pa['var_luma']} top={pa['top_band_mean_luma']} bottom={pa['bottom_band_mean_luma']}") + print(f" r={pa['pearson_col_luma_vs_frame_rms_db']} human={a['spectra']['human']['bytes']}B " + f"machine={a['spectra']['machine']['bytes']}B") + print(f" A spans[:3]: {s['method_a_silence'][:3]}") + print(f" speech spans A: {s['method_a_speech']}") +print("\nevidence:", json.dumps(man["reference_implementation"], indent=1)) +print("claims:", len(json.loads((R / 'claims.json').read_text())["claims"])) diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/segments.json b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/segments.json new file mode 100644 index 0000000..0288f53 --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/segments.json @@ -0,0 +1,1859 @@ +{ + "artifact": "sphinx/phonon-reel-audio-archive", + "index_kind": "time-addressable speech/silence segment index", + "threshold_db": -35.0, + "min_silence_s": 0.5, + "methods": { + "A": "ffmpeg silencedetect=noise=-35.0dB:d=0.5 (reference)", + "B": "frame RMS, 20 ms frames, full scale 1.0 (naive model)", + "C": "pure-Python reimplementation of A's per-sample rule (|x| < 10^(-35/20), runs >= 22050 samples)" + }, + "assets": { + "phonon-male-reel": { + "duration_s": 54.22, + "method_a_silence": [ + [ + 4.40615, + 5.91048 + ], + [ + 6.85474, + 7.36048 + ], + [ + 10.6623, + 12.2614 + ], + [ + 16.7238, + 18.3272 + ], + [ + 22.8441, + 24.4794 + ], + [ + 29.0325, + 30.5133 + ], + [ + 34.8266, + 36.3788 + ], + [ + 40.5726, + 42.0909 + ], + [ + 46.8084, + 48.3189 + ], + [ + 52.8152, + 54.22 + ] + ], + "method_b_silence": [ + [ + 0.9, + 1.42 + ], + [ + 4.32, + 5.92 + ], + [ + 6.84, + 7.36 + ], + [ + 10.64, + 12.28 + ], + [ + 16.66, + 18.36 + ], + [ + 19.04, + 19.7 + ], + [ + 22.82, + 24.52 + ], + [ + 28.9, + 30.52 + ], + [ + 34.68, + 36.4 + ], + [ + 40.42, + 42.1 + ], + [ + 43.02, + 43.54 + ], + [ + 46.7, + 48.34 + ], + [ + 49.04, + 49.66 + ], + [ + 52.8, + 54.22 + ] + ], + "method_c_silence": [ + [ + 4.406145, + 5.910476 + ], + [ + 6.854739, + 7.360476 + ], + [ + 10.662313, + 12.261383 + ], + [ + 16.72381, + 18.327234 + ], + [ + 22.844127, + 24.479388 + ], + [ + 29.03254, + 30.513265 + ], + [ + 34.826644, + 36.378844 + ], + [ + 40.572608, + 42.090862 + ], + [ + 46.808435, + 48.318889 + ], + [ + 52.815215, + 54.22 + ] + ], + "method_a_speech": [ + [ + 0.0, + 4.40615 + ], + [ + 5.91048, + 6.85474 + ], + [ + 7.36048, + 10.6623 + ], + [ + 12.2614, + 16.7238 + ], + [ + 18.3272, + 22.8441 + ], + [ + 24.4794, + 29.0325 + ], + [ + 30.5133, + 34.8266 + ], + [ + 36.3788, + 40.5726 + ], + [ + 42.0909, + 46.8084 + ], + [ + 48.3189, + 52.8152 + ] + ], + "method_b_speech": [ + [ + 0.0, + 0.9 + ], + [ + 1.42, + 4.32 + ], + [ + 5.92, + 6.84 + ], + [ + 7.36, + 10.64 + ], + [ + 12.28, + 16.66 + ], + [ + 18.36, + 19.04 + ], + [ + 19.7, + 22.82 + ], + [ + 24.52, + 28.9 + ], + [ + 30.52, + 34.68 + ], + [ + 36.4, + 40.42 + ], + [ + 42.1, + 43.02 + ], + [ + 43.54, + 46.7 + ], + [ + 48.34, + 49.04 + ], + [ + 49.66, + 52.8 + ] + ], + "method_c_speech": [ + [ + 0.0, + 4.406145 + ], + [ + 5.910476, + 6.854739 + ], + [ + 7.360476, + 10.662313 + ], + [ + 12.261383, + 16.72381 + ], + [ + 18.327234, + 22.844127 + ], + [ + 24.479388, + 29.03254 + ], + [ + 30.513265, + 34.826644 + ], + [ + 36.378844, + 40.572608 + ], + [ + 42.090862, + 46.808435 + ], + [ + 48.318889, + 52.815215 + ] + ], + "silence_total_a_s": 14.31447, + "silence_total_b_s": 17.56, + "silence_total_c_s": 14.314241, + "a_vs_c": { + "count_a": 10, + "count_b": 10, + "count_diff": 0, + "total_a_s": 14.31447, + "total_b_s": 14.314241, + "total_diff_s": 0.000229, + "matched": 10, + "max_start_dev_s": 4.4e-05, + "max_end_dev_s": 4.4e-05, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2, + "pairs": [ + { + "a": [ + 4.40615, + 5.91048 + ], + "b": [ + 4.406145, + 5.910476 + ], + "start_dev_s": 5e-06, + "end_dev_s": 4e-06 + }, + { + "a": [ + 6.85474, + 7.36048 + ], + "b": [ + 6.854739, + 7.360476 + ], + "start_dev_s": 1e-06, + "end_dev_s": 4e-06 + }, + { + "a": [ + 10.6623, + 12.2614 + ], + "b": [ + 10.662313, + 12.261383 + ], + "start_dev_s": 1.3e-05, + "end_dev_s": 1.7e-05 + }, + { + "a": [ + 16.7238, + 18.3272 + ], + "b": [ + 16.72381, + 18.327234 + ], + "start_dev_s": 1e-05, + "end_dev_s": 3.4e-05 + }, + { + "a": [ + 22.8441, + 24.4794 + ], + "b": [ + 22.844127, + 24.479388 + ], + "start_dev_s": 2.7e-05, + "end_dev_s": 1.2e-05 + }, + { + "a": [ + 29.0325, + 30.5133 + ], + "b": [ + 29.03254, + 30.513265 + ], + "start_dev_s": 4e-05, + "end_dev_s": 3.5e-05 + }, + { + "a": [ + 34.8266, + 36.3788 + ], + "b": [ + 34.826644, + 36.378844 + ], + "start_dev_s": 4.4e-05, + "end_dev_s": 4.4e-05 + }, + { + "a": [ + 40.5726, + 42.0909 + ], + "b": [ + 40.572608, + 42.090862 + ], + "start_dev_s": 8e-06, + "end_dev_s": 3.8e-05 + }, + { + "a": [ + 46.8084, + 48.3189 + ], + "b": [ + 46.808435, + 48.318889 + ], + "start_dev_s": 3.5e-05, + "end_dev_s": 1.1e-05 + }, + { + "a": [ + 52.8152, + 54.22 + ], + "b": [ + 52.815215, + 54.22 + ], + "start_dev_s": 1.5e-05, + "end_dev_s": 0.0 + } + ] + }, + "a_vs_b": { + "count_a": 10, + "count_b": 14, + "count_diff": 4, + "total_a_s": 14.31447, + "total_b_s": 17.56, + "total_diff_s": 3.24553, + "matched": 10, + "max_start_dev_s": 0.1526, + "max_end_dev_s": 0.0406, + "max_start_dev_samples": 6730, + "max_end_dev_samples": 1790, + "pairs": [ + { + "a": [ + 4.40615, + 5.91048 + ], + "b": [ + 4.32, + 5.92 + ], + "start_dev_s": 0.08615, + "end_dev_s": 0.00952 + }, + { + "a": [ + 6.85474, + 7.36048 + ], + "b": [ + 6.84, + 7.36 + ], + "start_dev_s": 0.01474, + "end_dev_s": 0.00048 + }, + { + "a": [ + 10.6623, + 12.2614 + ], + "b": [ + 10.64, + 12.28 + ], + "start_dev_s": 0.0223, + "end_dev_s": 0.0186 + }, + { + "a": [ + 16.7238, + 18.3272 + ], + "b": [ + 16.66, + 18.36 + ], + "start_dev_s": 0.0638, + "end_dev_s": 0.0328 + }, + { + "a": [ + 22.8441, + 24.4794 + ], + "b": [ + 22.82, + 24.52 + ], + "start_dev_s": 0.0241, + "end_dev_s": 0.0406 + }, + { + "a": [ + 29.0325, + 30.5133 + ], + "b": [ + 28.9, + 30.52 + ], + "start_dev_s": 0.1325, + "end_dev_s": 0.0067 + }, + { + "a": [ + 34.8266, + 36.3788 + ], + "b": [ + 34.68, + 36.4 + ], + "start_dev_s": 0.1466, + "end_dev_s": 0.0212 + }, + { + "a": [ + 40.5726, + 42.0909 + ], + "b": [ + 40.42, + 42.1 + ], + "start_dev_s": 0.1526, + "end_dev_s": 0.0091 + }, + { + "a": [ + 46.8084, + 48.3189 + ], + "b": [ + 46.7, + 48.34 + ], + "start_dev_s": 0.1084, + "end_dev_s": 0.0211 + }, + { + "a": [ + 52.8152, + 54.22 + ], + "b": [ + 52.8, + 54.22 + ], + "start_dev_s": 0.0152, + "end_dev_s": 0.0 + } + ] + }, + "silence_ratio_a": 0.264007, + "silence_ratio_c": 0.264003 + }, + "phonon-british-reel": { + "duration_s": 51.28, + "method_a_silence": [ + [ + 4.86844, + 6.37889 + ], + [ + 10.8752, + 12.4256 + ], + [ + 13.3068, + 13.9125 + ], + [ + 17.3125, + 18.8098 + ], + [ + 23.393, + 24.8547 + ], + [ + 29.5612, + 31.0388 + ], + [ + 35.2326, + 36.7418 + ], + [ + 37.8757, + 38.6815 + ], + [ + 42.7366, + 44.2715 + ], + [ + 45.2646, + 46.0442 + ], + [ + 49.9332, + 51.28 + ] + ], + "method_b_silence": [ + [ + 1.08, + 1.6 + ], + [ + 4.76, + 6.4 + ], + [ + 7.1, + 7.72 + ], + [ + 10.86, + 12.46 + ], + [ + 13.14, + 13.9 + ], + [ + 15.2, + 15.74 + ], + [ + 17.2, + 18.8 + ], + [ + 19.66, + 20.16 + ], + [ + 23.28, + 24.9 + ], + [ + 29.38, + 31.06 + ], + [ + 35.08, + 36.76 + ], + [ + 37.82, + 38.68 + ], + [ + 40.14, + 40.78 + ], + [ + 42.54, + 44.28 + ], + [ + 45.04, + 46.04 + ], + [ + 46.54, + 47.06 + ], + [ + 47.6, + 48.24 + ], + [ + 49.8, + 51.28 + ] + ], + "method_c_silence": [ + [ + 4.868435, + 6.378889 + ], + [ + 10.875215, + 12.425578 + ], + [ + 13.306848, + 13.912517 + ], + [ + 17.312494, + 18.809796 + ], + [ + 23.393039, + 24.854694 + ], + [ + 29.561247, + 31.038844 + ], + [ + 35.232608, + 36.741769 + ], + [ + 37.875692, + 38.681474 + ], + [ + 42.736621, + 44.271451 + ], + [ + 45.264649, + 46.044195 + ], + [ + 49.933152, + 51.28 + ] + ], + "method_a_speech": [ + [ + 0.0, + 4.86844 + ], + [ + 6.37889, + 10.8752 + ], + [ + 12.4256, + 13.3068 + ], + [ + 13.9125, + 17.3125 + ], + [ + 18.8098, + 23.393 + ], + [ + 24.8547, + 29.5612 + ], + [ + 31.0388, + 35.2326 + ], + [ + 36.7418, + 37.8757 + ], + [ + 38.6815, + 42.7366 + ], + [ + 44.2715, + 45.2646 + ], + [ + 46.0442, + 49.9332 + ] + ], + "method_b_speech": [ + [ + 0.0, + 1.08 + ], + [ + 1.6, + 4.76 + ], + [ + 6.4, + 7.1 + ], + [ + 7.72, + 10.86 + ], + [ + 12.46, + 13.14 + ], + [ + 13.9, + 15.2 + ], + [ + 15.74, + 17.2 + ], + [ + 18.8, + 19.66 + ], + [ + 20.16, + 23.28 + ], + [ + 24.9, + 29.38 + ], + [ + 31.06, + 35.08 + ], + [ + 36.76, + 37.82 + ], + [ + 38.68, + 40.14 + ], + [ + 40.78, + 42.54 + ], + [ + 44.28, + 45.04 + ], + [ + 46.04, + 46.54 + ], + [ + 47.06, + 47.6 + ], + [ + 48.24, + 49.8 + ] + ], + "method_c_speech": [ + [ + 0.0, + 4.868435 + ], + [ + 6.378889, + 10.875215 + ], + [ + 12.425578, + 13.306848 + ], + [ + 13.912517, + 17.312494 + ], + [ + 18.809796, + 23.393039 + ], + [ + 24.854694, + 29.561247 + ], + [ + 31.038844, + 35.232608 + ], + [ + 36.741769, + 37.875692 + ], + [ + 38.681474, + 42.736621 + ], + [ + 44.271451, + 45.264649 + ], + [ + 46.044195, + 49.933152 + ] + ], + "silence_total_a_s": 14.07945, + "silence_total_b_s": 19.64, + "silence_total_c_s": 14.079207, + "a_vs_c": { + "count_a": 11, + "count_b": 11, + "count_diff": 0, + "total_a_s": 14.07945, + "total_b_s": 14.079207, + "total_diff_s": 0.000243, + "matched": 11, + "max_start_dev_s": 4.9e-05, + "max_end_dev_s": 4.9e-05, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2, + "pairs": [ + { + "a": [ + 4.86844, + 6.37889 + ], + "b": [ + 4.868435, + 6.378889 + ], + "start_dev_s": 5e-06, + "end_dev_s": 1e-06 + }, + { + "a": [ + 10.8752, + 12.4256 + ], + "b": [ + 10.875215, + 12.425578 + ], + "start_dev_s": 1.5e-05, + "end_dev_s": 2.2e-05 + }, + { + "a": [ + 13.3068, + 13.9125 + ], + "b": [ + 13.306848, + 13.912517 + ], + "start_dev_s": 4.8e-05, + "end_dev_s": 1.7e-05 + }, + { + "a": [ + 17.3125, + 18.8098 + ], + "b": [ + 17.312494, + 18.809796 + ], + "start_dev_s": 6e-06, + "end_dev_s": 4e-06 + }, + { + "a": [ + 23.393, + 24.8547 + ], + "b": [ + 23.393039, + 24.854694 + ], + "start_dev_s": 3.9e-05, + "end_dev_s": 6e-06 + }, + { + "a": [ + 29.5612, + 31.0388 + ], + "b": [ + 29.561247, + 31.038844 + ], + "start_dev_s": 4.7e-05, + "end_dev_s": 4.4e-05 + }, + { + "a": [ + 35.2326, + 36.7418 + ], + "b": [ + 35.232608, + 36.741769 + ], + "start_dev_s": 8e-06, + "end_dev_s": 3.1e-05 + }, + { + "a": [ + 37.8757, + 38.6815 + ], + "b": [ + 37.875692, + 38.681474 + ], + "start_dev_s": 8e-06, + "end_dev_s": 2.6e-05 + }, + { + "a": [ + 42.7366, + 44.2715 + ], + "b": [ + 42.736621, + 44.271451 + ], + "start_dev_s": 2.1e-05, + "end_dev_s": 4.9e-05 + }, + { + "a": [ + 45.2646, + 46.0442 + ], + "b": [ + 45.264649, + 46.044195 + ], + "start_dev_s": 4.9e-05, + "end_dev_s": 5e-06 + }, + { + "a": [ + 49.9332, + 51.28 + ], + "b": [ + 49.933152, + 51.28 + ], + "start_dev_s": 4.8e-05, + "end_dev_s": 0.0 + } + ] + }, + "a_vs_b": { + "count_a": 11, + "count_b": 18, + "count_diff": 7, + "total_a_s": 14.07945, + "total_b_s": 19.64, + "total_diff_s": 5.56055, + "matched": 11, + "max_start_dev_s": 0.2246, + "max_end_dev_s": 0.0453, + "max_start_dev_samples": 9905, + "max_end_dev_samples": 1998, + "pairs": [ + { + "a": [ + 4.86844, + 6.37889 + ], + "b": [ + 4.76, + 6.4 + ], + "start_dev_s": 0.10844, + "end_dev_s": 0.02111 + }, + { + "a": [ + 10.8752, + 12.4256 + ], + "b": [ + 10.86, + 12.46 + ], + "start_dev_s": 0.0152, + "end_dev_s": 0.0344 + }, + { + "a": [ + 13.3068, + 13.9125 + ], + "b": [ + 13.14, + 13.9 + ], + "start_dev_s": 0.1668, + "end_dev_s": 0.0125 + }, + { + "a": [ + 17.3125, + 18.8098 + ], + "b": [ + 17.2, + 18.8 + ], + "start_dev_s": 0.1125, + "end_dev_s": 0.0098 + }, + { + "a": [ + 23.393, + 24.8547 + ], + "b": [ + 23.28, + 24.9 + ], + "start_dev_s": 0.113, + "end_dev_s": 0.0453 + }, + { + "a": [ + 29.5612, + 31.0388 + ], + "b": [ + 29.38, + 31.06 + ], + "start_dev_s": 0.1812, + "end_dev_s": 0.0212 + }, + { + "a": [ + 35.2326, + 36.7418 + ], + "b": [ + 35.08, + 36.76 + ], + "start_dev_s": 0.1526, + "end_dev_s": 0.0182 + }, + { + "a": [ + 37.8757, + 38.6815 + ], + "b": [ + 37.82, + 38.68 + ], + "start_dev_s": 0.0557, + "end_dev_s": 0.0015 + }, + { + "a": [ + 42.7366, + 44.2715 + ], + "b": [ + 42.54, + 44.28 + ], + "start_dev_s": 0.1966, + "end_dev_s": 0.0085 + }, + { + "a": [ + 45.2646, + 46.0442 + ], + "b": [ + 45.04, + 46.04 + ], + "start_dev_s": 0.2246, + "end_dev_s": 0.0042 + }, + { + "a": [ + 49.9332, + 51.28 + ], + "b": [ + 49.8, + 51.28 + ], + "start_dev_s": 0.1332, + "end_dev_s": 0.0 + } + ] + }, + "silence_ratio_a": 0.27456, + "silence_ratio_c": 0.274556 + }, + "phonon-alfred-reel": { + "duration_s": 34.76, + "method_a_silence": [ + [ + 0.623673, + 1.43066 + ], + [ + 4.69896, + 6.2098 + ], + [ + 6.69018, + 7.52333 + ], + [ + 10.9591, + 12.5765 + ], + [ + 13.0101, + 13.8732 + ], + [ + 14.8774, + 15.3897 + ], + [ + 17.0912, + 18.6443 + ], + [ + 22.4908, + 23.9839 + ], + [ + 24.3653, + 25.1144 + ], + [ + 27.8839, + 29.4943 + ], + [ + 29.9158, + 30.5287 + ], + [ + 33.3266, + 34.76 + ] + ], + "method_b_silence": [ + [ + 0.58, + 1.42 + ], + [ + 2.44, + 2.96 + ], + [ + 4.64, + 6.22 + ], + [ + 6.64, + 7.52 + ], + [ + 8.44, + 9.18 + ], + [ + 10.82, + 12.58 + ], + [ + 12.98, + 13.86 + ], + [ + 14.66, + 15.4 + ], + [ + 16.92, + 18.64 + ], + [ + 22.38, + 23.98 + ], + [ + 24.34, + 25.12 + ], + [ + 25.9, + 26.42 + ], + [ + 27.76, + 29.5 + ], + [ + 29.92, + 30.52 + ], + [ + 33.16, + 34.76 + ] + ], + "method_c_silence": [ + [ + 0.623673, + 1.430658 + ], + [ + 4.698957, + 6.209796 + ], + [ + 6.690181, + 7.523333 + ], + [ + 10.95907, + 12.576463 + ], + [ + 13.010068, + 13.873152 + ], + [ + 14.877438, + 15.38966 + ], + [ + 17.091179, + 18.644331 + ], + [ + 22.490839, + 23.983946 + ], + [ + 24.365329, + 25.114399 + ], + [ + 27.883878, + 29.494331 + ], + [ + 29.915782, + 30.528662 + ], + [ + 33.326644, + 34.76 + ] + ], + "method_a_speech": [ + [ + 0.0, + 0.623673 + ], + [ + 1.43066, + 4.69896 + ], + [ + 6.2098, + 6.69018 + ], + [ + 7.52333, + 10.9591 + ], + [ + 12.5765, + 13.0101 + ], + [ + 13.8732, + 14.8774 + ], + [ + 15.3897, + 17.0912 + ], + [ + 18.6443, + 22.4908 + ], + [ + 23.9839, + 24.3653 + ], + [ + 25.1144, + 27.8839 + ], + [ + 29.4943, + 29.9158 + ], + [ + 30.5287, + 33.3266 + ] + ], + "method_b_speech": [ + [ + 0.0, + 0.58 + ], + [ + 1.42, + 2.44 + ], + [ + 2.96, + 4.64 + ], + [ + 6.22, + 6.64 + ], + [ + 7.52, + 8.44 + ], + [ + 9.18, + 10.82 + ], + [ + 12.58, + 12.98 + ], + [ + 13.86, + 14.66 + ], + [ + 15.4, + 16.92 + ], + [ + 18.64, + 22.38 + ], + [ + 23.98, + 24.34 + ], + [ + 25.12, + 25.9 + ], + [ + 26.42, + 27.76 + ], + [ + 29.5, + 29.92 + ], + [ + 30.52, + 33.16 + ] + ], + "method_c_speech": [ + [ + 0.0, + 0.623673 + ], + [ + 1.430658, + 4.698957 + ], + [ + 6.209796, + 6.690181 + ], + [ + 7.523333, + 10.95907 + ], + [ + 12.576463, + 13.010068 + ], + [ + 13.873152, + 14.877438 + ], + [ + 15.38966, + 17.091179 + ], + [ + 18.644331, + 22.490839 + ], + [ + 23.983946, + 24.365329 + ], + [ + 25.114399, + 27.883878 + ], + [ + 29.494331, + 29.915782 + ], + [ + 30.528662, + 33.326644 + ] + ], + "silence_total_a_s": 13.595777, + "silence_total_b_s": 16.5, + "silence_total_c_s": 13.595693, + "a_vs_c": { + "count_a": 12, + "count_b": 12, + "count_diff": 0, + "total_a_s": 13.595777, + "total_b_s": 13.595693, + "total_diff_s": 8.4e-05, + "matched": 12, + "max_start_dev_s": 4.4e-05, + "max_end_dev_s": 4.8e-05, + "max_start_dev_samples": 2, + "max_end_dev_samples": 2, + "pairs": [ + { + "a": [ + 0.623673, + 1.43066 + ], + "b": [ + 0.623673, + 1.430658 + ], + "start_dev_s": 0.0, + "end_dev_s": 2e-06 + }, + { + "a": [ + 4.69896, + 6.2098 + ], + "b": [ + 4.698957, + 6.209796 + ], + "start_dev_s": 3e-06, + "end_dev_s": 4e-06 + }, + { + "a": [ + 6.69018, + 7.52333 + ], + "b": [ + 6.690181, + 7.523333 + ], + "start_dev_s": 1e-06, + "end_dev_s": 3e-06 + }, + { + "a": [ + 10.9591, + 12.5765 + ], + "b": [ + 10.95907, + 12.576463 + ], + "start_dev_s": 3e-05, + "end_dev_s": 3.7e-05 + }, + { + "a": [ + 13.0101, + 13.8732 + ], + "b": [ + 13.010068, + 13.873152 + ], + "start_dev_s": 3.2e-05, + "end_dev_s": 4.8e-05 + }, + { + "a": [ + 14.8774, + 15.3897 + ], + "b": [ + 14.877438, + 15.38966 + ], + "start_dev_s": 3.8e-05, + "end_dev_s": 4e-05 + }, + { + "a": [ + 17.0912, + 18.6443 + ], + "b": [ + 17.091179, + 18.644331 + ], + "start_dev_s": 2.1e-05, + "end_dev_s": 3.1e-05 + }, + { + "a": [ + 22.4908, + 23.9839 + ], + "b": [ + 22.490839, + 23.983946 + ], + "start_dev_s": 3.9e-05, + "end_dev_s": 4.6e-05 + }, + { + "a": [ + 24.3653, + 25.1144 + ], + "b": [ + 24.365329, + 25.114399 + ], + "start_dev_s": 2.9e-05, + "end_dev_s": 1e-06 + }, + { + "a": [ + 27.8839, + 29.4943 + ], + "b": [ + 27.883878, + 29.494331 + ], + "start_dev_s": 2.2e-05, + "end_dev_s": 3.1e-05 + }, + { + "a": [ + 29.9158, + 30.5287 + ], + "b": [ + 29.915782, + 30.528662 + ], + "start_dev_s": 1.8e-05, + "end_dev_s": 3.8e-05 + }, + { + "a": [ + 33.3266, + 34.76 + ], + "b": [ + 33.326644, + 34.76 + ], + "start_dev_s": 4.4e-05, + "end_dev_s": 0.0 + } + ] + }, + "a_vs_b": { + "count_a": 12, + "count_b": 15, + "count_diff": 3, + "total_a_s": 13.595777, + "total_b_s": 16.5, + "total_diff_s": 2.904223, + "matched": 12, + "max_start_dev_s": 0.2174, + "max_end_dev_s": 0.0132, + "max_start_dev_samples": 9587, + "max_end_dev_samples": 582, + "pairs": [ + { + "a": [ + 0.623673, + 1.43066 + ], + "b": [ + 0.58, + 1.42 + ], + "start_dev_s": 0.043673, + "end_dev_s": 0.01066 + }, + { + "a": [ + 4.69896, + 6.2098 + ], + "b": [ + 4.64, + 6.22 + ], + "start_dev_s": 0.05896, + "end_dev_s": 0.0102 + }, + { + "a": [ + 6.69018, + 7.52333 + ], + "b": [ + 6.64, + 7.52 + ], + "start_dev_s": 0.05018, + "end_dev_s": 0.00333 + }, + { + "a": [ + 10.9591, + 12.5765 + ], + "b": [ + 10.82, + 12.58 + ], + "start_dev_s": 0.1391, + "end_dev_s": 0.0035 + }, + { + "a": [ + 13.0101, + 13.8732 + ], + "b": [ + 12.98, + 13.86 + ], + "start_dev_s": 0.0301, + "end_dev_s": 0.0132 + }, + { + "a": [ + 14.8774, + 15.3897 + ], + "b": [ + 14.66, + 15.4 + ], + "start_dev_s": 0.2174, + "end_dev_s": 0.0103 + }, + { + "a": [ + 17.0912, + 18.6443 + ], + "b": [ + 16.92, + 18.64 + ], + "start_dev_s": 0.1712, + "end_dev_s": 0.0043 + }, + { + "a": [ + 22.4908, + 23.9839 + ], + "b": [ + 22.38, + 23.98 + ], + "start_dev_s": 0.1108, + "end_dev_s": 0.0039 + }, + { + "a": [ + 24.3653, + 25.1144 + ], + "b": [ + 24.34, + 25.12 + ], + "start_dev_s": 0.0253, + "end_dev_s": 0.0056 + }, + { + "a": [ + 27.8839, + 29.4943 + ], + "b": [ + 27.76, + 29.5 + ], + "start_dev_s": 0.1239, + "end_dev_s": 0.0057 + }, + { + "a": [ + 29.9158, + 30.5287 + ], + "b": [ + 29.92, + 30.52 + ], + "start_dev_s": 0.0042, + "end_dev_s": 0.0087 + }, + { + "a": [ + 33.3266, + 34.76 + ], + "b": [ + 33.16, + 34.76 + ], + "start_dev_s": 0.1666, + "end_dev_s": 0.0 + } + ] + }, + "silence_ratio_a": 0.391133, + "silence_ratio_c": 0.39113 + } + } +} diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-alfred-reel.mp3 b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-alfred-reel.mp3 new file mode 100644 index 0000000..842615d Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-alfred-reel.mp3 differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-british-reel.mp3 b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-british-reel.mp3 new file mode 100644 index 0000000..5a2d0b1 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-british-reel.mp3 differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-male-reel.mp3 b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-male-reel.mp3 new file mode 100644 index 0000000..064c2ce Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/sources/phonon-male-reel.mp3 differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.plain.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.plain.png new file mode 100644 index 0000000..d0c9341 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.plain.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.png new file mode 100644 index 0000000..489ecd1 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-alfred-reel.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.plain.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.plain.png new file mode 100644 index 0000000..8532ab4 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.plain.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.png new file mode 100644 index 0000000..4675327 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-british-reel.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.plain.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.plain.png new file mode 100644 index 0000000..dc9a951 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.plain.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.png b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.png new file mode 100644 index 0000000..1d52fc0 Binary files /dev/null and b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/spectra/phonon-male-reel.png differ diff --git a/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/verify.py b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/verify.py new file mode 100644 index 0000000..df5484f --- /dev/null +++ b/wizard-work/sphinx/phonon-reel-audio-archive-2026-10-21/verify.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +"""verify.py — recompute every claim in claims.json from the raw media. + +Reads nothing on trust. For each claim it re-runs the measurement that produced +it (ffprobe / astats / ebur128 / silencedetect / a fresh float32 decode / a +stdlib PNG decode / the synthetic calibration signals) and compares the fresh +number against the frozen one within the claim's stated tolerance. + +It also re-checks the emitted artifact files themselves (segments.json, +manifest.json, spectra hashes, the pinned ffmpeg source), because a claims file +that agrees with itself proves nothing. + +Exit 0 only if every claim and every artifact-file check passes. + +Usage: python3 verify.py [--claims PATH] [--quiet] +""" +import argparse +import hashlib +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build as B +import pngstat + +ROOT = Path(__file__).resolve().parent + + +def sha256_file(p): + h = hashlib.sha256() + with open(p, "rb") as fh: + for blk in iter(lambda: fh.read(1 << 20), b""): + h.update(blk) + return h.hexdigest() + + +# -------------------------------------------------------------------------- +# fresh measurements (heavy; computed once per run) +# -------------------------------------------------------------------------- +_MEASURED = None + + +def measure(): + global _MEASURED + if _MEASURED is not None: + return _MEASURED + m = {"assets": {}, "calibration": {}, "evidence": {}} + + for a in B.ASSETS: + src = B.SRC_DIR / a["file"] + aid = a["id"] + pcm = B.decode_f32(src) + span_a = B.silencedetect(src) + span_b, series = B.method_b(pcm) + span_c = B.method_c(pcm) + plain = B.SPEC_DIR / f"{aid}.plain.png" + human = B.SPEC_DIR / f"{aid}.png" + rec = { + "sha256": sha256_file(src), "bytes": src.stat().st_size, + "ffprobe": B.ffprobe(src), "astats": B.astats(src), "ebur128": B.ebur128(src), + "samples": len(pcm), "duration_s": B.r6(len(pcm) / B.RATE), + "span_a": span_a, "span_b": span_b, "span_c": span_c, + "a_vs_c": B.compare_spans(span_a, span_c), + "a_vs_b": B.compare_spans(span_a, span_b), + "pixels": B.analyse_spectrogram(plain, series), + "plain_sha256": sha256_file(plain), "plain_bytes": plain.stat().st_size, + "human_sha256": sha256_file(human), "human_bytes": human.stat().st_size, + } + m["assets"][aid] = rec + + m["calibration"]["dc_sweep"] = B.cal_dc_sweep() + m["calibration"]["dc_step"] = B.cal_dc_step() + m["calibration"]["tone_zero_crossing"] = B.cal_tone_zero_crossing() + + src_c = B.EVID_DIR / "af_silencedetect.c" + m["evidence"] = {"path": src_c, + "exists": src_c.exists(), + "sha256": sha256_file(src_c) if src_c.exists() else None, + "text": src_c.read_text() if src_c.exists() else ""} + return m + + +# -------------------------------------------------------------------------- +# comparison helpers +# -------------------------------------------------------------------------- +def num_close(got, want, tol): + return abs(float(got) - float(want)) <= tol + 1e-12 + + +def cmp_value(got, want, tol, path="", out=None): + """Recursively compare got against want within tol. Returns list of problems.""" + if out is None: + out = [] + if isinstance(want, dict): + if not isinstance(got, dict): + out.append(f"{path}: expected dict, got {type(got).__name__}") + return out + for k, v in want.items(): + if k not in got: + out.append(f"{path}.{k}: missing from fresh measurement") + else: + cmp_value(got[k], v, tol, f"{path}.{k}", out) + return out + if isinstance(want, list): + if not isinstance(got, list) or len(got) != len(want): + out.append(f"{path}: expected list of {len(want)}, got {got!r}") + return out + for i, v in enumerate(want): + cmp_value(got[i], v, tol, f"{path}[{i}]", out) + return out + if isinstance(want, str) or isinstance(want, bool) or want is None: + if got != want: + out.append(f"{path}: expected {want!r}, got {got!r}") + return out + if not num_close(got, want, tol): + out.append(f"{path}: expected {want} +/-{tol}, got {got}") + return out + + +# -------------------------------------------------------------------------- +# per-class checks +# -------------------------------------------------------------------------- +def c_sha256(cl, m): + return cmp_value(m["assets"][cl["asset"]]["sha256"], cl["value"], 0, "sha256") + + +def c_bytes(cl, m): + return cmp_value(m["assets"][cl["asset"]]["bytes"], cl["value"], 0, "bytes") + + +def c_container_duration(cl, m): + return cmp_value(m["assets"][cl["asset"]]["ffprobe"]["container"]["duration_s"], + cl["value"], cl["tolerance"], "duration_s") + + +def c_stream_props(cl, m): + st = m["assets"][cl["asset"]]["ffprobe"]["stream"] + return cmp_value({k: st[k] for k in cl["value"]}, cl["value"], 0, "stream") + + +def c_astats(cl, m): + return cmp_value(m["assets"][cl["asset"]]["astats"], cl["value"], + cl["tolerance"], "astats") + + +def c_ebur128(cl, m): + return cmp_value(m["assets"][cl["asset"]]["ebur128"], cl["value"], + cl["tolerance"], "ebur128") + + +def c_decoded_samples(cl, m): + r = m["assets"][cl["asset"]] + return cmp_value({"samples": r["samples"], "duration_s": r["duration_s"]}, + cl["value"], cl["tolerance"], "decoded") + + +def c_container_vs_decoded_gap(cl, m): + r = m["assets"][cl["asset"]] + c_s = r["ffprobe"]["container"]["duration_s"] + got = {"container_s": c_s, "decoded_s": r["duration_s"], + "gap_s": B.r6(c_s - r["duration_s"]), + "gap_samples": int(round((c_s - r["duration_s"]) * B.RATE))} + probs = cmp_value(got, cl["value"], cl["tolerance"], "gap") + if got["gap_s"] <= 0.02: + probs.append("container/decoded gap too small to be a real priming/padding " + f"artefact ({got['gap_s']} s)") + return probs + + +def c_method_c_fidelity(cl, m): + r = m["assets"][cl["asset"]]["a_vs_c"] + probs = cmp_value({"count_a": r["count_a"], "count_b": r["count_b"], + "max_start_dev_samples": r["max_start_dev_samples"], + "max_end_dev_samples": r["max_end_dev_samples"]}, + cl["value"], cl["tolerance"], "a_vs_c") + # substantive assertion, independent of the frozen numbers + if r["count_a"] != r["count_b"]: + probs.append(f"run counts differ: ffmpeg {r['count_a']} vs reimplementation " + f"{r['count_b']}") + if r["max_start_dev_samples"] > 2 or r["max_end_dev_samples"] > 2: + probs.append(f"boundary deviation exceeds 2 samples " + f"({r['max_start_dev_samples']}/{r['max_end_dev_samples']})") + return probs + + +def c_naive_rms_divergence(cl, m): + r = m["assets"][cl["asset"]]["a_vs_b"] + probs = cmp_value({"count_a": r["count_a"], "count_b": r["count_b"], + "total_a_s": r["total_a_s"], "total_b_s": r["total_b_s"], + "total_diff_s": r["total_diff_s"]}, + cl["value"], cl["tolerance"], "a_vs_b") + # the divergence must actually exist, in the stated direction + if not (r["total_b_s"] > r["total_a_s"]): + probs.append("frame-RMS method did not over-report silence vs the reference") + if r["total_diff_s"] <= 0.1: + probs.append(f"methods did not measurably diverge (diff {r['total_diff_s']} s)") + return probs + + +def c_spectrogram_pixels(cl, m): + return cmp_value(m["assets"][cl["asset"]]["pixels"], cl["value"], + cl["tolerance"], "pixels") + + +def c_cross_modal_pearson(cl, m): + got = m["assets"][cl["asset"]]["pixels"]["pearson_col_luma_vs_frame_rms_db"] + probs = cmp_value(got, cl["value"], cl["tolerance"], "pearson") + if got < 0.5: + probs.append(f"cross-modal correlation too weak to claim ({got})") + return probs + + +def c_frequency_tilt(cl, m): + p = m["assets"][cl["asset"]]["pixels"] + probs = cmp_value({"bottom": p["bottom_band_mean_luma"], "top": p["top_band_mean_luma"]}, + cl["value"], cl["tolerance"], "tilt") + if not p["bottom_band_mean_luma"] > p["top_band_mean_luma"]: + probs.append("bottom band is not brighter than top band") + return probs + + +def c_cal_dc_sweep(cl, m): + d = m["calibration"]["dc_sweep"] + probs = cmp_value({"flip_between_dbfs": d["flip_is_between_dbfs"], + "predicted_cutoff_amp_i16": d["predicted_cutoff_amp_i16"]}, + cl["value"], cl["tolerance"], "dc_sweep") + loud = d["loudest_block_called_silent"] + quiet = d["quietest_block_called_audible"] + if loud is None or quiet is None: + probs.append("DC sweep produced no flip point") + else: + if not (loud["amp_i16"] < d["predicted_cutoff_amp_i16"] < quiet["amp_i16"]): + probs.append( + f"predicted cut-off {d['predicted_cutoff_amp_i16']} does not lie between the " + f"measured flip bounds ({loud['amp_i16']}, {quiet['amp_i16']})") + if quiet["amp_i16"] - loud["amp_i16"] > 200: + probs.append("flip interval too wide to localise the threshold") + return probs + + +def c_cal_dc_step(cl, m): + d = m["calibration"]["dc_step"] + return cmp_value({"start_error_samples": d["start_error_samples"], + "end_error_samples": d["end_error_samples"]}, + cl["value"], cl["tolerance"], "dc_step") + + +def c_cal_tone(cl, m): + d = m["calibration"]["tone_zero_crossing"][0] + probs = cmp_value({"runs_reported": d["runs_reported"], + "mean_start_spacing_us": d["mean_start_spacing_us"]}, + cl["value"], cl["tolerance"], "tone") + if abs(d["mean_start_spacing_us"] - d["expected_start_spacing_us"]) > 5.0: + probs.append(f"spacing {d['mean_start_spacing_us']} us is not 1/(2f) = " + f"{d['expected_start_spacing_us']} us") + return probs + + +CHECKS = { + "sha256": c_sha256, "bytes": c_bytes, "container_duration": c_container_duration, + "stream_props": c_stream_props, "astats": c_astats, "ebur128": c_ebur128, + "decoded_samples": c_decoded_samples, "method_c_fidelity": c_method_c_fidelity, + "container_vs_decoded_gap": c_container_vs_decoded_gap, + "naive_rms_divergence": c_naive_rms_divergence, + "spectrogram_pixels": c_spectrogram_pixels, + "cross_modal_pearson": c_cross_modal_pearson, "frequency_tilt": c_frequency_tilt, + "cal_dc_sweep": c_cal_dc_sweep, "cal_dc_step": c_cal_dc_step, "cal_tone": c_cal_tone, +} + + +# -------------------------------------------------------------------------- +def evaluate(claims_doc, m): + results = [] + for cl in claims_doc["claims"]: + fn = CHECKS.get(cl["class"]) + if fn is None: + results.append({"id": cl["id"], "class": cl["class"], "asset": cl.get("asset"), + "ok": False, "problems": [f"no checker for class {cl['class']!r}"]}) + continue + try: + probs = fn(cl, m) + except Exception as e: # a broken measurement is a failure, not a crash + probs = [f"{type(e).__name__}: {e}"] + results.append({"id": cl["id"], "class": cl["class"], "asset": cl.get("asset"), + "ok": not probs, "problems": probs}) + return results + + +def check_artifact_files(m, root=None): + """The emitted JSON/PNG files must still describe what is on disk.""" + out = [] + root = Path(root) if root else ROOT + seg_path = root / "segments.json" + man_path = root / "manifest.json" + if not seg_path.exists() or not man_path.exists(): + return [{"name": "artifact files present", "ok": False, + "detail": "segments.json / manifest.json missing"}] + seg = json.loads(seg_path.read_text()) + man = json.loads(man_path.read_text()) + + for aid, r in m["assets"].items(): + s = seg["assets"].get(aid) + if s is None: + out.append({"name": f"segments.json[{aid}]", "ok": False, "detail": "missing"}) + continue + for key, fresh in (("method_a_silence", r["span_a"]), ("method_b_silence", r["span_b"]), + ("method_c_silence", r["span_c"])): + ok = s[key] == fresh + out.append({"name": f"segments.json {aid}.{key}", "ok": ok, + "detail": "matches fresh run" if ok + else f"stored {len(s[key])} spans != fresh {len(fresh)} spans"}) + + by_id = {a["id"]: a for a in man["assets"]} + for aid, r in m["assets"].items(): + a = by_id.get(aid) + if a is None: + out.append({"name": f"manifest.json[{aid}]", "ok": False, "detail": "missing"}) + continue + for name, got, want in ( + ("sha256", a["sha256"], r["sha256"]), + ("bytes", a["bytes"], r["bytes"]), + ("spectra.machine.sha256", a["spectra"]["machine"]["sha256"], r["plain_sha256"]), + ("spectra.human.sha256", a["spectra"]["human"]["sha256"], r["human_sha256"])): + out.append({"name": f"manifest.json {aid}.{name}", "ok": got == want, + "detail": "matches" if got == want else f"{got} != {want}"}) + + ev = m["evidence"] + out.append({"name": "evidence/af_silencedetect.c present", "ok": ev["exists"], + "detail": str(ev["path"])}) + if ev["exists"]: + out.append({"name": "evidence source sha256 pinned in manifest", + "ok": ev["sha256"] == man["reference_implementation"]["ffmpeg_source_sha256"], + "detail": ev["sha256"]}) + for needle in ("*p < noise && *p > -noise", "nb_samples_notify = s->duration", + "s->noise *= INT16_MAX"): + ok = needle in ev["text"] + out.append({"name": f"evidence source contains {needle!r}", "ok": ok, + "detail": "found" if ok else "NOT FOUND"}) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--claims", default=str(ROOT / "claims.json")) + ap.add_argument("--quiet", action="store_true") + args = ap.parse_args() + + claims_doc = json.loads(Path(args.claims).read_text()) + m = measure() + results = evaluate(claims_doc, m) + files = check_artifact_files(m) + + npass = sum(1 for r in results if r["ok"]) + fpass = sum(1 for r in files if r["ok"]) + if not args.quiet: + for r in results: + tag = "PASS" if r["ok"] else "FAIL" + label = f"{r['id']} {r['class']}" + if r["asset"]: + label += f" [{r['asset']}]" + print(f" {tag} {label}") + for p in r["problems"]: + print(f" -> {p}") + print() + for r in files: + tag = "PASS" if r["ok"] else "FAIL" + print(f" {tag} artifact-file {r['name']}") + if not r["ok"]: + print(f" -> {r['detail']}") + + total = len(results) + len(files) + ok = npass + fpass + print(f"\nverify.py: {ok}/{total} checks passed " + f"({npass}/{len(results)} claims, {fpass}/{len(files)} artifact-file checks)") + return 0 if ok == total else 1 + + +if __name__ == "__main__": + sys.exit(main())