feat(lab): add recorded realtime spatial playback
This commit is contained in:
@@ -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"/);
|
||||
|
||||
Reference in New Issue
Block a user