sphinx: phonon-reel audio archive — time-addressable index + measured proof ffmpeg silencedetect is a per-sample test (pure-Python reimplementation reproduces it to 2 samples; naive frame-RMS model over-reports silence 21-40%)

This commit is contained in:
sphinx committed 2026-10-21 07:30:18 +00:00
1 parent 903925a4d6
commit 52a8deb5b7
24 files changed
+5066

No files matched your search

@@ -0,0 +1,2 @@
__pycache__/
*.pyc
@@ -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.
@@ -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("<h", v) for v in samples_i16)
with open(path, "wb") as fh:
fh.write(b"RIFF")
fh.write(struct.pack("<I", 36 + len(buf)))
fh.write(b"WAVEfmt ")
fh.write(struct.pack("<IHHIIHH", 16, 1, 1, rate, rate * 2, 2, 16))
fh.write(b"data")
fh.write(struct.pack("<I", len(buf)))
fh.write(buf)
def cal_dc_sweep():
"""Locate the effective threshold with DC blocks (no zero crossings)."""
SCRATCH.mkdir(parents=True, exist_ok=True)
levels = [-60, -50, -45, -40, -38, -36, -35, -34, -32, -30, -25, -20]
block, gap = int(RATE * 0.5), int(RATE * 0.5)
samples, marks, t = [], [], 0.0
for lv in levels:
a = max(-32768, min(32767, int(round((10 ** (lv / 20.0)) * 32768))))
samples.extend([a] * block)
marks.append({"level_dbfs": lv, "amp_i16": a, "start_s": r6(t),
"end_s": r6(t + block / RATE)})
t += block / RATE
samples.extend([0] * gap)
t += gap / RATE
path = SCRATCH / "dc_sweep.wav"
write_wav(path, samples)
spans = silencedetect(path, noise_db=SILENCE_DB, min_dur=0.2)
for m in marks:
m["classified_silent"] = any(sp[0] < m["end_s"] - 0.05 and sp[1] > 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/<file>")
claim("bytes", aid, f"source file is {rec['bytes']} bytes",
rec["bytes"], 0, "os.stat on sources/<file>")
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()
@@ -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/<file>"
},
{
"id": "C05",
"class": "bytes",
"asset": "phonon-male-reel",
"statement": "source file is 868563 bytes",
"value": 868563,
"tolerance": 0,
"method": "os.stat on sources/<file>"
},
{
"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/<file>"
},
{
"id": "C18",
"class": "bytes",
"asset": "phonon-british-reel",
"statement": "source file is 821751 bytes",
"value": 821751,
"tolerance": 0,
"method": "os.stat on sources/<file>"
},
{
"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/<file>"
},
{
"id": "C31",
"class": "bytes",
"asset": "phonon-alfred-reel",
"statement": "source file is 557183 bytes",
"value": 557183,
"tolerance": 0,
"method": "os.stat on sources/<file>"
},
{
"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"
}
]
}
@@ -0,0 +1,273 @@
/*
* Copyright (c) 2012 Clément Bœsch <u pkh me>
*
* 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 <float.h> /* 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),
};
@@ -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);
@@ -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())
@@ -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/##"
@@ -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
]
}
}
}
]
}
@@ -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
@@ -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()
@@ -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("<h", v) for v in samples_i16)
with open(path, "wb") as fh:
fh.write(b"RIFF")
fh.write(struct.pack("<I", 36 + len(buf)))
fh.write(b"WAVEfmt ")
fh.write(struct.pack("<IHHIIHH", 16, 1, 1, RATE, RATE * 2, 2, 16))
fh.write(b"data")
fh.write(struct.pack("<I", len(buf)))
fh.write(buf)
def run_silencedetect(path, noise="-35.0dB", d=0.2, prefilter=None):
af = f"silencedetect=noise={noise}:d={d}"
if prefilter:
af = prefilter + "," + af
p = subprocess.run(["ffmpeg", "-hide_banner", "-nostats", "-v", "info",
"-i", str(path), "-af", af, "-f", "null", "-"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
err = p.stderr.decode("utf-8", "replace")
spans, cur = [], None
for line in err.splitlines():
m = re.search(r"silence_start:\s*(-?[\d.eE+-]+)", line)
if m:
cur = float(m.group(1))
continue
m = re.search(r"silence_end:\s*(-?[\d.eE+-]+)", line)
if m and cur is not None:
spans.append((cur, float(m.group(1))))
cur = None
return spans
def db_to_amp(db):
return 10 ** (db / 20.0)
def amp_to_db(a):
return 20 * math.log10(a) if a > 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()
@@ -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"]))
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

@@ -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())