release: add receiver artifact replay mode

This commit is contained in:
Brad Stein 2026-08-13 05:22:12 -03:00
parent ed9faecb20
commit 8f1371de97
7 changed files with 125 additions and 12 deletions

6
Cargo.lock generated
View File

@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.27.14"
version = "0.27.15"
dependencies = [
"anyhow",
"async-stream",
@ -1692,7 +1692,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.27.14"
version = "0.27.15"
dependencies = [
"anyhow",
"base64",
@ -1704,7 +1704,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.27.14"
version = "0.27.15"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.27.14"
version = "0.27.15"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.27.14"
version = "0.27.15"
edition = "2024"
build = "build.rs"

View File

@ -350,6 +350,12 @@ requires at least 90% duration coverage, at least 99% marker coverage, zero
marked cadence or visual corruption, a bounded successful deep decode, intact
native MJPEG, and clean mode-matched server-boundary evidence.
Release 0.27.15 adds first-class replay of extracted receiver recordings to the
real-video artifact probe. The same tear, grey-collapse, and exact-freeze
detector used for live Tethys captures can now analyze preserved files with an
explicit mode and duration, making historical bad/control A/B evidence
repeatable instead of dependent on one-off analysis commands.
## 7. The Resolved Downstream Video Failure
The current blank downstream feeds fail before transport or decoding.
@ -426,7 +432,7 @@ The safe completion sequence for this incident is:
6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
already-attached USB gadget unless a controlled rebuild is explicitly
required.
7. Confirm Theia reports server version `0.27.14`, the pushed release revision,
7. Confirm Theia reports server version `0.27.15`, the pushed release revision,
direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
8. Open both downstream RPCs and prove that each emits changing, decodable H.264
frames.
@ -444,7 +450,7 @@ hardware contract is repeatable. The remaining work falls into five groups.
### A. Install And Version Parity
- Push and deploy `0.27.14` through the client/server install scripts.
- Push and deploy `0.27.15` through the client/server install scripts.
- Confirm client/server version and revision in every hardware probe artifact.
- Eliminate the current state where a fixed client talks to an unfixed server.
@ -510,6 +516,6 @@ host repair:
9. disconnect/reconnect and device changes recover without stale media; and
10. diagnostics identify the failed physical stage when any item breaks.
Until that sequence passes on the installed `0.27.14` client/server pair, the
Until that sequence passes on the installed `0.27.15` client/server pair, the
current release should be described as a validated code correction awaiting
hardware deployment and end-to-end acceptance, not as a completed product fix.

View File

@ -30,8 +30,13 @@ def parse_args() -> argparse.Namespace:
)
)
parser.add_argument("--host", default="", help="optional SSH host, e.g. tethys")
parser.add_argument("--source", choices=["device", "x11"], default="device")
parser.add_argument("--source", choices=["device", "x11", "file"], default="device")
parser.add_argument("--device", default="auto", help="video device or auto")
parser.add_argument(
"--input-video",
default="",
help="receiver recording to analyze with --source file",
)
parser.add_argument("--display", default=":0", help="X11 display for --source x11")
parser.add_argument("--crop", default="", help="X11 crop as x,y,width,height for --source x11")
parser.add_argument("--device-label", default=DEFAULT_DEVICE_LABEL)
@ -118,6 +123,8 @@ def run_remote(args: argparse.Namespace) -> int:
args.source,
"--device",
args.device,
"--input-video",
args.input_video,
"--device-label",
args.device_label,
"--display",
@ -496,6 +503,23 @@ def parse_crop(value: str, args: argparse.Namespace) -> tuple[int, int, int, int
def ffmpeg_cmd(device: str, args: argparse.Namespace) -> list[str]:
if args.source == "file":
return [
"ffmpeg",
"-hide_banner",
"-nostdin",
"-loglevel",
"warning",
"-i",
args.input_video,
"-an",
"-pix_fmt",
"gray",
"-f",
"rawvideo",
"-",
]
if args.source == "x11":
x, y, width, height = parse_crop(args.crop, args)
display = f"{args.display}+{x},{y}"
@ -549,6 +573,15 @@ def ffmpeg_cmd(device: str, args: argparse.Namespace) -> list[str]:
def run_capture(args: argparse.Namespace) -> int:
artifact_dir = pathlib.Path(args.artifact_dir) if args.artifact_dir else default_artifact_dir()
artifact_dir.mkdir(parents=True, exist_ok=True)
if args.source == "file":
if not args.input_video:
raise SystemExit("--input-video is required with --source file")
if args.deep_capture:
raise SystemExit("--deep-capture is only available with --source device")
if not args.host and not pathlib.Path(args.input_video).is_file():
raise SystemExit(f"input video does not exist: {args.input_video}")
device = args.input_video
else:
device = detect_video_device(args.device_label) if args.device == "auto" else args.device
command = ffmpeg_cmd(device, args)
frame_size = args.width * args.height
@ -787,7 +820,10 @@ def run_capture(args: argparse.Namespace) -> int:
finally:
raw_path.unlink(missing_ok=True)
analysis_elapsed = time.monotonic() - analysis_started
elapsed = max(0.001, capture_elapsed)
elapsed = max(
0.001,
frame_index / max(1, args.fps) if args.source == "file" else capture_elapsed,
)
expected_frames = max(1, int(args.duration * args.fps))
coverage_ok = frame_index >= int(expected_frames * 0.9)
structural_ok = integrity_report is None or integrity_report.get("verdict") == "pass"
@ -802,6 +838,7 @@ def run_capture(args: argparse.Namespace) -> int:
"schema": "lesavka.rct-uvc-artifact-probe.v1",
"source": args.source,
"device": device,
"input_video": args.input_video or None,
"capture_mode": "deep-mjpeg" if args.deep_capture and args.source == "device" else ("stream" if args.stream_analyze else "rawfile"),
"width": args.width,
"height": args.height,

View File

@ -16,7 +16,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.27.14"
version = "0.27.15"
edition = "2024"
autobins = false

View File

@ -44,6 +44,8 @@ fn rct_uvc_artifact_probe_documents_late_path_lower_half_detection() {
"static_pct",
"reference_",
"--source",
"--input-video",
"choices=[\"device\", \"x11\", \"file\"]",
"--crop",
"PGM",
"--host",
@ -62,6 +64,74 @@ fn rct_uvc_artifact_probe_documents_late_path_lower_half_detection() {
}
}
#[test]
fn rct_uvc_artifact_probe_replays_extracted_receiver_video() {
let dir = tempfile::tempdir().expect("tempdir");
let video = dir.path().join("receiver-control.mkv");
let generate = Command::new("ffmpeg")
.args([
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"testsrc2=size=64x48:rate=5:duration=1",
"-an",
"-c:v",
"ffv1",
"-y",
])
.arg(&video)
.output()
.expect("generate receiver control recording");
assert!(
generate.status.success(),
"ffmpeg should generate the receiver control: {}",
String::from_utf8_lossy(&generate.stderr)
);
let artifacts = dir.path().join("replay");
let output = Command::new("python3")
.arg(repo_script_path())
.args(["--source", "file", "--input-video"])
.arg(&video)
.args([
"--width",
"64",
"--height",
"48",
"--fps",
"5",
"--duration",
"1",
"--max-exact-repeat-seconds",
"10",
"--artifact-dir",
])
.arg(&artifacts)
.output()
.expect("replay receiver recording");
assert_ne!(
output.status.code(),
Some(2),
"valid extracted video must not fail capture: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let summary: Value = serde_json::from_str(
&fs::read_to_string(artifacts.join("summary.json")).expect("summary json"),
)
.expect("parse summary json");
assert_eq!(summary["source"], "file");
assert_eq!(summary["input_video"], video.to_string_lossy().as_ref());
assert_eq!(summary["frames"], 5);
assert_eq!(summary["expected_frames"], 5);
assert_eq!(summary["coverage_ok"], true);
assert_eq!(summary["ffmpeg_rc"], 0);
}
#[test]
fn rct_uvc_artifact_probe_terminates_when_malformed_video_never_decodes() {
for expected in [