@camstack/addon-post-analysis 1.2.343 → 1.2.344

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. package/dist/{clip-model-registry-DZz3e781.mjs → clip-model-registry-C44gfwjm.mjs} +1992 -57
  2. package/dist/{clip-model-registry-Cd_y9S4U.js → clip-model-registry-Ca_IbZyD.js} +1992 -57
  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-DrdO3Pxp.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D73dyYqt.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-C-2BbuN4.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-ZZJLA-YE.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-DPNnWr-m.mjs → hostInit-h_8fvfFv.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +1562 -138
  11. package/dist/pipeline-analytics/index.mjs +1562 -138
  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-DEq-B6oq.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-CiTUMvVf.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-DZz3e781.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-C44gfwjm.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";
@@ -5831,16 +5831,16 @@ function ncDetectionSummary(texts, counts) {
5831
5831
  //#region src/notification-center/timelapse/detection-counts.ts
5832
5832
  /** One normalisation, used on both sides of every comparison — a rule that
5833
5833
  * named `Person` must match a track classified `person`. */
5834
- function normalise(name) {
5834
+ function normalise$1(name) {
5835
5835
  return name.trim().toLowerCase();
5836
5836
  }
5837
5837
  /** Count per class over the tracks, honouring the rule's selection. */
5838
5838
  function countDetections(input) {
5839
- const selected = (input.reportClasses ?? []).map(normalise).filter((c) => c.length > 0);
5839
+ const selected = (input.reportClasses ?? []).map(normalise$1).filter((c) => c.length > 0);
5840
5840
  const seen = /* @__PURE__ */ new Map();
5841
5841
  for (const t of input.tracks) {
5842
5842
  if (t.className === void 0) continue;
5843
- const key = normalise(t.className);
5843
+ const key = normalise$1(t.className);
5844
5844
  if (key.length === 0) continue;
5845
5845
  if (selected.length > 0 && !selected.includes(key)) continue;
5846
5846
  seen.set(key, (seen.get(key) ?? 0) + 1);
@@ -70575,10 +70575,12 @@ var TrackStore = class {
70575
70575
  */
70576
70576
  /**
70577
70577
  * @durable class=ledger owner=pipeline-analytics
70578
- * write="one row per frame the operator SELECTS into the retrain dataset. The
70579
- * copy is taken eagerly at selection (copyFrame), not referenced, so the row
70580
- * records a blob the dataset owns outright; re-selecting the same source reuses
70581
- * the existing copy rather than writing a second one."
70578
+ * write="one row per frame the operator SELECTS into the retrain dataset, or adds
70579
+ * from the recording at an instant (storeRecordingStill, origin 'recording', no
70580
+ * track). The copy is taken eagerly at selection (copyFrame), not referenced, so
70581
+ * the row records a blob the dataset owns outright; re-selecting the same source
70582
+ * (or the same camera instant) reuses the existing copy rather than writing a
70583
+ * second one."
70582
70584
  * retention="none — the blobs sit in the 'retrain/' subtree of the artifacts class
70583
70585
  * (D781), which no owner cascade and no age sweep walks, which is exactly what makes a 'trained' track evictable
70584
70586
  * again (D81). Rows go only when the operator deselects the frame (deleteFrame,
@@ -70693,6 +70695,19 @@ var RETRAIN_FRAME_COLUMNS = [
70693
70695
  {
70694
70696
  name: "complete",
70695
70697
  type: "BOOLEAN"
70698
+ }),
70699
+ (
70700
+ /** `recording` for a still taken from the recording at an instant (no
70701
+ * track, no source media row). NULL = a track copy. */
70702
+ {
70703
+ name: "origin",
70704
+ type: "TEXT"
70705
+ }),
70706
+ (
70707
+ /** The recording instant a `recording` still was asked for. */
70708
+ {
70709
+ name: "epochMs",
70710
+ type: "INTEGER"
70696
70711
  })
70697
70712
  ];
70698
70713
  var RETRAIN_FRAME_INDEXES = [
@@ -70718,6 +70733,8 @@ var RETRAIN_FRAME_INDEXES = [
70718
70733
  columns: ["locationId"]
70719
70734
  })
70720
70735
  ];
70736
+ /** Ids per `IN (…)` page in {@link RetrainFrameStore.getMany}. */
70737
+ var GET_MANY_PAGE = 500;
70721
70738
  /**
70722
70739
  * The copy's id, derived from `(trackId, sourceMediaId)`.
70723
70740
  *
@@ -70730,8 +70747,20 @@ var RETRAIN_FRAME_INDEXES = [
70730
70747
  function retrainFrameId(trackId, sourceMediaId) {
70731
70748
  return `retrain:${trackId}:${createHash("sha1").update(String(sourceMediaId)).digest("hex").slice(0, 16)}`;
70732
70749
  }
70733
- function framePath(deviceId, trackId, frameId) {
70734
- return `${RETRAIN_PATH_PREFIX}/${deviceId}/${trackId}/${frameId.slice(frameId.lastIndexOf(":") + 1)}.jpg`;
70750
+ /**
70751
+ * A recording still's id, derived from `(deviceId, epochMs)` — the same moment
70752
+ * of the same camera is one copy, however many times it is added.
70753
+ */
70754
+ function recordingFrameId(deviceId, epochMs) {
70755
+ const digest = createHash("sha1").update(String(epochMs)).digest("hex").slice(0, 16);
70756
+ return `retrain:rec:${String(deviceId)}:${digest}`;
70757
+ }
70758
+ /** The directory a recording still sits in, beside the track directories. */
70759
+ var RECORDING_STILL_DIR = "recording";
70760
+ function framePath(frame) {
70761
+ const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
70762
+ const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
70763
+ return `${RETRAIN_PATH_PREFIX}/${frame.deviceId}/${dir}/${leaf}.jpg`;
70735
70764
  }
70736
70765
  /** A stored number, or `undefined` for NULL / absent / junk. */
70737
70766
  function finiteNumber(value) {
@@ -70776,9 +70805,11 @@ var RetrainFrameStore = class {
70776
70805
  });
70777
70806
  }
70778
70807
  rowToFrame(id, data) {
70808
+ const origin = data["origin"] === "recording" ? "recording" : "track";
70779
70809
  return {
70780
70810
  frameId: id,
70781
70811
  deviceId: Number(data["deviceId"] ?? 0),
70812
+ origin,
70782
70813
  trackId: String(data["trackId"] ?? ""),
70783
70814
  sourceMediaId: Number(data["sourceMediaId"] ?? 0),
70784
70815
  sourceKind: String(data["sourceKind"] ?? "keyFrame"),
@@ -70789,9 +70820,54 @@ var RetrainFrameStore = class {
70789
70820
  locationId: typeof data["locationId"] === "string" && data["locationId"].length > 0 ? data["locationId"] : null,
70790
70821
  ...finiteNumber(data["capturedAt"]) !== void 0 ? { capturedAt: finiteNumber(data["capturedAt"]) } : {},
70791
70822
  ...finiteNumber(data["chromaSpread"]) !== void 0 ? { chromaSpread: finiteNumber(data["chromaSpread"]) } : {},
70792
- complete: data["complete"] === true || data["complete"] === 1
70823
+ complete: data["complete"] === true || data["complete"] === 1,
70824
+ ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {}
70793
70825
  };
70794
70826
  }
70827
+ /** The copies with these ids, in the order given; unknown ids are skipped. */
70828
+ async getMany(frameIds) {
70829
+ if (frameIds.length === 0) return [];
70830
+ try {
70831
+ const byId = /* @__PURE__ */ new Map();
70832
+ for (let i = 0; i < frameIds.length; i += GET_MANY_PAGE) {
70833
+ const page = frameIds.slice(i, i + GET_MANY_PAGE);
70834
+ const rows = await this.store.query.query({
70835
+ collection: RETRAIN_FRAMES_COLLECTION,
70836
+ filter: {
70837
+ whereIn: { id: [...page] },
70838
+ limit: page.length
70839
+ }
70840
+ });
70841
+ for (const r of rows) if (typeof r.id === "string") byId.set(r.id, this.rowToFrame(r.id, r.data));
70842
+ }
70843
+ return frameIds.flatMap((id) => {
70844
+ const frame = byId.get(id);
70845
+ return frame !== void 0 ? [frame] : [];
70846
+ });
70847
+ } catch (err) {
70848
+ this.logger.warn("RetrainFrameStore.getMany failed", { meta: {
70849
+ frames: frameIds.length,
70850
+ error: String(err)
70851
+ } });
70852
+ return [];
70853
+ }
70854
+ }
70855
+ /** Every camera the dataset holds a copy of — a recording still can be the
70856
+ * only thing a camera contributes, with no track anywhere. */
70857
+ async listDeviceIds() {
70858
+ try {
70859
+ const rows = await this.store.query.query({ collection: RETRAIN_FRAMES_COLLECTION });
70860
+ const ids = /* @__PURE__ */ new Set();
70861
+ for (const r of rows) {
70862
+ const id = Number(r.data["deviceId"]);
70863
+ if (Number.isInteger(id) && id > 0) ids.add(id);
70864
+ }
70865
+ return [...ids].toSorted((a, b) => a - b);
70866
+ } catch (err) {
70867
+ this.logger.warn("RetrainFrameStore.listDeviceIds failed", { meta: { error: String(err) } });
70868
+ return [];
70869
+ }
70870
+ }
70795
70871
  /** The copy with this id, or `null`. Index read only — no blob is touched. */
70796
70872
  async get(frameId) {
70797
70873
  try {
@@ -70808,8 +70884,10 @@ var RetrainFrameStore = class {
70808
70884
  return null;
70809
70885
  }
70810
70886
  }
70811
- /** Every copy taken from this track, oldest first. */
70887
+ /** Every copy taken from this track, oldest first. A recording still has
70888
+ * no track and is never answered here. */
70812
70889
  async listByTrack(trackId) {
70890
+ if (trackId === "") return [];
70813
70891
  try {
70814
70892
  return (await this.store.query.query({
70815
70893
  collection: RETRAIN_FRAMES_COLLECTION,
@@ -70883,7 +70961,7 @@ var RetrainFrameStore = class {
70883
70961
  try {
70884
70962
  return await this.storage.read({
70885
70963
  location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
70886
- relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
70964
+ relativePath: framePath(frame)
70887
70965
  });
70888
70966
  } catch (err) {
70889
70967
  this.logger.warn("retrain frame copy unreadable", {
@@ -70952,12 +71030,17 @@ var RetrainFrameStore = class {
70952
71030
  reason: "unreadable-image"
70953
71031
  };
70954
71032
  }
70955
- const path = framePath(input.deviceId, input.trackId, frameId);
71033
+ const path = framePath({
71034
+ ...input,
71035
+ frameId,
71036
+ origin: "track"
71037
+ });
70956
71038
  const locationId = RETRAIN_MEDIA_LOCATION;
70957
71039
  const chromaSpread = await this.chromaOf(bytes, input.deviceId);
70958
71040
  const frame = {
70959
71041
  frameId,
70960
71042
  deviceId: input.deviceId,
71043
+ origin: "track",
70961
71044
  trackId: input.trackId,
70962
71045
  sourceMediaId: input.sourceMediaId,
70963
71046
  sourceKind: input.sourceKind,
@@ -71032,6 +71115,115 @@ var RetrainFrameStore = class {
71032
71115
  };
71033
71116
  }
71034
71117
  /**
71118
+ * Store a still decoded from the RECORDING at `epochMs` — a frame with no
71119
+ * track and no media row, usually the one the model saw nothing in.
71120
+ *
71121
+ * Same contract as {@link copyFrame}: an existing copy of that moment is
71122
+ * returned untouched; the blob lands before the row; a refusal is named.
71123
+ * `capturedAt` IS the instant — the camera saw this frame then.
71124
+ */
71125
+ async storeRecordingStill(input) {
71126
+ const frameId = recordingFrameId(input.deviceId, input.epochMs);
71127
+ const existing = await this.get(frameId);
71128
+ if (existing !== null) return {
71129
+ kind: "copied",
71130
+ frame: existing,
71131
+ reused: true
71132
+ };
71133
+ const size = readJpegSize(input.bytes);
71134
+ if (size === null) {
71135
+ this.logger.warn("retrain recording still refused — not a readable JPEG", {
71136
+ tags: { deviceId: input.deviceId },
71137
+ meta: {
71138
+ epochMs: input.epochMs,
71139
+ bytes: input.bytes.length
71140
+ }
71141
+ });
71142
+ return {
71143
+ kind: "refused",
71144
+ reason: "unreadable-image"
71145
+ };
71146
+ }
71147
+ const chromaSpread = await this.chromaOf(input.bytes, input.deviceId);
71148
+ const frame = {
71149
+ frameId,
71150
+ deviceId: input.deviceId,
71151
+ origin: "recording",
71152
+ trackId: "",
71153
+ sourceMediaId: 0,
71154
+ sourceKind: "fullFrame",
71155
+ sizeBytes: input.bytes.length,
71156
+ width: size.width,
71157
+ height: size.height,
71158
+ copiedAt: this.now(),
71159
+ locationId: RETRAIN_MEDIA_LOCATION,
71160
+ capturedAt: input.epochMs,
71161
+ ...chromaSpread !== void 0 ? { chromaSpread } : {},
71162
+ complete: false,
71163
+ epochMs: input.epochMs
71164
+ };
71165
+ const path = framePath(frame);
71166
+ const persist = async () => {
71167
+ await this.storage.write({
71168
+ location: RETRAIN_MEDIA_LOCATION,
71169
+ relativePath: path,
71170
+ data: input.bytes
71171
+ });
71172
+ await this.store.set.mutate({
71173
+ collection: RETRAIN_FRAMES_COLLECTION,
71174
+ key: frameId,
71175
+ value: {
71176
+ deviceId: frame.deviceId,
71177
+ trackId: "",
71178
+ sourceMediaId: 0,
71179
+ sourceKind: frame.sourceKind,
71180
+ path,
71181
+ sizeBytes: frame.sizeBytes,
71182
+ width: frame.width,
71183
+ height: frame.height,
71184
+ copiedAt: frame.copiedAt,
71185
+ locationId: RETRAIN_MEDIA_LOCATION,
71186
+ capturedAt: input.epochMs,
71187
+ chromaSpread: frame.chromaSpread ?? null,
71188
+ complete: false,
71189
+ origin: "recording",
71190
+ epochMs: input.epochMs
71191
+ }
71192
+ });
71193
+ };
71194
+ try {
71195
+ if (this.writeGate) await this.writeGate.run(persist);
71196
+ else await persist();
71197
+ } catch (err) {
71198
+ this.logger.error("retrain recording still failed to persist", {
71199
+ tags: { deviceId: input.deviceId },
71200
+ meta: {
71201
+ frameId,
71202
+ epochMs: input.epochMs,
71203
+ error: String(err)
71204
+ }
71205
+ });
71206
+ return {
71207
+ kind: "refused",
71208
+ reason: "write-failed"
71209
+ };
71210
+ }
71211
+ this.logger.info("retrain recording still stored in the dataset", {
71212
+ tags: { deviceId: input.deviceId },
71213
+ meta: {
71214
+ frameId,
71215
+ epochMs: input.epochMs,
71216
+ width: frame.width,
71217
+ height: frame.height
71218
+ }
71219
+ });
71220
+ return {
71221
+ kind: "copied",
71222
+ frame,
71223
+ reused: false
71224
+ };
71225
+ }
71226
+ /**
71035
71227
  * The operator's "every subject in this frame is boxed" (D785 context: an
71036
71228
  * unboxed subject is a false positive to a detector evaluation). Only the
71037
71229
  * operator sets it; nothing derives it from the annotation count, because a
@@ -71064,7 +71256,7 @@ var RetrainFrameStore = class {
71064
71256
  try {
71065
71257
  await this.storage.delete({
71066
71258
  location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
71067
- relativePath: framePath(frame.deviceId, frame.trackId, frame.frameId)
71259
+ relativePath: framePath(frame)
71068
71260
  });
71069
71261
  } catch {}
71070
71262
  try {
@@ -71111,7 +71303,7 @@ var RetrainFrameStore = class {
71111
71303
  if (rowIsDurableRetrainCopy(data)) continue;
71112
71304
  const frame = this.rowToFrame(row.id, data);
71113
71305
  const from = frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION;
71114
- const relativePath = framePath(frame.deviceId, frame.trackId, frame.frameId);
71306
+ const relativePath = framePath(frame);
71115
71307
  let bytes;
71116
71308
  try {
71117
71309
  bytes = await this.storage.read({
@@ -82227,6 +82419,17 @@ var RETRAIN_ANNOTATION_INDEXES = [
82227
82419
  columns: ["deviceId", "createdAt"]
82228
82420
  }
82229
82421
  ];
82422
+ var ANNOTATION_SOURCES = [
82423
+ "operator",
82424
+ "assist",
82425
+ "pipeline",
82426
+ "llm"
82427
+ ];
82428
+ /** The stored source, narrowed. Unknown reads as `operator` — the least
82429
+ * presumptuous claim a row can make about who drew it. */
82430
+ function narrowSource(value) {
82431
+ return ANNOTATION_SOURCES.find((source) => source === value) ?? "operator";
82432
+ }
82230
82433
  function optionalString(value) {
82231
82434
  return typeof value === "string" && value.length > 0 ? value : void 0;
82232
82435
  }
@@ -82234,6 +82437,8 @@ function optionalString(value) {
82234
82437
  function narrowMacroClass(value) {
82235
82438
  return typeof value === "string" && isRetrainMacroClass(value) ? value : "person";
82236
82439
  }
82440
+ /** Frame ids per `IN (…)` page in {@link RetrainAnnotationStore.listByFrames}. */
82441
+ var LIST_BY_FRAMES_PAGE = 500;
82237
82442
  var RetrainAnnotationStore = class {
82238
82443
  store;
82239
82444
  logger;
@@ -82270,7 +82475,7 @@ var RetrainAnnotationStore = class {
82270
82475
  label: optionalString(data["label"]),
82271
82476
  subLabel: optionalString(data["subLabel"]),
82272
82477
  kind: data["kind"] === "model_error" ? "model_error" : "subject",
82273
- source: data["source"] === "assist" ? "assist" : "operator",
82478
+ source: narrowSource(data["source"]),
82274
82479
  assistModelId: optionalString(data["assistModelId"]),
82275
82480
  assistScore: typeof score === "number" ? score : void 0,
82276
82481
  exportedInBatch: optionalString(data["exportedInBatch"]),
@@ -82339,6 +82544,32 @@ var RetrainAnnotationStore = class {
82339
82544
  async listByFrame(mediaKey) {
82340
82545
  return this.queryWhere({ mediaKey }, `frame ${mediaKey}`);
82341
82546
  }
82547
+ /** Every annotation of these frames, oldest first — paged `IN (…)`. */
82548
+ async listByFrames(mediaKeys) {
82549
+ const out = [];
82550
+ for (let i = 0; i < mediaKeys.length; i += LIST_BY_FRAMES_PAGE) {
82551
+ const page = mediaKeys.slice(i, i + LIST_BY_FRAMES_PAGE);
82552
+ try {
82553
+ const rows = await this.store.query.query({
82554
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
82555
+ filter: {
82556
+ whereIn: { mediaKey: [...page] },
82557
+ orderBy: {
82558
+ field: "createdAt",
82559
+ direction: "asc"
82560
+ }
82561
+ }
82562
+ });
82563
+ for (const r of rows) if (typeof r.id === "string") out.push(this.rowToAnnotation(r.id, r.data));
82564
+ } catch (err) {
82565
+ this.logger.warn("RetrainAnnotationStore.listByFrames failed", { meta: {
82566
+ frames: page.length,
82567
+ error: String(err)
82568
+ } });
82569
+ }
82570
+ }
82571
+ return out;
82572
+ }
82342
82573
  async listByTrack(trackId) {
82343
82574
  return this.queryWhere({ trackId }, `track ${trackId}`);
82344
82575
  }
@@ -82452,13 +82683,41 @@ var RetrainAnnotationStore = class {
82452
82683
  //#endregion
82453
82684
  //#region src/pipeline-analytics/retrain/retrain-collection-store.ts
82454
82685
  /**
82686
+ * RetrainCollectionStore — named sets over the one retrain corpus.
82687
+ *
82688
+ * Collections: `pipeline-analytics:retrain-collections` (the names) and
82689
+ * `pipeline-analytics:retrain-collection-frames` (the membership).
82690
+ *
82691
+ * A collection OWNS NO PIXELS. Every frame — copied from a track or stilled
82692
+ * from the recording — lives once in `retrain-frames`, with its annotations,
82693
+ * its completeness and its export path; a collection is a name the operator
82694
+ * gives to a subset of it ("packages porch night", "false-positive cars"). So
82695
+ * a frame can be in several collections, a collection's count is its
82696
+ * membership, and deleting a collection removes memberships, never a frame
82697
+ * another collection or a track still holds. Which trackless frames are left
82698
+ * with no reason to exist is the caller's question
82699
+ * (`retrain-collection-service.ts`).
82700
+ */
82701
+ /**
82455
82702
  * @durable class=ledger owner=pipeline-analytics
82456
82703
  * write="one row per named training collection the operator creates. Names are
82457
- * unique on nameKey (lower(trim)); empty names never become rows."
82458
- * retention="none — operator-owned corpus metadata. Rows leave only when an
82459
- * explicit delete API lands (not V1)."
82704
+ * unique on nameKey (lower(trim)); empty names never become rows. A rename
82705
+ * rewrites name + nameKey under the same id."
82706
+ * retention="none — operator-owned corpus metadata. A row leaves only when the
82707
+ * operator deletes the collection (deleteRetrainCollection), which removes its
82708
+ * membership rows first."
82460
82709
  */
82461
82710
  var RETRAIN_COLLECTIONS_COLLECTION = "pipeline-analytics:retrain-collections";
82711
+ /**
82712
+ * @durable class=ledger owner=pipeline-analytics
82713
+ * write="one row per (collection, frame) pair the operator adds — a frame added
82714
+ * from the recording, or an existing retrain frame added to a collection. Keyed
82715
+ * by a hash of the pair, so adding twice is one row."
82716
+ * retention="none — rows leave when the operator removes the frame from the
82717
+ * collection, deletes the collection, or deselects the frame itself. Bounded by
82718
+ * operator curation."
82719
+ */
82720
+ var RETRAIN_COLLECTION_FRAMES_COLLECTION = "pipeline-analytics:retrain-collection-frames";
82462
82721
  var RETRAIN_COLLECTION_COLUMNS = [
82463
82722
  {
82464
82723
  name: "id",
@@ -82495,6 +82754,41 @@ var RETRAIN_COLLECTION_INDEXES = [{
82495
82754
  name: "idx_retrain_collections_created",
82496
82755
  columns: ["createdAt"]
82497
82756
  }];
82757
+ var RETRAIN_COLLECTION_FRAME_COLUMNS = [
82758
+ {
82759
+ name: "id",
82760
+ type: "TEXT",
82761
+ primaryKey: true,
82762
+ notNull: true
82763
+ },
82764
+ {
82765
+ name: "collectionId",
82766
+ type: "TEXT",
82767
+ notNull: true
82768
+ },
82769
+ {
82770
+ name: "frameId",
82771
+ type: "TEXT",
82772
+ notNull: true
82773
+ },
82774
+ {
82775
+ name: "deviceId",
82776
+ type: "INTEGER",
82777
+ notNull: true
82778
+ },
82779
+ {
82780
+ name: "addedAt",
82781
+ type: "INTEGER",
82782
+ notNull: true
82783
+ }
82784
+ ];
82785
+ var RETRAIN_COLLECTION_FRAME_INDEXES = [{
82786
+ name: "idx_retrain_coll_frames_collection",
82787
+ columns: ["collectionId", "addedAt"]
82788
+ }, {
82789
+ name: "idx_retrain_coll_frames_frame",
82790
+ columns: ["frameId"]
82791
+ }];
82498
82792
  function normalizeCollectionName(raw) {
82499
82793
  const name = raw.trim();
82500
82794
  if (name.length === 0) return null;
@@ -82503,18 +82797,22 @@ function normalizeCollectionName(raw) {
82503
82797
  nameKey: name.toLowerCase()
82504
82798
  };
82505
82799
  }
82800
+ /** The membership row's key — one row per pair, whatever the ids contain. */
82801
+ function membershipKey(collectionId, frameId) {
82802
+ return createHash("sha1").update(`${collectionId}\u0000${frameId}`).digest("hex");
82803
+ }
82804
+ /** Ids per `IN (…)` page. */
82805
+ var IN_PAGE = 500;
82506
82806
  var RetrainCollectionStore = class {
82507
82807
  store;
82508
82808
  logger;
82509
82809
  now;
82510
82810
  newId;
82511
- countFrames;
82512
82811
  constructor(deps) {
82513
82812
  this.store = deps.store;
82514
82813
  this.logger = deps.logger;
82515
82814
  this.now = deps.now;
82516
82815
  this.newId = deps.newId;
82517
- this.countFrames = deps.countFrames;
82518
82816
  }
82519
82817
  static async declare(store) {
82520
82818
  await store.declareCollection.mutate({
@@ -82522,6 +82820,11 @@ var RetrainCollectionStore = class {
82522
82820
  columns: [...RETRAIN_COLLECTION_COLUMNS],
82523
82821
  indexes: [...RETRAIN_COLLECTION_INDEXES]
82524
82822
  });
82823
+ await store.declareCollection.mutate({
82824
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82825
+ columns: [...RETRAIN_COLLECTION_FRAME_COLUMNS],
82826
+ indexes: [...RETRAIN_COLLECTION_FRAME_INDEXES]
82827
+ });
82525
82828
  }
82526
82829
  async create(input) {
82527
82830
  const normalized = normalizeCollectionName(input.name);
@@ -82529,7 +82832,7 @@ var RetrainCollectionStore = class {
82529
82832
  kind: "refused",
82530
82833
  reason: "empty-name"
82531
82834
  };
82532
- if (await this.findByNameKey(normalized.nameKey) !== null) return {
82835
+ if (await this.findIdByNameKey(normalized.nameKey) !== null) return {
82533
82836
  kind: "refused",
82534
82837
  reason: "duplicate-name"
82535
82838
  };
@@ -82545,6 +82848,7 @@ var RetrainCollectionStore = class {
82545
82848
  updatedAt: createdAt
82546
82849
  }
82547
82850
  });
82851
+ this.logger.info("retrain collection created", { meta: { collectionId: id } });
82548
82852
  return {
82549
82853
  kind: "created",
82550
82854
  collection: {
@@ -82555,6 +82859,7 @@ var RetrainCollectionStore = class {
82555
82859
  }
82556
82860
  };
82557
82861
  }
82862
+ /** Every collection, newest first, each with its membership count (one query). */
82558
82863
  async list() {
82559
82864
  let rows;
82560
82865
  try {
@@ -82569,34 +82874,158 @@ var RetrainCollectionStore = class {
82569
82874
  this.logger.warn("retrain collections list failed", { meta: { error: String(err) } });
82570
82875
  return [];
82571
82876
  }
82572
- const out = [];
82573
- for (const row of rows) {
82574
- const name = typeof row.data["name"] === "string" ? row.data["name"] : "";
82575
- const createdAt = Number(row.data["createdAt"] ?? 0);
82576
- const frameCount = this.countFrames !== void 0 ? await this.countFrames(row.id) : 0;
82577
- out.push({
82578
- id: row.id,
82579
- name,
82580
- createdAt,
82581
- frameCount
82582
- });
82877
+ const counts = await this.countMembers(rows.map((r) => r.id));
82878
+ return rows.map((row) => this.toCollection(row.id, row.data, counts.get(row.id) ?? 0));
82879
+ }
82880
+ async getById(id) {
82881
+ const data = await this.readRow(id);
82882
+ if (data === null) return null;
82883
+ const counts = await this.countMembers([id]);
82884
+ return this.toCollection(id, data, counts.get(id) ?? 0);
82885
+ }
82886
+ async rename(input) {
82887
+ const normalized = normalizeCollectionName(input.name);
82888
+ if (normalized === null) return {
82889
+ kind: "refused",
82890
+ reason: "empty-name"
82891
+ };
82892
+ const current = await this.readRow(input.id);
82893
+ if (current === null) return {
82894
+ kind: "refused",
82895
+ reason: "unknown-collection"
82896
+ };
82897
+ const holder = await this.findIdByNameKey(normalized.nameKey);
82898
+ if (holder !== null && holder !== input.id) return {
82899
+ kind: "refused",
82900
+ reason: "duplicate-name"
82901
+ };
82902
+ await this.store.update.mutate({
82903
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
82904
+ id: input.id,
82905
+ data: {
82906
+ name: normalized.name,
82907
+ nameKey: normalized.nameKey,
82908
+ updatedAt: this.now()
82909
+ }
82910
+ });
82911
+ this.logger.info("retrain collection renamed", { meta: { collectionId: input.id } });
82912
+ const counts = await this.countMembers([input.id]);
82913
+ return {
82914
+ kind: "renamed",
82915
+ collection: this.toCollection(input.id, {
82916
+ ...current,
82917
+ name: normalized.name
82918
+ }, counts.get(input.id) ?? 0)
82919
+ };
82920
+ }
82921
+ /** Remove the collection and its memberships — members first, so a crash
82922
+ * never leaves memberships pointing at a name that no longer exists. */
82923
+ async delete(id) {
82924
+ if (await this.readRow(id) === null) return {
82925
+ deleted: false,
82926
+ memberFrameIds: []
82927
+ };
82928
+ const members = await this.membershipRows({ collectionId: id });
82929
+ for (const m of members) await this.store.delete.mutate({
82930
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82931
+ key: m.id
82932
+ });
82933
+ await this.store.delete.mutate({
82934
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
82935
+ key: id
82936
+ });
82937
+ this.logger.info("retrain collection deleted", { meta: {
82938
+ collectionId: id,
82939
+ members: members.length
82940
+ } });
82941
+ return {
82942
+ deleted: true,
82943
+ memberFrameIds: members.map((m) => String(m.data["frameId"] ?? ""))
82944
+ };
82945
+ }
82946
+ /** Add a frame. `false` when it was already a member (nothing written). */
82947
+ async addFrame(input) {
82948
+ const key = membershipKey(input.collectionId, input.frameId);
82949
+ const existing = await this.store.get.query({
82950
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82951
+ key
82952
+ });
82953
+ if (existing !== null && existing !== void 0) return false;
82954
+ await this.store.set.mutate({
82955
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82956
+ key,
82957
+ value: {
82958
+ collectionId: input.collectionId,
82959
+ frameId: input.frameId,
82960
+ deviceId: input.deviceId,
82961
+ addedAt: this.now()
82962
+ }
82963
+ });
82964
+ return true;
82965
+ }
82966
+ /** Remove one membership. `false` when it did not exist. */
82967
+ async removeFrame(input) {
82968
+ const key = membershipKey(input.collectionId, input.frameId);
82969
+ const existing = await this.store.get.query({
82970
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82971
+ key
82972
+ });
82973
+ if (existing === null || existing === void 0) return false;
82974
+ await this.store.delete.mutate({
82975
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82976
+ key
82977
+ });
82978
+ return true;
82979
+ }
82980
+ /** Remove a frame from EVERY collection — the frame itself is leaving. */
82981
+ async forgetFrame(frameId) {
82982
+ const rows = await this.membershipRows({ frameId });
82983
+ for (const r of rows) await this.store.delete.mutate({
82984
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
82985
+ key: r.id
82986
+ });
82987
+ return rows.length;
82988
+ }
82989
+ /** The frames of a collection, in the order they were added. */
82990
+ async listFrameIds(collectionId) {
82991
+ return (await this.membershipRows({ collectionId })).toSorted((a, b) => Number(a.data["addedAt"] ?? 0) - Number(b.data["addedAt"] ?? 0)).map((r) => String(r.data["frameId"] ?? "")).filter((id) => id !== "");
82992
+ }
82993
+ /** frameId → the collections it belongs to. Frames in none are absent. */
82994
+ async collectionsOf(frameIds) {
82995
+ const out = /* @__PURE__ */ new Map();
82996
+ for (const row of await this.membershipRowsIn("frameId", frameIds)) {
82997
+ const frameId = String(row.data["frameId"] ?? "");
82998
+ const collectionId = String(row.data["collectionId"] ?? "");
82999
+ if (frameId === "" || collectionId === "") continue;
83000
+ out.set(frameId, [...out.get(frameId) ?? [], collectionId]);
82583
83001
  }
83002
+ for (const [frameId, ids] of out) out.set(frameId, ids.toSorted());
82584
83003
  return out;
82585
83004
  }
82586
- async getById(id) {
83005
+ /** The frame ids of several collections, de-duplicated. */
83006
+ async frameIdsOf(collectionIds) {
83007
+ const ids = /* @__PURE__ */ new Set();
83008
+ for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
83009
+ const frameId = String(row.data["frameId"] ?? "");
83010
+ if (frameId !== "") ids.add(frameId);
83011
+ }
83012
+ return [...ids];
83013
+ }
83014
+ toCollection(id, data, frameCount) {
83015
+ return {
83016
+ id,
83017
+ name: typeof data["name"] === "string" ? data["name"] : "",
83018
+ createdAt: Number(data["createdAt"] ?? 0),
83019
+ frameCount
83020
+ };
83021
+ }
83022
+ async readRow(id) {
82587
83023
  try {
82588
83024
  const row = await this.store.get.query({
82589
83025
  collection: RETRAIN_COLLECTIONS_COLLECTION,
82590
83026
  key: id
82591
83027
  });
82592
- if (row === null || row === void 0) return null;
82593
- const data = row;
82594
- return {
82595
- id,
82596
- name: typeof data["name"] === "string" ? data["name"] : "",
82597
- createdAt: Number(data["createdAt"] ?? 0),
82598
- frameCount: this.countFrames !== void 0 ? await this.countFrames(id) : 0
82599
- };
83028
+ return row === null || row === void 0 ? null : { ...row };
82600
83029
  } catch (err) {
82601
83030
  this.logger.warn("retrain collection get failed", { meta: {
82602
83031
  collectionId: id,
@@ -82605,14 +83034,125 @@ var RetrainCollectionStore = class {
82605
83034
  return null;
82606
83035
  }
82607
83036
  }
83037
+ async countMembers(collectionIds) {
83038
+ const counts = /* @__PURE__ */ new Map();
83039
+ for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
83040
+ const id = String(row.data["collectionId"] ?? "");
83041
+ counts.set(id, (counts.get(id) ?? 0) + 1);
83042
+ }
83043
+ return counts;
83044
+ }
83045
+ async membershipRows(where) {
83046
+ try {
83047
+ return await this.store.query.query({
83048
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
83049
+ filter: { where }
83050
+ });
83051
+ } catch (err) {
83052
+ this.logger.warn("retrain collection membership read failed", { meta: {
83053
+ ...where,
83054
+ error: String(err)
83055
+ } });
83056
+ return [];
83057
+ }
83058
+ }
83059
+ async membershipRowsIn(field, values) {
83060
+ const out = [];
83061
+ for (let i = 0; i < values.length; i += IN_PAGE) {
83062
+ const page = values.slice(i, i + IN_PAGE);
83063
+ try {
83064
+ out.push(...await this.store.query.query({
83065
+ collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
83066
+ filter: { whereIn: { [field]: [...page] } }
83067
+ }));
83068
+ } catch (err) {
83069
+ this.logger.warn("retrain collection membership read failed", { meta: {
83070
+ field,
83071
+ values: page.length,
83072
+ error: String(err)
83073
+ } });
83074
+ }
83075
+ }
83076
+ return out;
83077
+ }
83078
+ async findIdByNameKey(nameKey) {
83079
+ try {
83080
+ return (await this.store.query.query({
83081
+ collection: RETRAIN_COLLECTIONS_COLLECTION,
83082
+ filter: {
83083
+ where: { nameKey },
83084
+ limit: 1
83085
+ }
83086
+ }))[0]?.id ?? null;
83087
+ } catch (err) {
83088
+ this.logger.warn("retrain collection findByNameKey failed", { meta: {
83089
+ nameKey,
83090
+ error: String(err)
83091
+ } });
83092
+ return null;
83093
+ }
83094
+ }
83095
+ };
83096
+ //#endregion
83097
+ //#region src/pipeline-analytics/retrain/retrain-collection-service.ts
83098
+ /** What each still outcome tells the operator. */
83099
+ var STILL_REFUSAL = {
83100
+ "no-recording": {
83101
+ reason: "still-unavailable",
83102
+ detail: "this camera is not recording, so there is no footage to take a frame from"
83103
+ },
83104
+ "no-footage": {
83105
+ reason: "still-unavailable",
83106
+ detail: "no recorded footage covers that instant (a gap, or already pruned)"
83107
+ },
83108
+ "decode-failed": {
83109
+ reason: "still-unavailable",
83110
+ detail: "the footage is there but the frame could not be decoded"
83111
+ },
83112
+ busy: {
83113
+ reason: "still-busy",
83114
+ detail: "the recorder is already decoding other frames — try again in a moment"
83115
+ }
83116
+ };
83117
+ var RetrainCollectionService = class {
83118
+ deps;
83119
+ constructor(deps) {
83120
+ this.deps = deps;
83121
+ }
83122
+ create(input) {
83123
+ return this.deps.collections.create(input);
83124
+ }
83125
+ list() {
83126
+ return this.deps.collections.list();
83127
+ }
83128
+ rename(input) {
83129
+ return this.deps.collections.rename(input);
83130
+ }
82608
83131
  /**
82609
- * Stub until still ingest lands. Validates the collection exists, then
82610
- * refuses with a named reason so callers can wire UI against a stable shape.
83132
+ * Delete a collection. Its recording stills that no other collection holds
83133
+ * go with it (annotations first); track copies always stay.
82611
83134
  */
82612
- async addRetrainFrameFromRecording(input) {
82613
- if (await this.getById(input.collectionId) === null) {
82614
- this.logger.info("retrain collection addFrame refused: unknown-collection", {
82615
- tags: { deviceId: input.deviceId },
83135
+ async delete(input) {
83136
+ const result = await this.deps.collections.delete(input.id);
83137
+ if (!result.deleted) return {
83138
+ deleted: false,
83139
+ removedFrames: 0
83140
+ };
83141
+ return {
83142
+ deleted: true,
83143
+ removedFrames: await this.dropOrphanStills(result.memberFrameIds)
83144
+ };
83145
+ }
83146
+ /**
83147
+ * Decode the frame at `epochMs` from the camera's recording and add it to
83148
+ * the collection. The moment already in the dataset is reused without a
83149
+ * decode; every refusal is named and writes nothing.
83150
+ */
83151
+ async addFrameFromRecording(input) {
83152
+ const tags = { deviceId: input.deviceId };
83153
+ if (await this.deps.collections.getById(input.collectionId) === null) {
83154
+ this.deps.logger.info("retrain collection add refused: unknown collection", {
83155
+ tags,
82616
83156
  meta: {
82617
83157
  collectionId: input.collectionId,
82618
83158
  epochMs: input.epochMs
@@ -82623,42 +83163,517 @@ var RetrainCollectionStore = class {
82623
83163
  reason: "unknown-collection"
82624
83164
  };
82625
83165
  }
82626
- this.logger.info("retrain collection addFrame stub: still-capture-unimplemented", {
82627
- tags: { deviceId: input.deviceId },
83166
+ const stored = await this.storeStill(input.deviceId, input.epochMs);
83167
+ if (stored.kind === "refused") {
83168
+ this.deps.logger.info("retrain collection add refused", {
83169
+ tags,
83170
+ meta: {
83171
+ collectionId: input.collectionId,
83172
+ epochMs: input.epochMs,
83173
+ reason: stored.reason
83174
+ }
83175
+ });
83176
+ return stored;
83177
+ }
83178
+ await this.deps.collections.addFrame({
83179
+ collectionId: input.collectionId,
83180
+ frameId: stored.frame.frameId,
83181
+ deviceId: input.deviceId
83182
+ });
83183
+ this.deps.logger.info("retrain frame added to a collection from the recording", {
83184
+ tags,
82628
83185
  meta: {
82629
83186
  collectionId: input.collectionId,
82630
- epochMs: input.epochMs
83187
+ frameId: stored.frame.frameId,
83188
+ epochMs: input.epochMs,
83189
+ reused: stored.reused
82631
83190
  }
82632
83191
  });
83192
+ const [wire] = await this.deps.retrain.toWireFrames([stored.frame]);
83193
+ if (wire === void 0) throw new Error("retrain: a stored still produced no wire frame");
83194
+ return {
83195
+ kind: "added",
83196
+ collectionId: input.collectionId,
83197
+ frame: wire,
83198
+ reused: stored.reused
83199
+ };
83200
+ }
83201
+ /** Add frames already in the dataset (track copies or stills) to a collection. */
83202
+ async addFrames(input) {
83203
+ if (await this.deps.collections.getById(input.collectionId) === null) return {
83204
+ added: 0,
83205
+ alreadyMember: 0,
83206
+ unknownFrameIds: [...input.frameIds]
83207
+ };
83208
+ const frames = await this.deps.frames.getMany(input.frameIds);
83209
+ const known = new Set(frames.map((f) => f.frameId));
83210
+ let added = 0;
83211
+ let alreadyMember = 0;
83212
+ for (const frame of frames) if (await this.deps.collections.addFrame({
83213
+ collectionId: input.collectionId,
83214
+ frameId: frame.frameId,
83215
+ deviceId: frame.deviceId
83216
+ })) added += 1;
83217
+ else alreadyMember += 1;
83218
+ return {
83219
+ added,
83220
+ alreadyMember,
83221
+ unknownFrameIds: input.frameIds.filter((id) => !known.has(id))
83222
+ };
83223
+ }
83224
+ /**
83225
+ * Remove a frame from a collection. A recording still left in no collection
83226
+ * is deleted with its annotations; a track copy is never deleted here.
83227
+ */
83228
+ async removeFrame(input) {
83229
+ if (!await this.deps.collections.removeFrame(input)) return {
83230
+ removed: false,
83231
+ deletedFrame: false
83232
+ };
82633
83233
  return {
83234
+ removed: true,
83235
+ deletedFrame: await this.dropOrphanStills([input.frameId]) > 0
83236
+ };
83237
+ }
83238
+ /** The frames of a collection, as the canvas lists them. */
83239
+ async listFrames(input) {
83240
+ const ids = await this.deps.collections.listFrameIds(input.collectionId);
83241
+ return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
83242
+ }
83243
+ async storeStill(deviceId, epochMs) {
83244
+ const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
83245
+ if (existing !== null) return {
83246
+ kind: "stored",
83247
+ frame: existing,
83248
+ reused: true
83249
+ };
83250
+ const still = await this.deps.captureStill(deviceId, epochMs);
83251
+ if (still.outcome !== "ok" || still.jpeg === void 0) {
83252
+ const refusal = still.outcome === "ok" ? STILL_REFUSAL["decode-failed"] : STILL_REFUSAL[still.outcome];
83253
+ return {
83254
+ kind: "refused",
83255
+ reason: refusal.reason,
83256
+ detail: refusal.detail
83257
+ };
83258
+ }
83259
+ const stored = await this.deps.frames.storeRecordingStill({
83260
+ deviceId,
83261
+ epochMs,
83262
+ bytes: still.jpeg
83263
+ });
83264
+ return stored.kind === "copied" ? {
83265
+ kind: "stored",
83266
+ frame: stored.frame,
83267
+ reused: stored.reused
83268
+ } : {
82634
83269
  kind: "refused",
82635
- reason: "still-capture-unimplemented",
82636
- detail: "Still ingest lands in plan Task 3"
83270
+ reason: stored.reason
82637
83271
  };
82638
83272
  }
82639
- async findByNameKey(nameKey) {
82640
- try {
82641
- const row = (await this.store.query.query({
82642
- collection: RETRAIN_COLLECTIONS_COLLECTION,
82643
- filter: {
82644
- where: { nameKey },
82645
- limit: 1
83273
+ /** Delete the recording stills among `frameIds` that no collection holds any more. */
83274
+ async dropOrphanStills(frameIds) {
83275
+ if (frameIds.length === 0) return 0;
83276
+ const memberships = await this.deps.collections.collectionsOf(frameIds);
83277
+ const orphans = (await this.deps.frames.getMany(frameIds)).filter((f) => f.origin === "recording" && (memberships.get(f.frameId)?.length ?? 0) === 0);
83278
+ for (const frame of orphans) await this.deps.retrain.removeFrame(frame);
83279
+ return orphans.length;
83280
+ }
83281
+ };
83282
+ //#endregion
83283
+ //#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
83284
+ var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
83285
+ var ALWAYS_CLOUD_KINDS = new Set([
83286
+ "openai",
83287
+ "anthropic",
83288
+ "google"
83289
+ ]);
83290
+ function isPrivateIpv4(host) {
83291
+ const parts = host.split(".").map((p) => Number.parseInt(p, 10));
83292
+ if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return false;
83293
+ const [a, b] = parts;
83294
+ return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127;
83295
+ }
83296
+ function isLocalHost(rawHost) {
83297
+ const host = rawHost.replace(/^\[|\]$/g, "").toLowerCase();
83298
+ if (host.includes(":")) return host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80");
83299
+ if (host === "localhost" || host.endsWith(".local") || host.endsWith(".lan")) return true;
83300
+ if (isPrivateIpv4(host)) return true;
83301
+ return !host.includes(".") && !host.includes(":");
83302
+ }
83303
+ /** Does sending a frame to this profile send it outside the house? */
83304
+ function llmProfileLeavesHouse(profile) {
83305
+ if (ALWAYS_LOCAL_KINDS.has(profile.kind)) return false;
83306
+ if (ALWAYS_CLOUD_KINDS.has(profile.kind)) return true;
83307
+ if (profile.baseUrl === void 0 || profile.baseUrl === "") return true;
83308
+ try {
83309
+ return !isLocalHost(new URL(profile.baseUrl).hostname);
83310
+ } catch {
83311
+ return true;
83312
+ }
83313
+ }
83314
+ /** At most this many boxes are sent for review — a prompt, not a dump. */
83315
+ var LLM_REVIEW_MAX_BOXES = 50;
83316
+ var VERDICTS = [
83317
+ "confirmed",
83318
+ "wrong-class",
83319
+ "not-an-object",
83320
+ "uncertain"
83321
+ ];
83322
+ var SYSTEM_PROMPT = [
83323
+ "You annotate frames from home security cameras to build an object-detection training set.",
83324
+ "Be precise and conservative: only report what is clearly visible.",
83325
+ "Coordinates are normalised to the full frame: x and y are the top-left corner,",
83326
+ "w and h the width and height, all between 0 and 1.",
83327
+ "Answer with JSON only, exactly in the requested schema, with no other text."
83328
+ ].join(" ");
83329
+ /** The JSON schema asked of the model (and enforced again by the parser). */
83330
+ function answerJsonSchema(classes) {
83331
+ return {
83332
+ type: "object",
83333
+ additionalProperties: false,
83334
+ required: ["objects", "reviews"],
83335
+ properties: {
83336
+ objects: {
83337
+ type: "array",
83338
+ items: {
83339
+ type: "object",
83340
+ additionalProperties: false,
83341
+ required: [
83342
+ "class",
83343
+ "box",
83344
+ "confidence",
83345
+ "reason"
83346
+ ],
83347
+ properties: {
83348
+ class: {
83349
+ type: "string",
83350
+ enum: [...classes]
83351
+ },
83352
+ label: { type: "string" },
83353
+ box: {
83354
+ type: "array",
83355
+ items: { type: "number" },
83356
+ minItems: 4,
83357
+ maxItems: 4
83358
+ },
83359
+ confidence: {
83360
+ type: "number",
83361
+ minimum: 0,
83362
+ maximum: 1
83363
+ },
83364
+ reason: { type: "string" }
83365
+ }
82646
83366
  }
82647
- }))[0];
82648
- if (row === void 0) return null;
83367
+ },
83368
+ reviews: {
83369
+ type: "array",
83370
+ items: {
83371
+ type: "object",
83372
+ additionalProperties: false,
83373
+ required: [
83374
+ "ref",
83375
+ "verdict",
83376
+ "reason"
83377
+ ],
83378
+ properties: {
83379
+ ref: { type: "string" },
83380
+ verdict: {
83381
+ type: "string",
83382
+ enum: [...VERDICTS]
83383
+ },
83384
+ suggestedClass: { type: "string" },
83385
+ reason: { type: "string" }
83386
+ }
83387
+ }
83388
+ }
83389
+ }
83390
+ };
83391
+ }
83392
+ var round = (n) => Math.round(n * 1e3) / 1e3;
83393
+ /** Build the request: target classes, the frame size, the boxes to review. */
83394
+ function buildLlmAssistRequest(input) {
83395
+ const boxes = input.boxes.slice(0, LLM_REVIEW_MAX_BOXES).map((b) => JSON.stringify({
83396
+ ref: b.ref,
83397
+ class: b.macroClass,
83398
+ ...b.label !== void 0 ? { label: b.label } : {},
83399
+ box: [
83400
+ round(b.bbox.x),
83401
+ round(b.bbox.y),
83402
+ round(b.bbox.w),
83403
+ round(b.bbox.h)
83404
+ ]
83405
+ }));
83406
+ return {
83407
+ system: SYSTEM_PROMPT,
83408
+ prompt: [
83409
+ `The image is one camera frame, ${String(input.width)}×${String(input.height)} pixels.`,
83410
+ `1. List every object of these classes you can see: ${input.classes.join(", ")}.`,
83411
+ " For each give class, an optional free-text label (e.g. \"van\", \"postman\"),",
83412
+ " box [x, y, w, h] normalised to the frame, confidence 0–1, and a short reason.",
83413
+ " Include small and partly hidden ones; do not include anything else.",
83414
+ boxes.length > 0 ? `2. Review these existing boxes. For each ref say "confirmed", "wrong-class" (with suggestedClass), "not-an-object" or "uncertain", with a short reason:\n${boxes.join("\n")}` : "2. There are no existing boxes to review: answer \"reviews\": [].",
83415
+ "Answer with JSON: {\"objects\": [...], \"reviews\": [...]}."
83416
+ ].join("\n"),
83417
+ jsonSchema: answerJsonSchema(input.classes)
83418
+ };
83419
+ }
83420
+ /** The whole answer: two arrays and nothing else. Items are checked one by one. */
83421
+ var AnswerSchema = strictObject({
83422
+ objects: array(unknown()),
83423
+ reviews: array(unknown())
83424
+ });
83425
+ var ObjectSchema = strictObject({
83426
+ class: string(),
83427
+ label: string().optional(),
83428
+ box: tuple([
83429
+ number(),
83430
+ number(),
83431
+ number(),
83432
+ number()
83433
+ ]),
83434
+ confidence: number().min(0).max(1),
83435
+ reason: string().max(500)
83436
+ });
83437
+ var ReviewSchema = strictObject({
83438
+ ref: string(),
83439
+ verdict: _enum(VERDICTS),
83440
+ suggestedClass: string().optional(),
83441
+ reason: string().max(500)
83442
+ });
83443
+ function frameBox(box) {
83444
+ const [x, y, w, h] = box;
83445
+ if (![
83446
+ x,
83447
+ y,
83448
+ w,
83449
+ h
83450
+ ].every(Number.isFinite)) return null;
83451
+ if (!(w > 0) || !(h > 0) || x < -.01 || y < -.01) return null;
83452
+ if (x + w > 1.01 || y + h > 1.01) return null;
83453
+ const cx = Math.max(0, x);
83454
+ const cy = Math.max(0, y);
83455
+ return {
83456
+ x: cx,
83457
+ y: cy,
83458
+ w: Math.min(w, 1 - cx),
83459
+ h: Math.min(h, 1 - cy)
83460
+ };
83461
+ }
83462
+ /** Strip a Markdown code fence a model wrapped its JSON in. */
83463
+ function unfence(text) {
83464
+ const trimmed = text.trim();
83465
+ const fence = /^```(?:json)?\s*\n([\s\S]*?)\n?```$/i.exec(trimmed);
83466
+ return fence?.[1] !== void 0 ? fence[1].trim() : trimmed;
83467
+ }
83468
+ /** A free-text label, never a macro word (the tier rule). */
83469
+ function usableLabel(label) {
83470
+ const trimmed = label?.trim();
83471
+ if (trimmed === void 0 || trimmed === "") return void 0;
83472
+ return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
83473
+ }
83474
+ /** Parse the model's answer. See the module header for the rules. */
83475
+ function parseLlmAssistAnswer(text, expected) {
83476
+ let raw;
83477
+ try {
83478
+ raw = JSON.parse(unfence(text));
83479
+ } catch {
83480
+ return {
83481
+ kind: "malformed",
83482
+ detail: "the answer is not JSON"
83483
+ };
83484
+ }
83485
+ const answer = AnswerSchema.safeParse(raw);
83486
+ if (!answer.success) return {
83487
+ kind: "malformed",
83488
+ detail: "the answer is not {\"objects\": [...], \"reviews\": [...]}"
83489
+ };
83490
+ const asked = new Set(expected.classes);
83491
+ let rejected = 0;
83492
+ const objects = [];
83493
+ for (const item of answer.data.objects) {
83494
+ const parsed = ObjectSchema.safeParse(item);
83495
+ const cls = parsed.success ? parsed.data.class.trim().toLowerCase() : "";
83496
+ const bbox = parsed.success ? frameBox(parsed.data.box) : null;
83497
+ if (!parsed.success || bbox === null || !asked.has(cls) || !isRetrainMacroClass(cls)) {
83498
+ rejected += 1;
83499
+ continue;
83500
+ }
83501
+ const label = usableLabel(parsed.data.label);
83502
+ objects.push({
83503
+ macroClass: cls,
83504
+ ...label !== void 0 ? { label } : {},
83505
+ bbox,
83506
+ confidence: parsed.data.confidence,
83507
+ reason: parsed.data.reason
83508
+ });
83509
+ }
83510
+ const reviews = [];
83511
+ for (const item of answer.data.reviews) {
83512
+ const parsed = ReviewSchema.safeParse(item);
83513
+ if (!parsed.success || !expected.refs.has(parsed.data.ref)) {
83514
+ rejected += 1;
83515
+ continue;
83516
+ }
83517
+ const suggested = parsed.data.suggestedClass?.trim().toLowerCase();
83518
+ reviews.push({
83519
+ ref: parsed.data.ref,
83520
+ verdict: parsed.data.verdict,
83521
+ ...suggested !== void 0 && isRetrainMacroClass(suggested) ? { suggestedClass: suggested } : {},
83522
+ reason: parsed.data.reason
83523
+ });
83524
+ }
83525
+ return {
83526
+ kind: "parsed",
83527
+ objects,
83528
+ reviews,
83529
+ rejected
83530
+ };
83531
+ }
83532
+ //#endregion
83533
+ //#region src/pipeline-analytics/retrain/retrain-llm-service.ts
83534
+ /** The usage tag every AI-assist call is counted under (`llm.getUsage`). */
83535
+ var RETRAIN_LLM_CONSUMER = "retrain-assist";
83536
+ /** One frame, one answer: a cold local vision model can take a minute. */
83537
+ var RETRAIN_LLM_TIMEOUT_MS = 15e4;
83538
+ var RetrainLlmAssistService = class {
83539
+ deps;
83540
+ constructor(deps) {
83541
+ this.deps = deps;
83542
+ }
83543
+ /** The enabled, vision-capable profiles — a text-only one would be refused. */
83544
+ async listProfiles() {
83545
+ return (await this.deps.llm.listProfiles()).filter((p) => p.enabled && p.supportsVision).map((p) => ({
83546
+ id: p.id,
83547
+ name: p.name,
83548
+ model: p.model,
83549
+ kind: p.kind,
83550
+ leavesHouse: llmProfileLeavesHouse(p)
83551
+ }));
83552
+ }
83553
+ async propose(input) {
83554
+ const tags = { deviceId: input.deviceId };
83555
+ const frame = await this.deps.frames.get(input.frameId);
83556
+ if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
83557
+ kind: "refused",
83558
+ reason: "unknown-frame"
83559
+ };
83560
+ const profile = (await this.deps.llm.listProfiles()).find((p) => p.id === input.profileId);
83561
+ if (profile === void 0) return {
83562
+ kind: "refused",
83563
+ reason: "unknown-profile"
83564
+ };
83565
+ if (!profile.enabled) return {
83566
+ kind: "refused",
83567
+ reason: "profile-disabled"
83568
+ };
83569
+ if (!profile.supportsVision) return {
83570
+ kind: "refused",
83571
+ reason: "no-vision"
83572
+ };
83573
+ const leavesHouse = llmProfileLeavesHouse(profile);
83574
+ if (leavesHouse && !input.allowCloud) {
83575
+ this.deps.logger.warn("retrain AI assist refused: a cloud profile without confirmation", {
83576
+ tags,
83577
+ meta: {
83578
+ frameId: input.frameId,
83579
+ profileId: profile.id
83580
+ }
83581
+ });
82649
83582
  return {
82650
- id: row.id,
82651
- name: typeof row.data["name"] === "string" ? row.data["name"] : "",
82652
- createdAt: Number(row.data["createdAt"] ?? 0),
82653
- frameCount: 0
83583
+ kind: "refused",
83584
+ reason: "cloud-not-confirmed",
83585
+ detail: "this profile sends the frame outside the house; confirm the run first"
82654
83586
  };
82655
- } catch (err) {
82656
- this.logger.warn("retrain collection findByNameKey failed", { meta: {
82657
- nameKey,
82658
- error: String(err)
82659
- } });
82660
- return null;
82661
83587
  }
83588
+ const bytes = await this.deps.frames.readFrame(frame);
83589
+ if (bytes === null) return {
83590
+ kind: "refused",
83591
+ reason: "unreadable-frame"
83592
+ };
83593
+ const request = buildLlmAssistRequest({
83594
+ classes: input.classes,
83595
+ width: frame.width,
83596
+ height: frame.height,
83597
+ boxes: input.boxes
83598
+ });
83599
+ const answer = await this.deps.llm.generateVision({
83600
+ profileId: profile.id,
83601
+ system: request.system,
83602
+ prompt: request.prompt,
83603
+ jsonSchema: request.jsonSchema,
83604
+ image: bytes
83605
+ });
83606
+ if (!answer.ok) {
83607
+ this.deps.logger.warn("retrain AI assist: the model call failed", {
83608
+ tags,
83609
+ meta: {
83610
+ frameId: input.frameId,
83611
+ profileId: profile.id,
83612
+ code: answer.code
83613
+ }
83614
+ });
83615
+ return {
83616
+ kind: "refused",
83617
+ reason: "llm-error",
83618
+ detail: `${answer.code}: ${answer.message}`
83619
+ };
83620
+ }
83621
+ const parsed = parseLlmAssistAnswer(answer.text, {
83622
+ classes: input.classes,
83623
+ refs: new Set(input.boxes.map((b) => b.ref))
83624
+ });
83625
+ if (parsed.kind === "malformed") {
83626
+ this.deps.logger.warn("retrain AI assist: the answer was refused as malformed", {
83627
+ tags,
83628
+ meta: {
83629
+ frameId: input.frameId,
83630
+ profileId: profile.id,
83631
+ model: answer.model,
83632
+ detail: parsed.detail,
83633
+ answerChars: answer.text.length
83634
+ }
83635
+ });
83636
+ return {
83637
+ kind: "refused",
83638
+ reason: "malformed-answer",
83639
+ detail: parsed.detail
83640
+ };
83641
+ }
83642
+ this.deps.logger.info("retrain AI assist proposed boxes", {
83643
+ tags,
83644
+ meta: {
83645
+ frameId: input.frameId,
83646
+ profileId: profile.id,
83647
+ model: answer.model,
83648
+ leftHouse: leavesHouse,
83649
+ proposals: parsed.objects.length,
83650
+ reviews: parsed.reviews.length,
83651
+ rejected: parsed.rejected
83652
+ }
83653
+ });
83654
+ return {
83655
+ kind: "proposed",
83656
+ profileId: profile.id,
83657
+ model: answer.model,
83658
+ leftHouse: leavesHouse,
83659
+ proposals: parsed.objects.map((o) => ({
83660
+ bbox: o.bbox,
83661
+ macroClass: o.macroClass,
83662
+ ...o.label !== void 0 ? { label: o.label } : {},
83663
+ kind: "subject",
83664
+ source: "llm",
83665
+ assistModelId: answer.model,
83666
+ assistScore: o.confidence,
83667
+ reason: o.reason
83668
+ })),
83669
+ reviews: parsed.reviews.map((r) => ({
83670
+ ref: r.ref,
83671
+ verdict: r.verdict,
83672
+ ...r.suggestedClass !== void 0 ? { suggestedClass: r.suggestedClass } : {},
83673
+ reason: r.reason
83674
+ })),
83675
+ rejected: parsed.rejected
83676
+ };
82662
83677
  }
82663
83678
  };
82664
83679
  //#endregion
@@ -83138,12 +84153,18 @@ var RETRAIN_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
83138
84153
  /** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
83139
84154
  * the trainer resizes per model, so this only bounds the upload. */
83140
84155
  var RETRAIN_CROP_MAX_SIDE = 256;
83141
- /** Manifest schema id. v2 = capturedAt / complete / lighting / layout. */
83142
- var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v2";
83143
- /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`. */
84156
+ /**
84157
+ * Manifest schema id. v2 = capturedAt / complete / lighting / layout.
84158
+ * v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
84159
+ * `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
84160
+ */
84161
+ var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
84162
+ /** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
84163
+ * or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
83144
84164
  function retrainFramePath(frame) {
83145
84165
  const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
83146
- return `frames/${frame.deviceId}/${frame.trackId}/${leaf}.jpg`;
84166
+ const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
84167
+ return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
83147
84168
  }
83148
84169
  /** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
83149
84170
  function retrainCropPath(macroClass, annotationId) {
@@ -83153,13 +84174,15 @@ function retrainCropPath(macroClass, annotationId) {
83153
84174
  * Narrow the planned frames to a filter. Pure. Every field narrows; an empty
83154
84175
  * filter keeps everything. Order is the input's.
83155
84176
  */
83156
- function filterRetrainExport(frames, annotationsByFrame, filter) {
84177
+ function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
83157
84178
  const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
84179
+ const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
83158
84180
  const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
83159
84181
  return frames.filter((frame) => {
83160
84182
  if (wanted !== null && !wanted.has(frame.frameId)) return false;
83161
84183
  if (devices !== null && !devices.has(frame.deviceId)) return false;
83162
84184
  if (filter.completeOnly === true && !frame.complete) return false;
84185
+ if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
83163
84186
  if (filter.batchId !== void 0) {
83164
84187
  if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
83165
84188
  }
@@ -83214,7 +84237,10 @@ function buildRetrainManifest(input) {
83214
84237
  frameId: frame.frameId,
83215
84238
  path: retrainFramePath(frame),
83216
84239
  deviceId: frame.deviceId,
83217
- trackId: frame.trackId,
84240
+ origin: frame.origin,
84241
+ ...frame.origin === "track" ? { trackId: frame.trackId } : {},
84242
+ ...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
84243
+ collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
83218
84244
  width: frame.width,
83219
84245
  height: frame.height,
83220
84246
  sizeBytes: frame.sizeBytes,
@@ -83253,11 +84279,13 @@ function parseRetrainExportRequest(url) {
83253
84279
  const params = new URLSearchParams(query);
83254
84280
  const deviceIds = parseRetrainDeviceIds(url);
83255
84281
  const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
84282
+ const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
83256
84283
  const batchId = params.get("batchId")?.trim() ?? "";
83257
84284
  return {
83258
84285
  filter: {
83259
84286
  ...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
83260
84287
  ...frameIds.length > 0 ? { frameIds } : {},
84288
+ ...collectionIds.length > 0 ? { collectionIds } : {},
83261
84289
  ...batchId.length > 0 ? { batchId } : {},
83262
84290
  ...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
83263
84291
  },
@@ -83270,6 +84298,7 @@ function retrainExportQuery(request) {
83270
84298
  const { filter } = request;
83271
84299
  if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
83272
84300
  if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
84301
+ if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
83273
84302
  if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
83274
84303
  if (filter.completeOnly === true) params.set("completeOnly", "1");
83275
84304
  if (request.layout === "crops") params.set("layout", "crops");
@@ -83344,6 +84373,178 @@ function createRetrainExportHandler(deps) {
83344
84373
  };
83345
84374
  }
83346
84375
  //#endregion
84376
+ //#region src/pipeline-analytics/retrain/retrain-pipeline-boxes.ts
84377
+ /**
84378
+ * The boxes the PIPELINE drew on a copied frame — Step 1 of the training
84379
+ * screen.
84380
+ *
84381
+ * A correction has to start from the decision being corrected. Proposing with
84382
+ * a fresh model run answers a different question ("what would a model say
84383
+ * now"), and the frame the operator is fixing is precisely the one where the
84384
+ * live answer was wrong — a re-run that happens to be right hides the phantom
84385
+ * the operator came to remove. So the canvas opens with what the track store
84386
+ * says the pipeline had on that frame, and the operator accepts, corrects,
84387
+ * relabels or deletes it.
84388
+ *
84389
+ * ## Where a box comes from, best first
84390
+ *
84391
+ * 1. **The subject box recorded ON the source media row** (D510). Exact by
84392
+ * construction: the box was measured on the frame the blob shows. Only the
84393
+ * copy's own track can have one.
84394
+ * 2. **The track position nearest the capture time**, within
84395
+ * {@link PIPELINE_BOX_TOLERANCE_MS}. Positions are frame-rate, so the
84396
+ * nearest one is normally the same detection; the delta is reported so a
84397
+ * reviewer can see how far it was.
84398
+ *
84399
+ * Every OTHER track alive on the camera at that instant contributes its own
84400
+ * nearest position the same way — a frame with three people is three boxes,
84401
+ * and a recording still (no track of its own) gets everything the pipeline
84402
+ * was tracking at that moment, which is exactly what a missed-object review
84403
+ * needs to see is NOT there.
84404
+ *
84405
+ * Nothing here does I/O. The caller reads the tracks and the subject box.
84406
+ */
84407
+ /** How far from the capture time a position may be and still be "this frame".
84408
+ * Positions are recorded at the analysis rate (several per second), so half
84409
+ * a second is a generous match and still not a different moment. */
84410
+ var PIPELINE_BOX_TOLERANCE_MS = 500;
84411
+ /**
84412
+ * How far back a track may have STARTED and still be alive at the frame. The
84413
+ * track store windows on `firstSeen`, so a track born before this is not
84414
+ * asked about — a vehicle parked for hours is the case it misses, and the
84415
+ * operator draws that box by hand.
84416
+ */
84417
+ var PIPELINE_TRACK_LOOKBACK_MS = 30 * 6e4;
84418
+ /** At most this many tracks of one camera are read around one frame. */
84419
+ var PIPELINE_TRACK_AROUND_LIMIT = 200;
84420
+ /** The least-wrong macro tier for a class the macro map does not know. The
84421
+ * operator corrects it; what must not happen is the raw string being
84422
+ * promoted into the macro tier. */
84423
+ var FALLBACK_MACRO_CLASS = "person";
84424
+ /**
84425
+ * Split a detector's class into the two tiers.
84426
+ *
84427
+ * A macro class stays in the macro tier; anything else maps through the COCO
84428
+ * macro map, and the class itself becomes the LABEL. A macro word is never a
84429
+ * label, in any casing — the tier rule the annotation write enforces.
84430
+ */
84431
+ function toRetrainClassTiers(className, label) {
84432
+ const cls = className.trim().toLowerCase();
84433
+ const usableLabel = (value) => {
84434
+ const trimmed = value?.trim();
84435
+ if (trimmed === void 0 || trimmed === "") return void 0;
84436
+ return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
84437
+ };
84438
+ if (isRetrainMacroClass(cls)) {
84439
+ const kept = usableLabel(label);
84440
+ return kept !== void 0 ? {
84441
+ macroClass: cls,
84442
+ label: kept
84443
+ } : { macroClass: cls };
84444
+ }
84445
+ const mapped = COCO_TO_MACRO.mapping[cls];
84446
+ const macroClass = mapped !== void 0 && isRetrainMacroClass(mapped) ? mapped : FALLBACK_MACRO_CLASS;
84447
+ const kept = usableLabel(className);
84448
+ return kept !== void 0 ? {
84449
+ macroClass,
84450
+ label: kept
84451
+ } : { macroClass };
84452
+ }
84453
+ /** A pixel box as a clamped normalised one, or `null` when nothing is left. */
84454
+ function normalise(box, frameWidth, frameHeight) {
84455
+ if (frameWidth === void 0 || frameHeight === void 0) return null;
84456
+ if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
84457
+ const x = clampSpan(box.x, box.w, frameWidth);
84458
+ const y = clampSpan(box.y, box.h, frameHeight);
84459
+ if (x === null || y === null) return null;
84460
+ return {
84461
+ x: x.start,
84462
+ y: y.start,
84463
+ w: x.size,
84464
+ h: y.size
84465
+ };
84466
+ }
84467
+ /** One axis of a pixel box, normalised and clamped into `[0,1]`. The size is
84468
+ * divided directly when nothing was clamped, so an exact box stays exact. */
84469
+ function clampSpan(start, size, extent) {
84470
+ const lo = start / extent;
84471
+ const hi = (start + size) / extent;
84472
+ const s = Math.max(0, lo);
84473
+ const n = lo >= 0 && hi <= 1 ? size / extent : Math.min(1, hi) - s;
84474
+ return n > 0 ? {
84475
+ start: s,
84476
+ size: n
84477
+ } : null;
84478
+ }
84479
+ function nearestPosition(track, at, tolerance) {
84480
+ let best = null;
84481
+ let bestDelta = Number.POSITIVE_INFINITY;
84482
+ for (const p of track.positions) {
84483
+ const delta = Math.abs(p.timestamp - at);
84484
+ if (delta < bestDelta) {
84485
+ best = p;
84486
+ bestDelta = delta;
84487
+ }
84488
+ }
84489
+ if (best === null || bestDelta > tolerance) return null;
84490
+ return {
84491
+ position: best,
84492
+ deltaMs: bestDelta
84493
+ };
84494
+ }
84495
+ function boxFor(track, bbox, basis, deltaMs, score) {
84496
+ const tiers = toRetrainClassTiers(track.className, track.label);
84497
+ const subLabel = track.subLabel?.trim();
84498
+ return {
84499
+ bbox,
84500
+ macroClass: tiers.macroClass,
84501
+ ...tiers.label !== void 0 ? { label: tiers.label } : {},
84502
+ ...subLabel !== void 0 && subLabel !== "" && !isRetrainMacroClass(subLabel.toLowerCase()) ? { subLabel } : {},
84503
+ ...score !== void 0 ? { score } : {},
84504
+ ...track.modelId !== void 0 ? { modelId: track.modelId } : {},
84505
+ trackId: track.trackId,
84506
+ basis,
84507
+ deltaMs
84508
+ };
84509
+ }
84510
+ /** The own track's box: the recorded subject box first, the nearest position second. */
84511
+ function ownBox(input, tolerance) {
84512
+ const track = input.ownTrack;
84513
+ if (track === null) return null;
84514
+ const at = input.frame.capturedAt;
84515
+ const nearest = at !== void 0 ? nearestPosition(track, at, tolerance) : null;
84516
+ if (input.ownSubjectBox !== null) {
84517
+ const recorded = normalise(input.ownSubjectBox, input.ownSubjectBox.frameWidth, input.ownSubjectBox.frameHeight);
84518
+ if (recorded !== null) return boxFor(track, recorded, "subject-box", 0, nearest?.position.score);
84519
+ }
84520
+ if (nearest === null) return null;
84521
+ const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
84522
+ return placed === null ? null : boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score);
84523
+ }
84524
+ /**
84525
+ * Every box the pipeline had on this frame, own track first. Pure.
84526
+ */
84527
+ function pipelineBoxesForFrame(input) {
84528
+ const tolerance = input.toleranceMs ?? PIPELINE_BOX_TOLERANCE_MS;
84529
+ const out = [];
84530
+ const own = ownBox(input, tolerance);
84531
+ if (own !== null) out.push(own);
84532
+ const at = input.frame.capturedAt;
84533
+ if (at === void 0) return out;
84534
+ const ownId = input.frame.trackId;
84535
+ const seen = new Set(ownId !== void 0 ? [ownId] : []);
84536
+ for (const track of input.neighbours) {
84537
+ if (seen.has(track.trackId) || track.deviceId !== input.frame.deviceId) continue;
84538
+ seen.add(track.trackId);
84539
+ const nearest = nearestPosition(track, at, tolerance);
84540
+ if (nearest === null) continue;
84541
+ const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
84542
+ if (placed === null) continue;
84543
+ out.push(boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score));
84544
+ }
84545
+ return out;
84546
+ }
84547
+ //#endregion
83347
84548
  //#region src/pipeline-analytics/retrain/retrain-assist.ts
83348
84549
  /** The whole frame, as a normalised rectangle. */
83349
84550
  var WHOLE_FRAME = {
@@ -83414,12 +84615,11 @@ function mapAssistDetails(input) {
83414
84615
  withoutBox += 1;
83415
84616
  continue;
83416
84617
  }
83417
- const macroClass = isRetrainMacroClass(d.className) ? d.className : FALLBACK_MACRO_CLASS;
83418
- const label = isRetrainMacroClass(d.className) ? d.label : d.className;
84618
+ const tiers = toRetrainClassTiers(d.className, d.label);
83419
84619
  proposals.push({
83420
84620
  bbox,
83421
- macroClass,
83422
- ...label !== void 0 && label !== "" ? { label } : {},
84621
+ macroClass: tiers.macroClass,
84622
+ ...tiers.label !== void 0 ? { label: tiers.label } : {},
83423
84623
  score: d.score,
83424
84624
  modelId
83425
84625
  });
@@ -83430,16 +84630,6 @@ function mapAssistDetails(input) {
83430
84630
  withoutBox
83431
84631
  };
83432
84632
  }
83433
- /**
83434
- * Macro tier for a detector class we do not carry as a macro class.
83435
- *
83436
- * The class itself becomes the LABEL (see above) and the macro tier gets a
83437
- * value the operator will almost certainly correct — which is fine, because it
83438
- * is a PROPOSAL and correcting it is the job. What must not happen is the
83439
- * detector's own string being promoted into the macro tier: that is how
83440
- * `postman` becomes a class with one example in it.
83441
- */
83442
- var FALLBACK_MACRO_CLASS = "person";
83443
84633
  /** A proposal, as a draft annotation ready to store if the operator accepts it. */
83444
84634
  function proposalToDraft(proposal, owner) {
83445
84635
  return {
@@ -83648,12 +84838,18 @@ async function restageRetrainTrack(deps, input) {
83648
84838
  var DEFAULT_STAGING_LIMIT = 100;
83649
84839
  /** A macro class that failed validation — the write is refused, never coerced. */
83650
84840
  var RetrainAnnotationError = class extends Error {};
84841
+ /** A store frame on the wire. A recording still carries no track and no
84842
+ * source media row, and says so by leaving both out. */
83651
84843
  function toWireFrame(f) {
83652
84844
  return {
83653
84845
  frameId: f.frameId,
83654
84846
  deviceId: f.deviceId,
83655
- trackId: f.trackId,
83656
- sourceMediaId: f.sourceMediaId,
84847
+ origin: f.origin,
84848
+ ...f.origin === "track" ? {
84849
+ trackId: f.trackId,
84850
+ sourceMediaId: f.sourceMediaId
84851
+ } : {},
84852
+ ...f.epochMs !== void 0 ? { epochMs: f.epochMs } : {},
83657
84853
  sourceKind: f.sourceKind,
83658
84854
  sizeBytes: f.sizeBytes,
83659
84855
  width: f.width,
@@ -83666,7 +84862,7 @@ function toWireFrame(f) {
83666
84862
  function toWireAnnotation(a) {
83667
84863
  return {
83668
84864
  id: a.id,
83669
- trackId: a.trackId,
84865
+ ...a.trackId !== "" ? { trackId: a.trackId } : {},
83670
84866
  deviceId: a.deviceId,
83671
84867
  mediaKey: a.mediaKey,
83672
84868
  bbox: a.bbox,
@@ -83733,11 +84929,116 @@ var RetrainService = class {
83733
84929
  if (candidates.length === 0) await this.logNoAnnotatableFrame(trackId, media, copies.length);
83734
84930
  return {
83735
84931
  candidates,
83736
- copies: copies.map(toWireFrame),
84932
+ copies: await this.toWireFrames(copies),
83737
84933
  ...autoPick !== null ? { autoPickMediaId: autoPick.mediaId } : {}
83738
84934
  };
83739
84935
  }
83740
84936
  /**
84937
+ * The wire frames, each with the boxes the pipeline drew on it (Step 1) and
84938
+ * the collections it belongs to.
84939
+ *
84940
+ * Index reads only — the track rows and the media rows' subject boxes, no
84941
+ * blob and no inference. One read per distinct track and per distinct
84942
+ * instant, not per box. A failed read degrades to fewer boxes, logged, and
84943
+ * never to a failed listing: the frames are the operator's work and must
84944
+ * stay reachable when the evidence is not.
84945
+ */
84946
+ async toWireFrames(copies) {
84947
+ const collections = this.deps.memberships !== void 0 && copies.length > 0 ? await this.deps.memberships.collectionsOf(copies.map((c) => c.frameId)).catch((err) => {
84948
+ this.deps.logger.warn("retrain frames: collection membership unreadable", { meta: {
84949
+ frames: copies.length,
84950
+ error: String(err)
84951
+ } });
84952
+ return /* @__PURE__ */ new Map();
84953
+ }) : null;
84954
+ const evidence = this.deps.pipelineEvidence;
84955
+ const ownTracks = /* @__PURE__ */ new Map();
84956
+ const subjectBoxes = /* @__PURE__ */ new Map();
84957
+ const out = [];
84958
+ for (const copy of copies) {
84959
+ const wire = toWireFrame(copy);
84960
+ const collectionIds = collections?.get(copy.frameId);
84961
+ const withCollections = collections !== null ? {
84962
+ ...wire,
84963
+ collectionIds: collectionIds ?? []
84964
+ } : wire;
84965
+ if (evidence === void 0) {
84966
+ out.push(withCollections);
84967
+ continue;
84968
+ }
84969
+ const trackId = copy.origin === "track" ? copy.trackId : "";
84970
+ if (trackId !== "" && !ownTracks.has(trackId)) {
84971
+ ownTracks.set(trackId, await this.deps.tracks.getPersistedByTrackId(trackId).catch(() => null));
84972
+ subjectBoxes.set(trackId, await this.readSubjectBoxes(evidence, trackId, copy.deviceId));
84973
+ }
84974
+ const neighbours = copy.capturedAt !== void 0 ? await this.readTracksAround(evidence, copy.deviceId, copy.capturedAt) : [];
84975
+ const pipelineBoxes = pipelineBoxesForFrame({
84976
+ frame: {
84977
+ deviceId: copy.deviceId,
84978
+ ...trackId !== "" ? { trackId } : {},
84979
+ ...copy.capturedAt !== void 0 ? { capturedAt: copy.capturedAt } : {}
84980
+ },
84981
+ ownTrack: trackId !== "" ? ownTracks.get(trackId) ?? null : null,
84982
+ ownSubjectBox: trackId !== "" ? subjectBoxes.get(trackId)?.get(copy.sourceMediaId) ?? null : null,
84983
+ neighbours
84984
+ });
84985
+ out.push({
84986
+ ...withCollections,
84987
+ pipelineBoxes
84988
+ });
84989
+ }
84990
+ return out;
84991
+ }
84992
+ /**
84993
+ * The frame a frame-scoped call names, or a refusal. The frame is the
84994
+ * authority: its camera must be the caller's, and a `trackId`, when the
84995
+ * caller sends one, must be the frame's own — a recording still has none.
84996
+ */
84997
+ async requireFrame(ref, action) {
84998
+ const frame = await this.deps.frames.get(ref.frameId);
84999
+ const trackMismatch = frame !== null && ref.trackId !== void 0 && (frame.origin !== "track" || frame.trackId !== ref.trackId);
85000
+ if (frame === null || frame.deviceId !== ref.deviceId || trackMismatch) {
85001
+ this.deps.logger.warn(`retrain ${action} refused: the frame does not belong to that camera or track`, {
85002
+ tags: { deviceId: ref.deviceId },
85003
+ meta: {
85004
+ frameId: ref.frameId,
85005
+ ...ref.trackId !== void 0 ? { trackId: ref.trackId } : {}
85006
+ }
85007
+ });
85008
+ throw new RetrainAnnotationError(frame === null ? `retrain frame ${ref.frameId} does not exist — select it before annotating it` : `retrain frame ${ref.frameId} does not belong to ${ref.trackId !== void 0 ? `track ${ref.trackId}` : `camera ${String(ref.deviceId)}`}`);
85009
+ }
85010
+ return frame;
85011
+ }
85012
+ async readSubjectBoxes(evidence, trackId, deviceId) {
85013
+ try {
85014
+ const rows = await evidence.subjectBoxes(trackId, deviceId);
85015
+ return new Map(rows.map((r) => [r.mediaId, r.subjectBox]));
85016
+ } catch (err) {
85017
+ this.deps.logger.warn("retrain pipeline boxes: subject boxes unreadable", {
85018
+ tags: { deviceId },
85019
+ meta: {
85020
+ trackId,
85021
+ error: String(err)
85022
+ }
85023
+ });
85024
+ return /* @__PURE__ */ new Map();
85025
+ }
85026
+ }
85027
+ async readTracksAround(evidence, deviceId, atMs) {
85028
+ try {
85029
+ return await evidence.tracksAround(deviceId, atMs);
85030
+ } catch (err) {
85031
+ this.deps.logger.warn("retrain pipeline boxes: tracks around the frame unreadable", {
85032
+ tags: { deviceId },
85033
+ meta: {
85034
+ atMs,
85035
+ error: String(err)
85036
+ }
85037
+ });
85038
+ return [];
85039
+ }
85040
+ }
85041
+ /**
83741
85042
  * A track with no annotatable frame is a REAL state, and the page has to say
83742
85043
  * so rather than render an empty strip that reads as a slow load. Two causes,
83743
85044
  * named apart: the whole-frame media AGED OUT, or the track only ever held
@@ -83790,23 +85091,13 @@ var RetrainService = class {
83790
85091
  * track the caller names: a frame of another track is refused, loudly.
83791
85092
  */
83792
85093
  async setFrameComplete(input) {
83793
- const frame = await this.deps.frames.get(input.frameId);
83794
- if (frame === null || frame.trackId !== input.trackId || frame.deviceId !== input.deviceId) {
83795
- this.deps.logger.warn("retrain complete refused: the frame does not belong to that track", {
83796
- tags: { deviceId: input.deviceId },
83797
- meta: {
83798
- trackId: input.trackId,
83799
- frameId: input.frameId
83800
- }
83801
- });
83802
- throw new RetrainAnnotationError(`retrain frame ${input.frameId} does not belong to track ${input.trackId}`);
83803
- }
83804
- const updated = await this.deps.frames.setComplete(input.frameId, input.complete);
85094
+ const frame = await this.requireFrame(input, "complete");
85095
+ const updated = await this.deps.frames.setComplete(frame.frameId, input.complete);
83805
85096
  if (updated === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} vanished`);
83806
85097
  this.deps.logger.info("retrain frame completeness set", {
83807
85098
  tags: { deviceId: input.deviceId },
83808
85099
  meta: {
83809
- trackId: input.trackId,
85100
+ trackId: frame.trackId,
83810
85101
  frameId: input.frameId,
83811
85102
  complete: input.complete
83812
85103
  }
@@ -83814,36 +85105,41 @@ var RetrainService = class {
83814
85105
  return toWireFrame(updated);
83815
85106
  }
83816
85107
  /** Un-select: annotations first, then the copy. Never the other way round —
83817
- * an annotation whose frame is gone is unopenable and unfixable. */
85108
+ * an annotation whose frame is gone is unopenable and unfixable. The frame's
85109
+ * collection memberships go with it. */
83818
85110
  async deselectFrame(input) {
83819
85111
  const frame = await this.deps.frames.get(input.frameId);
83820
85112
  if (frame === null) {
83821
85113
  this.deps.logger.warn("retrain deselect: no such copied frame", {
83822
85114
  tags: { deviceId: input.deviceId },
83823
- meta: {
83824
- trackId: input.trackId,
83825
- frameId: input.frameId
83826
- }
85115
+ meta: { frameId: input.frameId }
83827
85116
  });
83828
85117
  return {
83829
85118
  removed: false,
83830
85119
  removedAnnotations: 0
83831
85120
  };
83832
85121
  }
83833
- const removedAnnotations = await this.deps.annotations.deleteForFrame(input.frameId);
85122
+ await this.requireFrame(input, "deselect");
85123
+ return {
85124
+ removed: true,
85125
+ removedAnnotations: await this.removeFrame(frame)
85126
+ };
85127
+ }
85128
+ /** Remove a frame from the dataset: annotations, memberships, then the copy. */
85129
+ async removeFrame(frame) {
85130
+ const removedAnnotations = await this.deps.annotations.deleteForFrame(frame.frameId);
85131
+ await this.deps.memberships?.forgetFrame(frame.frameId);
83834
85132
  await this.deps.frames.deleteFrame(frame);
83835
85133
  this.deps.logger.info("retrain frame removed from the dataset", {
83836
- tags: { deviceId: input.deviceId },
85134
+ tags: { deviceId: frame.deviceId },
83837
85135
  meta: {
83838
- trackId: input.trackId,
83839
- frameId: input.frameId,
85136
+ origin: frame.origin,
85137
+ ...frame.origin === "track" ? { trackId: frame.trackId } : {},
85138
+ frameId: frame.frameId,
83840
85139
  removedAnnotations
83841
85140
  }
83842
85141
  });
83843
- return {
83844
- removed: true,
83845
- removedAnnotations
83846
- };
85142
+ return removedAnnotations;
83847
85143
  }
83848
85144
  /** The copy's pixels plus its exact dimensions — the canvas needs both. */
83849
85145
  async getFrameImage(frameId) {
@@ -83883,7 +85179,7 @@ var RetrainService = class {
83883
85179
  };
83884
85180
  }
83885
85181
  const frame = await this.deps.frames.get(input.frameId);
83886
- if (frame === null) return {
85182
+ if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
83887
85183
  kind: "refused",
83888
85184
  reason: "unknown-frame"
83889
85185
  };
@@ -83938,8 +85234,8 @@ var RetrainService = class {
83938
85234
  }
83939
85235
  });
83940
85236
  const owner = {
83941
- trackId: input.trackId,
83942
- deviceId: input.deviceId,
85237
+ trackId: frame.trackId,
85238
+ deviceId: frame.deviceId,
83943
85239
  mediaKey: input.frameId
83944
85240
  };
83945
85241
  return {
@@ -83962,8 +85258,12 @@ var RetrainService = class {
83962
85258
  belowThreshold: mapping.belowThreshold
83963
85259
  };
83964
85260
  }
83965
- async listAnnotations(trackId) {
83966
- return (await this.deps.annotations.listByTrack(trackId)).map(toWireAnnotation);
85261
+ /** The annotations of a track, or of named frames (a recording still has
85262
+ * no track). One of the two must be given. */
85263
+ async listAnnotations(input) {
85264
+ if (input.trackId !== void 0 && input.trackId !== "") return (await this.deps.annotations.listByTrack(input.trackId)).map(toWireAnnotation);
85265
+ if (input.frameIds !== void 0) return (await this.deps.annotations.listByFrames(input.frameIds)).map(toWireAnnotation);
85266
+ throw new RetrainAnnotationError("listRetrainAnnotations needs a trackId or frameIds");
83967
85267
  }
83968
85268
  /**
83969
85269
  * Replace every annotation on one frame.
@@ -83975,15 +85275,15 @@ var RetrainService = class {
83975
85275
  * believes they stored.
83976
85276
  */
83977
85277
  async saveAnnotations(input) {
83978
- if (await this.deps.frames.get(input.frameId) === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} does not exist — select it before annotating it`);
85278
+ const frame = await this.requireFrame(input, "save");
83979
85279
  for (const a of input.annotations) {
83980
85280
  if (!isRetrainMacroClass(a.macroClass)) throw new RetrainAnnotationError(`"${a.macroClass}" is not a macro class`);
83981
85281
  const violation = checkTierSeparation(a);
83982
85282
  if (violation !== null) throw new RetrainAnnotationError(`"${violation.value}" is a MACRO CLASS and cannot be a ${violation.field} — the tiers are separate (person / postman / Marco)`);
83983
85283
  }
83984
85284
  const drafts = input.annotations.map((a) => ({
83985
- trackId: input.trackId,
83986
- deviceId: input.deviceId,
85285
+ trackId: frame.trackId,
85286
+ deviceId: frame.deviceId,
83987
85287
  mediaKey: input.frameId,
83988
85288
  bbox: a.bbox,
83989
85289
  macroClass: a.macroClass,
@@ -83999,7 +85299,8 @@ var RetrainService = class {
83999
85299
  this.deps.logger.info("retrain annotations saved", {
84000
85300
  tags: { deviceId: input.deviceId },
84001
85301
  meta: {
84002
- trackId: input.trackId,
85302
+ origin: frame.origin,
85303
+ trackId: frame.trackId,
84003
85304
  frameId: input.frameId,
84004
85305
  subjects: saved.length - errors,
84005
85306
  modelErrors: errors
@@ -93401,6 +94702,14 @@ var RECORDED_STILL_PROFILE = "high";
93401
94702
  */
93402
94703
  var RECOVERED_KEY_FRAME_MAX_WIDTH = 1920;
93403
94704
  /**
94705
+ * Width cap for a still added to the retrain dataset from the recording.
94706
+ *
94707
+ * Effectively none: a training frame is wanted at the camera's own resolution
94708
+ * (the same pixels a native key frame has), so the bound is only a ceiling
94709
+ * against a pathological stream, far above any camera on the fleet.
94710
+ */
94711
+ var RETRAIN_STILL_MAX_WIDTH = 7680;
94712
+ /**
93404
94713
  * Build the production ports.
93405
94714
  *
93406
94715
  * Every port swallows its own failure into the `null` the policy expects: a
@@ -107096,6 +108405,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
107096
108405
  /** The retrain curation surface — see `retrain/retrain-service.ts`. Null
107097
108406
  * until the stores are built. */
107098
108407
  retrain = null;
108408
+ retrainCollections = null;
108409
+ retrainLlmAssist = null;
108410
+ retrainCollectionStore = null;
107099
108411
  retrainFrames = null;
107100
108412
  retrainAnnotations = null;
107101
108413
  /** The NC artifact plane (signed public links for notification media) and its
@@ -109147,7 +110459,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
109147
110459
  });
109148
110460
  this.retrainFrames = retrainFrames;
109149
110461
  this.retrainAnnotations = retrainAnnotations;
109150
- this.retrain = new RetrainService({
110462
+ const retrainCollectionStore = new RetrainCollectionStore({
110463
+ store: api.settingsStore,
110464
+ logger: logger.child("RetrainCollections"),
110465
+ now: () => Date.now(),
110466
+ newId: () => randomUUID()
110467
+ });
110468
+ this.retrainCollectionStore = retrainCollectionStore;
110469
+ const retrain = new RetrainService({
109151
110470
  tracks: {
109152
110471
  listStaging: (deviceId, limit) => trackStore.listStaging(deviceId, limit),
109153
110472
  listDeviceIds: () => trackStore.listDeviceIds(),
@@ -109165,7 +110484,70 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
109165
110484
  frames: retrainFrames,
109166
110485
  annotations: retrainAnnotations,
109167
110486
  runner: (input, nodeId) => nodeId === void 0 ? api.pipelineRunner.runStatelessStep.mutate(input) : api.pipelineRunner.runStatelessStep.mutate(input, nodePin(nodeId)),
109168
- logger: logger.child("Retrain")
110487
+ logger: logger.child("Retrain"),
110488
+ pipelineEvidence: {
110489
+ tracksAround: (deviceId, atMs) => trackStore.queryHistorical({
110490
+ deviceId,
110491
+ since: atMs - PIPELINE_TRACK_LOOKBACK_MS,
110492
+ until: atMs + PIPELINE_BOX_TOLERANCE_MS,
110493
+ limit: PIPELINE_TRACK_AROUND_LIMIT
110494
+ }),
110495
+ subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
110496
+ },
110497
+ memberships: retrainCollectionStore
110498
+ });
110499
+ this.retrain = retrain;
110500
+ const retrainStills = new RecordedStillService(buildRecordedStillPorts({
110501
+ api: this.ctx.api.recordingArchive,
110502
+ logger: logger.child("RetrainStills"),
110503
+ resolveFfmpeg: this.pinnedFfmpeg
110504
+ }));
110505
+ this.retrainCollections = new RetrainCollectionService({
110506
+ collections: retrainCollectionStore,
110507
+ frames: retrainFrames,
110508
+ annotations: retrainAnnotations,
110509
+ retrain,
110510
+ captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
110511
+ logger: logger.child("RetrainCollections")
110512
+ });
110513
+ this.retrainLlmAssist = new RetrainLlmAssistService({
110514
+ frames: retrainFrames,
110515
+ llm: {
110516
+ listProfiles: async () => (await api.llm.listProfiles.query({})).map((p) => ({
110517
+ id: p.id,
110518
+ name: p.name,
110519
+ model: p.model,
110520
+ kind: p.kind,
110521
+ ...p.baseUrl !== void 0 ? { baseUrl: p.baseUrl } : {},
110522
+ enabled: p.enabled,
110523
+ supportsVision: p.supportsVision
110524
+ })),
110525
+ generateVision: async (request) => {
110526
+ const result = await api.llm.generateVision.mutate({
110527
+ profileId: request.profileId,
110528
+ consumer: RETRAIN_LLM_CONSUMER,
110529
+ system: request.system,
110530
+ prompt: request.prompt,
110531
+ jsonSchema: request.jsonSchema,
110532
+ images: [{
110533
+ bytes: new Uint8Array(request.image),
110534
+ mimeType: "image/jpeg"
110535
+ }],
110536
+ temperature: 0,
110537
+ timeoutMs: RETRAIN_LLM_TIMEOUT_MS
110538
+ });
110539
+ return result.ok ? {
110540
+ ok: true,
110541
+ text: result.text,
110542
+ model: result.model
110543
+ } : {
110544
+ ok: false,
110545
+ code: result.code,
110546
+ message: result.message
110547
+ };
110548
+ }
110549
+ },
110550
+ logger: logger.child("RetrainLlmAssist")
109169
110551
  });
109170
110552
  this.mediaRelocate = new MediaRelocateEngine({
109171
110553
  store: api.settingsStore,
@@ -110349,14 +111731,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
110349
111731
  };
110350
111732
  return this.requireRetrain().propose({
110351
111733
  deviceId: input.deviceId,
110352
- trackId: input.trackId,
111734
+ ...input.trackId !== void 0 ? { trackId: input.trackId } : {},
110353
111735
  frameId: input.frameId,
110354
111736
  subject,
110355
111737
  ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
110356
111738
  });
110357
111739
  }
110358
111740
  async listRetrainAnnotations(input) {
110359
- return this.requireRetrain().listAnnotations(input.trackId);
111741
+ return this.requireRetrain().listAnnotations(input);
110360
111742
  }
110361
111743
  async saveRetrainAnnotations(input) {
110362
111744
  return this.requireRetrain().saveAnnotations(input);
@@ -110380,6 +111762,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
110380
111762
  async setRetrainFrameComplete(input) {
110381
111763
  return this.requireRetrain().setFrameComplete(input);
110382
111764
  }
111765
+ requireRetrainLlmAssist() {
111766
+ const service = this.retrainLlmAssist;
111767
+ if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
111768
+ return service;
111769
+ }
111770
+ async listRetrainAssistProfiles(_input) {
111771
+ return this.requireRetrainLlmAssist().listProfiles();
111772
+ }
111773
+ async proposeRetrainAnnotationsWithLlm(input) {
111774
+ return this.requireRetrainLlmAssist().propose(input);
111775
+ }
111776
+ requireRetrainCollections() {
111777
+ const service = this.retrainCollections;
111778
+ if (service === null) throw new Error("retrain collections: analytics stores unavailable");
111779
+ return service;
111780
+ }
111781
+ async createRetrainCollection(input) {
111782
+ return this.requireRetrainCollections().create(input);
111783
+ }
111784
+ async listRetrainCollections(_input) {
111785
+ return this.requireRetrainCollections().list();
111786
+ }
111787
+ async renameRetrainCollection(input) {
111788
+ return this.requireRetrainCollections().rename(input);
111789
+ }
111790
+ async deleteRetrainCollection(input) {
111791
+ return this.requireRetrainCollections().delete(input);
111792
+ }
111793
+ async addRetrainFrameFromRecording(input) {
111794
+ return this.requireRetrainCollections().addFrameFromRecording(input);
111795
+ }
111796
+ async addRetrainFramesToCollection(input) {
111797
+ return this.requireRetrainCollections().addFrames(input);
111798
+ }
111799
+ async removeRetrainFrameFromCollection(input) {
111800
+ return this.requireRetrainCollections().removeFrame(input);
111801
+ }
111802
+ async listRetrainCollectionFrames(input) {
111803
+ return this.requireRetrainCollections().listFrames(input);
111804
+ }
110383
111805
  /**
110384
111806
  * Stream the annotated archive into ONE HTTP request to `target` — the
110385
111807
  * training upload. Same bytes as the download; never on the hub's disk.
@@ -110735,7 +112157,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
110735
112157
  const trackStore = this.trackStore;
110736
112158
  if (!frameStore || !annotationStore || !trackStore) throw new Error("retrain export: analytics stores unavailable");
110737
112159
  const deviceIds = request.filter.deviceIds ?? [];
110738
- const devices = deviceIds.length > 0 ? deviceIds : await trackStore.listDeviceIds();
112160
+ const devices = deviceIds.length > 0 ? deviceIds : [...new Set([...await trackStore.listDeviceIds(), ...await frameStore.listDeviceIds()])];
110739
112161
  const candidates = /* @__PURE__ */ new Map();
110740
112162
  const annotationsByFrame = /* @__PURE__ */ new Map();
110741
112163
  const zones = /* @__PURE__ */ new Map();
@@ -110752,11 +112174,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
110752
112174
  const frame = await frameStore.get(frameId);
110753
112175
  if (frame !== null) candidates.set(frameId, frame);
110754
112176
  }
110755
- const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter);
112177
+ const collectionsByFrame = await this.retrainCollectionStore?.collectionsOf([...candidates.keys()]) ?? /* @__PURE__ */ new Map();
112178
+ const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter, collectionsByFrame);
110756
112179
  return {
110757
112180
  manifest: buildRetrainManifest({
110758
112181
  frames,
110759
112182
  annotationsByFrame,
112183
+ collectionsByFrame,
110760
112184
  zoneForDevice: (deviceId) => zones.get(deviceId) ?? null,
110761
112185
  generatedAt: Date.now(),
110762
112186
  batchId: randomUUID(),