From e10b96b546fd11e627efa2c6bef4fffb2d2c2851 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 28 Aug 2026 15:37:10 +0300 Subject: [PATCH] feat(perception): split vegetation evidence layers --- .../src/core/laboratory/advancedIndex.ts | 17 +- .../laboratory/advancedLaboratoryResults.ts | 1 + .../src/core/laboratory/advancedResults.ts | 1 + .../src/core/laboratory/vegetationShadow.ts | 68 ++++- .../src/styles/m4-replay-threat.css | 15 ++ .../laboratory/AdvancedLaboratoryResult.tsx | 4 + .../laboratory/M49TgsFullShadowEvidence.tsx | 26 +- .../laboratory/M4ReplayThreatVisual.tsx | 96 +++++-- .../laboratory/VegetationBenchmarkResult.tsx | 147 +++++++++++ .../laboratory/VegetationShadowResult.tsx | 242 +++++++----------- .../laboratory/laboratoryArchiveProfiles.ts | 14 +- .../useAdvancedLaboratoryCatalog.ts | 2 + .../test/m49TgsFullShadow.test.mjs | 7 +- .../test/vegetationShadow.test.mjs | 63 ++++- .../lab-v1-vegetation-benchmark.json | 10 + config/laboratory-execution.json | 1 + config/laboratory-value-review.json | 11 +- .../vegetation_benchmark_archive.py | 139 ++++++++++ .../laboratory/vegetation_policy_review.py | 23 +- .../laboratory/vegetation_policy_video.py | 17 ++ .../laboratory/vegetation_shadow_lab.py | 29 ++- src/k1link/web/app.py | 16 +- src/k1link/web/vegetation_shadow_lab_api.py | 92 +++++-- tests/test_laboratory_evidence_registry.py | 3 +- .../test_laboratory_value_review_registry.py | 3 +- tests/test_vegetation_shadow_lab.py | 54 +++- 26 files changed, 868 insertions(+), 233 deletions(-) create mode 100644 apps/control-station/src/workspaces/laboratory/VegetationBenchmarkResult.tsx create mode 100644 config/laboratories/lab-v1-vegetation-benchmark.json create mode 100644 src/k1link/laboratory/vegetation_benchmark_archive.py diff --git a/apps/control-station/src/core/laboratory/advancedIndex.ts b/apps/control-station/src/core/laboratory/advancedIndex.ts index 48a601a..f2a19b7 100644 --- a/apps/control-station/src/core/laboratory/advancedIndex.ts +++ b/apps/control-station/src/core/laboratory/advancedIndex.ts @@ -48,9 +48,13 @@ import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector"; import { fetchM48TRiskQualityResult } from "./m48tRiskQuality"; import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed"; import { fetchM49TgsFullShadowResult } from "./m49TgsFullShadow"; -import { fetchVegetationShadowResult } from "./vegetationShadow"; +import { + fetchVegetationBenchmarkResult, + fetchVegetationShadowResult, +} from "./vegetationShadow"; export type AdvancedLaboratoryWorkId = + | "lab-v1-vegetation-benchmark" | "lab-v1-vegetation-shadow" | "m48-object-centric-quality" | "m48-small-static-passage-regression" @@ -102,6 +106,7 @@ export interface AdvancedLaboratoryIndexItem { } const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [ + "lab-v1-vegetation-benchmark", "lab-v1-vegetation-shadow", "m48-object-centric-quality", "m48-small-static-passage-regression", @@ -148,6 +153,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [ ]; const RESULT_PREFIX: Readonly> = { + "lab-v1-vegetation-benchmark": "lab-v1-vegetation-benchmark", "lab-v1-vegetation-shadow": "lab-v1-vegetation-shadow", "m48-object-centric-quality": "m48-object-quality-(?:pack|result)", "m48-small-static-passage-regression": "m48-small-static-passage-regression", @@ -201,6 +207,7 @@ export function isAdvancedLaboratoryWorkId( export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults { return { + vegetationBenchmark: null, vegetationShadow: null, m47Graph: null, m48: null, @@ -335,7 +342,8 @@ export function advancedLaboratoryResultAvailable( workId: AdvancedLaboratoryWorkId, results: AdvancedLaboratoryResults, ): boolean { - return workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null + return workId === "lab-v1-vegetation-benchmark" ? results.vegetationBenchmark !== null + : workId === "lab-v1-vegetation-shadow" ? results.vegetationShadow !== null : workId === "m48-object-centric-quality" ? results.m48 !== null : workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null : workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null @@ -393,7 +401,10 @@ export async function fetchAdvancedLaboratoryResult( } = {}, ): Promise { const results = emptyAdvancedLaboratoryResults(); - if (workId === "lab-v1-vegetation-shadow") { + if (workId === "lab-v1-vegetation-benchmark") { + if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation benchmark identity не выбрана."); + results.vegetationBenchmark = await fetchVegetationBenchmarkResult(resultId, { fetcher, signal }); + } else if (workId === "lab-v1-vegetation-shadow") { if (!resultId) throw new AdvancedLaboratoryContractError("Vegetation LAB identity не выбрана."); results.vegetationShadow = await fetchVegetationShadowResult(resultId, { fetcher, signal }); } else if (workId === "m48-object-centric-quality") { diff --git a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts index ca9ee36..07e5aae 100644 --- a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts +++ b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts @@ -45,6 +45,7 @@ import type { M49TgsFullShadowResult } from "./m49TgsFullShadow"; import type { VegetationShadowResult } from "./vegetationShadow"; export interface AdvancedLaboratoryResults { + vegetationBenchmark: VegetationShadowResult | null; vegetationShadow: VegetationShadowResult | null; m47Graph: M47ReferenceGraphLabResult | null; m48: M48AdvancedResult | null; diff --git a/apps/control-station/src/core/laboratory/advancedResults.ts b/apps/control-station/src/core/laboratory/advancedResults.ts index 7c9eb54..12be3e0 100644 --- a/apps/control-station/src/core/laboratory/advancedResults.ts +++ b/apps/control-station/src/core/laboratory/advancedResults.ts @@ -967,6 +967,7 @@ export async function fetchAdvancedLaboratoryResults({ const e39 = settledCatalogValue(settled[7]); const e40 = settledCatalogValue(settled[8]); return { + vegetationBenchmark: null, vegetationShadow: null, m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null, m48r3StaticOccupancy: null, diff --git a/apps/control-station/src/core/laboratory/vegetationShadow.ts b/apps/control-station/src/core/laboratory/vegetationShadow.ts index c6d4c6e..d3c962b 100644 --- a/apps/control-station/src/core/laboratory/vegetationShadow.ts +++ b/apps/control-station/src/core/laboratory/vegetationShadow.ts @@ -1,6 +1,7 @@ import type { LaboratoryFetch } from "./advancedResults"; const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/; +const BENCHMARK_RESULT_ID = /^lab-v1-vegetation-benchmark-[a-f0-9]{64}$/; const SHA256 = /^[a-f0-9]{64}$/; const CANDIDATES = ["ddrnet", "ppliteseg"] as const; const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const; @@ -75,6 +76,7 @@ export interface VegetationRouteVideo { aggregatePredictionPixels: readonly number[]; policyPresets: Readonly>>> | null; fusionMode: "synchronised-multilayer-review" | null; + validFovMaskSha256: string | null; } export interface VegetationShadowResult { @@ -192,6 +194,7 @@ function visualCaseValue( value: unknown, resultId: string, expectedKind: "goose" | "ravnoves", + endpointRoot: string, ): VegetationVisualCase { const row = objectValue(value, `vegetation.${expectedKind}.case`); exact(row.source_kind, expectedKind, "vegetation.case.source_kind"); @@ -210,7 +213,7 @@ function visualCaseValue( if (!SHA256.test(sha256) || !path.startsWith(`visual/${expectedKind}/${caseId}/`)) { throw new VegetationShadowContractError(`vegetation.case.assets.${key}: proof invalid.`); } - projected[key] = `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/assets/${path + projected[key] = `${endpointRoot}/${encodeURIComponent(resultId)}/assets/${path .split("/") .map(encodeURIComponent) .join("/")}`; @@ -342,10 +345,16 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { evidenceState, }; }); - const expectedClassCount = viewKind === "coarse-material-policy-review" ? 9 : 64; + const expectedClassCount = viewKind === "coarse-material-policy-review" ? 10 : 64; if (classes.length !== expectedClassCount) { throw new VegetationShadowContractError("vegetation.route_video: taxonomy size changed."); } + if ( + viewKind === "coarse-material-policy-review" + && (classes[9]?.disposition !== "undefined" || classes[9]?.evidenceState !== "UNOBSERVED") + ) { + throw new VegetationShadowContractError("vegetation.route_video: valid-FOV class changed."); + } const aggregatePredictionPixels = arrayValue( row.aggregate_prediction_pixels, "vegetation.route_video.aggregate_prediction_pixels", @@ -364,7 +373,18 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { integerValue(maskArchive.byte_length, "vegetation.route_video.mask_archive.byte_length"); let policyPresets: VegetationRouteVideo["policyPresets"] = null; let fusionMode: VegetationRouteVideo["fusionMode"] = null; + let validFovMaskSha256: string | null = null; if (viewKind === "coarse-material-policy-review") { + const validFov = objectValue(row.valid_fov, "vegetation.route_video.valid_fov"); + exact(validFov.mask_path, "video/valid-fov-mask.png", "vegetation.route_video.valid_fov.path"); + validFovMaskSha256 = textValue( + validFov.mask_sha256, + "vegetation.route_video.valid_fov.sha256", + ); + if (!SHA256.test(validFovMaskSha256)) { + throw new VegetationShadowContractError("vegetation.route_video: valid-FOV digest invalid."); + } + exact(validFov.outside_valid_fov_class_id, 9, "vegetation.route_video.valid_fov.class_id"); const policy = objectValue(row.policy, "vegetation.route_video.policy"); const presets = objectValue(policy.presets, "vegetation.route_video.policy.presets"); policyPresets = Object.fromEntries(Object.entries(presets).map(([presetId, rawRules]) => { @@ -399,10 +419,15 @@ function routeVideoValue(value: unknown): VegetationRouteVideo | null { aggregatePredictionPixels, policyPresets, fusionMode, + validFovMaskSha256, }; } -function parseResult(value: unknown, resultId: string): VegetationShadowResult { +function parseResult( + value: unknown, + resultId: string, + endpointRoot: string, +): VegetationShadowResult { const payload = objectValue(value, "Vegetation LAB"); exact(payload.schema_version, "missioncore.lab-v1-vegetation-shadow/v1", "vegetation.schema"); exact(payload.result_id, resultId, "vegetation.result_id"); @@ -438,9 +463,9 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult { "vegetation.authority.camera_semantics_can_clear_rigid_geometry", ); const routeCases = arrayValue(catalogs.ravnoves, "vegetation.catalogs.ravnoves") - .map((item) => visualCaseValue(item, resultId, "ravnoves")); + .map((item) => visualCaseValue(item, resultId, "ravnoves", endpointRoot)); const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose") - .map((item) => visualCaseValue(item, resultId, "goose")); + .map((item) => visualCaseValue(item, resultId, "goose", endpointRoot)); if (routeCases.length !== 0 || validationCases.length !== 12) { throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer."); } @@ -490,5 +515,36 @@ export async function fetchVegetationShadowResult( if (!response.ok) { throw new VegetationShadowContractError(`Vegetation LAB недоступна: HTTP ${response.status}.`); } - return parseResult(await response.json(), resultId); + return parseResult( + await response.json(), + resultId, + "/api/v1/laboratory/vegetation-shadow", + ); +} + +export async function fetchVegetationBenchmarkResult( + resultId: string, + { + fetcher = fetch, + signal, + }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {}, +): Promise { + if (!BENCHMARK_RESULT_ID.test(resultId)) { + throw new VegetationShadowContractError("Vegetation benchmark identity недопустима."); + } + const endpointRoot = "/api/v1/laboratory/vegetation-benchmark"; + const response = await fetcher( + `${endpointRoot}/${encodeURIComponent(resultId)}`, + { method: "GET", headers: { Accept: "application/json" }, signal }, + ); + if (!response.ok) { + throw new VegetationShadowContractError( + `Vegetation benchmark недоступен: HTTP ${response.status}.`, + ); + } + const result = parseResult(await response.json(), resultId, endpointRoot); + if (result.routeVideo) { + throw new VegetationShadowContractError("Vegetation benchmark содержит route video."); + } + return result; } diff --git a/apps/control-station/src/styles/m4-replay-threat.css b/apps/control-station/src/styles/m4-replay-threat.css index 5a1c808..5e6e4fd 100644 --- a/apps/control-station/src/styles/m4-replay-threat.css +++ b/apps/control-station/src/styles/m4-replay-threat.css @@ -81,6 +81,21 @@ justify-content: flex-end; } +.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] { + flex-wrap: wrap; +} + +.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] + > .m4-replay-threat-visual__pane-layer-controls { + flex: 1 0 100%; + justify-content: flex-start; +} + +.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] + > .m4-replay-threat-visual__pane-mode-controls { + margin-left: auto; +} + .m4-replay-threat-evidence-viewer[data-mode-controls="content"]:has( .m4-replay-threat-visual__review-controls ) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] { diff --git a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx index 58c1b6f..9abc052 100644 --- a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx @@ -51,6 +51,7 @@ import { M48TRiskQualityResultView } from "./M48TRiskQualityResult"; import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult"; import { M49TgsFullShadowResultView } from "./M49TgsFullShadowResult"; import { VegetationShadowResultView } from "./VegetationShadowResult"; +import { VegetationBenchmarkResultView } from "./VegetationBenchmarkResult"; export { isAdvancedLaboratoryWorkId }; export type { AdvancedLaboratoryWorkId }; @@ -93,6 +94,9 @@ export function AdvancedLaboratoryResult({ failedSessionId: string | null; replayError: string | null; }) { + if (workId === "lab-v1-vegetation-benchmark" && results.vegetationBenchmark) { + return ; + } if (workId === "lab-v1-vegetation-shadow" && results.vegetationShadow) { return ; } diff --git a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx index 5b17dc0..4cd1525 100644 --- a/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx +++ b/apps/control-station/src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx @@ -71,7 +71,6 @@ export function M49TgsFullShadowEvidence({ const controller = new AbortController(); setSemantic(null); setSemanticError(null); - if (semanticOverride) return () => controller.abort(); void fetchE47SemanticSlamResult({ resultId: result.source.linkedSemanticResultId, signal: controller.signal, @@ -87,7 +86,7 @@ export function M49TgsFullShadowEvidence({ if (!controller.signal.aborted) setSemanticError(message(caught)); }); return () => controller.abort(); - }, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId, semanticOverride]); + }, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]); useEffect(() => { const controller = new AbortController(); @@ -201,15 +200,28 @@ export function M49TgsFullShadowEvidence({ const handleSequenceChange = useCallback((sequence: number | null) => { setActiveSequence(sequence); }, []); + const semanticLayers = useMemo(() => [ + ...(semantic ? [{ + id: "urban", + controlLabel: "ГОРОД · EoMT", + resultId: semantic.resultId, + taxonomy: semantic.taxonomy, + label: "EoMT Cityscapes semantic · recorded video", + maskAriaLabel: "EoMT urban semantic prediction", + }] : []), + ...(semanticOverride ? [{ + ...semanticOverride, + id: semanticOverride.id ?? "vegetation", + controlLabel: semanticOverride.controlLabel ?? "ПРИРОДА · DDRNet", + }] : []), + ], [semantic, semanticOverride]); return ( <> - {!semanticOverride && semanticError ? ( + {semanticError ? (
Semantic overlay недоступен: {semanticError}
diff --git a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx index 33eb510..63ca0f4 100644 --- a/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx +++ b/apps/control-station/src/workspaces/laboratory/M4ReplayThreatVisual.tsx @@ -96,6 +96,8 @@ function SpatialState({ message: text }: { message: string }) { } export interface M4ReplayThreatSemanticLayer { + id?: string; + controlLabel?: string; resultId: string; spatialResultId?: string | null; maskUrl?: (sequence: number) => string; @@ -155,6 +157,8 @@ const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = []; export function M4ReplayThreatVisual({ resultId, semantic, + semanticLayers, + initialSemanticLayerId, reviewAnchors = EMPTY_REVIEW_ANCHORS, showReviewAnchorBoxes = true, reviewLabel = "Контрольные примеры M4.8R1", @@ -168,6 +172,8 @@ export function M4ReplayThreatVisual({ }: { resultId: string; semantic?: M4ReplayThreatSemanticLayer; + semanticLayers?: readonly M4ReplayThreatSemanticLayer[]; + initialSemanticLayerId?: string; reviewAnchors?: readonly M4ReplayThreatReviewAnchor[]; showReviewAnchorBoxes?: boolean; reviewLabel?: string; @@ -199,6 +205,35 @@ export function M4ReplayThreatVisual({ )); const [expanded, setExpanded] = useState(false); const [selectedReviewAnchorIndex, setSelectedReviewAnchorIndex] = useState(0); + const availableSemanticLayers = useMemo( + () => semanticLayers?.length ? semanticLayers : semantic ? [semantic] : [], + [semantic, semanticLayers], + ); + const semanticLayerIdentity = availableSemanticLayers + .map((layer, index) => layer.id ?? `${layer.resultId}:${index}`) + .join("|"); + const [selectedSemanticLayerId, setSelectedSemanticLayerId] = useState( + initialSemanticLayerId ?? "", + ); + useEffect(() => { + if (!availableSemanticLayers.length) { + setSelectedSemanticLayerId(""); + return; + } + const selectedStillExists = availableSemanticLayers.some( + (layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId, + ); + if (selectedStillExists) return; + const preferred = initialSemanticLayerId + ? availableSemanticLayers.find((layer) => layer.id === initialSemanticLayerId) + : null; + const next = preferred ?? availableSemanticLayers[0]!; + const nextIndex = availableSemanticLayers.indexOf(next); + setSelectedSemanticLayerId(next.id ?? `${next.resultId}:${nextIndex}`); + }, [availableSemanticLayers, initialSemanticLayerId, semanticLayerIdentity, selectedSemanticLayerId]); + const activeSemantic = availableSemanticLayers.find( + (layer, index) => (layer.id ?? `${layer.resultId}:${index}`) === selectedSemanticLayerId, + ) ?? availableSemanticLayers[0]; const metricSceneRef = useRef(null); const metadata = useM4ThreatTimelineMetadata(resultId, timelineEndpointRoot); const playbackRange = useMemo(() => metadata.timeline ? ({ @@ -298,19 +333,21 @@ export function M4ReplayThreatVisual({ sequence: frame?.sequence ?? null, endpointRoot: timelineEndpointRoot, }); - const semanticSpatialResultId = semantic - ? semantic.spatialResultId === undefined ? semantic.resultId : semantic.spatialResultId + const semanticSpatialResultId = activeSemantic + ? activeSemantic.spatialResultId === undefined + ? activeSemantic.resultId + : activeSemantic.spatialResultId : null; const spatialSemanticTaxonomy = useMemo( - () => semanticSpatialResultId && semantic - ? semantic.taxonomy.map((item) => ({ + () => semanticSpatialResultId && activeSemantic + ? activeSemantic.taxonomy.map((item) => ({ classId: item.classId, label: item.label, disposition: item.disposition === "ambiguous" ? "ambiguous" : "labeled", colorRgb: item.colorRgb, })) : [], - [semantic, semanticSpatialResultId], + [activeSemantic, semanticSpatialResultId], ); const semanticTimeline = useE47SemanticTimelineFrame({ resultId: semanticSpatialResultId, @@ -386,14 +423,14 @@ export function M4ReplayThreatVisual({ [frame, reviewAnchorBoxes, showReferenceMediaLayers, staticObstacleBoxes], ); const semanticClasses = useMemo( - () => semantic?.taxonomy.map((item) => ({ + () => activeSemantic?.taxonomy.map((item) => ({ id: item.classId, label: `semantic: ${item.label}`, })) ?? [], - [semantic?.taxonomy], + [activeSemantic?.taxonomy], ); const semanticPalette = useMemo( - () => semantic?.taxonomy.map((item) => ({ + () => activeSemantic?.taxonomy.map((item) => ({ classId: item.classId, color: item.disposition === "undefined" ? { kind: "transparent" as const } @@ -404,7 +441,7 @@ export function M4ReplayThreatVisual({ ? 0 : item.disposition === "ambiguous" ? 0.52 : 0.92, })) ?? [], - [semantic?.taxonomy], + [activeSemantic?.taxonomy], ); const semanticFrame = semanticTimeline.activeFrame?.sequence === frame?.sequence ? semanticTimeline.activeFrame @@ -422,7 +459,7 @@ export function M4ReplayThreatVisual({ && lastSpatialSemanticFrameRef.current.frame.sequence === spatialFrame?.sequence ? lastSpatialSemanticFrameRef.current.frame : null; - const semanticIntegrityError = semantic && spatialFrame && spatialSemanticFrame && ( + const semanticIntegrityError = activeSemantic && spatialFrame && spatialSemanticFrame && ( spatialSemanticFrame.sourcePointCount !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudSampleCount !== spatialFrame.pointCloudSourceCount || spatialFrame.pointCloudBodyXyzM.length !== spatialFrame.pointCloudSourceCount @@ -431,7 +468,7 @@ export function M4ReplayThreatVisual({ : null; const alignedSemanticPointIds = useMemo(() => { if ( - !semantic + !activeSemantic || !showSpatialSemantic || !spatialFrame || !spatialSemanticFrame @@ -441,7 +478,7 @@ export function M4ReplayThreatVisual({ const status = spatialSemanticFrame.statusCodes[index]; return status === 2 || status === 3 ? classId : null; }); - }, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]); + }, [activeSemantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]); const activeSpatialFrame = spatialFrame?.sequence === timelineFrame.activeSequence ? spatialFrame : null; @@ -610,19 +647,19 @@ export function M4ReplayThreatVisual({ }, ), [metadata.timeline, spatialFrame, timelineFrame.availableFrames]); const semanticOverlay: RecordedEvidenceSemanticOverlay | undefined = - semantic && showMediaSemantic && frame + activeSemantic && showMediaSemantic && frame ? { - src: semantic.maskUrl?.(frame.sequence) - ?? e47SemanticMaskUrl(semantic.resultId, frame.sequence), + src: activeSemantic.maskUrl?.(frame.sequence) + ?? e47SemanticMaskUrl(activeSemantic.resultId, frame.sequence), prefetchSrcs: Array.from({ length: 12 }, (_, index) => index + 1) .map((offset) => frame.sequence + offset) .filter((sequence) => sequence < (metadata.timeline?.frameCount ?? 0)) - .map((sequence) => semantic.maskUrl?.(sequence) - ?? e47SemanticMaskUrl(semantic.resultId, sequence)), + .map((sequence) => activeSemantic.maskUrl?.(sequence) + ?? e47SemanticMaskUrl(activeSemantic.resultId, sequence)), classes: semanticClasses, palette: semanticPalette, opacity: 0.9, - ariaLabel: `${semantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`, + ariaLabel: `${activeSemantic.maskAriaLabel ?? "Semantic prediction"} frame ${frame.sequence + 1}`, } : undefined; const accumulatedCameraPoints = cameraPointOverlay.overlay?.sequence === frame?.sequence @@ -692,7 +729,7 @@ export function M4ReplayThreatVisual({ ); - const mediaLayerControls = semantic + const mediaLayerControls = activeSemantic || (showReferenceMediaLayers && metadata.timeline?.cameraPointDelivery) || (showReferenceMediaLayers && metadata.timeline?.cameraObstacleProjectionDelivery) ? (
- {semantic ? ( + {activeSemantic ? (