@camstack/addon-post-analysis 1.2.343 → 1.2.345
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.
- package/dist/{clip-model-registry-Cd_y9S4U.js → clip-model-registry-Bd8rsIjt.js} +2133 -57
- package/dist/{clip-model-registry-DZz3e781.mjs → clip-model-registry-CIbZ6YOF.mjs} +2133 -57
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DrdO3Pxp.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DTenVWiv.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DTOF9hbS.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DUrhEd38.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DPNnWr-m.mjs → hostInit-BO3gbCue.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +2391 -405
- package/dist/pipeline-analytics/index.mjs +2391 -405
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- 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
- 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-
|
|
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-CIbZ6YOF.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);
|
|
@@ -12914,7 +12914,7 @@ function stringField(data, key) {
|
|
|
12914
12914
|
const value = data[key];
|
|
12915
12915
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
12916
12916
|
}
|
|
12917
|
-
function stringList(data, key) {
|
|
12917
|
+
function stringList$1(data, key) {
|
|
12918
12918
|
const raw = data[key];
|
|
12919
12919
|
if (!Array.isArray(raw)) return [];
|
|
12920
12920
|
const ids = [];
|
|
@@ -13726,7 +13726,7 @@ function incomingFromExportCompleted(facts, data) {
|
|
|
13726
13726
|
const deviceId = numberField(data, "deviceId");
|
|
13727
13727
|
const title = stringField(data, "title");
|
|
13728
13728
|
const bytes = numberField(data, "fileBytes");
|
|
13729
|
-
const notifyTargetIds = stringList(data, "notifyTargetIds");
|
|
13729
|
+
const notifyTargetIds = stringList$1(data, "notifyTargetIds");
|
|
13730
13730
|
const incoming = systemSubject({
|
|
13731
13731
|
facts,
|
|
13732
13732
|
kind: "export-completed",
|
|
@@ -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
|
|
70579
|
-
*
|
|
70580
|
-
*
|
|
70581
|
-
* the
|
|
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
|
-
|
|
70734
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
71306
|
+
const relativePath = framePath(frame);
|
|
71115
71307
|
let bytes;
|
|
71116
71308
|
try {
|
|
71117
71309
|
bytes = await this.storage.read({
|
|
@@ -74732,7 +74924,7 @@ function describeTapeStatus(input) {
|
|
|
74732
74924
|
* `viewer-settings-legacy-adoption.ts`.
|
|
74733
74925
|
*/
|
|
74734
74926
|
var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
|
|
74735
|
-
var COLUMNS = [
|
|
74927
|
+
var COLUMNS$1 = [
|
|
74736
74928
|
{
|
|
74737
74929
|
name: "id",
|
|
74738
74930
|
type: "TEXT",
|
|
@@ -74802,7 +74994,7 @@ function requireCaller(caller) {
|
|
|
74802
74994
|
if (!caller) throw new Error("viewerSettings: authenticated caller required");
|
|
74803
74995
|
return caller;
|
|
74804
74996
|
}
|
|
74805
|
-
function asRecord(data) {
|
|
74997
|
+
function asRecord$1(data) {
|
|
74806
74998
|
return typeof data === "object" && data !== null ? data : {};
|
|
74807
74999
|
}
|
|
74808
75000
|
function toSummary(id, data, callerUserId) {
|
|
@@ -74828,7 +75020,7 @@ var ViewerSettingsSnapshotStore = class {
|
|
|
74828
75020
|
static async declare(store) {
|
|
74829
75021
|
await store.declareCollection.mutate({
|
|
74830
75022
|
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
74831
|
-
columns: [...COLUMNS],
|
|
75023
|
+
columns: [...COLUMNS$1],
|
|
74832
75024
|
indexes: [{
|
|
74833
75025
|
name: "idx_pa_viewer_settings_owner",
|
|
74834
75026
|
columns: ["ownerUserId"]
|
|
@@ -74848,7 +75040,7 @@ var ViewerSettingsSnapshotStore = class {
|
|
|
74848
75040
|
});
|
|
74849
75041
|
const out = [];
|
|
74850
75042
|
for (const row of rows) {
|
|
74851
|
-
const summary = toSummary(row.id, asRecord(row.data), caller.userId);
|
|
75043
|
+
const summary = toSummary(row.id, asRecord$1(row.data), caller.userId);
|
|
74852
75044
|
if (summary) out.push(summary);
|
|
74853
75045
|
}
|
|
74854
75046
|
return out;
|
|
@@ -74862,7 +75054,7 @@ var ViewerSettingsSnapshotStore = class {
|
|
|
74862
75054
|
}
|
|
74863
75055
|
}))[0];
|
|
74864
75056
|
if (!row) return null;
|
|
74865
|
-
const data = asRecord(row.data);
|
|
75057
|
+
const data = asRecord$1(row.data);
|
|
74866
75058
|
const summary = toSummary(row.id, data, caller.userId);
|
|
74867
75059
|
if (!summary) return null;
|
|
74868
75060
|
const blob = BlobSchema.safeParse(data.blob);
|
|
@@ -74885,7 +75077,7 @@ var ViewerSettingsSnapshotStore = class {
|
|
|
74885
75077
|
}
|
|
74886
75078
|
}))[0];
|
|
74887
75079
|
const id = prior?.id ?? randomUUID();
|
|
74888
|
-
const createdAt = typeof asRecord(prior?.data).createdAt === "number" ? asRecord(prior?.data).createdAt : now;
|
|
75080
|
+
const createdAt = typeof asRecord$1(prior?.data).createdAt === "number" ? asRecord$1(prior?.data).createdAt : now;
|
|
74889
75081
|
await this.store.set.mutate({
|
|
74890
75082
|
collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
|
|
74891
75083
|
key: id,
|
|
@@ -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"]
|
|
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
|
-
*
|
|
82459
|
-
*
|
|
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.
|
|
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,161 @@ var RetrainCollectionStore = class {
|
|
|
82569
82874
|
this.logger.warn("retrain collections list failed", { meta: { error: String(err) } });
|
|
82570
82875
|
return [];
|
|
82571
82876
|
}
|
|
82572
|
-
const
|
|
82573
|
-
|
|
82574
|
-
|
|
82575
|
-
|
|
82576
|
-
|
|
82577
|
-
|
|
82578
|
-
|
|
82579
|
-
|
|
82580
|
-
|
|
82581
|
-
|
|
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.store.query.query({
|
|
82929
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82930
|
+
filter: { where: { collectionId: id } }
|
|
82931
|
+
});
|
|
82932
|
+
for (const m of members) await this.store.delete.mutate({
|
|
82933
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82934
|
+
key: m.id
|
|
82935
|
+
});
|
|
82936
|
+
await this.store.delete.mutate({
|
|
82937
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
82938
|
+
key: id
|
|
82939
|
+
});
|
|
82940
|
+
this.logger.info("retrain collection deleted", { meta: {
|
|
82941
|
+
collectionId: id,
|
|
82942
|
+
members: members.length
|
|
82943
|
+
} });
|
|
82944
|
+
return {
|
|
82945
|
+
deleted: true,
|
|
82946
|
+
memberFrameIds: members.map((m) => String(m.data["frameId"] ?? ""))
|
|
82947
|
+
};
|
|
82948
|
+
}
|
|
82949
|
+
/** Add a frame. `false` when it was already a member (nothing written). */
|
|
82950
|
+
async addFrame(input) {
|
|
82951
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
82952
|
+
const existing = await this.store.get.query({
|
|
82953
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82954
|
+
key
|
|
82955
|
+
});
|
|
82956
|
+
if (existing !== null && existing !== void 0) return false;
|
|
82957
|
+
await this.store.set.mutate({
|
|
82958
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82959
|
+
key,
|
|
82960
|
+
value: {
|
|
82961
|
+
collectionId: input.collectionId,
|
|
82962
|
+
frameId: input.frameId,
|
|
82963
|
+
deviceId: input.deviceId,
|
|
82964
|
+
addedAt: this.now()
|
|
82965
|
+
}
|
|
82966
|
+
});
|
|
82967
|
+
return true;
|
|
82968
|
+
}
|
|
82969
|
+
/** Remove one membership. `false` when it did not exist. */
|
|
82970
|
+
async removeFrame(input) {
|
|
82971
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
82972
|
+
const existing = await this.store.get.query({
|
|
82973
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82974
|
+
key
|
|
82975
|
+
});
|
|
82976
|
+
if (existing === null || existing === void 0) return false;
|
|
82977
|
+
await this.store.delete.mutate({
|
|
82978
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82979
|
+
key
|
|
82980
|
+
});
|
|
82981
|
+
return true;
|
|
82982
|
+
}
|
|
82983
|
+
/** Remove a frame from EVERY collection — the frame itself is leaving. */
|
|
82984
|
+
async forgetFrame(frameId) {
|
|
82985
|
+
const rows = await this.membershipRows({ frameId });
|
|
82986
|
+
for (const r of rows) await this.store.delete.mutate({
|
|
82987
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
82988
|
+
key: r.id
|
|
82989
|
+
});
|
|
82990
|
+
return rows.length;
|
|
82991
|
+
}
|
|
82992
|
+
/** The frames of a collection, in the order they were added. */
|
|
82993
|
+
async listFrameIds(collectionId) {
|
|
82994
|
+
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 !== "");
|
|
82995
|
+
}
|
|
82996
|
+
/** frameId → the collections it belongs to. Frames in none are absent. */
|
|
82997
|
+
async collectionsOf(frameIds) {
|
|
82998
|
+
const out = /* @__PURE__ */ new Map();
|
|
82999
|
+
for (const row of await this.membershipRowsIn("frameId", frameIds)) {
|
|
83000
|
+
const frameId = String(row.data["frameId"] ?? "");
|
|
83001
|
+
const collectionId = String(row.data["collectionId"] ?? "");
|
|
83002
|
+
if (frameId === "" || collectionId === "") continue;
|
|
83003
|
+
out.set(frameId, [...out.get(frameId) ?? [], collectionId]);
|
|
82583
83004
|
}
|
|
83005
|
+
for (const [frameId, ids] of out) out.set(frameId, ids.toSorted());
|
|
82584
83006
|
return out;
|
|
82585
83007
|
}
|
|
82586
|
-
|
|
83008
|
+
/** The frame ids of several collections, de-duplicated. */
|
|
83009
|
+
async frameIdsOf(collectionIds) {
|
|
83010
|
+
const ids = /* @__PURE__ */ new Set();
|
|
83011
|
+
for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
|
|
83012
|
+
const frameId = String(row.data["frameId"] ?? "");
|
|
83013
|
+
if (frameId !== "") ids.add(frameId);
|
|
83014
|
+
}
|
|
83015
|
+
return [...ids];
|
|
83016
|
+
}
|
|
83017
|
+
toCollection(id, data, frameCount) {
|
|
83018
|
+
return {
|
|
83019
|
+
id,
|
|
83020
|
+
name: typeof data["name"] === "string" ? data["name"] : "",
|
|
83021
|
+
createdAt: Number(data["createdAt"] ?? 0),
|
|
83022
|
+
frameCount
|
|
83023
|
+
};
|
|
83024
|
+
}
|
|
83025
|
+
async readRow(id) {
|
|
82587
83026
|
try {
|
|
82588
83027
|
const row = await this.store.get.query({
|
|
82589
83028
|
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
82590
83029
|
key: id
|
|
82591
83030
|
});
|
|
82592
|
-
|
|
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
|
-
};
|
|
83031
|
+
return row === null || row === void 0 ? null : { ...row };
|
|
82600
83032
|
} catch (err) {
|
|
82601
83033
|
this.logger.warn("retrain collection get failed", { meta: {
|
|
82602
83034
|
collectionId: id,
|
|
@@ -82605,14 +83037,125 @@ var RetrainCollectionStore = class {
|
|
|
82605
83037
|
return null;
|
|
82606
83038
|
}
|
|
82607
83039
|
}
|
|
83040
|
+
async countMembers(collectionIds) {
|
|
83041
|
+
const counts = /* @__PURE__ */ new Map();
|
|
83042
|
+
for (const row of await this.membershipRowsIn("collectionId", collectionIds)) {
|
|
83043
|
+
const id = String(row.data["collectionId"] ?? "");
|
|
83044
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
83045
|
+
}
|
|
83046
|
+
return counts;
|
|
83047
|
+
}
|
|
83048
|
+
async membershipRows(where) {
|
|
83049
|
+
try {
|
|
83050
|
+
return await this.store.query.query({
|
|
83051
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83052
|
+
filter: { where }
|
|
83053
|
+
});
|
|
83054
|
+
} catch (err) {
|
|
83055
|
+
this.logger.warn("retrain collection membership read failed", { meta: {
|
|
83056
|
+
...where,
|
|
83057
|
+
error: String(err)
|
|
83058
|
+
} });
|
|
83059
|
+
return [];
|
|
83060
|
+
}
|
|
83061
|
+
}
|
|
83062
|
+
async membershipRowsIn(field, values) {
|
|
83063
|
+
const out = [];
|
|
83064
|
+
for (let i = 0; i < values.length; i += IN_PAGE) {
|
|
83065
|
+
const page = values.slice(i, i + IN_PAGE);
|
|
83066
|
+
try {
|
|
83067
|
+
out.push(...await this.store.query.query({
|
|
83068
|
+
collection: RETRAIN_COLLECTION_FRAMES_COLLECTION,
|
|
83069
|
+
filter: { whereIn: { [field]: [...page] } }
|
|
83070
|
+
}));
|
|
83071
|
+
} catch (err) {
|
|
83072
|
+
this.logger.warn("retrain collection membership read failed", { meta: {
|
|
83073
|
+
field,
|
|
83074
|
+
values: page.length,
|
|
83075
|
+
error: String(err)
|
|
83076
|
+
} });
|
|
83077
|
+
}
|
|
83078
|
+
}
|
|
83079
|
+
return out;
|
|
83080
|
+
}
|
|
83081
|
+
async findIdByNameKey(nameKey) {
|
|
83082
|
+
try {
|
|
83083
|
+
return (await this.store.query.query({
|
|
83084
|
+
collection: RETRAIN_COLLECTIONS_COLLECTION,
|
|
83085
|
+
filter: {
|
|
83086
|
+
where: { nameKey },
|
|
83087
|
+
limit: 1
|
|
83088
|
+
}
|
|
83089
|
+
}))[0]?.id ?? null;
|
|
83090
|
+
} catch (err) {
|
|
83091
|
+
this.logger.warn("retrain collection findByNameKey failed", { meta: {
|
|
83092
|
+
nameKey,
|
|
83093
|
+
error: String(err)
|
|
83094
|
+
} });
|
|
83095
|
+
return null;
|
|
83096
|
+
}
|
|
83097
|
+
}
|
|
83098
|
+
};
|
|
83099
|
+
//#endregion
|
|
83100
|
+
//#region src/pipeline-analytics/retrain/retrain-collection-service.ts
|
|
83101
|
+
/** What each still outcome tells the operator. */
|
|
83102
|
+
var STILL_REFUSAL = {
|
|
83103
|
+
"no-recording": {
|
|
83104
|
+
reason: "still-unavailable",
|
|
83105
|
+
detail: "this camera is not recording, so there is no footage to take a frame from"
|
|
83106
|
+
},
|
|
83107
|
+
"no-footage": {
|
|
83108
|
+
reason: "still-unavailable",
|
|
83109
|
+
detail: "no recorded footage covers that instant (a gap, or already pruned)"
|
|
83110
|
+
},
|
|
83111
|
+
"decode-failed": {
|
|
83112
|
+
reason: "still-unavailable",
|
|
83113
|
+
detail: "the footage is there but the frame could not be decoded"
|
|
83114
|
+
},
|
|
83115
|
+
busy: {
|
|
83116
|
+
reason: "still-busy",
|
|
83117
|
+
detail: "the recorder is already decoding other frames — try again in a moment"
|
|
83118
|
+
}
|
|
83119
|
+
};
|
|
83120
|
+
var RetrainCollectionService = class {
|
|
83121
|
+
deps;
|
|
83122
|
+
constructor(deps) {
|
|
83123
|
+
this.deps = deps;
|
|
83124
|
+
}
|
|
83125
|
+
create(input) {
|
|
83126
|
+
return this.deps.collections.create(input);
|
|
83127
|
+
}
|
|
83128
|
+
list() {
|
|
83129
|
+
return this.deps.collections.list();
|
|
83130
|
+
}
|
|
83131
|
+
rename(input) {
|
|
83132
|
+
return this.deps.collections.rename(input);
|
|
83133
|
+
}
|
|
82608
83134
|
/**
|
|
82609
|
-
*
|
|
82610
|
-
*
|
|
83135
|
+
* Delete a collection. Its recording stills that no other collection holds
|
|
83136
|
+
* go with it (annotations first); track copies always stay.
|
|
82611
83137
|
*/
|
|
82612
|
-
async
|
|
82613
|
-
|
|
82614
|
-
|
|
82615
|
-
|
|
83138
|
+
async delete(input) {
|
|
83139
|
+
const result = await this.deps.collections.delete(input.id);
|
|
83140
|
+
if (!result.deleted) return {
|
|
83141
|
+
deleted: false,
|
|
83142
|
+
removedFrames: 0
|
|
83143
|
+
};
|
|
83144
|
+
return {
|
|
83145
|
+
deleted: true,
|
|
83146
|
+
removedFrames: await this.dropOrphanStills(result.memberFrameIds)
|
|
83147
|
+
};
|
|
83148
|
+
}
|
|
83149
|
+
/**
|
|
83150
|
+
* Decode the frame at `epochMs` from the camera's recording and add it to
|
|
83151
|
+
* the collection. The moment already in the dataset is reused without a
|
|
83152
|
+
* decode; every refusal is named and writes nothing.
|
|
83153
|
+
*/
|
|
83154
|
+
async addFrameFromRecording(input) {
|
|
83155
|
+
const tags = { deviceId: input.deviceId };
|
|
83156
|
+
if (await this.deps.collections.getById(input.collectionId) === null) {
|
|
83157
|
+
this.deps.logger.info("retrain collection add refused: unknown collection", {
|
|
83158
|
+
tags,
|
|
82616
83159
|
meta: {
|
|
82617
83160
|
collectionId: input.collectionId,
|
|
82618
83161
|
epochMs: input.epochMs
|
|
@@ -82623,42 +83166,1298 @@ var RetrainCollectionStore = class {
|
|
|
82623
83166
|
reason: "unknown-collection"
|
|
82624
83167
|
};
|
|
82625
83168
|
}
|
|
82626
|
-
|
|
82627
|
-
|
|
83169
|
+
const stored = await this.storeStill(input.deviceId, input.epochMs);
|
|
83170
|
+
if (stored.kind === "refused") {
|
|
83171
|
+
this.deps.logger.info("retrain collection add refused", {
|
|
83172
|
+
tags,
|
|
83173
|
+
meta: {
|
|
83174
|
+
collectionId: input.collectionId,
|
|
83175
|
+
epochMs: input.epochMs,
|
|
83176
|
+
reason: stored.reason
|
|
83177
|
+
}
|
|
83178
|
+
});
|
|
83179
|
+
return stored;
|
|
83180
|
+
}
|
|
83181
|
+
await this.deps.collections.addFrame({
|
|
83182
|
+
collectionId: input.collectionId,
|
|
83183
|
+
frameId: stored.frame.frameId,
|
|
83184
|
+
deviceId: input.deviceId
|
|
83185
|
+
});
|
|
83186
|
+
this.deps.logger.info("retrain frame added to a collection from the recording", {
|
|
83187
|
+
tags,
|
|
82628
83188
|
meta: {
|
|
82629
83189
|
collectionId: input.collectionId,
|
|
82630
|
-
|
|
83190
|
+
frameId: stored.frame.frameId,
|
|
83191
|
+
epochMs: input.epochMs,
|
|
83192
|
+
reused: stored.reused
|
|
82631
83193
|
}
|
|
82632
83194
|
});
|
|
83195
|
+
const [wire] = await this.deps.retrain.toWireFrames([stored.frame]);
|
|
83196
|
+
if (wire === void 0) throw new Error("retrain: a stored still produced no wire frame");
|
|
82633
83197
|
return {
|
|
83198
|
+
kind: "added",
|
|
83199
|
+
collectionId: input.collectionId,
|
|
83200
|
+
frame: wire,
|
|
83201
|
+
reused: stored.reused
|
|
83202
|
+
};
|
|
83203
|
+
}
|
|
83204
|
+
/** Add frames already in the dataset (track copies or stills) to a collection. */
|
|
83205
|
+
async addFrames(input) {
|
|
83206
|
+
if (await this.deps.collections.getById(input.collectionId) === null) return {
|
|
83207
|
+
added: 0,
|
|
83208
|
+
alreadyMember: 0,
|
|
83209
|
+
unknownFrameIds: [...input.frameIds]
|
|
83210
|
+
};
|
|
83211
|
+
const frames = await this.deps.frames.getMany(input.frameIds);
|
|
83212
|
+
const known = new Set(frames.map((f) => f.frameId));
|
|
83213
|
+
let added = 0;
|
|
83214
|
+
let alreadyMember = 0;
|
|
83215
|
+
for (const frame of frames) if (await this.deps.collections.addFrame({
|
|
83216
|
+
collectionId: input.collectionId,
|
|
83217
|
+
frameId: frame.frameId,
|
|
83218
|
+
deviceId: frame.deviceId
|
|
83219
|
+
})) added += 1;
|
|
83220
|
+
else alreadyMember += 1;
|
|
83221
|
+
return {
|
|
83222
|
+
added,
|
|
83223
|
+
alreadyMember,
|
|
83224
|
+
unknownFrameIds: input.frameIds.filter((id) => !known.has(id))
|
|
83225
|
+
};
|
|
83226
|
+
}
|
|
83227
|
+
/**
|
|
83228
|
+
* Remove a frame from a collection. A recording still left in no collection
|
|
83229
|
+
* is deleted with its annotations; a track copy is never deleted here.
|
|
83230
|
+
*/
|
|
83231
|
+
async removeFrame(input) {
|
|
83232
|
+
if (!await this.deps.collections.removeFrame(input)) return {
|
|
83233
|
+
removed: false,
|
|
83234
|
+
deletedFrame: false
|
|
83235
|
+
};
|
|
83236
|
+
return {
|
|
83237
|
+
removed: true,
|
|
83238
|
+
deletedFrame: await this.dropOrphanStills([input.frameId]) > 0
|
|
83239
|
+
};
|
|
83240
|
+
}
|
|
83241
|
+
/** The frames of a collection, as the canvas lists them. */
|
|
83242
|
+
async listFrames(input) {
|
|
83243
|
+
const ids = await this.deps.collections.listFrameIds(input.collectionId);
|
|
83244
|
+
return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
|
|
83245
|
+
}
|
|
83246
|
+
async storeStill(deviceId, epochMs) {
|
|
83247
|
+
const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
|
|
83248
|
+
if (existing !== null) return {
|
|
83249
|
+
kind: "stored",
|
|
83250
|
+
frame: existing,
|
|
83251
|
+
reused: true
|
|
83252
|
+
};
|
|
83253
|
+
const still = await this.deps.captureStill(deviceId, epochMs);
|
|
83254
|
+
if (still.outcome !== "ok" || still.jpeg === void 0) {
|
|
83255
|
+
const refusal = still.outcome === "ok" ? STILL_REFUSAL["decode-failed"] : STILL_REFUSAL[still.outcome];
|
|
83256
|
+
return {
|
|
83257
|
+
kind: "refused",
|
|
83258
|
+
reason: refusal.reason,
|
|
83259
|
+
detail: refusal.detail
|
|
83260
|
+
};
|
|
83261
|
+
}
|
|
83262
|
+
const stored = await this.deps.frames.storeRecordingStill({
|
|
83263
|
+
deviceId,
|
|
83264
|
+
epochMs,
|
|
83265
|
+
bytes: still.jpeg
|
|
83266
|
+
});
|
|
83267
|
+
return stored.kind === "copied" ? {
|
|
83268
|
+
kind: "stored",
|
|
83269
|
+
frame: stored.frame,
|
|
83270
|
+
reused: stored.reused
|
|
83271
|
+
} : {
|
|
82634
83272
|
kind: "refused",
|
|
82635
|
-
reason:
|
|
82636
|
-
detail: "Still ingest lands in plan Task 3"
|
|
83273
|
+
reason: stored.reason
|
|
82637
83274
|
};
|
|
82638
83275
|
}
|
|
82639
|
-
|
|
82640
|
-
|
|
82641
|
-
|
|
82642
|
-
|
|
82643
|
-
|
|
82644
|
-
|
|
82645
|
-
|
|
82646
|
-
|
|
82647
|
-
|
|
82648
|
-
|
|
83276
|
+
/** Delete the recording stills among `frameIds` that no collection holds any more. */
|
|
83277
|
+
async dropOrphanStills(frameIds) {
|
|
83278
|
+
if (frameIds.length === 0) return 0;
|
|
83279
|
+
const memberships = await this.deps.collections.collectionsOf(frameIds);
|
|
83280
|
+
const orphans = (await this.deps.frames.getMany(frameIds)).filter((f) => f.origin === "recording" && (memberships.get(f.frameId)?.length ?? 0) === 0);
|
|
83281
|
+
for (const frame of orphans) await this.deps.retrain.removeFrame(frame);
|
|
83282
|
+
return orphans.length;
|
|
83283
|
+
}
|
|
83284
|
+
};
|
|
83285
|
+
//#endregion
|
|
83286
|
+
//#region src/pipeline-analytics/retrain/retrain-dataset.ts
|
|
83287
|
+
/**
|
|
83288
|
+
* Dataset versions — the counts a version is judged by, and the hash that
|
|
83289
|
+
* freezes what it teaches. Pure.
|
|
83290
|
+
*
|
|
83291
|
+
* Both read the export MANIFEST (`retrain-export.ts`), not the stores: a
|
|
83292
|
+
* version is exactly what an export of its frames would carry, so its counts
|
|
83293
|
+
* and its hash cannot disagree with the archive a trainer receives.
|
|
83294
|
+
*/
|
|
83295
|
+
function lightingOf(frame) {
|
|
83296
|
+
if (frame.lighting === null) return "unknown";
|
|
83297
|
+
return frame.lighting.night ? "night" : "day";
|
|
83298
|
+
}
|
|
83299
|
+
/** The counts of a dataset, from the manifest an export of it would carry. */
|
|
83300
|
+
function datasetStats(manifest) {
|
|
83301
|
+
const cells = /* @__PURE__ */ new Map();
|
|
83302
|
+
const cameras = /* @__PURE__ */ new Map();
|
|
83303
|
+
let completeFrames = 0;
|
|
83304
|
+
let subjects = 0;
|
|
83305
|
+
let modelErrors = 0;
|
|
83306
|
+
let hardNegatives = 0;
|
|
83307
|
+
for (const frame of manifest.frames) {
|
|
83308
|
+
const camera = cameras.get(frame.deviceId) ?? {
|
|
83309
|
+
frames: 0,
|
|
83310
|
+
completeFrames: 0
|
|
83311
|
+
};
|
|
83312
|
+
cameras.set(frame.deviceId, {
|
|
83313
|
+
frames: camera.frames + 1,
|
|
83314
|
+
completeFrames: camera.completeFrames + (frame.complete ? 1 : 0)
|
|
83315
|
+
});
|
|
83316
|
+
if (frame.complete) completeFrames += 1;
|
|
83317
|
+
const lighting = lightingOf(frame);
|
|
83318
|
+
let frameSubjects = 0;
|
|
83319
|
+
for (const a of frame.annotations) {
|
|
83320
|
+
if (a.kind === "model_error") {
|
|
83321
|
+
modelErrors += 1;
|
|
83322
|
+
continue;
|
|
83323
|
+
}
|
|
83324
|
+
frameSubjects += 1;
|
|
83325
|
+
const key = `${a.macroClass}|${String(frame.deviceId)}|${lighting}`;
|
|
83326
|
+
const entry = cells.get(key) ?? {
|
|
83327
|
+
cell: {
|
|
83328
|
+
macroClass: a.macroClass,
|
|
83329
|
+
deviceId: frame.deviceId,
|
|
83330
|
+
lighting,
|
|
83331
|
+
subjects: 0,
|
|
83332
|
+
frames: 0
|
|
83333
|
+
},
|
|
83334
|
+
frameIds: /* @__PURE__ */ new Set()
|
|
83335
|
+
};
|
|
83336
|
+
entry.frameIds.add(frame.frameId);
|
|
83337
|
+
cells.set(key, {
|
|
83338
|
+
cell: {
|
|
83339
|
+
...entry.cell,
|
|
83340
|
+
subjects: entry.cell.subjects + 1,
|
|
83341
|
+
frames: entry.frameIds.size
|
|
83342
|
+
},
|
|
83343
|
+
frameIds: entry.frameIds
|
|
83344
|
+
});
|
|
83345
|
+
}
|
|
83346
|
+
subjects += frameSubjects;
|
|
83347
|
+
if (frame.complete && frameSubjects === 0) hardNegatives += 1;
|
|
83348
|
+
}
|
|
83349
|
+
const frames = manifest.frames.length;
|
|
83350
|
+
return {
|
|
83351
|
+
frames,
|
|
83352
|
+
completeFrames,
|
|
83353
|
+
completeness: frames > 0 ? completeFrames / frames : 0,
|
|
83354
|
+
subjects,
|
|
83355
|
+
modelErrors,
|
|
83356
|
+
hardNegatives,
|
|
83357
|
+
cells: [...cells.values()].map((e) => e.cell).toSorted((a, b) => a.macroClass.localeCompare(b.macroClass) || a.deviceId - b.deviceId || a.lighting.localeCompare(b.lighting)),
|
|
83358
|
+
cameras: [...cameras.entries()].map(([deviceId, c]) => ({
|
|
83359
|
+
deviceId,
|
|
83360
|
+
...c
|
|
83361
|
+
})).toSorted((a, b) => a.deviceId - b.deviceId)
|
|
83362
|
+
};
|
|
83363
|
+
}
|
|
83364
|
+
/** A coordinate at the precision a box is meaningful to (sub-pixel noise off). */
|
|
83365
|
+
var q = (n) => Math.round(n * 1e4) / 1e4;
|
|
83366
|
+
/**
|
|
83367
|
+
* The sha256 of what the frames TEACH: per frame its completeness and, per
|
|
83368
|
+
* box, the class tiers, the kind and the box. Row ids, timestamps, export
|
|
83369
|
+
* stamps and provenance are left out — they change on a re-save that teaches
|
|
83370
|
+
* the same thing. Order-independent.
|
|
83371
|
+
*/
|
|
83372
|
+
function datasetAnnotationHash(frames) {
|
|
83373
|
+
const canonical = frames.map((f) => ({
|
|
83374
|
+
f: f.frameId,
|
|
83375
|
+
c: f.complete,
|
|
83376
|
+
a: f.annotations.map((a) => [
|
|
83377
|
+
a.macroClass,
|
|
83378
|
+
a.label ?? "",
|
|
83379
|
+
a.subLabel ?? "",
|
|
83380
|
+
a.kind,
|
|
83381
|
+
q(a.bbox.x),
|
|
83382
|
+
q(a.bbox.y),
|
|
83383
|
+
q(a.bbox.w),
|
|
83384
|
+
q(a.bbox.h)
|
|
83385
|
+
].join("|")).toSorted()
|
|
83386
|
+
})).toSorted((a, b) => a.f < b.f ? -1 : a.f > b.f ? 1 : 0);
|
|
83387
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
83388
|
+
}
|
|
83389
|
+
/**
|
|
83390
|
+
* The manifest of a version's export, saying which version and whether its
|
|
83391
|
+
* frames still teach what was frozen. Frames of the version missing from the
|
|
83392
|
+
* archive count as empty and incomplete, so their absence changes the hash.
|
|
83393
|
+
*/
|
|
83394
|
+
function withDatasetVersion(manifest, version) {
|
|
83395
|
+
const present = new Map(manifest.frames.map((f) => [f.frameId, f]));
|
|
83396
|
+
const annotationHashNow = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
|
|
83397
|
+
frameId,
|
|
83398
|
+
complete: false,
|
|
83399
|
+
annotations: []
|
|
83400
|
+
}));
|
|
83401
|
+
return {
|
|
83402
|
+
...manifest,
|
|
83403
|
+
datasetVersion: {
|
|
83404
|
+
id: version.id,
|
|
83405
|
+
name: version.name,
|
|
83406
|
+
createdAt: version.createdAt,
|
|
83407
|
+
annotationHash: version.annotationHash,
|
|
83408
|
+
annotationHashNow,
|
|
83409
|
+
matches: annotationHashNow === version.annotationHash,
|
|
83410
|
+
missingFrames: version.frameIds.filter((id) => !present.has(id)).length
|
|
83411
|
+
}
|
|
83412
|
+
};
|
|
83413
|
+
}
|
|
83414
|
+
//#endregion
|
|
83415
|
+
//#region src/pipeline-analytics/retrain/retrain-geometry.ts
|
|
83416
|
+
function clamp01(v) {
|
|
83417
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
83418
|
+
}
|
|
83419
|
+
/** Is this a usable rectangle at all? A zero-area box is not a subject. */
|
|
83420
|
+
function isUsableRect(r) {
|
|
83421
|
+
return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
|
|
83422
|
+
}
|
|
83423
|
+
/**
|
|
83424
|
+
* The subject inside a square LETTERBOX of the full frame.
|
|
83425
|
+
*
|
|
83426
|
+
* The frame is scaled to fit the square and centred; the padding bars take the
|
|
83427
|
+
* rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
|
|
83428
|
+
* height, and every y coordinate shifts by half the leftover.
|
|
83429
|
+
*
|
|
83430
|
+
* Square-side agnostic on purpose: the answer is normalised against the square,
|
|
83431
|
+
* so it is the same number for a 640 and a 1280 input and no caller has to know
|
|
83432
|
+
* which the model uses.
|
|
83433
|
+
*/
|
|
83434
|
+
function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
|
|
83435
|
+
if (!isUsableRect(bbox)) return null;
|
|
83436
|
+
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
83437
|
+
const longSide = Math.max(frameWidth, frameHeight);
|
|
83438
|
+
const scaleX = frameWidth / longSide;
|
|
83439
|
+
const scaleY = frameHeight / longSide;
|
|
83440
|
+
const offsetX = (1 - scaleX) / 2;
|
|
83441
|
+
const offsetY = (1 - scaleY) / 2;
|
|
83442
|
+
return {
|
|
83443
|
+
bbox: {
|
|
83444
|
+
x: offsetX + bbox.x * scaleX,
|
|
83445
|
+
y: offsetY + bbox.y * scaleY,
|
|
83446
|
+
w: bbox.w * scaleX,
|
|
83447
|
+
h: bbox.h * scaleY
|
|
83448
|
+
},
|
|
83449
|
+
visibleFraction: 1
|
|
83450
|
+
};
|
|
83451
|
+
}
|
|
83452
|
+
/**
|
|
83453
|
+
* The subject inside a ZONE CROP — the shape a package model is trained on.
|
|
83454
|
+
*
|
|
83455
|
+
* Returns `null` when the subject does not intersect the zone at all. That is
|
|
83456
|
+
* the whole reason the package proposal runs on the crop rather than the frame:
|
|
83457
|
+
* a package detector at 0.35 on a full frame proposes furniture, and a subject
|
|
83458
|
+
* outside the zone is not part of this model's world.
|
|
83459
|
+
*/
|
|
83460
|
+
function deriveZoneCropRect(bbox, zone) {
|
|
83461
|
+
if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
|
|
83462
|
+
const left = Math.max(bbox.x, zone.x);
|
|
83463
|
+
const top = Math.max(bbox.y, zone.y);
|
|
83464
|
+
const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
|
|
83465
|
+
const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
|
|
83466
|
+
if (!(right > left) || !(bottom > top)) return null;
|
|
83467
|
+
const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
|
|
83468
|
+
return {
|
|
83469
|
+
bbox: {
|
|
83470
|
+
x: (left - zone.x) / zone.w,
|
|
83471
|
+
y: (top - zone.y) / zone.h,
|
|
83472
|
+
w: (right - left) / zone.w,
|
|
83473
|
+
h: (bottom - top) / zone.h
|
|
83474
|
+
},
|
|
83475
|
+
visibleFraction
|
|
83476
|
+
};
|
|
83477
|
+
}
|
|
83478
|
+
/**
|
|
83479
|
+
* The subject inside its OWN padded crop — the shape a classifier is trained on.
|
|
83480
|
+
*
|
|
83481
|
+
* The crop is the box grown by `padding` of its own size on each side and then
|
|
83482
|
+
* clipped to the frame, so the subject sits in the middle and the classifier
|
|
83483
|
+
* still sees a little context. The returned rectangle is where the subject
|
|
83484
|
+
* lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
|
|
83485
|
+
* clipped by a frame edge — a subject at the left border is flush left in its
|
|
83486
|
+
* crop, and a caller that assumed the centre would draw the box in the wrong
|
|
83487
|
+
* place.
|
|
83488
|
+
*/
|
|
83489
|
+
function deriveSubjectCropRect(bbox, padding) {
|
|
83490
|
+
if (!isUsableRect(bbox)) return null;
|
|
83491
|
+
const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
|
|
83492
|
+
const cropX = clamp01(bbox.x - bbox.w * pad);
|
|
83493
|
+
const cropY = clamp01(bbox.y - bbox.h * pad);
|
|
83494
|
+
const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
|
|
83495
|
+
const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
|
|
83496
|
+
const cropW = cropRight - cropX;
|
|
83497
|
+
const cropH = cropBottom - cropY;
|
|
83498
|
+
if (!(cropW > 0) || !(cropH > 0)) return null;
|
|
83499
|
+
return {
|
|
83500
|
+
bbox: {
|
|
83501
|
+
x: (bbox.x - cropX) / cropW,
|
|
83502
|
+
y: (bbox.y - cropY) / cropH,
|
|
83503
|
+
w: bbox.w / cropW,
|
|
83504
|
+
h: bbox.h / cropH
|
|
83505
|
+
},
|
|
83506
|
+
visibleFraction: 1
|
|
83507
|
+
};
|
|
83508
|
+
}
|
|
83509
|
+
//#endregion
|
|
83510
|
+
//#region src/pipeline-analytics/retrain/retrain-lighting.ts
|
|
83511
|
+
function isNight(condition) {
|
|
83512
|
+
return condition === "night" || condition === "ir";
|
|
83513
|
+
}
|
|
83514
|
+
function retrainFrameLighting(input) {
|
|
83515
|
+
if (input.capturedAt === void 0) {
|
|
83516
|
+
const measured = resolveConditionDetailed({
|
|
83517
|
+
...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
|
|
83518
|
+
now: /* @__PURE__ */ new Date(0)
|
|
83519
|
+
});
|
|
83520
|
+
return measured.source === "ir-chroma" ? {
|
|
83521
|
+
condition: measured.condition,
|
|
83522
|
+
night: true,
|
|
83523
|
+
source: measured.source
|
|
83524
|
+
} : null;
|
|
83525
|
+
}
|
|
83526
|
+
const resolved = resolveConditionDetailed({
|
|
83527
|
+
...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
|
|
83528
|
+
...input.coordinates !== null ? { coordinates: input.coordinates } : {},
|
|
83529
|
+
now: new Date(input.capturedAt)
|
|
83530
|
+
});
|
|
83531
|
+
return {
|
|
83532
|
+
...resolved,
|
|
83533
|
+
night: isNight(resolved.condition)
|
|
83534
|
+
};
|
|
83535
|
+
}
|
|
83536
|
+
//#endregion
|
|
83537
|
+
//#region src/pipeline-analytics/retrain/retrain-export.ts
|
|
83538
|
+
/**
|
|
83539
|
+
* The ANNOTATED dataset export — the retrain page's own archive.
|
|
83540
|
+
*
|
|
83541
|
+
* Sibling of `training-export/`, deliberately not the same thing. That one
|
|
83542
|
+
* streams a marked track's stored media verbatim and knows nothing about what
|
|
83543
|
+
* is in it; this one streams the frames the operator SELECTED (already copied
|
|
83544
|
+
* into retrain storage) plus the ground truth they drew.
|
|
83545
|
+
*
|
|
83546
|
+
* ## The one interesting decision
|
|
83547
|
+
*
|
|
83548
|
+
* `annotations.json` carries, for every subject, the canonical full-frame box
|
|
83549
|
+
* AND the geometry derived for each model shape. **Derived here, at export,
|
|
83550
|
+
* never stored** — one box in, three shapes out. Storing the derived shapes is
|
|
83551
|
+
* how two crops of the same subject end up in one feature space, which is the
|
|
83552
|
+
* failure already recorded for CLIP (D52). A consumer that wants a different
|
|
83553
|
+
* input size or a different padding re-derives from the canonical box; nothing
|
|
83554
|
+
* in the database has to change and nothing can disagree.
|
|
83555
|
+
*
|
|
83556
|
+
* Everything else follows the export next door: index reads to plan, one JPEG
|
|
83557
|
+
* in memory at a time, a yield between files, and NO inference — a training
|
|
83558
|
+
* set's pixels are the pixels the pipeline saw.
|
|
83559
|
+
*/
|
|
83560
|
+
var RETRAIN_EXPORT_PREFIX = "retrain-export";
|
|
83561
|
+
/**
|
|
83562
|
+
* Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
|
|
83563
|
+
* padding, read from the one convention `deriveDetailCropRect` cuts with
|
|
83564
|
+
* (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
|
|
83565
|
+
* here while the runtime fed `0.15`, so a classifier trained from this export
|
|
83566
|
+
* learned on more context than it is ever given live.
|
|
83567
|
+
*/
|
|
83568
|
+
var CLASSIFIER_CROP_PADDING = DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
83569
|
+
/** The convention a `crops` layout cuts with — the runtime default, recorded
|
|
83570
|
+
* in the manifest so a consumer never has to assume it. */
|
|
83571
|
+
var RETRAIN_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
|
|
83572
|
+
/** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
|
|
83573
|
+
* the trainer resizes per model, so this only bounds the upload. */
|
|
83574
|
+
var RETRAIN_CROP_MAX_SIDE = 256;
|
|
83575
|
+
/**
|
|
83576
|
+
* Manifest schema id. v2 = capturedAt / complete / lighting / layout.
|
|
83577
|
+
* v3 = `origin` (`track` | `recording`), `epochMs` on a recording still,
|
|
83578
|
+
* `collectionIds`, and `trackId` ABSENT on a recording still, which has none.
|
|
83579
|
+
*/
|
|
83580
|
+
var RETRAIN_MANIFEST_SCHEMA = "camstack.retrain-export/v3";
|
|
83581
|
+
/** Archive path of a frame: `frames/<deviceId>/<trackId>/<frameId leaf>.jpg`,
|
|
83582
|
+
* or `frames/<deviceId>/recording/<leaf>.jpg` for a recording still. */
|
|
83583
|
+
function retrainFramePath(frame) {
|
|
83584
|
+
const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
|
|
83585
|
+
const dir = frame.origin === "recording" ? RECORDING_STILL_DIR : frame.trackId;
|
|
83586
|
+
return `frames/${frame.deviceId}/${dir}/${leaf}.jpg`;
|
|
83587
|
+
}
|
|
83588
|
+
/** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
|
|
83589
|
+
function retrainCropPath(macroClass, annotationId) {
|
|
83590
|
+
return `crops/${macroClass}/${annotationId}.jpg`;
|
|
83591
|
+
}
|
|
83592
|
+
/**
|
|
83593
|
+
* Narrow the planned frames to a filter. Pure. Every field narrows; an empty
|
|
83594
|
+
* filter keeps everything. Order is the input's.
|
|
83595
|
+
*/
|
|
83596
|
+
function filterRetrainExport(frames, annotationsByFrame, filter, collectionsByFrame = /* @__PURE__ */ new Map()) {
|
|
83597
|
+
const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
|
|
83598
|
+
const collections = filter.collectionIds !== void 0 && filter.collectionIds.length > 0 ? new Set(filter.collectionIds) : null;
|
|
83599
|
+
const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
|
|
83600
|
+
return frames.filter((frame) => {
|
|
83601
|
+
if (wanted !== null && !wanted.has(frame.frameId)) return false;
|
|
83602
|
+
if (devices !== null && !devices.has(frame.deviceId)) return false;
|
|
83603
|
+
if (filter.completeOnly === true && !frame.complete) return false;
|
|
83604
|
+
if (collections !== null && !(collectionsByFrame.get(frame.frameId) ?? []).some((id) => collections.has(id))) return false;
|
|
83605
|
+
if (filter.batchId !== void 0) {
|
|
83606
|
+
if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
|
|
83607
|
+
}
|
|
83608
|
+
return true;
|
|
83609
|
+
});
|
|
83610
|
+
}
|
|
83611
|
+
/**
|
|
83612
|
+
* Derive every per-model shape for one annotation.
|
|
83613
|
+
*
|
|
83614
|
+
* `zone` is optional and per-frame: only a package annotation has one, and a
|
|
83615
|
+
* `zoneCrop` for a subject the zone does not contain is ABSENT rather than
|
|
83616
|
+
* clamped — a clamped box teaches the model that the subject ends where the
|
|
83617
|
+
* crop ends.
|
|
83618
|
+
*/
|
|
83619
|
+
function deriveAll(annotation, frame, zone) {
|
|
83620
|
+
const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
|
|
83621
|
+
const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
|
|
83622
|
+
const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
|
|
83623
|
+
return {
|
|
83624
|
+
...letterbox !== null ? { letterbox } : {},
|
|
83625
|
+
...zoneCrop !== null ? { zoneCrop } : {},
|
|
83626
|
+
...subjectCrop !== null ? { subjectCrop } : {}
|
|
83627
|
+
};
|
|
83628
|
+
}
|
|
83629
|
+
/** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
|
|
83630
|
+
function buildRetrainManifest(input) {
|
|
83631
|
+
const layout = input.layout ?? "frames";
|
|
83632
|
+
const coordinates = input.coordinates ?? null;
|
|
83633
|
+
let annotationCount = 0;
|
|
83634
|
+
let modelErrorCount = 0;
|
|
83635
|
+
const frames = input.frames.map((frame) => {
|
|
83636
|
+
const zone = input.zoneForDevice(frame.deviceId);
|
|
83637
|
+
const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
|
|
83638
|
+
annotationCount += 1;
|
|
83639
|
+
if (a.kind === "model_error") modelErrorCount += 1;
|
|
83640
|
+
const derived = deriveAll(a, frame, zone);
|
|
82649
83641
|
return {
|
|
82650
|
-
id:
|
|
82651
|
-
|
|
82652
|
-
|
|
82653
|
-
|
|
83642
|
+
id: a.id,
|
|
83643
|
+
macroClass: a.macroClass,
|
|
83644
|
+
...a.label !== void 0 ? { label: a.label } : {},
|
|
83645
|
+
...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
|
|
83646
|
+
kind: a.kind,
|
|
83647
|
+
source: a.source,
|
|
83648
|
+
...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
|
|
83649
|
+
...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
|
|
83650
|
+
bbox: a.bbox,
|
|
83651
|
+
...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
|
|
83652
|
+
derived
|
|
83653
|
+
};
|
|
83654
|
+
});
|
|
83655
|
+
return {
|
|
83656
|
+
frameId: frame.frameId,
|
|
83657
|
+
path: retrainFramePath(frame),
|
|
83658
|
+
deviceId: frame.deviceId,
|
|
83659
|
+
origin: frame.origin,
|
|
83660
|
+
...frame.origin === "track" ? { trackId: frame.trackId } : {},
|
|
83661
|
+
...frame.epochMs !== void 0 ? { epochMs: frame.epochMs } : {},
|
|
83662
|
+
collectionIds: input.collectionsByFrame?.get(frame.frameId) ?? [],
|
|
83663
|
+
width: frame.width,
|
|
83664
|
+
height: frame.height,
|
|
83665
|
+
sizeBytes: frame.sizeBytes,
|
|
83666
|
+
sourceKind: frame.sourceKind,
|
|
83667
|
+
copiedAt: frame.copiedAt,
|
|
83668
|
+
...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
|
|
83669
|
+
complete: frame.complete,
|
|
83670
|
+
lighting: retrainFrameLighting({
|
|
83671
|
+
...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
|
|
83672
|
+
...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
|
|
83673
|
+
coordinates
|
|
83674
|
+
}),
|
|
83675
|
+
...zone !== null ? { zoneUsed: zone } : {},
|
|
83676
|
+
annotations
|
|
83677
|
+
};
|
|
83678
|
+
});
|
|
83679
|
+
return {
|
|
83680
|
+
schema: RETRAIN_MANIFEST_SCHEMA,
|
|
83681
|
+
layout,
|
|
83682
|
+
detailCropConvention: RETRAIN_CROP_CONVENTION,
|
|
83683
|
+
...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
|
|
83684
|
+
generatedAt: input.generatedAt,
|
|
83685
|
+
batchId: input.batchId,
|
|
83686
|
+
frameCount: frames.length,
|
|
83687
|
+
annotationCount,
|
|
83688
|
+
modelErrorCount,
|
|
83689
|
+
classifierCropPadding: CLASSIFIER_CROP_PADDING,
|
|
83690
|
+
frames
|
|
83691
|
+
};
|
|
83692
|
+
}
|
|
83693
|
+
//#endregion
|
|
83694
|
+
//#region src/pipeline-analytics/retrain/retrain-dataset-service.ts
|
|
83695
|
+
function filterOf(selection, completeOnly) {
|
|
83696
|
+
return {
|
|
83697
|
+
...selection.deviceIds !== void 0 && selection.deviceIds.length > 0 ? { deviceIds: [...selection.deviceIds] } : {},
|
|
83698
|
+
...selection.collectionIds !== void 0 && selection.collectionIds.length > 0 ? { collectionIds: [...selection.collectionIds] } : {},
|
|
83699
|
+
...selection.frameIds !== void 0 && selection.frameIds.length > 0 ? { frameIds: [...selection.frameIds] } : {},
|
|
83700
|
+
...completeOnly ? { completeOnly: true } : {}
|
|
83701
|
+
};
|
|
83702
|
+
}
|
|
83703
|
+
function summaryOf(version) {
|
|
83704
|
+
const { frameIds, ...rest } = version;
|
|
83705
|
+
return {
|
|
83706
|
+
...rest,
|
|
83707
|
+
frameCount: frameIds.length
|
|
83708
|
+
};
|
|
83709
|
+
}
|
|
83710
|
+
var RetrainDatasetService = class {
|
|
83711
|
+
deps;
|
|
83712
|
+
constructor(deps) {
|
|
83713
|
+
this.deps = deps;
|
|
83714
|
+
}
|
|
83715
|
+
/** The counts a selection would freeze, without freezing anything. */
|
|
83716
|
+
async preview(input) {
|
|
83717
|
+
return datasetStats((await this.deps.buildPlan({
|
|
83718
|
+
filter: filterOf(input.selection, input.completeOnly),
|
|
83719
|
+
layout: "frames"
|
|
83720
|
+
})).manifest);
|
|
83721
|
+
}
|
|
83722
|
+
async create(input) {
|
|
83723
|
+
const name = input.name.trim();
|
|
83724
|
+
if (name === "") return {
|
|
83725
|
+
kind: "refused",
|
|
83726
|
+
reason: "empty-name"
|
|
83727
|
+
};
|
|
83728
|
+
if (await this.deps.versions.findIdByName(name) !== null) return {
|
|
83729
|
+
kind: "refused",
|
|
83730
|
+
reason: "duplicate-name"
|
|
83731
|
+
};
|
|
83732
|
+
const plan = await this.deps.buildPlan({
|
|
83733
|
+
filter: filterOf(input.selection, input.completeOnly),
|
|
83734
|
+
layout: input.layout
|
|
83735
|
+
});
|
|
83736
|
+
if (plan.manifest.frames.length === 0) return {
|
|
83737
|
+
kind: "refused",
|
|
83738
|
+
reason: "no-frames"
|
|
83739
|
+
};
|
|
83740
|
+
const version = {
|
|
83741
|
+
id: this.deps.newId(),
|
|
83742
|
+
name,
|
|
83743
|
+
createdAt: this.deps.now(),
|
|
83744
|
+
selection: input.selection,
|
|
83745
|
+
layout: input.layout,
|
|
83746
|
+
completeOnly: input.completeOnly,
|
|
83747
|
+
frameIds: plan.manifest.frames.map((f) => f.frameId).toSorted(),
|
|
83748
|
+
annotationHash: datasetAnnotationHash(plan.manifest.frames),
|
|
83749
|
+
manifestSchema: RETRAIN_MANIFEST_SCHEMA,
|
|
83750
|
+
stats: datasetStats(plan.manifest)
|
|
83751
|
+
};
|
|
83752
|
+
await this.deps.versions.insert(version);
|
|
83753
|
+
this.deps.logger.info("retrain dataset version created", { meta: {
|
|
83754
|
+
versionId: version.id,
|
|
83755
|
+
frames: version.frameIds.length,
|
|
83756
|
+
subjects: version.stats.subjects,
|
|
83757
|
+
completeness: version.stats.completeness,
|
|
83758
|
+
layout: version.layout
|
|
83759
|
+
} });
|
|
83760
|
+
return {
|
|
83761
|
+
kind: "created",
|
|
83762
|
+
version: summaryOf(version)
|
|
83763
|
+
};
|
|
83764
|
+
}
|
|
83765
|
+
async list() {
|
|
83766
|
+
return (await this.deps.versions.list()).map(summaryOf);
|
|
83767
|
+
}
|
|
83768
|
+
async delete(input) {
|
|
83769
|
+
const deleted = await this.deps.versions.remove(input.id);
|
|
83770
|
+
if (deleted) this.deps.logger.info("retrain dataset version deleted", { meta: { versionId: input.id } });
|
|
83771
|
+
return { deleted };
|
|
83772
|
+
}
|
|
83773
|
+
/** Does the corpus still teach what the version froze? */
|
|
83774
|
+
async verify(input) {
|
|
83775
|
+
const version = await this.deps.versions.get(input.id);
|
|
83776
|
+
if (version === null) return {
|
|
83777
|
+
found: false,
|
|
83778
|
+
annotationHash: "",
|
|
83779
|
+
matches: false,
|
|
83780
|
+
missingFrames: 0
|
|
83781
|
+
};
|
|
83782
|
+
const plan = await this.deps.buildPlan({
|
|
83783
|
+
filter: { frameIds: [...version.frameIds] },
|
|
83784
|
+
layout: "frames"
|
|
83785
|
+
});
|
|
83786
|
+
const present = new Map(plan.manifest.frames.map((f) => [f.frameId, f]));
|
|
83787
|
+
const annotationHash = datasetAnnotationHash(version.frameIds.map((frameId) => present.get(frameId) ?? {
|
|
83788
|
+
frameId,
|
|
83789
|
+
complete: false,
|
|
83790
|
+
annotations: []
|
|
83791
|
+
}));
|
|
83792
|
+
return {
|
|
83793
|
+
found: true,
|
|
83794
|
+
annotationHash,
|
|
83795
|
+
matches: annotationHash === version.annotationHash,
|
|
83796
|
+
missingFrames: version.frameIds.filter((id) => !present.has(id)).length
|
|
83797
|
+
};
|
|
83798
|
+
}
|
|
83799
|
+
/**
|
|
83800
|
+
* An export filter with `datasetVersionId` resolved to that version's
|
|
83801
|
+
* frames (and its completeOnly). An unknown version exports NOTHING — never
|
|
83802
|
+
* the whole dataset, which is what dropping the field would do.
|
|
83803
|
+
*/
|
|
83804
|
+
async exportFilterFor(filter) {
|
|
83805
|
+
if (filter.datasetVersionId === void 0) return filter;
|
|
83806
|
+
const { datasetVersionId, ...rest } = filter;
|
|
83807
|
+
const version = await this.deps.versions.get(datasetVersionId);
|
|
83808
|
+
if (version === null) {
|
|
83809
|
+
this.deps.logger.warn("retrain export asked for an unknown dataset version", { meta: { versionId: datasetVersionId } });
|
|
83810
|
+
return {
|
|
83811
|
+
...rest,
|
|
83812
|
+
frameIds: []
|
|
82654
83813
|
};
|
|
83814
|
+
}
|
|
83815
|
+
const wanted = rest.frameIds !== void 0 ? version.frameIds.filter((id) => rest.frameIds?.includes(id) === true) : version.frameIds;
|
|
83816
|
+
return {
|
|
83817
|
+
...rest,
|
|
83818
|
+
frameIds: [...wanted],
|
|
83819
|
+
...version.completeOnly ? { completeOnly: true } : {}
|
|
83820
|
+
};
|
|
83821
|
+
}
|
|
83822
|
+
};
|
|
83823
|
+
//#endregion
|
|
83824
|
+
//#region src/pipeline-analytics/retrain/retrain-dataset-version-store.ts
|
|
83825
|
+
/**
|
|
83826
|
+
* @durable class=ledger owner=pipeline-analytics
|
|
83827
|
+
* write="one row per dataset version the operator creates: a frozen frameIds list,
|
|
83828
|
+
* the sha256 of the annotations those frames carried, the export parameters and
|
|
83829
|
+
* the counts at creation. Never updated after the insert — a different selection
|
|
83830
|
+
* is a new version."
|
|
83831
|
+
* retention="none — rows leave only when the operator deletes the version. The
|
|
83832
|
+
* frames themselves are owned by the retrain corpus; deleting a version deletes no
|
|
83833
|
+
* frame, and deleting a frame does not edit a version (verify reports it missing)."
|
|
83834
|
+
*/
|
|
83835
|
+
var RETRAIN_DATASET_VERSIONS_COLLECTION = "pipeline-analytics:retrain-dataset-versions";
|
|
83836
|
+
var COLUMNS = [
|
|
83837
|
+
{
|
|
83838
|
+
name: "id",
|
|
83839
|
+
type: "TEXT",
|
|
83840
|
+
primaryKey: true,
|
|
83841
|
+
notNull: true
|
|
83842
|
+
},
|
|
83843
|
+
{
|
|
83844
|
+
name: "name",
|
|
83845
|
+
type: "TEXT",
|
|
83846
|
+
notNull: true
|
|
83847
|
+
},
|
|
83848
|
+
{
|
|
83849
|
+
name: "nameKey",
|
|
83850
|
+
type: "TEXT",
|
|
83851
|
+
notNull: true
|
|
83852
|
+
},
|
|
83853
|
+
{
|
|
83854
|
+
name: "createdAt",
|
|
83855
|
+
type: "INTEGER",
|
|
83856
|
+
notNull: true
|
|
83857
|
+
},
|
|
83858
|
+
{
|
|
83859
|
+
name: "selection",
|
|
83860
|
+
type: "JSON",
|
|
83861
|
+
notNull: true
|
|
83862
|
+
},
|
|
83863
|
+
{
|
|
83864
|
+
name: "layout",
|
|
83865
|
+
type: "TEXT",
|
|
83866
|
+
notNull: true
|
|
83867
|
+
},
|
|
83868
|
+
{
|
|
83869
|
+
name: "completeOnly",
|
|
83870
|
+
type: "BOOLEAN",
|
|
83871
|
+
notNull: true
|
|
83872
|
+
},
|
|
83873
|
+
{
|
|
83874
|
+
name: "frameIds",
|
|
83875
|
+
type: "JSON",
|
|
83876
|
+
notNull: true
|
|
83877
|
+
},
|
|
83878
|
+
{
|
|
83879
|
+
name: "annotationHash",
|
|
83880
|
+
type: "TEXT",
|
|
83881
|
+
notNull: true
|
|
83882
|
+
},
|
|
83883
|
+
{
|
|
83884
|
+
name: "manifestSchema",
|
|
83885
|
+
type: "TEXT",
|
|
83886
|
+
notNull: true
|
|
83887
|
+
},
|
|
83888
|
+
{
|
|
83889
|
+
name: "stats",
|
|
83890
|
+
type: "JSON",
|
|
83891
|
+
notNull: true
|
|
83892
|
+
}
|
|
83893
|
+
];
|
|
83894
|
+
var INDEXES = [{
|
|
83895
|
+
name: "idx_retrain_dataset_versions_name_key",
|
|
83896
|
+
columns: ["nameKey"],
|
|
83897
|
+
unique: true
|
|
83898
|
+
}, {
|
|
83899
|
+
name: "idx_retrain_dataset_versions_created",
|
|
83900
|
+
columns: ["createdAt"]
|
|
83901
|
+
}];
|
|
83902
|
+
var nameKeyOf = (name) => name.trim().toLowerCase();
|
|
83903
|
+
function numberList(value) {
|
|
83904
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "number") : void 0;
|
|
83905
|
+
}
|
|
83906
|
+
function stringList(value) {
|
|
83907
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : void 0;
|
|
83908
|
+
}
|
|
83909
|
+
function parseJson(value) {
|
|
83910
|
+
if (typeof value !== "string") return value;
|
|
83911
|
+
try {
|
|
83912
|
+
return JSON.parse(value);
|
|
83913
|
+
} catch {
|
|
83914
|
+
return;
|
|
83915
|
+
}
|
|
83916
|
+
}
|
|
83917
|
+
function asRecord(value) {
|
|
83918
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
|
|
83919
|
+
}
|
|
83920
|
+
var EMPTY_STATS = {
|
|
83921
|
+
frames: 0,
|
|
83922
|
+
completeFrames: 0,
|
|
83923
|
+
completeness: 0,
|
|
83924
|
+
subjects: 0,
|
|
83925
|
+
modelErrors: 0,
|
|
83926
|
+
hardNegatives: 0,
|
|
83927
|
+
cells: [],
|
|
83928
|
+
cameras: []
|
|
83929
|
+
};
|
|
83930
|
+
/** A stored stats object, trusted only as far as its shape. */
|
|
83931
|
+
function readStats(value) {
|
|
83932
|
+
const raw = asRecord(parseJson(value));
|
|
83933
|
+
const num = (k) => typeof raw[k] === "number" ? raw[k] : 0;
|
|
83934
|
+
const cells = Array.isArray(raw["cells"]) ? raw["cells"] : [];
|
|
83935
|
+
const cameras = Array.isArray(raw["cameras"]) ? raw["cameras"] : [];
|
|
83936
|
+
return {
|
|
83937
|
+
...EMPTY_STATS,
|
|
83938
|
+
frames: num("frames"),
|
|
83939
|
+
completeFrames: num("completeFrames"),
|
|
83940
|
+
completeness: num("completeness"),
|
|
83941
|
+
subjects: num("subjects"),
|
|
83942
|
+
modelErrors: num("modelErrors"),
|
|
83943
|
+
hardNegatives: num("hardNegatives"),
|
|
83944
|
+
cells: cells.map((c) => {
|
|
83945
|
+
const r = asRecord(c);
|
|
83946
|
+
const lighting = r["lighting"] === "day" || r["lighting"] === "night" ? r["lighting"] : "unknown";
|
|
83947
|
+
return {
|
|
83948
|
+
macroClass: String(r["macroClass"] ?? ""),
|
|
83949
|
+
deviceId: Number(r["deviceId"] ?? 0),
|
|
83950
|
+
lighting,
|
|
83951
|
+
subjects: Number(r["subjects"] ?? 0),
|
|
83952
|
+
frames: Number(r["frames"] ?? 0)
|
|
83953
|
+
};
|
|
83954
|
+
}),
|
|
83955
|
+
cameras: cameras.map((c) => {
|
|
83956
|
+
const r = asRecord(c);
|
|
83957
|
+
return {
|
|
83958
|
+
deviceId: Number(r["deviceId"] ?? 0),
|
|
83959
|
+
frames: Number(r["frames"] ?? 0),
|
|
83960
|
+
completeFrames: Number(r["completeFrames"] ?? 0)
|
|
83961
|
+
};
|
|
83962
|
+
})
|
|
83963
|
+
};
|
|
83964
|
+
}
|
|
83965
|
+
var RetrainDatasetVersionStore = class {
|
|
83966
|
+
store;
|
|
83967
|
+
logger;
|
|
83968
|
+
constructor(deps) {
|
|
83969
|
+
this.store = deps.store;
|
|
83970
|
+
this.logger = deps.logger;
|
|
83971
|
+
}
|
|
83972
|
+
static async declare(store) {
|
|
83973
|
+
await store.declareCollection.mutate({
|
|
83974
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
83975
|
+
columns: [...COLUMNS],
|
|
83976
|
+
indexes: [...INDEXES]
|
|
83977
|
+
});
|
|
83978
|
+
}
|
|
83979
|
+
insert = async (version) => {
|
|
83980
|
+
await this.store.set.mutate({
|
|
83981
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
83982
|
+
key: version.id,
|
|
83983
|
+
value: {
|
|
83984
|
+
name: version.name,
|
|
83985
|
+
nameKey: nameKeyOf(version.name),
|
|
83986
|
+
createdAt: version.createdAt,
|
|
83987
|
+
selection: version.selection,
|
|
83988
|
+
layout: version.layout,
|
|
83989
|
+
completeOnly: version.completeOnly,
|
|
83990
|
+
frameIds: version.frameIds,
|
|
83991
|
+
annotationHash: version.annotationHash,
|
|
83992
|
+
manifestSchema: version.manifestSchema,
|
|
83993
|
+
stats: version.stats
|
|
83994
|
+
}
|
|
83995
|
+
});
|
|
83996
|
+
};
|
|
83997
|
+
get = async (id) => {
|
|
83998
|
+
try {
|
|
83999
|
+
const row = await this.store.get.query({
|
|
84000
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
84001
|
+
key: id
|
|
84002
|
+
});
|
|
84003
|
+
return row === null || row === void 0 ? null : this.toVersion(id, asRecord(row));
|
|
82655
84004
|
} catch (err) {
|
|
82656
|
-
this.logger.warn("retrain
|
|
82657
|
-
|
|
84005
|
+
this.logger.warn("retrain dataset version read failed", { meta: {
|
|
84006
|
+
versionId: id,
|
|
82658
84007
|
error: String(err)
|
|
82659
84008
|
} });
|
|
82660
84009
|
return null;
|
|
82661
84010
|
}
|
|
84011
|
+
};
|
|
84012
|
+
list = async () => {
|
|
84013
|
+
try {
|
|
84014
|
+
return (await this.store.query.query({
|
|
84015
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
84016
|
+
filter: { orderBy: {
|
|
84017
|
+
field: "createdAt",
|
|
84018
|
+
direction: "desc"
|
|
84019
|
+
} }
|
|
84020
|
+
})).map((r) => this.toVersion(r.id, asRecord(r.data)));
|
|
84021
|
+
} catch (err) {
|
|
84022
|
+
this.logger.warn("retrain dataset versions list failed", { meta: { error: String(err) } });
|
|
84023
|
+
return [];
|
|
84024
|
+
}
|
|
84025
|
+
};
|
|
84026
|
+
findIdByName = async (name) => {
|
|
84027
|
+
return (await this.store.query.query({
|
|
84028
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
84029
|
+
filter: {
|
|
84030
|
+
where: { nameKey: nameKeyOf(name) },
|
|
84031
|
+
limit: 1
|
|
84032
|
+
}
|
|
84033
|
+
}))[0]?.id ?? null;
|
|
84034
|
+
};
|
|
84035
|
+
remove = async (id) => {
|
|
84036
|
+
if (await this.get(id) === null) return false;
|
|
84037
|
+
await this.store.delete.mutate({
|
|
84038
|
+
collection: RETRAIN_DATASET_VERSIONS_COLLECTION,
|
|
84039
|
+
key: id
|
|
84040
|
+
});
|
|
84041
|
+
return true;
|
|
84042
|
+
};
|
|
84043
|
+
toVersion(id, data) {
|
|
84044
|
+
const selection = asRecord(parseJson(data["selection"]));
|
|
84045
|
+
const deviceIds = numberList(selection["deviceIds"]);
|
|
84046
|
+
const collectionIds = stringList(selection["collectionIds"]);
|
|
84047
|
+
const frameIdsSel = stringList(selection["frameIds"]);
|
|
84048
|
+
return {
|
|
84049
|
+
id,
|
|
84050
|
+
name: String(data["name"] ?? ""),
|
|
84051
|
+
createdAt: Number(data["createdAt"] ?? 0),
|
|
84052
|
+
selection: {
|
|
84053
|
+
...deviceIds !== void 0 ? { deviceIds } : {},
|
|
84054
|
+
...collectionIds !== void 0 ? { collectionIds } : {},
|
|
84055
|
+
...frameIdsSel !== void 0 ? { frameIds: frameIdsSel } : {}
|
|
84056
|
+
},
|
|
84057
|
+
layout: data["layout"] === "crops" ? "crops" : "frames",
|
|
84058
|
+
completeOnly: data["completeOnly"] === true || data["completeOnly"] === 1,
|
|
84059
|
+
frameIds: stringList(parseJson(data["frameIds"])) ?? [],
|
|
84060
|
+
annotationHash: String(data["annotationHash"] ?? ""),
|
|
84061
|
+
manifestSchema: String(data["manifestSchema"] ?? ""),
|
|
84062
|
+
stats: readStats(data["stats"])
|
|
84063
|
+
};
|
|
84064
|
+
}
|
|
84065
|
+
};
|
|
84066
|
+
//#endregion
|
|
84067
|
+
//#region src/pipeline-analytics/retrain/retrain-llm-assist.ts
|
|
84068
|
+
var ALWAYS_LOCAL_KINDS = new Set(["managed-local", "apple-foundation"]);
|
|
84069
|
+
var ALWAYS_CLOUD_KINDS = new Set([
|
|
84070
|
+
"openai",
|
|
84071
|
+
"anthropic",
|
|
84072
|
+
"google"
|
|
84073
|
+
]);
|
|
84074
|
+
function isPrivateIpv4(host) {
|
|
84075
|
+
const parts = host.split(".").map((p) => Number.parseInt(p, 10));
|
|
84076
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return false;
|
|
84077
|
+
const [a, b] = parts;
|
|
84078
|
+
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;
|
|
84079
|
+
}
|
|
84080
|
+
function isLocalHost(rawHost) {
|
|
84081
|
+
const host = rawHost.replace(/^\[|\]$/g, "").toLowerCase();
|
|
84082
|
+
if (host.includes(":")) return host === "::1" || host.startsWith("fc") || host.startsWith("fd") || host.startsWith("fe80");
|
|
84083
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".lan")) return true;
|
|
84084
|
+
if (isPrivateIpv4(host)) return true;
|
|
84085
|
+
return !host.includes(".") && !host.includes(":");
|
|
84086
|
+
}
|
|
84087
|
+
/** Does sending a frame to this profile send it outside the house? */
|
|
84088
|
+
function llmProfileLeavesHouse(profile) {
|
|
84089
|
+
if (ALWAYS_LOCAL_KINDS.has(profile.kind)) return false;
|
|
84090
|
+
if (ALWAYS_CLOUD_KINDS.has(profile.kind)) return true;
|
|
84091
|
+
if (profile.baseUrl === void 0 || profile.baseUrl === "") return true;
|
|
84092
|
+
try {
|
|
84093
|
+
return !isLocalHost(new URL(profile.baseUrl).hostname);
|
|
84094
|
+
} catch {
|
|
84095
|
+
return true;
|
|
84096
|
+
}
|
|
84097
|
+
}
|
|
84098
|
+
/** At most this many boxes are sent for review — a prompt, not a dump. */
|
|
84099
|
+
var LLM_REVIEW_MAX_BOXES = 50;
|
|
84100
|
+
var VERDICTS = [
|
|
84101
|
+
"confirmed",
|
|
84102
|
+
"wrong-class",
|
|
84103
|
+
"not-an-object",
|
|
84104
|
+
"uncertain"
|
|
84105
|
+
];
|
|
84106
|
+
var SYSTEM_PROMPT = [
|
|
84107
|
+
"You annotate frames from home security cameras to build an object-detection training set.",
|
|
84108
|
+
"Be precise and conservative: only report what is clearly visible.",
|
|
84109
|
+
"Coordinates are normalised to the full frame: x and y are the top-left corner,",
|
|
84110
|
+
"w and h the width and height, all between 0 and 1.",
|
|
84111
|
+
"Answer with JSON only, exactly in the requested schema, with no other text."
|
|
84112
|
+
].join(" ");
|
|
84113
|
+
/** The JSON schema asked of the model (and enforced again by the parser). */
|
|
84114
|
+
function answerJsonSchema(classes) {
|
|
84115
|
+
return {
|
|
84116
|
+
type: "object",
|
|
84117
|
+
additionalProperties: false,
|
|
84118
|
+
required: ["objects", "reviews"],
|
|
84119
|
+
properties: {
|
|
84120
|
+
objects: {
|
|
84121
|
+
type: "array",
|
|
84122
|
+
items: {
|
|
84123
|
+
type: "object",
|
|
84124
|
+
additionalProperties: false,
|
|
84125
|
+
required: [
|
|
84126
|
+
"class",
|
|
84127
|
+
"box",
|
|
84128
|
+
"confidence",
|
|
84129
|
+
"reason"
|
|
84130
|
+
],
|
|
84131
|
+
properties: {
|
|
84132
|
+
class: {
|
|
84133
|
+
type: "string",
|
|
84134
|
+
enum: [...classes]
|
|
84135
|
+
},
|
|
84136
|
+
label: { type: "string" },
|
|
84137
|
+
box: {
|
|
84138
|
+
type: "array",
|
|
84139
|
+
items: { type: "number" },
|
|
84140
|
+
minItems: 4,
|
|
84141
|
+
maxItems: 4
|
|
84142
|
+
},
|
|
84143
|
+
confidence: {
|
|
84144
|
+
type: "number",
|
|
84145
|
+
minimum: 0,
|
|
84146
|
+
maximum: 1
|
|
84147
|
+
},
|
|
84148
|
+
reason: { type: "string" }
|
|
84149
|
+
}
|
|
84150
|
+
}
|
|
84151
|
+
},
|
|
84152
|
+
reviews: {
|
|
84153
|
+
type: "array",
|
|
84154
|
+
items: {
|
|
84155
|
+
type: "object",
|
|
84156
|
+
additionalProperties: false,
|
|
84157
|
+
required: [
|
|
84158
|
+
"ref",
|
|
84159
|
+
"verdict",
|
|
84160
|
+
"reason"
|
|
84161
|
+
],
|
|
84162
|
+
properties: {
|
|
84163
|
+
ref: { type: "string" },
|
|
84164
|
+
verdict: {
|
|
84165
|
+
type: "string",
|
|
84166
|
+
enum: [...VERDICTS]
|
|
84167
|
+
},
|
|
84168
|
+
suggestedClass: { type: "string" },
|
|
84169
|
+
reason: { type: "string" }
|
|
84170
|
+
}
|
|
84171
|
+
}
|
|
84172
|
+
}
|
|
84173
|
+
}
|
|
84174
|
+
};
|
|
84175
|
+
}
|
|
84176
|
+
var round = (n) => Math.round(n * 1e3) / 1e3;
|
|
84177
|
+
/** Build the request: target classes, the frame size, the boxes to review. */
|
|
84178
|
+
function buildLlmAssistRequest(input) {
|
|
84179
|
+
const boxes = input.boxes.slice(0, LLM_REVIEW_MAX_BOXES).map((b) => JSON.stringify({
|
|
84180
|
+
ref: b.ref,
|
|
84181
|
+
class: b.macroClass,
|
|
84182
|
+
...b.label !== void 0 ? { label: b.label } : {},
|
|
84183
|
+
box: [
|
|
84184
|
+
round(b.bbox.x),
|
|
84185
|
+
round(b.bbox.y),
|
|
84186
|
+
round(b.bbox.w),
|
|
84187
|
+
round(b.bbox.h)
|
|
84188
|
+
]
|
|
84189
|
+
}));
|
|
84190
|
+
return {
|
|
84191
|
+
system: SYSTEM_PROMPT,
|
|
84192
|
+
prompt: [
|
|
84193
|
+
`The image is one camera frame, ${String(input.width)}×${String(input.height)} pixels.`,
|
|
84194
|
+
`1. List every object of these classes you can see: ${input.classes.join(", ")}.`,
|
|
84195
|
+
" For each give class, an optional free-text label (e.g. \"van\", \"postman\"),",
|
|
84196
|
+
" box [x, y, w, h] normalised to the frame, confidence 0–1, and a short reason.",
|
|
84197
|
+
" Include small and partly hidden ones; do not include anything else.",
|
|
84198
|
+
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\": [].",
|
|
84199
|
+
"Answer with JSON: {\"objects\": [...], \"reviews\": [...]}."
|
|
84200
|
+
].join("\n"),
|
|
84201
|
+
jsonSchema: answerJsonSchema(input.classes)
|
|
84202
|
+
};
|
|
84203
|
+
}
|
|
84204
|
+
/** The whole answer: two arrays and nothing else. Items are checked one by one. */
|
|
84205
|
+
var AnswerSchema = strictObject({
|
|
84206
|
+
objects: array(unknown()),
|
|
84207
|
+
reviews: array(unknown())
|
|
84208
|
+
});
|
|
84209
|
+
var ObjectSchema = strictObject({
|
|
84210
|
+
class: string(),
|
|
84211
|
+
label: string().optional(),
|
|
84212
|
+
box: tuple([
|
|
84213
|
+
number(),
|
|
84214
|
+
number(),
|
|
84215
|
+
number(),
|
|
84216
|
+
number()
|
|
84217
|
+
]),
|
|
84218
|
+
confidence: number().min(0).max(1),
|
|
84219
|
+
reason: string().max(500)
|
|
84220
|
+
});
|
|
84221
|
+
var ReviewSchema = strictObject({
|
|
84222
|
+
ref: string(),
|
|
84223
|
+
verdict: _enum(VERDICTS),
|
|
84224
|
+
suggestedClass: string().optional(),
|
|
84225
|
+
reason: string().max(500)
|
|
84226
|
+
});
|
|
84227
|
+
function frameBox(box) {
|
|
84228
|
+
const [x, y, w, h] = box;
|
|
84229
|
+
if (![
|
|
84230
|
+
x,
|
|
84231
|
+
y,
|
|
84232
|
+
w,
|
|
84233
|
+
h
|
|
84234
|
+
].every(Number.isFinite)) return null;
|
|
84235
|
+
if (!(w > 0) || !(h > 0) || x < -.01 || y < -.01) return null;
|
|
84236
|
+
if (x + w > 1.01 || y + h > 1.01) return null;
|
|
84237
|
+
const cx = Math.max(0, x);
|
|
84238
|
+
const cy = Math.max(0, y);
|
|
84239
|
+
return {
|
|
84240
|
+
x: cx,
|
|
84241
|
+
y: cy,
|
|
84242
|
+
w: Math.min(w, 1 - cx),
|
|
84243
|
+
h: Math.min(h, 1 - cy)
|
|
84244
|
+
};
|
|
84245
|
+
}
|
|
84246
|
+
/** Strip a Markdown code fence a model wrapped its JSON in. */
|
|
84247
|
+
function unfence(text) {
|
|
84248
|
+
const trimmed = text.trim();
|
|
84249
|
+
const fence = /^```(?:json)?\s*\n([\s\S]*?)\n?```$/i.exec(trimmed);
|
|
84250
|
+
return fence?.[1] !== void 0 ? fence[1].trim() : trimmed;
|
|
84251
|
+
}
|
|
84252
|
+
/** A free-text label, never a macro word (the tier rule). */
|
|
84253
|
+
function usableLabel(label) {
|
|
84254
|
+
const trimmed = label?.trim();
|
|
84255
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
84256
|
+
return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
|
|
84257
|
+
}
|
|
84258
|
+
/** Parse the model's answer. See the module header for the rules. */
|
|
84259
|
+
function parseLlmAssistAnswer(text, expected) {
|
|
84260
|
+
let raw;
|
|
84261
|
+
try {
|
|
84262
|
+
raw = JSON.parse(unfence(text));
|
|
84263
|
+
} catch {
|
|
84264
|
+
return {
|
|
84265
|
+
kind: "malformed",
|
|
84266
|
+
detail: "the answer is not JSON"
|
|
84267
|
+
};
|
|
84268
|
+
}
|
|
84269
|
+
const answer = AnswerSchema.safeParse(raw);
|
|
84270
|
+
if (!answer.success) return {
|
|
84271
|
+
kind: "malformed",
|
|
84272
|
+
detail: "the answer is not {\"objects\": [...], \"reviews\": [...]}"
|
|
84273
|
+
};
|
|
84274
|
+
const asked = new Set(expected.classes);
|
|
84275
|
+
let rejected = 0;
|
|
84276
|
+
const objects = [];
|
|
84277
|
+
for (const item of answer.data.objects) {
|
|
84278
|
+
const parsed = ObjectSchema.safeParse(item);
|
|
84279
|
+
const cls = parsed.success ? parsed.data.class.trim().toLowerCase() : "";
|
|
84280
|
+
const bbox = parsed.success ? frameBox(parsed.data.box) : null;
|
|
84281
|
+
if (!parsed.success || bbox === null || !asked.has(cls) || !isRetrainMacroClass(cls)) {
|
|
84282
|
+
rejected += 1;
|
|
84283
|
+
continue;
|
|
84284
|
+
}
|
|
84285
|
+
const label = usableLabel(parsed.data.label);
|
|
84286
|
+
objects.push({
|
|
84287
|
+
macroClass: cls,
|
|
84288
|
+
...label !== void 0 ? { label } : {},
|
|
84289
|
+
bbox,
|
|
84290
|
+
confidence: parsed.data.confidence,
|
|
84291
|
+
reason: parsed.data.reason
|
|
84292
|
+
});
|
|
84293
|
+
}
|
|
84294
|
+
const reviews = [];
|
|
84295
|
+
for (const item of answer.data.reviews) {
|
|
84296
|
+
const parsed = ReviewSchema.safeParse(item);
|
|
84297
|
+
if (!parsed.success || !expected.refs.has(parsed.data.ref)) {
|
|
84298
|
+
rejected += 1;
|
|
84299
|
+
continue;
|
|
84300
|
+
}
|
|
84301
|
+
const suggested = parsed.data.suggestedClass?.trim().toLowerCase();
|
|
84302
|
+
reviews.push({
|
|
84303
|
+
ref: parsed.data.ref,
|
|
84304
|
+
verdict: parsed.data.verdict,
|
|
84305
|
+
...suggested !== void 0 && isRetrainMacroClass(suggested) ? { suggestedClass: suggested } : {},
|
|
84306
|
+
reason: parsed.data.reason
|
|
84307
|
+
});
|
|
84308
|
+
}
|
|
84309
|
+
return {
|
|
84310
|
+
kind: "parsed",
|
|
84311
|
+
objects,
|
|
84312
|
+
reviews,
|
|
84313
|
+
rejected
|
|
84314
|
+
};
|
|
84315
|
+
}
|
|
84316
|
+
//#endregion
|
|
84317
|
+
//#region src/pipeline-analytics/retrain/retrain-llm-service.ts
|
|
84318
|
+
/** The usage tag every AI-assist call is counted under (`llm.getUsage`). */
|
|
84319
|
+
var RETRAIN_LLM_CONSUMER = "retrain-assist";
|
|
84320
|
+
/** One frame, one answer: a cold local vision model can take a minute. */
|
|
84321
|
+
var RETRAIN_LLM_TIMEOUT_MS = 15e4;
|
|
84322
|
+
var RetrainLlmAssistService = class {
|
|
84323
|
+
deps;
|
|
84324
|
+
constructor(deps) {
|
|
84325
|
+
this.deps = deps;
|
|
84326
|
+
}
|
|
84327
|
+
/** The enabled, vision-capable profiles — a text-only one would be refused. */
|
|
84328
|
+
async listProfiles() {
|
|
84329
|
+
return (await this.deps.llm.listProfiles()).filter((p) => p.enabled && p.supportsVision).map((p) => ({
|
|
84330
|
+
id: p.id,
|
|
84331
|
+
name: p.name,
|
|
84332
|
+
model: p.model,
|
|
84333
|
+
kind: p.kind,
|
|
84334
|
+
leavesHouse: llmProfileLeavesHouse(p)
|
|
84335
|
+
}));
|
|
84336
|
+
}
|
|
84337
|
+
async propose(input) {
|
|
84338
|
+
const tags = { deviceId: input.deviceId };
|
|
84339
|
+
const frame = await this.deps.frames.get(input.frameId);
|
|
84340
|
+
if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
|
|
84341
|
+
kind: "refused",
|
|
84342
|
+
reason: "unknown-frame"
|
|
84343
|
+
};
|
|
84344
|
+
const profile = (await this.deps.llm.listProfiles()).find((p) => p.id === input.profileId);
|
|
84345
|
+
if (profile === void 0) return {
|
|
84346
|
+
kind: "refused",
|
|
84347
|
+
reason: "unknown-profile"
|
|
84348
|
+
};
|
|
84349
|
+
if (!profile.enabled) return {
|
|
84350
|
+
kind: "refused",
|
|
84351
|
+
reason: "profile-disabled"
|
|
84352
|
+
};
|
|
84353
|
+
if (!profile.supportsVision) return {
|
|
84354
|
+
kind: "refused",
|
|
84355
|
+
reason: "no-vision"
|
|
84356
|
+
};
|
|
84357
|
+
const leavesHouse = llmProfileLeavesHouse(profile);
|
|
84358
|
+
if (leavesHouse && !input.allowCloud) {
|
|
84359
|
+
this.deps.logger.warn("retrain AI assist refused: a cloud profile without confirmation", {
|
|
84360
|
+
tags,
|
|
84361
|
+
meta: {
|
|
84362
|
+
frameId: input.frameId,
|
|
84363
|
+
profileId: profile.id
|
|
84364
|
+
}
|
|
84365
|
+
});
|
|
84366
|
+
return {
|
|
84367
|
+
kind: "refused",
|
|
84368
|
+
reason: "cloud-not-confirmed",
|
|
84369
|
+
detail: "this profile sends the frame outside the house; confirm the run first"
|
|
84370
|
+
};
|
|
84371
|
+
}
|
|
84372
|
+
const bytes = await this.deps.frames.readFrame(frame);
|
|
84373
|
+
if (bytes === null) return {
|
|
84374
|
+
kind: "refused",
|
|
84375
|
+
reason: "unreadable-frame"
|
|
84376
|
+
};
|
|
84377
|
+
const request = buildLlmAssistRequest({
|
|
84378
|
+
classes: input.classes,
|
|
84379
|
+
width: frame.width,
|
|
84380
|
+
height: frame.height,
|
|
84381
|
+
boxes: input.boxes
|
|
84382
|
+
});
|
|
84383
|
+
const answer = await this.deps.llm.generateVision({
|
|
84384
|
+
profileId: profile.id,
|
|
84385
|
+
system: request.system,
|
|
84386
|
+
prompt: request.prompt,
|
|
84387
|
+
jsonSchema: request.jsonSchema,
|
|
84388
|
+
image: bytes
|
|
84389
|
+
});
|
|
84390
|
+
if (!answer.ok) {
|
|
84391
|
+
this.deps.logger.warn("retrain AI assist: the model call failed", {
|
|
84392
|
+
tags,
|
|
84393
|
+
meta: {
|
|
84394
|
+
frameId: input.frameId,
|
|
84395
|
+
profileId: profile.id,
|
|
84396
|
+
code: answer.code
|
|
84397
|
+
}
|
|
84398
|
+
});
|
|
84399
|
+
return {
|
|
84400
|
+
kind: "refused",
|
|
84401
|
+
reason: "llm-error",
|
|
84402
|
+
detail: `${answer.code}: ${answer.message}`
|
|
84403
|
+
};
|
|
84404
|
+
}
|
|
84405
|
+
const parsed = parseLlmAssistAnswer(answer.text, {
|
|
84406
|
+
classes: input.classes,
|
|
84407
|
+
refs: new Set(input.boxes.map((b) => b.ref))
|
|
84408
|
+
});
|
|
84409
|
+
if (parsed.kind === "malformed") {
|
|
84410
|
+
this.deps.logger.warn("retrain AI assist: the answer was refused as malformed", {
|
|
84411
|
+
tags,
|
|
84412
|
+
meta: {
|
|
84413
|
+
frameId: input.frameId,
|
|
84414
|
+
profileId: profile.id,
|
|
84415
|
+
model: answer.model,
|
|
84416
|
+
detail: parsed.detail,
|
|
84417
|
+
answerChars: answer.text.length
|
|
84418
|
+
}
|
|
84419
|
+
});
|
|
84420
|
+
return {
|
|
84421
|
+
kind: "refused",
|
|
84422
|
+
reason: "malformed-answer",
|
|
84423
|
+
detail: parsed.detail
|
|
84424
|
+
};
|
|
84425
|
+
}
|
|
84426
|
+
this.deps.logger.info("retrain AI assist proposed boxes", {
|
|
84427
|
+
tags,
|
|
84428
|
+
meta: {
|
|
84429
|
+
frameId: input.frameId,
|
|
84430
|
+
profileId: profile.id,
|
|
84431
|
+
model: answer.model,
|
|
84432
|
+
leftHouse: leavesHouse,
|
|
84433
|
+
proposals: parsed.objects.length,
|
|
84434
|
+
reviews: parsed.reviews.length,
|
|
84435
|
+
rejected: parsed.rejected
|
|
84436
|
+
}
|
|
84437
|
+
});
|
|
84438
|
+
return {
|
|
84439
|
+
kind: "proposed",
|
|
84440
|
+
profileId: profile.id,
|
|
84441
|
+
model: answer.model,
|
|
84442
|
+
leftHouse: leavesHouse,
|
|
84443
|
+
proposals: parsed.objects.map((o) => ({
|
|
84444
|
+
bbox: o.bbox,
|
|
84445
|
+
macroClass: o.macroClass,
|
|
84446
|
+
...o.label !== void 0 ? { label: o.label } : {},
|
|
84447
|
+
kind: "subject",
|
|
84448
|
+
source: "llm",
|
|
84449
|
+
assistModelId: answer.model,
|
|
84450
|
+
assistScore: o.confidence,
|
|
84451
|
+
reason: o.reason
|
|
84452
|
+
})),
|
|
84453
|
+
reviews: parsed.reviews.map((r) => ({
|
|
84454
|
+
ref: r.ref,
|
|
84455
|
+
verdict: r.verdict,
|
|
84456
|
+
...r.suggestedClass !== void 0 ? { suggestedClass: r.suggestedClass } : {},
|
|
84457
|
+
reason: r.reason
|
|
84458
|
+
})),
|
|
84459
|
+
rejected: parsed.rejected
|
|
84460
|
+
};
|
|
82662
84461
|
}
|
|
82663
84462
|
};
|
|
82664
84463
|
//#endregion
|
|
@@ -82978,274 +84777,6 @@ async function cutRetrainCrop(input) {
|
|
|
82978
84777
|
return pipeline.jpeg({ quality: 92 }).toBuffer();
|
|
82979
84778
|
}
|
|
82980
84779
|
//#endregion
|
|
82981
|
-
//#region src/pipeline-analytics/retrain/retrain-geometry.ts
|
|
82982
|
-
function clamp01(v) {
|
|
82983
|
-
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
82984
|
-
}
|
|
82985
|
-
/** Is this a usable rectangle at all? A zero-area box is not a subject. */
|
|
82986
|
-
function isUsableRect(r) {
|
|
82987
|
-
return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
|
|
82988
|
-
}
|
|
82989
|
-
/**
|
|
82990
|
-
* The subject inside a square LETTERBOX of the full frame.
|
|
82991
|
-
*
|
|
82992
|
-
* The frame is scaled to fit the square and centred; the padding bars take the
|
|
82993
|
-
* rest. A wide 16:9 frame therefore occupies the middle 9/16 of the square's
|
|
82994
|
-
* height, and every y coordinate shifts by half the leftover.
|
|
82995
|
-
*
|
|
82996
|
-
* Square-side agnostic on purpose: the answer is normalised against the square,
|
|
82997
|
-
* so it is the same number for a 640 and a 1280 input and no caller has to know
|
|
82998
|
-
* which the model uses.
|
|
82999
|
-
*/
|
|
83000
|
-
function deriveLetterboxRect(bbox, frameWidth, frameHeight) {
|
|
83001
|
-
if (!isUsableRect(bbox)) return null;
|
|
83002
|
-
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
83003
|
-
const longSide = Math.max(frameWidth, frameHeight);
|
|
83004
|
-
const scaleX = frameWidth / longSide;
|
|
83005
|
-
const scaleY = frameHeight / longSide;
|
|
83006
|
-
const offsetX = (1 - scaleX) / 2;
|
|
83007
|
-
const offsetY = (1 - scaleY) / 2;
|
|
83008
|
-
return {
|
|
83009
|
-
bbox: {
|
|
83010
|
-
x: offsetX + bbox.x * scaleX,
|
|
83011
|
-
y: offsetY + bbox.y * scaleY,
|
|
83012
|
-
w: bbox.w * scaleX,
|
|
83013
|
-
h: bbox.h * scaleY
|
|
83014
|
-
},
|
|
83015
|
-
visibleFraction: 1
|
|
83016
|
-
};
|
|
83017
|
-
}
|
|
83018
|
-
/**
|
|
83019
|
-
* The subject inside a ZONE CROP — the shape a package model is trained on.
|
|
83020
|
-
*
|
|
83021
|
-
* Returns `null` when the subject does not intersect the zone at all. That is
|
|
83022
|
-
* the whole reason the package proposal runs on the crop rather than the frame:
|
|
83023
|
-
* a package detector at 0.35 on a full frame proposes furniture, and a subject
|
|
83024
|
-
* outside the zone is not part of this model's world.
|
|
83025
|
-
*/
|
|
83026
|
-
function deriveZoneCropRect(bbox, zone) {
|
|
83027
|
-
if (!isUsableRect(bbox) || !isUsableRect(zone)) return null;
|
|
83028
|
-
const left = Math.max(bbox.x, zone.x);
|
|
83029
|
-
const top = Math.max(bbox.y, zone.y);
|
|
83030
|
-
const right = Math.min(bbox.x + bbox.w, zone.x + zone.w);
|
|
83031
|
-
const bottom = Math.min(bbox.y + bbox.h, zone.y + zone.h);
|
|
83032
|
-
if (!(right > left) || !(bottom > top)) return null;
|
|
83033
|
-
const visibleFraction = (right - left) * (bottom - top) / (bbox.w * bbox.h);
|
|
83034
|
-
return {
|
|
83035
|
-
bbox: {
|
|
83036
|
-
x: (left - zone.x) / zone.w,
|
|
83037
|
-
y: (top - zone.y) / zone.h,
|
|
83038
|
-
w: (right - left) / zone.w,
|
|
83039
|
-
h: (bottom - top) / zone.h
|
|
83040
|
-
},
|
|
83041
|
-
visibleFraction
|
|
83042
|
-
};
|
|
83043
|
-
}
|
|
83044
|
-
/**
|
|
83045
|
-
* The subject inside its OWN padded crop — the shape a classifier is trained on.
|
|
83046
|
-
*
|
|
83047
|
-
* The crop is the box grown by `padding` of its own size on each side and then
|
|
83048
|
-
* clipped to the frame, so the subject sits in the middle and the classifier
|
|
83049
|
-
* still sees a little context. The returned rectangle is where the subject
|
|
83050
|
-
* lands inside that crop, which is NOT `[0,0,1,1]` whenever the padding was
|
|
83051
|
-
* clipped by a frame edge — a subject at the left border is flush left in its
|
|
83052
|
-
* crop, and a caller that assumed the centre would draw the box in the wrong
|
|
83053
|
-
* place.
|
|
83054
|
-
*/
|
|
83055
|
-
function deriveSubjectCropRect(bbox, padding) {
|
|
83056
|
-
if (!isUsableRect(bbox)) return null;
|
|
83057
|
-
const pad = Number.isFinite(padding) && padding > 0 ? padding : 0;
|
|
83058
|
-
const cropX = clamp01(bbox.x - bbox.w * pad);
|
|
83059
|
-
const cropY = clamp01(bbox.y - bbox.h * pad);
|
|
83060
|
-
const cropRight = clamp01(bbox.x + bbox.w * (1 + pad));
|
|
83061
|
-
const cropBottom = clamp01(bbox.y + bbox.h * (1 + pad));
|
|
83062
|
-
const cropW = cropRight - cropX;
|
|
83063
|
-
const cropH = cropBottom - cropY;
|
|
83064
|
-
if (!(cropW > 0) || !(cropH > 0)) return null;
|
|
83065
|
-
return {
|
|
83066
|
-
bbox: {
|
|
83067
|
-
x: (bbox.x - cropX) / cropW,
|
|
83068
|
-
y: (bbox.y - cropY) / cropH,
|
|
83069
|
-
w: bbox.w / cropW,
|
|
83070
|
-
h: bbox.h / cropH
|
|
83071
|
-
},
|
|
83072
|
-
visibleFraction: 1
|
|
83073
|
-
};
|
|
83074
|
-
}
|
|
83075
|
-
//#endregion
|
|
83076
|
-
//#region src/pipeline-analytics/retrain/retrain-lighting.ts
|
|
83077
|
-
function isNight(condition) {
|
|
83078
|
-
return condition === "night" || condition === "ir";
|
|
83079
|
-
}
|
|
83080
|
-
function retrainFrameLighting(input) {
|
|
83081
|
-
if (input.capturedAt === void 0) {
|
|
83082
|
-
const measured = resolveConditionDetailed({
|
|
83083
|
-
...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
|
|
83084
|
-
now: /* @__PURE__ */ new Date(0)
|
|
83085
|
-
});
|
|
83086
|
-
return measured.source === "ir-chroma" ? {
|
|
83087
|
-
condition: measured.condition,
|
|
83088
|
-
night: true,
|
|
83089
|
-
source: measured.source
|
|
83090
|
-
} : null;
|
|
83091
|
-
}
|
|
83092
|
-
const resolved = resolveConditionDetailed({
|
|
83093
|
-
...input.chromaSpread !== void 0 ? { chromaSpread: input.chromaSpread } : {},
|
|
83094
|
-
...input.coordinates !== null ? { coordinates: input.coordinates } : {},
|
|
83095
|
-
now: new Date(input.capturedAt)
|
|
83096
|
-
});
|
|
83097
|
-
return {
|
|
83098
|
-
...resolved,
|
|
83099
|
-
night: isNight(resolved.condition)
|
|
83100
|
-
};
|
|
83101
|
-
}
|
|
83102
|
-
//#endregion
|
|
83103
|
-
//#region src/pipeline-analytics/retrain/retrain-export.ts
|
|
83104
|
-
/**
|
|
83105
|
-
* The ANNOTATED dataset export — the retrain page's own archive.
|
|
83106
|
-
*
|
|
83107
|
-
* Sibling of `training-export/`, deliberately not the same thing. That one
|
|
83108
|
-
* streams a marked track's stored media verbatim and knows nothing about what
|
|
83109
|
-
* is in it; this one streams the frames the operator SELECTED (already copied
|
|
83110
|
-
* into retrain storage) plus the ground truth they drew.
|
|
83111
|
-
*
|
|
83112
|
-
* ## The one interesting decision
|
|
83113
|
-
*
|
|
83114
|
-
* `annotations.json` carries, for every subject, the canonical full-frame box
|
|
83115
|
-
* AND the geometry derived for each model shape. **Derived here, at export,
|
|
83116
|
-
* never stored** — one box in, three shapes out. Storing the derived shapes is
|
|
83117
|
-
* how two crops of the same subject end up in one feature space, which is the
|
|
83118
|
-
* failure already recorded for CLIP (D52). A consumer that wants a different
|
|
83119
|
-
* input size or a different padding re-derives from the canonical box; nothing
|
|
83120
|
-
* in the database has to change and nothing can disagree.
|
|
83121
|
-
*
|
|
83122
|
-
* Everything else follows the export next door: index reads to plan, one JPEG
|
|
83123
|
-
* in memory at a time, a yield between files, and NO inference — a training
|
|
83124
|
-
* set's pixels are the pixels the pipeline saw.
|
|
83125
|
-
*/
|
|
83126
|
-
var RETRAIN_EXPORT_PREFIX = "retrain-export";
|
|
83127
|
-
/**
|
|
83128
|
-
* Padding a subject-crop classifier is trained with: the RUNTIME detail-crop
|
|
83129
|
-
* padding, read from the one convention `deriveDetailCropRect` cuts with
|
|
83130
|
-
* (`types/src/pipeline/detail-crop.ts`), never a second literal. It was `0.2`
|
|
83131
|
-
* here while the runtime fed `0.15`, so a classifier trained from this export
|
|
83132
|
-
* learned on more context than it is ever given live.
|
|
83133
|
-
*/
|
|
83134
|
-
var CLASSIFIER_CROP_PADDING = DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
83135
|
-
/** The convention a `crops` layout cuts with — the runtime default, recorded
|
|
83136
|
-
* in the manifest so a consumer never has to assume it. */
|
|
83137
|
-
var RETRAIN_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
|
|
83138
|
-
/** Longest side of a `crops`-layout JPEG. Classifier inputs are 224–260 px;
|
|
83139
|
-
* the trainer resizes per model, so this only bounds the upload. */
|
|
83140
|
-
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`. */
|
|
83144
|
-
function retrainFramePath(frame) {
|
|
83145
|
-
const leaf = frame.frameId.slice(frame.frameId.lastIndexOf(":") + 1);
|
|
83146
|
-
return `frames/${frame.deviceId}/${frame.trackId}/${leaf}.jpg`;
|
|
83147
|
-
}
|
|
83148
|
-
/** Archive path of a subject crop: `crops/<macroClass>/<annotationId>.jpg`. */
|
|
83149
|
-
function retrainCropPath(macroClass, annotationId) {
|
|
83150
|
-
return `crops/${macroClass}/${annotationId}.jpg`;
|
|
83151
|
-
}
|
|
83152
|
-
/**
|
|
83153
|
-
* Narrow the planned frames to a filter. Pure. Every field narrows; an empty
|
|
83154
|
-
* filter keeps everything. Order is the input's.
|
|
83155
|
-
*/
|
|
83156
|
-
function filterRetrainExport(frames, annotationsByFrame, filter) {
|
|
83157
|
-
const wanted = filter.frameIds !== void 0 ? new Set(filter.frameIds) : null;
|
|
83158
|
-
const devices = filter.deviceIds !== void 0 && filter.deviceIds.length > 0 ? new Set(filter.deviceIds) : null;
|
|
83159
|
-
return frames.filter((frame) => {
|
|
83160
|
-
if (wanted !== null && !wanted.has(frame.frameId)) return false;
|
|
83161
|
-
if (devices !== null && !devices.has(frame.deviceId)) return false;
|
|
83162
|
-
if (filter.completeOnly === true && !frame.complete) return false;
|
|
83163
|
-
if (filter.batchId !== void 0) {
|
|
83164
|
-
if (!(annotationsByFrame.get(frame.frameId) ?? []).some((a) => a.exportedInBatch === filter.batchId)) return false;
|
|
83165
|
-
}
|
|
83166
|
-
return true;
|
|
83167
|
-
});
|
|
83168
|
-
}
|
|
83169
|
-
/**
|
|
83170
|
-
* Derive every per-model shape for one annotation.
|
|
83171
|
-
*
|
|
83172
|
-
* `zone` is optional and per-frame: only a package annotation has one, and a
|
|
83173
|
-
* `zoneCrop` for a subject the zone does not contain is ABSENT rather than
|
|
83174
|
-
* clamped — a clamped box teaches the model that the subject ends where the
|
|
83175
|
-
* crop ends.
|
|
83176
|
-
*/
|
|
83177
|
-
function deriveAll(annotation, frame, zone) {
|
|
83178
|
-
const letterbox = deriveLetterboxRect(annotation.bbox, frame.width, frame.height);
|
|
83179
|
-
const subjectCrop = deriveSubjectCropRect(annotation.bbox, CLASSIFIER_CROP_PADDING);
|
|
83180
|
-
const zoneCrop = zone === null ? null : deriveZoneCropRect(annotation.bbox, zone);
|
|
83181
|
-
return {
|
|
83182
|
-
...letterbox !== null ? { letterbox } : {},
|
|
83183
|
-
...zoneCrop !== null ? { zoneCrop } : {},
|
|
83184
|
-
...subjectCrop !== null ? { subjectCrop } : {}
|
|
83185
|
-
};
|
|
83186
|
-
}
|
|
83187
|
-
/** Build the manifest. Pure — index rows in, JSON out, no blob touched. */
|
|
83188
|
-
function buildRetrainManifest(input) {
|
|
83189
|
-
const layout = input.layout ?? "frames";
|
|
83190
|
-
const coordinates = input.coordinates ?? null;
|
|
83191
|
-
let annotationCount = 0;
|
|
83192
|
-
let modelErrorCount = 0;
|
|
83193
|
-
const frames = input.frames.map((frame) => {
|
|
83194
|
-
const zone = input.zoneForDevice(frame.deviceId);
|
|
83195
|
-
const annotations = (input.annotationsByFrame.get(frame.frameId) ?? []).map((a) => {
|
|
83196
|
-
annotationCount += 1;
|
|
83197
|
-
if (a.kind === "model_error") modelErrorCount += 1;
|
|
83198
|
-
const derived = deriveAll(a, frame, zone);
|
|
83199
|
-
return {
|
|
83200
|
-
id: a.id,
|
|
83201
|
-
macroClass: a.macroClass,
|
|
83202
|
-
...a.label !== void 0 ? { label: a.label } : {},
|
|
83203
|
-
...a.subLabel !== void 0 ? { subLabel: a.subLabel } : {},
|
|
83204
|
-
kind: a.kind,
|
|
83205
|
-
source: a.source,
|
|
83206
|
-
...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
|
|
83207
|
-
...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
|
|
83208
|
-
bbox: a.bbox,
|
|
83209
|
-
...layout === "crops" && derived.subjectCrop !== void 0 ? { cropPath: retrainCropPath(a.macroClass, a.id) } : {},
|
|
83210
|
-
derived
|
|
83211
|
-
};
|
|
83212
|
-
});
|
|
83213
|
-
return {
|
|
83214
|
-
frameId: frame.frameId,
|
|
83215
|
-
path: retrainFramePath(frame),
|
|
83216
|
-
deviceId: frame.deviceId,
|
|
83217
|
-
trackId: frame.trackId,
|
|
83218
|
-
width: frame.width,
|
|
83219
|
-
height: frame.height,
|
|
83220
|
-
sizeBytes: frame.sizeBytes,
|
|
83221
|
-
sourceKind: frame.sourceKind,
|
|
83222
|
-
copiedAt: frame.copiedAt,
|
|
83223
|
-
...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
|
|
83224
|
-
complete: frame.complete,
|
|
83225
|
-
lighting: retrainFrameLighting({
|
|
83226
|
-
...frame.capturedAt !== void 0 ? { capturedAt: frame.capturedAt } : {},
|
|
83227
|
-
...frame.chromaSpread !== void 0 ? { chromaSpread: frame.chromaSpread } : {},
|
|
83228
|
-
coordinates
|
|
83229
|
-
}),
|
|
83230
|
-
...zone !== null ? { zoneUsed: zone } : {},
|
|
83231
|
-
annotations
|
|
83232
|
-
};
|
|
83233
|
-
});
|
|
83234
|
-
return {
|
|
83235
|
-
schema: RETRAIN_MANIFEST_SCHEMA,
|
|
83236
|
-
layout,
|
|
83237
|
-
detailCropConvention: RETRAIN_CROP_CONVENTION,
|
|
83238
|
-
...layout === "crops" ? { cropMaxSide: RETRAIN_CROP_MAX_SIDE } : {},
|
|
83239
|
-
generatedAt: input.generatedAt,
|
|
83240
|
-
batchId: input.batchId,
|
|
83241
|
-
frameCount: frames.length,
|
|
83242
|
-
annotationCount,
|
|
83243
|
-
modelErrorCount,
|
|
83244
|
-
classifierCropPadding: CLASSIFIER_CROP_PADDING,
|
|
83245
|
-
frames
|
|
83246
|
-
};
|
|
83247
|
-
}
|
|
83248
|
-
//#endregion
|
|
83249
84780
|
//#region src/pipeline-analytics/retrain/retrain-export-handler.ts
|
|
83250
84781
|
/** The export request a URL carries (`getRetrainExportUrl` composes it). */
|
|
83251
84782
|
function parseRetrainExportRequest(url) {
|
|
@@ -83253,11 +84784,15 @@ function parseRetrainExportRequest(url) {
|
|
|
83253
84784
|
const params = new URLSearchParams(query);
|
|
83254
84785
|
const deviceIds = parseRetrainDeviceIds(url);
|
|
83255
84786
|
const frameIds = (params.get("frameIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
84787
|
+
const collectionIds = (params.get("collectionIds") ?? "").split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
84788
|
+
const datasetVersionId = params.get("datasetVersionId")?.trim() ?? "";
|
|
83256
84789
|
const batchId = params.get("batchId")?.trim() ?? "";
|
|
83257
84790
|
return {
|
|
83258
84791
|
filter: {
|
|
83259
84792
|
...deviceIds.length > 0 ? { deviceIds: [...deviceIds] } : {},
|
|
83260
84793
|
...frameIds.length > 0 ? { frameIds } : {},
|
|
84794
|
+
...collectionIds.length > 0 ? { collectionIds } : {},
|
|
84795
|
+
...datasetVersionId.length > 0 ? { datasetVersionId } : {},
|
|
83261
84796
|
...batchId.length > 0 ? { batchId } : {},
|
|
83262
84797
|
...params.get("completeOnly") === "1" ? { completeOnly: true } : {}
|
|
83263
84798
|
},
|
|
@@ -83270,6 +84805,8 @@ function retrainExportQuery(request) {
|
|
|
83270
84805
|
const { filter } = request;
|
|
83271
84806
|
if (filter.deviceIds !== void 0 && filter.deviceIds.length > 0) params.set("deviceIds", filter.deviceIds.join(","));
|
|
83272
84807
|
if (filter.frameIds !== void 0 && filter.frameIds.length > 0) params.set("frameIds", filter.frameIds.join(","));
|
|
84808
|
+
if (filter.collectionIds !== void 0 && filter.collectionIds.length > 0) params.set("collectionIds", filter.collectionIds.join(","));
|
|
84809
|
+
if (filter.datasetVersionId !== void 0) params.set("datasetVersionId", filter.datasetVersionId);
|
|
83273
84810
|
if (filter.batchId !== void 0) params.set("batchId", filter.batchId);
|
|
83274
84811
|
if (filter.completeOnly === true) params.set("completeOnly", "1");
|
|
83275
84812
|
if (request.layout === "crops") params.set("layout", "crops");
|
|
@@ -83344,6 +84881,178 @@ function createRetrainExportHandler(deps) {
|
|
|
83344
84881
|
};
|
|
83345
84882
|
}
|
|
83346
84883
|
//#endregion
|
|
84884
|
+
//#region src/pipeline-analytics/retrain/retrain-pipeline-boxes.ts
|
|
84885
|
+
/**
|
|
84886
|
+
* The boxes the PIPELINE drew on a copied frame — Step 1 of the training
|
|
84887
|
+
* screen.
|
|
84888
|
+
*
|
|
84889
|
+
* A correction has to start from the decision being corrected. Proposing with
|
|
84890
|
+
* a fresh model run answers a different question ("what would a model say
|
|
84891
|
+
* now"), and the frame the operator is fixing is precisely the one where the
|
|
84892
|
+
* live answer was wrong — a re-run that happens to be right hides the phantom
|
|
84893
|
+
* the operator came to remove. So the canvas opens with what the track store
|
|
84894
|
+
* says the pipeline had on that frame, and the operator accepts, corrects,
|
|
84895
|
+
* relabels or deletes it.
|
|
84896
|
+
*
|
|
84897
|
+
* ## Where a box comes from, best first
|
|
84898
|
+
*
|
|
84899
|
+
* 1. **The subject box recorded ON the source media row** (D510). Exact by
|
|
84900
|
+
* construction: the box was measured on the frame the blob shows. Only the
|
|
84901
|
+
* copy's own track can have one.
|
|
84902
|
+
* 2. **The track position nearest the capture time**, within
|
|
84903
|
+
* {@link PIPELINE_BOX_TOLERANCE_MS}. Positions are frame-rate, so the
|
|
84904
|
+
* nearest one is normally the same detection; the delta is reported so a
|
|
84905
|
+
* reviewer can see how far it was.
|
|
84906
|
+
*
|
|
84907
|
+
* Every OTHER track alive on the camera at that instant contributes its own
|
|
84908
|
+
* nearest position the same way — a frame with three people is three boxes,
|
|
84909
|
+
* and a recording still (no track of its own) gets everything the pipeline
|
|
84910
|
+
* was tracking at that moment, which is exactly what a missed-object review
|
|
84911
|
+
* needs to see is NOT there.
|
|
84912
|
+
*
|
|
84913
|
+
* Nothing here does I/O. The caller reads the tracks and the subject box.
|
|
84914
|
+
*/
|
|
84915
|
+
/** How far from the capture time a position may be and still be "this frame".
|
|
84916
|
+
* Positions are recorded at the analysis rate (several per second), so half
|
|
84917
|
+
* a second is a generous match and still not a different moment. */
|
|
84918
|
+
var PIPELINE_BOX_TOLERANCE_MS = 500;
|
|
84919
|
+
/**
|
|
84920
|
+
* How far back a track may have STARTED and still be alive at the frame. The
|
|
84921
|
+
* track store windows on `firstSeen`, so a track born before this is not
|
|
84922
|
+
* asked about — a vehicle parked for hours is the case it misses, and the
|
|
84923
|
+
* operator draws that box by hand.
|
|
84924
|
+
*/
|
|
84925
|
+
var PIPELINE_TRACK_LOOKBACK_MS = 30 * 6e4;
|
|
84926
|
+
/** At most this many tracks of one camera are read around one frame. */
|
|
84927
|
+
var PIPELINE_TRACK_AROUND_LIMIT = 200;
|
|
84928
|
+
/** The least-wrong macro tier for a class the macro map does not know. The
|
|
84929
|
+
* operator corrects it; what must not happen is the raw string being
|
|
84930
|
+
* promoted into the macro tier. */
|
|
84931
|
+
var FALLBACK_MACRO_CLASS = "person";
|
|
84932
|
+
/**
|
|
84933
|
+
* Split a detector's class into the two tiers.
|
|
84934
|
+
*
|
|
84935
|
+
* A macro class stays in the macro tier; anything else maps through the COCO
|
|
84936
|
+
* macro map, and the class itself becomes the LABEL. A macro word is never a
|
|
84937
|
+
* label, in any casing — the tier rule the annotation write enforces.
|
|
84938
|
+
*/
|
|
84939
|
+
function toRetrainClassTiers(className, label) {
|
|
84940
|
+
const cls = className.trim().toLowerCase();
|
|
84941
|
+
const usableLabel = (value) => {
|
|
84942
|
+
const trimmed = value?.trim();
|
|
84943
|
+
if (trimmed === void 0 || trimmed === "") return void 0;
|
|
84944
|
+
return isRetrainMacroClass(trimmed.toLowerCase()) ? void 0 : trimmed;
|
|
84945
|
+
};
|
|
84946
|
+
if (isRetrainMacroClass(cls)) {
|
|
84947
|
+
const kept = usableLabel(label);
|
|
84948
|
+
return kept !== void 0 ? {
|
|
84949
|
+
macroClass: cls,
|
|
84950
|
+
label: kept
|
|
84951
|
+
} : { macroClass: cls };
|
|
84952
|
+
}
|
|
84953
|
+
const mapped = COCO_TO_MACRO.mapping[cls];
|
|
84954
|
+
const macroClass = mapped !== void 0 && isRetrainMacroClass(mapped) ? mapped : FALLBACK_MACRO_CLASS;
|
|
84955
|
+
const kept = usableLabel(className);
|
|
84956
|
+
return kept !== void 0 ? {
|
|
84957
|
+
macroClass,
|
|
84958
|
+
label: kept
|
|
84959
|
+
} : { macroClass };
|
|
84960
|
+
}
|
|
84961
|
+
/** A pixel box as a clamped normalised one, or `null` when nothing is left. */
|
|
84962
|
+
function normalise(box, frameWidth, frameHeight) {
|
|
84963
|
+
if (frameWidth === void 0 || frameHeight === void 0) return null;
|
|
84964
|
+
if (!(frameWidth > 0) || !(frameHeight > 0)) return null;
|
|
84965
|
+
const x = clampSpan(box.x, box.w, frameWidth);
|
|
84966
|
+
const y = clampSpan(box.y, box.h, frameHeight);
|
|
84967
|
+
if (x === null || y === null) return null;
|
|
84968
|
+
return {
|
|
84969
|
+
x: x.start,
|
|
84970
|
+
y: y.start,
|
|
84971
|
+
w: x.size,
|
|
84972
|
+
h: y.size
|
|
84973
|
+
};
|
|
84974
|
+
}
|
|
84975
|
+
/** One axis of a pixel box, normalised and clamped into `[0,1]`. The size is
|
|
84976
|
+
* divided directly when nothing was clamped, so an exact box stays exact. */
|
|
84977
|
+
function clampSpan(start, size, extent) {
|
|
84978
|
+
const lo = start / extent;
|
|
84979
|
+
const hi = (start + size) / extent;
|
|
84980
|
+
const s = Math.max(0, lo);
|
|
84981
|
+
const n = lo >= 0 && hi <= 1 ? size / extent : Math.min(1, hi) - s;
|
|
84982
|
+
return n > 0 ? {
|
|
84983
|
+
start: s,
|
|
84984
|
+
size: n
|
|
84985
|
+
} : null;
|
|
84986
|
+
}
|
|
84987
|
+
function nearestPosition(track, at, tolerance) {
|
|
84988
|
+
let best = null;
|
|
84989
|
+
let bestDelta = Number.POSITIVE_INFINITY;
|
|
84990
|
+
for (const p of track.positions) {
|
|
84991
|
+
const delta = Math.abs(p.timestamp - at);
|
|
84992
|
+
if (delta < bestDelta) {
|
|
84993
|
+
best = p;
|
|
84994
|
+
bestDelta = delta;
|
|
84995
|
+
}
|
|
84996
|
+
}
|
|
84997
|
+
if (best === null || bestDelta > tolerance) return null;
|
|
84998
|
+
return {
|
|
84999
|
+
position: best,
|
|
85000
|
+
deltaMs: bestDelta
|
|
85001
|
+
};
|
|
85002
|
+
}
|
|
85003
|
+
function boxFor(track, bbox, basis, deltaMs, score) {
|
|
85004
|
+
const tiers = toRetrainClassTiers(track.className, track.label);
|
|
85005
|
+
const subLabel = track.subLabel?.trim();
|
|
85006
|
+
return {
|
|
85007
|
+
bbox,
|
|
85008
|
+
macroClass: tiers.macroClass,
|
|
85009
|
+
...tiers.label !== void 0 ? { label: tiers.label } : {},
|
|
85010
|
+
...subLabel !== void 0 && subLabel !== "" && !isRetrainMacroClass(subLabel.toLowerCase()) ? { subLabel } : {},
|
|
85011
|
+
...score !== void 0 ? { score } : {},
|
|
85012
|
+
...track.modelId !== void 0 ? { modelId: track.modelId } : {},
|
|
85013
|
+
trackId: track.trackId,
|
|
85014
|
+
basis,
|
|
85015
|
+
deltaMs
|
|
85016
|
+
};
|
|
85017
|
+
}
|
|
85018
|
+
/** The own track's box: the recorded subject box first, the nearest position second. */
|
|
85019
|
+
function ownBox(input, tolerance) {
|
|
85020
|
+
const track = input.ownTrack;
|
|
85021
|
+
if (track === null) return null;
|
|
85022
|
+
const at = input.frame.capturedAt;
|
|
85023
|
+
const nearest = at !== void 0 ? nearestPosition(track, at, tolerance) : null;
|
|
85024
|
+
if (input.ownSubjectBox !== null) {
|
|
85025
|
+
const recorded = normalise(input.ownSubjectBox, input.ownSubjectBox.frameWidth, input.ownSubjectBox.frameHeight);
|
|
85026
|
+
if (recorded !== null) return boxFor(track, recorded, "subject-box", 0, nearest?.position.score);
|
|
85027
|
+
}
|
|
85028
|
+
if (nearest === null) return null;
|
|
85029
|
+
const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
|
|
85030
|
+
return placed === null ? null : boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score);
|
|
85031
|
+
}
|
|
85032
|
+
/**
|
|
85033
|
+
* Every box the pipeline had on this frame, own track first. Pure.
|
|
85034
|
+
*/
|
|
85035
|
+
function pipelineBoxesForFrame(input) {
|
|
85036
|
+
const tolerance = input.toleranceMs ?? PIPELINE_BOX_TOLERANCE_MS;
|
|
85037
|
+
const out = [];
|
|
85038
|
+
const own = ownBox(input, tolerance);
|
|
85039
|
+
if (own !== null) out.push(own);
|
|
85040
|
+
const at = input.frame.capturedAt;
|
|
85041
|
+
if (at === void 0) return out;
|
|
85042
|
+
const ownId = input.frame.trackId;
|
|
85043
|
+
const seen = new Set(ownId !== void 0 ? [ownId] : []);
|
|
85044
|
+
for (const track of input.neighbours) {
|
|
85045
|
+
if (seen.has(track.trackId) || track.deviceId !== input.frame.deviceId) continue;
|
|
85046
|
+
seen.add(track.trackId);
|
|
85047
|
+
const nearest = nearestPosition(track, at, tolerance);
|
|
85048
|
+
if (nearest === null) continue;
|
|
85049
|
+
const placed = normalise(nearest.position.bbox, track.frameWidth, track.frameHeight);
|
|
85050
|
+
if (placed === null) continue;
|
|
85051
|
+
out.push(boxFor(track, placed, "position", nearest.deltaMs, nearest.position.score));
|
|
85052
|
+
}
|
|
85053
|
+
return out;
|
|
85054
|
+
}
|
|
85055
|
+
//#endregion
|
|
83347
85056
|
//#region src/pipeline-analytics/retrain/retrain-assist.ts
|
|
83348
85057
|
/** The whole frame, as a normalised rectangle. */
|
|
83349
85058
|
var WHOLE_FRAME = {
|
|
@@ -83414,12 +85123,11 @@ function mapAssistDetails(input) {
|
|
|
83414
85123
|
withoutBox += 1;
|
|
83415
85124
|
continue;
|
|
83416
85125
|
}
|
|
83417
|
-
const
|
|
83418
|
-
const label = isRetrainMacroClass(d.className) ? d.label : d.className;
|
|
85126
|
+
const tiers = toRetrainClassTiers(d.className, d.label);
|
|
83419
85127
|
proposals.push({
|
|
83420
85128
|
bbox,
|
|
83421
|
-
macroClass,
|
|
83422
|
-
...label !== void 0
|
|
85129
|
+
macroClass: tiers.macroClass,
|
|
85130
|
+
...tiers.label !== void 0 ? { label: tiers.label } : {},
|
|
83423
85131
|
score: d.score,
|
|
83424
85132
|
modelId
|
|
83425
85133
|
});
|
|
@@ -83430,16 +85138,6 @@ function mapAssistDetails(input) {
|
|
|
83430
85138
|
withoutBox
|
|
83431
85139
|
};
|
|
83432
85140
|
}
|
|
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
85141
|
/** A proposal, as a draft annotation ready to store if the operator accepts it. */
|
|
83444
85142
|
function proposalToDraft(proposal, owner) {
|
|
83445
85143
|
return {
|
|
@@ -83648,12 +85346,18 @@ async function restageRetrainTrack(deps, input) {
|
|
|
83648
85346
|
var DEFAULT_STAGING_LIMIT = 100;
|
|
83649
85347
|
/** A macro class that failed validation — the write is refused, never coerced. */
|
|
83650
85348
|
var RetrainAnnotationError = class extends Error {};
|
|
85349
|
+
/** A store frame on the wire. A recording still carries no track and no
|
|
85350
|
+
* source media row, and says so by leaving both out. */
|
|
83651
85351
|
function toWireFrame(f) {
|
|
83652
85352
|
return {
|
|
83653
85353
|
frameId: f.frameId,
|
|
83654
85354
|
deviceId: f.deviceId,
|
|
83655
|
-
|
|
83656
|
-
|
|
85355
|
+
origin: f.origin,
|
|
85356
|
+
...f.origin === "track" ? {
|
|
85357
|
+
trackId: f.trackId,
|
|
85358
|
+
sourceMediaId: f.sourceMediaId
|
|
85359
|
+
} : {},
|
|
85360
|
+
...f.epochMs !== void 0 ? { epochMs: f.epochMs } : {},
|
|
83657
85361
|
sourceKind: f.sourceKind,
|
|
83658
85362
|
sizeBytes: f.sizeBytes,
|
|
83659
85363
|
width: f.width,
|
|
@@ -83666,7 +85370,7 @@ function toWireFrame(f) {
|
|
|
83666
85370
|
function toWireAnnotation(a) {
|
|
83667
85371
|
return {
|
|
83668
85372
|
id: a.id,
|
|
83669
|
-
trackId: a.trackId,
|
|
85373
|
+
...a.trackId !== "" ? { trackId: a.trackId } : {},
|
|
83670
85374
|
deviceId: a.deviceId,
|
|
83671
85375
|
mediaKey: a.mediaKey,
|
|
83672
85376
|
bbox: a.bbox,
|
|
@@ -83733,11 +85437,116 @@ var RetrainService = class {
|
|
|
83733
85437
|
if (candidates.length === 0) await this.logNoAnnotatableFrame(trackId, media, copies.length);
|
|
83734
85438
|
return {
|
|
83735
85439
|
candidates,
|
|
83736
|
-
copies:
|
|
85440
|
+
copies: await this.toWireFrames(copies),
|
|
83737
85441
|
...autoPick !== null ? { autoPickMediaId: autoPick.mediaId } : {}
|
|
83738
85442
|
};
|
|
83739
85443
|
}
|
|
83740
85444
|
/**
|
|
85445
|
+
* The wire frames, each with the boxes the pipeline drew on it (Step 1) and
|
|
85446
|
+
* the collections it belongs to.
|
|
85447
|
+
*
|
|
85448
|
+
* Index reads only — the track rows and the media rows' subject boxes, no
|
|
85449
|
+
* blob and no inference. One read per distinct track and per distinct
|
|
85450
|
+
* instant, not per box. A failed read degrades to fewer boxes, logged, and
|
|
85451
|
+
* never to a failed listing: the frames are the operator's work and must
|
|
85452
|
+
* stay reachable when the evidence is not.
|
|
85453
|
+
*/
|
|
85454
|
+
async toWireFrames(copies) {
|
|
85455
|
+
const collections = this.deps.memberships !== void 0 && copies.length > 0 ? await this.deps.memberships.collectionsOf(copies.map((c) => c.frameId)).catch((err) => {
|
|
85456
|
+
this.deps.logger.warn("retrain frames: collection membership unreadable", { meta: {
|
|
85457
|
+
frames: copies.length,
|
|
85458
|
+
error: String(err)
|
|
85459
|
+
} });
|
|
85460
|
+
return /* @__PURE__ */ new Map();
|
|
85461
|
+
}) : null;
|
|
85462
|
+
const evidence = this.deps.pipelineEvidence;
|
|
85463
|
+
const ownTracks = /* @__PURE__ */ new Map();
|
|
85464
|
+
const subjectBoxes = /* @__PURE__ */ new Map();
|
|
85465
|
+
const out = [];
|
|
85466
|
+
for (const copy of copies) {
|
|
85467
|
+
const wire = toWireFrame(copy);
|
|
85468
|
+
const collectionIds = collections?.get(copy.frameId);
|
|
85469
|
+
const withCollections = collections !== null ? {
|
|
85470
|
+
...wire,
|
|
85471
|
+
collectionIds: collectionIds ?? []
|
|
85472
|
+
} : wire;
|
|
85473
|
+
if (evidence === void 0) {
|
|
85474
|
+
out.push(withCollections);
|
|
85475
|
+
continue;
|
|
85476
|
+
}
|
|
85477
|
+
const trackId = copy.origin === "track" ? copy.trackId : "";
|
|
85478
|
+
if (trackId !== "" && !ownTracks.has(trackId)) {
|
|
85479
|
+
ownTracks.set(trackId, await this.deps.tracks.getPersistedByTrackId(trackId).catch(() => null));
|
|
85480
|
+
subjectBoxes.set(trackId, await this.readSubjectBoxes(evidence, trackId, copy.deviceId));
|
|
85481
|
+
}
|
|
85482
|
+
const neighbours = copy.capturedAt !== void 0 ? await this.readTracksAround(evidence, copy.deviceId, copy.capturedAt) : [];
|
|
85483
|
+
const pipelineBoxes = pipelineBoxesForFrame({
|
|
85484
|
+
frame: {
|
|
85485
|
+
deviceId: copy.deviceId,
|
|
85486
|
+
...trackId !== "" ? { trackId } : {},
|
|
85487
|
+
...copy.capturedAt !== void 0 ? { capturedAt: copy.capturedAt } : {}
|
|
85488
|
+
},
|
|
85489
|
+
ownTrack: trackId !== "" ? ownTracks.get(trackId) ?? null : null,
|
|
85490
|
+
ownSubjectBox: trackId !== "" ? subjectBoxes.get(trackId)?.get(copy.sourceMediaId) ?? null : null,
|
|
85491
|
+
neighbours
|
|
85492
|
+
});
|
|
85493
|
+
out.push({
|
|
85494
|
+
...withCollections,
|
|
85495
|
+
pipelineBoxes
|
|
85496
|
+
});
|
|
85497
|
+
}
|
|
85498
|
+
return out;
|
|
85499
|
+
}
|
|
85500
|
+
/**
|
|
85501
|
+
* The frame a frame-scoped call names, or a refusal. The frame is the
|
|
85502
|
+
* authority: its camera must be the caller's, and a `trackId`, when the
|
|
85503
|
+
* caller sends one, must be the frame's own — a recording still has none.
|
|
85504
|
+
*/
|
|
85505
|
+
async requireFrame(ref, action) {
|
|
85506
|
+
const frame = await this.deps.frames.get(ref.frameId);
|
|
85507
|
+
const trackMismatch = frame !== null && ref.trackId !== void 0 && (frame.origin !== "track" || frame.trackId !== ref.trackId);
|
|
85508
|
+
if (frame === null || frame.deviceId !== ref.deviceId || trackMismatch) {
|
|
85509
|
+
this.deps.logger.warn(`retrain ${action} refused: the frame does not belong to that camera or track`, {
|
|
85510
|
+
tags: { deviceId: ref.deviceId },
|
|
85511
|
+
meta: {
|
|
85512
|
+
frameId: ref.frameId,
|
|
85513
|
+
...ref.trackId !== void 0 ? { trackId: ref.trackId } : {}
|
|
85514
|
+
}
|
|
85515
|
+
});
|
|
85516
|
+
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)}`}`);
|
|
85517
|
+
}
|
|
85518
|
+
return frame;
|
|
85519
|
+
}
|
|
85520
|
+
async readSubjectBoxes(evidence, trackId, deviceId) {
|
|
85521
|
+
try {
|
|
85522
|
+
const rows = await evidence.subjectBoxes(trackId, deviceId);
|
|
85523
|
+
return new Map(rows.map((r) => [r.mediaId, r.subjectBox]));
|
|
85524
|
+
} catch (err) {
|
|
85525
|
+
this.deps.logger.warn("retrain pipeline boxes: subject boxes unreadable", {
|
|
85526
|
+
tags: { deviceId },
|
|
85527
|
+
meta: {
|
|
85528
|
+
trackId,
|
|
85529
|
+
error: String(err)
|
|
85530
|
+
}
|
|
85531
|
+
});
|
|
85532
|
+
return /* @__PURE__ */ new Map();
|
|
85533
|
+
}
|
|
85534
|
+
}
|
|
85535
|
+
async readTracksAround(evidence, deviceId, atMs) {
|
|
85536
|
+
try {
|
|
85537
|
+
return await evidence.tracksAround(deviceId, atMs);
|
|
85538
|
+
} catch (err) {
|
|
85539
|
+
this.deps.logger.warn("retrain pipeline boxes: tracks around the frame unreadable", {
|
|
85540
|
+
tags: { deviceId },
|
|
85541
|
+
meta: {
|
|
85542
|
+
atMs,
|
|
85543
|
+
error: String(err)
|
|
85544
|
+
}
|
|
85545
|
+
});
|
|
85546
|
+
return [];
|
|
85547
|
+
}
|
|
85548
|
+
}
|
|
85549
|
+
/**
|
|
83741
85550
|
* A track with no annotatable frame is a REAL state, and the page has to say
|
|
83742
85551
|
* so rather than render an empty strip that reads as a slow load. Two causes,
|
|
83743
85552
|
* named apart: the whole-frame media AGED OUT, or the track only ever held
|
|
@@ -83790,23 +85599,13 @@ var RetrainService = class {
|
|
|
83790
85599
|
* track the caller names: a frame of another track is refused, loudly.
|
|
83791
85600
|
*/
|
|
83792
85601
|
async setFrameComplete(input) {
|
|
83793
|
-
const frame = await this.
|
|
83794
|
-
|
|
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);
|
|
85602
|
+
const frame = await this.requireFrame(input, "complete");
|
|
85603
|
+
const updated = await this.deps.frames.setComplete(frame.frameId, input.complete);
|
|
83805
85604
|
if (updated === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} vanished`);
|
|
83806
85605
|
this.deps.logger.info("retrain frame completeness set", {
|
|
83807
85606
|
tags: { deviceId: input.deviceId },
|
|
83808
85607
|
meta: {
|
|
83809
|
-
trackId:
|
|
85608
|
+
trackId: frame.trackId,
|
|
83810
85609
|
frameId: input.frameId,
|
|
83811
85610
|
complete: input.complete
|
|
83812
85611
|
}
|
|
@@ -83814,36 +85613,41 @@ var RetrainService = class {
|
|
|
83814
85613
|
return toWireFrame(updated);
|
|
83815
85614
|
}
|
|
83816
85615
|
/** Un-select: annotations first, then the copy. Never the other way round —
|
|
83817
|
-
* an annotation whose frame is gone is unopenable and unfixable.
|
|
85616
|
+
* an annotation whose frame is gone is unopenable and unfixable. The frame's
|
|
85617
|
+
* collection memberships go with it. */
|
|
83818
85618
|
async deselectFrame(input) {
|
|
83819
85619
|
const frame = await this.deps.frames.get(input.frameId);
|
|
83820
85620
|
if (frame === null) {
|
|
83821
85621
|
this.deps.logger.warn("retrain deselect: no such copied frame", {
|
|
83822
85622
|
tags: { deviceId: input.deviceId },
|
|
83823
|
-
meta: {
|
|
83824
|
-
trackId: input.trackId,
|
|
83825
|
-
frameId: input.frameId
|
|
83826
|
-
}
|
|
85623
|
+
meta: { frameId: input.frameId }
|
|
83827
85624
|
});
|
|
83828
85625
|
return {
|
|
83829
85626
|
removed: false,
|
|
83830
85627
|
removedAnnotations: 0
|
|
83831
85628
|
};
|
|
83832
85629
|
}
|
|
83833
|
-
|
|
85630
|
+
await this.requireFrame(input, "deselect");
|
|
85631
|
+
return {
|
|
85632
|
+
removed: true,
|
|
85633
|
+
removedAnnotations: await this.removeFrame(frame)
|
|
85634
|
+
};
|
|
85635
|
+
}
|
|
85636
|
+
/** Remove a frame from the dataset: annotations, memberships, then the copy. */
|
|
85637
|
+
async removeFrame(frame) {
|
|
85638
|
+
const removedAnnotations = await this.deps.annotations.deleteForFrame(frame.frameId);
|
|
85639
|
+
await this.deps.memberships?.forgetFrame(frame.frameId);
|
|
83834
85640
|
await this.deps.frames.deleteFrame(frame);
|
|
83835
85641
|
this.deps.logger.info("retrain frame removed from the dataset", {
|
|
83836
|
-
tags: { deviceId:
|
|
85642
|
+
tags: { deviceId: frame.deviceId },
|
|
83837
85643
|
meta: {
|
|
83838
|
-
|
|
83839
|
-
|
|
85644
|
+
origin: frame.origin,
|
|
85645
|
+
...frame.origin === "track" ? { trackId: frame.trackId } : {},
|
|
85646
|
+
frameId: frame.frameId,
|
|
83840
85647
|
removedAnnotations
|
|
83841
85648
|
}
|
|
83842
85649
|
});
|
|
83843
|
-
return
|
|
83844
|
-
removed: true,
|
|
83845
|
-
removedAnnotations
|
|
83846
|
-
};
|
|
85650
|
+
return removedAnnotations;
|
|
83847
85651
|
}
|
|
83848
85652
|
/** The copy's pixels plus its exact dimensions — the canvas needs both. */
|
|
83849
85653
|
async getFrameImage(frameId) {
|
|
@@ -83883,7 +85687,7 @@ var RetrainService = class {
|
|
|
83883
85687
|
};
|
|
83884
85688
|
}
|
|
83885
85689
|
const frame = await this.deps.frames.get(input.frameId);
|
|
83886
|
-
if (frame === null) return {
|
|
85690
|
+
if (frame === null || frame.deviceId !== input.deviceId || input.trackId !== void 0 && frame.trackId !== input.trackId) return {
|
|
83887
85691
|
kind: "refused",
|
|
83888
85692
|
reason: "unknown-frame"
|
|
83889
85693
|
};
|
|
@@ -83938,8 +85742,8 @@ var RetrainService = class {
|
|
|
83938
85742
|
}
|
|
83939
85743
|
});
|
|
83940
85744
|
const owner = {
|
|
83941
|
-
trackId:
|
|
83942
|
-
deviceId:
|
|
85745
|
+
trackId: frame.trackId,
|
|
85746
|
+
deviceId: frame.deviceId,
|
|
83943
85747
|
mediaKey: input.frameId
|
|
83944
85748
|
};
|
|
83945
85749
|
return {
|
|
@@ -83962,8 +85766,12 @@ var RetrainService = class {
|
|
|
83962
85766
|
belowThreshold: mapping.belowThreshold
|
|
83963
85767
|
};
|
|
83964
85768
|
}
|
|
83965
|
-
|
|
83966
|
-
|
|
85769
|
+
/** The annotations of a track, or of named frames (a recording still has
|
|
85770
|
+
* no track). One of the two must be given. */
|
|
85771
|
+
async listAnnotations(input) {
|
|
85772
|
+
if (input.trackId !== void 0 && input.trackId !== "") return (await this.deps.annotations.listByTrack(input.trackId)).map(toWireAnnotation);
|
|
85773
|
+
if (input.frameIds !== void 0) return (await this.deps.annotations.listByFrames(input.frameIds)).map(toWireAnnotation);
|
|
85774
|
+
throw new RetrainAnnotationError("listRetrainAnnotations needs a trackId or frameIds");
|
|
83967
85775
|
}
|
|
83968
85776
|
/**
|
|
83969
85777
|
* Replace every annotation on one frame.
|
|
@@ -83975,15 +85783,15 @@ var RetrainService = class {
|
|
|
83975
85783
|
* believes they stored.
|
|
83976
85784
|
*/
|
|
83977
85785
|
async saveAnnotations(input) {
|
|
83978
|
-
|
|
85786
|
+
const frame = await this.requireFrame(input, "save");
|
|
83979
85787
|
for (const a of input.annotations) {
|
|
83980
85788
|
if (!isRetrainMacroClass(a.macroClass)) throw new RetrainAnnotationError(`"${a.macroClass}" is not a macro class`);
|
|
83981
85789
|
const violation = checkTierSeparation(a);
|
|
83982
85790
|
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
85791
|
}
|
|
83984
85792
|
const drafts = input.annotations.map((a) => ({
|
|
83985
|
-
trackId:
|
|
83986
|
-
deviceId:
|
|
85793
|
+
trackId: frame.trackId,
|
|
85794
|
+
deviceId: frame.deviceId,
|
|
83987
85795
|
mediaKey: input.frameId,
|
|
83988
85796
|
bbox: a.bbox,
|
|
83989
85797
|
macroClass: a.macroClass,
|
|
@@ -83999,7 +85807,8 @@ var RetrainService = class {
|
|
|
83999
85807
|
this.deps.logger.info("retrain annotations saved", {
|
|
84000
85808
|
tags: { deviceId: input.deviceId },
|
|
84001
85809
|
meta: {
|
|
84002
|
-
|
|
85810
|
+
origin: frame.origin,
|
|
85811
|
+
trackId: frame.trackId,
|
|
84003
85812
|
frameId: input.frameId,
|
|
84004
85813
|
subjects: saved.length - errors,
|
|
84005
85814
|
modelErrors: errors
|
|
@@ -93401,6 +95210,14 @@ var RECORDED_STILL_PROFILE = "high";
|
|
|
93401
95210
|
*/
|
|
93402
95211
|
var RECOVERED_KEY_FRAME_MAX_WIDTH = 1920;
|
|
93403
95212
|
/**
|
|
95213
|
+
* Width cap for a still added to the retrain dataset from the recording.
|
|
95214
|
+
*
|
|
95215
|
+
* Effectively none: a training frame is wanted at the camera's own resolution
|
|
95216
|
+
* (the same pixels a native key frame has), so the bound is only a ceiling
|
|
95217
|
+
* against a pathological stream, far above any camera on the fleet.
|
|
95218
|
+
*/
|
|
95219
|
+
var RETRAIN_STILL_MAX_WIDTH = 7680;
|
|
95220
|
+
/**
|
|
93404
95221
|
* Build the production ports.
|
|
93405
95222
|
*
|
|
93406
95223
|
* Every port swallows its own failure into the `null` the policy expects: a
|
|
@@ -107096,6 +108913,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
107096
108913
|
/** The retrain curation surface — see `retrain/retrain-service.ts`. Null
|
|
107097
108914
|
* until the stores are built. */
|
|
107098
108915
|
retrain = null;
|
|
108916
|
+
retrainCollections = null;
|
|
108917
|
+
retrainLlmAssist = null;
|
|
108918
|
+
retrainDatasetVersions = null;
|
|
108919
|
+
retrainDatasets = null;
|
|
108920
|
+
retrainCollectionStore = null;
|
|
107099
108921
|
retrainFrames = null;
|
|
107100
108922
|
retrainAnnotations = null;
|
|
107101
108923
|
/** The NC artifact plane (signed public links for notification media) and its
|
|
@@ -108891,6 +110713,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
108891
110713
|
RetrainFrameStore.declare(api.settingsStore),
|
|
108892
110714
|
RetrainAnnotationStore.declare(api.settingsStore),
|
|
108893
110715
|
RetrainCollectionStore.declare(api.settingsStore),
|
|
110716
|
+
RetrainDatasetVersionStore.declare(api.settingsStore),
|
|
108894
110717
|
OpsLogStore.declare(api.settingsStore),
|
|
108895
110718
|
DebugNoteArchiveStore.declare(api.settingsStore),
|
|
108896
110719
|
TrackingTapeIndexStore.declare(api.settingsStore),
|
|
@@ -109147,7 +110970,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
109147
110970
|
});
|
|
109148
110971
|
this.retrainFrames = retrainFrames;
|
|
109149
110972
|
this.retrainAnnotations = retrainAnnotations;
|
|
109150
|
-
|
|
110973
|
+
const retrainCollectionStore = new RetrainCollectionStore({
|
|
110974
|
+
store: api.settingsStore,
|
|
110975
|
+
logger: logger.child("RetrainCollections"),
|
|
110976
|
+
now: () => Date.now(),
|
|
110977
|
+
newId: () => randomUUID()
|
|
110978
|
+
});
|
|
110979
|
+
this.retrainCollectionStore = retrainCollectionStore;
|
|
110980
|
+
const retrain = new RetrainService({
|
|
109151
110981
|
tracks: {
|
|
109152
110982
|
listStaging: (deviceId, limit) => trackStore.listStaging(deviceId, limit),
|
|
109153
110983
|
listDeviceIds: () => trackStore.listDeviceIds(),
|
|
@@ -109165,7 +110995,82 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
109165
110995
|
frames: retrainFrames,
|
|
109166
110996
|
annotations: retrainAnnotations,
|
|
109167
110997
|
runner: (input, nodeId) => nodeId === void 0 ? api.pipelineRunner.runStatelessStep.mutate(input) : api.pipelineRunner.runStatelessStep.mutate(input, nodePin(nodeId)),
|
|
109168
|
-
logger: logger.child("Retrain")
|
|
110998
|
+
logger: logger.child("Retrain"),
|
|
110999
|
+
pipelineEvidence: {
|
|
111000
|
+
tracksAround: (deviceId, atMs) => trackStore.queryHistorical({
|
|
111001
|
+
deviceId,
|
|
111002
|
+
since: atMs - PIPELINE_TRACK_LOOKBACK_MS,
|
|
111003
|
+
until: atMs + PIPELINE_BOX_TOLERANCE_MS,
|
|
111004
|
+
limit: PIPELINE_TRACK_AROUND_LIMIT
|
|
111005
|
+
}),
|
|
111006
|
+
subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
|
|
111007
|
+
},
|
|
111008
|
+
memberships: retrainCollectionStore
|
|
111009
|
+
});
|
|
111010
|
+
this.retrain = retrain;
|
|
111011
|
+
const retrainStills = new RecordedStillService(buildRecordedStillPorts({
|
|
111012
|
+
api: this.ctx.api.recordingArchive,
|
|
111013
|
+
logger: logger.child("RetrainStills"),
|
|
111014
|
+
resolveFfmpeg: this.pinnedFfmpeg
|
|
111015
|
+
}));
|
|
111016
|
+
this.retrainCollections = new RetrainCollectionService({
|
|
111017
|
+
collections: retrainCollectionStore,
|
|
111018
|
+
frames: retrainFrames,
|
|
111019
|
+
annotations: retrainAnnotations,
|
|
111020
|
+
retrain,
|
|
111021
|
+
captureStill: (deviceId, epochMs) => retrainStills.recover(deviceId, epochMs, RETRAIN_STILL_MAX_WIDTH),
|
|
111022
|
+
logger: logger.child("RetrainCollections")
|
|
111023
|
+
});
|
|
111024
|
+
const retrainDatasetVersions = new RetrainDatasetVersionStore({
|
|
111025
|
+
store: api.settingsStore,
|
|
111026
|
+
logger: logger.child("RetrainDatasets")
|
|
111027
|
+
});
|
|
111028
|
+
this.retrainDatasetVersions = retrainDatasetVersions;
|
|
111029
|
+
this.retrainDatasets = new RetrainDatasetService({
|
|
111030
|
+
versions: retrainDatasetVersions,
|
|
111031
|
+
buildPlan: (request) => this.buildRetrainExportPlan(request),
|
|
111032
|
+
now: () => Date.now(),
|
|
111033
|
+
newId: () => randomUUID(),
|
|
111034
|
+
logger: logger.child("RetrainDatasets")
|
|
111035
|
+
});
|
|
111036
|
+
this.retrainLlmAssist = new RetrainLlmAssistService({
|
|
111037
|
+
frames: retrainFrames,
|
|
111038
|
+
llm: {
|
|
111039
|
+
listProfiles: async () => (await api.llm.listProfiles.query({})).map((p) => ({
|
|
111040
|
+
id: p.id,
|
|
111041
|
+
name: p.name,
|
|
111042
|
+
model: p.model,
|
|
111043
|
+
kind: p.kind,
|
|
111044
|
+
...p.baseUrl !== void 0 ? { baseUrl: p.baseUrl } : {},
|
|
111045
|
+
enabled: p.enabled,
|
|
111046
|
+
supportsVision: p.supportsVision
|
|
111047
|
+
})),
|
|
111048
|
+
generateVision: async (request) => {
|
|
111049
|
+
const result = await api.llm.generateVision.mutate({
|
|
111050
|
+
profileId: request.profileId,
|
|
111051
|
+
consumer: RETRAIN_LLM_CONSUMER,
|
|
111052
|
+
system: request.system,
|
|
111053
|
+
prompt: request.prompt,
|
|
111054
|
+
jsonSchema: request.jsonSchema,
|
|
111055
|
+
images: [{
|
|
111056
|
+
bytes: new Uint8Array(request.image),
|
|
111057
|
+
mimeType: "image/jpeg"
|
|
111058
|
+
}],
|
|
111059
|
+
temperature: 0,
|
|
111060
|
+
timeoutMs: RETRAIN_LLM_TIMEOUT_MS
|
|
111061
|
+
});
|
|
111062
|
+
return result.ok ? {
|
|
111063
|
+
ok: true,
|
|
111064
|
+
text: result.text,
|
|
111065
|
+
model: result.model
|
|
111066
|
+
} : {
|
|
111067
|
+
ok: false,
|
|
111068
|
+
code: result.code,
|
|
111069
|
+
message: result.message
|
|
111070
|
+
};
|
|
111071
|
+
}
|
|
111072
|
+
},
|
|
111073
|
+
logger: logger.child("RetrainLlmAssist")
|
|
109169
111074
|
});
|
|
109170
111075
|
this.mediaRelocate = new MediaRelocateEngine({
|
|
109171
111076
|
store: api.settingsStore,
|
|
@@ -110349,14 +112254,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
110349
112254
|
};
|
|
110350
112255
|
return this.requireRetrain().propose({
|
|
110351
112256
|
deviceId: input.deviceId,
|
|
110352
|
-
trackId: input.trackId,
|
|
112257
|
+
...input.trackId !== void 0 ? { trackId: input.trackId } : {},
|
|
110353
112258
|
frameId: input.frameId,
|
|
110354
112259
|
subject,
|
|
110355
112260
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
|
|
110356
112261
|
});
|
|
110357
112262
|
}
|
|
110358
112263
|
async listRetrainAnnotations(input) {
|
|
110359
|
-
return this.requireRetrain().listAnnotations(input
|
|
112264
|
+
return this.requireRetrain().listAnnotations(input);
|
|
110360
112265
|
}
|
|
110361
112266
|
async saveRetrainAnnotations(input) {
|
|
110362
112267
|
return this.requireRetrain().saveAnnotations(input);
|
|
@@ -110380,6 +112285,69 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
110380
112285
|
async setRetrainFrameComplete(input) {
|
|
110381
112286
|
return this.requireRetrain().setFrameComplete(input);
|
|
110382
112287
|
}
|
|
112288
|
+
requireRetrainDatasets() {
|
|
112289
|
+
const service = this.retrainDatasets;
|
|
112290
|
+
if (service === null) throw new Error("retrain datasets: analytics stores unavailable");
|
|
112291
|
+
return service;
|
|
112292
|
+
}
|
|
112293
|
+
async previewRetrainDataset(input) {
|
|
112294
|
+
return this.requireRetrainDatasets().preview({
|
|
112295
|
+
selection: input.selection,
|
|
112296
|
+
completeOnly: input.completeOnly ?? false
|
|
112297
|
+
});
|
|
112298
|
+
}
|
|
112299
|
+
async createRetrainDatasetVersion(input) {
|
|
112300
|
+
return this.requireRetrainDatasets().create(input);
|
|
112301
|
+
}
|
|
112302
|
+
async listRetrainDatasetVersions(_input) {
|
|
112303
|
+
return this.requireRetrainDatasets().list();
|
|
112304
|
+
}
|
|
112305
|
+
async deleteRetrainDatasetVersion(input) {
|
|
112306
|
+
return this.requireRetrainDatasets().delete(input);
|
|
112307
|
+
}
|
|
112308
|
+
async verifyRetrainDatasetVersion(input) {
|
|
112309
|
+
return this.requireRetrainDatasets().verify(input);
|
|
112310
|
+
}
|
|
112311
|
+
requireRetrainLlmAssist() {
|
|
112312
|
+
const service = this.retrainLlmAssist;
|
|
112313
|
+
if (service === null) throw new Error("retrain AI assist: analytics stores unavailable");
|
|
112314
|
+
return service;
|
|
112315
|
+
}
|
|
112316
|
+
async listRetrainAssistProfiles(_input) {
|
|
112317
|
+
return this.requireRetrainLlmAssist().listProfiles();
|
|
112318
|
+
}
|
|
112319
|
+
async proposeRetrainAnnotationsWithLlm(input) {
|
|
112320
|
+
return this.requireRetrainLlmAssist().propose(input);
|
|
112321
|
+
}
|
|
112322
|
+
requireRetrainCollections() {
|
|
112323
|
+
const service = this.retrainCollections;
|
|
112324
|
+
if (service === null) throw new Error("retrain collections: analytics stores unavailable");
|
|
112325
|
+
return service;
|
|
112326
|
+
}
|
|
112327
|
+
async createRetrainCollection(input) {
|
|
112328
|
+
return this.requireRetrainCollections().create(input);
|
|
112329
|
+
}
|
|
112330
|
+
async listRetrainCollections(_input) {
|
|
112331
|
+
return this.requireRetrainCollections().list();
|
|
112332
|
+
}
|
|
112333
|
+
async renameRetrainCollection(input) {
|
|
112334
|
+
return this.requireRetrainCollections().rename(input);
|
|
112335
|
+
}
|
|
112336
|
+
async deleteRetrainCollection(input) {
|
|
112337
|
+
return this.requireRetrainCollections().delete(input);
|
|
112338
|
+
}
|
|
112339
|
+
async addRetrainFrameFromRecording(input) {
|
|
112340
|
+
return this.requireRetrainCollections().addFrameFromRecording(input);
|
|
112341
|
+
}
|
|
112342
|
+
async addRetrainFramesToCollection(input) {
|
|
112343
|
+
return this.requireRetrainCollections().addFrames(input);
|
|
112344
|
+
}
|
|
112345
|
+
async removeRetrainFrameFromCollection(input) {
|
|
112346
|
+
return this.requireRetrainCollections().removeFrame(input);
|
|
112347
|
+
}
|
|
112348
|
+
async listRetrainCollectionFrames(input) {
|
|
112349
|
+
return this.requireRetrainCollections().listFrames(input);
|
|
112350
|
+
}
|
|
110383
112351
|
/**
|
|
110384
112352
|
* Stream the annotated archive into ONE HTTP request to `target` — the
|
|
110385
112353
|
* training upload. Same bytes as the download; never on the hub's disk.
|
|
@@ -110730,12 +112698,28 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
110730
112698
|
* zones simply has no `zoneCrop` geometry — absent, never a clamped guess.
|
|
110731
112699
|
*/
|
|
110732
112700
|
async buildRetrainExportPlan(request) {
|
|
112701
|
+
const versionId = request.filter.datasetVersionId;
|
|
112702
|
+
if (versionId !== void 0) {
|
|
112703
|
+
const version = await this.retrainDatasetVersions?.get(versionId) ?? null;
|
|
112704
|
+
const { datasetVersionId: _resolved, ...rest } = await this.retrainDatasets?.exportFilterFor(request.filter) ?? {
|
|
112705
|
+
...request.filter,
|
|
112706
|
+
frameIds: []
|
|
112707
|
+
};
|
|
112708
|
+
const plan = await this.buildRetrainExportPlan({
|
|
112709
|
+
...request,
|
|
112710
|
+
filter: rest
|
|
112711
|
+
});
|
|
112712
|
+
return version === null ? plan : {
|
|
112713
|
+
...plan,
|
|
112714
|
+
manifest: withDatasetVersion(plan.manifest, version)
|
|
112715
|
+
};
|
|
112716
|
+
}
|
|
110733
112717
|
const frameStore = this.retrainFrames;
|
|
110734
112718
|
const annotationStore = this.retrainAnnotations;
|
|
110735
112719
|
const trackStore = this.trackStore;
|
|
110736
112720
|
if (!frameStore || !annotationStore || !trackStore) throw new Error("retrain export: analytics stores unavailable");
|
|
110737
112721
|
const deviceIds = request.filter.deviceIds ?? [];
|
|
110738
|
-
const devices = deviceIds.length > 0 ? deviceIds : await trackStore.listDeviceIds();
|
|
112722
|
+
const devices = deviceIds.length > 0 ? deviceIds : [...new Set([...await trackStore.listDeviceIds(), ...await frameStore.listDeviceIds()])];
|
|
110739
112723
|
const candidates = /* @__PURE__ */ new Map();
|
|
110740
112724
|
const annotationsByFrame = /* @__PURE__ */ new Map();
|
|
110741
112725
|
const zones = /* @__PURE__ */ new Map();
|
|
@@ -110752,11 +112736,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
110752
112736
|
const frame = await frameStore.get(frameId);
|
|
110753
112737
|
if (frame !== null) candidates.set(frameId, frame);
|
|
110754
112738
|
}
|
|
110755
|
-
const
|
|
112739
|
+
const collectionsByFrame = await this.retrainCollectionStore?.collectionsOf([...candidates.keys()]) ?? /* @__PURE__ */ new Map();
|
|
112740
|
+
const frames = filterRetrainExport([...candidates.values()], annotationsByFrame, request.filter, collectionsByFrame);
|
|
110756
112741
|
return {
|
|
110757
112742
|
manifest: buildRetrainManifest({
|
|
110758
112743
|
frames,
|
|
110759
112744
|
annotationsByFrame,
|
|
112745
|
+
collectionsByFrame,
|
|
110760
112746
|
zoneForDevice: (deviceId) => zones.get(deviceId) ?? null,
|
|
110761
112747
|
generatedAt: Date.now(),
|
|
110762
112748
|
batchId: randomUUID(),
|