@camstack/addon-pipeline 1.1.61 → 1.1.62
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-p_JqOxtk.js → dist-DlMmkOsE.js} +164 -2
- package/dist/{dist-D16_dBsr.mjs → dist-LkwlodgP.mjs} +159 -3
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +161 -20
- package/dist/recorder/index.mjs +161 -20
- package/dist/{step-definitions-CoxF5G31.js → step-definitions-CxwB0fEq.js} +68 -1
- package/dist/{step-definitions-DxAcpUV-.mjs → step-definitions-DxN9tmQP.mjs} +68 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Wu4TlSuq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-3gAFmxjh.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHJB3xUN.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DGxcX3Wf.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C4pcysOQ.mjs} +1 -1
- package/dist/stream-broker/{hostInit-D_dSciqF.mjs → hostInit-B1aDV0u0.mjs} +3 -3
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BldL9_Ee.js → MaskShapeCanvas-DI4BY7W2-C7IhxQQ_.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BgWqMe4f.js → MotionZonesSettings-NcxxQN8r-DZXzBUU8.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-YBgaXffP.js → PrivacyMaskSettings-APgPLF7p-Dz9ttl4A.js} +1 -1
- package/embed-dist/assets/index-DRsQUyLF.js +81 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BPujpbZ0.mjs +0 -26
- package/embed-dist/assets/index-e0bQ8yep.js +0 -81
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-DlMmkOsE.js");
|
|
3
3
|
const require_sheet_geometry = require("../sheet-geometry-BR-ip0CP.js");
|
|
4
4
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-Dv-oFwjN.js");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
|
@@ -1005,6 +1005,34 @@ function segmentsByLocation(rows) {
|
|
|
1005
1005
|
return out;
|
|
1006
1006
|
}
|
|
1007
1007
|
/**
|
|
1008
|
+
* Evict a set of victim segments through the SegmentStore, grouped by their
|
|
1009
|
+
* storage location, and return the reclaimed bytes + deleted-bucket count.
|
|
1010
|
+
* Per-location isolated: one location's evict failure is logged and never aborts
|
|
1011
|
+
* the others. Shared by `pruneFootage` (retention) and `deleteFootprint`
|
|
1012
|
+
* (manual).
|
|
1013
|
+
*/
|
|
1014
|
+
async function evictVictims(deps, deviceId, victims) {
|
|
1015
|
+
let reclaimedBytes = 0;
|
|
1016
|
+
let deletedBuckets = 0;
|
|
1017
|
+
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1018
|
+
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1019
|
+
reclaimedBytes += freed;
|
|
1020
|
+
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
deps.logger.warn("recorder evict failed for location", {
|
|
1023
|
+
tags: { deviceId },
|
|
1024
|
+
meta: {
|
|
1025
|
+
locationId,
|
|
1026
|
+
error: require_dist.errMsg(err)
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
reclaimedBytes,
|
|
1032
|
+
deletedBuckets
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1008
1036
|
* Build the `IRecordingProvider`. The provider is a thin façade over the v2
|
|
1009
1037
|
* core; all storage I/O flows through the injected deps.
|
|
1010
1038
|
*/
|
|
@@ -1101,37 +1129,63 @@ function buildRecordingProvider(deps) {
|
|
|
1101
1129
|
rescanStorage: async ({ deviceId }) => {
|
|
1102
1130
|
const locations = await deps.refreshLocations();
|
|
1103
1131
|
await deps.hydrateDevice(deviceId, locations);
|
|
1104
|
-
|
|
1132
|
+
const config = await loadDeviceConfig(deps.configStore, deviceId);
|
|
1133
|
+
await deps.opsLog?.append({
|
|
1134
|
+
op: "rescan",
|
|
1135
|
+
reason: "operator",
|
|
1136
|
+
deviceId,
|
|
1137
|
+
itemsAffected: deps.index.segments(deviceId).length,
|
|
1138
|
+
bytesReclaimed: 0,
|
|
1139
|
+
detail: "rescanned footage from disk"
|
|
1140
|
+
});
|
|
1141
|
+
return statusFor(deps, deviceId, config);
|
|
1105
1142
|
},
|
|
1106
|
-
pruneFootage: async ({ deviceId }) => {
|
|
1143
|
+
pruneFootage: async ({ deviceId, reason }) => {
|
|
1107
1144
|
const retention = (await loadDeviceConfig(deps.configStore, deviceId)).retention;
|
|
1108
1145
|
const nowMs = Date.now();
|
|
1109
|
-
const
|
|
1146
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, selectEvictions(deps.index.segments(deviceId), {
|
|
1110
1147
|
maxAgeDays: retention?.maxAgeDays,
|
|
1111
1148
|
maxSizeGb: retention?.maxSizeGb,
|
|
1112
1149
|
nowMs
|
|
1150
|
+
}));
|
|
1151
|
+
const floorMs = deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null);
|
|
1152
|
+
const opReason = reason ?? "retention";
|
|
1153
|
+
await deps.opsLog?.append({
|
|
1154
|
+
op: "prune",
|
|
1155
|
+
reason: opReason,
|
|
1156
|
+
deviceId,
|
|
1157
|
+
itemsAffected: deletedBuckets,
|
|
1158
|
+
bytesReclaimed: reclaimedBytes,
|
|
1159
|
+
detail: floorMs === null ? "no footage remaining" : `floor moved to ${floorMs}`
|
|
1160
|
+
});
|
|
1161
|
+
return {
|
|
1162
|
+
floorMs,
|
|
1163
|
+
deletedBuckets,
|
|
1164
|
+
reclaimedBytes
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
deleteFootprint: async ({ deviceId, fromMs, toMs }) => {
|
|
1168
|
+
const lo = fromMs ?? Number.NEGATIVE_INFINITY;
|
|
1169
|
+
const hi = toMs ?? Number.POSITIVE_INFINITY;
|
|
1170
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, deps.index.segments(deviceId).filter((s) => s.startMs < hi && s.startMs + s.durMs > lo));
|
|
1171
|
+
const windowed = fromMs !== void 0 || toMs !== void 0;
|
|
1172
|
+
await deps.opsLog?.append({
|
|
1173
|
+
op: "manual-delete",
|
|
1174
|
+
reason: "manual",
|
|
1175
|
+
deviceId,
|
|
1176
|
+
itemsAffected: deletedBuckets,
|
|
1177
|
+
bytesReclaimed: reclaimedBytes,
|
|
1178
|
+
detail: windowed ? `deleted footage in [${fromMs ?? "−∞"}, ${toMs ?? "+∞"})` : "deleted all footage"
|
|
1113
1179
|
});
|
|
1114
|
-
let reclaimedBytes = 0;
|
|
1115
|
-
let deletedBuckets = 0;
|
|
1116
|
-
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1117
|
-
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1118
|
-
reclaimedBytes += freed;
|
|
1119
|
-
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1120
|
-
} catch (err) {
|
|
1121
|
-
deps.logger.warn("recorder pruneFootage evict failed for location", {
|
|
1122
|
-
tags: { deviceId },
|
|
1123
|
-
meta: {
|
|
1124
|
-
locationId,
|
|
1125
|
-
error: require_dist.errMsg(err)
|
|
1126
|
-
}
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
1180
|
return {
|
|
1130
|
-
floorMs: deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null),
|
|
1131
1181
|
deletedBuckets,
|
|
1132
1182
|
reclaimedBytes
|
|
1133
1183
|
};
|
|
1134
1184
|
},
|
|
1185
|
+
listOpsLog: async ({ deviceId, limit }) => await deps.opsLog?.list({
|
|
1186
|
+
deviceId,
|
|
1187
|
+
limit
|
|
1188
|
+
}) ?? [],
|
|
1135
1189
|
getDeviceSettingsContribution: async ({ deviceId }) => {
|
|
1136
1190
|
if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
|
|
1137
1191
|
return require_dist.hydrateSchema(buildRecordingDeviceSchema(), {});
|
|
@@ -1208,6 +1262,81 @@ async function listExportsFor(state, deviceId) {
|
|
|
1208
1262
|
return [...(await readExports(state)).values()].filter((r) => deviceId === void 0 || r.deviceId === deviceId).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
1209
1263
|
}
|
|
1210
1264
|
//#endregion
|
|
1265
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
1266
|
+
/**
|
|
1267
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
1268
|
+
*
|
|
1269
|
+
* A bounded, append-only audit ring lives in ONE addon-store blob (same
|
|
1270
|
+
* DurableState pattern as `export-store.ts` / `config-store.ts`) — no SQLite in
|
|
1271
|
+
* the recorder. The whole Zod-validated array round-trips on every read/write,
|
|
1272
|
+
* so no row is silently dropped on persist. Appends evict the oldest rows once
|
|
1273
|
+
* the ring exceeds its cap, so the blob stays bounded regardless of churn. A
|
|
1274
|
+
* corrupt blob reads back as EMPTY rather than crashing boot.
|
|
1275
|
+
*
|
|
1276
|
+
* Writes are BEST-EFFORT: `createRecordingOpsLogSink().append` never throws — a
|
|
1277
|
+
* failed audit write must not fail the operation it records.
|
|
1278
|
+
*/
|
|
1279
|
+
/** The addon-store key holding the bounded ops-log ring. */
|
|
1280
|
+
var RECORDING_OPS_LOG_KEY = "recordingOpsLog";
|
|
1281
|
+
/** Persisted shape: a flat array of ops-log rows (oldest-first on disk). */
|
|
1282
|
+
var RecordingOpsLogBlobSchema = require_dist.array(require_dist.OpsLogEntrySchema);
|
|
1283
|
+
/**
|
|
1284
|
+
* Append one row and persist, evicting the oldest rows so the ring never grows
|
|
1285
|
+
* past `maxEntries`. Validates at the boundary. Returns the stored row. Rows are
|
|
1286
|
+
* kept oldest-first on disk (the tail is newest); {@link listOpsLog} reverses.
|
|
1287
|
+
*/
|
|
1288
|
+
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
1289
|
+
const validated = require_dist.OpsLogEntrySchema.parse(entry);
|
|
1290
|
+
const next = [...await state.get(), validated];
|
|
1291
|
+
const bounded = next.length > maxEntries ? next.slice(next.length - maxEntries) : next;
|
|
1292
|
+
await state.set(bounded);
|
|
1293
|
+
return validated;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
1297
|
+
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
1298
|
+
*/
|
|
1299
|
+
async function listOpsLog(state, query) {
|
|
1300
|
+
const rows = await state.get();
|
|
1301
|
+
return (query.deviceId === void 0 ? rows : rows.filter((r) => r.deviceId === query.deviceId)).toSorted((a, b) => b.at - a.at).slice(0, query.limit ?? 200);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
1305
|
+
* `nodeId`, `id` and `at`, then persists best-effort — a failed write is logged,
|
|
1306
|
+
* never thrown, so it cannot fail the operation being audited.
|
|
1307
|
+
*/
|
|
1308
|
+
function createRecordingOpsLogSink(deps) {
|
|
1309
|
+
return {
|
|
1310
|
+
append: async (input) => {
|
|
1311
|
+
const entry = {
|
|
1312
|
+
id: deps.newId(),
|
|
1313
|
+
at: deps.now(),
|
|
1314
|
+
domain: "recording",
|
|
1315
|
+
op: input.op,
|
|
1316
|
+
reason: input.reason,
|
|
1317
|
+
deviceId: input.deviceId,
|
|
1318
|
+
nodeId: deps.nodeId,
|
|
1319
|
+
itemsAffected: input.itemsAffected,
|
|
1320
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
1321
|
+
detail: input.detail ?? null,
|
|
1322
|
+
actor: input.actor ?? "operator"
|
|
1323
|
+
};
|
|
1324
|
+
try {
|
|
1325
|
+
await appendOpsLog(deps.state, entry, deps.maxEntries);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
deps.logger.warn("recorder: ops-log append failed (best-effort)", {
|
|
1328
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
1329
|
+
meta: {
|
|
1330
|
+
op: input.op,
|
|
1331
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
list: (query) => listOpsLog(deps.state, query)
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
//#endregion
|
|
1211
1340
|
//#region src/recorder/addon/export-ffmpeg-args.ts
|
|
1212
1341
|
/** Widest [min,max] speed a single ffmpeg `atempo` filter accepts. Outside this
|
|
1213
1342
|
* band audio is dropped rather than chained (kept simple + predictable). */
|
|
@@ -3582,12 +3711,20 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3582
3711
|
}),
|
|
3583
3712
|
brokerCall: makeBrokerCall(brokerHandle)
|
|
3584
3713
|
});
|
|
3714
|
+
const opsLog = createRecordingOpsLogSink({
|
|
3715
|
+
state: this.opsLogStore(),
|
|
3716
|
+
nodeId: this.nodeId,
|
|
3717
|
+
now: () => Date.now(),
|
|
3718
|
+
newId: () => (0, node_crypto.randomUUID)(),
|
|
3719
|
+
logger: this.ctx.logger.child("ops-log")
|
|
3720
|
+
});
|
|
3585
3721
|
const recordingDeps = {
|
|
3586
3722
|
index: this.index,
|
|
3587
3723
|
segmentStore: this.segmentStore,
|
|
3588
3724
|
nodeId: this.nodeId,
|
|
3589
3725
|
logger: this.ctx.logger,
|
|
3590
3726
|
configStore: this.configStore(),
|
|
3727
|
+
opsLog,
|
|
3591
3728
|
locations: () => this.resolvedLocations,
|
|
3592
3729
|
refreshLocations: () => this.refreshLocations(),
|
|
3593
3730
|
capacity: (root) => this.locationCapacity(root),
|
|
@@ -3926,6 +4063,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3926
4063
|
exportsStore() {
|
|
3927
4064
|
return this.state(RECORDING_EXPORTS_KEY, RecordingExportsBlobSchema, {});
|
|
3928
4065
|
}
|
|
4066
|
+
/** Durable handle over the bounded recordings ops-log ring. */
|
|
4067
|
+
opsLogStore() {
|
|
4068
|
+
return this.state(RECORDING_OPS_LOG_KEY, RecordingOpsLogBlobSchema, []);
|
|
4069
|
+
}
|
|
3929
4070
|
/** Retention deps shared by the sweep and the download-complete hook. */
|
|
3930
4071
|
exportJanitorDeps() {
|
|
3931
4072
|
return {
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as
|
|
1
|
+
import { B as errMsg, D as migrateConfigToBands, I as storageEvictableCapability, K as hydrateSchema, M as recordingCapability, N as recordingExportCapability, P as resolveScrubThumbnailGeometry, Q as selectAssignedProfileSlots, V as BaseAddon, W as DeviceType, ct as record, d as OpsLogEntrySchema, dt as EventCategory, f as RecordingConfigSchema, k as nodePin, l as ExportRecordSchema, lt as string, q as isEvent, s as EVENT_PAD_MS, tt as array } from "../dist-LkwlodgP.mjs";
|
|
2
2
|
import { n as computeSheetGeometry, r as resolveHubHostname, t as allTilePlacements } from "../sheet-geometry-DcpuGTCm.mjs";
|
|
3
3
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -1002,6 +1002,34 @@ function segmentsByLocation(rows) {
|
|
|
1002
1002
|
return out;
|
|
1003
1003
|
}
|
|
1004
1004
|
/**
|
|
1005
|
+
* Evict a set of victim segments through the SegmentStore, grouped by their
|
|
1006
|
+
* storage location, and return the reclaimed bytes + deleted-bucket count.
|
|
1007
|
+
* Per-location isolated: one location's evict failure is logged and never aborts
|
|
1008
|
+
* the others. Shared by `pruneFootage` (retention) and `deleteFootprint`
|
|
1009
|
+
* (manual).
|
|
1010
|
+
*/
|
|
1011
|
+
async function evictVictims(deps, deviceId, victims) {
|
|
1012
|
+
let reclaimedBytes = 0;
|
|
1013
|
+
let deletedBuckets = 0;
|
|
1014
|
+
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1015
|
+
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1016
|
+
reclaimedBytes += freed;
|
|
1017
|
+
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
deps.logger.warn("recorder evict failed for location", {
|
|
1020
|
+
tags: { deviceId },
|
|
1021
|
+
meta: {
|
|
1022
|
+
locationId,
|
|
1023
|
+
error: errMsg(err)
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
reclaimedBytes,
|
|
1029
|
+
deletedBuckets
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1005
1033
|
* Build the `IRecordingProvider`. The provider is a thin façade over the v2
|
|
1006
1034
|
* core; all storage I/O flows through the injected deps.
|
|
1007
1035
|
*/
|
|
@@ -1098,37 +1126,63 @@ function buildRecordingProvider(deps) {
|
|
|
1098
1126
|
rescanStorage: async ({ deviceId }) => {
|
|
1099
1127
|
const locations = await deps.refreshLocations();
|
|
1100
1128
|
await deps.hydrateDevice(deviceId, locations);
|
|
1101
|
-
|
|
1129
|
+
const config = await loadDeviceConfig(deps.configStore, deviceId);
|
|
1130
|
+
await deps.opsLog?.append({
|
|
1131
|
+
op: "rescan",
|
|
1132
|
+
reason: "operator",
|
|
1133
|
+
deviceId,
|
|
1134
|
+
itemsAffected: deps.index.segments(deviceId).length,
|
|
1135
|
+
bytesReclaimed: 0,
|
|
1136
|
+
detail: "rescanned footage from disk"
|
|
1137
|
+
});
|
|
1138
|
+
return statusFor(deps, deviceId, config);
|
|
1102
1139
|
},
|
|
1103
|
-
pruneFootage: async ({ deviceId }) => {
|
|
1140
|
+
pruneFootage: async ({ deviceId, reason }) => {
|
|
1104
1141
|
const retention = (await loadDeviceConfig(deps.configStore, deviceId)).retention;
|
|
1105
1142
|
const nowMs = Date.now();
|
|
1106
|
-
const
|
|
1143
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, selectEvictions(deps.index.segments(deviceId), {
|
|
1107
1144
|
maxAgeDays: retention?.maxAgeDays,
|
|
1108
1145
|
maxSizeGb: retention?.maxSizeGb,
|
|
1109
1146
|
nowMs
|
|
1147
|
+
}));
|
|
1148
|
+
const floorMs = deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null);
|
|
1149
|
+
const opReason = reason ?? "retention";
|
|
1150
|
+
await deps.opsLog?.append({
|
|
1151
|
+
op: "prune",
|
|
1152
|
+
reason: opReason,
|
|
1153
|
+
deviceId,
|
|
1154
|
+
itemsAffected: deletedBuckets,
|
|
1155
|
+
bytesReclaimed: reclaimedBytes,
|
|
1156
|
+
detail: floorMs === null ? "no footage remaining" : `floor moved to ${floorMs}`
|
|
1157
|
+
});
|
|
1158
|
+
return {
|
|
1159
|
+
floorMs,
|
|
1160
|
+
deletedBuckets,
|
|
1161
|
+
reclaimedBytes
|
|
1162
|
+
};
|
|
1163
|
+
},
|
|
1164
|
+
deleteFootprint: async ({ deviceId, fromMs, toMs }) => {
|
|
1165
|
+
const lo = fromMs ?? Number.NEGATIVE_INFINITY;
|
|
1166
|
+
const hi = toMs ?? Number.POSITIVE_INFINITY;
|
|
1167
|
+
const { reclaimedBytes, deletedBuckets } = await evictVictims(deps, deviceId, deps.index.segments(deviceId).filter((s) => s.startMs < hi && s.startMs + s.durMs > lo));
|
|
1168
|
+
const windowed = fromMs !== void 0 || toMs !== void 0;
|
|
1169
|
+
await deps.opsLog?.append({
|
|
1170
|
+
op: "manual-delete",
|
|
1171
|
+
reason: "manual",
|
|
1172
|
+
deviceId,
|
|
1173
|
+
itemsAffected: deletedBuckets,
|
|
1174
|
+
bytesReclaimed: reclaimedBytes,
|
|
1175
|
+
detail: windowed ? `deleted footage in [${fromMs ?? "−∞"}, ${toMs ?? "+∞"})` : "deleted all footage"
|
|
1110
1176
|
});
|
|
1111
|
-
let reclaimedBytes = 0;
|
|
1112
|
-
let deletedBuckets = 0;
|
|
1113
|
-
for (const [locationId, locVictims] of segmentsByLocation(victims)) try {
|
|
1114
|
-
const freed = await deps.segmentStore.evict(locationId, locVictims);
|
|
1115
|
-
reclaimedBytes += freed;
|
|
1116
|
-
if (freed > 0) deletedBuckets += locVictims.length;
|
|
1117
|
-
} catch (err) {
|
|
1118
|
-
deps.logger.warn("recorder pruneFootage evict failed for location", {
|
|
1119
|
-
tags: { deviceId },
|
|
1120
|
-
meta: {
|
|
1121
|
-
locationId,
|
|
1122
|
-
error: errMsg(err)
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
1177
|
return {
|
|
1127
|
-
floorMs: deps.index.accounting(deviceId).oldestMs ?? (retention?.maxAgeDays ? nowMs - retention.maxAgeDays * DAY_MS : null),
|
|
1128
1178
|
deletedBuckets,
|
|
1129
1179
|
reclaimedBytes
|
|
1130
1180
|
};
|
|
1131
1181
|
},
|
|
1182
|
+
listOpsLog: async ({ deviceId, limit }) => await deps.opsLog?.list({
|
|
1183
|
+
deviceId,
|
|
1184
|
+
limit
|
|
1185
|
+
}) ?? [],
|
|
1132
1186
|
getDeviceSettingsContribution: async ({ deviceId }) => {
|
|
1133
1187
|
if (deps.isCameraDevice && !await deps.isCameraDevice(deviceId)) return null;
|
|
1134
1188
|
return hydrateSchema(buildRecordingDeviceSchema(), {});
|
|
@@ -1205,6 +1259,81 @@ async function listExportsFor(state, deviceId) {
|
|
|
1205
1259
|
return [...(await readExports(state)).values()].filter((r) => deviceId === void 0 || r.deviceId === deviceId).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
1206
1260
|
}
|
|
1207
1261
|
//#endregion
|
|
1262
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
1263
|
+
/**
|
|
1264
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
1265
|
+
*
|
|
1266
|
+
* A bounded, append-only audit ring lives in ONE addon-store blob (same
|
|
1267
|
+
* DurableState pattern as `export-store.ts` / `config-store.ts`) — no SQLite in
|
|
1268
|
+
* the recorder. The whole Zod-validated array round-trips on every read/write,
|
|
1269
|
+
* so no row is silently dropped on persist. Appends evict the oldest rows once
|
|
1270
|
+
* the ring exceeds its cap, so the blob stays bounded regardless of churn. A
|
|
1271
|
+
* corrupt blob reads back as EMPTY rather than crashing boot.
|
|
1272
|
+
*
|
|
1273
|
+
* Writes are BEST-EFFORT: `createRecordingOpsLogSink().append` never throws — a
|
|
1274
|
+
* failed audit write must not fail the operation it records.
|
|
1275
|
+
*/
|
|
1276
|
+
/** The addon-store key holding the bounded ops-log ring. */
|
|
1277
|
+
var RECORDING_OPS_LOG_KEY = "recordingOpsLog";
|
|
1278
|
+
/** Persisted shape: a flat array of ops-log rows (oldest-first on disk). */
|
|
1279
|
+
var RecordingOpsLogBlobSchema = array(OpsLogEntrySchema);
|
|
1280
|
+
/**
|
|
1281
|
+
* Append one row and persist, evicting the oldest rows so the ring never grows
|
|
1282
|
+
* past `maxEntries`. Validates at the boundary. Returns the stored row. Rows are
|
|
1283
|
+
* kept oldest-first on disk (the tail is newest); {@link listOpsLog} reverses.
|
|
1284
|
+
*/
|
|
1285
|
+
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
1286
|
+
const validated = OpsLogEntrySchema.parse(entry);
|
|
1287
|
+
const next = [...await state.get(), validated];
|
|
1288
|
+
const bounded = next.length > maxEntries ? next.slice(next.length - maxEntries) : next;
|
|
1289
|
+
await state.set(bounded);
|
|
1290
|
+
return validated;
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
1294
|
+
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
1295
|
+
*/
|
|
1296
|
+
async function listOpsLog(state, query) {
|
|
1297
|
+
const rows = await state.get();
|
|
1298
|
+
return (query.deviceId === void 0 ? rows : rows.filter((r) => r.deviceId === query.deviceId)).toSorted((a, b) => b.at - a.at).slice(0, query.limit ?? 200);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
1302
|
+
* `nodeId`, `id` and `at`, then persists best-effort — a failed write is logged,
|
|
1303
|
+
* never thrown, so it cannot fail the operation being audited.
|
|
1304
|
+
*/
|
|
1305
|
+
function createRecordingOpsLogSink(deps) {
|
|
1306
|
+
return {
|
|
1307
|
+
append: async (input) => {
|
|
1308
|
+
const entry = {
|
|
1309
|
+
id: deps.newId(),
|
|
1310
|
+
at: deps.now(),
|
|
1311
|
+
domain: "recording",
|
|
1312
|
+
op: input.op,
|
|
1313
|
+
reason: input.reason,
|
|
1314
|
+
deviceId: input.deviceId,
|
|
1315
|
+
nodeId: deps.nodeId,
|
|
1316
|
+
itemsAffected: input.itemsAffected,
|
|
1317
|
+
bytesReclaimed: input.bytesReclaimed,
|
|
1318
|
+
detail: input.detail ?? null,
|
|
1319
|
+
actor: input.actor ?? "operator"
|
|
1320
|
+
};
|
|
1321
|
+
try {
|
|
1322
|
+
await appendOpsLog(deps.state, entry, deps.maxEntries);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
deps.logger.warn("recorder: ops-log append failed (best-effort)", {
|
|
1325
|
+
tags: { deviceId: input.deviceId ?? void 0 },
|
|
1326
|
+
meta: {
|
|
1327
|
+
op: input.op,
|
|
1328
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
list: (query) => listOpsLog(deps.state, query)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
//#endregion
|
|
1208
1337
|
//#region src/recorder/addon/export-ffmpeg-args.ts
|
|
1209
1338
|
/** Widest [min,max] speed a single ffmpeg `atempo` filter accepts. Outside this
|
|
1210
1339
|
* band audio is dropped rather than chained (kept simple + predictable). */
|
|
@@ -3579,12 +3708,20 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
3579
3708
|
}),
|
|
3580
3709
|
brokerCall: makeBrokerCall(brokerHandle)
|
|
3581
3710
|
});
|
|
3711
|
+
const opsLog = createRecordingOpsLogSink({
|
|
3712
|
+
state: this.opsLogStore(),
|
|
3713
|
+
nodeId: this.nodeId,
|
|
3714
|
+
now: () => Date.now(),
|
|
3715
|
+
newId: () => randomUUID(),
|
|
3716
|
+
logger: this.ctx.logger.child("ops-log")
|
|
3717
|
+
});
|
|
3582
3718
|
const recordingDeps = {
|
|
3583
3719
|
index: this.index,
|
|
3584
3720
|
segmentStore: this.segmentStore,
|
|
3585
3721
|
nodeId: this.nodeId,
|
|
3586
3722
|
logger: this.ctx.logger,
|
|
3587
3723
|
configStore: this.configStore(),
|
|
3724
|
+
opsLog,
|
|
3588
3725
|
locations: () => this.resolvedLocations,
|
|
3589
3726
|
refreshLocations: () => this.refreshLocations(),
|
|
3590
3727
|
capacity: (root) => this.locationCapacity(root),
|
|
@@ -3923,6 +4060,10 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
3923
4060
|
exportsStore() {
|
|
3924
4061
|
return this.state(RECORDING_EXPORTS_KEY, RecordingExportsBlobSchema, {});
|
|
3925
4062
|
}
|
|
4063
|
+
/** Durable handle over the bounded recordings ops-log ring. */
|
|
4064
|
+
opsLogStore() {
|
|
4065
|
+
return this.state(RECORDING_OPS_LOG_KEY, RecordingOpsLogBlobSchema, []);
|
|
4066
|
+
}
|
|
3926
4067
|
/** Retention deps shared by the sweep and the download-complete hook. */
|
|
3927
4068
|
exportJanitorDeps() {
|
|
3928
4069
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-DlMmkOsE.js");
|
|
2
2
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
3
3
|
var HF_REPO = "camstack/camstack-models";
|
|
4
4
|
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
@@ -682,6 +682,58 @@ var PLATE_DETECTION_MODELS = [{
|
|
|
682
682
|
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
683
683
|
}
|
|
684
684
|
}];
|
|
685
|
+
var PACKAGE_DETECTION_MODELS = [{
|
|
686
|
+
id: "yolov8n-package",
|
|
687
|
+
name: "YOLOv8 Nano — Package",
|
|
688
|
+
description: "YOLOv8 Nano fine-tuned for parcel/package detection (mAP50 0.93) — dedicated model, not the COCO suitcase/backpack proxy",
|
|
689
|
+
inputSize: {
|
|
690
|
+
width: 640,
|
|
691
|
+
height: 640
|
|
692
|
+
},
|
|
693
|
+
labels: [{
|
|
694
|
+
id: "package",
|
|
695
|
+
name: "Package"
|
|
696
|
+
}],
|
|
697
|
+
preprocessMode: "letterbox",
|
|
698
|
+
license: "AGPL-3.0",
|
|
699
|
+
formats: {
|
|
700
|
+
onnx: {
|
|
701
|
+
url: hf("packageDetection/yolov8-package/onnx/camstack-yolov8n-package.onnx"),
|
|
702
|
+
sizeMB: 12
|
|
703
|
+
},
|
|
704
|
+
coreml: {
|
|
705
|
+
url: hf("packageDetection/yolov8-package/coreml/camstack-yolov8n-package.mlpackage"),
|
|
706
|
+
sizeMB: 12,
|
|
707
|
+
isDirectory: true,
|
|
708
|
+
files: [...MLPACKAGE_FILES],
|
|
709
|
+
runtimes: ["python"]
|
|
710
|
+
},
|
|
711
|
+
openvino: ovFormat(hf("packageDetection/yolov8-package/openvino/camstack-yolov8n-package.xml"), 12)
|
|
712
|
+
}
|
|
713
|
+
}, {
|
|
714
|
+
id: "rfdetr-package",
|
|
715
|
+
legacy: true,
|
|
716
|
+
name: "RF-DETR — Package (experimental)",
|
|
717
|
+
description: "RF-DETR parcel detector (onnx + openvino) — DETR-style decode; alternative to yolov8n-package. Requires an rfdetr postprocessor (not yet available)",
|
|
718
|
+
inputSize: {
|
|
719
|
+
width: 576,
|
|
720
|
+
height: 576
|
|
721
|
+
},
|
|
722
|
+
labels: [{
|
|
723
|
+
id: "package",
|
|
724
|
+
name: "Package"
|
|
725
|
+
}],
|
|
726
|
+
preprocessMode: "resize",
|
|
727
|
+
inputNormalization: "imagenet",
|
|
728
|
+
license: "Apache-2.0",
|
|
729
|
+
formats: {
|
|
730
|
+
onnx: {
|
|
731
|
+
url: hf("packageDetection/rfdetr-package/onnx/camstack-rfdetr-package.onnx"),
|
|
732
|
+
sizeMB: 30
|
|
733
|
+
},
|
|
734
|
+
openvino: ovFormat(hf("packageDetection/rfdetr-package/openvino/camstack-rfdetr-package.xml"), 30)
|
|
735
|
+
}
|
|
736
|
+
}];
|
|
685
737
|
var PLATE_OCR_MODELS = [{
|
|
686
738
|
id: "vgg-english-g2",
|
|
687
739
|
name: "VGG English G2",
|
|
@@ -1379,6 +1431,20 @@ var STEP_PLATE_OCR = new PipelineStepBase({
|
|
|
1379
1431
|
maxPerTrack: 5
|
|
1380
1432
|
}
|
|
1381
1433
|
});
|
|
1434
|
+
var STEP_PACKAGE_DETECTION = new PipelineStepBase({
|
|
1435
|
+
id: "package-detection",
|
|
1436
|
+
name: "Package Detection",
|
|
1437
|
+
slot: "detector",
|
|
1438
|
+
postprocessor: "yolo",
|
|
1439
|
+
extractMode: "crop-zone",
|
|
1440
|
+
inputClasses: null,
|
|
1441
|
+
outputClasses: ["package"],
|
|
1442
|
+
models: [...PACKAGE_DETECTION_MODELS],
|
|
1443
|
+
defaultModelId: "yolov8n-package",
|
|
1444
|
+
enabledByDefault: false,
|
|
1445
|
+
defaultConfidence: .5,
|
|
1446
|
+
labels: ["package"]
|
|
1447
|
+
});
|
|
1382
1448
|
var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1383
1449
|
id: "bird-classifier",
|
|
1384
1450
|
name: "Bird Classifier",
|
|
@@ -1454,6 +1520,7 @@ var ALL_PIPELINE_STEPS = [
|
|
|
1454
1520
|
STEP_CLIP_EMBEDDING,
|
|
1455
1521
|
STEP_PLATE_DETECTION,
|
|
1456
1522
|
STEP_PLATE_OCR,
|
|
1523
|
+
STEP_PACKAGE_DETECTION,
|
|
1457
1524
|
new AnimalClassifierStep(),
|
|
1458
1525
|
STEP_BIRD_CLASSIFIER,
|
|
1459
1526
|
STEP_VEHICLE_CLASSIFIER,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS, w as hfModelUrl } from "./dist-LkwlodgP.mjs";
|
|
2
2
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
3
3
|
var HF_REPO = "camstack/camstack-models";
|
|
4
4
|
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
@@ -682,6 +682,58 @@ var PLATE_DETECTION_MODELS = [{
|
|
|
682
682
|
openvino: ovFormat(hf("plateDetection/yolov8-plate/openvino/camstack-yolov8n-plate.xml"), 6.1)
|
|
683
683
|
}
|
|
684
684
|
}];
|
|
685
|
+
var PACKAGE_DETECTION_MODELS = [{
|
|
686
|
+
id: "yolov8n-package",
|
|
687
|
+
name: "YOLOv8 Nano — Package",
|
|
688
|
+
description: "YOLOv8 Nano fine-tuned for parcel/package detection (mAP50 0.93) — dedicated model, not the COCO suitcase/backpack proxy",
|
|
689
|
+
inputSize: {
|
|
690
|
+
width: 640,
|
|
691
|
+
height: 640
|
|
692
|
+
},
|
|
693
|
+
labels: [{
|
|
694
|
+
id: "package",
|
|
695
|
+
name: "Package"
|
|
696
|
+
}],
|
|
697
|
+
preprocessMode: "letterbox",
|
|
698
|
+
license: "AGPL-3.0",
|
|
699
|
+
formats: {
|
|
700
|
+
onnx: {
|
|
701
|
+
url: hf("packageDetection/yolov8-package/onnx/camstack-yolov8n-package.onnx"),
|
|
702
|
+
sizeMB: 12
|
|
703
|
+
},
|
|
704
|
+
coreml: {
|
|
705
|
+
url: hf("packageDetection/yolov8-package/coreml/camstack-yolov8n-package.mlpackage"),
|
|
706
|
+
sizeMB: 12,
|
|
707
|
+
isDirectory: true,
|
|
708
|
+
files: [...MLPACKAGE_FILES],
|
|
709
|
+
runtimes: ["python"]
|
|
710
|
+
},
|
|
711
|
+
openvino: ovFormat(hf("packageDetection/yolov8-package/openvino/camstack-yolov8n-package.xml"), 12)
|
|
712
|
+
}
|
|
713
|
+
}, {
|
|
714
|
+
id: "rfdetr-package",
|
|
715
|
+
legacy: true,
|
|
716
|
+
name: "RF-DETR — Package (experimental)",
|
|
717
|
+
description: "RF-DETR parcel detector (onnx + openvino) — DETR-style decode; alternative to yolov8n-package. Requires an rfdetr postprocessor (not yet available)",
|
|
718
|
+
inputSize: {
|
|
719
|
+
width: 576,
|
|
720
|
+
height: 576
|
|
721
|
+
},
|
|
722
|
+
labels: [{
|
|
723
|
+
id: "package",
|
|
724
|
+
name: "Package"
|
|
725
|
+
}],
|
|
726
|
+
preprocessMode: "resize",
|
|
727
|
+
inputNormalization: "imagenet",
|
|
728
|
+
license: "Apache-2.0",
|
|
729
|
+
formats: {
|
|
730
|
+
onnx: {
|
|
731
|
+
url: hf("packageDetection/rfdetr-package/onnx/camstack-rfdetr-package.onnx"),
|
|
732
|
+
sizeMB: 30
|
|
733
|
+
},
|
|
734
|
+
openvino: ovFormat(hf("packageDetection/rfdetr-package/openvino/camstack-rfdetr-package.xml"), 30)
|
|
735
|
+
}
|
|
736
|
+
}];
|
|
685
737
|
var PLATE_OCR_MODELS = [{
|
|
686
738
|
id: "vgg-english-g2",
|
|
687
739
|
name: "VGG English G2",
|
|
@@ -1379,6 +1431,20 @@ var STEP_PLATE_OCR = new PipelineStepBase({
|
|
|
1379
1431
|
maxPerTrack: 5
|
|
1380
1432
|
}
|
|
1381
1433
|
});
|
|
1434
|
+
var STEP_PACKAGE_DETECTION = new PipelineStepBase({
|
|
1435
|
+
id: "package-detection",
|
|
1436
|
+
name: "Package Detection",
|
|
1437
|
+
slot: "detector",
|
|
1438
|
+
postprocessor: "yolo",
|
|
1439
|
+
extractMode: "crop-zone",
|
|
1440
|
+
inputClasses: null,
|
|
1441
|
+
outputClasses: ["package"],
|
|
1442
|
+
models: [...PACKAGE_DETECTION_MODELS],
|
|
1443
|
+
defaultModelId: "yolov8n-package",
|
|
1444
|
+
enabledByDefault: false,
|
|
1445
|
+
defaultConfidence: .5,
|
|
1446
|
+
labels: ["package"]
|
|
1447
|
+
});
|
|
1382
1448
|
var STEP_BIRD_CLASSIFIER = new ClassifierWithMinConfidence({
|
|
1383
1449
|
id: "bird-classifier",
|
|
1384
1450
|
name: "Bird Classifier",
|
|
@@ -1454,6 +1520,7 @@ var ALL_PIPELINE_STEPS = [
|
|
|
1454
1520
|
STEP_CLIP_EMBEDDING,
|
|
1455
1521
|
STEP_PLATE_DETECTION,
|
|
1456
1522
|
STEP_PLATE_OCR,
|
|
1523
|
+
STEP_PACKAGE_DETECTION,
|
|
1457
1524
|
new AnimalClassifierStep(),
|
|
1458
1525
|
STEP_BIRD_CLASSIFIER,
|
|
1459
1526
|
STEP_VEHICLE_CLASSIFIER,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, i as r, l as i, n as a, o, r as s, s as c, t as l, u } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-
|
|
1
|
+
import { a as e, c as t, d as n, i as r, l as i, n as a, o, r as s, s as c, t as l, u } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C4pcysOQ.mjs";
|
|
2
2
|
import { a as d, i as f, n as p, o as m, r as h, t as g } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as _, r as v, t as y } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
4
|
import { n as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
5
|
-
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
5
|
+
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHJB3xUN.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var C = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), w = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), T = (e) => {
|
|
8
8
|
let t = w(e);
|