@camstack/addon-post-analysis 1.2.343 → 1.2.344
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.
- package/dist/{clip-model-registry-DZz3e781.mjs → clip-model-registry-C44gfwjm.mjs} +1992 -57
- package/dist/{clip-model-registry-Cd_y9S4U.js → clip-model-registry-Ca_IbZyD.js} +1992 -57
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrdO3Pxp.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D73dyYqt.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C-2BbuN4.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-ZZJLA-YE.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DPNnWr-m.mjs → hostInit-h_8fvfFv.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +1562 -138
- package/dist/pipeline-analytics/index.mjs +1562 -138
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DEq-B6oq.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CiTUMvVf.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-Ca_IbZyD.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -5839,16 +5839,16 @@ function ncDetectionSummary(texts, counts) {
|
|
|
5839
5839
|
//#region src/notification-center/timelapse/detection-counts.ts
|
|
5840
5840
|
/** One normalisation, used on both sides of every comparison — a rule that
|
|
5841
5841
|
* named `Person` must match a track classified `person`. */
|
|
5842
|
-
function normalise(name) {
|
|
5842
|
+
function normalise$1(name) {
|
|
5843
5843
|
return name.trim().toLowerCase();
|
|
5844
5844
|
}
|
|
5845
5845
|
/** Count per class over the tracks, honouring the rule's selection. */
|
|
5846
5846
|
function countDetections(input) {
|
|
5847
|
-
const selected = (input.reportClasses ?? []).map(normalise).filter((c) => c.length > 0);
|
|
5847
|
+
const selected = (input.reportClasses ?? []).map(normalise$1).filter((c) => c.length > 0);
|
|
5848
5848
|
const seen = /* @__PURE__ */ new Map();
|
|
5849
5849
|
for (const t of input.tracks) {
|
|
5850
5850
|
if (t.className === void 0) continue;
|
|
5851
|
-
const key = normalise(t.className);
|
|
5851
|
+
const key = normalise$1(t.className);
|
|
5852
5852
|
if (key.length === 0) continue;
|
|
5853
5853
|
if (selected.length > 0 && !selected.includes(key)) continue;
|
|
5854
5854
|
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
@@ -70619,10 +70619,12 @@ var TrackStore = class {
|
|
|
70619
70619
|
*/
|
|
70620
70620
|
/**
|
|
70621
70621
|
* @durable class=ledger owner=pipeline-analytics
|
|
70622
|
-
* write="one row per frame the operator SELECTS into the retrain dataset
|
|
70623
|
-
*
|
|
70624
|
-
*
|
|
70625
|
-
* the
|
|
70622
|
+
* write="one row per frame the operator SELECTS into the retrain dataset, or adds
|
|
70623
|
+
* from the recording at an instant (storeRecordingStill, origin 'recording', no
|
|
70624
|
+
* track). The copy is taken eagerly at selection (copyFrame), not referenced, so
|
|
70625
|
+
* the row records a blob the dataset owns outright; re-selecting the same source
|
|
70626
|
+
* (or the same camera instant) reuses the existing copy rather than writing a
|
|
70627
|
+
* second one."
|
|
70626
70628
|
* retention="none — the blobs sit in the 'retrain/' subtree of the artifacts class
|
|
70627
70629
|
* (D781), which no owner cascade and no age sweep walks, which is exactly what makes a 'trained' track evictable
|
|
70628
70630
|
* again (D81). Rows go only when the operator deselects the frame (deleteFrame,
|
|
@@ -70737,6 +70739,19 @@ var RETRAIN_FRAME_COLUMNS = [
|
|
|
70737
70739
|
{
|
|
70738
70740
|
name: "complete",
|
|
70739
70741
|
type: "BOOLEAN"
|
|
70742
|
+
}),
|
|
70743
|
+
(
|
|
70744
|
+
/** `recording` for a still taken from the recording at an instant (no
|
|
70745
|
+
* track, no source media row). NULL = a track copy. */
|
|
70746
|
+
{
|
|
70747
|
+
name: "origin",
|
|
70748
|
+
type: "TEXT"
|
|
70749
|
+
}),
|
|
70750
|
+
(
|
|
70751
|
+
/** The recording instant a `recording` still was asked for. */
|
|
70752
|
+
{
|
|
70753
|
+
name: "epochMs",
|
|
70754
|
+
type: "INTEGER"
|
|
70740
70755
|
})
|
|
70741
70756
|
];
|
|
70742
70757
|
var RETRAIN_FRAME_INDEXES = [
|
|
@@ -70762,6 +70777,8 @@ var RETRAIN_FRAME_INDEXES = [
|
|
|
70762
70777
|
columns: ["locationId"]
|
|
70763
70778
|
})
|
|
70764
70779
|
];
|
|
70780
|
+
/** Ids per `IN (…)` page in {@link RetrainFrameStore.getMany}. */
|
|
70781
|
+
var GET_MANY_PAGE = 500;
|
|
70765
70782
|
/**
|
|
70766
70783
|
* The copy's id, derived from `(trackId, sourceMediaId)`.
|
|
70767
70784
|
*
|
|
@@ -70774,8 +70791,20 @@ var RETRAIN_FRAME_INDEXES = [
|
|
|
70774
70791
|
function retrainFrameId(trackId, sourceMediaId) {
|
|
70775
70792
|
return `retrain:${trackId}:${(0, node_crypto.createHash)("sha1").update(String(sourceMediaId)).digest("hex").slice(0, 16)}`;
|
|
70776
70793
|
}
|
|
70777
|
-
|
|
70778
|
-
|
|
70794
|
+
/**
|
|
70795
|
+
* A recording still's id, derived from `(deviceId, epochMs)` — the same moment
|
|
70796
|
+
* of the same camera is one copy, however many times it is added.
|
|
70797
|
+
*/
|
|
70798
|
+
function recordingFrameId(deviceId, epochMs) {
|
|
70799
|
+
const digest = (0, node_crypto.createHash)("sha1").update(String(epochMs)).digest("hex").slice(0, 16);
|
|
70800
|
+
return `retrain:rec:${String(deviceId)}:${digest}`;
|
|
70801
|
+
}
|
|
70802
|
+
/** The directory a recording still sits in, beside the track directories. */
|
|
70803
|
+
var RECORDING_STILL_DIR = "recording";
|
|
70804
|
+
function framePath(frame) {
|
|
70805
|
+
const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
|
|
70806
|
+
const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
|
|
70807
|
+
return `${RETRAIN_PATH_PREFIX}/${frame.deviceId}/${dir}/${leaf}.jpg`;
|
|
70779
70808
|
}
|
|
70780
70809
|
/** A stored number, or `undefined` for NULL / absent / junk. */
|
|
70781
70810
|
function finiteNumber(value) {
|
|
@@ -70820,9 +70849,11 @@ var RetrainFrameStore = class {
|
|
|
70820
70849
|
});
|
|
70821
70850
|
}
|
|
70822
70851
|
rowToFrame(id, data) {
|
|
70852
|
+
const origin = data["origin"] === "recording" ? "recording" : "track";
|
|
70823
70853
|
return {
|
|
70824
70854
|
frameId: id,
|
|
70825
70855
|
deviceId: Number(data["deviceId"] ?? 0),
|
|
70856
|
+
origin,
|
|
70826
70857
|
trackId: String(data["trackId"] ?? ""),
|
|
70827
70858
|
sourceMediaId: Number(data["sourceMediaId"] ?? 0),
|
|
70828
70859
|
sourceKind: String(data["sourceKind"] ?? "keyFrame"),
|
|
@@ -70833,9 +70864,54 @@ var RetrainFrameStore = class {
|
|
|
70833
70864
|
locationId: typeof data["locationId"] === "string" && data["locationId"].length > 0 ? data["locationId"] : null,
|
|
70834
70865
|
...finiteNumber(data["capturedAt"]) !== void 0 ? { capturedAt: finiteNumber(data["capturedAt"]) } : {},
|
|
70835
70866
|
...finiteNumber(data["chromaSpread"]) !== void 0 ? { chromaSpread: finiteNumber(data["chromaSpread"]) } : {},
|
|
70836
|
-
complete: data["complete"] === true || data["complete"] === 1
|
|
70867
|
+
complete: data["complete"] === true || data["complete"] === 1,
|
|
70868
|
+
...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {}
|
|
70837
70869
|
};
|
|
70838
70870
|
}
|
|
70871
|
+
/** The copies with these ids, in the order given; unknown ids are skipped. */
|
|
70872
|
+
async getMany(frameIds) {
|
|
70873
|
+
if (frameIds.length === 0) return [];
|
|
70874
|
+
try {
|
|
70875
|
+
const byId = /* @__PURE__ */ new Map();
|
|
70876
|
+
for (let i = 0; i < frameIds.length; i += GET_MANY_PAGE) {
|
|
70877
|
+
const page = frameIds.slice(i, i + GET_MANY_PAGE);
|
|
70878
|
+
const rows = await this.store.query.query({
|
|
70879
|
+
collection: RETRAIN_FRAMES_COLLECTION,
|
|
70880
|
+
filter: {
|
|
70881
|
+
whereIn: { id: [...page] },
|
|
70882
|
+
limit: page.length
|
|
70883
|
+
}
|
|
70884
|
+
});
|
|
70885
|
+
for (const r of rows) if (typeof r.id === "string") byId.set(r.id, this.rowToFrame(r.id, r.data));
|
|
70886
|
+
}
|
|
70887
|
+
return frameIds.flatMap((id) => {
|
|
70888
|
+
const frame = byId.get(id);
|
|
70889
|
+
return frame !== void 0 ? [frame] : [];
|
|
70890
|
+
});
|
|
70891
|
+
} catch (err) {
|
|
70892
|
+
this.logger.warn("RetrainFrameStore.getMany failed", { meta: {
|
|
70893
|
+
frames: frameIds.length,
|
|
70894
|
+
error: String(err)
|
|
70895
|
+
} });
|
|
70896
|
+
return [];
|
|
70897
|
+
}
|
|
70898
|
+
}
|
|
70899
|
+
/** Every camera the dataset holds a copy of — a recording still can be the
|
|
70900
|
+
* only thing a camera contributes, with no track anywhere. */
|
|
70901
|
+
async listDeviceIds() {
|
|
70902
|
+
try {
|
|
70903
|
+
const rows = await this.store.query.query({ collection: RETRAIN_FRAMES_COLLECTION });
|
|
70904
|
+
const ids = /* @__PURE__ */ new Set();
|
|
70905
|
+
for (const r of rows) {
|
|
70906
|
+
const id = Number(r.data["deviceId"]);
|
|
70907
|
+
if (Number.isInteger(id) && id > 0) ids.add(id);
|
|
70908
|
+
}
|
|
70909
|
+
return [...ids].toSorted((a, b) => a - b);
|
|
70910
|
+
} catch (err) {
|
|
70911
|
+
this.logger.warn("RetrainFrameStore.listDeviceIds failed", { meta: { error: String(err) } });
|
|
70912
|
+
return [];
|
|
70913
|
+
}
|
|
70914
|
+
}
|
|
70839
70915
|
/** The copy with this id, or `null`. Index read only — no blob is touched. */
|
|
70840
70916
|
async get(frameId) {
|
|
70841
70917
|
try {
|
|
@@ -70852,8 +70928,10 @@ var RetrainFrameStore = class {
|
|
|
70852
70928
|
return null;
|
|
70853
70929
|
}
|
|
70854
70930
|
}
|
|
70855
|
-
/** Every copy taken from this track, oldest first.
|
|
70931
|
+
/** Every copy taken from this track, oldest first. A recording still has
|
|
70932
|
+
* no track and is never answered here. */
|
|
70856
70933
|
async listByTrack(trackId) {
|
|
70934
|
+
if (trackId === "") return [];
|
|
70857
70935
|
try {
|
|
70858
70936
|
return (await this.store.query.query({
|
|
70859
70937
|
collection: RETRAIN_FRAMES_COLLECTION,
|
|
@@ -70927,7 +71005,7 @@ var RetrainFrameStore = class {
|
|
|
70927
71005
|
try {
|
|
70928
71006
|
return await this.storage.read({
|
|
70929
71007
|
location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
|
|
70930
|
-
relativePath: framePath(frame
|
|
71008
|
+
relativePath: framePath(frame)
|
|
70931
71009
|
});
|
|
70932
71010
|
} catch (err) {
|
|
70933
71011
|
this.logger.warn("retrain frame copy unreadable", {
|
|
@@ -70996,12 +71074,17 @@ var RetrainFrameStore = class {
|
|
|
70996
71074
|
reason: "unreadable-image"
|
|
70997
71075
|
};
|
|
70998
71076
|
}
|
|
70999
|
-
const path = framePath(
|
|
71077
|
+
const path = framePath({
|
|
71078
|
+
...input,
|
|
71079
|
+
frameId,
|
|
71080
|
+
origin: "track"
|
|
71081
|
+
});
|
|
71000
71082
|
const locationId = RETRAIN_MEDIA_LOCATION;
|
|
71001
71083
|
const chromaSpread = await this.chromaOf(bytes, input.deviceId);
|
|
71002
71084
|
const frame = {
|
|
71003
71085
|
frameId,
|
|
71004
71086
|
deviceId: input.deviceId,
|
|
71087
|
+
origin: "track",
|
|
71005
71088
|
trackId: input.trackId,
|
|
71006
71089
|
sourceMediaId: input.sourceMediaId,
|
|
71007
71090
|
sourceKind: input.sourceKind,
|
|
@@ -71076,6 +71159,115 @@ var RetrainFrameStore = class {
|
|
|
71076
71159
|
};
|
|
71077
71160
|
}
|
|
71078
71161
|
/**
|
|
71162
|
+
* Store a still decoded from the RECORDING at `epochMs` — a frame with no
|
|
71163
|
+
* track and no media row, usually the one the model saw nothing in.
|
|
71164
|
+
*
|
|
71165
|
+
* Same contract as {@link copyFrame}: an existing copy of that moment is
|
|
71166
|
+
* returned untouched; the blob lands before the row; a refusal is named.
|
|
71167
|
+
* `capturedAt` IS the instant — the camera saw this frame then.
|
|
71168
|
+
*/
|
|
71169
|
+
async storeRecordingStill(input) {
|
|
71170
|
+
const frameId = recordingFrameId(input.deviceId, input.epochMs);
|
|
71171
|
+
const existing = await this.get(frameId);
|
|
71172
|
+
if (existing !== null) return {
|
|
71173
|
+
kind: "copied",
|
|
71174
|
+
frame: existing,
|
|
71175
|
+
reused: true
|
|
71176
|
+
};
|
|
71177
|
+
const size = readJpegSize(input.bytes);
|
|
71178
|
+
if (size === null) {
|
|
71179
|
+
this.logger.warn("retrain recording still refused — not a readable JPEG", {
|
|
71180
|
+
tags: { deviceId: input.deviceId },
|
|
71181
|
+
meta: {
|
|
71182
|
+
epochMs: input.epochMs,
|
|
71183
|
+
bytes: input.bytes.length
|
|
71184
|
+
}
|
|
71185
|
+
});
|
|
71186
|
+
return {
|
|
71187
|
+
kind: "refused",
|
|
71188
|
+
reason: "unreadable-image"
|
|
71189
|
+
};
|
|
71190
|
+
}
|
|
71191
|
+
const chromaSpread = await this.chromaOf(input.bytes, input.deviceId);
|
|
71192
|
+
const frame = {
|
|
71193
|
+
frameId,
|
|
71194
|
+
deviceId: input.deviceId,
|
|
71195
|
+
origin: "recording",
|
|
71196
|
+
trackId: "",
|
|
71197
|
+
sourceMediaId: 0,
|
|
71198
|
+
sourceKind: "fullFrame",
|
|
71199
|
+
sizeBytes: input.bytes.length,
|
|
71200
|
+
width: size.width,
|
|
71201
|
+
height: size.height,
|
|
71202
|
+
copiedAt: this.now(),
|
|
71203
|
+
locationId: RETRAIN_MEDIA_LOCATION,
|
|
71204
|
+
capturedAt: input.epochMs,
|
|
71205
|
+
...chromaSpread !== void 0 ? { chromaSpread } : {},
|
|
71206
|
+
complete: false,
|
|
71207
|
+
epochMs: input.epochMs
|
|
71208
|
+
};
|
|
71209
|
+
const path = framePath(frame);
|
|
71210
|
+
const persist = async () => {
|
|
71211
|
+
await this.storage.write({
|
|
71212
|
+
location: RETRAIN_MEDIA_LOCATION,
|
|
71213
|
+
relativePath: path,
|
|
71214
|
+
data: input.bytes
|
|
71215
|
+
});
|
|
71216
|
+
await this.store.set.mutate({
|
|
71217
|
+
collection: RETRAIN_FRAMES_COLLECTION,
|
|
71218
|
+
key: frameId,
|
|
71219
|
+
value: {
|
|
71220
|
+
deviceId: frame.deviceId,
|
|
71221
|
+
trackId: "",
|
|
71222
|
+
sourceMediaId: 0,
|
|
71223
|
+
sourceKind: frame.sourceKind,
|
|
71224
|
+
path,
|
|
71225
|
+
sizeBytes: frame.sizeBytes,
|
|
71226
|
+
width: frame.width,
|
|
71227
|
+
height: frame.height,
|
|
71228
|
+
copiedAt: frame.copiedAt,
|
|
71229
|
+
locationId: RETRAIN_MEDIA_LOCATION,
|
|
71230
|
+
capturedAt: input.epochMs,
|
|
71231
|
+
chromaSpread: frame.chromaSpread ?? null,
|
|
71232
|
+
complete: false,
|
|
71233
|
+
origin: "recording",
|
|
71234
|
+
epochMs: input.epochMs
|
|
71235
|
+
}
|
|
71236
|
+
});
|
|
71237
|
+
};
|
|
71238
|
+
try {
|
|
71239
|
+
if (this.writeGate) await this.writeGate.run(persist);
|
|
71240
|
+
else await persist();
|
|
71241
|
+
} catch (err) {
|
|
71242
|
+
this.logger.error("retrain recording still failed to persist", {
|
|
71243
|
+
tags: { deviceId: input.deviceId },
|
|
71244
|
+
meta: {
|
|
71245
|
+
frameId,
|
|
71246
|
+
epochMs: input.epochMs,
|
|
71247
|
+
error: String(err)
|
|
71248
|
+
}
|
|
71249
|
+
});
|
|
71250
|
+
return {
|
|
71251
|
+
kind: "refused",
|
|
71252
|
+
reason: "write-failed"
|
|
71253
|
+
};
|
|
71254
|
+
}
|
|
71255
|
+
this.logger.info("retrain recording still stored in the dataset", {
|
|
71256
|
+
tags: { deviceId: input.deviceId },
|
|
71257
|
+
meta: {
|
|
71258
|
+
frameId,
|
|
71259
|
+
epochMs: input.epochMs,
|
|
71260
|
+
width: frame.width,
|
|
71261
|
+
height: frame.height
|
|
71262
|
+
}
|
|
71263
|
+
});
|
|
71264
|
+
return {
|
|
71265
|
+
kind: "copied",
|
|
71266
|
+
frame,
|
|
71267
|
+
reused: false
|
|
71268
|
+
};
|
|
71269
|
+
}
|
|
71270
|
+
/**
|
|
71079
71271
|
* The operator's "every subject in this frame is boxed" (D785 context: an
|
|
71080
71272
|
* unboxed subject is a false positive to a detector evaluation). Only the
|
|
71081
71273
|
* operator sets it; nothing derives it from the annotation count, because a
|
|
@@ -71108,7 +71300,7 @@ var RetrainFrameStore = class {
|
|
|
71108
71300
|
try {
|
|
71109
71301
|
await this.storage.delete({
|
|
71110
71302
|
location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
|
|
71111
|
-
relativePath: framePath(frame
|
|
71303
|
+
relativePath: framePath(frame)
|
|
71112
71304
|
});
|
|
71113
71305
|
} catch {}
|
|
71114
71306
|
try {
|
|
@@ -71155,7 +71347,7 @@ var RetrainFrameStore = class {
|
|
|
71155
71347
|
if (rowIsDurableRetrainCopy(data)) continue;
|
|
71156
71348
|
const frame = this.rowToFrame(row.id, data);
|
|
71157
71349
|
const from = frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION;
|
|
71158
|
-
const relativePath = framePath(frame
|
|
71350
|
+
const relativePath = framePath(frame);
|
|
71159
71351
|
let bytes;
|
|
71160
71352
|
try {
|
|
71161
71353
|
bytes = await this.storage.read({
|
|
@@ -82322,6 +82514,17 @@ var RETRAIN_ANNOTATION_INDEXES = [
|
|
|
82322
82514
|
columns: ["deviceId", "createdAt"]
|
|
82323
82515
|
}
|
|
82324
82516
|
];
|
|
82517
|
+
var ANNOTATION_SOURCES = [
|
|
82518
|
+
"operator",
|
|
82519
|
+
"assist",
|
|
82520
|
+
"pipeline",
|
|
82521
|
+
"llm"
|
|
82522
|
+
];
|
|
82523
|
+
/** The stored source, narrowed. Unknown reads as `operator` — the least
|
|
82524
|
+
* presumptuous claim a row can make about who drew it. */
|
|
82525
|
+
function narrowSource(value) {
|
|
82526
|
+
return ANNOTATION_SOURCES.find((source) => source === value) ?? "operator";
|
|
82527
|
+
}
|
|
82325
82528
|
function optionalString(value) {
|
|
82326
82529
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
82327
82530
|
}
|
|
@@ -82329,6 +82532,8 @@ function optionalString(value) {
|
|
|
82329
82532
|
function narrowMacroClass(value) {
|
|
82330
82533
|
return typeof value === "string" && isRetrainMacroClass(value) ? value : "person";
|
|
82331
82534
|
}
|
|
82535
|
+
/** Frame ids per `IN (…)` page in {@link RetrainAnnotationStore.listByFrames}. */
|
|
82536
|
+
var LIST_BY_FRAMES_PAGE = 500;
|
|
82332
82537
|
var RetrainAnnotationStore = class {
|
|
82333
82538
|
store;
|
|
82334
82539
|
logger;
|
|
@@ -82365,7 +82570,7 @@ var RetrainAnnotationStore = class {
|
|
|
82365
82570
|
label: optionalString(data["label"]),
|
|
82366
82571
|
subLabel: optionalString(data["subLabel"]),
|
|
82367
82572
|
kind: data["kind"] === "model_error" ? "model_error" : "subject",
|
|
82368
|
-
source: data["source"]
|
|
82573
|
+
source: narrowSource(data["source"]),
|
|
82369
82574
|
assistModelId: optionalString(data["assistModelId"]),
|
|
82370
82575
|
assistScore: typeof score === "number" ? score : void 0,
|
|
82371
82576
|
exportedInBatch: optionalString(data["exportedInBatch"]),
|
|
@@ -82434,6 +82639,32 @@ var RetrainAnnotationStore = class {
|
|
|
82434
82639
|
async listByFrame(mediaKey) {
|
|
82435
82640
|
return this.queryWhere({ mediaKey }, `frame ${mediaKey}`);
|
|
82436
82641
|
}
|
|
82642
|
+
/** Every annotation of these frames, oldest first — paged `IN (…)`. */
|
|
82643
|
+
async listByFrames(mediaKeys) {
|
|
82644
|
+
const out = [];
|
|
82645
|
+
for (let i = 0; i < mediaKeys.length; i += LIST_BY_FRAMES_PAGE) {
|
|
82646
|
+
const page = mediaKeys.slice(i, i + LIST_BY_FRAMES_PAGE);
|
|
82647
|
+
try {
|
|
82648
|
+
const rows = await this.store.query.query({
|
|
82649
|
+
collection: RETRAIN_ANNOTATIONS_COLLECTION,
|
|
82650
|
+
filter: {
|
|
82651
|
+
whereIn: { mediaKey: [...page] },
|
|
82652
|
+
orderBy: {
|
|
82653
|
+
field: "createdAt",
|
|
82654
|
+
direction: "asc"
|
|
82655
|
+
}
|
|
82656
|
+
}
|
|
82657
|
+
});
|
|
82658
|
+
for (const r of rows) if (typeof r.id === "string") out.push(this.rowToAnnotation(r.id, r.data));
|
|
82659
|
+
} catch (err) {
|
|
82660
|
+
this.logger.warn("RetrainAnnotationStore.listByFrames failed", { meta: {
|
|
82661
|
+
frames: page.length,
|
|
82662
|
+
error: String(err)
|
|
82663
|
+
} });
|
|
82664
|
+
}
|
|
82665
|
+
}
|
|
82666
|
+
return out;
|
|
82667
|
+
}
|
|
82437
82668
|
async listByTrack(trackId) {
|
|
82438
82669
|
return this.queryWhere({ trackId }, `track ${trackId}`);
|
|
82439
82670
|
}
|
|
@@ -82547,13 +82778,41 @@ var RetrainAnnotationStore = class {
|
|
|
82547
82778
|
//#endregion
|
|
82548
82779
|
//#region src/pipeline-analytics/retrain/retrain-collection-store.ts
|
|
82549
82780
|
/**
|
|
82781
|
+
* RetrainCollectionStore — named sets over the one retrain corpus.
|
|
82782
|
+
*
|
|
82783
|
+
* Collections: `pipeline-analytics:retrain-collections` (the names) and
|
|
82784
|
+
* `pipeline-analytics:retrain-collection-frames` (the membership).
|
|
82785
|
+
*
|
|
82786
|
+
* A collection OWNS NO PIXELS. Every frame — copied from a track or stilled
|
|
82787
|
+
* from the recording — lives once in `retrain-frames`, with its annotations,
|
|
82788
|
+
* its completeness and its export path; a collection is a name the operator
|
|
82789
|
+
* gives to a subset of it ("packages porch night", "false-positive cars"). So
|
|
82790
|
+
* a frame can be in several collections, a collection's count is its
|
|
82791
|
+
* membership, and deleting a collection removes memberships, never a frame
|
|
82792
|
+
* another collection or a track still holds. Which trackless frames are left
|
|
82793
|
+
* with no reason to exist is the caller's question
|
|
82794
|
+
* (`retrain-collection-service.ts`).
|
|
82795
|
+
*/
|
|
82796
|
+
/**
|
|
82550
82797
|
* @durable class=ledger owner=pipeline-analytics
|
|
82551
82798
|
* write="one row per named training collection the operator creates. Names are
|
|
82552
|
-
* unique on nameKey (lower(trim)); empty names never become rows.
|
|
82553
|
-
*
|
|
82554
|
-
*
|
|
82799
|
+
* unique on nameKey (lower(trim)); empty names never become rows. A rename
|
|
82800
|
+
* rewrites name + nameKey under the same id."
|
|
82801
|
+
* retention="none — operator-owned corpus metadata. A row leaves only when the
|
|
82802
|
+
* operator deletes the collection (deleteRetrainCollection), which removes its
|
|
82803
|
+
* membership rows first."
|
|
82555
82804
|
*/
|
|
82556
82805
|
var RETRAIN_COLLECTIONS_COLLECTION = "pipeline-analytics:retrain-collections";
|
|
82806
|
+
/**
|
|
82807
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
82808
|
+
* write="one row per (collection, frame) pair the operator adds — a frame added
|
|
82809
|
+
* from the recording, or an existing retrain frame added to a collection. Keyed
|
|
82810
|
+
* by a hash of the pair, so adding twice is one row."
|
|
82811
|
+
* retention="none — rows leave when the operator removes the frame from the
|
|
82812
|
+
* collection, deletes the collection, or deselects the frame itself. Bounded by
|
|
82813
|
+
* operator curation."
|
|
82814
|
+
*/
|
|
82815
|
+
var RETRAIN_COLLECTION_FRAMES_COLLECTION = "pipeline-analytics:retrain-collection-frames";
|
|
82557
82816
|
var RETRAIN_COLLECTION_COLUMNS = [
|
|
82558
82817
|
{
|
|
82559
82818
|
name: "id",
|
|
@@ -82590,6 +82849,41 @@ var RETRAIN_COLLECTION_INDEXES = [{
|
|
|
82590
82849
|
name: "idx_retrain_collections_created",
|
|
82591
82850
|
columns: ["createdAt"]
|
|
82592
82851
|
}];
|
|
82852
|
+
var RETRAIN_COLLECTION_FRAME_COLUMNS = [
|
|
82853
|
+
{
|
|
82854
|
+
name: "id",
|
|
82855
|
+
type: "TEXT",
|
|
82856
|
+
primaryKey: true,
|
|
82857
|
+
notNull: true
|
|
82858
|
+
},
|
|
82859
|
+
{
|
|
82860
|
+
name: "collectionId",
|
|
82861
|
+
type: "TEXT",
|
|
82862
|
+
notNull: true
|
|
82863
|
+
},
|
|
82864
|
+
{
|
|
82865
|
+
name: "frameId",
|
|
82866
|
+
type: "TEXT",
|
|
82867
|
+
notNull: true
|
|
82868
|
+
},
|
|
82869
|
+
{
|
|
82870
|
+
name: "deviceId",
|
|
82871
|
+
type: "INTEGER",
|
|
82872
|
+
notNull: true
|
|
82873
|
+
},
|
|
82874
|
+
{
|
|
82875
|
+
name: "addedAt",
|
|
82876
|
+
type: "INTEGER",
|
|
82877
|
+
notNull: true
|
|
82878
|
+
}
|
|
82879
|
+
];
|
|
82880
|
+
var RETRAIN_COLLECTION_FRAME_INDEXES = [{
|
|
82881
|
+
name: "idx_retrain_coll_frames_collection",
|
|
82882
|
+
columns: ["collectionId", "addedAt"]
|
|
82883
|
+
}, {
|
|
82884
|
+
name: "idx_retrain_coll_frames_frame",
|
|
82885
|
+
columns: ["frameId"]
|
|
82886
|
+
}];
|
|
82593
82887
|
function normalizeCollectionName(raw) {
|
|
82594
82888
|
const name = raw.trim();
|
|
82595
82889
|
if (name.length === 0) return null;
|
|
@@ -82598,18 +82892,22 @@ function normalizeCollectionName(raw) {
|
|
|
82598
82892
|
nameKey: name.toLowerCase()
|
|
82599
82893
|
};
|
|
82600
82894
|
}
|
|
82895
|
+
/** The membership row's key — one row per pair, whatever the ids contain. */
|
|
82896
|
+
function membershipKey(collectionId, frameId) {
|
|
82897
|
+
return (0, node_crypto.createHash)("sha1").update(`${collectionId}\u0000${frameId}`).digest("hex");
|
|
82898
|
+
}
|
|
82899
|
+
/** Ids per `IN (…)` page. */
|
|
82900
|
+
var IN_PAGE = 500;
|
|
82601
82901
|
var RetrainCollectionStore = class {
|
|
82602
82902
|
store;
|
|
82603
82903
|
logger;
|
|
82604
82904
|
now;
|
|
82605
82905
|
newId;
|
|
82606
|
-
countFrames;
|
|
82607
82906
|
constructor(deps) {
|
|
82608
82907
|
this.store = deps.store;
|
|
82609
82908
|
this.logger = deps.logger;
|
|
82610
82909
|
this.now = deps.now;
|
|
82611
82910
|
this.newId = deps.newId;
|
|
82612
|
-
this.countFrames = deps.countFrames;
|
|
82613
82911
|
}
|
|
82614
82912
|
static async declare(store) {
|
|
82615
82913
|
await store.declareCollection.mutate({
|
|
@@ -82617,6 +82915,11 @@ var RetrainCollectionStore = class {
|
|
|
82617
82915
|
columns: [...RETRAIN_COLLECTION_COLUMNS],
|
|
82618
82916
|
indexes: [...RETRAIN_COLLECTION_INDEXES]
|
|
82619
82917
|
});
|
|
82918
|
+
await store.declareCollection.mutate({
|
|
82919
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82920
|
+
columns: [...RETRAIN_COLLECTION_FRAME_COLUMNS],
|
|
82921
|
+
indexes: [...RETRAIN_COLLECTION_FRAME_INDEXES]
|
|
82922
|
+
});
|
|
82620
82923
|
}
|
|
82621
82924
|
async create(input) {
|
|
82622
82925
|
const normalized = normalizeCollectionName(input.name);
|
|
@@ -82624,7 +82927,7 @@ var RetrainCollectionStore = class {
|
|
|
82624
82927
|
kind: "refused",
|
|
82625
82928
|
reason: "empty-name"
|
|
82626
82929
|
};
|
|
82627
|
-
if (await this.
|
|
82930
|
+
if (await this.findIdByNameKey(normalized.nameKey) !== null) return {
|
|
82628
82931
|
kind: "refused",
|
|
82629
82932
|
reason: "duplicate-name"
|
|
82630
82933
|
};
|
|
@@ -82640,6 +82943,7 @@ var RetrainCollectionStore = class {
|
|
|
82640
82943
|
updatedAt: createdAt
|
|
82641
82944
|
}
|
|
82642
82945
|
});
|
|
82946
|
+
this.logger.info("retrain collection created", { meta: { collectionId: id } });
|
|
82643
82947
|
return {
|
|
82644
82948
|
kind: "created",
|
|
82645
82949
|
collection: {
|
|
@@ -82650,6 +82954,7 @@ var RetrainCollectionStore = class {
|
|
|
82650
82954
|
}
|
|
82651
82955
|
};
|
|
82652
82956
|
}
|
|
82957
|
+
/** Every collection, newest first, each with its membership count (one query). */
|
|
82653
82958
|
async list() {
|
|
82654
82959
|
let rows;
|
|
82655
82960
|
try {
|
|
@@ -82664,34 +82969,158 @@ var RetrainCollectionStore = class {
|
|
|
82664
82969
|
this.logger.warn("retrain collections list failed", { meta: { error: String(err) } });
|
|
82665
82970
|
return [];
|
|
82666
82971
|
}
|
|
82667
|
-
const
|
|
82668
|
-
|
|
82669
|
-
|
|
82670
|
-
|
|
82671
|
-
|
|
82672
|
-
|
|
82673
|
-
|
|
82674
|
-
|
|
82675
|
-
|
|
82676
|
-
|
|
82677
|
-
|
|
82972
|
+
const counts = await this.countMembers(rows.map((r) => r.id));
|
|
82973
|
+
return rows.map((row) => this.toCollection(row.id, row.data, counts.get(row.id) ?? 0));
|
|
82974
|
+
}
|
|
82975
|
+
async getById(id) {
|
|
82976
|
+
const data = await this.readRow(id);
|
|
82977
|
+
if (data === null) return null;
|
|
82978
|
+
const counts = await this.countMembers([id]);
|
|
82979
|
+
return this.toCollection(id, data, counts.get(id) ?? 0);
|
|
82980
|
+
}
|
|
82981
|
+
async rename(input) {
|
|
82982
|
+
const normalized = normalizeCollectionName(input.name);
|
|
82983
|
+
if (normalized === null) return {
|
|
82984
|
+
kind: "refused",
|
|
82985
|
+
reason: "empty-name"
|
|
82986
|
+
};
|
|
82987
|
+
const current = await this.readRow(input.id);
|
|
82988
|
+
if (current === null) return {
|
|
82989
|
+
kind: "refused",
|
|
82990
|
+
reason: "unknown-collection"
|
|
82991
|
+
};
|
|
82992
|
+
const holder = await this.findIdByNameKey(normalized.nameKey);
|
|
82993
|
+
if (holder !== null && holder !== input.id) return {
|
|
82994
|
+
kind: "refused",
|
|
82995
|
+
reason: "duplicate-name"
|
|
82996
|
+
};
|
|
82997
|
+
await this.store.update.mutate({
|
|
82998
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
82999
|
+
id: input.id,
|
|
83000
|
+
data: {
|
|
83001
|
+
name: normalized.name,
|
|
83002
|
+
nameKey: normalized.nameKey,
|
|
83003
|
+
updatedAt: this.now()
|
|
83004
|
+
}
|
|
83005
|
+
});
|
|
83006
|
+
this.logger.info("retrain collection renamed", { meta: { collectionId: input.id } });
|
|
83007
|
+
const counts = await this.countMembers([input.id]);
|
|
83008
|
+
return {
|
|
83009
|
+
kind: "renamed",
|
|
83010
|
+
collection: this.toCollection(input.id, {
|
|
83011
|
+
...current,
|
|
83012
|
+
name: normalized.name
|
|
83013
|
+
}, counts.get(input.id) ?? 0)
|
|
83014
|
+
};
|
|
83015
|
+
}
|
|
83016
|
+
/** Remove the collection and its memberships — members first, so a crash
|
|
83017
|
+
* never leaves memberships pointing at a name that no longer exists. */
|
|
83018
|
+
async delete(id) {
|
|
83019
|
+
if (await this.readRow(id) === null) return {
|
|
83020
|
+
deleted: false,
|
|
83021
|
+
memberFrameIds: []
|
|
83022
|
+
};
|
|
83023
|
+
const members = await this.membershipRows({ collectionId: id });
|
|
83024
|
+
for (const m of members) await this.store.delete.mutate({
|
|
83025
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83026
|
+
key: m.id
|
|
83027
|
+
});
|
|
83028
|
+
await this.store.delete.mutate({
|
|
83029
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
83030
|
+
key: id
|
|
83031
|
+
});
|
|
83032
|
+
this.logger.info("retrain collection deleted", { meta: {
|
|
83033
|
+
collectionId: id,
|
|
83034
|
+
members: members.length
|
|
83035
|
+
} });
|
|
83036
|
+
return {
|
|
83037
|
+
deleted: true,
|
|
83038
|
+
memberFrameIds: members.map((m) => String(m.data["frameId"] ?? ""))
|
|
83039
|
+
};
|
|
83040
|
+
}
|
|
83041
|
+
/** Add a frame. `false` when it was already a member (nothing written). */
|
|
83042
|
+
async addFrame(input) {
|
|
83043
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
83044
|
+
const existing = await this.store.get.query({
|
|
83045
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83046
|
+
key
|
|
83047
|
+
});
|
|
83048
|
+
if (existing !== null && existing !== void 0) return false;
|
|
83049
|
+
await this.store.set.mutate({
|
|
83050
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83051
|
+
key,
|
|
83052
|
+
value: {
|
|
83053
|
+
collectionId: input.collectionId,
|
|
83054
|
+
frameId: input.frameId,
|
|
83055
|
+
deviceId: input.deviceId,
|
|
83056
|
+
addedAt: this.now()
|
|
83057
|
+
}
|
|
83058
|
+
});
|
|
83059
|
+
return true;
|
|
83060
|
+
}
|
|
83061
|
+
/** Remove one membership. `false` when it did not exist. */
|
|
83062
|
+
async removeFrame(input) {
|
|
83063
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
83064
|
+
const existing = await this.store.get.query({
|
|
83065
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83066
|
+
key
|
|
83067
|
+
});
|
|
83068
|
+
if (existing === null || existing === void 0) return false;
|
|
83069
|
+
await this.store.delete.mutate({
|
|
83070
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83071
|
+
key
|
|
83072
|
+
});
|
|
83073
|
+
return true;
|
|
83074
|
+
}
|
|
83075
|
+
/** Remove a frame from EVERY collection — the frame itself is leaving. */
|
|
83076
|
+
async forgetFrame(frameId) {
|
|
83077
|
+
const rows = await this.membershipRows({ frameId });
|
|
83078
|
+
for (const r of rows) await this.store.delete.mutate({
|
|
83079
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83080
|
+
key: r.id
|
|
83081
|
+
});
|
|
83082
|
+
return rows.length;
|
|
83083
|
+
}
|
|
83084
|
+
/** The frames of a collection, in the order they were added. */
|
|
83085
|
+
async listFrameIds(collectionId) {
|
|
83086
|
+
return (await this.membershipRows({ collectionId })).toSorted((a, b) => Number(a.data["addedAt"] ?? 0) - Number(b.data["addedAt"] ?? 0)).map((r) => String(r.data["frameId"] ?? "")).filter((id) => id !== "");
|
|
83087
|
+
}
|
|
83088
|
+
/** frameId → the collections it belongs to. Frames in none are absent. */
|
|
83089
|
+
async collectionsOf(frameIds) {
|
|
83090
|
+
const out = /* @__PURE__ */ new Map();
|
|
83091
|
+
for (const row of await this.membershipRowsIn("frameId", frameIds)) {
|
|
83092
|
+
const frameId = String(row.data["frameId"] ?? "");
|
|
83093
|
+
const collectionId = String(row.data["collectionId"] ?? "");
|
|
83094
|
+
if (frameId === "" || collectionId === "") continue;
|
|
83095
|
+
out.set(frameId, [...out.get(frameId) ?? [], collectionId]);
|
|
82678
83096
|
}
|
|
83097
|
+
for (const [frameId, ids] of out) out.set(frameId, ids.toSorted());
|
|
82679
83098
|
return out;
|
|
82680
83099
|
}
|
|
82681
|
-
|
|
83100
|
+
/** The frame ids of several collections, de-duplicated. */
|
|
83101
|
+
async frameIdsOf(collectionIds) {
|
|
83102
|
+
const ids = /* @__PURE__ */ new Set();
|
|
83103
|
+
for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
|
|
83104
|
+
const frameId = String(row.data["frameId"] ?? "");
|
|
83105
|
+
if (frameId !== "") ids.add(frameId);
|
|
83106
|
+
}
|
|
83107
|
+
return [...ids];
|
|
83108
|
+
}
|
|
83109
|
+
toCollection(id, data, frameCount) {
|
|
83110
|
+
return {
|
|
83111
|
+
id,
|
|
83112
|
+
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
83113
|
+
createdAt: Number(data["createdAt"] ?? 0),
|
|
83114
|
+
frameCount
|
|
83115
|
+
};
|
|
83116
|
+
}
|
|
83117
|
+
async readRow(id) {
|
|
82682
83118
|
try {
|
|
82683
83119
|
const row = await this.store.get.query({
|
|
82684
83120
|
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
82685
83121
|
key: id
|
|
82686
83122
|
});
|
|
82687
|
-
|
|
82688
|
-
const data = row;
|
|
82689
|
-
return {
|
|
82690
|
-
id,
|
|
82691
|
-
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
82692
|
-
createdAt: Number(data["createdAt"] ?? 0),
|
|
82693
|
-
frameCount: this.countFrames !== void 0 ? await this.countFrames(id) : 0
|
|
82694
|
-
};
|
|
83123
|
+
return row === null || row === void 0 ? null : { ...row };
|
|
82695
83124
|
} catch (err) {
|
|
82696
83125
|
this.logger.warn("retrain collection get failed", { meta: {
|
|
82697
83126
|
collectionId: id,
|
|
@@ -82700,14 +83129,125 @@ var RetrainCollectionStore = class {
|
|
|
82700
83129
|
return null;
|
|
82701
83130
|
}
|
|
82702
83131
|
}
|
|
83132
|
+
async countMembers(collectionIds) {
|
|
83133
|
+
const counts = /* @__PURE__ */ new Map();
|
|
83134
|
+
for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
|
|
83135
|
+
const id = String(row.data["collectionId"] ?? "");
|
|
83136
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
83137
|
+
}
|
|
83138
|
+
return counts;
|
|
83139
|
+
}
|
|
83140
|
+
async membershipRows(where) {
|
|
83141
|
+
try {
|
|
83142
|
+
return await this.store.query.query({
|
|
83143
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83144
|
+
filter: { where }
|
|
83145
|
+
});
|
|
83146
|
+
} catch (err) {
|
|
83147
|
+
this.logger.warn("retrain collection membership read failed", { meta: {
|
|
83148
|
+
...where,
|
|
83149
|
+
error: String(err)
|
|
83150
|
+
} });
|
|
83151
|
+
return [];
|
|
83152
|
+
}
|
|
83153
|
+
}
|
|
83154
|
+
async membershipRowsIn(field, values) {
|
|
83155
|
+
const out = [];
|
|
83156
|
+
for (let i = 0; i < values.length; i += IN_PAGE) {
|
|
83157
|
+
const page = values.slice(i, i + IN_PAGE);
|
|
83158
|
+
try {
|
|
83159
|
+
out.push(...await this.store.query.query({
|
|
83160
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83161
|
+
filter: { whereIn: { [field]: [...page] } }
|
|
83162
|
+
}));
|
|
83163
|
+
} catch (err) {
|
|
83164
|
+
this.logger.warn("retrain collection membership read failed", { meta: {
|
|
83165
|
+
field,
|
|
83166
|
+
values: page.length,
|
|
83167
|
+
error: String(err)
|
|
83168
|
+
} });
|
|
83169
|
+
}
|
|
83170
|
+
}
|
|
83171
|
+
return out;
|
|
83172
|
+
}
|
|
83173
|
+
async findIdByNameKey(nameKey) {
|
|
83174
|
+
try {
|
|
83175
|
+
return (await this.store.query.query({
|
|
83176
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
83177
|
+
filter: {
|
|
83178
|
+
where: { nameKey },
|
|
83179
|
+
limit: 1
|
|
83180
|
+
}
|
|
83181
|
+
}))[0]?.id ?? null;
|
|
83182
|
+
} catch (err) {
|
|
83183
|
+
this.logger.warn("retrain collection findByNameKey failed", { meta: {
|
|
83184
|
+
nameKey,
|
|
83185
|
+
error: String(err)
|
|
83186
|
+
} });
|
|
83187
|
+
return null;
|
|
83188
|
+
}
|
|
83189
|
+
}
|
|
83190
|
+
};
|
|
83191
|
+
//#endregion
|
|
83192
|
+
//#region src/pipeline-analytics/retrain/retrain-collection-service.ts
|
|
83193
|
+
/** What each still outcome tells the operator. */
|
|
83194
|
+
var STILL_REFUSAL = {
|
|
83195
|
+
"no-recording": {
|
|
83196
|
+
reason: "still-unavailable",
|
|
83197
|
+
detail: "this camera is not recording, so there is no footage to take a frame from"
|
|
83198
|
+
},
|
|
83199
|
+
"no-footage": {
|
|
83200
|
+
reason: "still-unavailable",
|
|
83201
|
+
detail: "no recorded footage covers that instant (a gap, or already pruned)"
|
|
83202
|
+
},
|
|
83203
|
+
"decode-failed": {
|
|
83204
|
+
reason: "still-unavailable",
|
|
83205
|
+
detail: "the footage is there but the frame could not be decoded"
|
|
83206
|
+
},
|
|
83207
|
+
busy: {
|
|
83208
|
+
reason: "still-busy",
|
|
83209
|
+
detail: "the recorder is already decoding other frames — try again in a moment"
|
|
83210
|
+
}
|
|
83211
|
+
};
|
|
83212
|
+
var RetrainCollectionService = class {
|
|
83213
|
+
deps;
|
|
83214
|
+
constructor(deps) {
|
|
83215
|
+
this.deps = deps;
|
|
83216
|
+
}
|
|
83217
|
+
create(input) {
|
|
83218
|
+
return this.deps.collections.create(input);
|
|
83219
|
+
}
|
|
83220
|
+
list() {
|
|
83221
|
+
return this.deps.collections.list();
|
|
83222
|
+
}
|
|
83223
|
+
rename(input) {
|
|
83224
|
+
return this.deps.collections.rename(input);
|
|
83225
|
+
}
|
|
82703
83226
|
/**
|
|
82704
|
-
*
|
|
82705
|
-
*
|
|
83227
|
+
* Delete a collection. Its recording stills that no other collection holds
|
|
83228
|
+
* go with it (annotations first); track copies always stay.
|
|
82706
83229
|
*/
|
|
82707
|
-
async
|
|
82708
|
-
|
|
82709
|
-
|
|
82710
|
-
|
|
83230
|
+
async delete(input) {
|
|
83231
|
+
const result = await this.deps.collections.delete(input.id);
|
|
83232
|
+
if (!result.deleted) return {
|
|
83233
|
+
deleted: false,
|
|
83234
|
+
removedFrames: 0
|
|
83235
|
+
};
|
|
83236
|
+
return {
|
|
83237
|
+
deleted: true,
|
|
83238
|
+
removedFrames: await this.dropOrphanStills(result.memberFrameIds)
|
|
83239
|
+
};
|
|
83240
|
+
}
|
|
83241
|
+
/**
|
|
83242
|
+
* Decode the frame at `epochMs` from the camera's recording and add it to
|
|
83243
|
+
* the collection. The moment already in the dataset is reused without a
|
|
83244
|
+
* decode; every refusal is named and writes nothing.
|
|
83245
|
+
*/
|
|
83246
|
+
async addFrameFromRecording(input) {
|
|
83247
|
+
const tags = { deviceId: input.deviceId };
|
|
83248
|
+
if (await this.deps.collections.getById(input.collectionId) === null) {
|
|
83249
|
+
this.deps.logger.info("retrain collection add refused: unknown collection", {
|
|
83250
|
+
tags,
|
|
82711
83251
|
meta: {
|
|
82712
83252
|
collectionId: input.collectionId,
|
|
82713
83253
|
epochMs: input.epochMs
|
|
@@ -82718,42 +83258,517 @@ var RetrainCollectionStore = class {
|
|
|
82718
83258
|
reason: "unknown-collection"
|
|
82719
83259
|
};
|
|
82720
83260
|
}
|
|
82721
|
-
|
|
82722
|
-
|
|
83261
|
+
const stored = await this.storeStill(input.deviceId, input.epochMs);
|
|
83262
|
+
if (stored.kind === "refused") {
|
|
83263
|
+
this.deps.logger.info("retrain collection add refused", {
|
|
83264
|
+
tags,
|
|
83265
|
+
meta: {
|
|
83266
|
+
collectionId: input.collectionId,
|
|
83267
|
+
epochMs: input.epochMs,
|
|
83268
|
+
reason: stored.reason
|
|
83269
|
+
}
|
|
83270
|
+
});
|
|
83271
|
+
return stored;
|
|
83272
|
+
}
|
|
83273
|
+
await this.deps.collections.addFrame({
|
|
83274
|
+
collectionId: input.collectionId,
|
|
83275
|
+
frameId: stored.frame.frameId,
|
|
83276
|
+
deviceId: input.deviceId
|
|
83277
|
+
});
|
|
83278
|
+
this.deps.logger.info("retrain frame added to a collection from the recording", {
|
|
83279
|
+
tags,
|
|
82723
83280
|
meta: {
|
|
82724
83281
|
collectionId: input.collectionId,
|
|
82725
|
-
|
|
83282
|
+
frameId: stored.frame.frameId,
|
|
83283
|
+
epochMs: input.epochMs,
|
|
83284
|
+
reused: stored.reused
|
|
82726
83285
|
}
|
|
82727
83286
|
});
|
|
83287
|
+
const [wire] = await this.deps.retrain.toWireFrames([stored.frame]);
|
|
83288
|
+
if (wire === void 0) throw new Error("retrain: a stored still produced no wire frame");
|
|
83289
|
+
return {
|
|
83290
|
+
kind: "added",
|
|
83291
|
+
collectionId: input.collectionId,
|
|
83292
|
+
frame: wire,
|
|
83293
|
+
reused: stored.reused
|
|
83294
|
+
};
|
|
83295
|
+
}
|
|
83296
|
+
/** Add frames already in the dataset (track copies or stills) to a collection. */
|
|
83297
|
+
async addFrames(input) {
|
|
83298
|
+
if (await this.deps.collections.getById(input.collectionId) === null) return {
|
|
83299
|
+
added: 0,
|
|
83300
|
+
alreadyMember: 0,
|
|
83301
|
+
unknownFrameIds: [...input.frameIds]
|
|
83302
|
+
};
|
|
83303
|
+
const frames = await this.deps.frames.getMany(input.frameIds);
|
|
83304
|
+
const known = new Set(frames.map((f) => f.frameId));
|
|
83305
|
+
let added = 0;
|
|
83306
|
+
let alreadyMember = 0;
|
|
83307
|
+
for (const frame of frames) if (await this.deps.collections.addFrame({
|
|
83308
|
+
collectionId: input.collectionId,
|
|
83309
|
+
frameId: frame.frameId,
|
|
83310
|
+
deviceId: frame.deviceId
|
|
83311
|
+
})) added += 1;
|
|
83312
|
+
else alreadyMember += 1;
|
|
83313
|
+
return {
|
|
83314
|
+
added,
|
|
83315
|
+
alreadyMember,
|
|
83316
|
+
unknownFrameIds: input.frameIds.filter((id) => !known.has(id))
|
|
83317
|
+
};
|
|
83318
|
+
}
|
|
83319
|
+
/**
|
|
83320
|
+
* Remove a frame from a collection. A recording still left in no collection
|
|
83321
|
+
* is deleted with its annotations; a track copy is never deleted here.
|
|
83322
|
+
*/
|
|
83323
|
+
async removeFrame(input) {
|
|
83324
|
+
if (!await this.deps.collections.removeFrame(input)) return {
|
|
83325
|
+
removed: false,
|
|
83326
|
+
deletedFrame: false
|
|
83327
|
+
};
|
|
82728
83328
|
return {
|
|
83329
|
+
removed: true,
|
|
83330
|
+
deletedFrame: await this.dropOrphanStills([input.frameId]) > 0
|
|
83331
|
+
};
|
|
83332
|
+
}
|
|
83333
|
+
/** The frames of a collection, as the canvas lists them. */
|
|
83334
|
+
async listFrames(input) {
|
|
83335
|
+
const ids = await this.deps.collections.listFrameIds(input.collectionId);
|
|
83336
|
+
return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
|
|
83337
|
+
}
|
|
83338
|
+
async storeStill(deviceId, epochMs) {
|
|
83339
|
+
const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
|
|
83340
|
+
if (existing !== null) return {
|
|
83341
|
+
kind: "stored",
|
|
83342
|
+
frame: existing,
|
|
83343
|
+
reused: true
|
|
83344
|
+
};
|
|
83345
|
+
const still = await this.deps.captureStill(deviceId, epochMs);
|
|
83346
|
+
if (still.outcome !== "ok" || still.jpeg === void 0) {
|
|
83347
|
+
const refusal = still.outcome === "ok" ? STILL_REFUSAL["decode-failed"] : STILL_REFUSAL[still.outcome];
|
|
83348
|
+
return {
|
|
83349
|
+
kind: "refused",
|
|
83350
|
+
reason: refusal.reason,
|
|
83351
|
+
detail: refusal.detail
|
|
83352
|
+
};
|
|
83353
|
+
}
|
|
83354
|
+
const stored = await this.deps.frames.storeRecordingStill({
|
|
83355
|
+
deviceId,
|
|
83356
|
+
epochMs,
|
|
83357
|
+
bytes: still.jpeg
|
|
83358
|
+
});
|
|
83359
|
+
return stored.kind === "copied" ? {
|
|
83360
|
+
kind: "stored",
|
|
83361
|
+
frame: stored.frame,
|
|
83362
|
+
reused: stored.reused
|
|
83363
|
+
} : {
|
|
82729
83364
|
kind: "refused",
|
|
82730
|
-
reason:
|
|
82731
|
-
detail: "Still ingest lands in plan Task 3"
|
|
83365
|
+
reason: stored.reason
|
|
82732
83366
|
};
|
|
82733
83367
|
}
|
|
82734
|
-
|
|
82735
|
-
|
|
82736
|
-
|
|
82737
|
-
|
|
82738
|
-
|
|
82739
|
-
|
|
82740
|
-
|
|
83368
|
+
/** Delete the recording stills among `frameIds` that no collection holds any more. */
|
|
83369
|
+
async dropOrphanStills(frameIds) {
|
|
83370
|
+
if (frameIds.length === 0) return 0;
|
|
83371
|
+
const memberships = await this.deps.collections.collectionsOf(frameIds);
|
|
83372
|
+
const orphans = (await this.deps.frames.getMany(frameIds)).filter((f) => f.origin === "recording" && (memberships.get(f.frameId)?.length ?? 0) === 0);
|
|
83373
|
+
for (const frame of orphans) await this.deps.retrain.removeFrame(frame);
|
|
83374
|
+
return orphans.length;
|
|
83375
|
+
}
|
|
83376
|
+
};
|
|
83377
|
+
//#endregion
|
|
83378
|
+
//#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
|
|
83379
|
+
var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
|
|
83380
|
+
var ALWAYS_CLOUD_KINDS = new Set([
|
|
83381
|
+
"openai",
|
|
83382
|
+
"anthropic",
|
|
83383
|
+
"google"
|
|
83384
|
+
]);
|
|
83385
|
+
function isPrivateIpv4(host) {
|
|
83386
|
+
const parts = host.split(".").map((p) => Number.parseInt(p, 10));
|
|
83387
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return false;
|
|
83388
|
+
const [a, b] = parts;
|
|
83389
|
+
return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127;
|
|
83390
|
+
}
|
|
83391
|
+
function isLocalHost(rawHost) {
|
|
83392
|
+
const host = rawHost.replace(/^\[|\]$/g, "").toLowerCase();
|
|
83393
|
+
if (host.includes(":")) return host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80");
|
|
83394
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".lan")) return true;
|
|
83395
|
+
if (isPrivateIpv4(host)) return true;
|
|
83396
|
+
return !host.includes(".") && !host.includes(":");
|
|
83397
|
+
}
|
|
83398
|
+
/** Does sending a frame to this profile send it outside the house? */
|
|
83399
|
+
function llmProfileLeavesHouse(profile) {
|
|
83400
|
+
if (ALWAYS_LOCAL_KINDS.has(profile.kind)) return false;
|
|
83401
|
+
if (ALWAYS_CLOUD_KINDS.has(profile.kind)) return true;
|
|
83402
|
+
if (profile.baseUrl === void 0 || profile.baseUrl === "") return true;
|
|
83403
|
+
try {
|
|
83404
|
+
return !isLocalHost(new URL(profile.baseUrl).hostname);
|
|
83405
|
+
} catch {
|
|
83406
|
+
return true;
|
|
83407
|
+
}
|
|
83408
|
+
}
|
|
83409
|
+
/** At most this many boxes are sent for review — a prompt, not a dump. */
|
|
83410
|
+
var LLM_REVIEW_MAX_BOXES = 50;
|
|
83411
|
+
var VERDICTS = [
|
|
83412
|
+
"confirmed",
|
|
83413
|
+
"wrong-class",
|
|
83414
|
+
"not-an-object",
|
|
83415
|
+
"uncertain"
|
|
83416
|
+
];
|
|
83417
|
+
var SYSTEM_PROMPT = [
|
|
83418
|
+
"You annotate frames from home security cameras to build an object-detection training set.",
|
|
83419
|
+
"Be precise and conservative: only report what is clearly visible.",
|
|
83420
|
+
"Coordinates are normalised to the full frame: x and y are the top-left corner,",
|
|
83421
|
+
"w and h the width and height, all between 0 and 1.",
|
|
83422
|
+
"Answer with JSON only, exactly in the requested schema, with no other text."
|
|
83423
|
+
].join(" ");
|
|
83424
|
+
/** The JSON schema asked of the model (and enforced again by the parser). */
|
|
83425
|
+
function answerJsonSchema(classes) {
|
|
83426
|
+
return {
|
|
83427
|
+
type: "object",
|
|
83428
|
+
additionalProperties: false,
|
|
83429
|
+
required: ["objects", "reviews"],
|
|
83430
|
+
properties: {
|
|
83431
|
+
objects: {
|
|
83432
|
+
type: "array",
|
|
83433
|
+
items: {
|
|
83434
|
+
type: "object",
|
|
83435
|
+
additionalProperties: false,
|
|
83436
|
+
required: [
|
|
83437
|
+
"class",
|
|
83438
|
+
"box",
|
|
83439
|
+
"confidence",
|
|
83440
|
+
"reason"
|
|
83441
|
+
],
|
|
83442
|
+
properties: {
|
|
83443
|
+
class: {
|
|
83444
|
+
type: "string",
|
|
83445
|
+
enum: [...classes]
|
|
83446
|
+
},
|
|
83447
|
+
label: { type: "string" },
|
|
83448
|
+
box: {
|
|
83449
|
+
type: "array",
|
|
83450
|
+
items: { type: "number" },
|
|
83451
|
+
minItems: 4,
|
|
83452
|
+
maxItems: 4
|
|
83453
|
+
},
|
|
83454
|
+
confidence: {
|
|
83455
|
+
type: "number",
|
|
83456
|
+
minimum: 0,
|
|
83457
|
+
maximum: 1
|
|
83458
|
+
},
|
|
83459
|
+
reason: { type: "string" }
|
|
83460
|
+
}
|
|
82741
83461
|
}
|
|
82742
|
-
}
|
|
82743
|
-
|
|
83462
|
+
},
|
|
83463
|
+
reviews: {
|
|
83464
|
+
type: "array",
|
|
83465
|
+
items: {
|
|
83466
|
+
type: "object",
|
|
83467
|
+
additionalProperties: false,
|
|
83468
|
+
required: [
|
|
83469
|
+
"ref",
|
|
83470
|
+
"verdict",
|
|
83471
|
+
"reason"
|
|
83472
|
+
],
|
|
83473
|
+
properties: {
|
|
83474
|
+
ref: { type: "string" },
|
|
83475
|
+
verdict: {
|
|
83476
|
+
type: "string",
|
|
83477
|
+
enum: [...VERDICTS]
|
|
83478
|
+
},
|
|
83479
|
+
suggestedClass: { type: "string" },
|
|
83480
|
+
reason: { type: "string" }
|
|
83481
|
+
}
|
|
83482
|
+
}
|
|
83483
|
+
}
|
|
83484
|
+
}
|
|
83485
|
+
};
|
|
83486
|
+
}
|
|
83487
|
+
var round = (n) => Math.round(n * 1e3) / 1e3;
|
|
83488
|
+
/** Build the request: target classes, the frame size, the boxes to review. */
|
|
83489
|
+
function buildLlmAssistRequest(input) {
|
|
83490
|
+
const boxes = input.boxes.slice(0, LLM_REVIEW_MAX_BOXES).map((b) => JSON.stringify({
|
|
83491
|
+
ref: b.ref,
|
|
83492
|
+
class: b.macroClass,
|
|
83493
|
+
...b.label !== void 0 ? { label: b.label } : {},
|
|
83494
|
+
box: [
|
|
83495
|
+
round(b.bbox.x),
|
|
83496
|
+
round(b.bbox.y),
|
|
83497
|
+
round(b.bbox.w),
|
|
83498
|
+
round(b.bbox.h)
|
|
83499
|
+
]
|
|
83500
|
+
}));
|
|
83501
|
+
return {
|
|
83502
|
+
system: SYSTEM_PROMPT,
|
|
83503
|
+
prompt: [
|
|
83504
|
+
`The image is one camera frame, ${String(input.width)}×${String(input.height)} pixels.`,
|
|
83505
|
+
`1. List every object of these classes you can see: ${input.classes.join(", ")}.`,
|
|
83506
|
+
" For each give class, an optional free-text label (e.g. \"van\", \"postman\"),",
|
|
83507
|
+
" box [x, y, w, h] normalised to the frame, confidence 0–1, and a short reason.",
|
|
83508
|
+
" Include small and partly hidden ones; do not include anything else.",
|
|
83509
|
+
boxes.length > 0 ? `2. Review these existing boxes. For each ref say "confirmed", "wrong-class" (with suggestedClass), "not-an-object" or "uncertain", with a short reason:\n${boxes.join("\n")}` : "2. There are no existing boxes to review: answer \"reviews\": [].",
|
|
83510
|
+
"Answer with JSON: {\"objects\": [...], \"reviews\": [...]}."
|
|
83511
|
+
].join("\n"),
|
|
83512
|
+
jsonSchema: answerJsonSchema(input.classes)
|
|
83513
|
+
};
|
|
83514
|
+
}
|
|
83515
|
+
/** The whole answer: two arrays and nothing else. Items are checked one by one. */
|
|
83516
|
+
var AnswerSchema = require_clip_model_registry.strictObject({
|
|
83517
|
+
objects: require_clip_model_registry.array(require_clip_model_registry.unknown()),
|
|
83518
|
+
reviews: require_clip_model_registry.array(require_clip_model_registry.unknown())
|
|
83519
|
+
});
|
|
83520
|
+
var ObjectSchema = require_clip_model_registry.strictObject({
|
|
83521
|
+
class: require_clip_model_registry.string(),
|
|
83522
|
+
label: require_clip_model_registry.string().optional(),
|
|
83523
|
+
box: require_clip_model_registry.tuple([
|
|
83524
|
+
require_clip_model_registry.number(),
|
|
83525
|
+
require_clip_model_registry.number(),
|
|
83526
|
+
require_clip_model_registry.number(),
|
|
83527
|
+
require_clip_model_registry.number()
|
|
83528
|
+
]),
|
|
83529
|
+
confidence: require_clip_model_registry.number().min(0).max(1),
|
|
83530
|
+
reason: require_clip_model_registry.string().max(500)
|
|
83531
|
+
});
|
|
83532
|
+
var ReviewSchema = require_clip_model_registry.strictObject({
|
|
83533
|
+
ref: require_clip_model_registry.string(),
|
|
83534
|
+
verdict: require_clip_model_registry._enum(VERDICTS),
|
|
83535
|
+
suggestedClass: require_clip_model_registry.string().optional(),
|
|
83536
|
+
reason: require_clip_model_registry.string().max(500)
|
|
83537
|
+
});
|
|
83538
|
+
function frameBox(box) {
|
|
83539
|
+
const [x, y, w, h] = box;
|
|
83540
|
+
if (![
|
|
83541
|
+
x,
|
|
83542
|
+
y,
|
|
83543
|
+
w,
|
|
83544
|
+
h
|
|
83545
|
+
].every(Number.isFinite)) return null;
|
|
83546
|
+
if (!(w > 0) || !(h > 0) || x < -.01 || y < -.01) return null;
|
|
83547
|
+
if (x + w > 1.01 || y + h > 1.01) return null;
|
|
83548
|
+
const cx = Math.max(0, x);
|
|
83549
|
+
const cy = Math.max(0, y);
|
|
83550
|
+
return {
|
|
83551
|
+
x: cx,
|
|
83552
|
+
y: cy,
|
|
83553
|
+
w: Math.min(w, 1 - cx),
|
|
83554
|
+
h: Math.min(h, 1 - cy)
|
|
83555
|
+
};
|
|
83556
|
+
}
|
|
83557
|
+
/** Strip a Markdown code fence a model wrapped its JSON in. */
|
|
83558
|
+
function unfence(text) {
|
|
83559
|
+
const trimmed = text.trim();
|
|
83560
|
+
const fence = /^```(?:json)?\s*\n([\s\S]*?)\n?```$/i.exec(trimmed);
|
|
83561
|
+
return fence?.[1] !== void 0 ? fence[1].trim() : trimmed;
|
|
83562
|
+
}
|
|
83563
|
+
/** A free-text label, never a macro word (the tier rule). */
|
|
83564
|
+
function usableLabel(label) {
|
|
83565
|
+
const trimmed = label?.trim();
|
|
83566
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
83567
|
+
return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
|
|
83568
|
+
}
|
|
83569
|
+
/** Parse the model's answer. See the module header for the rules. */
|
|
83570
|
+
function parseLlmAssistAnswer(text, expected) {
|
|
83571
|
+
let raw;
|
|
83572
|
+
try {
|
|
83573
|
+
raw = JSON.parse(unfence(text));
|
|
83574
|
+
} catch {
|
|
83575
|
+
return {
|
|
83576
|
+
kind: "malformed",
|
|
83577
|
+
detail: "the answer is not JSON"
|
|
83578
|
+
};
|
|
83579
|
+
}
|
|
83580
|
+
const answer = AnswerSchema.safeParse(raw);
|
|
83581
|
+
if (!answer.success) return {
|
|
83582
|
+
kind: "malformed",
|
|
83583
|
+
detail: "the answer is not {\"objects\": [...], \"reviews\": [...]}"
|
|
83584
|
+
};
|
|
83585
|
+
const asked = new Set(expected.classes);
|
|
83586
|
+
let rejected = 0;
|
|
83587
|
+
const objects = [];
|
|
83588
|
+
for (const item of answer.data.objects) {
|
|
83589
|
+
const parsed = ObjectSchema.safeParse(item);
|
|
83590
|
+
const cls = parsed.success ? parsed.data.class.trim().toLowerCase() : "";
|
|
83591
|
+
const bbox = parsed.success ? frameBox(parsed.data.box) : null;
|
|
83592
|
+
if (!parsed.success || bbox === null || !asked.has(cls) || !isRetrainMacroClass(cls)) {
|
|
83593
|
+
rejected += 1;
|
|
83594
|
+
continue;
|
|
83595
|
+
}
|
|
83596
|
+
const label = usableLabel(parsed.data.label);
|
|
83597
|
+
objects.push({
|
|
83598
|
+
macroClass: cls,
|
|
83599
|
+
...label !== void 0 ? { label } : {},
|
|
83600
|
+
bbox,
|
|
83601
|
+
confidence: parsed.data.confidence,
|
|
83602
|
+
reason: parsed.data.reason
|
|
83603
|
+
});
|
|
83604
|
+
}
|
|
83605
|
+
const reviews = [];
|
|
83606
|
+
for (const item of answer.data.reviews) {
|
|
83607
|
+
const parsed = ReviewSchema.safeParse(item);
|
|
83608
|
+
if (!parsed.success || !expected.refs.has(parsed.data.ref)) {
|
|
83609
|
+
rejected += 1;
|
|
83610
|
+
continue;
|
|
83611
|
+
}
|
|
83612
|
+
const suggested = parsed.data.suggestedClass?.trim().toLowerCase();
|
|
83613
|
+
reviews.push({
|
|
83614
|
+
ref: parsed.data.ref,
|
|
83615
|
+
verdict: parsed.data.verdict,
|
|
83616
|
+
...suggested !== void 0 && isRetrainMacroClass(suggested) ? { suggestedClass: suggested } : {},
|
|
83617
|
+
reason: parsed.data.reason
|
|
83618
|
+
});
|
|
83619
|
+
}
|
|
83620
|
+
return {
|
|
83621
|
+
kind: "parsed",
|
|
83622
|
+
objects,
|
|
83623
|
+
reviews,
|
|
83624
|
+
rejected
|
|
83625
|
+
};
|
|
83626
|
+
}
|
|
83627
|
+
//#endregion
|
|
83628
|
+
//#region src/pipeline-analytics/retrain/retrain-llm-service.ts
|
|
83629
|
+
/** The usage tag every AI-assist call is counted under (`llm.getUsage`). */
|
|
83630
|
+
var RETRAIN_LLM_CONSUMER = "retrain-assist";
|
|
83631
|
+
/** One frame, one answer: a cold local vision model can take a minute. */
|
|
83632
|
+
var RETRAIN_LLM_TIMEOUT_MS = 15e4;
|
|
83633
|
+
var RetrainLlmAssistService = class {
|
|
83634
|
+
deps;
|
|
83635
|
+
constructor(deps) {
|
|
83636
|
+
this.deps = deps;
|
|
83637
|
+
}
|
|
83638
|
+
/** The enabled, vision-capable profiles — a text-only one would be refused. */
|
|
83639
|
+
async listProfiles() {
|
|
83640
|
+
return (await this.deps.llm.listProfiles()).filter((p) => p.enabled && p.supportsVision).map((p) => ({
|
|
83641
|
+
id: p.id,
|
|
83642
|
+
name: p.name,
|
|
83643
|
+
model: p.model,
|
|
83644
|
+
kind: p.kind,
|
|
83645
|
+
leavesHouse: llmProfileLeavesHouse(p)
|
|
83646
|
+
}));
|
|
83647
|
+
}
|
|
83648
|
+
async propose(input) {
|
|
83649
|
+
const tags = { deviceId: input.deviceId };
|
|
83650
|
+
const frame = await this.deps.frames.get(input.frameId);
|
|
83651
|
+
if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
|
|
83652
|
+
kind: "refused",
|
|
83653
|
+
reason: "unknown-frame"
|
|
83654
|
+
};
|
|
83655
|
+
const profile = (await this.deps.llm.listProfiles()).find((p) => p.id === input.profileId);
|
|
83656
|
+
if (profile === void 0) return {
|
|
83657
|
+
kind: "refused",
|
|
83658
|
+
reason: "unknown-profile"
|
|
83659
|
+
};
|
|
83660
|
+
if (!profile.enabled) return {
|
|
83661
|
+
kind: "refused",
|
|
83662
|
+
reason: "profile-disabled"
|
|
83663
|
+
};
|
|
83664
|
+
if (!profile.supportsVision) return {
|
|
83665
|
+
kind: "refused",
|
|
83666
|
+
reason: "no-vision"
|
|
83667
|
+
};
|
|
83668
|
+
const leavesHouse = llmProfileLeavesHouse(profile);
|
|
83669
|
+
if (leavesHouse && !input.allowCloud) {
|
|
83670
|
+
this.deps.logger.warn("retrain AI assist refused: a cloud profile without confirmation", {
|
|
83671
|
+
tags,
|
|
83672
|
+
meta: {
|
|
83673
|
+
frameId: input.frameId,
|
|
83674
|
+
profileId: profile.id
|
|
83675
|
+
}
|
|
83676
|
+
});
|
|
82744
83677
|
return {
|
|
82745
|
-
|
|
82746
|
-
|
|
82747
|
-
|
|
82748
|
-
frameCount: 0
|
|
83678
|
+
kind: "refused",
|
|
83679
|
+
reason: "cloud-not-confirmed",
|
|
83680
|
+
detail: "this profile sends the frame outside the house; confirm the run first"
|
|
82749
83681
|
};
|
|
82750
|
-
} catch (err) {
|
|
82751
|
-
this.logger.warn("retrain collection findByNameKey failed", { meta: {
|
|
82752
|
-
nameKey,
|
|
82753
|
-
error: String(err)
|
|
82754
|
-
} });
|
|
82755
|
-
return null;
|
|
82756
83682
|
}
|
|
83683
|
+
const bytes = await this.deps.frames.readFrame(frame);
|
|
83684
|
+
if (bytes === null) return {
|
|
83685
|
+
kind: "refused",
|
|
83686
|
+
reason: "unreadable-frame"
|
|
83687
|
+
};
|
|
83688
|
+
const request = buildLlmAssistRequest({
|
|
83689
|
+
classes: input.classes,
|
|
83690
|
+
width: frame.width,
|
|
83691
|
+
height: frame.height,
|
|
83692
|
+
boxes: input.boxes
|
|
83693
|
+
});
|
|
83694
|
+
const answer = await this.deps.llm.generateVision({
|
|
83695
|
+
profileId: profile.id,
|
|
83696
|
+
system: request.system,
|
|
83697
|
+
prompt: request.prompt,
|
|
83698
|
+
jsonSchema: request.jsonSchema,
|
|
83699
|
+
image: bytes
|
|
83700
|
+
});
|
|
83701
|
+
if (!answer.ok) {
|
|
83702
|
+
this.deps.logger.warn("retrain AI assist: the model call failed", {
|
|
83703
|
+
tags,
|
|
83704
|
+
meta: {
|
|
83705
|
+
frameId: input.frameId,
|
|
83706
|
+
profileId: profile.id,
|
|
83707
|
+
code: answer.code
|
|
83708
|
+
}
|
|
83709
|
+
});
|
|
83710
|
+
return {
|
|
83711
|
+
kind: "refused",
|
|
83712
|
+
reason: "llm-error",
|
|
83713
|
+
detail: `${answer.code}: ${answer.message}`
|
|
83714
|
+
};
|
|
83715
|
+
}
|
|
83716
|
+
const parsed = parseLlmAssistAnswer(answer.text, {
|
|
83717
|
+
classes: input.classes,
|
|
83718
|
+
refs: new Set(input.boxes.map((b) => b.ref))
|
|
83719
|
+
});
|
|
83720
|
+
if (parsed.kind === "malformed") {
|
|
83721
|
+
this.deps.logger.warn("retrain AI assist: the answer was refused as malformed", {
|
|
83722
|
+
tags,
|
|
83723
|
+
meta: {
|
|
83724
|
+
frameId: input.frameId,
|
|
83725
|
+
profileId: profile.id,
|
|
83726
|
+
model: answer.model,
|
|
83727
|
+
detail: parsed.detail,
|
|
83728
|
+
answerChars: answer.text.length
|
|
83729
|
+
}
|
|
83730
|
+
});
|
|
83731
|
+
return {
|
|
83732
|
+
kind: "refused",
|
|
83733
|
+
reason: "malformed-answer",
|
|
83734
|
+
detail: parsed.detail
|
|
83735
|
+
};
|
|
83736
|
+
}
|
|
83737
|
+
this.deps.logger.info("retrain AI assist proposed boxes", {
|
|
83738
|
+
tags,
|
|
83739
|
+
meta: {
|
|
83740
|
+
frameId: input.frameId,
|
|
83741
|
+
profileId: profile.id,
|
|
83742
|
+
model: answer.model,
|
|
83743
|
+
leftHouse: leavesHouse,
|
|
83744
|
+
proposals: parsed.objects.length,
|
|
83745
|
+
reviews: parsed.reviews.length,
|
|
83746
|
+
rejected: parsed.rejected
|
|
83747
|
+
}
|
|
83748
|
+
});
|
|
83749
|
+
return {
|
|
83750
|
+
kind: "proposed",
|
|
83751
|
+
profileId: profile.id,
|
|
83752
|
+
model: answer.model,
|
|
83753
|
+
leftHouse: leavesHouse,
|
|
83754
|
+
proposals: parsed.objects.map((o) => ({
|
|
83755
|
+
bbox: o.bbox,
|
|
83756
|
+
macroClass: o.macroClass,
|
|
83757
|
+
...o.label !== void 0 ? { label: o.label } : {},
|
|
83758
|
+
kind: "subject",
|
|
83759
|
+
source: "llm",
|
|
83760
|
+
assistModelId: answer.model,
|
|
83761
|
+
assistScore: o.confidence,
|
|
83762
|
+
reason: o.reason
|
|
83763
|
+
})),
|
|
83764
|
+
reviews: parsed.reviews.map((r) => ({
|
|
83765
|
+
ref: r.ref,
|
|
83766
|
+
verdict: r.verdict,
|
|
83767
|
+
...r.suggestedClass !== void 0 ? { suggestedClass: r.suggestedClass } : {},
|
|
83768
|
+
reason: r.reason
|
|
83769
|
+
})),
|
|
83770
|
+
rejected: parsed.rejected
|
|
83771
|
+
};
|
|
82757
83772
|
}
|
|
82758
83773
|
};
|
|
82759
83774
|
//#endregion
|
|
@@ -83233,12 +84248,18 @@ var RETRAIN_CROP_CONVENTION = require_clip_model_registry.DEFAULT_DETAIL_CROP_CO
|
|
|
83233
84248
|
/** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
|
|
83234
84249
|
* the trainer resizes per model, so this only bounds the upload. */
|
|
83235
84250
|
var RETRAIN_CROP_MAX_SIDE = 256;
|
|
83236
|
-
/**
|
|
83237
|
-
|
|
83238
|
-
|
|
84251
|
+
/**
|
|
84252
|
+
* Manifest schema id. v2 = capturedAt / complete / lighting / layout.
|
|
84253
|
+
* v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
|
|
84254
|
+
* `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
|
|
84255
|
+
*/
|
|
84256
|
+
var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
|
|
84257
|
+
/** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
|
|
84258
|
+
* or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
|
|
83239
84259
|
function retrainFramePath(frame) {
|
|
83240
84260
|
const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
|
|
83241
|
-
|
|
84261
|
+
const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
|
|
84262
|
+
return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
|
|
83242
84263
|
}
|
|
83243
84264
|
/** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
|
|
83244
84265
|
function retrainCropPath(macroClass, annotationId) {
|
|
@@ -83248,13 +84269,15 @@ function retrainCropPath(macroClass, annotationId) {
|
|
|
83248
84269
|
* Narrow the planned frames to a filter. Pure. Every field narrows; an empty
|
|
83249
84270
|
* filter keeps everything. Order is the input's.
|
|
83250
84271
|
*/
|
|
83251
|
-
function filterRetrainExport(frames, annotationsByFrame, filter) {
|
|
84272
|
+
function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
|
|
83252
84273
|
const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
|
|
84274
|
+
const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
|
|
83253
84275
|
const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
|
|
83254
84276
|
return frames.filter((frame) => {
|
|
83255
84277
|
if (wanted !== null && !wanted.has(frame.frameId)) return false;
|
|
83256
84278
|
if (devices !== null && !devices.has(frame.deviceId)) return false;
|
|
83257
84279
|
if (filter.completeOnly === true && !frame.complete) return false;
|
|
84280
|
+
if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
|
|
83258
84281
|
if (filter.batchId !== void 0) {
|
|
83259
84282
|
if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
|
|
83260
84283
|
}
|
|
@@ -83309,7 +84332,10 @@ function buildRetrainManifest(input) {
|
|
|
83309
84332
|
frameId: frame.frameId,
|
|
83310
84333
|
path: retrainFramePath(frame),
|
|
83311
84334
|
deviceId: frame.deviceId,
|
|
83312
|
-
|
|
84335
|
+
origin: frame.origin,
|
|
84336
|
+
...frame.origin === "track" ? { trackId: frame.trackId } : {},
|
|
84337
|
+
...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
|
|
84338
|
+
collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
|
|
83313
84339
|
width: frame.width,
|
|
83314
84340
|
height: frame.height,
|
|
83315
84341
|
sizeBytes: frame.sizeBytes,
|
|
@@ -83348,11 +84374,13 @@ function parseRetrainExportRequest(url) {
|
|
|
83348
84374
|
const params = new URLSearchParams(query);
|
|
83349
84375
|
const deviceIds = parseRetrainDeviceIds(url);
|
|
83350
84376
|
const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
84377
|
+
const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
83351
84378
|
const batchId = params.get("batchId")?.trim() ?? "";
|
|
83352
84379
|
return {
|
|
83353
84380
|
filter: {
|
|
83354
84381
|
...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
|
|
83355
84382
|
...frameIds.length > 0 ? { frameIds } : {},
|
|
84383
|
+
...collectionIds.length > 0 ? { collectionIds } : {},
|
|
83356
84384
|
...batchId.length > 0 ? { batchId } : {},
|
|
83357
84385
|
...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
|
|
83358
84386
|
},
|
|
@@ -83365,6 +84393,7 @@ function retrainExportQuery(request) {
|
|
|
83365
84393
|
const { filter } = request;
|
|
83366
84394
|
if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
|
|
83367
84395
|
if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
|
|
84396
|
+
if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
|
|
83368
84397
|
if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
|
|
83369
84398
|
if (filter.completeOnly === true) params.set("completeOnly", "1");
|
|
83370
84399
|
if (request.layout === "crops") params.set("layout", "crops");
|
|
@@ -83439,6 +84468,178 @@ function createRetrainExportHandler(deps) {
|
|
|
83439
84468
|
};
|
|
83440
84469
|
}
|
|
83441
84470
|
//#endregion
|
|
84471
|
+
//#region src/pipeline-analytics/retrain/retrain-pipeline-boxes.ts
|
|
84472
|
+
/**
|
|
84473
|
+
* The boxes the PIPELINE drew on a copied frame — Step 1 of the training
|
|
84474
|
+
* screen.
|
|
84475
|
+
*
|
|
84476
|
+
* A correction has to start from the decision being corrected. Proposing with
|
|
84477
|
+
* a fresh model run answers a different question ("what would a model say
|
|
84478
|
+
* now"), and the frame the operator is fixing is precisely the one where the
|
|
84479
|
+
* live answer was wrong — a re-run that happens to be right hides the phantom
|
|
84480
|
+
* the operator came to remove. So the canvas opens with what the track store
|
|
84481
|
+
* says the pipeline had on that frame, and the operator accepts, corrects,
|
|
84482
|
+
* relabels or deletes it.
|
|
84483
|
+
*
|
|
84484
|
+
* ## Where a box comes from, best first
|
|
84485
|
+
*
|
|
84486
|
+
* 1. **The subject box recorded ON the source media row** (D510). Exact by
|
|
84487
|
+
* construction: the box was measured on the frame the blob shows. Only the
|
|
84488
|
+
* copy's own track can have one.
|
|
84489
|
+
* 2. **The track position nearest the capture time**, within
|
|
84490
|
+
* {@link PIPELINE_BOX_TOLERANCE_MS}. Positions are frame-rate, so the
|
|
84491
|
+
* nearest one is normally the same detection; the delta is reported so a
|
|
84492
|
+
* reviewer can see how far it was.
|
|
84493
|
+
*
|
|
84494
|
+
* Every OTHER track alive on the camera at that instant contributes its own
|
|
84495
|
+
* nearest position the same way — a frame with three people is three boxes,
|
|
84496
|
+
* and a recording still (no track of its own) gets everything the pipeline
|
|
84497
|
+
* was tracking at that moment, which is exactly what a missed-object review
|
|
84498
|
+
* needs to see is NOT there.
|
|
84499
|
+
*
|
|
84500
|
+
* Nothing here does I/O. The caller reads the tracks and the subject box.
|
|
84501
|
+
*/
|
|
84502
|
+
/** How far from the capture time a position may be and still be "this frame".
|
|
84503
|
+
* Positions are recorded at the analysis rate (several per second), so half
|
|
84504
|
+
* a second is a generous match and still not a different moment. */
|
|
84505
|
+
var PIPELINE_BOX_TOLERANCE_MS = 500;
|
|
84506
|
+
/**
|
|
84507
|
+
* How far back a track may have STARTED and still be alive at the frame. The
|
|
84508
|
+
* track store windows on `firstSeen`, so a track born before this is not
|
|
84509
|
+
* asked about — a vehicle parked for hours is the case it misses, and the
|
|
84510
|
+
* operator draws that box by hand.
|
|
84511
|
+
*/
|
|
84512
|
+
var PIPELINE_TRACK_LOOKBACK_MS = 30 * 6e4;
|
|
84513
|
+
/** At most this many tracks of one camera are read around one frame. */
|
|
84514
|
+
var PIPELINE_TRACK_AROUND_LIMIT = 200;
|
|
84515
|
+
/** The least-wrong macro tier for a class the macro map does not know. The
|
|
84516
|
+
* operator corrects it; what must not happen is the raw string being
|
|
84517
|
+
* promoted into the macro tier. */
|
|
84518
|
+
var FALLBACK_MACRO_CLASS = "person";
|
|
84519
|
+
/**
|
|
84520
|
+
* Split a detector's class into the two tiers.
|
|
84521
|
+
*
|
|
84522
|
+
* A macro class stays in the macro tier; anything else maps through the COCO
|
|
84523
|
+
* macro map, and the class itself becomes the LABEL. A macro word is never a
|
|
84524
|
+
* label, in any casing — the tier rule the annotation write enforces.
|
|
84525
|
+
*/
|
|
84526
|
+
function toRetrainClassTiers(className, label) {
|
|
84527
|
+
const cls = className.trim().toLowerCase();
|
|
84528
|
+
const usableLabel = (value) => {
|
|
84529
|
+
const trimmed = value?.trim();
|
|
84530
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
84531
|
+
return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
|
|
84532
|
+
};
|
|
84533
|
+
if (isRetrainMacroClass(cls)) {
|
|
84534
|
+
const kept = usableLabel(label);
|
|
84535
|
+
return kept !== void 0 ? {
|
|
84536
|
+
macroClass: cls,
|
|
84537
|
+
label: kept
|
|
84538
|
+
} : { macroClass: cls };
|
|
84539
|
+
}
|
|
84540
|
+
const mapped = require_clip_model_registry.COCO_TO_MACRO.mapping[cls];
|
|
84541
|
+
const macroClass = mapped !== void 0 && isRetrainMacroClass(mapped) ? mapped : FALLBACK_MACRO_CLASS;
|
|
84542
|
+
const kept = usableLabel(className);
|
|
84543
|
+
return kept !== void 0 ? {
|
|
84544
|
+
macroClass,
|
|
84545
|
+
label: kept
|
|
84546
|
+
} : { macroClass };
|
|
84547
|
+
}
|
|
84548
|
+
/** A pixel box as a clamped normalised one, or `null` when nothing is left. */
|
|
84549
|
+
function normalise(box, frameWidth, frameHeight) {
|
|
84550
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
84551
|
+
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
84552
|
+
const x = clampSpan(box.x, box.w, frameWidth);
|
|
84553
|
+
const y = clampSpan(box.y, box.h, frameHeight);
|
|
84554
|
+
if (x === null || y === null) return null;
|
|
84555
|
+
return {
|
|
84556
|
+
x: x.start,
|
|
84557
|
+
y: y.start,
|
|
84558
|
+
w: x.size,
|
|
84559
|
+
h: y.size
|
|
84560
|
+
};
|
|
84561
|
+
}
|
|
84562
|
+
/** One axis of a pixel box, normalised and clamped into `[0,1]`. The size is
|
|
84563
|
+
* divided directly when nothing was clamped, so an exact box stays exact. */
|
|
84564
|
+
function clampSpan(start, size, extent) {
|
|
84565
|
+
const lo = start / extent;
|
|
84566
|
+
const hi = (start + size) / extent;
|
|
84567
|
+
const s = Math.max(0, lo);
|
|
84568
|
+
const n = lo >= 0 && hi <= 1 ? size / extent : Math.min(1, hi) - s;
|
|
84569
|
+
return n > 0 ? {
|
|
84570
|
+
start: s,
|
|
84571
|
+
size: n
|
|
84572
|
+
} : null;
|
|
84573
|
+
}
|
|
84574
|
+
function nearestPosition(track, at, tolerance) {
|
|
84575
|
+
let best = null;
|
|
84576
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
84577
|
+
for (const p of track.positions) {
|
|
84578
|
+
const delta = Math.abs(p.timestamp - at);
|
|
84579
|
+
if (delta < bestDelta) {
|
|
84580
|
+
best = p;
|
|
84581
|
+
bestDelta = delta;
|
|
84582
|
+
}
|
|
84583
|
+
}
|
|
84584
|
+
if (best === null || bestDelta > tolerance) return null;
|
|
84585
|
+
return {
|
|
84586
|
+
position: best,
|
|
84587
|
+
deltaMs: bestDelta
|
|
84588
|
+
};
|
|
84589
|
+
}
|
|
84590
|
+
function boxFor(track, bbox, basis, deltaMs, score) {
|
|
84591
|
+
const tiers = toRetrainClassTiers(track.className, track.label);
|
|
84592
|
+
const subLabel = track.subLabel?.trim();
|
|
84593
|
+
return {
|
|
84594
|
+
bbox,
|
|
84595
|
+
macroClass: tiers.macroClass,
|
|
84596
|
+
...tiers.label !== void 0 ? { label: tiers.label } : {},
|
|
84597
|
+
...subLabel !== void 0 && subLabel !== "" && !isRetrainMacroClass(subLabel.toLowerCase()) ? { subLabel } : {},
|
|
84598
|
+
...score !== void 0 ? { score } : {},
|
|
84599
|
+
...track.modelId !== void 0 ? { modelId: track.modelId } : {},
|
|
84600
|
+
trackId: track.trackId,
|
|
84601
|
+
basis,
|
|
84602
|
+
deltaMs
|
|
84603
|
+
};
|
|
84604
|
+
}
|
|
84605
|
+
/** The own track's box: the recorded subject box first, the nearest position second. */
|
|
84606
|
+
function ownBox(input, tolerance) {
|
|
84607
|
+
const track = input.ownTrack;
|
|
84608
|
+
if (track === null) return null;
|
|
84609
|
+
const at = input.frame.capturedAt;
|
|
84610
|
+
const nearest = at !== void 0 ? nearestPosition(track, at, tolerance) : null;
|
|
84611
|
+
if (input.ownSubjectBox !== null) {
|
|
84612
|
+
const recorded = normalise(input.ownSubjectBox, input.ownSubjectBox.frameWidth, input.ownSubjectBox.frameHeight);
|
|
84613
|
+
if (recorded !== null) return boxFor(track, recorded, "subject-box", 0, nearest?.position.score);
|
|
84614
|
+
}
|
|
84615
|
+
if (nearest === null) return null;
|
|
84616
|
+
const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
|
|
84617
|
+
return placed === null ? null : boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score);
|
|
84618
|
+
}
|
|
84619
|
+
/**
|
|
84620
|
+
* Every box the pipeline had on this frame, own track first. Pure.
|
|
84621
|
+
*/
|
|
84622
|
+
function pipelineBoxesForFrame(input) {
|
|
84623
|
+
const tolerance = input.toleranceMs ?? PIPELINE_BOX_TOLERANCE_MS;
|
|
84624
|
+
const out = [];
|
|
84625
|
+
const own = ownBox(input, tolerance);
|
|
84626
|
+
if (own !== null) out.push(own);
|
|
84627
|
+
const at = input.frame.capturedAt;
|
|
84628
|
+
if (at === void 0) return out;
|
|
84629
|
+
const ownId = input.frame.trackId;
|
|
84630
|
+
const seen = new Set(ownId !== void 0 ? [ownId] : []);
|
|
84631
|
+
for (const track of input.neighbours) {
|
|
84632
|
+
if (seen.has(track.trackId) || track.deviceId !== input.frame.deviceId) continue;
|
|
84633
|
+
seen.add(track.trackId);
|
|
84634
|
+
const nearest = nearestPosition(track, at, tolerance);
|
|
84635
|
+
if (nearest === null) continue;
|
|
84636
|
+
const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
|
|
84637
|
+
if (placed === null) continue;
|
|
84638
|
+
out.push(boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score));
|
|
84639
|
+
}
|
|
84640
|
+
return out;
|
|
84641
|
+
}
|
|
84642
|
+
//#endregion
|
|
83442
84643
|
//#region src/pipeline-analytics/retrain/retrain-assist.ts
|
|
83443
84644
|
/** The whole frame, as a normalised rectangle. */
|
|
83444
84645
|
var WHOLE_FRAME = {
|
|
@@ -83509,12 +84710,11 @@ function mapAssistDetails(input) {
|
|
|
83509
84710
|
withoutBox += 1;
|
|
83510
84711
|
continue;
|
|
83511
84712
|
}
|
|
83512
|
-
const
|
|
83513
|
-
const label = isRetrainMacroClass(d.className) ? d.label : d.className;
|
|
84713
|
+
const tiers = toRetrainClassTiers(d.className, d.label);
|
|
83514
84714
|
proposals.push({
|
|
83515
84715
|
bbox,
|
|
83516
|
-
macroClass,
|
|
83517
|
-
...label !== void 0
|
|
84716
|
+
macroClass: tiers.macroClass,
|
|
84717
|
+
...tiers.label !== void 0 ? { label: tiers.label } : {},
|
|
83518
84718
|
score: d.score,
|
|
83519
84719
|
modelId
|
|
83520
84720
|
});
|
|
@@ -83525,16 +84725,6 @@ function mapAssistDetails(input) {
|
|
|
83525
84725
|
withoutBox
|
|
83526
84726
|
};
|
|
83527
84727
|
}
|
|
83528
|
-
/**
|
|
83529
|
-
* Macro tier for a detector class we do not carry as a macro class.
|
|
83530
|
-
*
|
|
83531
|
-
* The class itself becomes the LABEL (see above) and the macro tier gets a
|
|
83532
|
-
* value the operator will almost certainly correct — which is fine, because it
|
|
83533
|
-
* is a PROPOSAL and correcting it is the job. What must not happen is the
|
|
83534
|
-
* detector's own string being promoted into the macro tier: that is how
|
|
83535
|
-
* `postman` becomes a class with one example in it.
|
|
83536
|
-
*/
|
|
83537
|
-
var FALLBACK_MACRO_CLASS = "person";
|
|
83538
84728
|
/** A proposal, as a draft annotation ready to store if the operator accepts it. */
|
|
83539
84729
|
function proposalToDraft(proposal, owner) {
|
|
83540
84730
|
return {
|
|
@@ -83743,12 +84933,18 @@ async function restageRetrainTrack(deps, input) {
|
|
|
83743
84933
|
var DEFAULT_STAGING_LIMIT = 100;
|
|
83744
84934
|
/** A macro class that failed validation — the write is refused, never coerced. */
|
|
83745
84935
|
var RetrainAnnotationError = class extends Error {};
|
|
84936
|
+
/** A store frame on the wire. A recording still carries no track and no
|
|
84937
|
+
* source media row, and says so by leaving both out. */
|
|
83746
84938
|
function toWireFrame(f) {
|
|
83747
84939
|
return {
|
|
83748
84940
|
frameId: f.frameId,
|
|
83749
84941
|
deviceId: f.deviceId,
|
|
83750
|
-
|
|
83751
|
-
|
|
84942
|
+
origin: f.origin,
|
|
84943
|
+
...f.origin === "track" ? {
|
|
84944
|
+
trackId: f.trackId,
|
|
84945
|
+
sourceMediaId: f.sourceMediaId
|
|
84946
|
+
} : {},
|
|
84947
|
+
...f.epochMs !== void 0 ? { epochMs: f.epochMs } : {},
|
|
83752
84948
|
sourceKind: f.sourceKind,
|
|
83753
84949
|
sizeBytes: f.sizeBytes,
|
|
83754
84950
|
width: f.width,
|
|
@@ -83761,7 +84957,7 @@ function toWireFrame(f) {
|
|
|
83761
84957
|
function toWireAnnotation(a) {
|
|
83762
84958
|
return {
|
|
83763
84959
|
id: a.id,
|
|
83764
|
-
trackId: a.trackId,
|
|
84960
|
+
...a.trackId !== "" ? { trackId: a.trackId } : {},
|
|
83765
84961
|
deviceId: a.deviceId,
|
|
83766
84962
|
mediaKey: a.mediaKey,
|
|
83767
84963
|
bbox: a.bbox,
|
|
@@ -83828,11 +85024,116 @@ var RetrainService = class {
|
|
|
83828
85024
|
if (candidates.length === 0) await this.logNoAnnotatableFrame(trackId, media, copies.length);
|
|
83829
85025
|
return {
|
|
83830
85026
|
candidates,
|
|
83831
|
-
copies:
|
|
85027
|
+
copies: await this.toWireFrames(copies),
|
|
83832
85028
|
...autoPick !== null ? { autoPickMediaId: autoPick.mediaId } : {}
|
|
83833
85029
|
};
|
|
83834
85030
|
}
|
|
83835
85031
|
/**
|
|
85032
|
+
* The wire frames, each with the boxes the pipeline drew on it (Step 1) and
|
|
85033
|
+
* the collections it belongs to.
|
|
85034
|
+
*
|
|
85035
|
+
* Index reads only — the track rows and the media rows' subject boxes, no
|
|
85036
|
+
* blob and no inference. One read per distinct track and per distinct
|
|
85037
|
+
* instant, not per box. A failed read degrades to fewer boxes, logged, and
|
|
85038
|
+
* never to a failed listing: the frames are the operator's work and must
|
|
85039
|
+
* stay reachable when the evidence is not.
|
|
85040
|
+
*/
|
|
85041
|
+
async toWireFrames(copies) {
|
|
85042
|
+
const collections = this.deps.memberships !== void 0 && copies.length > 0 ? await this.deps.memberships.collectionsOf(copies.map((c) => c.frameId)).catch((err) => {
|
|
85043
|
+
this.deps.logger.warn("retrain frames: collection membership unreadable", { meta: {
|
|
85044
|
+
frames: copies.length,
|
|
85045
|
+
error: String(err)
|
|
85046
|
+
} });
|
|
85047
|
+
return /* @__PURE__ */ new Map();
|
|
85048
|
+
}) : null;
|
|
85049
|
+
const evidence = this.deps.pipelineEvidence;
|
|
85050
|
+
const ownTracks = /* @__PURE__ */ new Map();
|
|
85051
|
+
const subjectBoxes = /* @__PURE__ */ new Map();
|
|
85052
|
+
const out = [];
|
|
85053
|
+
for (const copy of copies) {
|
|
85054
|
+
const wire = toWireFrame(copy);
|
|
85055
|
+
const collectionIds = collections?.get(copy.frameId);
|
|
85056
|
+
const withCollections = collections !== null ? {
|
|
85057
|
+
...wire,
|
|
85058
|
+
collectionIds: collectionIds ?? []
|
|
85059
|
+
} : wire;
|
|
85060
|
+
if (evidence === void 0) {
|
|
85061
|
+
out.push(withCollections);
|
|
85062
|
+
continue;
|
|
85063
|
+
}
|
|
85064
|
+
const trackId = copy.origin === "track" ? copy.trackId : "";
|
|
85065
|
+
if (trackId !== "" && !ownTracks.has(trackId)) {
|
|
85066
|
+
ownTracks.set(trackId, await this.deps.tracks.getPersistedByTrackId(trackId).catch(() => null));
|
|
85067
|
+
subjectBoxes.set(trackId, await this.readSubjectBoxes(evidence, trackId, copy.deviceId));
|
|
85068
|
+
}
|
|
85069
|
+
const neighbours = copy.capturedAt !== void 0 ? await this.readTracksAround(evidence, copy.deviceId, copy.capturedAt) : [];
|
|
85070
|
+
const pipelineBoxes = pipelineBoxesForFrame({
|
|
85071
|
+
frame: {
|
|
85072
|
+
deviceId: copy.deviceId,
|
|
85073
|
+
...trackId !== "" ? { trackId } : {},
|
|
85074
|
+
...copy.capturedAt !== void 0 ? { capturedAt: copy.capturedAt } : {}
|
|
85075
|
+
},
|
|
85076
|
+
ownTrack: trackId !== "" ? ownTracks.get(trackId) ?? null : null,
|
|
85077
|
+
ownSubjectBox: trackId !== "" ? subjectBoxes.get(trackId)?.get(copy.sourceMediaId) ?? null : null,
|
|
85078
|
+
neighbours
|
|
85079
|
+
});
|
|
85080
|
+
out.push({
|
|
85081
|
+
...withCollections,
|
|
85082
|
+
pipelineBoxes
|
|
85083
|
+
});
|
|
85084
|
+
}
|
|
85085
|
+
return out;
|
|
85086
|
+
}
|
|
85087
|
+
/**
|
|
85088
|
+
* The frame a frame-scoped call names, or a refusal. The frame is the
|
|
85089
|
+
* authority: its camera must be the caller's, and a `trackId`, when the
|
|
85090
|
+
* caller sends one, must be the frame's own — a recording still has none.
|
|
85091
|
+
*/
|
|
85092
|
+
async requireFrame(ref, action) {
|
|
85093
|
+
const frame = await this.deps.frames.get(ref.frameId);
|
|
85094
|
+
const trackMismatch = frame !== null && ref.trackId !== void 0 && (frame.origin !== "track" || frame.trackId !== ref.trackId);
|
|
85095
|
+
if (frame === null || frame.deviceId !== ref.deviceId || trackMismatch) {
|
|
85096
|
+
this.deps.logger.warn(`retrain ${action} refused: the frame does not belong to that camera or track`, {
|
|
85097
|
+
tags: { deviceId: ref.deviceId },
|
|
85098
|
+
meta: {
|
|
85099
|
+
frameId: ref.frameId,
|
|
85100
|
+
...ref.trackId !== void 0 ? { trackId: ref.trackId } : {}
|
|
85101
|
+
}
|
|
85102
|
+
});
|
|
85103
|
+
throw new RetrainAnnotationError(frame === null ? `retrain frame ${ref.frameId} does not exist — select it before annotating it` : `retrain frame ${ref.frameId} does not belong to ${ref.trackId !== void 0 ? `track ${ref.trackId}` : `camera ${String(ref.deviceId)}`}`);
|
|
85104
|
+
}
|
|
85105
|
+
return frame;
|
|
85106
|
+
}
|
|
85107
|
+
async readSubjectBoxes(evidence, trackId, deviceId) {
|
|
85108
|
+
try {
|
|
85109
|
+
const rows = await evidence.subjectBoxes(trackId, deviceId);
|
|
85110
|
+
return new Map(rows.map((r) => [r.mediaId, r.subjectBox]));
|
|
85111
|
+
} catch (err) {
|
|
85112
|
+
this.deps.logger.warn("retrain pipeline boxes: subject boxes unreadable", {
|
|
85113
|
+
tags: { deviceId },
|
|
85114
|
+
meta: {
|
|
85115
|
+
trackId,
|
|
85116
|
+
error: String(err)
|
|
85117
|
+
}
|
|
85118
|
+
});
|
|
85119
|
+
return /* @__PURE__ */ new Map();
|
|
85120
|
+
}
|
|
85121
|
+
}
|
|
85122
|
+
async readTracksAround(evidence, deviceId, atMs) {
|
|
85123
|
+
try {
|
|
85124
|
+
return await evidence.tracksAround(deviceId, atMs);
|
|
85125
|
+
} catch (err) {
|
|
85126
|
+
this.deps.logger.warn("retrain pipeline boxes: tracks around the frame unreadable", {
|
|
85127
|
+
tags: { deviceId },
|
|
85128
|
+
meta: {
|
|
85129
|
+
atMs,
|
|
85130
|
+
error: String(err)
|
|
85131
|
+
}
|
|
85132
|
+
});
|
|
85133
|
+
return [];
|
|
85134
|
+
}
|
|
85135
|
+
}
|
|
85136
|
+
/**
|
|
83836
85137
|
* A track with no annotatable frame is a REAL state, and the page has to say
|
|
83837
85138
|
* so rather than render an empty strip that reads as a slow load. Two causes,
|
|
83838
85139
|
* named apart: the whole-frame media AGED OUT, or the track only ever held
|
|
@@ -83885,23 +85186,13 @@ var RetrainService = class {
|
|
|
83885
85186
|
* track the caller names: a frame of another track is refused, loudly.
|
|
83886
85187
|
*/
|
|
83887
85188
|
async setFrameComplete(input) {
|
|
83888
|
-
const frame = await this.
|
|
83889
|
-
|
|
83890
|
-
this.deps.logger.warn("retrain complete refused: the frame does not belong to that track", {
|
|
83891
|
-
tags: { deviceId: input.deviceId },
|
|
83892
|
-
meta: {
|
|
83893
|
-
trackId: input.trackId,
|
|
83894
|
-
frameId: input.frameId
|
|
83895
|
-
}
|
|
83896
|
-
});
|
|
83897
|
-
throw new RetrainAnnotationError(`retrain frame ${input.frameId} does not belong to track ${input.trackId}`);
|
|
83898
|
-
}
|
|
83899
|
-
const updated = await this.deps.frames.setComplete(input.frameId, input.complete);
|
|
85189
|
+
const frame = await this.requireFrame(input, "complete");
|
|
85190
|
+
const updated = await this.deps.frames.setComplete(frame.frameId, input.complete);
|
|
83900
85191
|
if (updated === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} vanished`);
|
|
83901
85192
|
this.deps.logger.info("retrain frame completeness set", {
|
|
83902
85193
|
tags: { deviceId: input.deviceId },
|
|
83903
85194
|
meta: {
|
|
83904
|
-
trackId:
|
|
85195
|
+
trackId: frame.trackId,
|
|
83905
85196
|
frameId: input.frameId,
|
|
83906
85197
|
complete: input.complete
|
|
83907
85198
|
}
|
|
@@ -83909,36 +85200,41 @@ var RetrainService = class {
|
|
|
83909
85200
|
return toWireFrame(updated);
|
|
83910
85201
|
}
|
|
83911
85202
|
/** Un-select: annotations first, then the copy. Never the other way round —
|
|
83912
|
-
* an annotation whose frame is gone is unopenable and unfixable.
|
|
85203
|
+
* an annotation whose frame is gone is unopenable and unfixable. The frame's
|
|
85204
|
+
* collection memberships go with it. */
|
|
83913
85205
|
async deselectFrame(input) {
|
|
83914
85206
|
const frame = await this.deps.frames.get(input.frameId);
|
|
83915
85207
|
if (frame === null) {
|
|
83916
85208
|
this.deps.logger.warn("retrain deselect: no such copied frame", {
|
|
83917
85209
|
tags: { deviceId: input.deviceId },
|
|
83918
|
-
meta: {
|
|
83919
|
-
trackId: input.trackId,
|
|
83920
|
-
frameId: input.frameId
|
|
83921
|
-
}
|
|
85210
|
+
meta: { frameId: input.frameId }
|
|
83922
85211
|
});
|
|
83923
85212
|
return {
|
|
83924
85213
|
removed: false,
|
|
83925
85214
|
removedAnnotations: 0
|
|
83926
85215
|
};
|
|
83927
85216
|
}
|
|
83928
|
-
|
|
85217
|
+
await this.requireFrame(input, "deselect");
|
|
85218
|
+
return {
|
|
85219
|
+
removed: true,
|
|
85220
|
+
removedAnnotations: await this.removeFrame(frame)
|
|
85221
|
+
};
|
|
85222
|
+
}
|
|
85223
|
+
/** Remove a frame from the dataset: annotations, memberships, then the copy. */
|
|
85224
|
+
async removeFrame(frame) {
|
|
85225
|
+
const removedAnnotations = await this.deps.annotations.deleteForFrame(frame.frameId);
|
|
85226
|
+
await this.deps.memberships?.forgetFrame(frame.frameId);
|
|
83929
85227
|
await this.deps.frames.deleteFrame(frame);
|
|
83930
85228
|
this.deps.logger.info("retrain frame removed from the dataset", {
|
|
83931
|
-
tags: { deviceId:
|
|
85229
|
+
tags: { deviceId: frame.deviceId },
|
|
83932
85230
|
meta: {
|
|
83933
|
-
|
|
83934
|
-
|
|
85231
|
+
origin: frame.origin,
|
|
85232
|
+
...frame.origin === "track" ? { trackId: frame.trackId } : {},
|
|
85233
|
+
frameId: frame.frameId,
|
|
83935
85234
|
removedAnnotations
|
|
83936
85235
|
}
|
|
83937
85236
|
});
|
|
83938
|
-
return
|
|
83939
|
-
removed: true,
|
|
83940
|
-
removedAnnotations
|
|
83941
|
-
};
|
|
85237
|
+
return removedAnnotations;
|
|
83942
85238
|
}
|
|
83943
85239
|
/** The copy's pixels plus its exact dimensions — the canvas needs both. */
|
|
83944
85240
|
async getFrameImage(frameId) {
|
|
@@ -83978,7 +85274,7 @@ var RetrainService = class {
|
|
|
83978
85274
|
};
|
|
83979
85275
|
}
|
|
83980
85276
|
const frame = await this.deps.frames.get(input.frameId);
|
|
83981
|
-
if (frame === null) return {
|
|
85277
|
+
if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
|
|
83982
85278
|
kind: "refused",
|
|
83983
85279
|
reason: "unknown-frame"
|
|
83984
85280
|
};
|
|
@@ -84033,8 +85329,8 @@ var RetrainService = class {
|
|
|
84033
85329
|
}
|
|
84034
85330
|
});
|
|
84035
85331
|
const owner = {
|
|
84036
|
-
trackId:
|
|
84037
|
-
deviceId:
|
|
85332
|
+
trackId: frame.trackId,
|
|
85333
|
+
deviceId: frame.deviceId,
|
|
84038
85334
|
mediaKey: input.frameId
|
|
84039
85335
|
};
|
|
84040
85336
|
return {
|
|
@@ -84057,8 +85353,12 @@ var RetrainService = class {
|
|
|
84057
85353
|
belowThreshold: mapping.belowThreshold
|
|
84058
85354
|
};
|
|
84059
85355
|
}
|
|
84060
|
-
|
|
84061
|
-
|
|
85356
|
+
/** The annotations of a track, or of named frames (a recording still has
|
|
85357
|
+
* no track). One of the two must be given. */
|
|
85358
|
+
async listAnnotations(input) {
|
|
85359
|
+
if (input.trackId !== void 0 && input.trackId !== "") return (await this.deps.annotations.listByTrack(input.trackId)).map(toWireAnnotation);
|
|
85360
|
+
if (input.frameIds !== void 0) return (await this.deps.annotations.listByFrames(input.frameIds)).map(toWireAnnotation);
|
|
85361
|
+
throw new RetrainAnnotationError("listRetrainAnnotations needs a trackId or frameIds");
|
|
84062
85362
|
}
|
|
84063
85363
|
/**
|
|
84064
85364
|
* Replace every annotation on one frame.
|
|
@@ -84070,15 +85370,15 @@ var RetrainService = class {
|
|
|
84070
85370
|
* believes they stored.
|
|
84071
85371
|
*/
|
|
84072
85372
|
async saveAnnotations(input) {
|
|
84073
|
-
|
|
85373
|
+
const frame = await this.requireFrame(input, "save");
|
|
84074
85374
|
for (const a of input.annotations) {
|
|
84075
85375
|
if (!isRetrainMacroClass(a.macroClass)) throw new RetrainAnnotationError(`"${a.macroClass}" is not a macro class`);
|
|
84076
85376
|
const violation = checkTierSeparation(a);
|
|
84077
85377
|
if (violation !== null) throw new RetrainAnnotationError(`"${violation.value}" is a MACRO CLASS and cannot be a ${violation.field} — the tiers are separate (person / postman / Marco)`);
|
|
84078
85378
|
}
|
|
84079
85379
|
const drafts = input.annotations.map((a) => ({
|
|
84080
|
-
trackId:
|
|
84081
|
-
deviceId:
|
|
85380
|
+
trackId: frame.trackId,
|
|
85381
|
+
deviceId: frame.deviceId,
|
|
84082
85382
|
mediaKey: input.frameId,
|
|
84083
85383
|
bbox: a.bbox,
|
|
84084
85384
|
macroClass: a.macroClass,
|
|
@@ -84094,7 +85394,8 @@ var RetrainService = class {
|
|
|
84094
85394
|
this.deps.logger.info("retrain annotations saved", {
|
|
84095
85395
|
tags: { deviceId: input.deviceId },
|
|
84096
85396
|
meta: {
|
|
84097
|
-
|
|
85397
|
+
origin: frame.origin,
|
|
85398
|
+
trackId: frame.trackId,
|
|
84098
85399
|
frameId: input.frameId,
|
|
84099
85400
|
subjects: saved.length - errors,
|
|
84100
85401
|
modelErrors: errors
|
|
@@ -93496,6 +94797,14 @@ var RECORDED_STILL_PROFILE = "high";
|
|
|
93496
94797
|
*/
|
|
93497
94798
|
var RECOVERED_KEY_FRAME_MAX_WIDTH = 1920;
|
|
93498
94799
|
/**
|
|
94800
|
+
* Width cap for a still added to the retrain dataset from the recording.
|
|
94801
|
+
*
|
|
94802
|
+
* Effectively none: a training frame is wanted at the camera's own resolution
|
|
94803
|
+
* (the same pixels a native key frame has), so the bound is only a ceiling
|
|
94804
|
+
* against a pathological stream, far above any camera on the fleet.
|
|
94805
|
+
*/
|
|
94806
|
+
var RETRAIN_STILL_MAX_WIDTH = 7680;
|
|
94807
|
+
/**
|
|
93499
94808
|
* Build the production ports.
|
|
93500
94809
|
*
|
|
93501
94810
|
* Every port swallows its own failure into the `null` the policy expects: a
|
|
@@ -107191,6 +108500,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
107191
108500
|
/** The retrain curation surface — see `retrain/retrain-service.ts`. Null
|
|
107192
108501
|
* until the stores are built. */
|
|
107193
108502
|
retrain = null;
|
|
108503
|
+
retrainCollections = null;
|
|
108504
|
+
retrainLlmAssist = null;
|
|
108505
|
+
retrainCollectionStore = null;
|
|
107194
108506
|
retrainFrames = null;
|
|
107195
108507
|
retrainAnnotations = null;
|
|
107196
108508
|
/** The NC artifact plane (signed public links for notification media) and its
|
|
@@ -109242,7 +110554,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
109242
110554
|
});
|
|
109243
110555
|
this.retrainFrames = retrainFrames;
|
|
109244
110556
|
this.retrainAnnotations = retrainAnnotations;
|
|
109245
|
-
|
|
110557
|
+
const retrainCollectionStore = new RetrainCollectionStore({
|
|
110558
|
+
store: api.settingsStore,
|
|
110559
|
+
logger: logger.child("RetrainCollections"),
|
|
110560
|
+
now: () => Date.now(),
|
|
110561
|
+
newId: () => (0, node_crypto.randomUUID)()
|
|
110562
|
+
});
|
|
110563
|
+
this.retrainCollectionStore = retrainCollectionStore;
|
|
110564
|
+
const retrain = new RetrainService({
|
|
109246
110565
|
tracks: {
|
|
109247
110566
|
listStaging: (deviceId, limit) => trackStore.listStaging(deviceId, limit),
|
|
109248
110567
|
listDeviceIds: () => trackStore.listDeviceIds(),
|
|
@@ -109260,7 +110579,70 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
109260
110579
|
frames: retrainFrames,
|
|
109261
110580
|
annotations: retrainAnnotations,
|
|
109262
110581
|
runner: (input, nodeId) => nodeId === void 0 ? api.pipelineRunner.runStatelessStep.mutate(input) : api.pipelineRunner.runStatelessStep.mutate(input, require_clip_model_registry.nodePin(nodeId)),
|
|
109263
|
-
logger: logger.child("Retrain")
|
|
110582
|
+
logger: logger.child("Retrain"),
|
|
110583
|
+
pipelineEvidence: {
|
|
110584
|
+
tracksAround: (deviceId, atMs) => trackStore.queryHistorical({
|
|
110585
|
+
deviceId,
|
|
110586
|
+
since: atMs - PIPELINE_TRACK_LOOKBACK_MS,
|
|
110587
|
+
until: atMs + PIPELINE_BOX_TOLERANCE_MS,
|
|
110588
|
+
limit: PIPELINE_TRACK_AROUND_LIMIT
|
|
110589
|
+
}),
|
|
110590
|
+
subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
|
|
110591
|
+
},
|
|
110592
|
+
memberships: retrainCollectionStore
|
|
110593
|
+
});
|
|
110594
|
+
this.retrain = retrain;
|
|
110595
|
+
const retrainStills = new RecordedStillService(buildRecordedStillPorts({
|
|
110596
|
+
api: this.ctx.api.recordingArchive,
|
|
110597
|
+
logger: logger.child("RetrainStills"),
|
|
110598
|
+
resolveFfmpeg: this.pinnedFfmpeg
|
|
110599
|
+
}));
|
|
110600
|
+
this.retrainCollections = new RetrainCollectionService({
|
|
110601
|
+
collections: retrainCollectionStore,
|
|
110602
|
+
frames: retrainFrames,
|
|
110603
|
+
annotations: retrainAnnotations,
|
|
110604
|
+
retrain,
|
|
110605
|
+
captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
|
|
110606
|
+
logger: logger.child("RetrainCollections")
|
|
110607
|
+
});
|
|
110608
|
+
this.retrainLlmAssist = new RetrainLlmAssistService({
|
|
110609
|
+
frames: retrainFrames,
|
|
110610
|
+
llm: {
|
|
110611
|
+
listProfiles: async () => (await api.llm.listProfiles.query({})).map((p) => ({
|
|
110612
|
+
id: p.id,
|
|
110613
|
+
name: p.name,
|
|
110614
|
+
model: p.model,
|
|
110615
|
+
kind: p.kind,
|
|
110616
|
+
...p.baseUrl !== void 0 ? { baseUrl: p.baseUrl } : {},
|
|
110617
|
+
enabled: p.enabled,
|
|
110618
|
+
supportsVision: p.supportsVision
|
|
110619
|
+
})),
|
|
110620
|
+
generateVision: async (request) => {
|
|
110621
|
+
const result = await api.llm.generateVision.mutate({
|
|
110622
|
+
profileId: request.profileId,
|
|
110623
|
+
consumer: RETRAIN_LLM_CONSUMER,
|
|
110624
|
+
system: request.system,
|
|
110625
|
+
prompt: request.prompt,
|
|
110626
|
+
jsonSchema: request.jsonSchema,
|
|
110627
|
+
images: [{
|
|
110628
|
+
bytes: new Uint8Array(request.image),
|
|
110629
|
+
mimeType: "image/jpeg"
|
|
110630
|
+
}],
|
|
110631
|
+
temperature: 0,
|
|
110632
|
+
timeoutMs: RETRAIN_LLM_TIMEOUT_MS
|
|
110633
|
+
});
|
|
110634
|
+
return result.ok ? {
|
|
110635
|
+
ok: true,
|
|
110636
|
+
text: result.text,
|
|
110637
|
+
model: result.model
|
|
110638
|
+
} : {
|
|
110639
|
+
ok: false,
|
|
110640
|
+
code: result.code,
|
|
110641
|
+
message: result.message
|
|
110642
|
+
};
|
|
110643
|
+
}
|
|
110644
|
+
},
|
|
110645
|
+
logger: logger.child("RetrainLlmAssist")
|
|
109264
110646
|
});
|
|
109265
110647
|
this.mediaRelocate = new MediaRelocateEngine({
|
|
109266
110648
|
store: api.settingsStore,
|
|
@@ -110444,14 +111826,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
110444
111826
|
};
|
|
110445
111827
|
return this.requireRetrain().propose({
|
|
110446
111828
|
deviceId: input.deviceId,
|
|
110447
|
-
trackId: input.trackId,
|
|
111829
|
+
...input.trackId !== void 0 ? { trackId: input.trackId } : {},
|
|
110448
111830
|
frameId: input.frameId,
|
|
110449
111831
|
subject,
|
|
110450
111832
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
110451
111833
|
});
|
|
110452
111834
|
}
|
|
110453
111835
|
async listRetrainAnnotations(input) {
|
|
110454
|
-
return this.requireRetrain().listAnnotations(input
|
|
111836
|
+
return this.requireRetrain().listAnnotations(input);
|
|
110455
111837
|
}
|
|
110456
111838
|
async saveRetrainAnnotations(input) {
|
|
110457
111839
|
return this.requireRetrain().saveAnnotations(input);
|
|
@@ -110475,6 +111857,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
110475
111857
|
async setRetrainFrameComplete(input) {
|
|
110476
111858
|
return this.requireRetrain().setFrameComplete(input);
|
|
110477
111859
|
}
|
|
111860
|
+
requireRetrainLlmAssist() {
|
|
111861
|
+
const service = this.retrainLlmAssist;
|
|
111862
|
+
if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
|
|
111863
|
+
return service;
|
|
111864
|
+
}
|
|
111865
|
+
async listRetrainAssistProfiles(_input) {
|
|
111866
|
+
return this.requireRetrainLlmAssist().listProfiles();
|
|
111867
|
+
}
|
|
111868
|
+
async proposeRetrainAnnotationsWithLlm(input) {
|
|
111869
|
+
return this.requireRetrainLlmAssist().propose(input);
|
|
111870
|
+
}
|
|
111871
|
+
requireRetrainCollections() {
|
|
111872
|
+
const service = this.retrainCollections;
|
|
111873
|
+
if (service === null) throw new Error("retrain collections: analytics stores unavailable");
|
|
111874
|
+
return service;
|
|
111875
|
+
}
|
|
111876
|
+
async createRetrainCollection(input) {
|
|
111877
|
+
return this.requireRetrainCollections().create(input);
|
|
111878
|
+
}
|
|
111879
|
+
async listRetrainCollections(_input) {
|
|
111880
|
+
return this.requireRetrainCollections().list();
|
|
111881
|
+
}
|
|
111882
|
+
async renameRetrainCollection(input) {
|
|
111883
|
+
return this.requireRetrainCollections().rename(input);
|
|
111884
|
+
}
|
|
111885
|
+
async deleteRetrainCollection(input) {
|
|
111886
|
+
return this.requireRetrainCollections().delete(input);
|
|
111887
|
+
}
|
|
111888
|
+
async addRetrainFrameFromRecording(input) {
|
|
111889
|
+
return this.requireRetrainCollections().addFrameFromRecording(input);
|
|
111890
|
+
}
|
|
111891
|
+
async addRetrainFramesToCollection(input) {
|
|
111892
|
+
return this.requireRetrainCollections().addFrames(input);
|
|
111893
|
+
}
|
|
111894
|
+
async removeRetrainFrameFromCollection(input) {
|
|
111895
|
+
return this.requireRetrainCollections().removeFrame(input);
|
|
111896
|
+
}
|
|
111897
|
+
async listRetrainCollectionFrames(input) {
|
|
111898
|
+
return this.requireRetrainCollections().listFrames(input);
|
|
111899
|
+
}
|
|
110478
111900
|
/**
|
|
110479
111901
|
* Stream the annotated archive into ONE HTTP request to `target` — the
|
|
110480
111902
|
* training upload. Same bytes as the download; never on the hub's disk.
|
|
@@ -110830,7 +112252,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
110830
112252
|
const trackStore = this.trackStore;
|
|
110831
112253
|
if (!frameStore || !annotationStore || !trackStore) throw new Error("retrain export: analytics stores unavailable");
|
|
110832
112254
|
const deviceIds = request.filter.deviceIds ?? [];
|
|
110833
|
-
const devices = deviceIds.length > 0 ? deviceIds : await trackStore.listDeviceIds();
|
|
112255
|
+
const devices = deviceIds.length > 0 ? deviceIds : [...new Set([...await trackStore.listDeviceIds(), ...await frameStore.listDeviceIds()])];
|
|
110834
112256
|
const candidates = /* @__PURE__ */ new Map();
|
|
110835
112257
|
const annotationsByFrame = /* @__PURE__ */ new Map();
|
|
110836
112258
|
const zones = /* @__PURE__ */ new Map();
|
|
@@ -110847,11 +112269,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
110847
112269
|
const frame = await frameStore.get(frameId);
|
|
110848
112270
|
if (frame !== null) candidates.set(frameId, frame);
|
|
110849
112271
|
}
|
|
110850
|
-
const
|
|
112272
|
+
const collectionsByFrame = await this.retrainCollectionStore?.collectionsOf([...candidates.keys()]) ?? /* @__PURE__ */ new Map();
|
|
112273
|
+
const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter, collectionsByFrame);
|
|
110851
112274
|
return {
|
|
110852
112275
|
manifest: buildRetrainManifest({
|
|
110853
112276
|
frames,
|
|
110854
112277
|
annotationsByFrame,
|
|
112278
|
+
collectionsByFrame,
|
|
110855
112279
|
zoneForDevice: (deviceId) => zones.get(deviceId) ?? null,
|
|
110856
112280
|
generatedAt: Date.now(),
|
|
110857
112281
|
batchId: (0, node_crypto.randomUUID)(),
|