feat(lab): add recorded realtime spatial playback

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 23:17:48 +03:00
parent aacc6dc43b
commit 9df9f58ab8
23 changed files with 1443 additions and 517 deletions
+101 -24
View File
@@ -7,8 +7,11 @@ import { createServer } from "vite";
let server;
let fetchM4ThreatReplayResult;
let fetchM4ThreatVisual;
let fetchM4ThreatVideoOverlay;
let selectM4ThreatVideoFrame;
let fetchM4ThreatTimeline;
let fetchM4ThreatTimelineChunk;
let selectM4ThreatTimelineFrame;
let selectM4ThreatTimelineSequence;
let advanceRecordedEvidencePlayback;
const resultId = `m4-threat-replay-${"a".repeat(64)}`;
@@ -21,9 +24,14 @@ before(async () => {
({
fetchM4ThreatReplayResult,
fetchM4ThreatVisual,
fetchM4ThreatVideoOverlay,
selectM4ThreatVideoFrame,
fetchM4ThreatTimeline,
fetchM4ThreatTimelineChunk,
selectM4ThreatTimelineFrame,
selectM4ThreatTimelineSequence,
} = await server.ssrLoadModule("/src/core/laboratory/m4ReplayThreat.ts"));
({ advanceRecordedEvidencePlayback } = await server.ssrLoadModule(
"/src/components/laboratory/useRecordedEvidencePlayback.ts",
));
});
after(async () => {
@@ -44,6 +52,29 @@ function proposal(overrides = {}) {
};
}
function timelineFrame(sequence, sessionSeconds, overrides = {}) {
return {
schema_version: "missioncore.recorded-spatial-evidence-frame/v1",
sequence,
frame_id: `frame-${String(sequence).padStart(6, "0")}`,
source_time_ns: Math.round(sessionSeconds * 1_000_000_000),
session_seconds: sessionSeconds,
source_available: true,
spatial_available: true,
point_cloud_body_xyz_m: [[1, 0, 0.1]],
point_cloud_source_count: 1,
point_cloud_sample_count: 1,
point_cloud_layer: "current-increment",
rolling_map_component_count: 0,
metric_obstacles: [],
camera_proposals: [],
decision_counts: { threat: 0, "not-threat": 0, unknown: 0 },
camera_url: `/api/v1/laboratory/m4-threat/results/${resultId}/timeline/frames/${sequence}/camera`,
authority: "replay-simulated",
...overrides,
};
}
test("M4.6 decodes accepted dual-evidence result without physical authority", async () => {
const result = await fetchM4ThreatReplayResult({
fetcher: async () => new Response(JSON.stringify({
@@ -200,38 +231,82 @@ test("M4.6 v2 keeps CURRENT INCREMENT separate from ROLLING MAP", async () => {
assert.equal(frame.metricObstacles[0].assessment.decision, "threat");
});
test("M4.6 full video preserves camera-only unknown and nearest-frame selection", async () => {
const overlay = await fetchM4ThreatVideoOverlay(resultId, {
test("M4.6 timeline keeps only a compact index and decodes bounded spatial chunks", async () => {
const frameTimesNs = Array.from(
{ length: 4489 },
(_, index) => 35_421_857_292 + index * 100_000_000,
);
const timeline = await fetchM4ThreatTimeline(resultId, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.m4-threat-video-overlay/v1",
schema_version: "missioncore.recorded-spatial-evidence-timeline/v1",
result_id: resultId,
recorded_source: { session_id: "20260720T065719Z_viewer_live" },
recorded_source: {
session_id: "20260720T065719Z_viewer_live",
source_id: "RAVNOVES00",
synchronization: "host-arrival-best-effort",
},
image_width: 800,
image_height: 600,
timeline_start_seconds: 35.421857292,
timeline_end_seconds: 484.044857292,
frame_count: 4489,
frame_times_ns: frameTimesNs,
timeline_start_seconds: 35.421857292,
timeline_end_seconds: 484.221857292,
nominal_frame_interval_seconds: 0.1,
nominal_rate_hz: 10,
max_chunk_frames: 24,
point_sample_limit: 2000,
rig: { length_m: 1, width_m: 0.6, nominal_sensor_height_m: 1.25 },
corridor: {
forward_length_m: 8,
rear_margin_m: 0.5,
half_width_m: 0.5,
prediction_horizon_seconds: 5,
},
authority: "replay-simulated",
}), { status: 200 }),
});
assert.equal(timeline.frameTimesNs.length, 4489);
assert.equal(selectM4ThreatTimelineSequence(timeline.frameTimesNs, 35.50), 1);
const chunk = await fetchM4ThreatTimelineChunk(resultId, 0, 2, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.recorded-spatial-evidence-chunk/v1",
result_id: resultId,
start_sequence: 0,
frame_count: 2,
next_sequence: 2,
frames: [
{
frame_index: 0,
session_seconds: 35.421857292,
source_available: true,
camera_proposals: [],
decision_counts: { threat: 0, "not-threat": 0, unknown: 0 },
},
{
frame_index: 1,
session_seconds: 35.521857292,
source_available: true,
timelineFrame(0, 35.421857292),
timelineFrame(1, 35.521857292, {
camera_proposals: [proposal()],
decision_counts: { threat: 0, "not-threat": 0, unknown: 1 },
},
}),
],
authority: "replay-simulated",
}), { status: 200 }),
});
assert.equal(overlay.frames[1].cameraProposals[0].rangeM, null);
assert.equal(selectM4ThreatVideoFrame(overlay.frames, 35.50).frameIndex, 1);
assert.equal(chunk.frames[1].cameraProposals[0].rangeM, null);
assert.equal(selectM4ThreatTimelineFrame(chunk.frames, 35.50).sequence, 1);
});
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
const range = { startSeconds: 10, endSeconds: 20 };
assert.deepEqual(
advanceRecordedEvidencePlayback(
{ currentSeconds: 12, playing: true, rate: 2 },
1.5,
range,
),
{ currentSeconds: 15, playing: true, rate: 2 },
);
assert.deepEqual(
advanceRecordedEvidencePlayback(
{ currentSeconds: 19.5, playing: true, rate: 1 },
1,
range,
),
{ currentSeconds: 20, playing: false, rate: 1 },
);
});
test("M4.6 viewer reuses shared camera, video and metric evidence renderers", async () => {
@@ -244,6 +319,8 @@ test("M4.6 viewer reuses shared camera, video and metric evidence renderers", as
assert.match(visual, /<RecordedEvidenceVideoScene/);
assert.match(visual, /<RecordedEvidenceImageScene/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.match(visual, /<ObservationTimeline/);
assert.match(visual, /useM4ThreatTimelineFrame/);
assert.match(visual, /label: "VIDEO"/);
assert.match(visual, /label: "CAMERA"/);
assert.match(visual, /label: "3D"/);