rto_graph/media/gate.rs
1//! The **pre-generation gate**: a cheap, deterministic refusal of media blobs
2//! that obviously contain nothing to read, evaluated *before* a model is loaded
3//! (ADR-0015).
4//!
5//! Two measurements, one per modality:
6//!
7//! - **Audio** — root-mean-square amplitude over the whole clip, normalised so
8//! that full scale is `1.0`. Digital silence measures exactly `0.0`.
9//! - **Images** — the variance of the luma plane, likewise normalised (a pixel is
10//! `0.0`…`1.0`). A flat-colour image measures exactly `0.0`.
11//!
12//! Three properties make it worth having, and each is asserted by a test rather
13//! than assumed:
14//!
15//! 1. **It runs before the model loads.** [`super::build_media`] evaluates the
16//! gate and, on a refusal, never calls [`MediaProducer::generate`](super::MediaProducer::generate)
17//! at all — and the llama.cpp engines are built lazily *inside* `generate`
18//! (see [`super::producers`]), so a repository of silent or blank assets
19//! performs no 715 MB projector load whatsoever (issue #301).
20//! 2. **The refusal is recorded, not silent.** A gated blob still gets a
21//! [`MediaRecord`](super::MediaRecord) — one carrying a [`MediaSkip`] instead
22//! of text — so `media status` can print *"skipped: below silence threshold
23//! (rms=0.0)"* rather than leaving an indistinguishable hole. An operator can
24//! tell **not generated** from **generated nothing**; an invisible skip would
25//! be its own small lie, which would be an odd thing to add to an ADR about
26//! not lying.
27//! 3. **It is deterministic** — a pure function of the bytes and the thresholds —
28//! so it costs nothing in reproducibility.
29//!
30//! # What it does not do
31//!
32//! The gate raises the floor. It does not fix the label, and it does not stop
33//! confabulation:
34//!
35//! - **Quiet speech, room tone, tape hiss and hum all pass**, by design: the
36//! defaults sit near the digital noise floor, far below any real recording.
37//! A model handed room tone will still return confident invented prose.
38//! - **Subtly-textured images pass** — a faint gradient, a watermark, a scan of a
39//! blank page. A VLM will still describe them.
40//! - **Only WAV is measurable.** MP3 and FLAC need a decoder this workspace does
41//! not depend on, so the gate **abstains** for them (see [`audio_stats`]) and
42//! they go to the model as before. Abstention is a pass, never a skip.
43//! - **Images are measurable only in a build with an image codec** — that is,
44//! with `image-ocr` or `image-vision` — which is exactly the build that can
45//! generate a description at all.
46//!
47//! Only the artifact store fixes the label. This is adopted because it is cheap
48//! and helps, not because it addresses the defect in issue #300.
49//!
50//! @rto:0015
51
52use serde::{Deserialize, Serialize};
53
54use super::MediaKind;
55
56/// Why the gate refused a blob.
57///
58/// One variant per modality, because one measurement per modality is what the
59/// gate makes. The token is stored in `media_content.skip_reason` and is part of
60/// a `CHECK` constraint, so adding a variant is a schema change, not a rename.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
62#[serde(rename_all = "lowercase")]
63pub enum GateReason {
64 /// An audio clip whose RMS amplitude was at or below the silence threshold.
65 Silence,
66 /// An image whose luma variance was at or below the uniformity threshold.
67 Uniform,
68}
69
70impl GateReason {
71 /// Stable token used in the `SQLite` store and in `--json` output.
72 #[must_use]
73 pub fn as_str(self) -> &'static str {
74 match self {
75 Self::Silence => "silence",
76 Self::Uniform => "uniform",
77 }
78 }
79
80 /// Parse a reason from its stable token; `None` for an unrecognised value (a
81 /// corrupt row).
82 #[must_use]
83 pub fn from_token(s: &str) -> Option<Self> {
84 match s {
85 "silence" => Some(Self::Silence),
86 "uniform" => Some(Self::Uniform),
87 _ => None,
88 }
89 }
90
91 /// The name of the quantity that was measured, as printed beside the value:
92 /// `rms` for silence, `variance` for uniformity.
93 #[must_use]
94 pub fn metric(self) -> &'static str {
95 match self {
96 Self::Silence => "rms",
97 Self::Uniform => "variance",
98 }
99 }
100
101 /// The threshold's name in prose, for the one-line explanation.
102 #[must_use]
103 pub fn threshold_name(self) -> &'static str {
104 match self {
105 Self::Silence => "silence",
106 Self::Uniform => "uniformity",
107 }
108 }
109}
110
111impl std::fmt::Display for GateReason {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.write_str(self.as_str())
114 }
115}
116
117/// A recorded refusal: why, what was measured, and what it was measured against.
118///
119/// The **measured value** is the load-bearing field. "Skipped" alone would tell
120/// an operator nothing about whether the gate was right; `rms=0.0` against a
121/// threshold of `0.0001` says the clip was digitally silent, and `rms=0.00009`
122/// says it was very nearly so and the threshold is worth a look.
123#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
124pub struct MediaSkip {
125 /// Which measurement refused the blob.
126 pub reason: GateReason,
127 /// The value measured on this blob, in the metric's own units.
128 pub value: f64,
129 /// The threshold it was compared against, recorded so a later change of
130 /// defaults cannot silently reinterpret an old skip.
131 pub threshold: f64,
132}
133
134impl std::fmt::Display for MediaSkip {
135 /// The one-line explanation `media status` and the explorer print, e.g.
136 /// `below silence threshold (rms=0, threshold 0.0001)`.
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(
139 f,
140 "below {} threshold ({}={}, threshold {})",
141 self.reason.threshold_name(),
142 self.reason.metric(),
143 self.value,
144 self.threshold,
145 )
146 }
147}
148
149/// The gate's two tunable thresholds.
150///
151/// **Conservative by default.** A false skip is a silently missing description,
152/// which is worse than a false pass now that generated output is clearly
153/// labelled — so both defaults sit at the digital noise floor, where "there is
154/// nothing here" is not a judgement call. Raising them trades that safety for
155/// fewer pointless model loads; that is an operator's decision, taken in
156/// `roteiro.toml` under `[media]`.
157#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
158pub struct GateThresholds {
159 /// RMS amplitude (full scale `1.0`) at or below which a clip is silent.
160 pub silence_rms: f64,
161 /// Luma variance (a pixel is `0.0`…`1.0`) at or below which an image is
162 /// uniform.
163 pub image_variance: f64,
164}
165
166/// RMS at or below which a clip counts as silent: `1e-4`, about **-80 dBFS**.
167///
168/// A 16-bit sample is `1/32768` ≈ `3e-5` of full scale, so this admits a couple
169/// of least-significant bits of dither and nothing else. Ordinary quiet speech
170/// sits around -40 dBFS (`1e-2`) and even a hissy room tone around -60 dBFS
171/// (`1e-3`) — both two to three orders of magnitude clear of the gate.
172pub const DEFAULT_SILENCE_RMS: f64 = 1e-4;
173
174/// Luma variance at or below which an image counts as uniform: `1e-5`, a standard
175/// deviation of about **0.8 levels out of 255**.
176///
177/// A flat colour is exactly `0.0`; a flat colour that has been through JPEG
178/// picks up a little ringing and stays well under. Anything with visible
179/// structure — a faint gradient, a watermark, a line of text — is orders of
180/// magnitude above, and passes.
181pub const DEFAULT_IMAGE_VARIANCE: f64 = 1e-5;
182
183impl Default for GateThresholds {
184 fn default() -> Self {
185 Self {
186 silence_rms: DEFAULT_SILENCE_RMS,
187 image_variance: DEFAULT_IMAGE_VARIANCE,
188 }
189 }
190}
191
192impl GateThresholds {
193 /// Thresholds that refuse nothing — the gate turned off, as
194 /// `[media] gate = false` produces.
195 ///
196 /// Expressed as *negative* thresholds rather than as a separate `enabled`
197 /// flag: no measurement can be below zero, so "off" is the same code path as
198 /// "on", and there is no second way for a blob to reach the model.
199 #[must_use]
200 pub fn disabled() -> Self {
201 Self {
202 silence_rms: -1.0,
203 image_variance: -1.0,
204 }
205 }
206}
207
208/// Evaluate the gate for one blob: `Some(skip)` to refuse it, `None` to let it
209/// through.
210///
211/// `None` covers both "there is something here" and "this build cannot measure
212/// this input" — abstention is always a pass, never a skip, because a false skip
213/// is the expensive mistake.
214#[must_use]
215pub fn evaluate(kind: MediaKind, bytes: &[u8], thresholds: GateThresholds) -> Option<MediaSkip> {
216 match kind {
217 MediaKind::Audio => {
218 let stats = audio_stats(bytes)?;
219 (stats.rms <= thresholds.silence_rms).then_some(MediaSkip {
220 reason: GateReason::Silence,
221 value: stats.rms,
222 threshold: thresholds.silence_rms,
223 })
224 }
225 MediaKind::Vision => {
226 let stats = image_stats(bytes)?;
227 (stats.variance <= thresholds.image_variance).then_some(MediaSkip {
228 reason: GateReason::Uniform,
229 value: stats.variance,
230 threshold: thresholds.image_variance,
231 })
232 }
233 }
234}
235
236/// What the gate measured on an audio clip. Amplitudes are normalised so that
237/// full scale is `1.0`, whatever the source sample format.
238#[derive(Debug, Clone, Copy, PartialEq)]
239pub struct AudioStats {
240 /// Largest absolute sample amplitude.
241 pub peak: f64,
242 /// Root-mean-square amplitude over the whole clip — the value the gate
243 /// compares, and the one a skip records.
244 pub rms: f64,
245}
246
247/// What the gate measured on an image, over its luma plane with each pixel
248/// normalised to `0.0`…`1.0`.
249#[derive(Debug, Clone, Copy, PartialEq)]
250pub struct ImageStats {
251 /// Mean luma. Recorded for legibility; the gate does not compare it — a
252 /// black image and a white image are equally empty.
253 pub mean: f64,
254 /// Variance of the luma plane — the value the gate compares.
255 pub variance: f64,
256}
257
258/// Measure an audio blob, or `None` when this build cannot read it.
259///
260/// Only **WAV** is measurable: it is a container over raw PCM, so peak and RMS
261/// fall out of a header parse and a pass over the samples, with no decoder and
262/// therefore no new dependency. MP3 and FLAC are entropy-coded; measuring them
263/// means decoding them, and the audio decoder in this process lives inside
264/// llama.cpp's bundled miniaudio — behind the very model load the gate exists to
265/// avoid. So the gate abstains for those formats and they reach the model
266/// unchanged, which is the conservative failure.
267#[must_use]
268pub fn audio_stats(bytes: &[u8]) -> Option<AudioStats> {
269 let pcm = wav_pcm(bytes)?;
270 Some(pcm_stats(&pcm))
271}
272
273/// Peak and RMS over already-normalised samples. Separated from the container
274/// parse so the statistics are exercised in every build, on samples a test can
275/// write by hand.
276///
277/// An empty clip measures `0.0` for both, so a zero-length `data` chunk is
278/// silence — which it is.
279#[must_use]
280pub fn pcm_stats(samples: &[f64]) -> AudioStats {
281 let mut peak = 0.0_f64;
282 let mut sum_squares = 0.0_f64;
283 for s in samples {
284 peak = peak.max(s.abs());
285 sum_squares += s * s;
286 }
287 #[expect(
288 clippy::cast_precision_loss,
289 reason = "sample counts are far below 2^53; the mean only needs to be accurate \
290 to many more digits than a threshold comparison uses"
291 )]
292 let rms = if samples.is_empty() {
293 0.0
294 } else {
295 (sum_squares / samples.len() as f64).sqrt()
296 };
297 AudioStats { peak, rms }
298}
299
300/// Mean and variance over an already-decoded 8-bit luma plane, normalised to
301/// `0.0`…`1.0`. Separated from the codec for the same reason as [`pcm_stats`]:
302/// the statistics are then testable in a build with no image codec at all.
303///
304/// An empty plane measures `0.0` for both.
305#[must_use]
306pub fn luma_stats(luma: &[u8]) -> ImageStats {
307 if luma.is_empty() {
308 return ImageStats {
309 mean: 0.0,
310 variance: 0.0,
311 };
312 }
313 #[expect(
314 clippy::cast_precision_loss,
315 reason = "pixel counts are far below 2^53 (the pixel cap is 40 megapixels)"
316 )]
317 let n = luma.len() as f64;
318 let mut sum = 0.0_f64;
319 let mut sum_squares = 0.0_f64;
320 for &b in luma {
321 let v = f64::from(b) / 255.0;
322 sum += v;
323 sum_squares += v * v;
324 }
325 let mean = sum / n;
326 // The population variance, computed as E[x²] - E[x]². Clamped at zero
327 // because floating-point cancellation can push a genuinely flat plane a
328 // hair below it, and a negative variance would be nonsense to print.
329 ImageStats {
330 mean,
331 variance: (sum_squares / n - mean * mean).max(0.0),
332 }
333}
334
335/// Decode a WAV blob's samples to `[-1.0, 1.0]`, or `None` when it is not a WAV
336/// this parser understands.
337///
338/// Handles the sample formats a recorder actually emits: unsigned 8-bit, signed
339/// 16/24/32-bit PCM, and 32/64-bit IEEE float, in both plain (`0x0001`/`0x0003`)
340/// and `WAVE_FORMAT_EXTENSIBLE` (`0xFFFE`) flavours. Channels are not separated —
341/// the gate asks "is there anything here at all", which is a question about the
342/// whole clip.
343fn wav_pcm(bytes: &[u8]) -> Option<Vec<f64>> {
344 /// Uncompressed integer PCM.
345 const FORMAT_PCM: u16 = 0x0001;
346 /// IEEE 754 float samples.
347 const FORMAT_FLOAT: u16 = 0x0003;
348 /// `WAVE_FORMAT_EXTENSIBLE`; the real format is the first two bytes of the
349 /// sub-format GUID in the extension, which we read below.
350 const FORMAT_EXTENSIBLE: u16 = 0xFFFE;
351 /// Largest number of samples the gate will measure. A clip is capped at
352 /// [`MAX_AUDIO_BYTES`](super::MAX_AUDIO_BYTES) already, but that is a
353 /// *compressed* cap and says nothing about a hand-written header, so bound
354 /// the allocation independently.
355 const MAX_SAMPLES: usize = 64 * 1024 * 1024;
356
357 let u16_at = |at: usize| -> Option<u16> {
358 Some(u16::from_le_bytes(bytes.get(at..at + 2)?.try_into().ok()?))
359 };
360 let u32_at = |at: usize| -> Option<u32> {
361 Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
362 };
363
364 if bytes.get(..4)? != b"RIFF" || bytes.get(8..12)? != b"WAVE" {
365 return None;
366 }
367 // Walk the chunk list rather than assuming `fmt ` then `data`: real files
368 // interleave `LIST`, `fact` and padding chunks, and a parser that assumed
369 // the layout would abstain on perfectly ordinary recordings.
370 let (mut format, mut bits) = (None, None);
371 let mut cursor = 12;
372 while cursor + 8 <= bytes.len() {
373 let id = bytes.get(cursor..cursor + 4)?;
374 let size = u32_at(cursor + 4)? as usize;
375 let body = cursor + 8;
376 match id {
377 b"fmt " if size >= 16 => {
378 let tag = u16_at(body)?;
379 let declared_bits = u16_at(body + 14)?;
380 // `EXTENSIBLE` defers the real format to the sub-format GUID,
381 // whose first two bytes are the plain tag it stands in for.
382 let tag = if tag == FORMAT_EXTENSIBLE && size >= 26 {
383 u16_at(body + 24)?
384 } else {
385 tag
386 };
387 format = Some(tag);
388 bits = Some(declared_bits);
389 }
390 b"data" => {
391 let (format, bits) = (format?, bits?);
392 let data = bytes.get(body..body.saturating_add(size))?;
393 return decode_samples(data, format, bits, FORMAT_PCM, FORMAT_FLOAT, MAX_SAMPLES);
394 }
395 _ => {}
396 }
397 // Chunk bodies are word-aligned: an odd size is followed by a pad byte.
398 cursor = body.checked_add(size)?.checked_add(size & 1)?;
399 }
400 None
401}
402
403/// Turn a `data` chunk into normalised samples for one of the formats
404/// [`wav_pcm`] accepts, or `None` for anything else.
405fn decode_samples(
406 data: &[u8],
407 format: u16,
408 bits: u16,
409 format_pcm: u16,
410 format_float: u16,
411 max_samples: usize,
412) -> Option<Vec<f64>> {
413 let width = usize::from(bits).div_ceil(8);
414 if width == 0 || data.len() / width > max_samples {
415 return None;
416 }
417 // A `data` chunk that is not a whole number of samples is a corrupt or
418 // truncated file. **Abstain rather than measure the aligned prefix**: five
419 // of the six decoder arms below use `as_chunks`, which yields only the
420 // complete chunks and silently drops the remainder, so measuring anyway
421 // would report an RMS for part of a clip and could refuse a blob on the
422 // strength of it. That is a false skip — a silently missing description —
423 // and avoiding it is what the whole gate is calibrated around. Handing an
424 // unreadable clip to the model costs one model load; refusing a readable
425 // one costs the operator something they cannot see.
426 //
427 // The sixth arm, 8-bit PCM, iterates bytes rather than chunking. It needs
428 // no protection here because it cannot receive any: at 8 bits `width == 1`,
429 // so the check below reads `!data.len().is_multiple_of(1)` and is always
430 // false. The guard is *vacuous* at that width, not quietly missing a case
431 // — every byte is a whole sample, so an 8-bit `data` chunk of any length is
432 // aligned by construction and there is nothing for it to catch.
433 if !data.len().is_multiple_of(width) {
434 return None;
435 }
436 let mut out = Vec::with_capacity(data.len() / width);
437 match (format, bits) {
438 // Unsigned, mid-scale at 128 — the one PCM width that is not signed.
439 (f, 8) if f == format_pcm => {
440 out.extend(data.iter().map(|&b| (f64::from(b) - 128.0) / 128.0));
441 }
442 (f, 16) if f == format_pcm => {
443 out.extend(
444 data.as_chunks::<2>()
445 .0
446 .iter()
447 .map(|c| f64::from(i16::from_le_bytes([c[0], c[1]])) / f64::from(1_i32 << 15)),
448 );
449 }
450 // 24-bit is stored packed, three bytes little-endian, so sign-extend it
451 // into an i32 by placing it in the top three bytes and shifting back.
452 (f, 24) if f == format_pcm => {
453 out.extend(data.as_chunks::<3>().0.iter().map(|c| {
454 let v = i32::from_le_bytes([0, c[0], c[1], c[2]]) >> 8;
455 f64::from(v) / f64::from(1_i32 << 23)
456 }));
457 }
458 (f, 32) if f == format_pcm => {
459 out.extend(data.as_chunks::<4>().0.iter().map(|c| {
460 f64::from(i32::from_le_bytes([c[0], c[1], c[2], c[3]])) / 2_147_483_648.0
461 }));
462 }
463 (f, 32) if f == format_float => {
464 out.extend(
465 data.as_chunks::<4>()
466 .0
467 .iter()
468 .map(|c| f64::from(f32::from_le_bytes([c[0], c[1], c[2], c[3]]))),
469 );
470 }
471 (f, 64) if f == format_float => {
472 out.extend(
473 data.as_chunks::<8>()
474 .0
475 .iter()
476 .map(|c| f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]])),
477 );
478 }
479 _ => return None,
480 }
481 // A non-finite float sample is a corrupt file, not a loud one; refusing to
482 // measure it abstains rather than reporting a NaN RMS that compares false
483 // against every threshold in a way nobody could debug.
484 out.iter().all(|s| s.is_finite()).then_some(out)
485}
486
487/// Measure an image blob, or `None` when this build has no image codec.
488///
489/// Gated on the same features that make a description possible in the first
490/// place, so the abstention is never reachable from a build that could have
491/// generated something.
492#[cfg(any(feature = "image-ocr", feature = "image-vision"))]
493#[must_use]
494pub fn image_stats(bytes: &[u8]) -> Option<ImageStats> {
495 // The decompression-bomb guard the OCR and vision paths already apply: read
496 // the dimensions from the header and refuse an over-large image before any
497 // pixel is decoded. Abstaining here is right — an image too large to measure
498 // is not an image the gate should claim is empty.
499 if !crate::extract::image_dimensions_ok(bytes) {
500 return None;
501 }
502 let luma = image::load_from_memory(bytes).ok()?.into_luma8();
503 Some(luma_stats(luma.as_raw()))
504}
505
506/// Without an image codec there is nothing to decode with, so the gate abstains
507/// and every image passes. Such a build cannot generate a description either, so
508/// nothing is lost.
509#[cfg(not(any(feature = "image-ocr", feature = "image-vision")))]
510#[must_use]
511pub fn image_stats(_bytes: &[u8]) -> Option<ImageStats> {
512 None
513}
514
515#[cfg(test)]
516mod tests {
517 use super::{
518 AudioStats, DEFAULT_IMAGE_VARIANCE, DEFAULT_SILENCE_RMS, GateReason, GateThresholds,
519 ImageStats, MediaKind, MediaSkip, audio_stats, evaluate, luma_stats, pcm_stats,
520 };
521
522 /// A minimal 16-bit mono WAV around `samples`.
523 fn wav16(samples: &[i16]) -> Vec<u8> {
524 let data: Vec<u8> = samples.iter().flat_map(|s| s.to_le_bytes()).collect();
525 let mut out = Vec::new();
526 out.extend_from_slice(b"RIFF");
527 out.extend_from_slice(&u32::try_from(36 + data.len()).expect("fits").to_le_bytes());
528 out.extend_from_slice(b"WAVEfmt ");
529 out.extend_from_slice(&16_u32.to_le_bytes()); // chunk size
530 out.extend_from_slice(&1_u16.to_le_bytes()); // PCM
531 out.extend_from_slice(&1_u16.to_le_bytes()); // mono
532 out.extend_from_slice(&8000_u32.to_le_bytes()); // sample rate
533 out.extend_from_slice(&16_000_u32.to_le_bytes()); // byte rate
534 out.extend_from_slice(&2_u16.to_le_bytes()); // block align
535 out.extend_from_slice(&16_u16.to_le_bytes()); // bits
536 out.extend_from_slice(b"data");
537 out.extend_from_slice(&u32::try_from(data.len()).expect("fits").to_le_bytes());
538 out.extend_from_slice(&data);
539 out
540 }
541
542 /// A WAV declaring `bits` per sample around a `data` chunk of exactly
543 /// `data` — which the caller may deliberately leave unaligned to the sample
544 /// width, unlike [`wav16`], whose `data` is always whole samples.
545 fn wav_with_data(bits: u16, data: &[u8]) -> Vec<u8> {
546 let mut out = Vec::new();
547 out.extend_from_slice(b"RIFF");
548 out.extend_from_slice(&u32::try_from(36 + data.len()).expect("fits").to_le_bytes());
549 out.extend_from_slice(b"WAVEfmt ");
550 out.extend_from_slice(&16_u32.to_le_bytes()); // chunk size
551 out.extend_from_slice(&1_u16.to_le_bytes()); // PCM
552 out.extend_from_slice(&1_u16.to_le_bytes()); // mono
553 out.extend_from_slice(&8000_u32.to_le_bytes()); // sample rate
554 out.extend_from_slice(&16_000_u32.to_le_bytes()); // byte rate
555 out.extend_from_slice(&2_u16.to_le_bytes()); // block align
556 out.extend_from_slice(&bits.to_le_bytes());
557 out.extend_from_slice(b"data");
558 out.extend_from_slice(&u32::try_from(data.len()).expect("fits").to_le_bytes());
559 out.extend_from_slice(data);
560 out
561 }
562
563 /// **A `data` chunk that is not a whole number of samples must abstain, not
564 /// be measured on its aligned prefix.**
565 ///
566 /// The multi-byte decoders use `as_chunks`, which yields only the complete
567 /// chunks and silently drops the remainder — so without the length check a
568 /// truncated file would be measured on whatever happened to align. Every
569 /// case here is all-zero bytes, which is the dangerous shape: the aligned
570 /// prefix measures `rms = 0`, so the gate would **refuse** a clip it could
571 /// not actually read. A false skip is a silently missing description, and
572 /// avoiding it is the rule the whole gate is calibrated around.
573 ///
574 /// 8-bit PCM is absent from the cases below by construction, not by
575 /// omission: there `width == 1`, so no `data` chunk is ever unaligned and
576 /// the check cannot fire. There is no case to write.
577 #[test]
578 fn an_unaligned_data_chunk_abstains_rather_than_being_measured() {
579 // `(bits, data length)`, each length short of a whole sample. Only the
580 // integer-PCM widths this builder declares (`format = 1`), so every case
581 // reaches a real decoder arm and is refused for its length rather than
582 // for an unsupported format.
583 for (bits, len) in [(16_u16, 65_usize), (24, 64), (32, 66)] {
584 let wav = wav_with_data(bits, &vec![0_u8; len]);
585 assert!(
586 audio_stats(&wav).is_none(),
587 "{bits}-bit with a {len}-byte data chunk must not be measured",
588 );
589 assert!(
590 evaluate(MediaKind::Audio, &wav, GateThresholds::default()).is_none(),
591 "{bits}-bit with a {len}-byte data chunk must PASS — measuring the \
592 aligned prefix of a corrupt clip would be a false skip",
593 );
594 }
595
596 // The same builder with an aligned chunk still measures, so the guard is
597 // rejecting misalignment and not simply everything it is handed.
598 assert_eq!(
599 audio_stats(&wav_with_data(16, &[0; 64])).expect("64 bytes is 32 whole samples"),
600 AudioStats {
601 peak: 0.0,
602 rms: 0.0
603 },
604 );
605 }
606
607 #[test]
608 fn digital_silence_measures_exactly_zero() {
609 let stats = audio_stats(&wav16(&[0; 800])).expect("a WAV is measurable");
610 assert_eq!(
611 stats,
612 AudioStats {
613 peak: 0.0,
614 rms: 0.0
615 }
616 );
617 }
618
619 #[test]
620 fn a_tone_is_far_above_the_silence_threshold() {
621 // A half-scale square wave: RMS is 0.5, four thousand times the default.
622 let samples: Vec<i16> = (0..800)
623 .map(|i| if i % 2 == 0 { 16_384 } else { -16_384 })
624 .collect();
625 let stats = audio_stats(&wav16(&samples)).expect("measurable");
626 assert!(
627 stats.rms > DEFAULT_SILENCE_RMS * 1000.0,
628 "a tone must clear the gate by orders of magnitude: {stats:?}"
629 );
630 assert!(
631 evaluate(
632 MediaKind::Audio,
633 &wav16(&samples),
634 GateThresholds::default()
635 )
636 .is_none()
637 );
638 }
639
640 /// The headline: silence is refused, and the refusal carries its measurement.
641 #[test]
642 fn silence_is_refused_with_its_measured_value() {
643 let skip = evaluate(
644 MediaKind::Audio,
645 &wav16(&[0; 800]),
646 GateThresholds::default(),
647 )
648 .expect("digital silence must be refused");
649 assert_eq!(
650 skip,
651 MediaSkip {
652 reason: GateReason::Silence,
653 value: 0.0,
654 threshold: DEFAULT_SILENCE_RMS,
655 }
656 );
657 assert_eq!(
658 skip.to_string(),
659 "below silence threshold (rms=0, threshold 0.0001)"
660 );
661 }
662
663 /// The defaults sit at the digital noise floor, not near real audio. This is
664 /// the "conservative" claim, made checkable: a signal three least-significant
665 /// bits wide is still refused, and one at -60 dBFS — a hissy room tone, far
666 /// quieter than speech — is not.
667 #[test]
668 fn the_default_threshold_admits_dither_and_nothing_louder() {
669 let dither: Vec<i16> = (0..800).map(|i| if i % 2 == 0 { 2 } else { -2 }).collect();
670 assert!(
671 evaluate(MediaKind::Audio, &wav16(&dither), GateThresholds::default()).is_some(),
672 "a couple of least-significant bits is still silence"
673 );
674
675 // -60 dBFS ≈ 0.001 of full scale ≈ 32 counts at 16-bit.
676 let room_tone: Vec<i16> = (0..800)
677 .map(|i| if i % 2 == 0 { 33 } else { -33 })
678 .collect();
679 assert!(
680 evaluate(
681 MediaKind::Audio,
682 &wav16(&room_tone),
683 GateThresholds::default()
684 )
685 .is_none(),
686 "room tone must pass — the gate does not claim to stop confabulation"
687 );
688 }
689
690 /// Abstention is a pass. A format the gate cannot decode, a truncated file
691 /// and a blob that is not audio at all must all reach the model unchanged,
692 /// because a false skip is the expensive mistake.
693 #[test]
694 fn an_unmeasurable_blob_passes_rather_than_being_refused() {
695 for bytes in [
696 b"ID3\x04\x00\x00\x00\x00\x00\x00".as_slice(), // an MP3 tag header
697 b"fLaC\x00\x00\x00\x22".as_slice(), // a FLAC stream marker
698 b"RIFF".as_slice(), // truncated before WAVE
699 b"".as_slice(),
700 &wav16(&[0; 4])[..20], // a WAV truncated inside its header
701 ] {
702 assert!(audio_stats(bytes).is_none(), "must not measure {bytes:?}");
703 assert!(
704 evaluate(MediaKind::Audio, bytes, GateThresholds::default()).is_none(),
705 "abstention must pass, not skip: {bytes:?}"
706 );
707 }
708 }
709
710 /// Every sample format a recorder emits must be measurable, or the gate
711 /// abstains on ordinary files and quietly does nothing.
712 #[test]
713 fn the_common_wav_sample_formats_are_all_measured_as_silent() {
714 /// `(format tag, bits, one silent sample's bytes)`.
715 const CASES: [(u16, u16, &[u8]); 6] = [
716 (1, 8, &[128]), // unsigned 8-bit: mid-scale is 128
717 (1, 16, &[0, 0]), // signed 16-bit
718 (1, 24, &[0, 0, 0]), // packed signed 24-bit
719 (1, 32, &[0, 0, 0, 0]), // signed 32-bit
720 (3, 32, &[0, 0, 0, 0]), // f32
721 (3, 64, &[0, 0, 0, 0, 0, 0, 0, 0]), // f64
722 ];
723 for (format, bits, sample) in CASES {
724 let data: Vec<u8> = sample.repeat(64);
725 let mut out = Vec::new();
726 out.extend_from_slice(b"RIFF");
727 out.extend_from_slice(&u32::try_from(36 + data.len()).expect("fits").to_le_bytes());
728 out.extend_from_slice(b"WAVEfmt ");
729 out.extend_from_slice(&16_u32.to_le_bytes());
730 out.extend_from_slice(&format.to_le_bytes());
731 out.extend_from_slice(&1_u16.to_le_bytes());
732 out.extend_from_slice(&8000_u32.to_le_bytes());
733 out.extend_from_slice(&16_000_u32.to_le_bytes());
734 out.extend_from_slice(&2_u16.to_le_bytes());
735 out.extend_from_slice(&bits.to_le_bytes());
736 out.extend_from_slice(b"data");
737 out.extend_from_slice(&u32::try_from(data.len()).expect("fits").to_le_bytes());
738 out.extend_from_slice(&data);
739 let stats = audio_stats(&out).unwrap_or_else(|| panic!("{format}/{bits} must decode"));
740 assert_eq!(
741 stats,
742 AudioStats {
743 peak: 0.0,
744 rms: 0.0
745 },
746 "{format}/{bits} silence must measure zero",
747 );
748 }
749 }
750
751 /// A `fmt ` chunk followed by anything other than `data` — real files carry
752 /// `LIST`/`fact` chunks, and an odd-sized chunk is padded to a word boundary.
753 #[test]
754 fn intervening_chunks_and_their_padding_are_walked_over() {
755 let data: Vec<u8> = vec![0; 128];
756 let mut out = Vec::new();
757 out.extend_from_slice(b"RIFFxxxxWAVEfmt ");
758 out.extend_from_slice(&16_u32.to_le_bytes());
759 out.extend_from_slice(&1_u16.to_le_bytes());
760 out.extend_from_slice(&1_u16.to_le_bytes());
761 out.extend_from_slice(&8000_u32.to_le_bytes());
762 out.extend_from_slice(&16_000_u32.to_le_bytes());
763 out.extend_from_slice(&2_u16.to_le_bytes());
764 out.extend_from_slice(&16_u16.to_le_bytes());
765 // An odd-sized `LIST` chunk, plus its pad byte.
766 out.extend_from_slice(b"LIST");
767 out.extend_from_slice(&3_u32.to_le_bytes());
768 out.extend_from_slice(b"abc\0");
769 out.extend_from_slice(b"data");
770 out.extend_from_slice(&u32::try_from(data.len()).expect("fits").to_le_bytes());
771 out.extend_from_slice(&data);
772 assert_eq!(
773 audio_stats(&out).expect("measurable"),
774 AudioStats {
775 peak: 0.0,
776 rms: 0.0
777 }
778 );
779 }
780
781 #[test]
782 #[expect(
783 clippy::float_cmp,
784 reason = "a flat plane's variance is exactly zero — E[x^2] and E[x]^2 are the \
785 same sum, so the subtraction cancels bit for bit; a tolerance here \
786 would weaken the claim being made"
787 )]
788 fn a_flat_luma_plane_has_no_variance_whatever_its_colour() {
789 for level in [0_u8, 128, 255] {
790 let stats = luma_stats(&[level; 4096]);
791 assert_eq!(stats.variance, 0.0, "level {level} must be uniform");
792 // The mean is a running sum over 4096 terms, so it lands within a
793 // few ULPs of the exact level rather than on it.
794 assert!(
795 (stats.mean - f64::from(level) / 255.0).abs() < 1e-12,
796 "level {level} mean was {}",
797 stats.mean,
798 );
799 }
800 // The gate does not compare the mean: black and white are equally empty.
801 assert!(luma_stats(&[0; 16]).variance <= DEFAULT_IMAGE_VARIANCE);
802 assert!(luma_stats(&[255; 16]).variance <= DEFAULT_IMAGE_VARIANCE);
803 }
804
805 #[test]
806 fn a_textured_luma_plane_clears_the_uniformity_threshold() {
807 // A checkerboard of adjacent grey levels — about as subtle as structure
808 // gets — is still three orders of magnitude above the threshold.
809 let plane: Vec<u8> = (0..4096)
810 .map(|i| if i % 2 == 0 { 120 } else { 136 })
811 .collect();
812 let variance = luma_stats(&plane).variance;
813 assert!(
814 variance > DEFAULT_IMAGE_VARIANCE * 50.0,
815 "a checkerboard of adjacent greys must clear the threshold: got {variance}",
816 );
817 }
818
819 #[test]
820 fn statistics_of_nothing_are_zero_rather_than_nan() {
821 assert_eq!(
822 pcm_stats(&[]),
823 AudioStats {
824 peak: 0.0,
825 rms: 0.0
826 }
827 );
828 assert_eq!(
829 luma_stats(&[]),
830 ImageStats {
831 mean: 0.0,
832 variance: 0.0
833 }
834 );
835 }
836
837 /// The disabled thresholds are negative, and no measurement can be negative,
838 /// so nothing is ever refused. This is what `[media] gate = false` becomes.
839 #[test]
840 fn disabled_thresholds_refuse_nothing() {
841 assert!(
842 evaluate(
843 MediaKind::Audio,
844 &wav16(&[0; 800]),
845 GateThresholds::disabled()
846 )
847 .is_none()
848 );
849 }
850
851 #[test]
852 fn gate_reason_tokens_round_trip() {
853 for reason in [GateReason::Silence, GateReason::Uniform] {
854 assert_eq!(GateReason::from_token(reason.as_str()), Some(reason));
855 }
856 assert_eq!(GateReason::from_token("blank"), None);
857 }
858}