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:
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),
|
||||
};
|
||||
+19
@@ -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
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 371 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 396 KiB |
BIN
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())
|
||||
Reference in new issue
Block a user