@camstack/addon-post-analysis 1.2.351 → 1.2.353

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 (15) hide show
  1. package/dist/{clip-model-registry-DI12tTgY.js → clip-model-registry-CPNsQNt5.js} +302 -158
  2. package/dist/{clip-model-registry-CC-BJYiK.mjs → clip-model-registry-VI28etdt.mjs} +302 -158
  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-5v6z6gQL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cfcb9thb.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-N64PaPIw.mjs +26 -0
  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-Clwp5JZq.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-BKDWAGHG.mjs → hostInit-DpCJPMQZ.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +678 -942
  11. package/dist/pipeline-analytics/index.mjs +678 -942
  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_types__loadShare__.js-Du_tzgi2.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CQOJKiwa.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-DI12tTgY.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-CPNsQNt5.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$1(data, key) {
12925
+ function stringList(data, key) {
12926
12926
  const raw = data[key];
12927
12927
  if (!Array.isArray(raw)) return [];
12928
12928
  const ids = [];
@@ -13739,7 +13739,7 @@ function incomingFromExportCompleted(facts, data) {
13739
13739
  const deviceId = numberField(data, "deviceId");
13740
13740
  const title = stringField(data, "title");
13741
13741
  const bytes = numberField(data, "fileBytes");
13742
- const notifyTargetIds = stringList$1(data, "notifyTargetIds");
13742
+ const notifyTargetIds = stringList(data, "notifyTargetIds");
13743
13743
  const incoming = systemSubject({
13744
13744
  facts,
13745
13745
  kind: "export-completed",
@@ -23031,7 +23031,7 @@ function withSummaryBody(rule, section) {
23031
23031
  //#region src/notification-center/system-event-tap-url.ts
23032
23032
  /** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
23033
23033
  var ADMIN_ADDONS_PATH = "/system/addons";
23034
- var ADMIN_CLUSTER_PATH = "/system/cluster";
23034
+ var ADMIN_NODES_PATH = "/system/nodes";
23035
23035
  var ADMIN_DEVICES_PATH = "/devices";
23036
23036
  /**
23037
23037
  * The query parameter naming a node.
@@ -23105,9 +23105,9 @@ var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
23105
23105
  * Cluster page, and the summary row is about all of them at once.
23106
23106
  */
23107
23107
  var SUMMARY_SECTION_PATHS = {
23108
- nodes: ADMIN_CLUSTER_PATH,
23108
+ nodes: ADMIN_NODES_PATH,
23109
23109
  "addon-updates": ADMIN_ADDONS_PATH,
23110
- updates: ADMIN_CLUSTER_PATH
23110
+ updates: ADMIN_NODES_PATH
23111
23111
  };
23112
23112
  function withParam(path, key, value) {
23113
23113
  if (value === void 0 || value.length === 0) return path;
@@ -23124,7 +23124,7 @@ function systemEventTapPath(subject) {
23124
23124
  const sectionPath = subject.summarySection !== void 0 ? SUMMARY_SECTION_PATHS[subject.summarySection] : void 0;
23125
23125
  if (sectionPath !== void 0) return sectionPath;
23126
23126
  if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
23127
- if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
23127
+ if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_NODES_PATH, NODE_PARAM, subject.nodeId);
23128
23128
  if (DEVICE_STREAM_KINDS.has(subject.kind)) {
23129
23129
  const deviceId = subject.deviceId;
23130
23130
  if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
@@ -47582,7 +47582,7 @@ function buildTitleSvg(input) {
47582
47582
  const top = Math.round((height - lineHeight * lines.length) / 2);
47583
47583
  const shadow = Math.max(1, Math.round(fontPx * TITLE_SHADOW_FRACTION));
47584
47584
  const cx = Math.round(width / 2);
47585
- const font = `font-size="${fontPx}" font-family="sans-serif" font-weight="bold" text-anchor="middle"`;
47585
+ const font = `font-size="${fontPx}" font-family="DejaVu Sans,Helvetica,Arial,sans-serif" font-weight="bold" text-anchor="middle"`;
47586
47586
  return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
47587
47587
  <rect x="0" y="0" width="${width}" height="${height}" fill="black" fill-opacity="${TITLE_SCRIM_OPACITY}"/>
47588
47588
  ${lines.map((line, i) => {
@@ -71047,6 +71047,15 @@ var RetrainFrameStore = class {
71047
71047
  return [];
71048
71048
  }
71049
71049
  }
71050
+ /** Every corpus copy — index rows only, no blob. Newest-first is the caller's. */
71051
+ async listAll() {
71052
+ try {
71053
+ return (await this.store.query.query({ collection: RETRAIN_FRAMES_COLLECTION })).filter((r) => typeof r.id === "string").map((r) => this.rowToFrame(r.id, r.data));
71054
+ } catch (err) {
71055
+ this.logger.warn("RetrainFrameStore.listAll failed", { meta: { error: String(err) } });
71056
+ return [];
71057
+ }
71058
+ }
71050
71059
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
71051
71060
  async get(frameId) {
71052
71061
  try {
@@ -75153,7 +75162,7 @@ function describeTapeStatus(input) {
75153
75162
  * `viewer-settings-legacy-adoption.ts`.
75154
75163
  */
75155
75164
  var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
75156
- var COLUMNS$1 = [
75165
+ var COLUMNS = [
75157
75166
  {
75158
75167
  name: "id",
75159
75168
  type: "TEXT",
@@ -75224,7 +75233,7 @@ function requireCaller(caller) {
75224
75233
  if (!caller) throw new Error("viewerSettings: authenticated caller required");
75225
75234
  return caller;
75226
75235
  }
75227
- function asRecord$1(data) {
75236
+ function asRecord(data) {
75228
75237
  return typeof data === "object" && data !== null ? data : {};
75229
75238
  }
75230
75239
  function toSummary(id, data, callerUserId) {
@@ -75250,7 +75259,7 @@ var ViewerSettingsSnapshotStore = class {
75250
75259
  static async declare(store) {
75251
75260
  await store.declareCollection.mutate({
75252
75261
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75253
- columns: [...COLUMNS$1],
75262
+ columns: [...COLUMNS],
75254
75263
  indexes: [{
75255
75264
  name: "idx_pa_viewer_settings_owner",
75256
75265
  columns: ["ownerUserId"]
@@ -75270,7 +75279,7 @@ var ViewerSettingsSnapshotStore = class {
75270
75279
  });
75271
75280
  const out = [];
75272
75281
  for (const row of rows) {
75273
- const summary = toSummary(row.id, asRecord$1(row.data), caller.userId);
75282
+ const summary = toSummary(row.id, asRecord(row.data), caller.userId);
75274
75283
  if (summary) out.push(summary);
75275
75284
  }
75276
75285
  return out;
@@ -75284,7 +75293,7 @@ var ViewerSettingsSnapshotStore = class {
75284
75293
  }
75285
75294
  }))[0];
75286
75295
  if (!row) return null;
75287
- const data = asRecord$1(row.data);
75296
+ const data = asRecord(row.data);
75288
75297
  const summary = toSummary(row.id, data, caller.userId);
75289
75298
  if (!summary) return null;
75290
75299
  const blob = BlobSchema.safeParse(data.blob);
@@ -75307,7 +75316,7 @@ var ViewerSettingsSnapshotStore = class {
75307
75316
  }
75308
75317
  }))[0];
75309
75318
  const id = prior?.id ?? (0, node_crypto.randomUUID)();
75310
- const createdAt = typeof asRecord$1(prior?.data).createdAt === "number" ? asRecord$1(prior?.data).createdAt : now;
75319
+ const createdAt = typeof asRecord(prior?.data).createdAt === "number" ? asRecord(prior?.data).createdAt : now;
75311
75320
  await this.store.set.mutate({
75312
75321
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75313
75322
  key: id,
@@ -83513,850 +83522,6 @@ var RetrainCollectionService = class {
83513
83522
  }
83514
83523
  };
83515
83524
  //#endregion
83516
- //#region src/pipeline-analytics/retrain/retrain-dataset.ts
83517
- /**
83518
- * Dataset versions — the counts a version is judged by, and the hash that
83519
- * freezes what it teaches. Pure.
83520
- *
83521
- * Both read the export MANIFEST (`retrain-export.ts`), not the stores: a
83522
- * version is exactly what an export of its frames would carry, so its counts
83523
- * and its hash cannot disagree with the archive a trainer receives.
83524
- */
83525
- function lightingOf(frame) {
83526
- if (frame.lighting === null) return "unknown";
83527
- return frame.lighting.night ? "night" : "day";
83528
- }
83529
- /** The counts of a dataset, from the manifest an export of it would carry. */
83530
- function datasetStats(manifest) {
83531
- const cells = /* @__PURE__ */ new Map();
83532
- const cameras = /* @__PURE__ */ new Map();
83533
- let completeFrames = 0;
83534
- let subjects = 0;
83535
- let modelErrors = 0;
83536
- let hardNegatives = 0;
83537
- for (const frame of manifest.frames) {
83538
- const camera = cameras.get(frame.deviceId) ?? {
83539
- frames: 0,
83540
- completeFrames: 0
83541
- };
83542
- cameras.set(frame.deviceId, {
83543
- frames: camera.frames + 1,
83544
- completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
83545
- });
83546
- if (frame.complete) completeFrames += 1;
83547
- const lighting = lightingOf(frame);
83548
- let frameSubjects = 0;
83549
- for (const a of frame.annotations) {
83550
- if (a.kind === "model_error") {
83551
- modelErrors += 1;
83552
- continue;
83553
- }
83554
- frameSubjects += 1;
83555
- const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
83556
- const entry = cells.get(key) ?? {
83557
- cell: {
83558
- macroClass: a.macroClass,
83559
- deviceId: frame.deviceId,
83560
- lighting,
83561
- subjects: 0,
83562
- frames: 0
83563
- },
83564
- frameIds: /* @__PURE__ */ new Set()
83565
- };
83566
- entry.frameIds.add(frame.frameId);
83567
- cells.set(key, {
83568
- cell: {
83569
- ...entry.cell,
83570
- subjects: entry.cell.subjects + 1,
83571
- frames: entry.frameIds.size
83572
- },
83573
- frameIds: entry.frameIds
83574
- });
83575
- }
83576
- subjects += frameSubjects;
83577
- if (frame.complete && frameSubjects === 0) hardNegatives += 1;
83578
- }
83579
- const frames = manifest.frames.length;
83580
- return {
83581
- frames,
83582
- completeFrames,
83583
- completeness: frames > 0 ? completeFrames / frames : 0,
83584
- subjects,
83585
- modelErrors,
83586
- hardNegatives,
83587
- cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
83588
- cameras: [...cameras.entries()].map(([deviceId, c]) => ({
83589
- deviceId,
83590
- ...c
83591
- })).toSorted((a, b) => a.deviceId - b.deviceId)
83592
- };
83593
- }
83594
- /** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
83595
- var q = (n) => Math.round(n * 1e4) / 1e4;
83596
- /**
83597
- * The sha256 of what the frames TEACH: per frame its completeness and, per
83598
- * box, the class tiers, the kind and the box. Row ids, timestamps, export
83599
- * stamps and provenance are left out — they change on a re-save that teaches
83600
- * the same thing. Order-independent.
83601
- */
83602
- function datasetAnnotationHash(frames) {
83603
- const canonical = frames.map((f) => ({
83604
- f: f.frameId,
83605
- c: f.complete,
83606
- a: f.annotations.map((a) => [
83607
- a.macroClass,
83608
- a.label ?? "",
83609
- a.subLabel ?? "",
83610
- a.kind,
83611
- q(a.bbox.x),
83612
- q(a.bbox.y),
83613
- q(a.bbox.w),
83614
- q(a.bbox.h)
83615
- ].join("|")).toSorted()
83616
- })).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
83617
- return (0, node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
83618
- }
83619
- /**
83620
- * The manifest of a version's export, saying which version and whether its
83621
- * frames still teach what was frozen. Frames of the version missing from the
83622
- * archive count as empty and incomplete, so their absence changes the hash.
83623
- */
83624
- function withDatasetVersion(manifest, version) {
83625
- const present = new Map(manifest.frames.map((f) => [f.frameId, f]));
83626
- const annotationHashNow = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
83627
- frameId,
83628
- complete: false,
83629
- annotations: []
83630
- }));
83631
- return {
83632
- ...manifest,
83633
- datasetVersion: {
83634
- id: version.id,
83635
- name: version.name,
83636
- createdAt: version.createdAt,
83637
- annotationHash: version.annotationHash,
83638
- annotationHashNow,
83639
- matches: annotationHashNow === version.annotationHash,
83640
- missingFrames: version.frameIds.filter((id) => !present.has(id)).length
83641
- }
83642
- };
83643
- }
83644
- //#endregion
83645
- //#region src/pipeline-analytics/retrain/retrain-dataset-version-store.ts
83646
- /**
83647
- * @durable class=ledger owner=pipeline-analytics
83648
- * write="one row per dataset version the operator creates: a frozen frameIds list,
83649
- * the sha256 of the annotations those frames carried, the export parameters and
83650
- * the counts at creation. Never updated after the insert — a different selection
83651
- * is a new version."
83652
- * retention="none — rows leave only when the operator deletes the version. The
83653
- * frames themselves are owned by the retrain corpus; deleting a version deletes no
83654
- * frame, and deleting a frame does not edit a version (verify reports it missing)."
83655
- */
83656
- var RETRAIN_DATASET_VERSIONS_COLLECTION = "pipeline-analytics:retrain-dataset-versions";
83657
- var COLUMNS = [
83658
- {
83659
- name: "id",
83660
- type: "TEXT",
83661
- primaryKey: true,
83662
- notNull: true
83663
- },
83664
- {
83665
- name: "name",
83666
- type: "TEXT",
83667
- notNull: true
83668
- },
83669
- {
83670
- name: "nameKey",
83671
- type: "TEXT",
83672
- notNull: true
83673
- },
83674
- {
83675
- name: "createdAt",
83676
- type: "INTEGER",
83677
- notNull: true
83678
- },
83679
- {
83680
- name: "selection",
83681
- type: "JSON",
83682
- notNull: true
83683
- },
83684
- {
83685
- name: "layout",
83686
- type: "TEXT",
83687
- notNull: true
83688
- },
83689
- {
83690
- name: "completeOnly",
83691
- type: "BOOLEAN",
83692
- notNull: true
83693
- },
83694
- {
83695
- name: "frameIds",
83696
- type: "JSON",
83697
- notNull: true
83698
- },
83699
- {
83700
- name: "annotationHash",
83701
- type: "TEXT",
83702
- notNull: true
83703
- },
83704
- {
83705
- name: "manifestSchema",
83706
- type: "TEXT",
83707
- notNull: true
83708
- },
83709
- {
83710
- name: "stats",
83711
- type: "JSON",
83712
- notNull: true
83713
- }
83714
- ];
83715
- var INDEXES = [{
83716
- name: "idx_retrain_dataset_versions_name_key",
83717
- columns: ["nameKey"],
83718
- unique: true
83719
- }, {
83720
- name: "idx_retrain_dataset_versions_created",
83721
- columns: ["createdAt"]
83722
- }];
83723
- function summaryOfVersion(version) {
83724
- const { frameIds, ...rest } = version;
83725
- return {
83726
- ...rest,
83727
- frameCount: frameIds.length
83728
- };
83729
- }
83730
- var nameKeyOf = (name) => name.trim().toLowerCase();
83731
- function numberList(value) {
83732
- return Array.isArray(value) ? value.filter((v) => typeof v === "number") : void 0;
83733
- }
83734
- function stringList(value) {
83735
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : void 0;
83736
- }
83737
- function parseJson(value) {
83738
- if (typeof value !== "string") return value;
83739
- try {
83740
- return JSON.parse(value);
83741
- } catch {
83742
- return;
83743
- }
83744
- }
83745
- function asRecord(value) {
83746
- return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
83747
- }
83748
- var EMPTY_STATS = {
83749
- frames: 0,
83750
- completeFrames: 0,
83751
- completeness: 0,
83752
- subjects: 0,
83753
- modelErrors: 0,
83754
- hardNegatives: 0,
83755
- cells: [],
83756
- cameras: []
83757
- };
83758
- /** A stored stats object, trusted only as far as its shape. */
83759
- function readStats(value) {
83760
- const raw = asRecord(parseJson(value));
83761
- const num = (k) => typeof raw[k] === "number" ? raw[k] : 0;
83762
- const cells = Array.isArray(raw["cells"]) ? raw["cells"] : [];
83763
- const cameras = Array.isArray(raw["cameras"]) ? raw["cameras"] : [];
83764
- return {
83765
- ...EMPTY_STATS,
83766
- frames: num("frames"),
83767
- completeFrames: num("completeFrames"),
83768
- completeness: num("completeness"),
83769
- subjects: num("subjects"),
83770
- modelErrors: num("modelErrors"),
83771
- hardNegatives: num("hardNegatives"),
83772
- cells: cells.map((c) => {
83773
- const r = asRecord(c);
83774
- const lighting = r["lighting"] === "day" || r["lighting"] === "night" ? r["lighting"] : "unknown";
83775
- return {
83776
- macroClass: String(r["macroClass"] ?? ""),
83777
- deviceId: Number(r["deviceId"] ?? 0),
83778
- lighting,
83779
- subjects: Number(r["subjects"] ?? 0),
83780
- frames: Number(r["frames"] ?? 0)
83781
- };
83782
- }),
83783
- cameras: cameras.map((c) => {
83784
- const r = asRecord(c);
83785
- return {
83786
- deviceId: Number(r["deviceId"] ?? 0),
83787
- frames: Number(r["frames"] ?? 0),
83788
- completeFrames: Number(r["completeFrames"] ?? 0)
83789
- };
83790
- })
83791
- };
83792
- }
83793
- var RetrainDatasetVersionStore = class {
83794
- store;
83795
- logger;
83796
- constructor(deps) {
83797
- this.store = deps.store;
83798
- this.logger = deps.logger;
83799
- }
83800
- static async declare(store) {
83801
- await store.declareCollection.mutate({
83802
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83803
- columns: [...COLUMNS],
83804
- indexes: [...INDEXES]
83805
- });
83806
- }
83807
- insert = async (version) => {
83808
- await this.store.set.mutate({
83809
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83810
- key: version.id,
83811
- value: {
83812
- name: version.name,
83813
- nameKey: nameKeyOf(version.name),
83814
- createdAt: version.createdAt,
83815
- selection: version.selection,
83816
- layout: version.layout,
83817
- completeOnly: version.completeOnly,
83818
- frameIds: version.frameIds,
83819
- annotationHash: version.annotationHash,
83820
- manifestSchema: version.manifestSchema,
83821
- stats: version.stats
83822
- }
83823
- });
83824
- };
83825
- get = async (id) => {
83826
- try {
83827
- const row = await this.store.get.query({
83828
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83829
- key: id
83830
- });
83831
- return row === null || row === void 0 ? null : this.toVersion(id, asRecord(row));
83832
- } catch (err) {
83833
- this.logger.warn("retrain dataset version read failed", { meta: {
83834
- versionId: id,
83835
- error: String(err)
83836
- } });
83837
- return null;
83838
- }
83839
- };
83840
- list = async () => {
83841
- return (await this.listRows()).map(summaryOfVersion);
83842
- };
83843
- /** Every row WITH its frame list — the migration into `training` reads this. */
83844
- listRows = async () => {
83845
- try {
83846
- return (await this.store.query.query({
83847
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83848
- filter: { orderBy: {
83849
- field: "createdAt",
83850
- direction: "desc"
83851
- } }
83852
- })).map((r) => this.toVersion(r.id, asRecord(r.data)));
83853
- } catch (err) {
83854
- this.logger.warn("retrain dataset versions list failed", { meta: { error: String(err) } });
83855
- return [];
83856
- }
83857
- };
83858
- findIdByName = async (name) => {
83859
- return (await this.store.query.query({
83860
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83861
- filter: {
83862
- where: { nameKey: nameKeyOf(name) },
83863
- limit: 1
83864
- }
83865
- }))[0]?.id ?? null;
83866
- };
83867
- remove = async (id) => {
83868
- if (await this.get(id) === null) return false;
83869
- await this.store.delete.mutate({
83870
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83871
- key: id
83872
- });
83873
- return true;
83874
- };
83875
- toVersion(id, data) {
83876
- const selection = asRecord(parseJson(data["selection"]));
83877
- const deviceIds = numberList(selection["deviceIds"]);
83878
- const collectionIds = stringList(selection["collectionIds"]);
83879
- const frameIdsSel = stringList(selection["frameIds"]);
83880
- return {
83881
- id,
83882
- name: String(data["name"] ?? ""),
83883
- createdAt: Number(data["createdAt"] ?? 0),
83884
- selection: {
83885
- ...deviceIds !== void 0 ? { deviceIds } : {},
83886
- ...collectionIds !== void 0 ? { collectionIds } : {},
83887
- ...frameIdsSel !== void 0 ? { frameIds: frameIdsSel } : {}
83888
- },
83889
- layout: data["layout"] === "crops" ? "crops" : "frames",
83890
- completeOnly: data["completeOnly"] === true || data["completeOnly"] === 1,
83891
- frameIds: stringList(parseJson(data["frameIds"])) ?? [],
83892
- annotationHash: String(data["annotationHash"] ?? ""),
83893
- manifestSchema: String(data["manifestSchema"] ?? ""),
83894
- stats: readStats(data["stats"])
83895
- };
83896
- }
83897
- };
83898
- //#endregion
83899
- //#region src/pipeline-analytics/retrain/retrain-geometry.ts
83900
- function clamp01(v) {
83901
- return v < 0 ? 0 : v > 1 ? 1 : v;
83902
- }
83903
- /** Is this a usable rectangle at all? A zero-area box is not a subject. */
83904
- function isUsableRect(r) {
83905
- return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
83906
- }
83907
- /**
83908
- * The subject inside a square LETTERBOX of the full frame.
83909
- *
83910
- * The frame is scaled to fit the square and centred; the padding bars take the
83911
- * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
83912
- * height, and every y coordinate shifts by half the leftover.
83913
- *
83914
- * Square-side agnostic on purpose: the answer is normalised against the square,
83915
- * so it is the same number for a 640 and a 1280 input and no caller has to know
83916
- * which the model uses.
83917
- */
83918
- function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
83919
- if (!isUsableRect(bbox)) return null;
83920
- if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
83921
- const longSide = Math.max(frameWidth, frameHeight);
83922
- const scaleX = frameWidth / longSide;
83923
- const scaleY = frameHeight / longSide;
83924
- const offsetX = (1 - scaleX) / 2;
83925
- const offsetY = (1 - scaleY) / 2;
83926
- return {
83927
- bbox: {
83928
- x: offsetX + bbox.x * scaleX,
83929
- y: offsetY + bbox.y * scaleY,
83930
- w: bbox.w * scaleX,
83931
- h: bbox.h * scaleY
83932
- },
83933
- visibleFraction: 1
83934
- };
83935
- }
83936
- /**
83937
- * The subject inside a ZONE CROP — the shape a package model is trained on.
83938
- *
83939
- * Returns `null` when the subject does not intersect the zone at all. That is
83940
- * the whole reason the package proposal runs on the crop rather than the frame:
83941
- * a package detector at 0.35 on a full frame proposes furniture, and a subject
83942
- * outside the zone is not part of this model's world.
83943
- */
83944
- function deriveZoneCropRect(bbox, zone) {
83945
- if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
83946
- const left = Math.max(bbox.x, zone.x);
83947
- const top = Math.max(bbox.y, zone.y);
83948
- const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
83949
- const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
83950
- if (!(right > left) || !(bottom > top)) return null;
83951
- const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
83952
- return {
83953
- bbox: {
83954
- x: (left - zone.x) / zone.w,
83955
- y: (top - zone.y) / zone.h,
83956
- w: (right - left) / zone.w,
83957
- h: (bottom - top) / zone.h
83958
- },
83959
- visibleFraction
83960
- };
83961
- }
83962
- /**
83963
- * The subject inside its OWN padded crop — the shape a classifier is trained on.
83964
- *
83965
- * The crop is the box grown by `padding` of its own size on each side and then
83966
- * clipped to the frame, so the subject sits in the middle and the classifier
83967
- * still sees a little context. The returned rectangle is where the subject
83968
- * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
83969
- * clipped by a frame edge — a subject at the left border is flush left in its
83970
- * crop, and a caller that assumed the centre would draw the box in the wrong
83971
- * place.
83972
- */
83973
- function deriveSubjectCropRect(bbox, padding) {
83974
- if (!isUsableRect(bbox)) return null;
83975
- const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
83976
- const cropX = clamp01(bbox.x - bbox.w * pad);
83977
- const cropY = clamp01(bbox.y - bbox.h * pad);
83978
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
83979
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
83980
- const cropW = cropRight - cropX;
83981
- const cropH = cropBottom - cropY;
83982
- if (!(cropW > 0) || !(cropH > 0)) return null;
83983
- return {
83984
- bbox: {
83985
- x: (bbox.x - cropX) / cropW,
83986
- y: (bbox.y - cropY) / cropH,
83987
- w: bbox.w / cropW,
83988
- h: bbox.h / cropH
83989
- },
83990
- visibleFraction: 1
83991
- };
83992
- }
83993
- //#endregion
83994
- //#region src/pipeline-analytics/retrain/retrain-lighting.ts
83995
- function isNight(condition) {
83996
- return condition === "night" || condition === "ir";
83997
- }
83998
- function retrainFrameLighting(input) {
83999
- if (input.capturedAt === void 0) {
84000
- const measured = resolveConditionDetailed({
84001
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84002
- now: /* @__PURE__ */ new Date(0)
84003
- });
84004
- return measured.source === "ir-chroma" ? {
84005
- condition: measured.condition,
84006
- night: true,
84007
- source: measured.source
84008
- } : null;
84009
- }
84010
- const resolved = resolveConditionDetailed({
84011
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84012
- ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
84013
- now: new Date(input.capturedAt)
84014
- });
84015
- return {
84016
- ...resolved,
84017
- night: isNight(resolved.condition)
84018
- };
84019
- }
84020
- //#endregion
84021
- //#region src/pipeline-analytics/retrain/retrain-export.ts
84022
- /**
84023
- * The ANNOTATED dataset export — the retrain page's own archive.
84024
- *
84025
- * Sibling of `training-export/`, deliberately not the same thing. That one
84026
- * streams a marked track's stored media verbatim and knows nothing about what
84027
- * is in it; this one streams the frames the operator SELECTED (already copied
84028
- * into retrain storage) plus the ground truth they drew.
84029
- *
84030
- * ## The one interesting decision
84031
- *
84032
- * `annotations.json` carries, for every subject, the canonical full-frame box
84033
- * AND the geometry derived for each model shape. **Derived here, at export,
84034
- * never stored** — one box in, three shapes out. Storing the derived shapes is
84035
- * how two crops of the same subject end up in one feature space, which is the
84036
- * failure already recorded for CLIP (D52). A consumer that wants a different
84037
- * input size or a different padding re-derives from the canonical box; nothing
84038
- * in the database has to change and nothing can disagree.
84039
- *
84040
- * Everything else follows the export next door: index reads to plan, one JPEG
84041
- * in memory at a time, a yield between files, and NO inference — a training
84042
- * set's pixels are the pixels the pipeline saw.
84043
- */
84044
- var RETRAIN_EXPORT_PREFIX = "retrain-export";
84045
- /**
84046
- * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
84047
- * padding, read from the one convention `deriveDetailCropRect` cuts with
84048
- * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
84049
- * here while the runtime fed `0.15`, so a classifier trained from this export
84050
- * learned on more context than it is ever given live.
84051
- */
84052
- var CLASSIFIER_CROP_PADDING = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
84053
- /** The convention a `crops` layout cuts with — the runtime default, recorded
84054
- * in the manifest so a consumer never has to assume it. */
84055
- var RETRAIN_CROP_CONVENTION = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION;
84056
- /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
84057
- * the trainer resizes per model, so this only bounds the upload. */
84058
- var RETRAIN_CROP_MAX_SIDE = 256;
84059
- /**
84060
- * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
84061
- * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
84062
- * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
84063
- */
84064
- var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
84065
- /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
84066
- * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
84067
- function retrainFramePath(frame) {
84068
- const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
84069
- const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
84070
- return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
84071
- }
84072
- /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
84073
- function retrainCropPath(macroClass, annotationId) {
84074
- return `crops/${macroClass}/${annotationId}.jpg`;
84075
- }
84076
- /**
84077
- * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
84078
- * filter keeps everything. Order is the input's.
84079
- */
84080
- function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
84081
- const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
84082
- const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
84083
- const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84084
- return frames.filter((frame) => {
84085
- if (wanted !== null && !wanted.has(frame.frameId)) return false;
84086
- if (devices !== null && !devices.has(frame.deviceId)) return false;
84087
- if (filter.completeOnly === true && !frame.complete) return false;
84088
- if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
84089
- if (filter.batchId !== void 0) {
84090
- if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
84091
- }
84092
- return true;
84093
- });
84094
- }
84095
- /**
84096
- * Derive every per-model shape for one annotation.
84097
- *
84098
- * `zone` is optional and per-frame: only a package annotation has one, and a
84099
- * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
84100
- * clamped — a clamped box teaches the model that the subject ends where the
84101
- * crop ends.
84102
- */
84103
- function deriveAll(annotation, frame, zone) {
84104
- const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
84105
- const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
84106
- const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
84107
- return {
84108
- ...letterbox !== null ? { letterbox } : {},
84109
- ...zoneCrop !== null ? { zoneCrop } : {},
84110
- ...subjectCrop !== null ? { subjectCrop } : {}
84111
- };
84112
- }
84113
- /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
84114
- function buildRetrainManifest(input) {
84115
- const layout = input.layout ?? "frames";
84116
- const coordinates = input.coordinates ?? null;
84117
- let annotationCount = 0;
84118
- let modelErrorCount = 0;
84119
- const frames = input.frames.map((frame) => {
84120
- const zone = input.zoneForDevice(frame.deviceId);
84121
- const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
84122
- annotationCount += 1;
84123
- if (a.kind === "model_error") modelErrorCount += 1;
84124
- const derived = deriveAll(a, frame, zone);
84125
- return {
84126
- id: a.id,
84127
- macroClass: a.macroClass,
84128
- ...a.label !== void 0 ? { label: a.label } : {},
84129
- ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
84130
- kind: a.kind,
84131
- source: a.source,
84132
- ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
84133
- ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
84134
- bbox: a.bbox,
84135
- ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
84136
- derived
84137
- };
84138
- });
84139
- return {
84140
- frameId: frame.frameId,
84141
- path: retrainFramePath(frame),
84142
- deviceId: frame.deviceId,
84143
- origin: frame.origin,
84144
- ...frame.origin === "track" ? { trackId: frame.trackId } : {},
84145
- ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
84146
- collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
84147
- width: frame.width,
84148
- height: frame.height,
84149
- sizeBytes: frame.sizeBytes,
84150
- sourceKind: frame.sourceKind,
84151
- copiedAt: frame.copiedAt,
84152
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84153
- complete: frame.complete,
84154
- lighting: retrainFrameLighting({
84155
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84156
- ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
84157
- coordinates
84158
- }),
84159
- ...zone !== null ? { zoneUsed: zone } : {},
84160
- annotations
84161
- };
84162
- });
84163
- return {
84164
- schema: RETRAIN_MANIFEST_SCHEMA,
84165
- layout,
84166
- detailCropConvention: RETRAIN_CROP_CONVENTION,
84167
- ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
84168
- generatedAt: input.generatedAt,
84169
- batchId: input.batchId,
84170
- frameCount: frames.length,
84171
- annotationCount,
84172
- modelErrorCount,
84173
- classifierCropPadding: CLASSIFIER_CROP_PADDING,
84174
- frames
84175
- };
84176
- }
84177
- //#endregion
84178
- //#region src/pipeline-analytics/retrain/retrain-dataset-service.ts
84179
- function filterOf(selection, completeOnly) {
84180
- return {
84181
- ...selection.deviceIds !== void 0 && selection.deviceIds.length > 0 ? { deviceIds: [...selection.deviceIds] } : {},
84182
- ...selection.collectionIds !== void 0 && selection.collectionIds.length > 0 ? { collectionIds: [...selection.collectionIds] } : {},
84183
- ...selection.frameIds !== void 0 && selection.frameIds.length > 0 ? { frameIds: [...selection.frameIds] } : {},
84184
- ...completeOnly ? { completeOnly: true } : {}
84185
- };
84186
- }
84187
- var RetrainDatasetService = class {
84188
- deps;
84189
- constructor(deps) {
84190
- this.deps = deps;
84191
- }
84192
- /** The counts a selection would freeze, without freezing anything. */
84193
- async preview(input) {
84194
- return datasetStats((await this.deps.buildPlan({
84195
- filter: filterOf(input.selection, input.completeOnly),
84196
- layout: "frames"
84197
- })).manifest);
84198
- }
84199
- async create(input) {
84200
- const name = input.name.trim();
84201
- if (name === "") return {
84202
- kind: "refused",
84203
- reason: "empty-name"
84204
- };
84205
- if (await this.deps.versions.findIdByName(name) !== null) return {
84206
- kind: "refused",
84207
- reason: "duplicate-name"
84208
- };
84209
- const plan = await this.deps.buildPlan({
84210
- filter: filterOf(input.selection, input.completeOnly),
84211
- layout: input.layout
84212
- });
84213
- if (plan.manifest.frames.length === 0) return {
84214
- kind: "refused",
84215
- reason: "no-frames"
84216
- };
84217
- const version = {
84218
- id: this.deps.newId(),
84219
- name,
84220
- createdAt: this.deps.now(),
84221
- selection: input.selection,
84222
- layout: input.layout,
84223
- completeOnly: input.completeOnly,
84224
- frameIds: plan.manifest.frames.map((f) => f.frameId).toSorted(),
84225
- annotationHash: datasetAnnotationHash(plan.manifest.frames),
84226
- manifestSchema: RETRAIN_MANIFEST_SCHEMA,
84227
- stats: datasetStats(plan.manifest)
84228
- };
84229
- await this.deps.versions.insert(version);
84230
- this.deps.logger.info("retrain dataset version created", { meta: {
84231
- versionId: version.id,
84232
- frames: version.frameIds.length,
84233
- subjects: version.stats.subjects,
84234
- completeness: version.stats.completeness,
84235
- layout: version.layout
84236
- } });
84237
- return {
84238
- kind: "created",
84239
- version: summaryOfVersion(version)
84240
- };
84241
- }
84242
- async list() {
84243
- return this.deps.versions.list();
84244
- }
84245
- async delete(input) {
84246
- const deleted = await this.deps.versions.remove(input.id);
84247
- if (deleted) this.deps.logger.info("retrain dataset version deleted", { meta: { versionId: input.id } });
84248
- return { deleted };
84249
- }
84250
- /** Does the corpus still teach what the version froze? */
84251
- async verify(input) {
84252
- const version = await this.deps.versions.get(input.id);
84253
- if (version === null) return {
84254
- found: false,
84255
- annotationHash: "",
84256
- matches: false,
84257
- missingFrames: 0
84258
- };
84259
- const plan = await this.deps.buildPlan({
84260
- filter: { frameIds: [...version.frameIds] },
84261
- layout: "frames"
84262
- });
84263
- const present = new Map(plan.manifest.frames.map((f) => [f.frameId, f]));
84264
- const annotationHash = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
84265
- frameId,
84266
- complete: false,
84267
- annotations: []
84268
- }));
84269
- return {
84270
- found: true,
84271
- annotationHash,
84272
- matches: annotationHash === version.annotationHash,
84273
- missingFrames: version.frameIds.filter((id) => !present.has(id)).length
84274
- };
84275
- }
84276
- /**
84277
- * An export filter with `datasetVersionId` resolved to that version's
84278
- * frames (and its completeOnly). An unknown version exports NOTHING — never
84279
- * the whole dataset, which is what dropping the field would do.
84280
- */
84281
- async exportFilterFor(filter) {
84282
- if (filter.datasetVersionId === void 0) return filter;
84283
- const { datasetVersionId, ...rest } = filter;
84284
- const version = await this.deps.versions.get(datasetVersionId);
84285
- if (version === null) {
84286
- this.deps.logger.warn("retrain export asked for an unknown dataset version", { meta: { versionId: datasetVersionId } });
84287
- return {
84288
- ...rest,
84289
- frameIds: []
84290
- };
84291
- }
84292
- const wanted = rest.frameIds !== void 0 ? version.frameIds.filter((id) => rest.frameIds?.includes(id) === true) : version.frameIds;
84293
- return {
84294
- ...rest,
84295
- frameIds: [...wanted],
84296
- ...version.completeOnly ? { completeOnly: true } : {}
84297
- };
84298
- }
84299
- };
84300
- //#endregion
84301
- //#region src/pipeline-analytics/retrain/retrain-dataset-version-routing.ts
84302
- var RoutingDatasetVersionRepository = class {
84303
- deps;
84304
- lastPresence = false;
84305
- migrated = false;
84306
- migration = null;
84307
- constructor(deps) {
84308
- this.deps = deps;
84309
- }
84310
- insert = async (version) => {
84311
- if (await this.route() === "local") {
84312
- await this.deps.local.insert(version);
84313
- return;
84314
- }
84315
- await this.deps.training.put(version);
84316
- };
84317
- get = async (id) => await this.route() === "local" ? this.deps.local.get(id) : this.deps.training.get(id);
84318
- list = async () => await this.route() === "local" ? this.deps.local.list() : this.deps.training.list();
84319
- findIdByName = async (name) => await this.route() === "local" ? this.deps.local.findIdByName(name) : this.deps.training.findIdByName(name);
84320
- remove = async (id) => await this.route() === "local" ? this.deps.local.remove(id) : this.deps.training.remove(id);
84321
- async route() {
84322
- if (!await this.presence()) return "local";
84323
- if (this.migrated) return "training";
84324
- return await this.migrateOnce() ? "training" : "local";
84325
- }
84326
- async presence() {
84327
- let answer;
84328
- try {
84329
- answer = await this.deps.trainingPresent();
84330
- } catch {
84331
- answer = null;
84332
- }
84333
- if (answer !== null) this.lastPresence = answer;
84334
- return this.lastPresence;
84335
- }
84336
- migrateOnce() {
84337
- if (this.migration === null) this.migration = this.migrate().finally(() => {
84338
- this.migration = null;
84339
- });
84340
- return this.migration;
84341
- }
84342
- async migrate() {
84343
- const { local, training, logger } = this.deps;
84344
- try {
84345
- const rows = await local.listRows();
84346
- for (const row of rows) {
84347
- await training.put(row);
84348
- await local.remove(row.id);
84349
- }
84350
- this.migrated = true;
84351
- if (rows.length > 0) logger.info("retrain dataset versions moved to the training capability", { meta: { versions: rows.length } });
84352
- return true;
84353
- } catch (err) {
84354
- logger.warn("retrain dataset versions could not move to training yet — kept local", { meta: { error: err instanceof Error ? err.message : String(err) } });
84355
- return false;
84356
- }
84357
- }
84358
- };
84359
- //#endregion
84360
83525
  //#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
84361
83526
  var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
84362
83527
  var ALWAYS_CLOUD_KINDS = new Set([
@@ -85125,7 +84290,526 @@ async function cutRetrainCrop(input) {
85125
84290
  return pipeline.jpeg({ quality: 92 }).toBuffer();
85126
84291
  }
85127
84292
  //#endregion
84293
+ //#region src/pipeline-analytics/retrain/retrain-dataset.ts
84294
+ /**
84295
+ * Dataset stats and annotation hash over an export manifest. Pure.
84296
+ *
84297
+ * Both read the export MANIFEST (`retrain-export.ts`), not the stores: the
84298
+ * counts a plan is judged by cannot disagree with the archive a trainer
84299
+ * receives.
84300
+ */
84301
+ function lightingOf(frame) {
84302
+ if (frame.lighting === null) return "unknown";
84303
+ return frame.lighting.night ? "night" : "day";
84304
+ }
84305
+ /** The counts of a dataset, from the manifest an export of it would carry. */
84306
+ function datasetStats(manifest) {
84307
+ const cells = /* @__PURE__ */ new Map();
84308
+ const cameras = /* @__PURE__ */ new Map();
84309
+ let completeFrames = 0;
84310
+ let subjects = 0;
84311
+ let modelErrors = 0;
84312
+ let hardNegatives = 0;
84313
+ for (const frame of manifest.frames) {
84314
+ const camera = cameras.get(frame.deviceId) ?? {
84315
+ frames: 0,
84316
+ completeFrames: 0
84317
+ };
84318
+ cameras.set(frame.deviceId, {
84319
+ frames: camera.frames + 1,
84320
+ completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
84321
+ });
84322
+ if (frame.complete) completeFrames += 1;
84323
+ const lighting = lightingOf(frame);
84324
+ let frameSubjects = 0;
84325
+ for (const a of frame.annotations) {
84326
+ if (a.kind === "model_error") {
84327
+ modelErrors += 1;
84328
+ continue;
84329
+ }
84330
+ frameSubjects += 1;
84331
+ const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
84332
+ const entry = cells.get(key) ?? {
84333
+ cell: {
84334
+ macroClass: a.macroClass,
84335
+ deviceId: frame.deviceId,
84336
+ lighting,
84337
+ subjects: 0,
84338
+ frames: 0
84339
+ },
84340
+ frameIds: /* @__PURE__ */ new Set()
84341
+ };
84342
+ entry.frameIds.add(frame.frameId);
84343
+ cells.set(key, {
84344
+ cell: {
84345
+ ...entry.cell,
84346
+ subjects: entry.cell.subjects + 1,
84347
+ frames: entry.frameIds.size
84348
+ },
84349
+ frameIds: entry.frameIds
84350
+ });
84351
+ }
84352
+ subjects += frameSubjects;
84353
+ if (frame.complete && frameSubjects === 0) hardNegatives += 1;
84354
+ }
84355
+ const frames = manifest.frames.length;
84356
+ return {
84357
+ frames,
84358
+ completeFrames,
84359
+ completeness: frames > 0 ? completeFrames / frames : 0,
84360
+ subjects,
84361
+ modelErrors,
84362
+ hardNegatives,
84363
+ cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
84364
+ cameras: [...cameras.entries()].map(([deviceId, c]) => ({
84365
+ deviceId,
84366
+ ...c
84367
+ })).toSorted((a, b) => a.deviceId - b.deviceId)
84368
+ };
84369
+ }
84370
+ /** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
84371
+ var q = (n) => Math.round(n * 1e4) / 1e4;
84372
+ /**
84373
+ * The sha256 of what the frames TEACH: per frame its completeness and, per
84374
+ * box, the class tiers, the kind and the box. Row ids, timestamps, export
84375
+ * stamps and provenance are left out — they change on a re-save that teaches
84376
+ * the same thing. Order-independent.
84377
+ */
84378
+ function datasetAnnotationHash(frames) {
84379
+ const canonical = frames.map((f) => ({
84380
+ f: f.frameId,
84381
+ c: f.complete,
84382
+ a: f.annotations.map((a) => [
84383
+ a.macroClass,
84384
+ a.label ?? "",
84385
+ a.subLabel ?? "",
84386
+ a.kind,
84387
+ q(a.bbox.x),
84388
+ q(a.bbox.y),
84389
+ q(a.bbox.w),
84390
+ q(a.bbox.h)
84391
+ ].join("|")).toSorted()
84392
+ })).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
84393
+ return (0, node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
84394
+ }
84395
+ //#endregion
84396
+ //#region src/pipeline-analytics/retrain/retrain-corpus-query.ts
84397
+ /** Map a frame row and its saved annotation count to the exclusive work state. */
84398
+ function workStateOf(frame, annotationCount) {
84399
+ if (annotationCount <= 0) return "unannotated";
84400
+ if (frame.complete) return "complete";
84401
+ return "annotated";
84402
+ }
84403
+ /** `complete(camera) / totalCorpus`, or `null` when the corpus is empty. */
84404
+ function corpusPctOfCorpus(completeOnCamera, totalCorpus) {
84405
+ if (totalCorpus === 0) return null;
84406
+ return completeOnCamera / totalCorpus;
84407
+ }
84408
+ /** Every narrowing field is ANDed; omitted fields impose no constraint. */
84409
+ function filterCorpusRows(rows, filter) {
84410
+ const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84411
+ const classes = filter.classes !== void 0 && filter.classes.length > 0 ? new Set(filter.classes) : null;
84412
+ return rows.filter((row) => {
84413
+ if (devices !== null && !devices.has(row.deviceId)) return false;
84414
+ if (filter.collectionId !== void 0 && !row.collectionIds.includes(filter.collectionId)) return false;
84415
+ if (classes !== null && !row.classes.some((c) => classes.has(c))) return false;
84416
+ if (filter.workState !== void 0 && workStateOf(row, row.annotationCount) !== filter.workState) return false;
84417
+ return true;
84418
+ });
84419
+ }
84420
+ /**
84421
+ * Newest copies first (`copiedAt` desc). Stable on equal timestamps by frameId.
84422
+ */
84423
+ function sortCorpusRowsNewestFirst(rows) {
84424
+ return [...rows].toSorted((a, b) => {
84425
+ if (b.copiedAt !== a.copiedAt) return b.copiedAt - a.copiedAt;
84426
+ return a.frameId < b.frameId ? -1 : a.frameId > b.frameId ? 1 : 0;
84427
+ });
84428
+ }
84429
+ /** 1-based page slice. An empty match is `{ rows: [], total: 0, … }`. */
84430
+ function paginateCorpusRows(rows, page, pageSize) {
84431
+ const safePage = page < 1 ? 1 : page;
84432
+ const safeSize = pageSize < 1 ? 1 : pageSize;
84433
+ const start = (safePage - 1) * safeSize;
84434
+ return {
84435
+ rows: rows.slice(start, start + safeSize),
84436
+ total: rows.length,
84437
+ page: safePage,
84438
+ pageSize: safeSize
84439
+ };
84440
+ }
84441
+ /** Global + per-camera KPI; classification always via {@link workStateOf}. */
84442
+ function aggregateCorpusKpi(rows) {
84443
+ let unannotated = 0;
84444
+ let annotated = 0;
84445
+ let complete = 0;
84446
+ const perCamera = /* @__PURE__ */ new Map();
84447
+ for (const row of rows) {
84448
+ const state = workStateOf(row, row.annotationCount);
84449
+ const cam = perCamera.get(row.deviceId) ?? {
84450
+ unannotated: 0,
84451
+ annotated: 0,
84452
+ complete: 0
84453
+ };
84454
+ if (state === "unannotated") {
84455
+ unannotated += 1;
84456
+ cam.unannotated += 1;
84457
+ } else if (state === "annotated") {
84458
+ annotated += 1;
84459
+ cam.annotated += 1;
84460
+ } else {
84461
+ complete += 1;
84462
+ cam.complete += 1;
84463
+ }
84464
+ perCamera.set(row.deviceId, cam);
84465
+ }
84466
+ const total = rows.length;
84467
+ const cameras = [...perCamera.entries()].toSorted(([a], [b]) => a - b).map(([deviceId, counts]) => ({
84468
+ deviceId,
84469
+ unannotated: counts.unannotated,
84470
+ annotated: counts.annotated,
84471
+ complete: counts.complete,
84472
+ pctOfCorpus: corpusPctOfCorpus(counts.complete, total)
84473
+ }));
84474
+ return {
84475
+ total,
84476
+ unannotated,
84477
+ annotated,
84478
+ complete,
84479
+ cameras
84480
+ };
84481
+ }
84482
+ //#endregion
84483
+ //#region src/pipeline-analytics/retrain/retrain-geometry.ts
84484
+ function clamp01(v) {
84485
+ return v < 0 ? 0 : v > 1 ? 1 : v;
84486
+ }
84487
+ /** Is this a usable rectangle at all? A zero-area box is not a subject. */
84488
+ function isUsableRect(r) {
84489
+ return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
84490
+ }
84491
+ /**
84492
+ * The subject inside a square LETTERBOX of the full frame.
84493
+ *
84494
+ * The frame is scaled to fit the square and centred; the padding bars take the
84495
+ * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
84496
+ * height, and every y coordinate shifts by half the leftover.
84497
+ *
84498
+ * Square-side agnostic on purpose: the answer is normalised against the square,
84499
+ * so it is the same number for a 640 and a 1280 input and no caller has to know
84500
+ * which the model uses.
84501
+ */
84502
+ function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
84503
+ if (!isUsableRect(bbox)) return null;
84504
+ if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
84505
+ const longSide = Math.max(frameWidth, frameHeight);
84506
+ const scaleX = frameWidth / longSide;
84507
+ const scaleY = frameHeight / longSide;
84508
+ const offsetX = (1 - scaleX) / 2;
84509
+ const offsetY = (1 - scaleY) / 2;
84510
+ return {
84511
+ bbox: {
84512
+ x: offsetX + bbox.x * scaleX,
84513
+ y: offsetY + bbox.y * scaleY,
84514
+ w: bbox.w * scaleX,
84515
+ h: bbox.h * scaleY
84516
+ },
84517
+ visibleFraction: 1
84518
+ };
84519
+ }
84520
+ /**
84521
+ * The subject inside a ZONE CROP — the shape a package model is trained on.
84522
+ *
84523
+ * Returns `null` when the subject does not intersect the zone at all. That is
84524
+ * the whole reason the package proposal runs on the crop rather than the frame:
84525
+ * a package detector at 0.35 on a full frame proposes furniture, and a subject
84526
+ * outside the zone is not part of this model's world.
84527
+ */
84528
+ function deriveZoneCropRect(bbox, zone) {
84529
+ if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
84530
+ const left = Math.max(bbox.x, zone.x);
84531
+ const top = Math.max(bbox.y, zone.y);
84532
+ const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
84533
+ const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
84534
+ if (!(right > left) || !(bottom > top)) return null;
84535
+ const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
84536
+ return {
84537
+ bbox: {
84538
+ x: (left - zone.x) / zone.w,
84539
+ y: (top - zone.y) / zone.h,
84540
+ w: (right - left) / zone.w,
84541
+ h: (bottom - top) / zone.h
84542
+ },
84543
+ visibleFraction
84544
+ };
84545
+ }
84546
+ /**
84547
+ * The subject inside its OWN padded crop — the shape a classifier is trained on.
84548
+ *
84549
+ * The crop is the box grown by `padding` of its own size on each side and then
84550
+ * clipped to the frame, so the subject sits in the middle and the classifier
84551
+ * still sees a little context. The returned rectangle is where the subject
84552
+ * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
84553
+ * clipped by a frame edge — a subject at the left border is flush left in its
84554
+ * crop, and a caller that assumed the centre would draw the box in the wrong
84555
+ * place.
84556
+ */
84557
+ function deriveSubjectCropRect(bbox, padding) {
84558
+ if (!isUsableRect(bbox)) return null;
84559
+ const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
84560
+ const cropX = clamp01(bbox.x - bbox.w * pad);
84561
+ const cropY = clamp01(bbox.y - bbox.h * pad);
84562
+ const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
84563
+ const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
84564
+ const cropW = cropRight - cropX;
84565
+ const cropH = cropBottom - cropY;
84566
+ if (!(cropW > 0) || !(cropH > 0)) return null;
84567
+ return {
84568
+ bbox: {
84569
+ x: (bbox.x - cropX) / cropW,
84570
+ y: (bbox.y - cropY) / cropH,
84571
+ w: bbox.w / cropW,
84572
+ h: bbox.h / cropH
84573
+ },
84574
+ visibleFraction: 1
84575
+ };
84576
+ }
84577
+ //#endregion
84578
+ //#region src/pipeline-analytics/retrain/retrain-lighting.ts
84579
+ function isNight(condition) {
84580
+ return condition === "night" || condition === "ir";
84581
+ }
84582
+ function retrainFrameLighting(input) {
84583
+ if (input.capturedAt === void 0) {
84584
+ const measured = resolveConditionDetailed({
84585
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84586
+ now: /* @__PURE__ */ new Date(0)
84587
+ });
84588
+ return measured.source === "ir-chroma" ? {
84589
+ condition: measured.condition,
84590
+ night: true,
84591
+ source: measured.source
84592
+ } : null;
84593
+ }
84594
+ const resolved = resolveConditionDetailed({
84595
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84596
+ ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
84597
+ now: new Date(input.capturedAt)
84598
+ });
84599
+ return {
84600
+ ...resolved,
84601
+ night: isNight(resolved.condition)
84602
+ };
84603
+ }
84604
+ //#endregion
84605
+ //#region src/pipeline-analytics/retrain/retrain-export.ts
84606
+ /**
84607
+ * The ANNOTATED dataset export — the retrain page's own archive.
84608
+ *
84609
+ * Sibling of `training-export/`, deliberately not the same thing. That one
84610
+ * streams a marked track's stored media verbatim and knows nothing about what
84611
+ * is in it; this one streams the frames the operator SELECTED (already copied
84612
+ * into retrain storage) plus the ground truth they drew.
84613
+ *
84614
+ * ## The one interesting decision
84615
+ *
84616
+ * `annotations.json` carries, for every subject, the canonical full-frame box
84617
+ * AND the geometry derived for each model shape. **Derived here, at export,
84618
+ * never stored** — one box in, three shapes out. Storing the derived shapes is
84619
+ * how two crops of the same subject end up in one feature space, which is the
84620
+ * failure already recorded for CLIP (D52). A consumer that wants a different
84621
+ * input size or a different padding re-derives from the canonical box; nothing
84622
+ * in the database has to change and nothing can disagree.
84623
+ *
84624
+ * Everything else follows the export next door: index reads to plan, one JPEG
84625
+ * in memory at a time, a yield between files, and NO inference — a training
84626
+ * set's pixels are the pixels the pipeline saw.
84627
+ */
84628
+ var RETRAIN_EXPORT_PREFIX = "retrain-export";
84629
+ /**
84630
+ * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
84631
+ * padding, read from the one convention `deriveDetailCropRect` cuts with
84632
+ * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
84633
+ * here while the runtime fed `0.15`, so a classifier trained from this export
84634
+ * learned on more context than it is ever given live.
84635
+ */
84636
+ var CLASSIFIER_CROP_PADDING = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
84637
+ /** The convention a `crops` layout cuts with — the runtime default, recorded
84638
+ * in the manifest so a consumer never has to assume it. */
84639
+ var RETRAIN_CROP_CONVENTION = require_clip_model_registry.DEFAULT_DETAIL_CROP_CONVENTION;
84640
+ /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
84641
+ * the trainer resizes per model, so this only bounds the upload. */
84642
+ var RETRAIN_CROP_MAX_SIDE = 256;
84643
+ /**
84644
+ * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
84645
+ * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
84646
+ * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
84647
+ */
84648
+ var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
84649
+ /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
84650
+ * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
84651
+ function retrainFramePath(frame) {
84652
+ const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
84653
+ const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
84654
+ return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
84655
+ }
84656
+ /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
84657
+ function retrainCropPath(macroClass, annotationId) {
84658
+ return `crops/${macroClass}/${annotationId}.jpg`;
84659
+ }
84660
+ /**
84661
+ * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
84662
+ * filter keeps everything. Order is the input's.
84663
+ */
84664
+ function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
84665
+ const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
84666
+ const collectionIds = /* @__PURE__ */ new Set();
84667
+ if (filter.collectionIds !== void 0) for (const id of filter.collectionIds) collectionIds.add(id);
84668
+ if (filter.collectionId !== void 0) collectionIds.add(filter.collectionId);
84669
+ const collections = collectionIds.size > 0 ? collectionIds : null;
84670
+ const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84671
+ const classes = filter.classes !== void 0 && filter.classes.length > 0 ? new Set(filter.classes) : null;
84672
+ return frames.filter((frame) => {
84673
+ if (wanted !== null && !wanted.has(frame.frameId)) return false;
84674
+ if (devices !== null && !devices.has(frame.deviceId)) return false;
84675
+ if (filter.completeOnly === true && !frame.complete) return false;
84676
+ if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
84677
+ const annotations = annotationsByFrame.get(frame.frameId) ?? [];
84678
+ if (filter.batchId !== void 0) {
84679
+ if (!annotations.some((a) => a.exportedInBatch === filter.batchId)) return false;
84680
+ }
84681
+ if (classes !== null && !annotations.some((a) => classes.has(a.macroClass))) return false;
84682
+ if (filter.workState !== void 0 && workStateOf(frame, annotations.length) !== filter.workState) return false;
84683
+ return true;
84684
+ });
84685
+ }
84686
+ /** Whether the export filter needs every corpus copy (not only annotated/complete). */
84687
+ function exportNeedsFullCorpus(filter) {
84688
+ if (filter.workState !== void 0) return true;
84689
+ if (filter.collectionId !== void 0) return true;
84690
+ if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) return true;
84691
+ if (filter.classes !== void 0 && filter.classes.length > 0) return true;
84692
+ return false;
84693
+ }
84694
+ /**
84695
+ * Derive every per-model shape for one annotation.
84696
+ *
84697
+ * `zone` is optional and per-frame: only a package annotation has one, and a
84698
+ * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
84699
+ * clamped — a clamped box teaches the model that the subject ends where the
84700
+ * crop ends.
84701
+ */
84702
+ function deriveAll(annotation, frame, zone) {
84703
+ const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
84704
+ const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
84705
+ const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
84706
+ return {
84707
+ ...letterbox !== null ? { letterbox } : {},
84708
+ ...zoneCrop !== null ? { zoneCrop } : {},
84709
+ ...subjectCrop !== null ? { subjectCrop } : {}
84710
+ };
84711
+ }
84712
+ /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
84713
+ function buildRetrainManifest(input) {
84714
+ const layout = input.layout ?? "frames";
84715
+ const coordinates = input.coordinates ?? null;
84716
+ let annotationCount = 0;
84717
+ let modelErrorCount = 0;
84718
+ const frames = input.frames.map((frame) => {
84719
+ const zone = input.zoneForDevice(frame.deviceId);
84720
+ const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
84721
+ annotationCount += 1;
84722
+ if (a.kind === "model_error") modelErrorCount += 1;
84723
+ const derived = deriveAll(a, frame, zone);
84724
+ return {
84725
+ id: a.id,
84726
+ macroClass: a.macroClass,
84727
+ ...a.label !== void 0 ? { label: a.label } : {},
84728
+ ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
84729
+ kind: a.kind,
84730
+ source: a.source,
84731
+ ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
84732
+ ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
84733
+ bbox: a.bbox,
84734
+ ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
84735
+ derived
84736
+ };
84737
+ });
84738
+ return {
84739
+ frameId: frame.frameId,
84740
+ path: retrainFramePath(frame),
84741
+ deviceId: frame.deviceId,
84742
+ origin: frame.origin,
84743
+ ...frame.origin === "track" ? { trackId: frame.trackId } : {},
84744
+ ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
84745
+ collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
84746
+ width: frame.width,
84747
+ height: frame.height,
84748
+ sizeBytes: frame.sizeBytes,
84749
+ sourceKind: frame.sourceKind,
84750
+ copiedAt: frame.copiedAt,
84751
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84752
+ complete: frame.complete,
84753
+ lighting: retrainFrameLighting({
84754
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84755
+ ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
84756
+ coordinates
84757
+ }),
84758
+ ...zone !== null ? { zoneUsed: zone } : {},
84759
+ annotations
84760
+ };
84761
+ });
84762
+ return {
84763
+ schema: RETRAIN_MANIFEST_SCHEMA,
84764
+ layout,
84765
+ detailCropConvention: RETRAIN_CROP_CONVENTION,
84766
+ ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
84767
+ generatedAt: input.generatedAt,
84768
+ batchId: input.batchId,
84769
+ frameCount: frames.length,
84770
+ annotationCount,
84771
+ modelErrorCount,
84772
+ classifierCropPadding: CLASSIFIER_CROP_PADDING,
84773
+ frames
84774
+ };
84775
+ }
84776
+ //#endregion
84777
+ //#region src/pipeline-analytics/retrain/retrain-training-corpus.ts
84778
+ /**
84779
+ * Export filter that loads the membership set for a selection.
84780
+ * `complete` uses `workState` so the plan is already narrowed; the other
84781
+ * modes load every copy in the collections and keep via {@link keepTrainingCorpusFrame}.
84782
+ */
84783
+ function exportFilterForTrainingCorpus(selection) {
84784
+ const collectionIds = [...selection.collectionIds];
84785
+ if (selection.workState === "complete") return {
84786
+ collectionIds,
84787
+ workState: "complete"
84788
+ };
84789
+ return { collectionIds };
84790
+ }
84791
+ /** Whether a manifest frame belongs under the selection's work-state rule. */
84792
+ function keepTrainingCorpusFrame(frame, workState) {
84793
+ const state = workStateOf(frame, frame.annotations.length);
84794
+ switch (workState) {
84795
+ case "complete": return state === "complete";
84796
+ case "annotated_or_complete": return state === "annotated" || state === "complete";
84797
+ case "any_annotated": return frame.annotations.length > 0 || frame.complete;
84798
+ }
84799
+ }
84800
+ //#endregion
85128
84801
  //#region src/pipeline-analytics/retrain/retrain-export-handler.ts
84802
+ function parseExportWorkState(raw) {
84803
+ if (raw === "unannotated" || raw === "annotated" || raw === "complete") return raw;
84804
+ }
84805
+ function parseExportClasses(raw) {
84806
+ const out = [];
84807
+ for (const part of raw.split(",")) {
84808
+ const trimmed = part.trim();
84809
+ if (isRetrainMacroClass(trimmed) && !out.includes(trimmed)) out.push(trimmed);
84810
+ }
84811
+ return out;
84812
+ }
85129
84813
  /** The export request a URL carries (`getRetrainExportUrl` composes it). */
85130
84814
  function parseRetrainExportRequest(url) {
85131
84815
  const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
@@ -85133,14 +84817,18 @@ function parseRetrainExportRequest(url) {
85133
84817
  const deviceIds = parseRetrainDeviceIds(url);
85134
84818
  const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
85135
84819
  const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
85136
- const datasetVersionId = params.get("datasetVersionId")?.trim() ?? "";
84820
+ const collectionId = params.get("collectionId")?.trim() ?? "";
84821
+ const classes = parseExportClasses(params.get("classes") ?? "");
84822
+ const workState = parseExportWorkState(params.get("workState")?.trim() ?? "");
85137
84823
  const batchId = params.get("batchId")?.trim() ?? "";
85138
84824
  return {
85139
84825
  filter: {
85140
84826
  ...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
85141
84827
  ...frameIds.length > 0 ? { frameIds } : {},
85142
84828
  ...collectionIds.length > 0 ? { collectionIds } : {},
85143
- ...datasetVersionId.length > 0 ? { datasetVersionId } : {},
84829
+ ...collectionId.length > 0 ? { collectionId } : {},
84830
+ ...classes.length > 0 ? { classes: [...classes] } : {},
84831
+ ...workState !== void 0 ? { workState } : {},
85144
84832
  ...batchId.length > 0 ? { batchId } : {},
85145
84833
  ...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
85146
84834
  },
@@ -85154,7 +84842,9 @@ function retrainExportQuery(request) {
85154
84842
  if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
85155
84843
  if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
85156
84844
  if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
85157
- if (filter.datasetVersionId !== void 0) params.set("datasetVersionId", filter.datasetVersionId);
84845
+ if (filter.collectionId !== void 0) params.set("collectionId", filter.collectionId);
84846
+ if (filter.workState !== void 0) params.set("workState", filter.workState);
84847
+ if (filter.classes !== void 0 && filter.classes.length > 0) params.set("classes", filter.classes.join(","));
85158
84848
  if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
85159
84849
  if (filter.completeOnly === true) params.set("completeOnly", "1");
85160
84850
  if (request.layout === "crops") params.set("layout", "crops");
@@ -86164,6 +85854,74 @@ var RetrainService = class {
86164
85854
  });
86165
85855
  return saved.map(toWireAnnotation);
86166
85856
  }
85857
+ /**
85858
+ * Paginated corpus list for the Frames grid. Index reads only — copies,
85859
+ * annotation counts/classes, collection membership; no JPEG.
85860
+ */
85861
+ async listCorpusPage(filter) {
85862
+ const page = paginateCorpusRows(sortCorpusRowsNewestFirst(filterCorpusRows(await this.loadCorpusRows(), filter)), filter.page, filter.pageSize);
85863
+ if (page.rows.length === 0) return {
85864
+ frames: [],
85865
+ total: page.total,
85866
+ page: page.page,
85867
+ pageSize: page.pageSize
85868
+ };
85869
+ const byId = new Map((await this.deps.frames.getMany(page.rows.map((r) => r.frameId))).map((f) => [f.frameId, f]));
85870
+ const copies = page.rows.flatMap((r) => {
85871
+ const frame = byId.get(r.frameId);
85872
+ return frame !== void 0 ? [frame] : [];
85873
+ });
85874
+ const wire = await this.toWireFrames(copies);
85875
+ const wireById = new Map(wire.map((f) => [f.frameId, f]));
85876
+ return {
85877
+ frames: page.rows.flatMap((r) => {
85878
+ const frame = wireById.get(r.frameId);
85879
+ if (frame === void 0) return [];
85880
+ return [{
85881
+ ...frame,
85882
+ workState: workStateOf(r, r.annotationCount)
85883
+ }];
85884
+ }),
85885
+ total: page.total,
85886
+ page: page.page,
85887
+ pageSize: page.pageSize
85888
+ };
85889
+ }
85890
+ /** Unfiltered corpus KPI — globals + per-camera rows via {@link workStateOf}. */
85891
+ async getCorpusKpi() {
85892
+ return aggregateCorpusKpi(await this.loadCorpusRows());
85893
+ }
85894
+ /** Index facts for every corpus copy. */
85895
+ async loadCorpusRows() {
85896
+ const frames = await this.deps.frames.listAll();
85897
+ if (frames.length === 0) return [];
85898
+ const frameIds = frames.map((f) => f.frameId);
85899
+ const annotations = await this.deps.annotations.listByFrames(frameIds);
85900
+ const countByFrame = /* @__PURE__ */ new Map();
85901
+ const classesByFrame = /* @__PURE__ */ new Map();
85902
+ for (const a of annotations) {
85903
+ countByFrame.set(a.mediaKey, (countByFrame.get(a.mediaKey) ?? 0) + 1);
85904
+ const classes = classesByFrame.get(a.mediaKey) ?? /* @__PURE__ */ new Set();
85905
+ classes.add(a.macroClass);
85906
+ classesByFrame.set(a.mediaKey, classes);
85907
+ }
85908
+ const collections = this.deps.memberships !== void 0 ? await this.deps.memberships.collectionsOf(frameIds).catch((err) => {
85909
+ this.deps.logger.warn("retrain corpus: collection membership unreadable", { meta: {
85910
+ frames: frameIds.length,
85911
+ error: String(err)
85912
+ } });
85913
+ return /* @__PURE__ */ new Map();
85914
+ }) : /* @__PURE__ */ new Map();
85915
+ return frames.map((f) => ({
85916
+ frameId: f.frameId,
85917
+ deviceId: f.deviceId,
85918
+ complete: f.complete,
85919
+ annotationCount: countByFrame.get(f.frameId) ?? 0,
85920
+ copiedAt: f.copiedAt,
85921
+ collectionIds: collections.get(f.frameId) ?? [],
85922
+ classes: [...classesByFrame.get(f.frameId) ?? []].toSorted()
85923
+ }));
85924
+ }
86167
85925
  /** `staging → trained` — the first and only writer of that state. */
86168
85926
  async complete(input) {
86169
85927
  const result = await completeRetrainTrack(this.lifecycleDeps(), input);
@@ -109263,8 +109021,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
109263
109021
  retrain = null;
109264
109022
  retrainCollections = null;
109265
109023
  retrainLlmAssist = null;
109266
- retrainDatasetVersions = null;
109267
- retrainDatasets = null;
109268
109024
  retrainCollectionStore = null;
109269
109025
  retrainFrames = null;
109270
109026
  retrainAnnotations = null;
@@ -111061,7 +110817,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
111061
110817
  RetrainFrameStore.declare(api.settingsStore),
111062
110818
  RetrainAnnotationStore.declare(api.settingsStore),
111063
110819
  RetrainCollectionStore.declare(api.settingsStore),
111064
- RetrainDatasetVersionStore.declare(api.settingsStore),
111065
110820
  OpsLogStore.declare(api.settingsStore),
111066
110821
  DebugNoteArchiveStore.declare(api.settingsStore),
111067
110822
  TrackingTapeIndexStore.declare(api.settingsStore),
@@ -111369,36 +111124,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
111369
111124
  captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
111370
111125
  logger: logger.child("RetrainCollections")
111371
111126
  });
111372
- const retrainDatasetVersions = new RetrainDatasetVersionStore({
111373
- store: api.settingsStore,
111374
- logger: logger.child("RetrainDatasets")
111375
- });
111376
- this.retrainDatasetVersions = retrainDatasetVersions;
111377
- const routedDatasetVersions = new RoutingDatasetVersionRepository({
111378
- local: retrainDatasetVersions,
111379
- training: {
111380
- put: (row) => api.training.putDatasetVersion.mutate({ ...row }),
111381
- get: (id) => api.training.getDatasetVersion.query({ id }),
111382
- list: async () => api.training.listDatasetVersions.query({}),
111383
- findIdByName: async (name) => (await api.training.findDatasetVersionIdByName.query({ name })).id,
111384
- remove: async (id) => (await api.training.removeDatasetVersion.mutate({ id })).removed
111385
- },
111386
- trainingPresent: async () => {
111387
- try {
111388
- return (await api.addons.listCapabilityProviders.query({ capName: "training" })).length > 0;
111389
- } catch {
111390
- return null;
111391
- }
111392
- },
111393
- logger: logger.child("RetrainDatasets")
111394
- });
111395
- this.retrainDatasets = new RetrainDatasetService({
111396
- versions: routedDatasetVersions,
111397
- buildPlan: (request) => this.buildRetrainExportPlan(request),
111398
- now: () => Date.now(),
111399
- newId: () => (0, node_crypto.randomUUID)(),
111400
- logger: logger.child("RetrainDatasets")
111401
- });
111402
111127
  this.retrainLlmAssist = new RetrainLlmAssistService({
111403
111128
  frames: retrainFrames,
111404
111129
  llm: {
@@ -112597,6 +112322,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
112597
112322
  ...input.limit !== void 0 ? { limit: input.limit } : {}
112598
112323
  });
112599
112324
  }
112325
+ async listRetrainCorpusPage(input) {
112326
+ return this.requireRetrain().listCorpusPage(input);
112327
+ }
112328
+ async getRetrainCorpusKpi(_input) {
112329
+ return this.requireRetrain().getCorpusKpi();
112330
+ }
112600
112331
  async listRetrainFrames(input) {
112601
112332
  return this.requireRetrain().listFrames(input.trackId);
112602
112333
  }
@@ -112651,29 +112382,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
112651
112382
  async setRetrainFrameComplete(input) {
112652
112383
  return this.requireRetrain().setFrameComplete(input);
112653
112384
  }
112654
- requireRetrainDatasets() {
112655
- const service = this.retrainDatasets;
112656
- if (service === null) throw new Error("retrain datasets: analytics stores unavailable");
112657
- return service;
112658
- }
112659
- async previewRetrainDataset(input) {
112660
- return this.requireRetrainDatasets().preview({
112661
- selection: input.selection,
112662
- completeOnly: input.completeOnly ?? false
112663
- });
112664
- }
112665
- async createRetrainDatasetVersion(input) {
112666
- return this.requireRetrainDatasets().create(input);
112667
- }
112668
- async listRetrainDatasetVersions(_input) {
112669
- return this.requireRetrainDatasets().list();
112670
- }
112671
- async deleteRetrainDatasetVersion(input) {
112672
- return this.requireRetrainDatasets().delete(input);
112673
- }
112674
- async verifyRetrainDatasetVersion(input) {
112675
- return this.requireRetrainDatasets().verify(input);
112676
- }
112677
112385
  requireRetrainLlmAssist() {
112678
112386
  const service = this.retrainLlmAssist;
112679
112387
  if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
@@ -112696,6 +112404,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
112696
112404
  async listRetrainCollections(_input) {
112697
112405
  return this.requireRetrainCollections().list();
112698
112406
  }
112407
+ /**
112408
+ * Live training selection → frame ids + stats (index reads only). Refuses an
112409
+ * unknown collection id or an empty match after the work-state rule.
112410
+ */
112411
+ async resolveRetrainTrainingCorpus(input) {
112412
+ const collections = await this.requireRetrainCollections().list();
112413
+ const known = new Set(collections.map((c) => c.id));
112414
+ for (const id of input.collectionIds) if (!known.has(id)) return {
112415
+ kind: "refused",
112416
+ reason: "unknown-collection",
112417
+ detail: `collection "${id}" does not exist`
112418
+ };
112419
+ const filter = exportFilterForTrainingCorpus(input);
112420
+ const plan = await this.buildRetrainExportPlan({
112421
+ filter,
112422
+ layout: "frames"
112423
+ });
112424
+ const frames = plan.manifest.frames.filter((frame) => keepTrainingCorpusFrame(frame, input.workState));
112425
+ if (frames.length === 0) return {
112426
+ kind: "refused",
112427
+ reason: "no-frames",
112428
+ detail: "no frames match the selected collections and work-state rule"
112429
+ };
112430
+ const manifest = {
112431
+ ...plan.manifest,
112432
+ frames
112433
+ };
112434
+ return {
112435
+ kind: "resolved",
112436
+ frameIds: frames.map((f) => f.frameId).toSorted(),
112437
+ stats: datasetStats(manifest),
112438
+ annotationHash: datasetAnnotationHash(frames),
112439
+ layout: "frames"
112440
+ };
112441
+ }
112699
112442
  async renameRetrainCollection(input) {
112700
112443
  return this.requireRetrainCollections().rename(input);
112701
112444
  }
@@ -113109,22 +112852,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113109
112852
  * zones simply has no `zoneCrop` geometry — absent, never a clamped guess.
113110
112853
  */
113111
112854
  async buildRetrainExportPlan(request) {
113112
- const versionId = request.filter.datasetVersionId;
113113
- if (versionId !== void 0) {
113114
- const version = await this.retrainDatasetVersions?.get(versionId) ?? null;
113115
- const { datasetVersionId: _resolved, ...rest } = await this.retrainDatasets?.exportFilterFor(request.filter) ?? {
113116
- ...request.filter,
113117
- frameIds: []
113118
- };
113119
- const plan = await this.buildRetrainExportPlan({
113120
- ...request,
113121
- filter: rest
113122
- });
113123
- return version === null ? plan : {
113124
- ...plan,
113125
- manifest: withDatasetVersion(plan.manifest, version)
113126
- };
113127
- }
113128
112855
  const frameStore = this.retrainFrames;
113129
112856
  const annotationStore = this.retrainAnnotations;
113130
112857
  const trackStore = this.trackStore;
@@ -113134,18 +112861,27 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
113134
112861
  const candidates = /* @__PURE__ */ new Map();
113135
112862
  const annotationsByFrame = /* @__PURE__ */ new Map();
113136
112863
  const zones = /* @__PURE__ */ new Map();
113137
- for (const deviceId of devices) {
113138
- zones.set(deviceId, await this.retrainZoneForDevice(deviceId));
113139
- for (const annotation of await annotationStore.listByDevice(deviceId, RETRAIN_EXPORT_MAX)) {
112864
+ for (const deviceId of devices) zones.set(deviceId, await this.retrainZoneForDevice(deviceId));
112865
+ if (exportNeedsFullCorpus(request.filter)) {
112866
+ const corpus = deviceIds.length > 0 ? (await frameStore.listAll()).filter((f) => deviceIds.includes(f.deviceId)) : await frameStore.listAll();
112867
+ for (const frame of corpus) candidates.set(frame.frameId, frame);
112868
+ for (const annotation of await annotationStore.listByFrames([...candidates.keys()])) {
113140
112869
  const existing = annotationsByFrame.get(annotation.mediaKey) ?? [];
113141
112870
  annotationsByFrame.set(annotation.mediaKey, [...existing, annotation]);
113142
112871
  }
113143
- for (const frame of await frameStore.listCompleteByDevice(deviceId, RETRAIN_EXPORT_MAX)) candidates.set(frame.frameId, frame);
113144
- }
113145
- for (const frameId of annotationsByFrame.keys()) {
113146
- if (candidates.has(frameId)) continue;
113147
- const frame = await frameStore.get(frameId);
113148
- if (frame !== null) candidates.set(frameId, frame);
112872
+ } else {
112873
+ for (const deviceId of devices) {
112874
+ for (const annotation of await annotationStore.listByDevice(deviceId, RETRAIN_EXPORT_MAX)) {
112875
+ const existing = annotationsByFrame.get(annotation.mediaKey) ?? [];
112876
+ annotationsByFrame.set(annotation.mediaKey, [...existing, annotation]);
112877
+ }
112878
+ for (const frame of await frameStore.listCompleteByDevice(deviceId, RETRAIN_EXPORT_MAX)) candidates.set(frame.frameId, frame);
112879
+ }
112880
+ for (const frameId of annotationsByFrame.keys()) {
112881
+ if (candidates.has(frameId)) continue;
112882
+ const frame = await frameStore.get(frameId);
112883
+ if (frame !== null) candidates.set(frameId, frame);
112884
+ }
113149
112885
  }
113150
112886
  const collectionsByFrame = await this.retrainCollectionStore?.collectionsOf([...candidates.keys()]) ?? /* @__PURE__ */ new Map();
113151
112887
  const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter, collectionsByFrame);