Skip to main content

When the AI Speaks

A camera on a ridge captures a frame; the CONDOR pipeline flags smoke; a second or so later the arrow for that camera turns red and pulses, its tile grows a badge, and a chime plays in every operator's open tab. On the map that is the only event that changes a renderer rather than an attribute, and it is also the only map state that depends on who is looking. This page follows a detection from the store to the GPU and lists every place the AI role is enforced along the way.

The gate comes first​

Nothing AI-related renders for a user without the AI.Access role. hooks/useHasAiAccess.ts reads it from the next-auth session, and every map path takes the result as an input: getArrowStateCode only returns aiDetection when hasAiAccess is true, the transition scan in the polling hook returns an empty list without it, and the Layers Menu prunes the three portal layers that carry detection output (Raw AI, AI Hits, AI Ray Lines) through layersToAccessRequirements. The data itself is also gated upstream: in snapshot mode the edge-cached fleet snapshot strips every AI flag before it reaches the store, and only the private overlay, served to signed-in users from backend memory, writes them. The map cannot leak what the store never holds.

What counts as a hit​

hasAiDetectionHits alone is not enough. Operators choose which detection kinds they want to see (60°, 360°, video) in the AI settings, and that preference narrows every surface through one function, isEffectiveAiHit(record, prefs) in util/cameras/aiVideo.ts. A camera whose only current detection is a video clip stops lighting up anywhere the moment video is switched off, including the arrow and the pulse. A record that reports a hit but no kind flags, which older backends send, counts as an image hit. The arrow-state function is called with that result, not with the raw flag, in both the initial build and every update.

From flag to red arrow​

The pulse, on stageFire detections on a few cameras; toggle the role and the video preference and watch what disappears.
chime × 0
Bald Mtn 1
Black Butte 1
Cahuilla 1
Devils Head 1
Eagle Peak 1
Fremont 1
Grizzly 1
Hogback 1
Iron Mtn 1
Josephine 1

Fire a detection. The false→true transition flips arrowState to aiDetection, swaps the aiDetection renderer entry for the animated marker for 10 s, pulses the tile through aiDetectionStore, and plays the chime.

Two things carry the flag into the diff. hasAiDetectionHits is a field of PTZSnapshot, so a change to it marks the camera changed even when nothing moved; without that line in the snapshot the arrow would stay black until the camera panned. The resulting edit writes arrowState: 'aiDetection' and the UniqueValueRenderer paints the red filled arrow.

The fast path is SignalR. When Notification patches a camera's AI flags it pushes the new state to the ai-detection-events group, which the hub admits only for connections carrying the AI role (the negotiate call goes through the BFF with the user's bearer; the socket itself connects straight to Azure SignalR). The browser applies it to the store immediately, the store diff runs, and the arrow turns red about a second after CONDOR reported, instead of on the next ten-second poll. The poll and the private overlay remain the reconciliation.

The pulse is a renderer swap​

markAiHitTransitions keeps a previousAiHitStateRef per camera and returns the ids that went from no effective hit to one. Two consumers act on that list.

On the map, the detecting cameras' Velocity points bloom for the same window (pulseAiDetectionHit, a featureEffect on the Locations and Camera Feed layer views); see below. The arrows renderer swap that did this on the legacy arrows layer went with that layer.

Off the map, the same ids go into aiDetectionStore as aiPulsingCameraIds, cleared per camera after 10.6 s (animation plus fade), which is how the camera tiles and the callout pulse without re-detecting the transition themselves.

The map pulse on the Velocity points​

The transition scan in useFeatureMapPolling feeds two pulses: the camera-tile border pulse (aiDetectionStore) and the map pulse. pulseAiDetectionHit puts a bloom featureEffect (deviceId IN (…) of the detecting cameras) on the Locations and Camera Feed layer views for AI_DETECTION_PULSE_DURATION_MS, restarting the window on a new hit. Whether a stream point shows a detection colour is the stream renderer's business, authored in the portal.

The AI filter and the map​

Lighting the AI detected spotlight in the navbar does two things to the map. The filter effect in useFeatureMapLayers recomputes the cId IN (…) definition expression from shouldCameraBeFiltered, where AI is an AND gate narrowed further by the duration filter and the kind preference, and applies it to every Velocity layer. And the camera list switches to the detection-start comparator so newest detections come first. The confidence slider is enforced server-side through the latest-detections filter query in legacy mode and client-side against the overlay's confidence in snapshot mode; either way the map reads the resulting id set, not the slider.

Where else the detection shows​

The site hover popup carries an "AI Anomaly" line naming the kinds in play and a play glyph when a clip exists; the camera callout shows the same AIIndicatorBtn chip the tiles use, reading whichever of its own poll and the shared store reports a hit; and the three AI portal layers, when an operator with the role turns them on, draw CONDOR's own output beside ours. Clicking any of those badges opens the AI alert modal, which is the subject of its own feature lesson.

Wrapping up​

The role gates the data, the state function and the menu. One rule, isEffectiveAiHit, decides what counts. The flag rides the ordinary diff into an attribute edit, and the false-to-true transition drives a ten-second renderer swap on the map, a store-fed pulse on the tiles, and an independently subscribed chime. SignalR makes it fast; the poll makes it certain.

Next: Streams, Beams, and the Handoff, the second way cameras can be drawn.