@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
@@ -1,4 +1,4 @@
1
- import { $ as NC_ALARM_DEFAULT_PROFILE_ID, $r as subKindsOf, $t as RECORDING_EXPORT_MAX_READ_BYTES, A as DetectionSourceSchema, Ai as boolean, An as applyBlockingEdit, At as NcConditionsSchema, B as FailureCounters, Bi as templateLiteral, Bn as consumableLowPercentOf, Br as resolveModeNotifyPolicy, Bt as NcSharedSequenceSchema, C as CoverStateSchema, Ci as isDeviceScopedCap, Cn as TrackRelationSchema, Cr as isVectorIndexDimConflict, Ct as NC_TEMPLATE_VARS, D as DETECTION_MACRO_CLASSES, Di as ZodObject, Dn as addonWidgetsSourceCapability, Dr as memoizePinnedFfmpeg, Dt as NcAlarmSensorsSchema, E as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ei as sleep$1, En as activateAlarmProfile, Er as mayWriteToLocation, Et as NcAlarmSensorsInputSchema, F as EVENT_KIND_BY_CAP, Fi as object, Fn as audioMetricsCapability, Fr as readTimelapseGeneratedAt, Ft as NcRulePatchSchema, G as MAX_ARCHIVED_DEBUG_NOTES, Gn as deriveDetailCropRect, Gr as ruleDeviceCompatCapQueries, Gt as NcSystemEventKindSchema, H as ListTrackRelationsInputSchema, Hi as union, Hn as cosineSimilarity$1, Hr as rootSupplyServes, Ht as NcSnoozeSchema, I as EVENT_OWNER_TYPES, Ii as partialRecord, In as audioModeOf, Ir as renameAlarmProfile, It as NcRuleSchema, J as MAX_TRACK_PURITIES, Jn as describeSystemEventDeliveryProblem, Jr as sceneMonitorCapability, Jt as NcTemplatePreviewSchema, K as MAX_BIRTH_DECISION_RECORDS, Kn as derivePostTriggeredAction, Kr as ruleGroupingApplies, Kt as NcTaxonomySchema, L as EXPORT_DENSE_MAX_RANGES, Li as record, Ln as buildEventKindDescriptor, Lr as resolveClipSearchMinScore, Lt as NcRuleTargetSchema, M as EMBEDDING_REQUIREMENTS_FAILED, Mi as literal, Mn as applyNonBlockingEdit, Mr as plateGalleryCapability, Mt as NcRuleActionSchema, N as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Ni as looseObject, Nn as applySecureOnArmEdit, Nr as readDeviceStateFrom, Nt as NcRuleActionsSchema, O as DETECTION_PIPELINE_CAP_NAME, Oi as _enum, On as alarmDeviceSecureOnArm, Or as notificationRulesCapability, Ot as NcAlarmSettingsPatchSchema, P as EVENT_DENSITY_BATCH_MAX, Pi as number, Pn as assertTimelapseCadences, Pr as readMotionTriggeredBy, Pt as NcRuleInputSchema, Q as MotionTriggeredBySchema, Qn as embeddingRefusalOf, Qr as storageOccupancyCapability, R as FIRST_LEVEL_MACRO_CLASSES, Ri as strictObject, Rn as buildViewerLink, Rr as resolveLocationMode, Rt as NcRuleUpdateInputSchema, S as ClearTrackRelationResultSchema, Si as hydrateSchema, Sn as TrackRelationKindSchema, Sr as isSystemEventSummaryRule, St as NC_TAXONOMY, T as DEFAULT_EVENT_COLOR, Tn as TrackSourceSchema, Tr as linkedStepOf, Tt as NcAlarmPostTriggeredActionSchema, U as ListTrackRelationsResultSchema, Ui as unknown, Un as createAlarmProfile, Ur as ruleAutoTriggersAlarm, Ut as NcSnoozeSuppressedSchema, V as LabelAttributionSchema, Vi as tuple, Vn as coreBlockIdFromAddonId, Vt as NcSnoozeInputSchema, W as LockStateSchema, Wi as EventCategory, Wn as deleteAlarmProfile, Wr as ruleBelongsToAlarm$1, Wt as NcStateSummarySectionIdSchema, X as MOTION_CLOSE_AFTER_MS, Xr as sensorRaisedOf, Xt as OPS_LOG_DEFAULT_LIMIT, Y as MAX_TRACK_RELATIONS, Yn as describeSystemEventFamilyProblem, Yr as secureActuationFor, Yt as NcTemplateVarDescriptorSchema, Z as MediaFileKindEnum, Zr as servedTemplateVars, Zt as OpsLogEntrySchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _i as BaseAddon, _n as TRACK_RELATIONS_DEFAULT_LIMIT, _r as isMediaOwnerType, _t as NC_GROUPING_MIN_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as systemEventSummarySplitOf, an as SCENE_DEFAULT_ANCHOR_THRESHOLD, ar as faceGalleryCapability, at as NC_ALARM_TRIGGERED_MAX_SEC, b as COCO_TO_MACRO, bi as customAction, bn as TimelapseRuleSchema, br as isScheduleActive, bt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, c as resolveClusterModelPin, ci as vectorDimFromBase64, cn as SCENE_DEFAULT_QUIET_SECONDS, cr as gifFromMp4Args, ct as NC_CLIP_POST_ROLL_DEFAULT_SEC, d as APPEARANCE_BLOCKS, di as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, dn as SCENE_RESET_RECAPTURES, dr as isAudioClassName, dt as NC_CONDITION_CATALOG, ei as syncActiveAlarmProfileSettings, en as RECORDING_SOURCE_CAMSTACK_ADDON, et as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fi as canonicalHash, fn as SceneMonitorSchema, fr as isDefaultAlarmProfileId, ft as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, g as ArchivedDebugNoteSchema, gi as errMsg, gn as TIMELAPSE_DENSE_FLOOR_SEC, gr as isKnownTemplateVar, gt as NC_GROUPING_MAX_SEC, h as AlarmArmModeSchema, hi as DeviceType, hn as SetTrackRelationInputSchema, hr as isEventOwnerType, ht as NC_GROUPING_DEFAULT_SEC, ii as systemEventSummaryScheduleOf, in as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ir as evictionPolicyOfLocation, it as NC_ALARM_SYSTEM_EVENT_KINDS, j as EMBEDDING_MODEL_FAILED, ji as discriminatedUnion, jn as applyModeDelaysEdits, jr as pipelineAnalyticsCapability, jt as NcLegacyRuleActionSequenceSchema, k as DeclaredDevices, ki as array, kn as alarmPanelCapability, kr as occupancyScope, kt as NcConditionDescriptorSchema, l as ALL_CAPABILITY_DEFINITIONS, li as viewerLinkBaseOf, ln as SCENE_DEFAULT_UNCOVERED_POLICY, lr as isAlarmControllableCap, lt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, m as AUDIO_MACRO_LABELS, mi as isSourceCap, mn as SetTrackPurityResultSchema, mr as isEventKindLabelKey, mt as NC_DEFAULT_SNOOZE_MINUTES, ni as systemEventFamilyProblems, nn as RetrainStatusSchema, nr as ensureAlarmProfiles, nt as NC_ALARM_DELAY_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, oi as templateCarriesSummary, on as SCENE_DEFAULT_CHECK_INTERVAL_SEC, or as failureContributionCapability, ot as NC_AUDIO_CONFIRM_HITS_DEFAULT, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pi as evaluateSensorEdge, pn as SetTrackPurityInputSchema, pr as isDetectionMacroClass, pt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, q as MAX_BIRTH_LATENCY_PROXY_MS, qn as deriveRecordingMode, qr as ruleKindOf, qt as NcTemplatePreviewInputSchema, ri as systemEventFilterApplies, rn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, rr as eventKindLabel, rt as NC_ALARM_NOTIFY_FIELDS, s as readClusterModelPinStrict, si as templateVarsFor, sn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, sr as gifDeriveSpeed, st as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as systemEventDeliveryProblems, tn as RebuildObjectEmbeddingsRefusalSchema, tr as encodeVectorBase64, tt as NC_ALARM_DELAY_FIELDS, u as APPEARANCE_BIN_COUNT, ui as zoneAnalyticsCapability, un as SCENE_DIVERGED, ur as isAppLinkBase, ut as NC_CLIP_RETENTION_MAX_SEC, v as BaseDevice, vi as CamProfileSchema, vn as TimelapseRuleInputSchema, vr as isRetiredClearKey, vt as NC_SENSOR_TRANSITION_KINDS, w as DEFAULT_DETAIL_CROP_CONVENTION, wi as nodePin, wn as TrackSideSchema, wr as kebabToCamel, wt as NcAlarmConfigSchema, x as ClearTrackRelationInputSchema, xi as defineCustomActions, xn as TrackPuritySchema, xr as isSystemDelivery, xt as NC_TANK_SYSTEM_EVENT_KINDS, y as BirthDecisionRecordSchema, yi as createEvent, yn as TimelapseRulePatchSchema, yr as isRootSuppliableStepId, yt as NC_SNOOZE_MAX_MINUTES, z as FULL_IMAGE_BBOX, zi as string, zn as cloneAlarmSettings, zr as resolveModeDelays, zt as NcScheduleSchema } from "../clip-model-registry-CC-BJYiK.mjs";
1
+ import { $ as NC_ALARM_DEFAULT_PROFILE_ID, $r as subKindsOf, $t as RECORDING_EXPORT_MAX_READ_BYTES, A as DetectionSourceSchema, Ai as boolean, An as applyBlockingEdit, At as NcConditionsSchema, B as FailureCounters, Bi as templateLiteral, Bn as consumableLowPercentOf, Br as resolveModeNotifyPolicy, Bt as NcSharedSequenceSchema, C as CoverStateSchema, Ci as isDeviceScopedCap, Cn as TrackRelationSchema, Cr as isVectorIndexDimConflict, Ct as NC_TEMPLATE_VARS, D as DETECTION_MACRO_CLASSES, Di as ZodObject, Dn as addonWidgetsSourceCapability, Dr as memoizePinnedFfmpeg, Dt as NcAlarmSensorsSchema, E as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ei as sleep$1, En as activateAlarmProfile, Er as mayWriteToLocation, Et as NcAlarmSensorsInputSchema, F as EVENT_KIND_BY_CAP, Fi as object, Fn as audioMetricsCapability, Fr as readTimelapseGeneratedAt, Ft as NcRulePatchSchema, G as MAX_ARCHIVED_DEBUG_NOTES, Gn as deriveDetailCropRect, Gr as ruleDeviceCompatCapQueries, Gt as NcSystemEventKindSchema, H as ListTrackRelationsInputSchema, Hi as union, Hn as cosineSimilarity$1, Hr as rootSupplyServes, Ht as NcSnoozeSchema, I as EVENT_OWNER_TYPES, Ii as partialRecord, In as audioModeOf, Ir as renameAlarmProfile, It as NcRuleSchema, J as MAX_TRACK_PURITIES, Jn as describeSystemEventDeliveryProblem, Jr as sceneMonitorCapability, Jt as NcTemplatePreviewSchema, K as MAX_BIRTH_DECISION_RECORDS, Kn as derivePostTriggeredAction, Kr as ruleGroupingApplies, Kt as NcTaxonomySchema, L as EXPORT_DENSE_MAX_RANGES, Li as record, Ln as buildEventKindDescriptor, Lr as resolveClipSearchMinScore, Lt as NcRuleTargetSchema, M as EMBEDDING_REQUIREMENTS_FAILED, Mi as literal, Mn as applyNonBlockingEdit, Mr as plateGalleryCapability, Mt as NcRuleActionSchema, N as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Ni as looseObject, Nn as applySecureOnArmEdit, Nr as readDeviceStateFrom, Nt as NcRuleActionsSchema, O as DETECTION_PIPELINE_CAP_NAME, Oi as _enum, On as alarmDeviceSecureOnArm, Or as notificationRulesCapability, Ot as NcAlarmSettingsPatchSchema, P as EVENT_DENSITY_BATCH_MAX, Pi as number, Pn as assertTimelapseCadences, Pr as readMotionTriggeredBy, Pt as NcRuleInputSchema, Q as MotionTriggeredBySchema, Qn as embeddingRefusalOf, Qr as storageOccupancyCapability, R as FIRST_LEVEL_MACRO_CLASSES, Ri as strictObject, Rn as buildViewerLink, Rr as resolveLocationMode, Rt as NcRuleUpdateInputSchema, S as ClearTrackRelationResultSchema, Si as hydrateSchema, Sn as TrackRelationKindSchema, Sr as isSystemEventSummaryRule, St as NC_TAXONOMY, T as DEFAULT_EVENT_COLOR, Tn as TrackSourceSchema, Tr as linkedStepOf, Tt as NcAlarmPostTriggeredActionSchema, U as ListTrackRelationsResultSchema, Ui as unknown, Un as createAlarmProfile, Ur as ruleAutoTriggersAlarm, Ut as NcSnoozeSuppressedSchema, V as LabelAttributionSchema, Vi as tuple, Vn as coreBlockIdFromAddonId, Vt as NcSnoozeInputSchema, W as LockStateSchema, Wi as EventCategory, Wn as deleteAlarmProfile, Wr as ruleBelongsToAlarm$1, Wt as NcStateSummarySectionIdSchema, X as MOTION_CLOSE_AFTER_MS, Xr as sensorRaisedOf, Xt as OPS_LOG_DEFAULT_LIMIT, Y as MAX_TRACK_RELATIONS, Yn as describeSystemEventFamilyProblem, Yr as secureActuationFor, Yt as NcTemplateVarDescriptorSchema, Z as MediaFileKindEnum, Zr as servedTemplateVars, Zt as OpsLogEntrySchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _i as BaseAddon, _n as TRACK_RELATIONS_DEFAULT_LIMIT, _r as isMediaOwnerType, _t as NC_GROUPING_MIN_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as systemEventSummarySplitOf, an as SCENE_DEFAULT_ANCHOR_THRESHOLD, ar as faceGalleryCapability, at as NC_ALARM_TRIGGERED_MAX_SEC, b as COCO_TO_MACRO, bi as customAction, bn as TimelapseRuleSchema, br as isScheduleActive, bt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, c as resolveClusterModelPin, ci as vectorDimFromBase64, cn as SCENE_DEFAULT_QUIET_SECONDS, cr as gifFromMp4Args, ct as NC_CLIP_POST_ROLL_DEFAULT_SEC, d as APPEARANCE_BLOCKS, di as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, dn as SCENE_RESET_RECAPTURES, dr as isAudioClassName, dt as NC_CONDITION_CATALOG, ei as syncActiveAlarmProfileSettings, en as RECORDING_SOURCE_CAMSTACK_ADDON, et as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fi as canonicalHash, fn as SceneMonitorSchema, fr as isDefaultAlarmProfileId, ft as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, g as ArchivedDebugNoteSchema, gi as errMsg, gn as TIMELAPSE_DENSE_FLOOR_SEC, gr as isKnownTemplateVar, gt as NC_GROUPING_MAX_SEC, h as AlarmArmModeSchema, hi as DeviceType, hn as SetTrackRelationInputSchema, hr as isEventOwnerType, ht as NC_GROUPING_DEFAULT_SEC, ii as systemEventSummaryScheduleOf, in as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ir as evictionPolicyOfLocation, it as NC_ALARM_SYSTEM_EVENT_KINDS, j as EMBEDDING_MODEL_FAILED, ji as discriminatedUnion, jn as applyModeDelaysEdits, jr as pipelineAnalyticsCapability, jt as NcLegacyRuleActionSequenceSchema, k as DeclaredDevices, ki as array, kn as alarmPanelCapability, kr as occupancyScope, kt as NcConditionDescriptorSchema, l as ALL_CAPABILITY_DEFINITIONS, li as viewerLinkBaseOf, ln as SCENE_DEFAULT_UNCOVERED_POLICY, lr as isAlarmControllableCap, lt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, m as AUDIO_MACRO_LABELS, mi as isSourceCap, mn as SetTrackPurityResultSchema, mr as isEventKindLabelKey, mt as NC_DEFAULT_SNOOZE_MINUTES, ni as systemEventFamilyProblems, nn as RetrainStatusSchema, nr as ensureAlarmProfiles, nt as NC_ALARM_DELAY_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, oi as templateCarriesSummary, on as SCENE_DEFAULT_CHECK_INTERVAL_SEC, or as failureContributionCapability, ot as NC_AUDIO_CONFIRM_HITS_DEFAULT, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pi as evaluateSensorEdge, pn as SetTrackPurityInputSchema, pr as isDetectionMacroClass, pt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, q as MAX_BIRTH_LATENCY_PROXY_MS, qn as deriveRecordingMode, qr as ruleKindOf, qt as NcTemplatePreviewInputSchema, ri as systemEventFilterApplies, rn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, rr as eventKindLabel, rt as NC_ALARM_NOTIFY_FIELDS, s as readClusterModelPinStrict, si as templateVarsFor, sn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, sr as gifDeriveSpeed, st as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as systemEventDeliveryProblems, tn as RebuildObjectEmbeddingsRefusalSchema, tr as encodeVectorBase64, tt as NC_ALARM_DELAY_FIELDS, u as APPEARANCE_BIN_COUNT, ui as zoneAnalyticsCapability, un as SCENE_DIVERGED, ur as isAppLinkBase, ut as NC_CLIP_RETENTION_MAX_SEC, v as BaseDevice, vi as CamProfileSchema, vn as TimelapseRuleInputSchema, vr as isRetiredClearKey, vt as NC_SENSOR_TRANSITION_KINDS, w as DEFAULT_DETAIL_CROP_CONVENTION, wi as nodePin, wn as TrackSideSchema, wr as kebabToCamel, wt as NcAlarmConfigSchema, x as ClearTrackRelationInputSchema, xi as defineCustomActions, xn as TrackPuritySchema, xr as isSystemDelivery, xt as NC_TANK_SYSTEM_EVENT_KINDS, y as BirthDecisionRecordSchema, yi as createEvent, yn as TimelapseRulePatchSchema, yr as isRootSuppliableStepId, yt as NC_SNOOZE_MAX_MINUTES, z as FULL_IMAGE_BBOX, zi as string, zn as cloneAlarmSettings, zr as resolveModeDelays, zt as NcScheduleSchema } from "../clip-model-registry-VI28etdt.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -12914,7 +12914,7 @@ function stringField(data, key) {
12914
12914
  const value = data[key];
12915
12915
  return typeof value === "string" && value.length > 0 ? value : void 0;
12916
12916
  }
12917
- function stringList$1(data, key) {
12917
+ function stringList(data, key) {
12918
12918
  const raw = data[key];
12919
12919
  if (!Array.isArray(raw)) return [];
12920
12920
  const ids = [];
@@ -13731,7 +13731,7 @@ function incomingFromExportCompleted(facts, data) {
13731
13731
  const deviceId = numberField(data, "deviceId");
13732
13732
  const title = stringField(data, "title");
13733
13733
  const bytes = numberField(data, "fileBytes");
13734
- const notifyTargetIds = stringList$1(data, "notifyTargetIds");
13734
+ const notifyTargetIds = stringList(data, "notifyTargetIds");
13735
13735
  const incoming = systemSubject({
13736
13736
  facts,
13737
13737
  kind: "export-completed",
@@ -23008,7 +23008,7 @@ function withSummaryBody(rule, section) {
23008
23008
  //#region src/notification-center/system-event-tap-url.ts
23009
23009
  /** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
23010
23010
  var ADMIN_ADDONS_PATH = "/system/addons";
23011
- var ADMIN_CLUSTER_PATH = "/system/cluster";
23011
+ var ADMIN_NODES_PATH = "/system/nodes";
23012
23012
  var ADMIN_DEVICES_PATH = "/devices";
23013
23013
  /**
23014
23014
  * The query parameter naming a node.
@@ -23082,9 +23082,9 @@ var CAMERA_BY_CONSTRUCTION_KINDS = new Set([
23082
23082
  * Cluster page, and the summary row is about all of them at once.
23083
23083
  */
23084
23084
  var SUMMARY_SECTION_PATHS = {
23085
- nodes: ADMIN_CLUSTER_PATH,
23085
+ nodes: ADMIN_NODES_PATH,
23086
23086
  "addon-updates": ADMIN_ADDONS_PATH,
23087
- updates: ADMIN_CLUSTER_PATH
23087
+ updates: ADMIN_NODES_PATH
23088
23088
  };
23089
23089
  function withParam(path, key, value) {
23090
23090
  if (value === void 0 || value.length === 0) return path;
@@ -23101,7 +23101,7 @@ function systemEventTapPath(subject) {
23101
23101
  const sectionPath = subject.summarySection !== void 0 ? SUMMARY_SECTION_PATHS[subject.summarySection] : void 0;
23102
23102
  if (sectionPath !== void 0) return sectionPath;
23103
23103
  if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
23104
- if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
23104
+ if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_NODES_PATH, NODE_PARAM, subject.nodeId);
23105
23105
  if (DEVICE_STREAM_KINDS.has(subject.kind)) {
23106
23106
  const deviceId = subject.deviceId;
23107
23107
  if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
@@ -47559,7 +47559,7 @@ function buildTitleSvg(input) {
47559
47559
  const top = Math.round((height - lineHeight * lines.length) / 2);
47560
47560
  const shadow = Math.max(1, Math.round(fontPx * TITLE_SHADOW_FRACTION));
47561
47561
  const cx = Math.round(width / 2);
47562
- const font = `font-size="${fontPx}" font-family="sans-serif" font-weight="bold" text-anchor="middle"`;
47562
+ const font = `font-size="${fontPx}" font-family="DejaVu Sans,Helvetica,Arial,sans-serif" font-weight="bold" text-anchor="middle"`;
47563
47563
  return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
47564
47564
  <rect x="0" y="0" width="${width}" height="${height}" fill="black" fill-opacity="${TITLE_SCRIM_OPACITY}"/>
47565
47565
  ${lines.map((line, i) => {
@@ -71003,6 +71003,15 @@ var RetrainFrameStore = class {
71003
71003
  return [];
71004
71004
  }
71005
71005
  }
71006
+ /** Every corpus copy — index rows only, no blob. Newest-first is the caller's. */
71007
+ async listAll() {
71008
+ try {
71009
+ return (await this.store.query.query({ collection: RETRAIN_FRAMES_COLLECTION })).filter((r) => typeof r.id === "string").map((r) => this.rowToFrame(r.id, r.data));
71010
+ } catch (err) {
71011
+ this.logger.warn("RetrainFrameStore.listAll failed", { meta: { error: String(err) } });
71012
+ return [];
71013
+ }
71014
+ }
71006
71015
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
71007
71016
  async get(frameId) {
71008
71017
  try {
@@ -75059,7 +75068,7 @@ function describeTapeStatus(input) {
75059
75068
  * `viewer-settings-legacy-adoption.ts`.
75060
75069
  */
75061
75070
  var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
75062
- var COLUMNS$1 = [
75071
+ var COLUMNS = [
75063
75072
  {
75064
75073
  name: "id",
75065
75074
  type: "TEXT",
@@ -75129,7 +75138,7 @@ function requireCaller(caller) {
75129
75138
  if (!caller) throw new Error("viewerSettings: authenticated caller required");
75130
75139
  return caller;
75131
75140
  }
75132
- function asRecord$1(data) {
75141
+ function asRecord(data) {
75133
75142
  return typeof data === "object" && data !== null ? data : {};
75134
75143
  }
75135
75144
  function toSummary(id, data, callerUserId) {
@@ -75155,7 +75164,7 @@ var ViewerSettingsSnapshotStore = class {
75155
75164
  static async declare(store) {
75156
75165
  await store.declareCollection.mutate({
75157
75166
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75158
- columns: [...COLUMNS$1],
75167
+ columns: [...COLUMNS],
75159
75168
  indexes: [{
75160
75169
  name: "idx_pa_viewer_settings_owner",
75161
75170
  columns: ["ownerUserId"]
@@ -75175,7 +75184,7 @@ var ViewerSettingsSnapshotStore = class {
75175
75184
  });
75176
75185
  const out = [];
75177
75186
  for (const row of rows) {
75178
- const summary = toSummary(row.id, asRecord$1(row.data), caller.userId);
75187
+ const summary = toSummary(row.id, asRecord(row.data), caller.userId);
75179
75188
  if (summary) out.push(summary);
75180
75189
  }
75181
75190
  return out;
@@ -75189,7 +75198,7 @@ var ViewerSettingsSnapshotStore = class {
75189
75198
  }
75190
75199
  }))[0];
75191
75200
  if (!row) return null;
75192
- const data = asRecord$1(row.data);
75201
+ const data = asRecord(row.data);
75193
75202
  const summary = toSummary(row.id, data, caller.userId);
75194
75203
  if (!summary) return null;
75195
75204
  const blob = BlobSchema.safeParse(data.blob);
@@ -75212,7 +75221,7 @@ var ViewerSettingsSnapshotStore = class {
75212
75221
  }
75213
75222
  }))[0];
75214
75223
  const id = prior?.id ?? randomUUID();
75215
- const createdAt = typeof asRecord$1(prior?.data).createdAt === "number" ? asRecord$1(prior?.data).createdAt : now;
75224
+ const createdAt = typeof asRecord(prior?.data).createdAt === "number" ? asRecord(prior?.data).createdAt : now;
75216
75225
  await this.store.set.mutate({
75217
75226
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
75218
75227
  key: id,
@@ -83418,850 +83427,6 @@ var RetrainCollectionService = class {
83418
83427
  }
83419
83428
  };
83420
83429
  //#endregion
83421
- //#region src/pipeline-analytics/retrain/retrain-dataset.ts
83422
- /**
83423
- * Dataset versions — the counts a version is judged by, and the hash that
83424
- * freezes what it teaches. Pure.
83425
- *
83426
- * Both read the export MANIFEST (`retrain-export.ts`), not the stores: a
83427
- * version is exactly what an export of its frames would carry, so its counts
83428
- * and its hash cannot disagree with the archive a trainer receives.
83429
- */
83430
- function lightingOf(frame) {
83431
- if (frame.lighting === null) return "unknown";
83432
- return frame.lighting.night ? "night" : "day";
83433
- }
83434
- /** The counts of a dataset, from the manifest an export of it would carry. */
83435
- function datasetStats(manifest) {
83436
- const cells = /* @__PURE__ */ new Map();
83437
- const cameras = /* @__PURE__ */ new Map();
83438
- let completeFrames = 0;
83439
- let subjects = 0;
83440
- let modelErrors = 0;
83441
- let hardNegatives = 0;
83442
- for (const frame of manifest.frames) {
83443
- const camera = cameras.get(frame.deviceId) ?? {
83444
- frames: 0,
83445
- completeFrames: 0
83446
- };
83447
- cameras.set(frame.deviceId, {
83448
- frames: camera.frames + 1,
83449
- completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
83450
- });
83451
- if (frame.complete) completeFrames += 1;
83452
- const lighting = lightingOf(frame);
83453
- let frameSubjects = 0;
83454
- for (const a of frame.annotations) {
83455
- if (a.kind === "model_error") {
83456
- modelErrors += 1;
83457
- continue;
83458
- }
83459
- frameSubjects += 1;
83460
- const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
83461
- const entry = cells.get(key) ?? {
83462
- cell: {
83463
- macroClass: a.macroClass,
83464
- deviceId: frame.deviceId,
83465
- lighting,
83466
- subjects: 0,
83467
- frames: 0
83468
- },
83469
- frameIds: /* @__PURE__ */ new Set()
83470
- };
83471
- entry.frameIds.add(frame.frameId);
83472
- cells.set(key, {
83473
- cell: {
83474
- ...entry.cell,
83475
- subjects: entry.cell.subjects + 1,
83476
- frames: entry.frameIds.size
83477
- },
83478
- frameIds: entry.frameIds
83479
- });
83480
- }
83481
- subjects += frameSubjects;
83482
- if (frame.complete && frameSubjects === 0) hardNegatives += 1;
83483
- }
83484
- const frames = manifest.frames.length;
83485
- return {
83486
- frames,
83487
- completeFrames,
83488
- completeness: frames > 0 ? completeFrames / frames : 0,
83489
- subjects,
83490
- modelErrors,
83491
- hardNegatives,
83492
- cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
83493
- cameras: [...cameras.entries()].map(([deviceId, c]) => ({
83494
- deviceId,
83495
- ...c
83496
- })).toSorted((a, b) => a.deviceId - b.deviceId)
83497
- };
83498
- }
83499
- /** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
83500
- var q = (n) => Math.round(n * 1e4) / 1e4;
83501
- /**
83502
- * The sha256 of what the frames TEACH: per frame its completeness and, per
83503
- * box, the class tiers, the kind and the box. Row ids, timestamps, export
83504
- * stamps and provenance are left out — they change on a re-save that teaches
83505
- * the same thing. Order-independent.
83506
- */
83507
- function datasetAnnotationHash(frames) {
83508
- const canonical = frames.map((f) => ({
83509
- f: f.frameId,
83510
- c: f.complete,
83511
- a: f.annotations.map((a) => [
83512
- a.macroClass,
83513
- a.label ?? "",
83514
- a.subLabel ?? "",
83515
- a.kind,
83516
- q(a.bbox.x),
83517
- q(a.bbox.y),
83518
- q(a.bbox.w),
83519
- q(a.bbox.h)
83520
- ].join("|")).toSorted()
83521
- })).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
83522
- return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
83523
- }
83524
- /**
83525
- * The manifest of a version's export, saying which version and whether its
83526
- * frames still teach what was frozen. Frames of the version missing from the
83527
- * archive count as empty and incomplete, so their absence changes the hash.
83528
- */
83529
- function withDatasetVersion(manifest, version) {
83530
- const present = new Map(manifest.frames.map((f) => [f.frameId, f]));
83531
- const annotationHashNow = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
83532
- frameId,
83533
- complete: false,
83534
- annotations: []
83535
- }));
83536
- return {
83537
- ...manifest,
83538
- datasetVersion: {
83539
- id: version.id,
83540
- name: version.name,
83541
- createdAt: version.createdAt,
83542
- annotationHash: version.annotationHash,
83543
- annotationHashNow,
83544
- matches: annotationHashNow === version.annotationHash,
83545
- missingFrames: version.frameIds.filter((id) => !present.has(id)).length
83546
- }
83547
- };
83548
- }
83549
- //#endregion
83550
- //#region src/pipeline-analytics/retrain/retrain-dataset-version-store.ts
83551
- /**
83552
- * @durable class=ledger owner=pipeline-analytics
83553
- * write="one row per dataset version the operator creates: a frozen frameIds list,
83554
- * the sha256 of the annotations those frames carried, the export parameters and
83555
- * the counts at creation. Never updated after the insert — a different selection
83556
- * is a new version."
83557
- * retention="none — rows leave only when the operator deletes the version. The
83558
- * frames themselves are owned by the retrain corpus; deleting a version deletes no
83559
- * frame, and deleting a frame does not edit a version (verify reports it missing)."
83560
- */
83561
- var RETRAIN_DATASET_VERSIONS_COLLECTION = "pipeline-analytics:retrain-dataset-versions";
83562
- var COLUMNS = [
83563
- {
83564
- name: "id",
83565
- type: "TEXT",
83566
- primaryKey: true,
83567
- notNull: true
83568
- },
83569
- {
83570
- name: "name",
83571
- type: "TEXT",
83572
- notNull: true
83573
- },
83574
- {
83575
- name: "nameKey",
83576
- type: "TEXT",
83577
- notNull: true
83578
- },
83579
- {
83580
- name: "createdAt",
83581
- type: "INTEGER",
83582
- notNull: true
83583
- },
83584
- {
83585
- name: "selection",
83586
- type: "JSON",
83587
- notNull: true
83588
- },
83589
- {
83590
- name: "layout",
83591
- type: "TEXT",
83592
- notNull: true
83593
- },
83594
- {
83595
- name: "completeOnly",
83596
- type: "BOOLEAN",
83597
- notNull: true
83598
- },
83599
- {
83600
- name: "frameIds",
83601
- type: "JSON",
83602
- notNull: true
83603
- },
83604
- {
83605
- name: "annotationHash",
83606
- type: "TEXT",
83607
- notNull: true
83608
- },
83609
- {
83610
- name: "manifestSchema",
83611
- type: "TEXT",
83612
- notNull: true
83613
- },
83614
- {
83615
- name: "stats",
83616
- type: "JSON",
83617
- notNull: true
83618
- }
83619
- ];
83620
- var INDEXES = [{
83621
- name: "idx_retrain_dataset_versions_name_key",
83622
- columns: ["nameKey"],
83623
- unique: true
83624
- }, {
83625
- name: "idx_retrain_dataset_versions_created",
83626
- columns: ["createdAt"]
83627
- }];
83628
- function summaryOfVersion(version) {
83629
- const { frameIds, ...rest } = version;
83630
- return {
83631
- ...rest,
83632
- frameCount: frameIds.length
83633
- };
83634
- }
83635
- var nameKeyOf = (name) => name.trim().toLowerCase();
83636
- function numberList(value) {
83637
- return Array.isArray(value) ? value.filter((v) => typeof v === "number") : void 0;
83638
- }
83639
- function stringList(value) {
83640
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : void 0;
83641
- }
83642
- function parseJson(value) {
83643
- if (typeof value !== "string") return value;
83644
- try {
83645
- return JSON.parse(value);
83646
- } catch {
83647
- return;
83648
- }
83649
- }
83650
- function asRecord(value) {
83651
- return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
83652
- }
83653
- var EMPTY_STATS = {
83654
- frames: 0,
83655
- completeFrames: 0,
83656
- completeness: 0,
83657
- subjects: 0,
83658
- modelErrors: 0,
83659
- hardNegatives: 0,
83660
- cells: [],
83661
- cameras: []
83662
- };
83663
- /** A stored stats object, trusted only as far as its shape. */
83664
- function readStats(value) {
83665
- const raw = asRecord(parseJson(value));
83666
- const num = (k) => typeof raw[k] === "number" ? raw[k] : 0;
83667
- const cells = Array.isArray(raw["cells"]) ? raw["cells"] : [];
83668
- const cameras = Array.isArray(raw["cameras"]) ? raw["cameras"] : [];
83669
- return {
83670
- ...EMPTY_STATS,
83671
- frames: num("frames"),
83672
- completeFrames: num("completeFrames"),
83673
- completeness: num("completeness"),
83674
- subjects: num("subjects"),
83675
- modelErrors: num("modelErrors"),
83676
- hardNegatives: num("hardNegatives"),
83677
- cells: cells.map((c) => {
83678
- const r = asRecord(c);
83679
- const lighting = r["lighting"] === "day" || r["lighting"] === "night" ? r["lighting"] : "unknown";
83680
- return {
83681
- macroClass: String(r["macroClass"] ?? ""),
83682
- deviceId: Number(r["deviceId"] ?? 0),
83683
- lighting,
83684
- subjects: Number(r["subjects"] ?? 0),
83685
- frames: Number(r["frames"] ?? 0)
83686
- };
83687
- }),
83688
- cameras: cameras.map((c) => {
83689
- const r = asRecord(c);
83690
- return {
83691
- deviceId: Number(r["deviceId"] ?? 0),
83692
- frames: Number(r["frames"] ?? 0),
83693
- completeFrames: Number(r["completeFrames"] ?? 0)
83694
- };
83695
- })
83696
- };
83697
- }
83698
- var RetrainDatasetVersionStore = class {
83699
- store;
83700
- logger;
83701
- constructor(deps) {
83702
- this.store = deps.store;
83703
- this.logger = deps.logger;
83704
- }
83705
- static async declare(store) {
83706
- await store.declareCollection.mutate({
83707
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83708
- columns: [...COLUMNS],
83709
- indexes: [...INDEXES]
83710
- });
83711
- }
83712
- insert = async (version) => {
83713
- await this.store.set.mutate({
83714
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83715
- key: version.id,
83716
- value: {
83717
- name: version.name,
83718
- nameKey: nameKeyOf(version.name),
83719
- createdAt: version.createdAt,
83720
- selection: version.selection,
83721
- layout: version.layout,
83722
- completeOnly: version.completeOnly,
83723
- frameIds: version.frameIds,
83724
- annotationHash: version.annotationHash,
83725
- manifestSchema: version.manifestSchema,
83726
- stats: version.stats
83727
- }
83728
- });
83729
- };
83730
- get = async (id) => {
83731
- try {
83732
- const row = await this.store.get.query({
83733
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83734
- key: id
83735
- });
83736
- return row === null || row === void 0 ? null : this.toVersion(id, asRecord(row));
83737
- } catch (err) {
83738
- this.logger.warn("retrain dataset version read failed", { meta: {
83739
- versionId: id,
83740
- error: String(err)
83741
- } });
83742
- return null;
83743
- }
83744
- };
83745
- list = async () => {
83746
- return (await this.listRows()).map(summaryOfVersion);
83747
- };
83748
- /** Every row WITH its frame list — the migration into `training` reads this. */
83749
- listRows = async () => {
83750
- try {
83751
- return (await this.store.query.query({
83752
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83753
- filter: { orderBy: {
83754
- field: "createdAt",
83755
- direction: "desc"
83756
- } }
83757
- })).map((r) => this.toVersion(r.id, asRecord(r.data)));
83758
- } catch (err) {
83759
- this.logger.warn("retrain dataset versions list failed", { meta: { error: String(err) } });
83760
- return [];
83761
- }
83762
- };
83763
- findIdByName = async (name) => {
83764
- return (await this.store.query.query({
83765
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83766
- filter: {
83767
- where: { nameKey: nameKeyOf(name) },
83768
- limit: 1
83769
- }
83770
- }))[0]?.id ?? null;
83771
- };
83772
- remove = async (id) => {
83773
- if (await this.get(id) === null) return false;
83774
- await this.store.delete.mutate({
83775
- collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
83776
- key: id
83777
- });
83778
- return true;
83779
- };
83780
- toVersion(id, data) {
83781
- const selection = asRecord(parseJson(data["selection"]));
83782
- const deviceIds = numberList(selection["deviceIds"]);
83783
- const collectionIds = stringList(selection["collectionIds"]);
83784
- const frameIdsSel = stringList(selection["frameIds"]);
83785
- return {
83786
- id,
83787
- name: String(data["name"] ?? ""),
83788
- createdAt: Number(data["createdAt"] ?? 0),
83789
- selection: {
83790
- ...deviceIds !== void 0 ? { deviceIds } : {},
83791
- ...collectionIds !== void 0 ? { collectionIds } : {},
83792
- ...frameIdsSel !== void 0 ? { frameIds: frameIdsSel } : {}
83793
- },
83794
- layout: data["layout"] === "crops" ? "crops" : "frames",
83795
- completeOnly: data["completeOnly"] === true || data["completeOnly"] === 1,
83796
- frameIds: stringList(parseJson(data["frameIds"])) ?? [],
83797
- annotationHash: String(data["annotationHash"] ?? ""),
83798
- manifestSchema: String(data["manifestSchema"] ?? ""),
83799
- stats: readStats(data["stats"])
83800
- };
83801
- }
83802
- };
83803
- //#endregion
83804
- //#region src/pipeline-analytics/retrain/retrain-geometry.ts
83805
- function clamp01(v) {
83806
- return v < 0 ? 0 : v > 1 ? 1 : v;
83807
- }
83808
- /** Is this a usable rectangle at all? A zero-area box is not a subject. */
83809
- function isUsableRect(r) {
83810
- return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
83811
- }
83812
- /**
83813
- * The subject inside a square LETTERBOX of the full frame.
83814
- *
83815
- * The frame is scaled to fit the square and centred; the padding bars take the
83816
- * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
83817
- * height, and every y coordinate shifts by half the leftover.
83818
- *
83819
- * Square-side agnostic on purpose: the answer is normalised against the square,
83820
- * so it is the same number for a 640 and a 1280 input and no caller has to know
83821
- * which the model uses.
83822
- */
83823
- function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
83824
- if (!isUsableRect(bbox)) return null;
83825
- if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
83826
- const longSide = Math.max(frameWidth, frameHeight);
83827
- const scaleX = frameWidth / longSide;
83828
- const scaleY = frameHeight / longSide;
83829
- const offsetX = (1 - scaleX) / 2;
83830
- const offsetY = (1 - scaleY) / 2;
83831
- return {
83832
- bbox: {
83833
- x: offsetX + bbox.x * scaleX,
83834
- y: offsetY + bbox.y * scaleY,
83835
- w: bbox.w * scaleX,
83836
- h: bbox.h * scaleY
83837
- },
83838
- visibleFraction: 1
83839
- };
83840
- }
83841
- /**
83842
- * The subject inside a ZONE CROP — the shape a package model is trained on.
83843
- *
83844
- * Returns `null` when the subject does not intersect the zone at all. That is
83845
- * the whole reason the package proposal runs on the crop rather than the frame:
83846
- * a package detector at 0.35 on a full frame proposes furniture, and a subject
83847
- * outside the zone is not part of this model's world.
83848
- */
83849
- function deriveZoneCropRect(bbox, zone) {
83850
- if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
83851
- const left = Math.max(bbox.x, zone.x);
83852
- const top = Math.max(bbox.y, zone.y);
83853
- const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
83854
- const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
83855
- if (!(right > left) || !(bottom > top)) return null;
83856
- const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
83857
- return {
83858
- bbox: {
83859
- x: (left - zone.x) / zone.w,
83860
- y: (top - zone.y) / zone.h,
83861
- w: (right - left) / zone.w,
83862
- h: (bottom - top) / zone.h
83863
- },
83864
- visibleFraction
83865
- };
83866
- }
83867
- /**
83868
- * The subject inside its OWN padded crop — the shape a classifier is trained on.
83869
- *
83870
- * The crop is the box grown by `padding` of its own size on each side and then
83871
- * clipped to the frame, so the subject sits in the middle and the classifier
83872
- * still sees a little context. The returned rectangle is where the subject
83873
- * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
83874
- * clipped by a frame edge — a subject at the left border is flush left in its
83875
- * crop, and a caller that assumed the centre would draw the box in the wrong
83876
- * place.
83877
- */
83878
- function deriveSubjectCropRect(bbox, padding) {
83879
- if (!isUsableRect(bbox)) return null;
83880
- const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
83881
- const cropX = clamp01(bbox.x - bbox.w * pad);
83882
- const cropY = clamp01(bbox.y - bbox.h * pad);
83883
- const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
83884
- const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
83885
- const cropW = cropRight - cropX;
83886
- const cropH = cropBottom - cropY;
83887
- if (!(cropW > 0) || !(cropH > 0)) return null;
83888
- return {
83889
- bbox: {
83890
- x: (bbox.x - cropX) / cropW,
83891
- y: (bbox.y - cropY) / cropH,
83892
- w: bbox.w / cropW,
83893
- h: bbox.h / cropH
83894
- },
83895
- visibleFraction: 1
83896
- };
83897
- }
83898
- //#endregion
83899
- //#region src/pipeline-analytics/retrain/retrain-lighting.ts
83900
- function isNight(condition) {
83901
- return condition === "night" || condition === "ir";
83902
- }
83903
- function retrainFrameLighting(input) {
83904
- if (input.capturedAt === void 0) {
83905
- const measured = resolveConditionDetailed({
83906
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
83907
- now: /* @__PURE__ */ new Date(0)
83908
- });
83909
- return measured.source === "ir-chroma" ? {
83910
- condition: measured.condition,
83911
- night: true,
83912
- source: measured.source
83913
- } : null;
83914
- }
83915
- const resolved = resolveConditionDetailed({
83916
- ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
83917
- ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
83918
- now: new Date(input.capturedAt)
83919
- });
83920
- return {
83921
- ...resolved,
83922
- night: isNight(resolved.condition)
83923
- };
83924
- }
83925
- //#endregion
83926
- //#region src/pipeline-analytics/retrain/retrain-export.ts
83927
- /**
83928
- * The ANNOTATED dataset export — the retrain page's own archive.
83929
- *
83930
- * Sibling of `training-export/`, deliberately not the same thing. That one
83931
- * streams a marked track's stored media verbatim and knows nothing about what
83932
- * is in it; this one streams the frames the operator SELECTED (already copied
83933
- * into retrain storage) plus the ground truth they drew.
83934
- *
83935
- * ## The one interesting decision
83936
- *
83937
- * `annotations.json` carries, for every subject, the canonical full-frame box
83938
- * AND the geometry derived for each model shape. **Derived here, at export,
83939
- * never stored** — one box in, three shapes out. Storing the derived shapes is
83940
- * how two crops of the same subject end up in one feature space, which is the
83941
- * failure already recorded for CLIP (D52). A consumer that wants a different
83942
- * input size or a different padding re-derives from the canonical box; nothing
83943
- * in the database has to change and nothing can disagree.
83944
- *
83945
- * Everything else follows the export next door: index reads to plan, one JPEG
83946
- * in memory at a time, a yield between files, and NO inference — a training
83947
- * set's pixels are the pixels the pipeline saw.
83948
- */
83949
- var RETRAIN_EXPORT_PREFIX = "retrain-export";
83950
- /**
83951
- * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
83952
- * padding, read from the one convention `deriveDetailCropRect` cuts with
83953
- * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
83954
- * here while the runtime fed `0.15`, so a classifier trained from this export
83955
- * learned on more context than it is ever given live.
83956
- */
83957
- var CLASSIFIER_CROP_PADDING = DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
83958
- /** The convention a `crops` layout cuts with — the runtime default, recorded
83959
- * in the manifest so a consumer never has to assume it. */
83960
- var RETRAIN_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
83961
- /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
83962
- * the trainer resizes per model, so this only bounds the upload. */
83963
- var RETRAIN_CROP_MAX_SIDE = 256;
83964
- /**
83965
- * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
83966
- * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
83967
- * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
83968
- */
83969
- var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
83970
- /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
83971
- * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
83972
- function retrainFramePath(frame) {
83973
- const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
83974
- const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
83975
- return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
83976
- }
83977
- /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
83978
- function retrainCropPath(macroClass, annotationId) {
83979
- return `crops/${macroClass}/${annotationId}.jpg`;
83980
- }
83981
- /**
83982
- * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
83983
- * filter keeps everything. Order is the input's.
83984
- */
83985
- function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
83986
- const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
83987
- const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
83988
- const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
83989
- return frames.filter((frame) => {
83990
- if (wanted !== null && !wanted.has(frame.frameId)) return false;
83991
- if (devices !== null && !devices.has(frame.deviceId)) return false;
83992
- if (filter.completeOnly === true && !frame.complete) return false;
83993
- if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
83994
- if (filter.batchId !== void 0) {
83995
- if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
83996
- }
83997
- return true;
83998
- });
83999
- }
84000
- /**
84001
- * Derive every per-model shape for one annotation.
84002
- *
84003
- * `zone` is optional and per-frame: only a package annotation has one, and a
84004
- * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
84005
- * clamped — a clamped box teaches the model that the subject ends where the
84006
- * crop ends.
84007
- */
84008
- function deriveAll(annotation, frame, zone) {
84009
- const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
84010
- const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
84011
- const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
84012
- return {
84013
- ...letterbox !== null ? { letterbox } : {},
84014
- ...zoneCrop !== null ? { zoneCrop } : {},
84015
- ...subjectCrop !== null ? { subjectCrop } : {}
84016
- };
84017
- }
84018
- /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
84019
- function buildRetrainManifest(input) {
84020
- const layout = input.layout ?? "frames";
84021
- const coordinates = input.coordinates ?? null;
84022
- let annotationCount = 0;
84023
- let modelErrorCount = 0;
84024
- const frames = input.frames.map((frame) => {
84025
- const zone = input.zoneForDevice(frame.deviceId);
84026
- const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
84027
- annotationCount += 1;
84028
- if (a.kind === "model_error") modelErrorCount += 1;
84029
- const derived = deriveAll(a, frame, zone);
84030
- return {
84031
- id: a.id,
84032
- macroClass: a.macroClass,
84033
- ...a.label !== void 0 ? { label: a.label } : {},
84034
- ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
84035
- kind: a.kind,
84036
- source: a.source,
84037
- ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
84038
- ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
84039
- bbox: a.bbox,
84040
- ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
84041
- derived
84042
- };
84043
- });
84044
- return {
84045
- frameId: frame.frameId,
84046
- path: retrainFramePath(frame),
84047
- deviceId: frame.deviceId,
84048
- origin: frame.origin,
84049
- ...frame.origin === "track" ? { trackId: frame.trackId } : {},
84050
- ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
84051
- collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
84052
- width: frame.width,
84053
- height: frame.height,
84054
- sizeBytes: frame.sizeBytes,
84055
- sourceKind: frame.sourceKind,
84056
- copiedAt: frame.copiedAt,
84057
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84058
- complete: frame.complete,
84059
- lighting: retrainFrameLighting({
84060
- ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84061
- ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
84062
- coordinates
84063
- }),
84064
- ...zone !== null ? { zoneUsed: zone } : {},
84065
- annotations
84066
- };
84067
- });
84068
- return {
84069
- schema: RETRAIN_MANIFEST_SCHEMA,
84070
- layout,
84071
- detailCropConvention: RETRAIN_CROP_CONVENTION,
84072
- ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
84073
- generatedAt: input.generatedAt,
84074
- batchId: input.batchId,
84075
- frameCount: frames.length,
84076
- annotationCount,
84077
- modelErrorCount,
84078
- classifierCropPadding: CLASSIFIER_CROP_PADDING,
84079
- frames
84080
- };
84081
- }
84082
- //#endregion
84083
- //#region src/pipeline-analytics/retrain/retrain-dataset-service.ts
84084
- function filterOf(selection, completeOnly) {
84085
- return {
84086
- ...selection.deviceIds !== void 0 && selection.deviceIds.length > 0 ? { deviceIds: [...selection.deviceIds] } : {},
84087
- ...selection.collectionIds !== void 0 && selection.collectionIds.length > 0 ? { collectionIds: [...selection.collectionIds] } : {},
84088
- ...selection.frameIds !== void 0 && selection.frameIds.length > 0 ? { frameIds: [...selection.frameIds] } : {},
84089
- ...completeOnly ? { completeOnly: true } : {}
84090
- };
84091
- }
84092
- var RetrainDatasetService = class {
84093
- deps;
84094
- constructor(deps) {
84095
- this.deps = deps;
84096
- }
84097
- /** The counts a selection would freeze, without freezing anything. */
84098
- async preview(input) {
84099
- return datasetStats((await this.deps.buildPlan({
84100
- filter: filterOf(input.selection, input.completeOnly),
84101
- layout: "frames"
84102
- })).manifest);
84103
- }
84104
- async create(input) {
84105
- const name = input.name.trim();
84106
- if (name === "") return {
84107
- kind: "refused",
84108
- reason: "empty-name"
84109
- };
84110
- if (await this.deps.versions.findIdByName(name) !== null) return {
84111
- kind: "refused",
84112
- reason: "duplicate-name"
84113
- };
84114
- const plan = await this.deps.buildPlan({
84115
- filter: filterOf(input.selection, input.completeOnly),
84116
- layout: input.layout
84117
- });
84118
- if (plan.manifest.frames.length === 0) return {
84119
- kind: "refused",
84120
- reason: "no-frames"
84121
- };
84122
- const version = {
84123
- id: this.deps.newId(),
84124
- name,
84125
- createdAt: this.deps.now(),
84126
- selection: input.selection,
84127
- layout: input.layout,
84128
- completeOnly: input.completeOnly,
84129
- frameIds: plan.manifest.frames.map((f) => f.frameId).toSorted(),
84130
- annotationHash: datasetAnnotationHash(plan.manifest.frames),
84131
- manifestSchema: RETRAIN_MANIFEST_SCHEMA,
84132
- stats: datasetStats(plan.manifest)
84133
- };
84134
- await this.deps.versions.insert(version);
84135
- this.deps.logger.info("retrain dataset version created", { meta: {
84136
- versionId: version.id,
84137
- frames: version.frameIds.length,
84138
- subjects: version.stats.subjects,
84139
- completeness: version.stats.completeness,
84140
- layout: version.layout
84141
- } });
84142
- return {
84143
- kind: "created",
84144
- version: summaryOfVersion(version)
84145
- };
84146
- }
84147
- async list() {
84148
- return this.deps.versions.list();
84149
- }
84150
- async delete(input) {
84151
- const deleted = await this.deps.versions.remove(input.id);
84152
- if (deleted) this.deps.logger.info("retrain dataset version deleted", { meta: { versionId: input.id } });
84153
- return { deleted };
84154
- }
84155
- /** Does the corpus still teach what the version froze? */
84156
- async verify(input) {
84157
- const version = await this.deps.versions.get(input.id);
84158
- if (version === null) return {
84159
- found: false,
84160
- annotationHash: "",
84161
- matches: false,
84162
- missingFrames: 0
84163
- };
84164
- const plan = await this.deps.buildPlan({
84165
- filter: { frameIds: [...version.frameIds] },
84166
- layout: "frames"
84167
- });
84168
- const present = new Map(plan.manifest.frames.map((f) => [f.frameId, f]));
84169
- const annotationHash = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
84170
- frameId,
84171
- complete: false,
84172
- annotations: []
84173
- }));
84174
- return {
84175
- found: true,
84176
- annotationHash,
84177
- matches: annotationHash === version.annotationHash,
84178
- missingFrames: version.frameIds.filter((id) => !present.has(id)).length
84179
- };
84180
- }
84181
- /**
84182
- * An export filter with `datasetVersionId` resolved to that version's
84183
- * frames (and its completeOnly). An unknown version exports NOTHING — never
84184
- * the whole dataset, which is what dropping the field would do.
84185
- */
84186
- async exportFilterFor(filter) {
84187
- if (filter.datasetVersionId === void 0) return filter;
84188
- const { datasetVersionId, ...rest } = filter;
84189
- const version = await this.deps.versions.get(datasetVersionId);
84190
- if (version === null) {
84191
- this.deps.logger.warn("retrain export asked for an unknown dataset version", { meta: { versionId: datasetVersionId } });
84192
- return {
84193
- ...rest,
84194
- frameIds: []
84195
- };
84196
- }
84197
- const wanted = rest.frameIds !== void 0 ? version.frameIds.filter((id) => rest.frameIds?.includes(id) === true) : version.frameIds;
84198
- return {
84199
- ...rest,
84200
- frameIds: [...wanted],
84201
- ...version.completeOnly ? { completeOnly: true } : {}
84202
- };
84203
- }
84204
- };
84205
- //#endregion
84206
- //#region src/pipeline-analytics/retrain/retrain-dataset-version-routing.ts
84207
- var RoutingDatasetVersionRepository = class {
84208
- deps;
84209
- lastPresence = false;
84210
- migrated = false;
84211
- migration = null;
84212
- constructor(deps) {
84213
- this.deps = deps;
84214
- }
84215
- insert = async (version) => {
84216
- if (await this.route() === "local") {
84217
- await this.deps.local.insert(version);
84218
- return;
84219
- }
84220
- await this.deps.training.put(version);
84221
- };
84222
- get = async (id) => await this.route() === "local" ? this.deps.local.get(id) : this.deps.training.get(id);
84223
- list = async () => await this.route() === "local" ? this.deps.local.list() : this.deps.training.list();
84224
- findIdByName = async (name) => await this.route() === "local" ? this.deps.local.findIdByName(name) : this.deps.training.findIdByName(name);
84225
- remove = async (id) => await this.route() === "local" ? this.deps.local.remove(id) : this.deps.training.remove(id);
84226
- async route() {
84227
- if (!await this.presence()) return "local";
84228
- if (this.migrated) return "training";
84229
- return await this.migrateOnce() ? "training" : "local";
84230
- }
84231
- async presence() {
84232
- let answer;
84233
- try {
84234
- answer = await this.deps.trainingPresent();
84235
- } catch {
84236
- answer = null;
84237
- }
84238
- if (answer !== null) this.lastPresence = answer;
84239
- return this.lastPresence;
84240
- }
84241
- migrateOnce() {
84242
- if (this.migration === null) this.migration = this.migrate().finally(() => {
84243
- this.migration = null;
84244
- });
84245
- return this.migration;
84246
- }
84247
- async migrate() {
84248
- const { local, training, logger } = this.deps;
84249
- try {
84250
- const rows = await local.listRows();
84251
- for (const row of rows) {
84252
- await training.put(row);
84253
- await local.remove(row.id);
84254
- }
84255
- this.migrated = true;
84256
- if (rows.length > 0) logger.info("retrain dataset versions moved to the training capability", { meta: { versions: rows.length } });
84257
- return true;
84258
- } catch (err) {
84259
- logger.warn("retrain dataset versions could not move to training yet — kept local", { meta: { error: err instanceof Error ? err.message : String(err) } });
84260
- return false;
84261
- }
84262
- }
84263
- };
84264
- //#endregion
84265
83430
  //#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
84266
83431
  var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
84267
83432
  var ALWAYS_CLOUD_KINDS = new Set([
@@ -85030,7 +84195,526 @@ async function cutRetrainCrop(input) {
85030
84195
  return pipeline.jpeg({ quality: 92 }).toBuffer();
85031
84196
  }
85032
84197
  //#endregion
84198
+ //#region src/pipeline-analytics/retrain/retrain-dataset.ts
84199
+ /**
84200
+ * Dataset stats and annotation hash over an export manifest. Pure.
84201
+ *
84202
+ * Both read the export MANIFEST (`retrain-export.ts`), not the stores: the
84203
+ * counts a plan is judged by cannot disagree with the archive a trainer
84204
+ * receives.
84205
+ */
84206
+ function lightingOf(frame) {
84207
+ if (frame.lighting === null) return "unknown";
84208
+ return frame.lighting.night ? "night" : "day";
84209
+ }
84210
+ /** The counts of a dataset, from the manifest an export of it would carry. */
84211
+ function datasetStats(manifest) {
84212
+ const cells = /* @__PURE__ */ new Map();
84213
+ const cameras = /* @__PURE__ */ new Map();
84214
+ let completeFrames = 0;
84215
+ let subjects = 0;
84216
+ let modelErrors = 0;
84217
+ let hardNegatives = 0;
84218
+ for (const frame of manifest.frames) {
84219
+ const camera = cameras.get(frame.deviceId) ?? {
84220
+ frames: 0,
84221
+ completeFrames: 0
84222
+ };
84223
+ cameras.set(frame.deviceId, {
84224
+ frames: camera.frames + 1,
84225
+ completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
84226
+ });
84227
+ if (frame.complete) completeFrames += 1;
84228
+ const lighting = lightingOf(frame);
84229
+ let frameSubjects = 0;
84230
+ for (const a of frame.annotations) {
84231
+ if (a.kind === "model_error") {
84232
+ modelErrors += 1;
84233
+ continue;
84234
+ }
84235
+ frameSubjects += 1;
84236
+ const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
84237
+ const entry = cells.get(key) ?? {
84238
+ cell: {
84239
+ macroClass: a.macroClass,
84240
+ deviceId: frame.deviceId,
84241
+ lighting,
84242
+ subjects: 0,
84243
+ frames: 0
84244
+ },
84245
+ frameIds: /* @__PURE__ */ new Set()
84246
+ };
84247
+ entry.frameIds.add(frame.frameId);
84248
+ cells.set(key, {
84249
+ cell: {
84250
+ ...entry.cell,
84251
+ subjects: entry.cell.subjects + 1,
84252
+ frames: entry.frameIds.size
84253
+ },
84254
+ frameIds: entry.frameIds
84255
+ });
84256
+ }
84257
+ subjects += frameSubjects;
84258
+ if (frame.complete && frameSubjects === 0) hardNegatives += 1;
84259
+ }
84260
+ const frames = manifest.frames.length;
84261
+ return {
84262
+ frames,
84263
+ completeFrames,
84264
+ completeness: frames > 0 ? completeFrames / frames : 0,
84265
+ subjects,
84266
+ modelErrors,
84267
+ hardNegatives,
84268
+ cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
84269
+ cameras: [...cameras.entries()].map(([deviceId, c]) => ({
84270
+ deviceId,
84271
+ ...c
84272
+ })).toSorted((a, b) => a.deviceId - b.deviceId)
84273
+ };
84274
+ }
84275
+ /** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
84276
+ var q = (n) => Math.round(n * 1e4) / 1e4;
84277
+ /**
84278
+ * The sha256 of what the frames TEACH: per frame its completeness and, per
84279
+ * box, the class tiers, the kind and the box. Row ids, timestamps, export
84280
+ * stamps and provenance are left out — they change on a re-save that teaches
84281
+ * the same thing. Order-independent.
84282
+ */
84283
+ function datasetAnnotationHash(frames) {
84284
+ const canonical = frames.map((f) => ({
84285
+ f: f.frameId,
84286
+ c: f.complete,
84287
+ a: f.annotations.map((a) => [
84288
+ a.macroClass,
84289
+ a.label ?? "",
84290
+ a.subLabel ?? "",
84291
+ a.kind,
84292
+ q(a.bbox.x),
84293
+ q(a.bbox.y),
84294
+ q(a.bbox.w),
84295
+ q(a.bbox.h)
84296
+ ].join("|")).toSorted()
84297
+ })).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
84298
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
84299
+ }
84300
+ //#endregion
84301
+ //#region src/pipeline-analytics/retrain/retrain-corpus-query.ts
84302
+ /** Map a frame row and its saved annotation count to the exclusive work state. */
84303
+ function workStateOf(frame, annotationCount) {
84304
+ if (annotationCount <= 0) return "unannotated";
84305
+ if (frame.complete) return "complete";
84306
+ return "annotated";
84307
+ }
84308
+ /** `complete(camera) / totalCorpus`, or `null` when the corpus is empty. */
84309
+ function corpusPctOfCorpus(completeOnCamera, totalCorpus) {
84310
+ if (totalCorpus === 0) return null;
84311
+ return completeOnCamera / totalCorpus;
84312
+ }
84313
+ /** Every narrowing field is ANDed; omitted fields impose no constraint. */
84314
+ function filterCorpusRows(rows, filter) {
84315
+ const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84316
+ const classes = filter.classes !== void 0 && filter.classes.length > 0 ? new Set(filter.classes) : null;
84317
+ return rows.filter((row) => {
84318
+ if (devices !== null && !devices.has(row.deviceId)) return false;
84319
+ if (filter.collectionId !== void 0 && !row.collectionIds.includes(filter.collectionId)) return false;
84320
+ if (classes !== null && !row.classes.some((c) => classes.has(c))) return false;
84321
+ if (filter.workState !== void 0 && workStateOf(row, row.annotationCount) !== filter.workState) return false;
84322
+ return true;
84323
+ });
84324
+ }
84325
+ /**
84326
+ * Newest copies first (`copiedAt` desc). Stable on equal timestamps by frameId.
84327
+ */
84328
+ function sortCorpusRowsNewestFirst(rows) {
84329
+ return [...rows].toSorted((a, b) => {
84330
+ if (b.copiedAt !== a.copiedAt) return b.copiedAt - a.copiedAt;
84331
+ return a.frameId < b.frameId ? -1 : a.frameId > b.frameId ? 1 : 0;
84332
+ });
84333
+ }
84334
+ /** 1-based page slice. An empty match is `{ rows: [], total: 0, … }`. */
84335
+ function paginateCorpusRows(rows, page, pageSize) {
84336
+ const safePage = page < 1 ? 1 : page;
84337
+ const safeSize = pageSize < 1 ? 1 : pageSize;
84338
+ const start = (safePage - 1) * safeSize;
84339
+ return {
84340
+ rows: rows.slice(start, start + safeSize),
84341
+ total: rows.length,
84342
+ page: safePage,
84343
+ pageSize: safeSize
84344
+ };
84345
+ }
84346
+ /** Global + per-camera KPI; classification always via {@link workStateOf}. */
84347
+ function aggregateCorpusKpi(rows) {
84348
+ let unannotated = 0;
84349
+ let annotated = 0;
84350
+ let complete = 0;
84351
+ const perCamera = /* @__PURE__ */ new Map();
84352
+ for (const row of rows) {
84353
+ const state = workStateOf(row, row.annotationCount);
84354
+ const cam = perCamera.get(row.deviceId) ?? {
84355
+ unannotated: 0,
84356
+ annotated: 0,
84357
+ complete: 0
84358
+ };
84359
+ if (state === "unannotated") {
84360
+ unannotated += 1;
84361
+ cam.unannotated += 1;
84362
+ } else if (state === "annotated") {
84363
+ annotated += 1;
84364
+ cam.annotated += 1;
84365
+ } else {
84366
+ complete += 1;
84367
+ cam.complete += 1;
84368
+ }
84369
+ perCamera.set(row.deviceId, cam);
84370
+ }
84371
+ const total = rows.length;
84372
+ const cameras = [...perCamera.entries()].toSorted(([a], [b]) => a - b).map(([deviceId, counts]) => ({
84373
+ deviceId,
84374
+ unannotated: counts.unannotated,
84375
+ annotated: counts.annotated,
84376
+ complete: counts.complete,
84377
+ pctOfCorpus: corpusPctOfCorpus(counts.complete, total)
84378
+ }));
84379
+ return {
84380
+ total,
84381
+ unannotated,
84382
+ annotated,
84383
+ complete,
84384
+ cameras
84385
+ };
84386
+ }
84387
+ //#endregion
84388
+ //#region src/pipeline-analytics/retrain/retrain-geometry.ts
84389
+ function clamp01(v) {
84390
+ return v < 0 ? 0 : v > 1 ? 1 : v;
84391
+ }
84392
+ /** Is this a usable rectangle at all? A zero-area box is not a subject. */
84393
+ function isUsableRect(r) {
84394
+ return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
84395
+ }
84396
+ /**
84397
+ * The subject inside a square LETTERBOX of the full frame.
84398
+ *
84399
+ * The frame is scaled to fit the square and centred; the padding bars take the
84400
+ * rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
84401
+ * height, and every y coordinate shifts by half the leftover.
84402
+ *
84403
+ * Square-side agnostic on purpose: the answer is normalised against the square,
84404
+ * so it is the same number for a 640 and a 1280 input and no caller has to know
84405
+ * which the model uses.
84406
+ */
84407
+ function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
84408
+ if (!isUsableRect(bbox)) return null;
84409
+ if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
84410
+ const longSide = Math.max(frameWidth, frameHeight);
84411
+ const scaleX = frameWidth / longSide;
84412
+ const scaleY = frameHeight / longSide;
84413
+ const offsetX = (1 - scaleX) / 2;
84414
+ const offsetY = (1 - scaleY) / 2;
84415
+ return {
84416
+ bbox: {
84417
+ x: offsetX + bbox.x * scaleX,
84418
+ y: offsetY + bbox.y * scaleY,
84419
+ w: bbox.w * scaleX,
84420
+ h: bbox.h * scaleY
84421
+ },
84422
+ visibleFraction: 1
84423
+ };
84424
+ }
84425
+ /**
84426
+ * The subject inside a ZONE CROP — the shape a package model is trained on.
84427
+ *
84428
+ * Returns `null` when the subject does not intersect the zone at all. That is
84429
+ * the whole reason the package proposal runs on the crop rather than the frame:
84430
+ * a package detector at 0.35 on a full frame proposes furniture, and a subject
84431
+ * outside the zone is not part of this model's world.
84432
+ */
84433
+ function deriveZoneCropRect(bbox, zone) {
84434
+ if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
84435
+ const left = Math.max(bbox.x, zone.x);
84436
+ const top = Math.max(bbox.y, zone.y);
84437
+ const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
84438
+ const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
84439
+ if (!(right > left) || !(bottom > top)) return null;
84440
+ const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
84441
+ return {
84442
+ bbox: {
84443
+ x: (left - zone.x) / zone.w,
84444
+ y: (top - zone.y) / zone.h,
84445
+ w: (right - left) / zone.w,
84446
+ h: (bottom - top) / zone.h
84447
+ },
84448
+ visibleFraction
84449
+ };
84450
+ }
84451
+ /**
84452
+ * The subject inside its OWN padded crop — the shape a classifier is trained on.
84453
+ *
84454
+ * The crop is the box grown by `padding` of its own size on each side and then
84455
+ * clipped to the frame, so the subject sits in the middle and the classifier
84456
+ * still sees a little context. The returned rectangle is where the subject
84457
+ * lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
84458
+ * clipped by a frame edge — a subject at the left border is flush left in its
84459
+ * crop, and a caller that assumed the centre would draw the box in the wrong
84460
+ * place.
84461
+ */
84462
+ function deriveSubjectCropRect(bbox, padding) {
84463
+ if (!isUsableRect(bbox)) return null;
84464
+ const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
84465
+ const cropX = clamp01(bbox.x - bbox.w * pad);
84466
+ const cropY = clamp01(bbox.y - bbox.h * pad);
84467
+ const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
84468
+ const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
84469
+ const cropW = cropRight - cropX;
84470
+ const cropH = cropBottom - cropY;
84471
+ if (!(cropW > 0) || !(cropH > 0)) return null;
84472
+ return {
84473
+ bbox: {
84474
+ x: (bbox.x - cropX) / cropW,
84475
+ y: (bbox.y - cropY) / cropH,
84476
+ w: bbox.w / cropW,
84477
+ h: bbox.h / cropH
84478
+ },
84479
+ visibleFraction: 1
84480
+ };
84481
+ }
84482
+ //#endregion
84483
+ //#region src/pipeline-analytics/retrain/retrain-lighting.ts
84484
+ function isNight(condition) {
84485
+ return condition === "night" || condition === "ir";
84486
+ }
84487
+ function retrainFrameLighting(input) {
84488
+ if (input.capturedAt === void 0) {
84489
+ const measured = resolveConditionDetailed({
84490
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84491
+ now: /* @__PURE__ */ new Date(0)
84492
+ });
84493
+ return measured.source === "ir-chroma" ? {
84494
+ condition: measured.condition,
84495
+ night: true,
84496
+ source: measured.source
84497
+ } : null;
84498
+ }
84499
+ const resolved = resolveConditionDetailed({
84500
+ ...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
84501
+ ...input.coordinates !== null ? { coordinates: input.coordinates } : {},
84502
+ now: new Date(input.capturedAt)
84503
+ });
84504
+ return {
84505
+ ...resolved,
84506
+ night: isNight(resolved.condition)
84507
+ };
84508
+ }
84509
+ //#endregion
84510
+ //#region src/pipeline-analytics/retrain/retrain-export.ts
84511
+ /**
84512
+ * The ANNOTATED dataset export — the retrain page's own archive.
84513
+ *
84514
+ * Sibling of `training-export/`, deliberately not the same thing. That one
84515
+ * streams a marked track's stored media verbatim and knows nothing about what
84516
+ * is in it; this one streams the frames the operator SELECTED (already copied
84517
+ * into retrain storage) plus the ground truth they drew.
84518
+ *
84519
+ * ## The one interesting decision
84520
+ *
84521
+ * `annotations.json` carries, for every subject, the canonical full-frame box
84522
+ * AND the geometry derived for each model shape. **Derived here, at export,
84523
+ * never stored** — one box in, three shapes out. Storing the derived shapes is
84524
+ * how two crops of the same subject end up in one feature space, which is the
84525
+ * failure already recorded for CLIP (D52). A consumer that wants a different
84526
+ * input size or a different padding re-derives from the canonical box; nothing
84527
+ * in the database has to change and nothing can disagree.
84528
+ *
84529
+ * Everything else follows the export next door: index reads to plan, one JPEG
84530
+ * in memory at a time, a yield between files, and NO inference — a training
84531
+ * set's pixels are the pixels the pipeline saw.
84532
+ */
84533
+ var RETRAIN_EXPORT_PREFIX = "retrain-export";
84534
+ /**
84535
+ * Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
84536
+ * padding, read from the one convention `deriveDetailCropRect` cuts with
84537
+ * (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
84538
+ * here while the runtime fed `0.15`, so a classifier trained from this export
84539
+ * learned on more context than it is ever given live.
84540
+ */
84541
+ var CLASSIFIER_CROP_PADDING = DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
84542
+ /** The convention a `crops` layout cuts with — the runtime default, recorded
84543
+ * in the manifest so a consumer never has to assume it. */
84544
+ var RETRAIN_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
84545
+ /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
84546
+ * the trainer resizes per model, so this only bounds the upload. */
84547
+ var RETRAIN_CROP_MAX_SIDE = 256;
84548
+ /**
84549
+ * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
84550
+ * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
84551
+ * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
84552
+ */
84553
+ var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
84554
+ /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
84555
+ * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
84556
+ function retrainFramePath(frame) {
84557
+ const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
84558
+ const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
84559
+ return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
84560
+ }
84561
+ /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
84562
+ function retrainCropPath(macroClass, annotationId) {
84563
+ return `crops/${macroClass}/${annotationId}.jpg`;
84564
+ }
84565
+ /**
84566
+ * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
84567
+ * filter keeps everything. Order is the input's.
84568
+ */
84569
+ function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
84570
+ const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
84571
+ const collectionIds = /* @__PURE__ */ new Set();
84572
+ if (filter.collectionIds !== void 0) for (const id of filter.collectionIds) collectionIds.add(id);
84573
+ if (filter.collectionId !== void 0) collectionIds.add(filter.collectionId);
84574
+ const collections = collectionIds.size > 0 ? collectionIds : null;
84575
+ const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
84576
+ const classes = filter.classes !== void 0 && filter.classes.length > 0 ? new Set(filter.classes) : null;
84577
+ return frames.filter((frame) => {
84578
+ if (wanted !== null && !wanted.has(frame.frameId)) return false;
84579
+ if (devices !== null && !devices.has(frame.deviceId)) return false;
84580
+ if (filter.completeOnly === true && !frame.complete) return false;
84581
+ if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
84582
+ const annotations = annotationsByFrame.get(frame.frameId) ?? [];
84583
+ if (filter.batchId !== void 0) {
84584
+ if (!annotations.some((a) => a.exportedInBatch === filter.batchId)) return false;
84585
+ }
84586
+ if (classes !== null && !annotations.some((a) => classes.has(a.macroClass))) return false;
84587
+ if (filter.workState !== void 0 && workStateOf(frame, annotations.length) !== filter.workState) return false;
84588
+ return true;
84589
+ });
84590
+ }
84591
+ /** Whether the export filter needs every corpus copy (not only annotated/complete). */
84592
+ function exportNeedsFullCorpus(filter) {
84593
+ if (filter.workState !== void 0) return true;
84594
+ if (filter.collectionId !== void 0) return true;
84595
+ if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) return true;
84596
+ if (filter.classes !== void 0 && filter.classes.length > 0) return true;
84597
+ return false;
84598
+ }
84599
+ /**
84600
+ * Derive every per-model shape for one annotation.
84601
+ *
84602
+ * `zone` is optional and per-frame: only a package annotation has one, and a
84603
+ * `zoneCrop` for a subject the zone does not contain is ABSENT rather than
84604
+ * clamped — a clamped box teaches the model that the subject ends where the
84605
+ * crop ends.
84606
+ */
84607
+ function deriveAll(annotation, frame, zone) {
84608
+ const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
84609
+ const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
84610
+ const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
84611
+ return {
84612
+ ...letterbox !== null ? { letterbox } : {},
84613
+ ...zoneCrop !== null ? { zoneCrop } : {},
84614
+ ...subjectCrop !== null ? { subjectCrop } : {}
84615
+ };
84616
+ }
84617
+ /** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
84618
+ function buildRetrainManifest(input) {
84619
+ const layout = input.layout ?? "frames";
84620
+ const coordinates = input.coordinates ?? null;
84621
+ let annotationCount = 0;
84622
+ let modelErrorCount = 0;
84623
+ const frames = input.frames.map((frame) => {
84624
+ const zone = input.zoneForDevice(frame.deviceId);
84625
+ const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
84626
+ annotationCount += 1;
84627
+ if (a.kind === "model_error") modelErrorCount += 1;
84628
+ const derived = deriveAll(a, frame, zone);
84629
+ return {
84630
+ id: a.id,
84631
+ macroClass: a.macroClass,
84632
+ ...a.label !== void 0 ? { label: a.label } : {},
84633
+ ...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
84634
+ kind: a.kind,
84635
+ source: a.source,
84636
+ ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
84637
+ ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
84638
+ bbox: a.bbox,
84639
+ ...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
84640
+ derived
84641
+ };
84642
+ });
84643
+ return {
84644
+ frameId: frame.frameId,
84645
+ path: retrainFramePath(frame),
84646
+ deviceId: frame.deviceId,
84647
+ origin: frame.origin,
84648
+ ...frame.origin === "track" ? { trackId: frame.trackId } : {},
84649
+ ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
84650
+ collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
84651
+ width: frame.width,
84652
+ height: frame.height,
84653
+ sizeBytes: frame.sizeBytes,
84654
+ sourceKind: frame.sourceKind,
84655
+ copiedAt: frame.copiedAt,
84656
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84657
+ complete: frame.complete,
84658
+ lighting: retrainFrameLighting({
84659
+ ...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
84660
+ ...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
84661
+ coordinates
84662
+ }),
84663
+ ...zone !== null ? { zoneUsed: zone } : {},
84664
+ annotations
84665
+ };
84666
+ });
84667
+ return {
84668
+ schema: RETRAIN_MANIFEST_SCHEMA,
84669
+ layout,
84670
+ detailCropConvention: RETRAIN_CROP_CONVENTION,
84671
+ ...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
84672
+ generatedAt: input.generatedAt,
84673
+ batchId: input.batchId,
84674
+ frameCount: frames.length,
84675
+ annotationCount,
84676
+ modelErrorCount,
84677
+ classifierCropPadding: CLASSIFIER_CROP_PADDING,
84678
+ frames
84679
+ };
84680
+ }
84681
+ //#endregion
84682
+ //#region src/pipeline-analytics/retrain/retrain-training-corpus.ts
84683
+ /**
84684
+ * Export filter that loads the membership set for a selection.
84685
+ * `complete` uses `workState` so the plan is already narrowed; the other
84686
+ * modes load every copy in the collections and keep via {@link keepTrainingCorpusFrame}.
84687
+ */
84688
+ function exportFilterForTrainingCorpus(selection) {
84689
+ const collectionIds = [...selection.collectionIds];
84690
+ if (selection.workState === "complete") return {
84691
+ collectionIds,
84692
+ workState: "complete"
84693
+ };
84694
+ return { collectionIds };
84695
+ }
84696
+ /** Whether a manifest frame belongs under the selection's work-state rule. */
84697
+ function keepTrainingCorpusFrame(frame, workState) {
84698
+ const state = workStateOf(frame, frame.annotations.length);
84699
+ switch (workState) {
84700
+ case "complete": return state === "complete";
84701
+ case "annotated_or_complete": return state === "annotated" || state === "complete";
84702
+ case "any_annotated": return frame.annotations.length > 0 || frame.complete;
84703
+ }
84704
+ }
84705
+ //#endregion
85033
84706
  //#region src/pipeline-analytics/retrain/retrain-export-handler.ts
84707
+ function parseExportWorkState(raw) {
84708
+ if (raw === "unannotated" || raw === "annotated" || raw === "complete") return raw;
84709
+ }
84710
+ function parseExportClasses(raw) {
84711
+ const out = [];
84712
+ for (const part of raw.split(",")) {
84713
+ const trimmed = part.trim();
84714
+ if (isRetrainMacroClass(trimmed) && !out.includes(trimmed)) out.push(trimmed);
84715
+ }
84716
+ return out;
84717
+ }
85034
84718
  /** The export request a URL carries (`getRetrainExportUrl` composes it). */
85035
84719
  function parseRetrainExportRequest(url) {
85036
84720
  const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
@@ -85038,14 +84722,18 @@ function parseRetrainExportRequest(url) {
85038
84722
  const deviceIds = parseRetrainDeviceIds(url);
85039
84723
  const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
85040
84724
  const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
85041
- const datasetVersionId = params.get("datasetVersionId")?.trim() ?? "";
84725
+ const collectionId = params.get("collectionId")?.trim() ?? "";
84726
+ const classes = parseExportClasses(params.get("classes") ?? "");
84727
+ const workState = parseExportWorkState(params.get("workState")?.trim() ?? "");
85042
84728
  const batchId = params.get("batchId")?.trim() ?? "";
85043
84729
  return {
85044
84730
  filter: {
85045
84731
  ...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
85046
84732
  ...frameIds.length > 0 ? { frameIds } : {},
85047
84733
  ...collectionIds.length > 0 ? { collectionIds } : {},
85048
- ...datasetVersionId.length > 0 ? { datasetVersionId } : {},
84734
+ ...collectionId.length > 0 ? { collectionId } : {},
84735
+ ...classes.length > 0 ? { classes: [...classes] } : {},
84736
+ ...workState !== void 0 ? { workState } : {},
85049
84737
  ...batchId.length > 0 ? { batchId } : {},
85050
84738
  ...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
85051
84739
  },
@@ -85059,7 +84747,9 @@ function retrainExportQuery(request) {
85059
84747
  if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
85060
84748
  if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
85061
84749
  if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
85062
- if (filter.datasetVersionId !== void 0) params.set("datasetVersionId", filter.datasetVersionId);
84750
+ if (filter.collectionId !== void 0) params.set("collectionId", filter.collectionId);
84751
+ if (filter.workState !== void 0) params.set("workState", filter.workState);
84752
+ if (filter.classes !== void 0 && filter.classes.length > 0) params.set("classes", filter.classes.join(","));
85063
84753
  if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
85064
84754
  if (filter.completeOnly === true) params.set("completeOnly", "1");
85065
84755
  if (request.layout === "crops") params.set("layout", "crops");
@@ -86069,6 +85759,74 @@ var RetrainService = class {
86069
85759
  });
86070
85760
  return saved.map(toWireAnnotation);
86071
85761
  }
85762
+ /**
85763
+ * Paginated corpus list for the Frames grid. Index reads only — copies,
85764
+ * annotation counts/classes, collection membership; no JPEG.
85765
+ */
85766
+ async listCorpusPage(filter) {
85767
+ const page = paginateCorpusRows(sortCorpusRowsNewestFirst(filterCorpusRows(await this.loadCorpusRows(), filter)), filter.page, filter.pageSize);
85768
+ if (page.rows.length === 0) return {
85769
+ frames: [],
85770
+ total: page.total,
85771
+ page: page.page,
85772
+ pageSize: page.pageSize
85773
+ };
85774
+ const byId = new Map((await this.deps.frames.getMany(page.rows.map((r) => r.frameId))).map((f) => [f.frameId, f]));
85775
+ const copies = page.rows.flatMap((r) => {
85776
+ const frame = byId.get(r.frameId);
85777
+ return frame !== void 0 ? [frame] : [];
85778
+ });
85779
+ const wire = await this.toWireFrames(copies);
85780
+ const wireById = new Map(wire.map((f) => [f.frameId, f]));
85781
+ return {
85782
+ frames: page.rows.flatMap((r) => {
85783
+ const frame = wireById.get(r.frameId);
85784
+ if (frame === void 0) return [];
85785
+ return [{
85786
+ ...frame,
85787
+ workState: workStateOf(r, r.annotationCount)
85788
+ }];
85789
+ }),
85790
+ total: page.total,
85791
+ page: page.page,
85792
+ pageSize: page.pageSize
85793
+ };
85794
+ }
85795
+ /** Unfiltered corpus KPI — globals + per-camera rows via {@link workStateOf}. */
85796
+ async getCorpusKpi() {
85797
+ return aggregateCorpusKpi(await this.loadCorpusRows());
85798
+ }
85799
+ /** Index facts for every corpus copy. */
85800
+ async loadCorpusRows() {
85801
+ const frames = await this.deps.frames.listAll();
85802
+ if (frames.length === 0) return [];
85803
+ const frameIds = frames.map((f) => f.frameId);
85804
+ const annotations = await this.deps.annotations.listByFrames(frameIds);
85805
+ const countByFrame = /* @__PURE__ */ new Map();
85806
+ const classesByFrame = /* @__PURE__ */ new Map();
85807
+ for (const a of annotations) {
85808
+ countByFrame.set(a.mediaKey, (countByFrame.get(a.mediaKey) ?? 0) + 1);
85809
+ const classes = classesByFrame.get(a.mediaKey) ?? /* @__PURE__ */ new Set();
85810
+ classes.add(a.macroClass);
85811
+ classesByFrame.set(a.mediaKey, classes);
85812
+ }
85813
+ const collections = this.deps.memberships !== void 0 ? await this.deps.memberships.collectionsOf(frameIds).catch((err) => {
85814
+ this.deps.logger.warn("retrain corpus: collection membership unreadable", { meta: {
85815
+ frames: frameIds.length,
85816
+ error: String(err)
85817
+ } });
85818
+ return /* @__PURE__ */ new Map();
85819
+ }) : /* @__PURE__ */ new Map();
85820
+ return frames.map((f) => ({
85821
+ frameId: f.frameId,
85822
+ deviceId: f.deviceId,
85823
+ complete: f.complete,
85824
+ annotationCount: countByFrame.get(f.frameId) ?? 0,
85825
+ copiedAt: f.copiedAt,
85826
+ collectionIds: collections.get(f.frameId) ?? [],
85827
+ classes: [...classesByFrame.get(f.frameId) ?? []].toSorted()
85828
+ }));
85829
+ }
86072
85830
  /** `staging → trained` — the first and only writer of that state. */
86073
85831
  async complete(input) {
86074
85832
  const result = await completeRetrainTrack(this.lifecycleDeps(), input);
@@ -109168,8 +108926,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
109168
108926
  retrain = null;
109169
108927
  retrainCollections = null;
109170
108928
  retrainLlmAssist = null;
109171
- retrainDatasetVersions = null;
109172
- retrainDatasets = null;
109173
108929
  retrainCollectionStore = null;
109174
108930
  retrainFrames = null;
109175
108931
  retrainAnnotations = null;
@@ -110966,7 +110722,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
110966
110722
  RetrainFrameStore.declare(api.settingsStore),
110967
110723
  RetrainAnnotationStore.declare(api.settingsStore),
110968
110724
  RetrainCollectionStore.declare(api.settingsStore),
110969
- RetrainDatasetVersionStore.declare(api.settingsStore),
110970
110725
  OpsLogStore.declare(api.settingsStore),
110971
110726
  DebugNoteArchiveStore.declare(api.settingsStore),
110972
110727
  TrackingTapeIndexStore.declare(api.settingsStore),
@@ -111274,36 +111029,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
111274
111029
  captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
111275
111030
  logger: logger.child("RetrainCollections")
111276
111031
  });
111277
- const retrainDatasetVersions = new RetrainDatasetVersionStore({
111278
- store: api.settingsStore,
111279
- logger: logger.child("RetrainDatasets")
111280
- });
111281
- this.retrainDatasetVersions = retrainDatasetVersions;
111282
- const routedDatasetVersions = new RoutingDatasetVersionRepository({
111283
- local: retrainDatasetVersions,
111284
- training: {
111285
- put: (row) => api.training.putDatasetVersion.mutate({ ...row }),
111286
- get: (id) => api.training.getDatasetVersion.query({ id }),
111287
- list: async () => api.training.listDatasetVersions.query({}),
111288
- findIdByName: async (name) => (await api.training.findDatasetVersionIdByName.query({ name })).id,
111289
- remove: async (id) => (await api.training.removeDatasetVersion.mutate({ id })).removed
111290
- },
111291
- trainingPresent: async () => {
111292
- try {
111293
- return (await api.addons.listCapabilityProviders.query({ capName: "training" })).length > 0;
111294
- } catch {
111295
- return null;
111296
- }
111297
- },
111298
- logger: logger.child("RetrainDatasets")
111299
- });
111300
- this.retrainDatasets = new RetrainDatasetService({
111301
- versions: routedDatasetVersions,
111302
- buildPlan: (request) => this.buildRetrainExportPlan(request),
111303
- now: () => Date.now(),
111304
- newId: () => randomUUID(),
111305
- logger: logger.child("RetrainDatasets")
111306
- });
111307
111032
  this.retrainLlmAssist = new RetrainLlmAssistService({
111308
111033
  frames: retrainFrames,
111309
111034
  llm: {
@@ -112502,6 +112227,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
112502
112227
  ...input.limit !== void 0 ? { limit: input.limit } : {}
112503
112228
  });
112504
112229
  }
112230
+ async listRetrainCorpusPage(input) {
112231
+ return this.requireRetrain().listCorpusPage(input);
112232
+ }
112233
+ async getRetrainCorpusKpi(_input) {
112234
+ return this.requireRetrain().getCorpusKpi();
112235
+ }
112505
112236
  async listRetrainFrames(input) {
112506
112237
  return this.requireRetrain().listFrames(input.trackId);
112507
112238
  }
@@ -112556,29 +112287,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
112556
112287
  async setRetrainFrameComplete(input) {
112557
112288
  return this.requireRetrain().setFrameComplete(input);
112558
112289
  }
112559
- requireRetrainDatasets() {
112560
- const service = this.retrainDatasets;
112561
- if (service === null) throw new Error("retrain datasets: analytics stores unavailable");
112562
- return service;
112563
- }
112564
- async previewRetrainDataset(input) {
112565
- return this.requireRetrainDatasets().preview({
112566
- selection: input.selection,
112567
- completeOnly: input.completeOnly ?? false
112568
- });
112569
- }
112570
- async createRetrainDatasetVersion(input) {
112571
- return this.requireRetrainDatasets().create(input);
112572
- }
112573
- async listRetrainDatasetVersions(_input) {
112574
- return this.requireRetrainDatasets().list();
112575
- }
112576
- async deleteRetrainDatasetVersion(input) {
112577
- return this.requireRetrainDatasets().delete(input);
112578
- }
112579
- async verifyRetrainDatasetVersion(input) {
112580
- return this.requireRetrainDatasets().verify(input);
112581
- }
112582
112290
  requireRetrainLlmAssist() {
112583
112291
  const service = this.retrainLlmAssist;
112584
112292
  if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
@@ -112601,6 +112309,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
112601
112309
  async listRetrainCollections(_input) {
112602
112310
  return this.requireRetrainCollections().list();
112603
112311
  }
112312
+ /**
112313
+ * Live training selection → frame ids + stats (index reads only). Refuses an
112314
+ * unknown collection id or an empty match after the work-state rule.
112315
+ */
112316
+ async resolveRetrainTrainingCorpus(input) {
112317
+ const collections = await this.requireRetrainCollections().list();
112318
+ const known = new Set(collections.map((c) => c.id));
112319
+ for (const id of input.collectionIds) if (!known.has(id)) return {
112320
+ kind: "refused",
112321
+ reason: "unknown-collection",
112322
+ detail: `collection "${id}" does not exist`
112323
+ };
112324
+ const filter = exportFilterForTrainingCorpus(input);
112325
+ const plan = await this.buildRetrainExportPlan({
112326
+ filter,
112327
+ layout: "frames"
112328
+ });
112329
+ const frames = plan.manifest.frames.filter((frame) => keepTrainingCorpusFrame(frame, input.workState));
112330
+ if (frames.length === 0) return {
112331
+ kind: "refused",
112332
+ reason: "no-frames",
112333
+ detail: "no frames match the selected collections and work-state rule"
112334
+ };
112335
+ const manifest = {
112336
+ ...plan.manifest,
112337
+ frames
112338
+ };
112339
+ return {
112340
+ kind: "resolved",
112341
+ frameIds: frames.map((f) => f.frameId).toSorted(),
112342
+ stats: datasetStats(manifest),
112343
+ annotationHash: datasetAnnotationHash(frames),
112344
+ layout: "frames"
112345
+ };
112346
+ }
112604
112347
  async renameRetrainCollection(input) {
112605
112348
  return this.requireRetrainCollections().rename(input);
112606
112349
  }
@@ -113014,22 +112757,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113014
112757
  * zones simply has no `zoneCrop` geometry — absent, never a clamped guess.
113015
112758
  */
113016
112759
  async buildRetrainExportPlan(request) {
113017
- const versionId = request.filter.datasetVersionId;
113018
- if (versionId !== void 0) {
113019
- const version = await this.retrainDatasetVersions?.get(versionId) ?? null;
113020
- const { datasetVersionId: _resolved, ...rest } = await this.retrainDatasets?.exportFilterFor(request.filter) ?? {
113021
- ...request.filter,
113022
- frameIds: []
113023
- };
113024
- const plan = await this.buildRetrainExportPlan({
113025
- ...request,
113026
- filter: rest
113027
- });
113028
- return version === null ? plan : {
113029
- ...plan,
113030
- manifest: withDatasetVersion(plan.manifest, version)
113031
- };
113032
- }
113033
112760
  const frameStore = this.retrainFrames;
113034
112761
  const annotationStore = this.retrainAnnotations;
113035
112762
  const trackStore = this.trackStore;
@@ -113039,18 +112766,27 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113039
112766
  const candidates = /* @__PURE__ */ new Map();
113040
112767
  const annotationsByFrame = /* @__PURE__ */ new Map();
113041
112768
  const zones = /* @__PURE__ */ new Map();
113042
- for (const deviceId of devices) {
113043
- zones.set(deviceId, await this.retrainZoneForDevice(deviceId));
113044
- for (const annotation of await annotationStore.listByDevice(deviceId, RETRAIN_EXPORT_MAX)) {
112769
+ for (const deviceId of devices) zones.set(deviceId, await this.retrainZoneForDevice(deviceId));
112770
+ if (exportNeedsFullCorpus(request.filter)) {
112771
+ const corpus = deviceIds.length > 0 ? (await frameStore.listAll()).filter((f) => deviceIds.includes(f.deviceId)) : await frameStore.listAll();
112772
+ for (const frame of corpus) candidates.set(frame.frameId, frame);
112773
+ for (const annotation of await annotationStore.listByFrames([...candidates.keys()])) {
113045
112774
  const existing = annotationsByFrame.get(annotation.mediaKey) ?? [];
113046
112775
  annotationsByFrame.set(annotation.mediaKey, [...existing, annotation]);
113047
112776
  }
113048
- for (const frame of await frameStore.listCompleteByDevice(deviceId, RETRAIN_EXPORT_MAX)) candidates.set(frame.frameId, frame);
113049
- }
113050
- for (const frameId of annotationsByFrame.keys()) {
113051
- if (candidates.has(frameId)) continue;
113052
- const frame = await frameStore.get(frameId);
113053
- if (frame !== null) candidates.set(frameId, frame);
112777
+ } else {
112778
+ for (const deviceId of devices) {
112779
+ for (const annotation of await annotationStore.listByDevice(deviceId, RETRAIN_EXPORT_MAX)) {
112780
+ const existing = annotationsByFrame.get(annotation.mediaKey) ?? [];
112781
+ annotationsByFrame.set(annotation.mediaKey, [...existing, annotation]);
112782
+ }
112783
+ for (const frame of await frameStore.listCompleteByDevice(deviceId, RETRAIN_EXPORT_MAX)) candidates.set(frame.frameId, frame);
112784
+ }
112785
+ for (const frameId of annotationsByFrame.keys()) {
112786
+ if (candidates.has(frameId)) continue;
112787
+ const frame = await frameStore.get(frameId);
112788
+ if (frame !== null) candidates.set(frameId, frame);
112789
+ }
113054
112790
  }
113055
112791
  const collectionsByFrame = await this.retrainCollectionStore?.collectionsOf([...candidates.keys()]) ?? /* @__PURE__ */ new Map();
113056
112792
  const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter, collectionsByFrame);