@camstack/addon-post-analysis 1.2.344 → 1.2.346

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 (16) hide show
  1. package/assets/pipeline-analytics/icon.svg +7 -0
  2. package/dist/{clip-model-registry-Ca_IbZyD.js → clip-model-registry--UsI8Ud4.js} +175 -2
  3. package/dist/{clip-model-registry-C44gfwjm.mjs → clip-model-registry-BftmbgcV.mjs} +175 -2
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D73dyYqt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CeJTtS8B.mjs} +3 -3
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-4Fu7jNn9.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Cutp7k0_.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-h_8fvfFv.mjs → hostInit-DkInRTZ0.mjs} +3 -3
  11. package/dist/pipeline-analytics/index.js +851 -289
  12. package/dist/pipeline-analytics/index.mjs +851 -289
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C-2BbuN4.mjs +0 -26
  16. 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 +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-Ca_IbZyD.js");
5
+ const require_clip_model_registry = require("../clip-model-registry--UsI8Ud4.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");
@@ -12922,7 +12922,7 @@ function stringField(data, key) {
12922
12922
  const value = data[key];
12923
12923
  return typeof value === "string" && value.length > 0 ? value : void 0;
12924
12924
  }
12925
- function stringList(data, key) {
12925
+ function stringList$1(data, key) {
12926
12926
  const raw = data[key];
12927
12927
  if (!Array.isArray(raw)) return [];
12928
12928
  const ids = [];
@@ -13734,7 +13734,7 @@ function incomingFromExportCompleted(facts, data) {
13734
13734
  const deviceId = numberField(data, "deviceId");
13735
13735
  const title = stringField(data, "title");
13736
13736
  const bytes = numberField(data, "fileBytes");
13737
- const notifyTargetIds = stringList(data, "notifyTargetIds");
13737
+ const notifyTargetIds = stringList$1(data, "notifyTargetIds");
13738
13738
  const incoming = systemSubject({
13739
13739
  facts,
13740
13740
  kind: "export-completed",
@@ -75018,7 +75018,7 @@ function describeTapeStatus(input) {
75018
75018
  * `viewer-settings-legacy-adoption.ts`.
75019
75019
  */
75020
75020
  var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
75021
- var COLUMNS = [
75021
+ var COLUMNS$1 = [
75022
75022
  {
75023
75023
  name: "id",
75024
75024
  type: "TEXT",
@@ -75089,7 +75089,7 @@ function requireCaller(caller) {
75089
75089
  if (!caller) throw new Error("viewerSettings: authenticated caller required");
75090
75090
  return caller;
75091
75091
  }
75092
- function asRecord(data) {
75092
+ function asRecord$1(data) {
75093
75093
  return typeof data === "object" && data !== null ? data : {};
75094
75094
  }
75095
75095
  function toSummary(id, data, callerUserId) {
@@ -75115,7 +75115,7 @@ var ViewerSettingsSnapshotStore = class {
75115
75115
  static async declare(store) {
75116
75116
  await store.declareCollection.mutate({
75117
75117
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75118
- columns: [...COLUMNS],
75118
+ columns: [...COLUMNS$1],
75119
75119
  indexes: [{
75120
75120
  name: "idx_pa_viewer_settings_owner",
75121
75121
  columns: ["ownerUserId"]
@@ -75135,7 +75135,7 @@ var ViewerSettingsSnapshotStore = class {
75135
75135
  });
75136
75136
  const out = [];
75137
75137
  for (const row of rows) {
75138
- const summary = toSummary(row.id, asRecord(row.data), caller.userId);
75138
+ const summary = toSummary(row.id, asRecord$1(row.data), caller.userId);
75139
75139
  if (summary) out.push(summary);
75140
75140
  }
75141
75141
  return out;
@@ -75149,7 +75149,7 @@ var ViewerSettingsSnapshotStore = class {
75149
75149
  }
75150
75150
  }))[0];
75151
75151
  if (!row) return null;
75152
- const data = asRecord(row.data);
75152
+ const data = asRecord$1(row.data);
75153
75153
  const summary = toSummary(row.id, data, caller.userId);
75154
75154
  if (!summary) return null;
75155
75155
  const blob = BlobSchema.safeParse(data.blob);
@@ -75172,7 +75172,7 @@ var ViewerSettingsSnapshotStore = class {
75172
75172
  }
75173
75173
  }))[0];
75174
75174
  const id = prior?.id ?? (0, node_crypto.randomUUID)();
75175
- const createdAt = typeof asRecord(prior?.data).createdAt === "number" ? asRecord(prior?.data).createdAt : now;
75175
+ const createdAt = typeof asRecord$1(prior?.data).createdAt === "number" ? asRecord$1(prior?.data).createdAt : now;
75176
75176
  await this.store.set.mutate({
75177
75177
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75178
75178
  key: id,
@@ -83020,7 +83020,10 @@ var RetrainCollectionStore = class {
83020
83020
  deleted: false,
83021
83021
  memberFrameIds: []
83022
83022
  };
83023
- const members = await this.membershipRows({ collectionId: id });
83023
+ const members = await this.store.query.query({
83024
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
83025
+ filter: { where: { collectionId: id } }
83026
+ });
83024
83027
  for (const m of members) await this.store.delete.mutate({
83025
83028
  collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
83026
83029
  key: m.id
@@ -83375,6 +83378,787 @@ var RetrainCollectionService = class {
83375
83378
  }
83376
83379
  };
83377
83380
  //#endregion
83381
+ //#region src/pipeline-analytics/retrain/retrain-dataset.ts
83382
+ /**
83383
+ * Dataset versions — the counts a version is judged by, and the hash that
83384
+ * freezes what it teaches. Pure.
83385
+ *
83386
+ * Both read the export MANIFEST (`retrain-export.ts`), not the stores: a
83387
+ * version is exactly what an export of its frames would carry, so its counts
83388
+ * and its hash cannot disagree with the archive a trainer receives.
83389
+ */
83390
+ function lightingOf(frame) {
83391
+ if (frame.lighting === null) return "unknown";
83392
+ return frame.lighting.night ? "night" : "day";
83393
+ }
83394
+ /** The counts of a dataset, from the manifest an export of it would carry. */
83395
+ function datasetStats(manifest) {
83396
+ const cells = /* @__PURE__ */ new Map();
83397
+ const cameras = /* @__PURE__ */ new Map();
83398
+ let completeFrames = 0;
83399
+ let subjects = 0;
83400
+ let modelErrors = 0;
83401
+ let hardNegatives = 0;
83402
+ for (const frame of manifest.frames) {
83403
+ const camera = cameras.get(frame.deviceId) ?? {
83404
+ frames: 0,
83405
+ completeFrames: 0
83406
+ };
83407
+ cameras.set(frame.deviceId, {
83408
+ frames: camera.frames + 1,
83409
+ completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
83410
+ });
83411
+ if (frame.complete) completeFrames += 1;
83412
+ const lighting = lightingOf(frame);
83413
+ let frameSubjects = 0;
83414
+ for (const a of frame.annotations) {
83415
+ if (a.kind === "model_error") {
83416
+ modelErrors += 1;
83417
+ continue;
83418
+ }
83419
+ frameSubjects += 1;
83420
+ const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
83421
+ const entry = cells.get(key) ?? {
83422
+ cell: {
83423
+ macroClass: a.macroClass,
83424
+ deviceId: frame.deviceId,
83425
+ lighting,
83426
+ subjects: 0,
83427
+ frames: 0
83428
+ },
83429
+ frameIds: /* @__PURE__ */ new Set()
83430
+ };
83431
+ entry.frameIds.add(frame.frameId);
83432
+ cells.set(key, {
83433
+ cell: {
83434
+ ...entry.cell,
83435
+ subjects: entry.cell.subjects + 1,
83436
+ frames: entry.frameIds.size
83437
+ },
83438
+ frameIds: entry.frameIds
83439
+ });
83440
+ }
83441
+ subjects += frameSubjects;
83442
+ if (frame.complete && frameSubjects === 0) hardNegatives += 1;
83443
+ }
83444
+ const frames = manifest.frames.length;
83445
+ return {
83446
+ frames,
83447
+ completeFrames,
83448
+ completeness: frames > 0 ? completeFrames / frames : 0,
83449
+ subjects,
83450
+ modelErrors,
83451
+ hardNegatives,
83452
+ cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
83453
+ cameras: [...cameras.entries()].map(([deviceId, c]) => ({
83454
+ deviceId,
83455
+ ...c
83456
+ })).toSorted((a, b) => a.deviceId - b.deviceId)
83457
+ };
83458
+ }
83459
+ /** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
83460
+ var q = (n) => Math.round(n * 1e4) / 1e4;
83461
+ /**
83462
+ * The sha256 of what the frames TEACH: per frame its completeness and, per
83463
+ * box, the class tiers, the kind and the box. Row ids, timestamps, export
83464
+ * stamps and provenance are left out — they change on a re-save that teaches
83465
+ * the same thing. Order-independent.
83466
+ */
83467
+ function datasetAnnotationHash(frames) {
83468
+ const canonical = frames.map((f) => ({
83469
+ f: f.frameId,
83470
+ c: f.complete,
83471
+ a: f.annotations.map((a) => [
83472
+ a.macroClass,
83473
+ a.label ?? "",
83474
+ a.subLabel ?? "",
83475
+ a.kind,
83476
+ q(a.bbox.x),
83477
+ q(a.bbox.y),
83478
+ q(a.bbox.w),
83479
+ q(a.bbox.h)
83480
+ ].join("|")).toSorted()
83481
+ })).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
83482
+ return (0, node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
83483
+ }
83484
+ /**
83485
+ * The manifest of a version's export, saying which version and whether its
83486
+ * frames still teach what was frozen. Frames of the version missing from the
83487
+ * archive count as empty and incomplete, so their absence changes the hash.
83488
+ */
83489
+ function withDatasetVersion(manifest, version) {
83490
+ const present = new Map(manifest.frames.map((f) => [f.frameId, f]));
83491
+ const annotationHashNow = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
83492
+ frameId,
83493
+ complete: false,
83494
+ annotations: []
83495
+ }));
83496
+ return {
83497
+ ...manifest,
83498
+ datasetVersion: {
83499
+ id: version.id,
83500
+ name: version.name,
83501
+ createdAt: version.createdAt,
83502
+ annotationHash: version.annotationHash,
83503
+ annotationHashNow,
83504
+ matches: annotationHashNow === version.annotationHash,
83505
+ missingFrames: version.frameIds.filter((id) => !present.has(id)).length
83506
+ }
83507
+ };
83508
+ }
83509
+ //#endregion
83510
+ //#region src/pipeline-analytics/retrain/retrain-geometry.ts
83511
+ function clamp01(v) {
83512
+ return v < 0 ? 0 : v > 1 ? 1 : v;
83513
+ }
83514
+ /** Is this a usable rectangle at all? A zero-area box is not a subject. */
83515
+ function isUsableRect(r) {
83516
+ return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
83517
+ }
83518
+ /**
83519
+ * The subject inside a square LETTERBOX of the full frame.
83520
+ *
83521
+ * The frame is scaled to fit the square and centred; the padding bars take the
83522
+ * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
83523
+ * height, and every y coordinate shifts by half the leftover.
83524
+ *
83525
+ * Square-side agnostic on purpose: the answer is normalised against the square,
83526
+ * so it is the same number for a 640 and a 1280 input and no caller has to know
83527
+ * which the model uses.
83528
+ */
83529
+ function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
83530
+ if (!isUsableRect(bbox)) return null;
83531
+ if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
83532
+ const longSide = Math.max(frameWidth, frameHeight);
83533
+ const scaleX = frameWidth / longSide;
83534
+ const scaleY = frameHeight / longSide;
83535
+ const offsetX = (1 - scaleX) / 2;
83536
+ const offsetY = (1 - scaleY) / 2;
83537
+ return {
83538
+ bbox: {
83539
+ x: offsetX + bbox.x * scaleX,
83540
+ y: offsetY + bbox.y * scaleY,
83541
+ w: bbox.w * scaleX,
83542
+ h: bbox.h * scaleY
83543
+ },
83544
+ visibleFraction: 1
83545
+ };
83546
+ }
83547
+ /**
83548
+ * The subject inside a ZONE CROP — the shape a package model is trained on.
83549
+ *
83550
+ * Returns `null` when the subject does not intersect the zone at all. That is
83551
+ * the whole reason the package proposal runs on the crop rather than the frame:
83552
+ * a package detector at 0.35 on a full frame proposes furniture, and a subject
83553
+ * outside the zone is not part of this model's world.
83554
+ */
83555
+ function deriveZoneCropRect(bbox, zone) {
83556
+ if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
83557
+ const left = Math.max(bbox.x, zone.x);
83558
+ const top = Math.max(bbox.y, zone.y);
83559
+ const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
83560
+ const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
83561
+ if (!(right > left) || !(bottom > top)) return null;
83562
+ const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
83563
+ return {
83564
+ bbox: {
83565
+ x: (left - zone.x) / zone.w,
83566
+ y: (top - zone.y) / zone.h,
83567
+ w: (right - left) / zone.w,
83568
+ h: (bottom - top) / zone.h
83569
+ },
83570
+ visibleFraction
83571
+ };
83572
+ }
83573
+ /**
83574
+ * The subject inside its OWN padded crop — the shape a classifier is trained on.
83575
+ *
83576
+ * The crop is the box grown by `padding` of its own size on each side and then
83577
+ * clipped to the frame, so the subject sits in the middle and the classifier
83578
+ * still sees a little context. The returned rectangle is where the subject
83579
+ * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
83580
+ * clipped by a frame edge — a subject at the left border is flush left in its
83581
+ * crop, and a caller that assumed the centre would draw the box in the wrong
83582
+ * place.
83583
+ */
83584
+ function deriveSubjectCropRect(bbox, padding) {
83585
+ if (!isUsableRect(bbox)) return null;
83586
+ const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
83587
+ const cropX = clamp01(bbox.x - bbox.w * pad);
83588
+ const cropY = clamp01(bbox.y - bbox.h * pad);
83589
+ const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
83590
+ const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
83591
+ const cropW = cropRight - cropX;
83592
+ const cropH = cropBottom - cropY;
83593
+ if (!(cropW > 0) || !(cropH > 0)) return null;
83594
+ return {
83595
+ bbox: {
83596
+ x: (bbox.x - cropX) / cropW,
83597
+ y: (bbox.y - cropY) / cropH,
83598
+ w: bbox.w / cropW,
83599
+ h: bbox.h / cropH
83600
+ },
83601
+ visibleFraction: 1
83602
+ };
83603
+ }
83604
+ //#endregion
83605
+ //#region src/pipeline-analytics/retrain/retrain-lighting.ts
83606
+ function isNight(condition) {
83607
+ return condition === "night" || condition === "ir";
83608
+ }
83609
+ function retrainFrameLighting(input) {
83610
+ if (input.capturedAt === void 0) {
83611
+ const measured = resolveConditionDetailed({
83612
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
83613
+ now: /* @__PURE__ */ new Date(0)
83614
+ });
83615
+ return measured.source === "ir-chroma" ? {
83616
+ condition: measured.condition,
83617
+ night: true,
83618
+ source: measured.source
83619
+ } : null;
83620
+ }
83621
+ const resolved = resolveConditionDetailed({
83622
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
83623
+ ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
83624
+ now: new Date(input.capturedAt)
83625
+ });
83626
+ return {
83627
+ ...resolved,
83628
+ night: isNight(resolved.condition)
83629
+ };
83630
+ }
83631
+ //#endregion
83632
+ //#region src/pipeline-analytics/retrain/retrain-export.ts
83633
+ /**
83634
+ * The ANNOTATED dataset export — the retrain page's own archive.
83635
+ *
83636
+ * Sibling of `training-export/`, deliberately not the same thing. That one
83637
+ * streams a marked track's stored media verbatim and knows nothing about what
83638
+ * is in it; this one streams the frames the operator SELECTED (already copied
83639
+ * into retrain storage) plus the ground truth they drew.
83640
+ *
83641
+ * ## The one interesting decision
83642
+ *
83643
+ * `annotations.json` carries, for every subject, the canonical full-frame box
83644
+ * AND the geometry derived for each model shape. **Derived here, at export,
83645
+ * never stored** — one box in, three shapes out. Storing the derived shapes is
83646
+ * how two crops of the same subject end up in one feature space, which is the
83647
+ * failure already recorded for CLIP (D52). A consumer that wants a different
83648
+ * input size or a different padding re-derives from the canonical box; nothing
83649
+ * in the database has to change and nothing can disagree.
83650
+ *
83651
+ * Everything else follows the export next door: index reads to plan, one JPEG
83652
+ * in memory at a time, a yield between files, and NO inference — a training
83653
+ * set's pixels are the pixels the pipeline saw.
83654
+ */
83655
+ var RETRAIN_EXPORT_PREFIX = "retrain-export";
83656
+ /**
83657
+ * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
83658
+ * padding, read from the one convention `deriveDetailCropRect` cuts with
83659
+ * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
83660
+ * here while the runtime fed `0.15`, so a classifier trained from this export
83661
+ * learned on more context than it is ever given live.
83662
+ */
83663
+ var CLASSIFIER_CROP_PADDING = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
83664
+ /** The convention a `crops` layout cuts with — the runtime default, recorded
83665
+ * in the manifest so a consumer never has to assume it. */
83666
+ var RETRAIN_CROP_CONVENTION = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION;
83667
+ /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
83668
+ * the trainer resizes per model, so this only bounds the upload. */
83669
+ var RETRAIN_CROP_MAX_SIDE = 256;
83670
+ /**
83671
+ * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
83672
+ * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
83673
+ * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
83674
+ */
83675
+ var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
83676
+ /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
83677
+ * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
83678
+ function retrainFramePath(frame) {
83679
+ const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
83680
+ const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
83681
+ return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
83682
+ }
83683
+ /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
83684
+ function retrainCropPath(macroClass, annotationId) {
83685
+ return `crops/${macroClass}/${annotationId}.jpg`;
83686
+ }
83687
+ /**
83688
+ * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
83689
+ * filter keeps everything. Order is the input's.
83690
+ */
83691
+ function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
83692
+ const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
83693
+ const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
83694
+ const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
83695
+ return frames.filter((frame) => {
83696
+ if (wanted !== null && !wanted.has(frame.frameId)) return false;
83697
+ if (devices !== null && !devices.has(frame.deviceId)) return false;
83698
+ if (filter.completeOnly === true && !frame.complete) return false;
83699
+ if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
83700
+ if (filter.batchId !== void 0) {
83701
+ if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
83702
+ }
83703
+ return true;
83704
+ });
83705
+ }
83706
+ /**
83707
+ * Derive every per-model shape for one annotation.
83708
+ *
83709
+ * `zone` is optional and per-frame: only a package annotation has one, and a
83710
+ * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
83711
+ * clamped — a clamped box teaches the model that the subject ends where the
83712
+ * crop ends.
83713
+ */
83714
+ function deriveAll(annotation, frame, zone) {
83715
+ const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
83716
+ const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
83717
+ const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
83718
+ return {
83719
+ ...letterbox !== null ? { letterbox } : {},
83720
+ ...zoneCrop !== null ? { zoneCrop } : {},
83721
+ ...subjectCrop !== null ? { subjectCrop } : {}
83722
+ };
83723
+ }
83724
+ /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
83725
+ function buildRetrainManifest(input) {
83726
+ const layout = input.layout ?? "frames";
83727
+ const coordinates = input.coordinates ?? null;
83728
+ let annotationCount = 0;
83729
+ let modelErrorCount = 0;
83730
+ const frames = input.frames.map((frame) => {
83731
+ const zone = input.zoneForDevice(frame.deviceId);
83732
+ const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
83733
+ annotationCount += 1;
83734
+ if (a.kind === "model_error") modelErrorCount += 1;
83735
+ const derived = deriveAll(a, frame, zone);
83736
+ return {
83737
+ id: a.id,
83738
+ macroClass: a.macroClass,
83739
+ ...a.label !== void 0 ? { label: a.label } : {},
83740
+ ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
83741
+ kind: a.kind,
83742
+ source: a.source,
83743
+ ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
83744
+ ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
83745
+ bbox: a.bbox,
83746
+ ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
83747
+ derived
83748
+ };
83749
+ });
83750
+ return {
83751
+ frameId: frame.frameId,
83752
+ path: retrainFramePath(frame),
83753
+ deviceId: frame.deviceId,
83754
+ origin: frame.origin,
83755
+ ...frame.origin === "track" ? { trackId: frame.trackId } : {},
83756
+ ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
83757
+ collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
83758
+ width: frame.width,
83759
+ height: frame.height,
83760
+ sizeBytes: frame.sizeBytes,
83761
+ sourceKind: frame.sourceKind,
83762
+ copiedAt: frame.copiedAt,
83763
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
83764
+ complete: frame.complete,
83765
+ lighting: retrainFrameLighting({
83766
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
83767
+ ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
83768
+ coordinates
83769
+ }),
83770
+ ...zone !== null ? { zoneUsed: zone } : {},
83771
+ annotations
83772
+ };
83773
+ });
83774
+ return {
83775
+ schema: RETRAIN_MANIFEST_SCHEMA,
83776
+ layout,
83777
+ detailCropConvention: RETRAIN_CROP_CONVENTION,
83778
+ ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
83779
+ generatedAt: input.generatedAt,
83780
+ batchId: input.batchId,
83781
+ frameCount: frames.length,
83782
+ annotationCount,
83783
+ modelErrorCount,
83784
+ classifierCropPadding: CLASSIFIER_CROP_PADDING,
83785
+ frames
83786
+ };
83787
+ }
83788
+ //#endregion
83789
+ //#region src/pipeline-analytics/retrain/retrain-dataset-service.ts
83790
+ function filterOf(selection, completeOnly) {
83791
+ return {
83792
+ ...selection.deviceIds !== void 0 && selection.deviceIds.length > 0 ? { deviceIds: [...selection.deviceIds] } : {},
83793
+ ...selection.collectionIds !== void 0 && selection.collectionIds.length > 0 ? { collectionIds: [...selection.collectionIds] } : {},
83794
+ ...selection.frameIds !== void 0 && selection.frameIds.length > 0 ? { frameIds: [...selection.frameIds] } : {},
83795
+ ...completeOnly ? { completeOnly: true } : {}
83796
+ };
83797
+ }
83798
+ function summaryOf(version) {
83799
+ const { frameIds, ...rest } = version;
83800
+ return {
83801
+ ...rest,
83802
+ frameCount: frameIds.length
83803
+ };
83804
+ }
83805
+ var RetrainDatasetService = class {
83806
+ deps;
83807
+ constructor(deps) {
83808
+ this.deps = deps;
83809
+ }
83810
+ /** The counts a selection would freeze, without freezing anything. */
83811
+ async preview(input) {
83812
+ return datasetStats((await this.deps.buildPlan({
83813
+ filter: filterOf(input.selection, input.completeOnly),
83814
+ layout: "frames"
83815
+ })).manifest);
83816
+ }
83817
+ async create(input) {
83818
+ const name = input.name.trim();
83819
+ if (name === "") return {
83820
+ kind: "refused",
83821
+ reason: "empty-name"
83822
+ };
83823
+ if (await this.deps.versions.findIdByName(name) !== null) return {
83824
+ kind: "refused",
83825
+ reason: "duplicate-name"
83826
+ };
83827
+ const plan = await this.deps.buildPlan({
83828
+ filter: filterOf(input.selection, input.completeOnly),
83829
+ layout: input.layout
83830
+ });
83831
+ if (plan.manifest.frames.length === 0) return {
83832
+ kind: "refused",
83833
+ reason: "no-frames"
83834
+ };
83835
+ const version = {
83836
+ id: this.deps.newId(),
83837
+ name,
83838
+ createdAt: this.deps.now(),
83839
+ selection: input.selection,
83840
+ layout: input.layout,
83841
+ completeOnly: input.completeOnly,
83842
+ frameIds: plan.manifest.frames.map((f) => f.frameId).toSorted(),
83843
+ annotationHash: datasetAnnotationHash(plan.manifest.frames),
83844
+ manifestSchema: RETRAIN_MANIFEST_SCHEMA,
83845
+ stats: datasetStats(plan.manifest)
83846
+ };
83847
+ await this.deps.versions.insert(version);
83848
+ this.deps.logger.info("retrain dataset version created", { meta: {
83849
+ versionId: version.id,
83850
+ frames: version.frameIds.length,
83851
+ subjects: version.stats.subjects,
83852
+ completeness: version.stats.completeness,
83853
+ layout: version.layout
83854
+ } });
83855
+ return {
83856
+ kind: "created",
83857
+ version: summaryOf(version)
83858
+ };
83859
+ }
83860
+ async list() {
83861
+ return (await this.deps.versions.list()).map(summaryOf);
83862
+ }
83863
+ async delete(input) {
83864
+ const deleted = await this.deps.versions.remove(input.id);
83865
+ if (deleted) this.deps.logger.info("retrain dataset version deleted", { meta: { versionId: input.id } });
83866
+ return { deleted };
83867
+ }
83868
+ /** Does the corpus still teach what the version froze? */
83869
+ async verify(input) {
83870
+ const version = await this.deps.versions.get(input.id);
83871
+ if (version === null) return {
83872
+ found: false,
83873
+ annotationHash: "",
83874
+ matches: false,
83875
+ missingFrames: 0
83876
+ };
83877
+ const plan = await this.deps.buildPlan({
83878
+ filter: { frameIds: [...version.frameIds] },
83879
+ layout: "frames"
83880
+ });
83881
+ const present = new Map(plan.manifest.frames.map((f) => [f.frameId, f]));
83882
+ const annotationHash = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
83883
+ frameId,
83884
+ complete: false,
83885
+ annotations: []
83886
+ }));
83887
+ return {
83888
+ found: true,
83889
+ annotationHash,
83890
+ matches: annotationHash === version.annotationHash,
83891
+ missingFrames: version.frameIds.filter((id) => !present.has(id)).length
83892
+ };
83893
+ }
83894
+ /**
83895
+ * An export filter with `datasetVersionId` resolved to that version's
83896
+ * frames (and its completeOnly). An unknown version exports NOTHING — never
83897
+ * the whole dataset, which is what dropping the field would do.
83898
+ */
83899
+ async exportFilterFor(filter) {
83900
+ if (filter.datasetVersionId === void 0) return filter;
83901
+ const { datasetVersionId, ...rest } = filter;
83902
+ const version = await this.deps.versions.get(datasetVersionId);
83903
+ if (version === null) {
83904
+ this.deps.logger.warn("retrain export asked for an unknown dataset version", { meta: { versionId: datasetVersionId } });
83905
+ return {
83906
+ ...rest,
83907
+ frameIds: []
83908
+ };
83909
+ }
83910
+ const wanted = rest.frameIds !== void 0 ? version.frameIds.filter((id) => rest.frameIds?.includes(id) === true) : version.frameIds;
83911
+ return {
83912
+ ...rest,
83913
+ frameIds: [...wanted],
83914
+ ...version.completeOnly ? { completeOnly: true } : {}
83915
+ };
83916
+ }
83917
+ };
83918
+ //#endregion
83919
+ //#region src/pipeline-analytics/retrain/retrain-dataset-version-store.ts
83920
+ /**
83921
+ * @durable class=ledger owner=pipeline-analytics
83922
+ * write="one row per dataset version the operator creates: a frozen frameIds list,
83923
+ * the sha256 of the annotations those frames carried, the export parameters and
83924
+ * the counts at creation. Never updated after the insert — a different selection
83925
+ * is a new version."
83926
+ * retention="none — rows leave only when the operator deletes the version. The
83927
+ * frames themselves are owned by the retrain corpus; deleting a version deletes no
83928
+ * frame, and deleting a frame does not edit a version (verify reports it missing)."
83929
+ */
83930
+ var RETRAIN_DATASET_VERSIONS_COLLECTION = "pipeline-analytics:retrain-dataset-versions";
83931
+ var COLUMNS = [
83932
+ {
83933
+ name: "id",
83934
+ type: "TEXT",
83935
+ primaryKey: true,
83936
+ notNull: true
83937
+ },
83938
+ {
83939
+ name: "name",
83940
+ type: "TEXT",
83941
+ notNull: true
83942
+ },
83943
+ {
83944
+ name: "nameKey",
83945
+ type: "TEXT",
83946
+ notNull: true
83947
+ },
83948
+ {
83949
+ name: "createdAt",
83950
+ type: "INTEGER",
83951
+ notNull: true
83952
+ },
83953
+ {
83954
+ name: "selection",
83955
+ type: "JSON",
83956
+ notNull: true
83957
+ },
83958
+ {
83959
+ name: "layout",
83960
+ type: "TEXT",
83961
+ notNull: true
83962
+ },
83963
+ {
83964
+ name: "completeOnly",
83965
+ type: "BOOLEAN",
83966
+ notNull: true
83967
+ },
83968
+ {
83969
+ name: "frameIds",
83970
+ type: "JSON",
83971
+ notNull: true
83972
+ },
83973
+ {
83974
+ name: "annotationHash",
83975
+ type: "TEXT",
83976
+ notNull: true
83977
+ },
83978
+ {
83979
+ name: "manifestSchema",
83980
+ type: "TEXT",
83981
+ notNull: true
83982
+ },
83983
+ {
83984
+ name: "stats",
83985
+ type: "JSON",
83986
+ notNull: true
83987
+ }
83988
+ ];
83989
+ var INDEXES = [{
83990
+ name: "idx_retrain_dataset_versions_name_key",
83991
+ columns: ["nameKey"],
83992
+ unique: true
83993
+ }, {
83994
+ name: "idx_retrain_dataset_versions_created",
83995
+ columns: ["createdAt"]
83996
+ }];
83997
+ var nameKeyOf = (name) => name.trim().toLowerCase();
83998
+ function numberList(value) {
83999
+ return Array.isArray(value) ? value.filter((v) => typeof v === "number") : void 0;
84000
+ }
84001
+ function stringList(value) {
84002
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : void 0;
84003
+ }
84004
+ function parseJson(value) {
84005
+ if (typeof value !== "string") return value;
84006
+ try {
84007
+ return JSON.parse(value);
84008
+ } catch {
84009
+ return;
84010
+ }
84011
+ }
84012
+ function asRecord(value) {
84013
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
84014
+ }
84015
+ var EMPTY_STATS = {
84016
+ frames: 0,
84017
+ completeFrames: 0,
84018
+ completeness: 0,
84019
+ subjects: 0,
84020
+ modelErrors: 0,
84021
+ hardNegatives: 0,
84022
+ cells: [],
84023
+ cameras: []
84024
+ };
84025
+ /** A stored stats object, trusted only as far as its shape. */
84026
+ function readStats(value) {
84027
+ const raw = asRecord(parseJson(value));
84028
+ const num = (k) => typeof raw[k] === "number" ? raw[k] : 0;
84029
+ const cells = Array.isArray(raw["cells"]) ? raw["cells"] : [];
84030
+ const cameras = Array.isArray(raw["cameras"]) ? raw["cameras"] : [];
84031
+ return {
84032
+ ...EMPTY_STATS,
84033
+ frames: num("frames"),
84034
+ completeFrames: num("completeFrames"),
84035
+ completeness: num("completeness"),
84036
+ subjects: num("subjects"),
84037
+ modelErrors: num("modelErrors"),
84038
+ hardNegatives: num("hardNegatives"),
84039
+ cells: cells.map((c) => {
84040
+ const r = asRecord(c);
84041
+ const lighting = r["lighting"] === "day" || r["lighting"] === "night" ? r["lighting"] : "unknown";
84042
+ return {
84043
+ macroClass: String(r["macroClass"] ?? ""),
84044
+ deviceId: Number(r["deviceId"] ?? 0),
84045
+ lighting,
84046
+ subjects: Number(r["subjects"] ?? 0),
84047
+ frames: Number(r["frames"] ?? 0)
84048
+ };
84049
+ }),
84050
+ cameras: cameras.map((c) => {
84051
+ const r = asRecord(c);
84052
+ return {
84053
+ deviceId: Number(r["deviceId"] ?? 0),
84054
+ frames: Number(r["frames"] ?? 0),
84055
+ completeFrames: Number(r["completeFrames"] ?? 0)
84056
+ };
84057
+ })
84058
+ };
84059
+ }
84060
+ var RetrainDatasetVersionStore = class {
84061
+ store;
84062
+ logger;
84063
+ constructor(deps) {
84064
+ this.store = deps.store;
84065
+ this.logger = deps.logger;
84066
+ }
84067
+ static async declare(store) {
84068
+ await store.declareCollection.mutate({
84069
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84070
+ columns: [...COLUMNS],
84071
+ indexes: [...INDEXES]
84072
+ });
84073
+ }
84074
+ insert = async (version) => {
84075
+ await this.store.set.mutate({
84076
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84077
+ key: version.id,
84078
+ value: {
84079
+ name: version.name,
84080
+ nameKey: nameKeyOf(version.name),
84081
+ createdAt: version.createdAt,
84082
+ selection: version.selection,
84083
+ layout: version.layout,
84084
+ completeOnly: version.completeOnly,
84085
+ frameIds: version.frameIds,
84086
+ annotationHash: version.annotationHash,
84087
+ manifestSchema: version.manifestSchema,
84088
+ stats: version.stats
84089
+ }
84090
+ });
84091
+ };
84092
+ get = async (id) => {
84093
+ try {
84094
+ const row = await this.store.get.query({
84095
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84096
+ key: id
84097
+ });
84098
+ return row === null || row === void 0 ? null : this.toVersion(id, asRecord(row));
84099
+ } catch (err) {
84100
+ this.logger.warn("retrain dataset version read failed", { meta: {
84101
+ versionId: id,
84102
+ error: String(err)
84103
+ } });
84104
+ return null;
84105
+ }
84106
+ };
84107
+ list = async () => {
84108
+ try {
84109
+ return (await this.store.query.query({
84110
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84111
+ filter: { orderBy: {
84112
+ field: "createdAt",
84113
+ direction: "desc"
84114
+ } }
84115
+ })).map((r) => this.toVersion(r.id, asRecord(r.data)));
84116
+ } catch (err) {
84117
+ this.logger.warn("retrain dataset versions list failed", { meta: { error: String(err) } });
84118
+ return [];
84119
+ }
84120
+ };
84121
+ findIdByName = async (name) => {
84122
+ return (await this.store.query.query({
84123
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84124
+ filter: {
84125
+ where: { nameKey: nameKeyOf(name) },
84126
+ limit: 1
84127
+ }
84128
+ }))[0]?.id ?? null;
84129
+ };
84130
+ remove = async (id) => {
84131
+ if (await this.get(id) === null) return false;
84132
+ await this.store.delete.mutate({
84133
+ collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
84134
+ key: id
84135
+ });
84136
+ return true;
84137
+ };
84138
+ toVersion(id, data) {
84139
+ const selection = asRecord(parseJson(data["selection"]));
84140
+ const deviceIds = numberList(selection["deviceIds"]);
84141
+ const collectionIds = stringList(selection["collectionIds"]);
84142
+ const frameIdsSel = stringList(selection["frameIds"]);
84143
+ return {
84144
+ id,
84145
+ name: String(data["name"] ?? ""),
84146
+ createdAt: Number(data["createdAt"] ?? 0),
84147
+ selection: {
84148
+ ...deviceIds !== void 0 ? { deviceIds } : {},
84149
+ ...collectionIds !== void 0 ? { collectionIds } : {},
84150
+ ...frameIdsSel !== void 0 ? { frameIds: frameIdsSel } : {}
84151
+ },
84152
+ layout: data["layout"] === "crops" ? "crops" : "frames",
84153
+ completeOnly: data["completeOnly"] === true || data["completeOnly"] === 1,
84154
+ frameIds: stringList(parseJson(data["frameIds"])) ?? [],
84155
+ annotationHash: String(data["annotationHash"] ?? ""),
84156
+ manifestSchema: String(data["manifestSchema"] ?? ""),
84157
+ stats: readStats(data["stats"])
84158
+ };
84159
+ }
84160
+ };
84161
+ //#endregion
83378
84162
  //#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
83379
84163
  var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
83380
84164
  var ALWAYS_CLOUD_KINDS = new Set([
@@ -84088,285 +84872,6 @@ async function cutRetrainCrop(input) {
84088
84872
  return pipeline.jpeg({ quality: 92 }).toBuffer();
84089
84873
  }
84090
84874
  //#endregion
84091
- //#region src/pipeline-analytics/retrain/retrain-geometry.ts
84092
- function clamp01(v) {
84093
- return v < 0 ? 0 : v > 1 ? 1 : v;
84094
- }
84095
- /** Is this a usable rectangle at all? A zero-area box is not a subject. */
84096
- function isUsableRect(r) {
84097
- return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
84098
- }
84099
- /**
84100
- * The subject inside a square LETTERBOX of the full frame.
84101
- *
84102
- * The frame is scaled to fit the square and centred; the padding bars take the
84103
- * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
84104
- * height, and every y coordinate shifts by half the leftover.
84105
- *
84106
- * Square-side agnostic on purpose: the answer is normalised against the square,
84107
- * so it is the same number for a 640 and a 1280 input and no caller has to know
84108
- * which the model uses.
84109
- */
84110
- function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
84111
- if (!isUsableRect(bbox)) return null;
84112
- if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
84113
- const longSide = Math.max(frameWidth, frameHeight);
84114
- const scaleX = frameWidth / longSide;
84115
- const scaleY = frameHeight / longSide;
84116
- const offsetX = (1 - scaleX) / 2;
84117
- const offsetY = (1 - scaleY) / 2;
84118
- return {
84119
- bbox: {
84120
- x: offsetX + bbox.x * scaleX,
84121
- y: offsetY + bbox.y * scaleY,
84122
- w: bbox.w * scaleX,
84123
- h: bbox.h * scaleY
84124
- },
84125
- visibleFraction: 1
84126
- };
84127
- }
84128
- /**
84129
- * The subject inside a ZONE CROP — the shape a package model is trained on.
84130
- *
84131
- * Returns `null` when the subject does not intersect the zone at all. That is
84132
- * the whole reason the package proposal runs on the crop rather than the frame:
84133
- * a package detector at 0.35 on a full frame proposes furniture, and a subject
84134
- * outside the zone is not part of this model's world.
84135
- */
84136
- function deriveZoneCropRect(bbox, zone) {
84137
- if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
84138
- const left = Math.max(bbox.x, zone.x);
84139
- const top = Math.max(bbox.y, zone.y);
84140
- const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
84141
- const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
84142
- if (!(right > left) || !(bottom > top)) return null;
84143
- const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
84144
- return {
84145
- bbox: {
84146
- x: (left - zone.x) / zone.w,
84147
- y: (top - zone.y) / zone.h,
84148
- w: (right - left) / zone.w,
84149
- h: (bottom - top) / zone.h
84150
- },
84151
- visibleFraction
84152
- };
84153
- }
84154
- /**
84155
- * The subject inside its OWN padded crop — the shape a classifier is trained on.
84156
- *
84157
- * The crop is the box grown by `padding` of its own size on each side and then
84158
- * clipped to the frame, so the subject sits in the middle and the classifier
84159
- * still sees a little context. The returned rectangle is where the subject
84160
- * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
84161
- * clipped by a frame edge — a subject at the left border is flush left in its
84162
- * crop, and a caller that assumed the centre would draw the box in the wrong
84163
- * place.
84164
- */
84165
- function deriveSubjectCropRect(bbox, padding) {
84166
- if (!isUsableRect(bbox)) return null;
84167
- const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
84168
- const cropX = clamp01(bbox.x - bbox.w * pad);
84169
- const cropY = clamp01(bbox.y - bbox.h * pad);
84170
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
84171
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
84172
- const cropW = cropRight - cropX;
84173
- const cropH = cropBottom - cropY;
84174
- if (!(cropW > 0) || !(cropH > 0)) return null;
84175
- return {
84176
- bbox: {
84177
- x: (bbox.x - cropX) / cropW,
84178
- y: (bbox.y - cropY) / cropH,
84179
- w: bbox.w / cropW,
84180
- h: bbox.h / cropH
84181
- },
84182
- visibleFraction: 1
84183
- };
84184
- }
84185
- //#endregion
84186
- //#region src/pipeline-analytics/retrain/retrain-lighting.ts
84187
- function isNight(condition) {
84188
- return condition === "night" || condition === "ir";
84189
- }
84190
- function retrainFrameLighting(input) {
84191
- if (input.capturedAt === void 0) {
84192
- const measured = resolveConditionDetailed({
84193
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84194
- now: /* @__PURE__ */ new Date(0)
84195
- });
84196
- return measured.source === "ir-chroma" ? {
84197
- condition: measured.condition,
84198
- night: true,
84199
- source: measured.source
84200
- } : null;
84201
- }
84202
- const resolved = resolveConditionDetailed({
84203
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84204
- ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
84205
- now: new Date(input.capturedAt)
84206
- });
84207
- return {
84208
- ...resolved,
84209
- night: isNight(resolved.condition)
84210
- };
84211
- }
84212
- //#endregion
84213
- //#region src/pipeline-analytics/retrain/retrain-export.ts
84214
- /**
84215
- * The ANNOTATED dataset export — the retrain page's own archive.
84216
- *
84217
- * Sibling of `training-export/`, deliberately not the same thing. That one
84218
- * streams a marked track's stored media verbatim and knows nothing about what
84219
- * is in it; this one streams the frames the operator SELECTED (already copied
84220
- * into retrain storage) plus the ground truth they drew.
84221
- *
84222
- * ## The one interesting decision
84223
- *
84224
- * `annotations.json` carries, for every subject, the canonical full-frame box
84225
- * AND the geometry derived for each model shape. **Derived here, at export,
84226
- * never stored** — one box in, three shapes out. Storing the derived shapes is
84227
- * how two crops of the same subject end up in one feature space, which is the
84228
- * failure already recorded for CLIP (D52). A consumer that wants a different
84229
- * input size or a different padding re-derives from the canonical box; nothing
84230
- * in the database has to change and nothing can disagree.
84231
- *
84232
- * Everything else follows the export next door: index reads to plan, one JPEG
84233
- * in memory at a time, a yield between files, and NO inference — a training
84234
- * set's pixels are the pixels the pipeline saw.
84235
- */
84236
- var RETRAIN_EXPORT_PREFIX = "retrain-export";
84237
- /**
84238
- * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
84239
- * padding, read from the one convention `deriveDetailCropRect` cuts with
84240
- * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
84241
- * here while the runtime fed `0.15`, so a classifier trained from this export
84242
- * learned on more context than it is ever given live.
84243
- */
84244
- var CLASSIFIER_CROP_PADDING = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
84245
- /** The convention a `crops` layout cuts with — the runtime default, recorded
84246
- * in the manifest so a consumer never has to assume it. */
84247
- var RETRAIN_CROP_CONVENTION = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION;
84248
- /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
84249
- * the trainer resizes per model, so this only bounds the upload. */
84250
- var RETRAIN_CROP_MAX_SIDE = 256;
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. */
84259
- function retrainFramePath(frame) {
84260
- const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
84261
- const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
84262
- return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
84263
- }
84264
- /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
84265
- function retrainCropPath(macroClass, annotationId) {
84266
- return `crops/${macroClass}/${annotationId}.jpg`;
84267
- }
84268
- /**
84269
- * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
84270
- * filter keeps everything. Order is the input's.
84271
- */
84272
- function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
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;
84275
- const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84276
- return frames.filter((frame) => {
84277
- if (wanted !== null && !wanted.has(frame.frameId)) return false;
84278
- if (devices !== null && !devices.has(frame.deviceId)) return false;
84279
- if (filter.completeOnly === true && !frame.complete) return false;
84280
- if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
84281
- if (filter.batchId !== void 0) {
84282
- if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
84283
- }
84284
- return true;
84285
- });
84286
- }
84287
- /**
84288
- * Derive every per-model shape for one annotation.
84289
- *
84290
- * `zone` is optional and per-frame: only a package annotation has one, and a
84291
- * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
84292
- * clamped — a clamped box teaches the model that the subject ends where the
84293
- * crop ends.
84294
- */
84295
- function deriveAll(annotation, frame, zone) {
84296
- const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
84297
- const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
84298
- const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
84299
- return {
84300
- ...letterbox !== null ? { letterbox } : {},
84301
- ...zoneCrop !== null ? { zoneCrop } : {},
84302
- ...subjectCrop !== null ? { subjectCrop } : {}
84303
- };
84304
- }
84305
- /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
84306
- function buildRetrainManifest(input) {
84307
- const layout = input.layout ?? "frames";
84308
- const coordinates = input.coordinates ?? null;
84309
- let annotationCount = 0;
84310
- let modelErrorCount = 0;
84311
- const frames = input.frames.map((frame) => {
84312
- const zone = input.zoneForDevice(frame.deviceId);
84313
- const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
84314
- annotationCount += 1;
84315
- if (a.kind === "model_error") modelErrorCount += 1;
84316
- const derived = deriveAll(a, frame, zone);
84317
- return {
84318
- id: a.id,
84319
- macroClass: a.macroClass,
84320
- ...a.label !== void 0 ? { label: a.label } : {},
84321
- ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
84322
- kind: a.kind,
84323
- source: a.source,
84324
- ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
84325
- ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
84326
- bbox: a.bbox,
84327
- ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
84328
- derived
84329
- };
84330
- });
84331
- return {
84332
- frameId: frame.frameId,
84333
- path: retrainFramePath(frame),
84334
- deviceId: frame.deviceId,
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) ?? [],
84339
- width: frame.width,
84340
- height: frame.height,
84341
- sizeBytes: frame.sizeBytes,
84342
- sourceKind: frame.sourceKind,
84343
- copiedAt: frame.copiedAt,
84344
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84345
- complete: frame.complete,
84346
- lighting: retrainFrameLighting({
84347
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84348
- ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
84349
- coordinates
84350
- }),
84351
- ...zone !== null ? { zoneUsed: zone } : {},
84352
- annotations
84353
- };
84354
- });
84355
- return {
84356
- schema: RETRAIN_MANIFEST_SCHEMA,
84357
- layout,
84358
- detailCropConvention: RETRAIN_CROP_CONVENTION,
84359
- ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
84360
- generatedAt: input.generatedAt,
84361
- batchId: input.batchId,
84362
- frameCount: frames.length,
84363
- annotationCount,
84364
- modelErrorCount,
84365
- classifierCropPadding: CLASSIFIER_CROP_PADDING,
84366
- frames
84367
- };
84368
- }
84369
- //#endregion
84370
84875
  //#region src/pipeline-analytics/retrain/retrain-export-handler.ts
84371
84876
  /** The export request a URL carries (`getRetrainExportUrl` composes it). */
84372
84877
  function parseRetrainExportRequest(url) {
@@ -84375,12 +84880,14 @@ function parseRetrainExportRequest(url) {
84375
84880
  const deviceIds = parseRetrainDeviceIds(url);
84376
84881
  const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
84377
84882
  const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
84883
+ const datasetVersionId = params.get("datasetVersionId")?.trim() ?? "";
84378
84884
  const batchId = params.get("batchId")?.trim() ?? "";
84379
84885
  return {
84380
84886
  filter: {
84381
84887
  ...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
84382
84888
  ...frameIds.length > 0 ? { frameIds } : {},
84383
84889
  ...collectionIds.length > 0 ? { collectionIds } : {},
84890
+ ...datasetVersionId.length > 0 ? { datasetVersionId } : {},
84384
84891
  ...batchId.length > 0 ? { batchId } : {},
84385
84892
  ...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
84386
84893
  },
@@ -84394,6 +84901,7 @@ function retrainExportQuery(request) {
84394
84901
  if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
84395
84902
  if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
84396
84903
  if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
84904
+ if (filter.datasetVersionId !== void 0) params.set("datasetVersionId", filter.datasetVersionId);
84397
84905
  if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
84398
84906
  if (filter.completeOnly === true) params.set("completeOnly", "1");
84399
84907
  if (request.layout === "crops") params.set("layout", "crops");
@@ -108502,6 +109010,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
108502
109010
  retrain = null;
108503
109011
  retrainCollections = null;
108504
109012
  retrainLlmAssist = null;
109013
+ retrainDatasetVersions = null;
109014
+ retrainDatasets = null;
108505
109015
  retrainCollectionStore = null;
108506
109016
  retrainFrames = null;
108507
109017
  retrainAnnotations = null;
@@ -110298,6 +110808,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
110298
110808
  RetrainFrameStore.declare(api.settingsStore),
110299
110809
  RetrainAnnotationStore.declare(api.settingsStore),
110300
110810
  RetrainCollectionStore.declare(api.settingsStore),
110811
+ RetrainDatasetVersionStore.declare(api.settingsStore),
110301
110812
  OpsLogStore.declare(api.settingsStore),
110302
110813
  DebugNoteArchiveStore.declare(api.settingsStore),
110303
110814
  TrackingTapeIndexStore.declare(api.settingsStore),
@@ -110605,6 +111116,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
110605
111116
  captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
110606
111117
  logger: logger.child("RetrainCollections")
110607
111118
  });
111119
+ const retrainDatasetVersions = new RetrainDatasetVersionStore({
111120
+ store: api.settingsStore,
111121
+ logger: logger.child("RetrainDatasets")
111122
+ });
111123
+ this.retrainDatasetVersions = retrainDatasetVersions;
111124
+ this.retrainDatasets = new RetrainDatasetService({
111125
+ versions: retrainDatasetVersions,
111126
+ buildPlan: (request) => this.buildRetrainExportPlan(request),
111127
+ now: () => Date.now(),
111128
+ newId: () => (0, node_crypto.randomUUID)(),
111129
+ logger: logger.child("RetrainDatasets")
111130
+ });
110608
111131
  this.retrainLlmAssist = new RetrainLlmAssistService({
110609
111132
  frames: retrainFrames,
110610
111133
  llm: {
@@ -111857,6 +112380,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
111857
112380
  async setRetrainFrameComplete(input) {
111858
112381
  return this.requireRetrain().setFrameComplete(input);
111859
112382
  }
112383
+ requireRetrainDatasets() {
112384
+ const service = this.retrainDatasets;
112385
+ if (service === null) throw new Error("retrain datasets: analytics stores unavailable");
112386
+ return service;
112387
+ }
112388
+ async previewRetrainDataset(input) {
112389
+ return this.requireRetrainDatasets().preview({
112390
+ selection: input.selection,
112391
+ completeOnly: input.completeOnly ?? false
112392
+ });
112393
+ }
112394
+ async createRetrainDatasetVersion(input) {
112395
+ return this.requireRetrainDatasets().create(input);
112396
+ }
112397
+ async listRetrainDatasetVersions(_input) {
112398
+ return this.requireRetrainDatasets().list();
112399
+ }
112400
+ async deleteRetrainDatasetVersion(input) {
112401
+ return this.requireRetrainDatasets().delete(input);
112402
+ }
112403
+ async verifyRetrainDatasetVersion(input) {
112404
+ return this.requireRetrainDatasets().verify(input);
112405
+ }
111860
112406
  requireRetrainLlmAssist() {
111861
112407
  const service = this.retrainLlmAssist;
111862
112408
  if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
@@ -112247,6 +112793,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
112247
112793
  * zones simply has no `zoneCrop` geometry — absent, never a clamped guess.
112248
112794
  */
112249
112795
  async buildRetrainExportPlan(request) {
112796
+ const versionId = request.filter.datasetVersionId;
112797
+ if (versionId !== void 0) {
112798
+ const version = await this.retrainDatasetVersions?.get(versionId) ?? null;
112799
+ const { datasetVersionId: _resolved, ...rest } = await this.retrainDatasets?.exportFilterFor(request.filter) ?? {
112800
+ ...request.filter,
112801
+ frameIds: []
112802
+ };
112803
+ const plan = await this.buildRetrainExportPlan({
112804
+ ...request,
112805
+ filter: rest
112806
+ });
112807
+ return version === null ? plan : {
112808
+ ...plan,
112809
+ manifest: withDatasetVersion(plan.manifest, version)
112810
+ };
112811
+ }
112250
112812
  const frameStore = this.retrainFrames;
112251
112813
  const annotationStore = this.retrainAnnotations;
112252
112814
  const trackStore = this.trackStore;