diff --git a/docs/operational-env.md b/docs/operational-env.md index 3b8755d..a1c1f31 100644 --- a/docs/operational-env.md +++ b/docs/operational-env.md @@ -371,6 +371,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_COVERAGE_PCT` | server MJPEG pixel-artifact guard threshold; minimum sampled-row coverage for partial-width mosaic/tile seams, default `32` | | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_DELTA` | server MJPEG pixel-artifact guard threshold; sampled luma jump for partial-width mosaic/tile seams, default `28` | | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_RUN_PCT` | server MJPEG pixel-artifact guard threshold; minimum contiguous sampled-row run for partial-width mosaic/tile seams, default `12` | +| `LESAVKA_UVC_MJPEG_PIXEL_GUARD_PARTIAL_SEAM_BLOCK_STDDEV` | server MJPEG pixel-artifact guard threshold; maximum sampled-block luma variation for partial-width mosaic/tile seams, default `14` | | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_SAMPLE_WIDTH` | server MJPEG pixel-artifact guard sampling width; defaults to `160` pixels and is clamped to `64..320` | | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_SEAM_COVERAGE_PCT` | server MJPEG pixel-artifact guard threshold; minimum sampled-row coverage for hard horizontal tile seams, default `42` | | `LESAVKA_UVC_MJPEG_PIXEL_GUARD_SEAM_DELTA` | server MJPEG pixel-artifact guard threshold; sampled luma jump that marks a row as suspicious, default `34` | diff --git a/scripts/ci/hygiene_gate_baseline.json b/scripts/ci/hygiene_gate_baseline.json index 636cf9a..38674f5 100644 --- a/scripts/ci/hygiene_gate_baseline.json +++ b/scripts/ci/hygiene_gate_baseline.json @@ -1308,7 +1308,7 @@ "server/src/video_sinks/hevc_mjpeg_guard.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 385 + "loc": 436 }, "server/src/video_sinks/hevc_mjpeg_guard/mjpeg_frame_inspection.rs": { "clippy_warnings": 0, @@ -1323,22 +1323,17 @@ "server/src/video_sinks/hevc_mjpeg_guard/mjpeg_visual_seams.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 311 - }, - "server/src/video_sinks/hevc_mjpeg_guard/tests.rs": { - "clippy_warnings": 0, - "doc_debt": 7, - "loc": 471 + "loc": 404 }, "server/src/video_sinks/mjpeg_spool.rs": { "clippy_warnings": 0, "doc_debt": 4, - "loc": 396 + "loc": 443 }, "server/src/video_sinks/mjpeg_spool/audit.rs": { "clippy_warnings": 0, - "doc_debt": 1, - "loc": 226 + "doc_debt": 0, + "loc": 287 }, "server/src/video_sinks/mjpeg_spool/tests.rs": { "clippy_warnings": 0, @@ -1358,7 +1353,7 @@ "server/src/video_sinks/webcam_sink/frame_handoff.rs": { "clippy_warnings": 0, "doc_debt": 4, - "loc": 347 + "loc": 357 }, "server/src/video_sinks/webcam_sink/tests.rs": { "clippy_warnings": 0, diff --git a/server/src/video_sinks/hevc_mjpeg_guard.rs b/server/src/video_sinks/hevc_mjpeg_guard.rs index 0599f23..fea2015 100644 --- a/server/src/video_sinks/hevc_mjpeg_guard.rs +++ b/server/src/video_sinks/hevc_mjpeg_guard.rs @@ -311,6 +311,11 @@ pub(super) fn should_freeze_decoded_mjpeg(previous_bytes: u64, next_bytes: usize next_bytes < threshold_bytes } +/// Return the size-collapse reason for a decoded HEVC->MJPEG frame. +/// +/// Inputs: last accepted decoded frame size and next frame size. Output: the +/// collapse reason when the next frame is too small. Why: callers need the +/// exact threshold for operator-visible diagnostics instead of a boolean. fn decoded_size_collapse_reason( previous_bytes: u64, next_bytes: usize, @@ -427,5 +432,5 @@ pub(super) fn direct_mjpeg_reject_reason( } #[cfg(test)] -#[path = "hevc_mjpeg_guard/tests.rs"] +#[path = "hevc_mjpeg_guard/tests/mod.rs"] mod tests; diff --git a/server/src/video_sinks/hevc_mjpeg_guard/mjpeg_visual_seams.rs b/server/src/video_sinks/hevc_mjpeg_guard/mjpeg_visual_seams.rs index fb88c6d..4526ed8 100644 --- a/server/src/video_sinks/hevc_mjpeg_guard/mjpeg_visual_seams.rs +++ b/server/src/video_sinks/hevc_mjpeg_guard/mjpeg_visual_seams.rs @@ -138,6 +138,11 @@ fn row_bounds(frame: &SampledFrame) -> std::ops::Range { start..end.min(frame.height) } +/// Measure sampled luma variation inside a bounded rectangle. +/// +/// Inputs: sampled frame and exclusive x/y bounds. Output: luma standard +/// deviation, or `None` for empty regions. Why: partial seam detection should +/// reject smooth fake tile blocks without punishing naturally textured edges. fn region_luma_stddev( frame: &SampledFrame, row_start: usize, @@ -169,6 +174,11 @@ fn region_luma_stddev( Some((sum_sq / count_f - mean * mean).max(0.0).sqrt()) } +/// Measure texture around a candidate partial-width seam. +/// +/// Inputs: sampled frame, seam row, and candidate horizontal run. Output: +/// standard deviation for the lower suspect block. Why: real scene detail can +/// have a sharp edge, but damaged UVC tiles tend to be broad low-texture slabs. fn partial_seam_block_stddev( frame: &SampledFrame, row: usize, diff --git a/server/src/video_sinks/hevc_mjpeg_guard/tests.rs b/server/src/video_sinks/hevc_mjpeg_guard/tests/mod.rs similarity index 99% rename from server/src/video_sinks/hevc_mjpeg_guard/tests.rs rename to server/src/video_sinks/hevc_mjpeg_guard/tests/mod.rs index 05a05e8..d40843e 100644 --- a/server/src/video_sinks/hevc_mjpeg_guard/tests.rs +++ b/server/src/video_sinks/hevc_mjpeg_guard/tests/mod.rs @@ -1,3 +1,5 @@ +//! Unit coverage for HEVC and direct-MJPEG corruption guard decisions. + #[test] fn hevc_jpeg_quality_defaults_to_moderate_transport_pressure() { temp_env::with_var_unset("LESAVKA_UVC_HEVC_JPEG_QUALITY", || { diff --git a/server/src/video_sinks/mjpeg_spool/audit.rs b/server/src/video_sinks/mjpeg_spool/audit.rs index 6c08c5c..5f9c103 100644 --- a/server/src/video_sinks/mjpeg_spool/audit.rs +++ b/server/src/video_sinks/mjpeg_spool/audit.rs @@ -128,6 +128,10 @@ pub(super) fn mjpeg_spool_audit_log_path(dir: &Path) -> PathBuf { .unwrap_or_else(|| dir.join("spool-audit.jsonl")) } +/// Hash an audited frame payload into a compact stable identifier. +/// +/// Inputs: frame bytes. Output: lowercase FNV-1a hex digest. Why: audit JSONL +/// should correlate frame files without embedding large binary payloads. fn fnv1a64_hex(data: &[u8]) -> String { let mut hash = FNV1A64_OFFSET; for byte in data { @@ -199,6 +203,11 @@ fn format_audit_record( ) } +/// Format one guard-rejected frame audit record. +/// +/// Inputs: sequence, saved slot, rejected bytes, timing, filename, and reason. +/// Output: one JSONL row. Why: rejected frames need the same timing/hash +/// evidence as accepted frames so guard tuning can compare both paths. fn format_rejected_audit_record( sequence: u64, slot: u64,