@camstack/addon-post-analysis 1.2.364 → 1.2.366

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.
Files changed (14) hide show
  1. package/dist/{clip-model-registry-B69Mktce.mjs → clip-model-registry-Bsj2t3Ec.mjs} +375 -4
  2. package/dist/{clip-model-registry-D8XeJhT4.js → clip-model-registry-CfYSfakK.js} +404 -3
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CUQTWug8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CBiuAq6S.mjs} +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3Ko_8Ah.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cul2786X.mjs} +1 -1
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DTg0F1dD.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-Be7_ixng.mjs → hostInit-6daqknJk.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +519 -41
  11. package/dist/pipeline-analytics/index.mjs +519 -41
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DOcfQnib.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-D8XeJhT4.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-CfYSfakK.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");
@@ -48402,6 +48402,7 @@ function startAlarmPanelConvergence(ports) {
48402
48402
  var NC_ARTIFACT_READ_PREFIX_REF = NC_ARTIFACT_READ_PREFIX;
48403
48403
  var TRAINING_EXPORT_PREFIX_REF = "training-export";
48404
48404
  var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
48405
+ var RETRAIN_FRAME_PREFIX_REF = "retrain-frame";
48405
48406
  var DEBUG_EXPORT_PREFIX_REF = "debug-export";
48406
48407
  var TAPE_EXPORT_PREFIX_REF = "tape-export";
48407
48408
  /**
@@ -48427,6 +48428,13 @@ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
48427
48428
  /** Every frame behind a marked selection — a corpus, not an item. */
48428
48429
  [TRAINING_EXPORT_PREFIX_REF]: "admin",
48429
48430
  [RETRAIN_EXPORT_PREFIX_REF]: "admin",
48431
+ /**
48432
+ * One copied retrain frame by its id. `admin`, not `authenticated`: the item
48433
+ * is single, but the cap that served the same bytes (`getRetrainFrameImage`)
48434
+ * is `auth: 'admin'` and the surface is admin curation — widening the bytes
48435
+ * beyond the method they replace would be a policy change nobody asked for.
48436
+ */
48437
+ [RETRAIN_FRAME_PREFIX_REF]: "admin",
48430
48438
  /** Diagnostic bundle: logs, config and frames. */
48431
48439
  [DEBUG_EXPORT_PREFIX_REF]: "admin",
48432
48440
  /** A promoted tracking tape and its ground truth (D681) — a corpus, not an item. */
@@ -70955,6 +70963,67 @@ var TrackStore = class {
70955
70963
  }
70956
70964
  };
70957
70965
  //#endregion
70966
+ //#region src/pipeline-analytics/retrain/retrain-origin.ts
70967
+ /**
70968
+ * Reading the two JSON columns the review rests on — an annotation's `origin`
70969
+ * and a frame's `dismissed` set — back into typed values.
70970
+ *
70971
+ * Parsed with the cap's own schemas rather than cast: a column written by
70972
+ * something that bypassed the guard reads as "no origin" / "nothing dismissed",
70973
+ * which is the safe direction (a missing snapshot makes a verdict approximate;
70974
+ * it never invents one).
70975
+ */
70976
+ function parseJsonIfText(value) {
70977
+ if (typeof value !== "string") return value;
70978
+ try {
70979
+ return JSON.parse(value);
70980
+ } catch {
70981
+ return null;
70982
+ }
70983
+ }
70984
+ /** The stored origin snapshot, or `null` (legacy row / unreadable value). */
70985
+ function narrowOrigin(value) {
70986
+ const parsed = require_clip_model_registry.RetrainOriginSchema.safeParse(parseJsonIfText(value));
70987
+ return parsed.success ? parsed.data : null;
70988
+ }
70989
+ /** The stored dismissed set, or an empty list. */
70990
+ function narrowDismissed(value) {
70991
+ const parsed = require_clip_model_registry.RetrainDismissedSchema.array().safeParse(parseJsonIfText(value));
70992
+ return parsed.success ? parsed.data : [];
70993
+ }
70994
+ /**
70995
+ * The stable name of a pipeline box inside its frame. One live track draws one
70996
+ * box on a frame, and the track id survives a re-derivation, which is what lets
70997
+ * a dismissal made today still match tomorrow's list.
70998
+ */
70999
+ function pipelineOriginRef(trackId) {
71000
+ return `pipeline:${trackId}`;
71001
+ }
71002
+ /** A re-derived pipeline box as the snapshot an annotation or dismissal keeps. */
71003
+ function pipelineBoxOrigin(box) {
71004
+ return {
71005
+ source: "pipeline",
71006
+ ref: pipelineOriginRef(box.trackId),
71007
+ bbox: box.bbox,
71008
+ macroClass: box.macroClass,
71009
+ ...box.label !== void 0 ? { label: box.label } : {},
71010
+ ...box.score !== void 0 ? { score: box.score } : {},
71011
+ ...box.modelId !== void 0 ? { modelId: box.modelId } : {}
71012
+ };
71013
+ }
71014
+ /** The n-th proposal of a detector (`assist`) or vision-LLM (`llm`) run. */
71015
+ function proposalOrigin(source, index, facts) {
71016
+ return {
71017
+ source,
71018
+ ref: `${source}:${String(index)}`,
71019
+ bbox: facts.bbox,
71020
+ macroClass: facts.macroClass,
71021
+ ...facts.label !== void 0 ? { label: facts.label } : {},
71022
+ ...facts.score !== void 0 ? { score: facts.score } : {},
71023
+ ...facts.modelId !== void 0 ? { modelId: facts.modelId } : {}
71024
+ };
71025
+ }
71026
+ //#endregion
70958
71027
  //#region src/pipeline-analytics/retrain/retrain-frame-store.ts
70959
71028
  /**
70960
71029
  * RetrainFrameStore — the COPIES the retrain dataset owns.
@@ -71124,6 +71193,22 @@ var RETRAIN_FRAME_COLUMNS = [
71124
71193
  {
71125
71194
  name: "epochMs",
71126
71195
  type: "INTEGER"
71196
+ }),
71197
+ (
71198
+ /** Bumped by every annotation save; the compare-and-set a save is guarded
71199
+ * by. The column default makes a pre-revision row read `0`. */
71200
+ {
71201
+ name: "revision",
71202
+ type: "INTEGER",
71203
+ notNull: true,
71204
+ defaultValue: 0
71205
+ }),
71206
+ (
71207
+ /** The proposals the operator deleted (`RetrainDismissed[]`), so a deleted
71208
+ * pipeline box is not offered again. NULL = none. */
71209
+ {
71210
+ name: "dismissed",
71211
+ type: "JSON"
71127
71212
  })
71128
71213
  ];
71129
71214
  var RETRAIN_FRAME_INDEXES = [
@@ -71237,7 +71322,9 @@ var RetrainFrameStore = class {
71237
71322
  ...finiteNumber(data["capturedAt"]) !== void 0 ? { capturedAt: finiteNumber(data["capturedAt"]) } : {},
71238
71323
  ...finiteNumber(data["chromaSpread"]) !== void 0 ? { chromaSpread: finiteNumber(data["chromaSpread"]) } : {},
71239
71324
  complete: data["complete"] === true || data["complete"] === 1,
71240
- ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {}
71325
+ ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {},
71326
+ revision: finiteNumber(data["revision"]) ?? 0,
71327
+ dismissed: narrowDismissed(data["dismissed"])
71241
71328
  };
71242
71329
  }
71243
71330
  /** The copies with these ids, in the order given; unknown ids are skipped. */
@@ -80810,7 +80897,7 @@ async function ensureTrackHierarchyMedia(deps, input) {
80810
80897
  }
80811
80898
  //#endregion
80812
80899
  //#region src/pipeline-analytics/event-media-handler.ts
80813
- var CACHE_CONTROL = "public, max-age=31536000, immutable";
80900
+ var CACHE_CONTROL$1 = "public, max-age=31536000, immutable";
80814
80901
  /** Default / clamp bounds for the `thumb` variant edge (px). Mirrors
80815
80902
  * `shared/frame/square-thumb.ts`; kept here so query parsing stays pure. */
80816
80903
  var THUMB_DEFAULT_SIZE = 160;
@@ -80995,14 +81082,14 @@ function createEventMediaHandler(deps) {
80995
81082
  if (req.headers["if-none-match"] === etag) {
80996
81083
  res.writeHead(304, {
80997
81084
  etag,
80998
- "cache-control": CACHE_CONTROL,
81085
+ "cache-control": CACHE_CONTROL$1,
80999
81086
  "server-timing": serverTiming
81000
81087
  }).end();
81001
81088
  return;
81002
81089
  }
81003
81090
  res.writeHead(200, {
81004
81091
  "content-type": "image/jpeg",
81005
- "cache-control": CACHE_CONTROL,
81092
+ "cache-control": CACHE_CONTROL$1,
81006
81093
  etag,
81007
81094
  "content-length": String(media.bytes.byteLength),
81008
81095
  "server-timing": serverTiming
@@ -82932,6 +83019,13 @@ var RETRAIN_ANNOTATION_COLUMNS = [
82932
83019
  name: "assistScore",
82933
83020
  type: "REAL"
82934
83021
  },
83022
+ (
83023
+ /** The proposal the box started from (`RetrainOrigin`), snapshotted at
83024
+ * accept time. NULL on a row saved before origins existed. */
83025
+ {
83026
+ name: "origin",
83027
+ type: "JSON"
83028
+ }),
82935
83029
  {
82936
83030
  name: "exportedInBatch",
82937
83031
  type: "TEXT"
@@ -83015,6 +83109,7 @@ var RetrainAnnotationStore = class {
83015
83109
  source: narrowSource(data["source"]),
83016
83110
  assistModelId: optionalString(data["assistModelId"]),
83017
83111
  assistScore: typeof score === "number" ? score : void 0,
83112
+ origin: narrowOrigin(data["origin"]),
83018
83113
  exportedInBatch: optionalString(data["exportedInBatch"]),
83019
83114
  createdAt: Number(data["createdAt"] ?? 0)
83020
83115
  };
@@ -83032,12 +83127,14 @@ var RetrainAnnotationStore = class {
83032
83127
  source: a.source,
83033
83128
  assistModelId: a.assistModelId ?? null,
83034
83129
  assistScore: a.assistScore ?? null,
83130
+ origin: a.origin ?? null,
83035
83131
  exportedInBatch: a.exportedInBatch ?? null,
83036
83132
  createdAt: a.createdAt
83037
83133
  };
83038
83134
  }
83039
83135
  /**
83040
- * Replace EVERY annotation on one frame with the supplied set.
83136
+ * Replace EVERY annotation on one frame with the supplied set — atomically,
83137
+ * and only if the frame is still at the revision the caller saw.
83041
83138
  *
83042
83139
  * Whole-frame replacement rather than per-box upsert, deliberately: the unit
83043
83140
  * of ground truth is the frame, and "the operator deleted a box" and "the
@@ -83045,38 +83142,113 @@ var RetrainAnnotationStore = class {
83045
83142
  * patch API would let a frame keep a box the operator removed on a surface
83046
83143
  * that only knew about the boxes it sent.
83047
83144
  *
83145
+ * The old rows' deletion, the new rows, the frame's `dismissed` set and its
83146
+ * bumped `revision` are ONE `settingsStore.transact`, guarded by
83147
+ * `commit.fromRevision`: a crash leaves the frame as it was, a concurrent
83148
+ * save leaves one winner and one `conflict` — never a blend, never a frame
83149
+ * blanked between a delete and an insert.
83150
+ *
83048
83151
  * Returns the persisted rows so the caller renders ids it can address.
83049
83152
  */
83050
- async replaceForFrame(mediaKey, drafts) {
83051
- const existing = await this.listByFrame(mediaKey);
83153
+ async replaceForFrame(mediaKey, drafts, commit) {
83052
83154
  const createdAt = this.now();
83053
83155
  const rows = drafts.map((d) => ({
83054
83156
  ...d,
83157
+ origin: d.origin ?? null,
83055
83158
  id: this.newId(),
83056
83159
  createdAt,
83057
83160
  mediaKey
83058
83161
  }));
83059
- for (const row of rows) await this.store.set.mutate({
83060
- collection: RETRAIN_ANNOTATIONS_COLLECTION,
83061
- key: row.id,
83062
- value: this.toRecord(row)
83162
+ const nextRevision = commit.fromRevision + 1;
83163
+ const result = await this.store.transact.mutate({
83164
+ guard: {
83165
+ collection: RETRAIN_FRAMES_COLLECTION,
83166
+ key: mediaKey,
83167
+ column: "revision",
83168
+ equals: commit.fromRevision
83169
+ },
83170
+ ops: [
83171
+ {
83172
+ op: "deleteWhere",
83173
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
83174
+ filter: { where: { mediaKey } }
83175
+ },
83176
+ {
83177
+ op: "insertMany",
83178
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
83179
+ records: rows.map((row) => ({
83180
+ id: row.id,
83181
+ data: this.toRecord(row)
83182
+ }))
83183
+ },
83184
+ {
83185
+ op: "update",
83186
+ collection: RETRAIN_FRAMES_COLLECTION,
83187
+ id: mediaKey,
83188
+ data: {
83189
+ revision: nextRevision,
83190
+ ...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
83191
+ }
83192
+ }
83193
+ ]
83063
83194
  });
83064
- for (const old of existing) try {
83065
- await this.store.delete.mutate({
83195
+ if (!result.applied) {
83196
+ const current = result.current;
83197
+ return {
83198
+ kind: "conflict",
83199
+ currentRevision: typeof current === "number" ? current : null
83200
+ };
83201
+ }
83202
+ return {
83203
+ kind: "saved",
83204
+ rows,
83205
+ revision: nextRevision
83206
+ };
83207
+ }
83208
+ /**
83209
+ * Set `origin` on rows that have none, in one guarded transaction with a
83210
+ * revision bump. Rows that already carry an origin, or are not on this frame,
83211
+ * are left untouched and not counted. No eligible row ⇒ no write at all.
83212
+ */
83213
+ async setOrigins(mediaKey, origins, commit) {
83214
+ const eligible = (await this.listByFrame(mediaKey)).filter((row) => !row.origin && origins.has(row.id));
83215
+ if (eligible.length === 0) return {
83216
+ kind: "updated",
83217
+ updated: 0,
83218
+ revision: commit.fromRevision
83219
+ };
83220
+ const nextRevision = commit.fromRevision + 1;
83221
+ const result = await this.store.transact.mutate({
83222
+ guard: {
83223
+ collection: RETRAIN_FRAMES_COLLECTION,
83224
+ key: mediaKey,
83225
+ column: "revision",
83226
+ equals: commit.fromRevision
83227
+ },
83228
+ ops: [...eligible.map((row) => ({
83229
+ op: "update",
83066
83230
  collection: RETRAIN_ANNOTATIONS_COLLECTION,
83067
- key: old.id
83068
- });
83069
- } catch (err) {
83070
- this.logger.warn("retrain annotation replace: stale row delete failed", {
83071
- tags: { deviceId: old.deviceId },
83072
- meta: {
83073
- annotationId: old.id,
83074
- mediaKey,
83075
- error: String(err)
83076
- }
83077
- });
83231
+ id: row.id,
83232
+ data: { origin: origins.get(row.id) ?? null }
83233
+ })), {
83234
+ op: "update",
83235
+ collection: RETRAIN_FRAMES_COLLECTION,
83236
+ id: mediaKey,
83237
+ data: { revision: nextRevision }
83238
+ }]
83239
+ });
83240
+ if (!result.applied) {
83241
+ const current = result.current;
83242
+ return {
83243
+ kind: "conflict",
83244
+ currentRevision: typeof current === "number" ? current : null
83245
+ };
83078
83246
  }
83079
- return rows;
83247
+ return {
83248
+ kind: "updated",
83249
+ updated: eligible.length,
83250
+ revision: nextRevision
83251
+ };
83080
83252
  }
83081
83253
  async listByFrame(mediaKey) {
83082
83254
  return this.queryWhere({ mediaKey }, `frame ${mediaKey}`);
@@ -84196,7 +84368,14 @@ var RetrainLlmAssistService = class {
84196
84368
  profileId: profile.id,
84197
84369
  model: answer.model,
84198
84370
  leftHouse: leavesHouse,
84199
- proposals: parsed.objects.map((o) => ({
84371
+ proposals: parsed.objects.map((o, index) => ({
84372
+ origin: proposalOrigin("llm", index, {
84373
+ bbox: o.bbox,
84374
+ macroClass: o.macroClass,
84375
+ ...o.label !== void 0 ? { label: o.label } : {},
84376
+ score: o.confidence,
84377
+ modelId: answer.model
84378
+ }),
84200
84379
  bbox: o.bbox,
84201
84380
  macroClass: o.macroClass,
84202
84381
  ...o.label !== void 0 ? { label: o.label } : {},
@@ -84541,6 +84720,87 @@ async function serveRetrainArchive(_req, res, writeArchive) {
84541
84720
  }
84542
84721
  }
84543
84722
  //#endregion
84723
+ //#region src/pipeline-analytics/retrain/retrain-frame-handler.ts
84724
+ var RETRAIN_FRAME_PREFIX = "retrain-frame";
84725
+ /** Longest side of a `size=thumb` response. */
84726
+ var RETRAIN_THUMB_MAX_SIDE = 480;
84727
+ var CACHE_CONTROL = "private, max-age=31536000, immutable";
84728
+ /** The path `getRetrainFrameUrl`-style callers compose for a frame. */
84729
+ function retrainFramePath$1(addonId, frameId, size) {
84730
+ return `/addon/${addonId}/${RETRAIN_FRAME_PREFIX}/${encodeURIComponent(frameId)}?size=${size}`;
84731
+ }
84732
+ /** `{ frameId, size }` from the request URL (path after the prefix), or `null`. */
84733
+ function parseRetrainFrameRequest(url) {
84734
+ const queryAt = url.indexOf("?");
84735
+ const rawPath = queryAt === -1 ? url : url.slice(0, queryAt);
84736
+ const query = queryAt === -1 ? "" : url.slice(queryAt + 1);
84737
+ const segment = rawPath.replace(/^\/+/, "");
84738
+ if (segment === "" || segment.includes("/")) return null;
84739
+ let frameId;
84740
+ try {
84741
+ frameId = decodeURIComponent(segment);
84742
+ } catch {
84743
+ return null;
84744
+ }
84745
+ if (frameId === "") return null;
84746
+ const size = new URLSearchParams(query).get("size");
84747
+ if (size !== null && size !== "thumb" && size !== "full") return null;
84748
+ return {
84749
+ frameId,
84750
+ size: size ?? "full"
84751
+ };
84752
+ }
84753
+ function createRetrainFrameHandler(deps) {
84754
+ return async (req, res) => {
84755
+ const method = req.method ?? "GET";
84756
+ if (method !== "GET" && method !== "HEAD") {
84757
+ res.writeHead(405, { allow: "GET, HEAD" });
84758
+ res.end();
84759
+ return;
84760
+ }
84761
+ const request = parseRetrainFrameRequest(req.url ?? "");
84762
+ if (request === null) {
84763
+ res.writeHead(404);
84764
+ res.end();
84765
+ return;
84766
+ }
84767
+ const etag = `"${request.frameId}:${request.size}"`;
84768
+ if (req.headers["if-none-match"] === etag) {
84769
+ res.writeHead(304, {
84770
+ etag,
84771
+ "cache-control": CACHE_CONTROL
84772
+ });
84773
+ res.end();
84774
+ return;
84775
+ }
84776
+ try {
84777
+ const stored = await deps.readFrame(request.frameId);
84778
+ if (stored === null) {
84779
+ res.writeHead(404);
84780
+ res.end();
84781
+ return;
84782
+ }
84783
+ const body = request.size === "thumb" ? await deps.resize(stored, RETRAIN_THUMB_MAX_SIDE) : stored;
84784
+ res.writeHead(200, {
84785
+ "content-type": "image/jpeg",
84786
+ "content-length": String(body.byteLength),
84787
+ "cache-control": CACHE_CONTROL,
84788
+ etag
84789
+ });
84790
+ if (method === "HEAD") res.end();
84791
+ else res.end(body);
84792
+ } catch (err) {
84793
+ deps.logger.warn("retrain frame read failed", { meta: {
84794
+ frameId: request.frameId,
84795
+ size: request.size,
84796
+ error: String(err)
84797
+ } });
84798
+ res.writeHead(500, { "content-type": "text/plain" });
84799
+ res.end("retrain frame read failed");
84800
+ }
84801
+ };
84802
+ }
84803
+ //#endregion
84544
84804
  //#region src/pipeline-analytics/retrain/retrain-crop-cut.ts
84545
84805
  /**
84546
84806
  * The `crops` layout's cutter: one subject crop from a retrain frame copy.
@@ -84575,6 +84835,15 @@ function retrainCropWindow(input) {
84575
84835
  height: bottom - top
84576
84836
  };
84577
84837
  }
84838
+ /** A frame re-encoded to a JPEG whose longest side is at most `maxSide` (grid thumbnails). */
84839
+ async function resizeRetrainJpeg(bytes, maxSide) {
84840
+ return (0, sharp.default)(bytes).resize({
84841
+ width: maxSide,
84842
+ height: maxSide,
84843
+ fit: "inside",
84844
+ withoutEnlargement: true
84845
+ }).jpeg({ quality: 80 }).toBuffer();
84846
+ }
84578
84847
  async function cutRetrainCrop(input) {
84579
84848
  const window = retrainCropWindow(input);
84580
84849
  if (window === null) return null;
@@ -85677,9 +85946,27 @@ async function restageRetrainTrack(deps, input) {
85677
85946
  }
85678
85947
  //#endregion
85679
85948
  //#region src/pipeline-analytics/retrain/retrain-service.ts
85949
+ /**
85950
+ * RetrainService — the bodies behind the eleven `pipelineAnalytics.*Retrain*`
85951
+ * cap methods.
85952
+ *
85953
+ * It exists as a service rather than as eleven more methods on a 7,500-line
85954
+ * addon class for the reason `analytics-query-facade.ts` and `track-flags.ts`
85955
+ * already exist: the interesting parts (the copy ordering, the assist plan, the
85956
+ * lifecycle preconditions) are decidable from injected contracts alone, and a
85957
+ * test that had to boot the addon to reach them would not be written.
85958
+ *
85959
+ * Everything durable lives in two collections and one blob prefix:
85960
+ * `pipeline-analytics:retrain-frames` (the copies),
85961
+ * `pipeline-analytics:retrain-annotations` (the ground truth), and
85962
+ * `retrain/<deviceId>/<trackId>/…` (the pixels). No age sweep and no owner
85963
+ * cascade walks any of them — that is the whole point of copying.
85964
+ */
85680
85965
  /** Default page size for the staging worklist. Staging is bounded at 500 per
85681
85966
  * camera (D81), so this is a page, not a ceiling. */
85682
85967
  var DEFAULT_STAGING_LIMIT = 100;
85968
+ /** Tries a last-write-wins save gets when it keeps losing the revision race. */
85969
+ var LAST_WRITE_WINS_ATTEMPTS = 3;
85683
85970
  /** A macro class that failed validation — the write is refused, never coerced. */
85684
85971
  var RetrainAnnotationError = class extends Error {};
85685
85972
  /** A store frame on the wire. A recording still carries no track and no
@@ -85700,7 +85987,31 @@ function toWireFrame(f) {
85700
85987
  height: f.height,
85701
85988
  copiedAt: f.copiedAt,
85702
85989
  ...f.capturedAt !== void 0 ? { capturedAt: f.capturedAt } : {},
85703
- complete: f.complete
85990
+ complete: f.complete,
85991
+ revision: f.revision ?? 0,
85992
+ dismissed: f.dismissed ?? []
85993
+ };
85994
+ }
85995
+ /**
85996
+ * What the operator did with the proposals on a frame (`retrain-review.ts`).
85997
+ *
85998
+ * A frame whose rows predate origins is judged against the pipeline boxes
85999
+ * re-derived now, and a proposal nobody kept is reported `removed` — both
86000
+ * flagged approximate. A frame with snapshots is judged on them alone.
86001
+ */
86002
+ function reviewOfFrame(annotations, pipelineBoxes, dismissed) {
86003
+ const legacy = annotations.some((a) => !a.origin);
86004
+ const review = require_clip_model_registry.classifyRetrainReview(annotations.map((a) => ({
86005
+ id: a.id,
86006
+ bbox: a.bbox,
86007
+ macroClass: a.macroClass,
86008
+ ...a.label !== void 0 ? { label: a.label } : {},
86009
+ kind: a.kind,
86010
+ origin: a.origin ?? null
86011
+ })), pipelineBoxes.map(pipelineBoxOrigin), dismissed, { inferRemoved: legacy });
86012
+ return {
86013
+ boxes: review.boxes,
86014
+ summary: require_clip_model_registry.summarizeReview(review)
85704
86015
  };
85705
86016
  }
85706
86017
  function toWireAnnotation(a) {
@@ -85717,6 +86028,7 @@ function toWireAnnotation(a) {
85717
86028
  source: a.source,
85718
86029
  ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
85719
86030
  ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
86031
+ origin: a.origin ?? null,
85720
86032
  ...a.exportedInBatch !== void 0 ? { exportedInBatch: a.exportedInBatch } : {},
85721
86033
  createdAt: a.createdAt
85722
86034
  };
@@ -85798,16 +86110,24 @@ var RetrainService = class {
85798
86110
  const evidence = this.deps.pipelineEvidence;
85799
86111
  const ownTracks = /* @__PURE__ */ new Map();
85800
86112
  const subjectBoxes = /* @__PURE__ */ new Map();
86113
+ const annotationsByFrame = await this.annotationsByFrame(copies);
85801
86114
  const out = [];
85802
86115
  for (const copy of copies) {
85803
86116
  const wire = toWireFrame(copy);
85804
86117
  const collectionIds = collections?.get(copy.frameId);
85805
- const withCollections = collections !== null ? {
85806
- ...wire,
85807
- collectionIds: collectionIds ?? []
85808
- } : wire;
86118
+ const withCollections = {
86119
+ ...collections !== null ? {
86120
+ ...wire,
86121
+ collectionIds: collectionIds ?? []
86122
+ } : wire,
86123
+ ...this.frameUrls(copy.frameId)
86124
+ };
86125
+ const reviewOf = (boxes) => reviewOfFrame(annotationsByFrame.get(copy.frameId) ?? [], boxes, copy.dismissed ?? []);
85809
86126
  if (evidence === void 0) {
85810
- out.push(withCollections);
86127
+ out.push({
86128
+ ...withCollections,
86129
+ review: reviewOf([])
86130
+ });
85811
86131
  continue;
85812
86132
  }
85813
86133
  const trackId = copy.origin === "track" ? copy.trackId : "";
@@ -85828,11 +86148,32 @@ var RetrainService = class {
85828
86148
  });
85829
86149
  out.push({
85830
86150
  ...withCollections,
85831
- pipelineBoxes
86151
+ pipelineBoxes,
86152
+ review: reviewOf(pipelineBoxes)
85832
86153
  });
85833
86154
  }
85834
86155
  return out;
85835
86156
  }
86157
+ /** The URLs of a frame's pixels, when a data plane is served for them. */
86158
+ frameUrls(frameId) {
86159
+ const frameUrl = this.deps.frameUrl;
86160
+ if (frameUrl === void 0) return {};
86161
+ return {
86162
+ imageUrl: frameUrl(frameId, "full"),
86163
+ thumbnailUrl: frameUrl(frameId, "thumb")
86164
+ };
86165
+ }
86166
+ /** The saved annotations of these frames, grouped by frame. */
86167
+ async annotationsByFrame(copies) {
86168
+ const grouped = /* @__PURE__ */ new Map();
86169
+ if (copies.length === 0) return grouped;
86170
+ for (const a of await this.deps.annotations.listByFrames(copies.map((c) => c.frameId))) {
86171
+ const list = grouped.get(a.mediaKey);
86172
+ if (list === void 0) grouped.set(a.mediaKey, [a]);
86173
+ else list.push(a);
86174
+ }
86175
+ return grouped;
86176
+ }
85836
86177
  /**
85837
86178
  * The frame a frame-scoped call names, or a refusal. The frame is the
85838
86179
  * authority: its camera must be the caller's, and a `trackId`, when the
@@ -85919,7 +86260,10 @@ var RetrainService = class {
85919
86260
  sourceKind: kindOf.get(mediaId) ?? "keyFrame",
85920
86261
  ...capturedAtOf.has(mediaId) ? { capturedAt: capturedAtOf.get(mediaId) } : {}
85921
86262
  });
85922
- if (result.kind === "copied") copied.push(toWireFrame(result.frame));
86263
+ if (result.kind === "copied") copied.push({
86264
+ ...toWireFrame(result.frame),
86265
+ ...this.frameUrls(result.frame.frameId)
86266
+ });
85923
86267
  else refused.push({
85924
86268
  sourceMediaId: result.sourceMediaId,
85925
86269
  reason: result.reason
@@ -85946,7 +86290,10 @@ var RetrainService = class {
85946
86290
  complete: input.complete
85947
86291
  }
85948
86292
  });
85949
- return toWireFrame(updated);
86293
+ return {
86294
+ ...toWireFrame(updated),
86295
+ ...this.frameUrls(updated.frameId)
86296
+ };
85950
86297
  }
85951
86298
  /** Un-select: annotations first, then the copy. Never the other way round —
85952
86299
  * an annotation whose frame is gone is unopenable and unfixable. The frame's
@@ -86087,9 +86434,16 @@ var RetrainService = class {
86087
86434
  modelId: result.modelId,
86088
86435
  stepId: plan.stepId,
86089
86436
  minScore: plan.minScore,
86090
- proposals: mapping.proposals.map((p) => {
86437
+ proposals: mapping.proposals.map((p, index) => {
86091
86438
  const draft = proposalToDraft(p, owner);
86092
86439
  return {
86440
+ origin: proposalOrigin("assist", index, {
86441
+ bbox: draft.bbox,
86442
+ macroClass: draft.macroClass,
86443
+ ...draft.label !== void 0 ? { label: draft.label } : {},
86444
+ ...draft.assistScore !== void 0 ? { score: draft.assistScore } : {},
86445
+ ...draft.assistModelId !== void 0 ? { modelId: draft.assistModelId } : {}
86446
+ }),
86093
86447
  bbox: draft.bbox,
86094
86448
  macroClass: draft.macroClass,
86095
86449
  ...draft.label !== void 0 ? { label: draft.label } : {},
@@ -86136,9 +86490,12 @@ var RetrainService = class {
86136
86490
  kind: a.kind,
86137
86491
  source: a.source,
86138
86492
  ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
86139
- ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {}
86493
+ ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
86494
+ origin: a.origin ?? null
86140
86495
  }));
86141
- const saved = await this.deps.annotations.replaceForFrame(input.frameId, drafts);
86496
+ const keptRefs = new Set(drafts.flatMap((d) => d.origin ? [d.origin.ref] : []));
86497
+ const dismissed = input.dismissed !== void 0 ? input.dismissed.filter((d) => !keptRefs.has(d.ref)) : void 0;
86498
+ const saved = await this.replaceGuarded(frame, input, drafts, dismissed);
86142
86499
  const errors = saved.filter((a) => a.kind === "model_error").length;
86143
86500
  this.deps.logger.info("retrain annotations saved", {
86144
86501
  tags: { deviceId: input.deviceId },
@@ -86147,12 +86504,99 @@ var RetrainService = class {
86147
86504
  trackId: frame.trackId,
86148
86505
  frameId: input.frameId,
86149
86506
  subjects: saved.length - errors,
86150
- modelErrors: errors
86507
+ modelErrors: errors,
86508
+ dismissed: dismissed?.length
86151
86509
  }
86152
86510
  });
86153
86511
  return saved.map(toWireAnnotation);
86154
86512
  }
86155
86513
  /**
86514
+ * The guarded replace. A caller that names the revision it saw gets exactly
86515
+ * one attempt: a different current revision is a conflict, never a retry —
86516
+ * the point is that it must look again. A caller that names none asked for
86517
+ * last-write-wins, so a race lost between our read and the transaction is
86518
+ * retried on the fresh revision, a bounded number of times.
86519
+ */
86520
+ async replaceGuarded(frame, input, drafts, dismissed) {
86521
+ return await this.underRevision(frame, input, async (revision) => {
86522
+ const result = await this.deps.annotations.replaceForFrame(input.frameId, drafts, {
86523
+ fromRevision: revision,
86524
+ ...dismissed !== void 0 ? { dismissed } : {}
86525
+ });
86526
+ return result.kind === "saved" ? {
86527
+ kind: "done",
86528
+ value: result.rows
86529
+ } : {
86530
+ kind: "conflict",
86531
+ currentRevision: result.currentRevision
86532
+ };
86533
+ });
86534
+ }
86535
+ /**
86536
+ * Run one revision-guarded write. `attempt` is handed the revision to guard
86537
+ * on and reports `done` or `conflict` (the frame moved; `null` = it is gone).
86538
+ */
86539
+ async underRevision(frame, input, attempt) {
86540
+ let revision = frame.revision ?? 0;
86541
+ for (let tries = 0; tries < LAST_WRITE_WINS_ATTEMPTS; tries += 1) {
86542
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== revision) this.refuseStale(input, revision, input.expectedRevision);
86543
+ const result = await attempt(revision);
86544
+ if (result.kind === "done") return result.value;
86545
+ if (result.currentRevision === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} vanished`);
86546
+ revision = result.currentRevision;
86547
+ if (input.expectedRevision !== void 0) this.refuseStale(input, revision, input.expectedRevision);
86548
+ }
86549
+ throw new RetrainAnnotationError(`retrain frame ${input.frameId} kept changing under the save — try again`);
86550
+ }
86551
+ /**
86552
+ * Give rows saved before origins existed the origin a caller reconstructed
86553
+ * for them — and touch nothing else (ids, creation time and the export stamp
86554
+ * of the row stay). Only rows that have NO origin yet are written; a row that
86555
+ * already has one is skipped, because a snapshot taken at accept time is
86556
+ * evidence and a reconstruction must never overwrite it.
86557
+ */
86558
+ async backfillOrigins(input) {
86559
+ const frame = await this.requireFrame(input, "backfill-origins");
86560
+ const wanted = new Map(input.origins.map((o) => [o.annotationId, o.origin]));
86561
+ const result = await this.underRevision(frame, input, async (revision) => {
86562
+ const outcome = await this.deps.annotations.setOrigins(input.frameId, wanted, { fromRevision: revision });
86563
+ return outcome.kind === "updated" ? {
86564
+ kind: "done",
86565
+ value: outcome
86566
+ } : {
86567
+ kind: "conflict",
86568
+ currentRevision: outcome.currentRevision
86569
+ };
86570
+ });
86571
+ this.deps.logger.info("retrain origins backfilled", {
86572
+ tags: { deviceId: input.deviceId },
86573
+ meta: {
86574
+ frameId: input.frameId,
86575
+ updated: result.updated,
86576
+ skipped: input.origins.length - result.updated
86577
+ }
86578
+ });
86579
+ return {
86580
+ updated: result.updated,
86581
+ skipped: input.origins.length - result.updated,
86582
+ revision: result.revision
86583
+ };
86584
+ }
86585
+ refuseStale(input, current, expected) {
86586
+ this.deps.logger.warn("retrain annotations save refused — the frame moved on", {
86587
+ tags: { deviceId: input.deviceId },
86588
+ meta: {
86589
+ frameId: input.frameId,
86590
+ currentRevision: current,
86591
+ expectedRevision: expected
86592
+ }
86593
+ });
86594
+ throw new RetrainAnnotationError(require_clip_model_registry.retrainRevisionConflictMessage(input.frameId, {
86595
+ currentRevision: current,
86596
+ expectedRevision: expected
86597
+ }));
86598
+ }
86599
+ /**
86156
86600
  * Paginated corpus list for the Frames grid. Index reads only — copies,
86157
86601
  * annotation counts/classes, collection membership; no JPEG.
86158
86602
  */
@@ -111351,6 +111795,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
111351
111795
  trainingExportDataPlane = null;
111352
111796
  /** Handle for the retrain-export data-plane listener (dispose on shutdown). */
111353
111797
  retrainExportDataPlane = null;
111798
+ /** Handle for the retrain-frame data-plane listener (dispose on shutdown). */
111799
+ retrainFrameDataPlane = null;
111354
111800
  /** Handle for the debug-export data-plane listener (dispose on shutdown). */
111355
111801
  debugExportDataPlane = null;
111356
111802
  /** Handle for the tape-export data-plane listener (dispose on shutdown). */
@@ -113448,7 +113894,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113448
113894
  }),
113449
113895
  subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
113450
113896
  },
113451
- memberships: retrainCollectionStore
113897
+ memberships: retrainCollectionStore,
113898
+ frameUrl: (frameId, size) => retrainFramePath$1(this.ctx.id, frameId, size)
113452
113899
  });
113453
113900
  this.retrain = retrain;
113454
113901
  const retrainStills = new RecordedStillService(buildRecordedStillPorts({
@@ -114552,6 +114999,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114552
114999
  await this.serveNcArtifactPlane();
114553
115000
  await this.serveTrainingExportPlane();
114554
115001
  await this.serveRetrainExportPlane();
115002
+ await this.serveRetrainFramePlane();
114555
115003
  await this.serveDebugExportPlane();
114556
115004
  await this.serveTapeExportPlane();
114557
115005
  this.ctx.logger.info("event-media data-plane served", { meta: {
@@ -114703,6 +115151,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
114703
115151
  async saveRetrainAnnotations(input) {
114704
115152
  return this.requireRetrain().saveAnnotations(input);
114705
115153
  }
115154
+ async backfillRetrainOrigins(input) {
115155
+ return this.requireRetrain().backfillOrigins(input);
115156
+ }
114706
115157
  async completeRetrainTrack(input) {
114707
115158
  return this.requireRetrain().complete(input);
114708
115159
  }
@@ -115183,6 +115634,31 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115183
115634
  }
115184
115635
  }
115185
115636
  /**
115637
+ * The retrain-frame data plane: one copied frame's JPEG (`?size=thumb|full`).
115638
+ * Admin-only like the cap that used to serve it as base64 (D613).
115639
+ */
115640
+ async serveRetrainFramePlane() {
115641
+ try {
115642
+ const handler = createRetrainFrameHandler({
115643
+ readFrame: async (frameId) => {
115644
+ const frames = this.retrainFrames;
115645
+ if (frames === null) return null;
115646
+ const frame = await frames.get(frameId);
115647
+ return frame === null ? null : frames.readFrame(frame);
115648
+ },
115649
+ resize: resizeRetrainJpeg,
115650
+ logger: this.ctx.logger
115651
+ });
115652
+ this.retrainFrameDataPlane = await this.ctx.dataPlane?.serve({
115653
+ prefix: RETRAIN_FRAME_PREFIX,
115654
+ access: planeAccess("retrain-frame"),
115655
+ handler
115656
+ }) ?? null;
115657
+ } catch (err) {
115658
+ this.ctx.logger.warn("retrain-frame data-plane failed to serve", { meta: { error: require_clip_model_registry.errMsg(err) } });
115659
+ }
115660
+ }
115661
+ /**
115186
115662
  * Plan the annotated export — INDEX READS ONLY, no blob touched.
115187
115663
  *
115188
115664
  * The zone used for the `zoneCrop` derivation is the bounding box of the
@@ -115964,6 +116440,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
115964
116440
  this.trainingExportDataPlane = null;
115965
116441
  await this.retrainExportDataPlane?.dispose();
115966
116442
  this.retrainExportDataPlane = null;
116443
+ await this.retrainFrameDataPlane?.dispose();
116444
+ this.retrainFrameDataPlane = null;
115967
116445
  await this.debugExportDataPlane?.dispose();
115968
116446
  this.debugExportDataPlane = null;
115969
116447
  await this.tapeExportDataPlane?.dispose();