Skip to main content

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}