@camstack/ui-library 1.2.48 → 1.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,411 @@
1
+ const require_index = require("./index.cjs");
2
+ const require_MaskShapeCanvas = require("./MaskShapeCanvas-BByN3jvt.cjs");
3
+ let react = require("react");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ /**
6
+ * @license lucide-react v0.576.0 - ISC
7
+ *
8
+ * This source code is licensed under the ISC license.
9
+ * See the LICENSE file in the root directory of this source tree.
10
+ */
11
+ var ScanEye = require_index.createLucideIcon("scan-eye", [
12
+ ["path", {
13
+ d: "M3 7V5a2 2 0 0 1 2-2h2",
14
+ key: "aa7l1z"
15
+ }],
16
+ ["path", {
17
+ d: "M17 3h2a2 2 0 0 1 2 2v2",
18
+ key: "4qcy5o"
19
+ }],
20
+ ["path", {
21
+ d: "M21 17v2a2 2 0 0 1-2 2h-2",
22
+ key: "6vwrx8"
23
+ }],
24
+ ["path", {
25
+ d: "M7 21H5a2 2 0 0 1-2-2v-2",
26
+ key: "ioqczr"
27
+ }],
28
+ ["circle", {
29
+ cx: "12",
30
+ cy: "12",
31
+ r: "1",
32
+ key: "41hilf"
33
+ }],
34
+ ["path", {
35
+ d: "M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0",
36
+ key: "11ak4c"
37
+ }]
38
+ ]);
39
+ //#endregion
40
+ //#region src/composites/cap-settings/scene-monitor-copy.ts
41
+ /** The lighting variants an operator is asked to capture. The resolver can also
42
+ * report `night`, `dawn` and `dusk`; those are shown when present but never
43
+ * demanded, because most installs only ever see day and IR. */
44
+ var SCENE_CAPTURE_VARIANTS = ["day", "ir"];
45
+ var VARIANT_LABEL = {
46
+ day: "Daylight",
47
+ ir: "Night (IR)",
48
+ night: "Night",
49
+ dawn: "Dawn",
50
+ dusk: "Dusk"
51
+ };
52
+ function variantLabel(condition) {
53
+ return VARIANT_LABEL[condition] ?? condition;
54
+ }
55
+ /**
56
+ * "day ✓ · ir ✗ — this scene reports UNKNOWN at night."
57
+ *
58
+ * Converts the silent cross-condition fallback this engine used to have into an
59
+ * operator-visible fact. Without it, a scene with only a daylight reference
60
+ * simply stops answering after sunset and nothing anywhere says why.
61
+ */
62
+ function coverageLine(monitor) {
63
+ const covered = new Set(monitor.coveredConditions);
64
+ const parts = SCENE_CAPTURE_VARIANTS.map((v) => `${variantLabel(v)} ${covered.has(v) ? "✓" : "✗"}`);
65
+ const extra = [...covered].filter((c) => !SCENE_CAPTURE_VARIANTS.some((v) => v === c));
66
+ for (const c of extra) parts.push(`${variantLabel(c)} ✓`);
67
+ const missing = SCENE_CAPTURE_VARIANTS.filter((v) => !covered.has(v));
68
+ if (missing.length === 0) return parts.join(" · ");
69
+ return `${parts.join(" · ")} — reports “can’t tell” in ${missing.map((v) => variantLabel(v).toLowerCase()).join(" and ")}`;
70
+ }
71
+ var UNAVAILABLE_SENTENCE = {
72
+ "no-reference-for-condition": "can’t tell — nothing was captured in this light yet. Use “Capture now” when the camera is in it.",
73
+ "view-shifted": "can’t tell — the camera view has moved since the reference was captured. Re-capture it.",
74
+ "no-vision-profile": "can’t tell — the vision model is unreachable, so the change is on hold.",
75
+ "encoder-model-changed": "can’t tell — the embedding model changed, so the stored references are unreadable. Re-capture them.",
76
+ "no-snapshot": "can’t tell — this camera did not return a frame."
77
+ };
78
+ /**
79
+ * The one line under a scene's name. It answers, in order: can it judge, what
80
+ * does it say, and how long has it said it.
81
+ */
82
+ function statusLine(monitor, now) {
83
+ if (monitor.states.length === 0) return "Not armed yet — capture the initial state to start watching.";
84
+ if (monitor.verdict === "unknown") return monitor.unavailable !== null ? UNAVAILABLE_SENTENCE[monitor.unavailable] : "can’t tell right now.";
85
+ const since = monitor.verdict === "diverged" ? monitor.divergedAt : monitor.armedAt;
86
+ const held = since !== null ? ` for ${humanDuration(Math.max(0, now - since))}` : "";
87
+ if (monitor.latched) return monitor.verdict === "diverged" ? `CHANGED${held} — press Reset when you have dealt with it.` : `Back to normal, but still flagged as changed${held}. Press Reset to clear it.`;
88
+ return monitor.verdict === "diverged" ? `CHANGED${held}` : `Normal${held}`;
89
+ }
90
+ /** The badge word. `latched` wins over `verdict`, because the latch is the
91
+ * thing the operator asked to be told and it outlives the live state. */
92
+ function badgeFor(monitor) {
93
+ if (monitor.states.length === 0) return {
94
+ text: "Not armed",
95
+ tone: "unknown"
96
+ };
97
+ if (monitor.latched) return {
98
+ text: "Changed",
99
+ tone: "alarm"
100
+ };
101
+ if (monitor.verdict === "unknown") return {
102
+ text: "Can’t tell",
103
+ tone: "unknown"
104
+ };
105
+ return monitor.verdict === "diverged" ? {
106
+ text: "Changed",
107
+ tone: "alarm"
108
+ } : {
109
+ text: "Normal",
110
+ tone: "ok"
111
+ };
112
+ }
113
+ /** A rect covering most of the frame is dominated by weather, sky and parked
114
+ * cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
115
+ * failure is silent INSENSITIVITY, which is the worst kind — hence a warning
116
+ * rather than a refusal. */
117
+ var ROI_AREA_WARN_FRACTION = .4;
118
+ function roiTooLarge(roi) {
119
+ return roi.width * roi.height > ROI_AREA_WARN_FRACTION;
120
+ }
121
+ function humanDuration(ms) {
122
+ const s = Math.floor(ms / 1e3);
123
+ if (s < 60) return `${s}s`;
124
+ const m = Math.floor(s / 60);
125
+ if (m < 60) return `${m}m`;
126
+ const h = Math.floor(m / 60);
127
+ if (h < 48) return `${h}h`;
128
+ return `${Math.floor(h / 24)}d`;
129
+ }
130
+ //#endregion
131
+ //#region src/composites/cap-settings/SceneMonitorEditor.tsx
132
+ var BTN_NEUTRAL = "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors";
133
+ var BTN_PRIMARY = "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors";
134
+ var BTN_DANGER = "rounded-md border border-danger/50 bg-danger/10 px-2 py-1 text-[11px] font-medium text-danger hover:bg-danger/20 disabled:opacity-40 transition-colors";
135
+ var TONE_CLASS = {
136
+ alarm: "border-danger/50 bg-danger/15 text-danger",
137
+ ok: "border-success/50 bg-success/15 text-success",
138
+ unknown: "border-border bg-surface text-foreground-subtle"
139
+ };
140
+ /** A scene without at least one reference cannot judge anything, so the
141
+ * similarity threshold only becomes meaningful after the first capture. */
142
+ var DEFAULT_THRESHOLD = .8;
143
+ var DEFAULT_HYSTERESIS = 3;
144
+ function SceneMonitorEditor({ deviceId }) {
145
+ const dev = require_index.useDeviceProxy$1(require_index.useSystem$1().trpcClient, deviceId);
146
+ const pushed = require_index.useDeviceStateSlice$1(dev?.state.sceneMonitor);
147
+ const snapshot = require_index.useDeviceSnapshotImage$1(deviceId, { width: 640 });
148
+ const [fetched, setFetched] = (0, react.useState)(null);
149
+ const [unsupported, setUnsupported] = (0, react.useState)(false);
150
+ const [busy, setBusy] = (0, react.useState)(null);
151
+ const [error, setError] = (0, react.useState)(null);
152
+ const [selectedId, setSelectedId] = (0, react.useState)(null);
153
+ const [drafting, setDrafting] = (0, react.useState)(false);
154
+ const [draftRoi, setDraftRoi] = (0, react.useState)(null);
155
+ const [draftLabel, setDraftLabel] = (0, react.useState)("");
156
+ const mounted = (0, react.useRef)(true);
157
+ (0, react.useEffect)(() => {
158
+ mounted.current = true;
159
+ return () => {
160
+ mounted.current = false;
161
+ };
162
+ }, []);
163
+ const reload = (0, react.useCallback)(async () => {
164
+ if (!dev) return;
165
+ try {
166
+ const status = await dev.sceneMonitor?.listScenes({});
167
+ if (!mounted.current) return;
168
+ if (status) setFetched(status);
169
+ } catch (err) {
170
+ if (!mounted.current) return;
171
+ if (require_index.isAbsentProvider$1(err)) setUnsupported(true);
172
+ else setError(err instanceof Error ? err.message : String(err));
173
+ }
174
+ }, [dev]);
175
+ (0, react.useEffect)(() => {
176
+ setFetched(null);
177
+ setUnsupported(false);
178
+ setError(null);
179
+ reload();
180
+ }, [reload]);
181
+ const monitors = pushed?.monitors ?? fetched?.monitors ?? [];
182
+ const now = Date.now();
183
+ const run = (0, react.useCallback)(async (key, fn) => {
184
+ setBusy(key);
185
+ setError(null);
186
+ try {
187
+ await fn();
188
+ await reload();
189
+ } catch (err) {
190
+ if (mounted.current) setError(err instanceof Error ? err.message : String(err));
191
+ } finally {
192
+ if (mounted.current) setBusy(null);
193
+ }
194
+ }, [reload]);
195
+ const items = (0, react.useMemo)(() => {
196
+ const existing = monitors.map((m) => ({
197
+ id: m.id,
198
+ shape: m.roi,
199
+ label: m.label,
200
+ enabled: m.enabled
201
+ }));
202
+ return draftRoi !== null ? [...existing, {
203
+ id: "__draft__",
204
+ shape: draftRoi,
205
+ label: "New scene"
206
+ }] : existing;
207
+ }, [monitors, draftRoi]);
208
+ const onShapeChange = (0, react.useCallback)((id, shape) => {
209
+ if (shape.kind !== "rect") return;
210
+ if (id === "__draft__") {
211
+ setDraftRoi(shape);
212
+ return;
213
+ }
214
+ run(`roi-${String(id)}`, async () => dev?.sceneMonitor?.updateScene({
215
+ monitorId: String(id),
216
+ patch: { roi: shape }
217
+ }));
218
+ }, [dev, run]);
219
+ const onDrawComplete = (0, react.useCallback)((shape) => {
220
+ if (shape.kind !== "rect") return;
221
+ setDraftRoi(shape);
222
+ }, []);
223
+ const createScene = (0, react.useCallback)(async () => {
224
+ if (draftRoi === null || draftLabel.trim().length === 0) return;
225
+ await run("create", async () => {
226
+ await dev?.sceneMonitor?.createScene({
227
+ label: draftLabel.trim(),
228
+ roi: draftRoi,
229
+ check: {
230
+ mode: "similarity",
231
+ threshold: DEFAULT_THRESHOLD,
232
+ hysteresisCount: DEFAULT_HYSTERESIS
233
+ }
234
+ });
235
+ });
236
+ setDraftRoi(null);
237
+ setDraftLabel("");
238
+ setDrafting(false);
239
+ }, [
240
+ dev,
241
+ draftRoi,
242
+ draftLabel,
243
+ run
244
+ ]);
245
+ if (!dev) return null;
246
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.WidgetPanel, {
247
+ title: "Scenes",
248
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScanEye, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
249
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
250
+ className: "flex flex-col gap-3",
251
+ children: unsupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
252
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
253
+ children: "Scene monitoring is not available for this camera."
254
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
256
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
257
+ children: "A scene watches one region of this camera and tells you when it stops looking like the picture you captured. Draw a box around the thing you care about — a wheelie bin, a gate, a parking space — then capture what “normal” looks like, in daylight and again at night."
258
+ }),
259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
260
+ className: "relative overflow-hidden rounded-md border border-border",
261
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_MaskShapeCanvas.MaskShapeCanvas, {
262
+ items,
263
+ supportedShapes: ["rect"],
264
+ selectedId: selectedId ?? (draftRoi !== null ? "__draft__" : null),
265
+ onSelect: (id) => setSelectedId(id === null ? null : String(id)),
266
+ onShapeChange,
267
+ onDrawComplete,
268
+ drawingKind: drafting && draftRoi === null ? "rect" : null,
269
+ backdrop: snapshot.src !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
270
+ src: snapshot.src,
271
+ alt: "",
272
+ className: "h-full w-full object-contain",
273
+ draggable: false
274
+ }) : null
275
+ })
276
+ }),
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
278
+ className: "flex flex-wrap items-center gap-2",
279
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
280
+ type: "button",
281
+ className: drafting ? BTN_PRIMARY : BTN_NEUTRAL,
282
+ "aria-pressed": drafting,
283
+ onClick: () => {
284
+ setDrafting((v) => !v);
285
+ setDraftRoi(null);
286
+ },
287
+ disabled: busy !== null,
288
+ children: drafting ? "Cancel" : "+ New scene"
289
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
290
+ type: "button",
291
+ className: BTN_NEUTRAL,
292
+ onClick: snapshot.refresh,
293
+ disabled: busy !== null,
294
+ children: "Refresh frame"
295
+ })]
296
+ }),
297
+ drafting && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
298
+ className: "flex flex-col gap-2 rounded-md border border-border bg-surface p-2",
299
+ children: [
300
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
301
+ className: "text-[10px] text-foreground-subtle",
302
+ children: draftRoi === null ? "Click on the frame to drop a box, then drag its corner to fit it around the thing you want watched." : "Name it, then create. You capture what “normal” looks like next."
303
+ }),
304
+ draftRoi !== null && roiTooLarge(draftRoi) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
305
+ className: "text-[11px] leading-relaxed text-warning",
306
+ children: "That box covers most of the frame. A scene that big is dominated by the weather, the sky and passing cars — it will quietly stop noticing the thing you actually care about. Draw it tighter."
307
+ }),
308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
309
+ className: "flex items-center gap-2",
310
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
311
+ type: "text",
312
+ value: draftLabel,
313
+ onChange: (e) => setDraftLabel(e.target.value),
314
+ placeholder: "e.g. Immondizia prelevata",
315
+ className: "flex-1 rounded-md border border-border bg-surface px-2 py-1 text-[12px] text-foreground"
316
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
317
+ type: "button",
318
+ className: BTN_PRIMARY,
319
+ onClick: () => void createScene(),
320
+ disabled: draftRoi === null || draftLabel.trim().length === 0 || busy !== null,
321
+ children: "Create"
322
+ })]
323
+ })
324
+ ]
325
+ }),
326
+ error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
327
+ className: "text-[11px] leading-relaxed text-danger",
328
+ children: error
329
+ }),
330
+ monitors.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
331
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
332
+ children: "No scenes on this camera yet."
333
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
334
+ className: "flex flex-col gap-2",
335
+ children: monitors.map((m) => {
336
+ const badge = badgeFor(m);
337
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
338
+ className: `flex flex-col gap-1.5 rounded-md border p-2 ${selectedId === m.id ? "border-primary/50" : "border-border"}`,
339
+ children: [
340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
341
+ className: "flex items-center gap-2",
342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
343
+ type: "button",
344
+ className: "flex-1 text-left text-[12px] font-medium text-foreground",
345
+ onClick: () => setSelectedId(m.id),
346
+ children: m.label
347
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
348
+ className: `rounded border px-1.5 py-0.5 text-[10px] font-medium ${TONE_CLASS[badge.tone]}`,
349
+ children: badge.text
350
+ })]
351
+ }),
352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
353
+ className: `${require_index.TEXT_HINT} leading-relaxed`,
354
+ children: statusLine(m, now)
355
+ }),
356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
357
+ className: `${require_index.TEXT_HINT} tabular-nums`,
358
+ children: coverageLine(m)
359
+ }),
360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
361
+ className: "flex flex-wrap items-center gap-1.5",
362
+ children: [
363
+ SCENE_CAPTURE_VARIANTS.map((variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
364
+ type: "button",
365
+ className: BTN_NEUTRAL,
366
+ disabled: busy !== null,
367
+ title: `Take a fresh photo now and store it as the ${variantLabel(variant).toLowerCase()} reference`,
368
+ onClick: () => void run(`cap-${m.id}-${variant}`, async () => dev.sceneMonitor?.captureReference({
369
+ monitorId: m.id,
370
+ condition: variant
371
+ })),
372
+ children: [
373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.Camera, { className: "mr-1 inline h-3 w-3" }),
374
+ "Capture ",
375
+ variantLabel(variant).toLowerCase()
376
+ ]
377
+ }, variant)),
378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
379
+ type: "button",
380
+ className: BTN_PRIMARY,
381
+ disabled: busy !== null || m.states.length === 0,
382
+ title: "Clear the flag and re-photograph what normal looks like right now",
383
+ onClick: () => void run(`reset-${m.id}`, async () => dev.sceneMonitor?.resetScene({ monitorId: m.id })),
384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.RotateCcw, { className: "mr-1 inline h-3 w-3" }), "Reset"]
385
+ }),
386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
387
+ type: "button",
388
+ className: BTN_NEUTRAL,
389
+ disabled: busy !== null,
390
+ onClick: () => void run(`check-${m.id}`, async () => dev.sceneMonitor?.recheckNow({ monitorId: m.id })),
391
+ children: "Check now"
392
+ }),
393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
394
+ type: "button",
395
+ className: BTN_DANGER,
396
+ disabled: busy !== null,
397
+ onClick: () => void run(`del-${m.id}`, async () => dev.sceneMonitor?.deleteScene({ monitorId: m.id })),
398
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_index.Trash2, { className: "h-3 w-3" })
399
+ })
400
+ ]
401
+ })
402
+ ]
403
+ }, m.id);
404
+ })
405
+ })
406
+ ] })
407
+ })
408
+ });
409
+ }
410
+ //#endregion
411
+ exports.SceneMonitorEditor = SceneMonitorEditor;
@@ -0,0 +1,2 @@
1
+ import { CapSettingsComponentProps } from './index';
2
+ export declare function SceneMonitorEditor({ deviceId }: CapSettingsComponentProps): import("react").JSX.Element | null;
@@ -1,13 +1,14 @@
1
- export { PtzPanel } from './PtzPanel';
2
- export { ConsumablesPanel } from './ConsumablesPanel';
3
1
  export { AutotrackSection } from './AutotrackSection';
2
+ export { ConsumablesPanel } from './ConsumablesPanel';
3
+ export type { MaskShapeCanvasProps, MaskShapeItem } from './MaskShapeCanvas';
4
+ export { MaskShapeCanvas } from './MaskShapeCanvas';
4
5
  export { MotionZonesSettings } from './MotionZonesSettings';
5
6
  export { PrivacyMaskSettings } from './PrivacyMaskSettings';
7
+ export { PtzPanel } from './PtzPanel';
8
+ export { RecordingBandsEditor } from './RecordingBandsEditor';
6
9
  export { RecordingPanel } from './RecordingPanel';
7
10
  export { RecordingSettings } from './RecordingSettings';
8
- export { RecordingBandsEditor } from './RecordingBandsEditor';
9
- export { MaskShapeCanvas } from './MaskShapeCanvas';
10
- export type { MaskShapeItem, MaskShapeCanvasProps } from './MaskShapeCanvas';
11
+ export { SceneMonitorEditor } from './SceneMonitorEditor';
11
12
  /**
12
13
  * Props every cap-settings component receives.
13
14
  *
@@ -0,0 +1,37 @@
1
+ import { SceneMonitor } from '@camstack/types';
2
+ /** The lighting variants an operator is asked to capture. The resolver can also
3
+ * report `night`, `dawn` and `dusk`; those are shown when present but never
4
+ * demanded, because most installs only ever see day and IR. */
5
+ export declare const SCENE_CAPTURE_VARIANTS: readonly ["day", "ir"];
6
+ export type SceneCaptureVariant = (typeof SCENE_CAPTURE_VARIANTS)[number];
7
+ export declare const VARIANT_LABEL: Readonly<Record<string, string>>;
8
+ export declare function variantLabel(condition: string): string;
9
+ /**
10
+ * "day ✓ · ir ✗ — this scene reports UNKNOWN at night."
11
+ *
12
+ * Converts the silent cross-condition fallback this engine used to have into an
13
+ * operator-visible fact. Without it, a scene with only a daylight reference
14
+ * simply stops answering after sunset and nothing anywhere says why.
15
+ */
16
+ export declare function coverageLine(monitor: SceneMonitor): string;
17
+ /**
18
+ * The one line under a scene's name. It answers, in order: can it judge, what
19
+ * does it say, and how long has it said it.
20
+ */
21
+ export declare function statusLine(monitor: SceneMonitor, now: number): string;
22
+ /** The badge word. `latched` wins over `verdict`, because the latch is the
23
+ * thing the operator asked to be told and it outlives the live state. */
24
+ export declare function badgeFor(monitor: SceneMonitor): {
25
+ text: string;
26
+ tone: 'alarm' | 'ok' | 'unknown';
27
+ };
28
+ /** A rect covering most of the frame is dominated by weather, sky and parked
29
+ * cars: the bin is 2 % of the pixels and CLIP will not notice it leaving. The
30
+ * failure is silent INSENSITIVITY, which is the worst kind — hence a warning
31
+ * rather than a refusal. */
32
+ export declare const ROI_AREA_WARN_FRACTION = 0.4;
33
+ export declare function roiTooLarge(roi: {
34
+ width: number;
35
+ height: number;
36
+ }): boolean;
37
+ export declare function humanDuration(ms: number): string;