@camstack/addon-post-analysis 1.2.55 → 1.2.58

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.
@@ -1,7 +1,11 @@
1
- import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-D9Bm8RyD.mjs";
1
+ import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-CI2VspUN.mjs";
2
+ import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
+ import * as fs from "node:fs";
2
4
  import { promises } from "node:fs";
5
+ import * as path$1 from "node:path";
3
6
  import path from "node:path";
4
7
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
8
+ import "node:child_process";
5
9
  import sharp from "sharp";
6
10
  //#region src/notification-center/action-token.ts
7
11
  /**
@@ -721,6 +725,227 @@ var NC_ALARM_DECLARATION = {
721
725
  DeviceClass: NcAlarmPanelDevice
722
726
  };
723
727
  //#endregion
728
+ //#region ../types/dist/node.mjs
729
+ var node_exports = /* @__PURE__ */ __exportAll({
730
+ FilesystemStorageProvider: () => FilesystemStorageProvider,
731
+ PYTHON_VERSION: () => PYTHON_VERSION,
732
+ signExpiringUrl: () => signExpiringUrl,
733
+ verifyExpiringUrl: () => verifyExpiringUrl
734
+ });
735
+ /**
736
+ * Download portable Python (headless) from bjia56/portable-python.
737
+ *
738
+ * Source: https://github.com/bjia56/portable-python
739
+ *
740
+ * Advantages over python-build-standalone:
741
+ * - macOS universal2 binary (arm64+x86_64 in one)
742
+ * - Lighter (~24-28MB headless zip)
743
+ * - glibc 2.17 minimum (better compat)
744
+ * - Headless variant excludes tkinter/UI (ideal for ML inference)
745
+ *
746
+ * The zip's top-level dir is platform-specific (e.g.
747
+ * `python-headless-3.12.12-darwin-universal2/`); after extraction
748
+ * we rename it to a stable `python/` so consumers can rely on
749
+ * `<dataDir>/deps/python/bin/python3`.
750
+ */
751
+ var PYTHON_VERSION = "3.12.12";
752
+ `${PYTHON_VERSION}`;
753
+ var STORAGE_LOCATION_TYPES = [
754
+ "data",
755
+ "media",
756
+ "recordings",
757
+ "recordings-high",
758
+ "recordings-low",
759
+ "recordings-clips",
760
+ "event-images",
761
+ "models",
762
+ "addons-data",
763
+ "cache",
764
+ "logs",
765
+ "backups"
766
+ ];
767
+ var DEFAULT_LOCATION_SUBDIRS = {
768
+ data: "db",
769
+ media: "media",
770
+ recordings: "recordings",
771
+ "recordings-high": "recordings-high",
772
+ "recordings-low": "recordings-low",
773
+ "recordings-clips": "recordings-clips",
774
+ "event-images": "event-images",
775
+ models: "models",
776
+ "addons-data": "addons-data",
777
+ cache: "/tmp/camstack-cache",
778
+ logs: "logs",
779
+ backups: "backups"
780
+ };
781
+ /**
782
+ * Filesystem storage provider — serves all location types from a local directory tree.
783
+ *
784
+ * Default layout:
785
+ * {rootPath}/recordings-high/
786
+ * {rootPath}/recordings-low/
787
+ * {rootPath}/recordings-clips/
788
+ * {rootPath}/event-images/
789
+ * {rootPath}/models/
790
+ * {rootPath}/addons-data/
791
+ * {rootPath}/logs/
792
+ * /tmp/camstack-cache/ (cache is always local)
793
+ *
794
+ * Individual location paths can be overridden.
795
+ */
796
+ var FilesystemStorageProvider = class {
797
+ id = "local";
798
+ name = "Local Filesystem";
799
+ supportedLocations = [...STORAGE_LOCATION_TYPES];
800
+ rootPath;
801
+ locationPaths;
802
+ constructor(rootPath, overrides) {
803
+ this.rootPath = path$1.resolve(rootPath);
804
+ this.locationPaths = /* @__PURE__ */ new Map();
805
+ for (const loc of STORAGE_LOCATION_TYPES) {
806
+ const override = overrides?.[loc];
807
+ if (override) this.locationPaths.set(loc, path$1.resolve(override));
808
+ else {
809
+ const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
810
+ this.locationPaths.set(loc, path$1.isAbsolute(subdir) ? subdir : path$1.join(this.rootPath, subdir));
811
+ }
812
+ }
813
+ for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, path$1.resolve(override));
814
+ }
815
+ async resolve({ location, relativePath }) {
816
+ const base = this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
817
+ return path$1.join(base, relativePath);
818
+ }
819
+ async write({ location, relativePath, data }) {
820
+ const filePath = await this.resolve({
821
+ location,
822
+ relativePath
823
+ });
824
+ await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
825
+ if (Buffer.isBuffer(data)) await fs.promises.writeFile(filePath, data);
826
+ else {
827
+ const writeStream = fs.createWriteStream(filePath);
828
+ await new Promise((resolve, reject) => {
829
+ data.pipe(writeStream);
830
+ writeStream.on("finish", resolve);
831
+ writeStream.on("error", reject);
832
+ });
833
+ }
834
+ }
835
+ async read({ location, relativePath }) {
836
+ return fs.promises.readFile(await this.resolve({
837
+ location,
838
+ relativePath
839
+ }));
840
+ }
841
+ async exists({ location, relativePath }) {
842
+ try {
843
+ await fs.promises.access(await this.resolve({
844
+ location,
845
+ relativePath
846
+ }));
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+ async list({ location, prefix }) {
853
+ const base = this.locationPaths.get(location);
854
+ if (!base) return [];
855
+ const dir = prefix ? path$1.join(base, prefix) : base;
856
+ try {
857
+ return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
858
+ } catch {
859
+ return [];
860
+ }
861
+ }
862
+ async delete({ location, relativePath }) {
863
+ const filePath = await this.resolve({
864
+ location,
865
+ relativePath
866
+ });
867
+ await fs.promises.rm(filePath, { force: true });
868
+ }
869
+ async getAvailableSpace({ location }) {
870
+ const base = this.locationPaths.get(location);
871
+ if (!base) return null;
872
+ try {
873
+ let target = base;
874
+ while (!fs.existsSync(target)) {
875
+ const parent = path$1.dirname(target);
876
+ if (!parent || parent === target) return null;
877
+ target = parent;
878
+ }
879
+ const stats = await fs.promises.statfs(target);
880
+ return stats.bavail * stats.bsize;
881
+ } catch {
882
+ return null;
883
+ }
884
+ }
885
+ /** Get the resolved path for a location type (addon-declared ids fall back
886
+ * to `<rootPath>/<id>`). */
887
+ getLocationPath(location) {
888
+ return this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
889
+ }
890
+ /** Get the root path */
891
+ getRootPath() {
892
+ return this.rootPath;
893
+ }
894
+ };
895
+ /**
896
+ * The ONE derivation of "a signed, expiring URL".
897
+ *
898
+ * This repo mints unguessable, self-expiring links in three places now — the
899
+ * notification artifact plane, the Home Assistant media plane, and the snapshot
900
+ * link plane. The first two grew independently and are byte-identical logic
901
+ * (`hmac(secret, "<id>:<exp>")`, expiry checked before a constant-time compare),
902
+ * each restating the crypto locally because **addons never import each other**.
903
+ *
904
+ * That reason is real, and the conclusion drawn from it was wrong. Two copies of
905
+ * a signing scheme is how one of them quietly ends up with a different TTL, a
906
+ * different compare, or a missing expiry check, and nothing fails until a link
907
+ * that should have died keeps working. The fix is the same one D52 applies to
908
+ * crop geometry: one derivation, in a place every addon may depend on. A
909
+ * framework package is exactly that place — `@camstack/types/node`, off the root
910
+ * entry because `node:crypto` must never be traversed by a browser bundler.
911
+ *
912
+ * What this module deliberately does NOT decide: the TTL, the base URL, the
913
+ * shape of `id`, and the access level of the route. Those are per-plane policy
914
+ * and each caller states them where a reader can see them.
915
+ */
916
+ /**
917
+ * The signature over `(id, expMs)`.
918
+ *
919
+ * `id` is whatever the plane uses to name the thing being served — an artifact
920
+ * id, a track id, a `<deviceId>:<width>` pair. It is joined with `:` so a caller
921
+ * must not put a `:` inside a field whose boundary matters; where a plane has
922
+ * more than one field, it composes them itself and owns that ambiguity.
923
+ */
924
+ function signExpiringUrl(secret, id, expMs) {
925
+ return createHmac("sha256", secret).update(`${id}:${String(expMs)}`).digest("hex");
926
+ }
927
+ /**
928
+ * Verify a request's `(id, exp, sig)`.
929
+ *
930
+ * **Expiry is checked BEFORE the compare**, so an expired link is refused
931
+ * whether or not its signature is valid — a leaked URL stops working on its own
932
+ * and cannot be kept alive by holding a correct signature. The compare itself is
933
+ * constant-time so a public route cannot be probed for the signature byte by
934
+ * byte.
935
+ */
936
+ function verifyExpiringUrl(input) {
937
+ const { secret, id, exp, sig, nowMs } = input;
938
+ if (exp === void 0 || sig === void 0) return false;
939
+ const expMs = typeof exp === "number" ? exp : Number(exp);
940
+ if (!Number.isFinite(expMs)) return false;
941
+ if (expMs <= nowMs) return false;
942
+ const expected = signExpiringUrl(secret, id, expMs);
943
+ const a = Buffer.from(expected, "utf8");
944
+ const b = Buffer.from(sig, "utf8");
945
+ if (a.length !== b.length) return false;
946
+ return timingSafeEqual(a, b);
947
+ }
948
+ //#endregion
724
949
  //#region src/notification-center/artifact-url.ts
725
950
  /**
726
951
  * Signed, externally-reachable URLs for notification artifacts.
@@ -737,7 +962,10 @@ var NC_ALARM_DECLARATION = {
737
962
  * recipient's phone) — it holds no session and no admin token. The route is
738
963
  * therefore `access:'public'` and the authority is the signature: an
739
964
  * unguessable HMAC over `(id, exp)` with a per-install secret, expiring on
740
- * its own. Nothing else about the install is reachable through it.
965
+ * its own. Nothing else about the install is reachable through it. The HMAC
966
+ * is the shared one in `@camstack/types/node` — this module used to restate
967
+ * it, as did the Home Assistant media plane, and two copies of a signing
968
+ * scheme is how one of them quietly loses its expiry check.
741
969
  * - **The base URL is CHOSEN, never assumed.** A notification carrying
742
970
  * `http://127.0.0.1:...` is useless (the Alexa `hubUrl` bug, again). The
743
971
  * ranked endpoint list the `local-network` cap already computes — public
@@ -754,9 +982,17 @@ var NC_ALARM_DECLARATION = {
754
982
  function pickArtifactBaseUrl(endpoints) {
755
983
  return endpoints.filter((e) => e.kind !== "loopback").toSorted((a, b) => a.priority - b.priority)[0]?.baseUrl ?? null;
756
984
  }
757
- /** Sign `(id, exp)` — the exact string the verifier recomputes. */
985
+ /**
986
+ * Sign `(id, exp)` — the exact string the verifier recomputes.
987
+ *
988
+ * Delegates to the one derivation in `@camstack/types/node`. This used to
989
+ * restate the HMAC locally, and so did the Home Assistant media plane, on the
990
+ * correct reasoning that addons never import each other — but two copies of a
991
+ * signing scheme is how one of them ends up with a different expiry check that
992
+ * nothing catches.
993
+ */
758
994
  function signArtifact(secret, id, expMs) {
759
- return createHmac("sha256", secret).update(`${id}:${expMs}`).digest("hex");
995
+ return signExpiringUrl(secret, id, expMs);
760
996
  }
761
997
  /**
762
998
  * Build the fully-qualified, signed URL for an artifact.
@@ -773,12 +1009,7 @@ function buildArtifactUrl(input) {
773
1009
  * the compare so an expired link is rejected even with a valid signature.
774
1010
  */
775
1011
  function verifyArtifactSignature(input) {
776
- if (!Number.isFinite(input.exp) || input.exp <= input.nowMs) return false;
777
- const expected = signArtifact(input.secret, input.id, input.exp);
778
- const a = Buffer.from(expected, "utf8");
779
- const b = Buffer.from(input.sig, "utf8");
780
- if (a.length !== b.length) return false;
781
- return timingSafeEqual(a, b);
1012
+ return verifyExpiringUrl(input);
782
1013
  }
783
1014
  //#endregion
784
1015
  //#region src/notification-center/artifact-plane.ts
@@ -12300,26 +12531,6 @@ function wideCentralSquareLayout(bbox, frame) {
12300
12531
  frameOriginX
12301
12532
  };
12302
12533
  }
12303
- //#endregion
12304
- //#region src/shared/frame/subject-crop-variants.ts
12305
- /**
12306
- * Best-shot subject-crop variants (best-crop fast-load, 2026-07-21; 16:9
12307
- * central-square reframe, 2026-07-22).
12308
- *
12309
- * ONE native fetch (the 16:9 central-square WINDOW — see
12310
- * {@link wide-central-square-crop}) backs both persisted variants:
12311
- * - `thumbnail` — the native uncapped 16:9 window JPEG (subject in the
12312
- * central square, lateral scene context, never upscaled).
12313
- * - `thumbnailSmall` — the SAME window downscaled to {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH}
12314
- * long side (854×480), the reel / lists / grid fast-load
12315
- * representative.
12316
- *
12317
- * The small variant is DERIVED from the already-composed native window — never a
12318
- * second native round-trip and NEVER upscaled: if the window is already ≤ 854 on
12319
- * its long side it is stored AS-IS (byte-identical). The window is composed from
12320
- * the in-frame slab plus lateral letterbox padding for the (rare) part of the
12321
- * 16:9 window that falls outside the frame at a horizontal edge.
12322
- */
12323
12534
  /** Gaussian sigma for the blurred-scene letterbox fill. */
12324
12535
  var LETTERBOX_BLUR_SIGMA = 25;
12325
12536
  /** Brightness factor for the blurred fill — dimmed so the real slab reads as
@@ -12365,15 +12576,12 @@ async function composeWideCentralSquareThumbnail(slab, layout) {
12365
12576
  }
12366
12577
  /**
12367
12578
  * Derive the `thumbnailSmall` JPEG from the composed native 16:9 window.
12368
- * Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (854) on the long
12369
- * side (aspect preserved → ~854×480). If the window's long side is already ≤ the
12370
- * cap, the ORIGINAL buffer is returned unchanged (store as-is, never upscale).
12579
+ * Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (480) on the long
12580
+ * side (aspect preserved → 480×270) and re-encodes at representative quality.
12581
+ * `withoutEnlargement` preserves smaller sources.
12371
12582
  */
12372
12583
  async function deriveThumbnailSmall(nativeWideJpeg) {
12373
- const meta = await sharp(nativeWideJpeg).metadata();
12374
- const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
12375
- if (longSide > 0 && longSide <= 854) return nativeWideJpeg;
12376
- return sharp(nativeWideJpeg).resize(854, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
12584
+ return sharp(nativeWideJpeg).resize(480, null, { withoutEnlargement: true }).jpeg({ quality: 75 }).toBuffer();
12377
12585
  }
12378
12586
  //#endregion
12379
12587
  //#region src/shared/frame/keyframe-wide-thumbnail.ts
@@ -14627,6 +14835,7 @@ function createEventMediaHandler(deps) {
14627
14835
  }
14628
14836
  const variant = parseEventMediaVariant(query);
14629
14837
  const preferKind = parseEventMediaKind(query);
14838
+ const resolveStartedAt = performance.now();
14630
14839
  let media = null;
14631
14840
  try {
14632
14841
  media = await deps.getMedia(eventId, variant ?? void 0, preferKind ?? void 0);
@@ -14634,21 +14843,24 @@ function createEventMediaHandler(deps) {
14634
14843
  const body = "Internal server error";
14635
14844
  res.writeHead(500, {
14636
14845
  "content-type": "text/plain",
14637
- "content-length": String(Buffer.byteLength(body))
14846
+ "content-length": String(Buffer.byteLength(body)),
14847
+ "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`
14638
14848
  });
14639
14849
  if (req.method !== "HEAD") res.end(body);
14640
14850
  else res.end();
14641
14851
  return;
14642
14852
  }
14643
14853
  if (media === null) {
14644
- res.writeHead(404).end();
14854
+ res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
14645
14855
  return;
14646
14856
  }
14647
14857
  const etag = "\"" + media.key + "\"";
14858
+ const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
14648
14859
  if (req.headers["if-none-match"] === etag) {
14649
14860
  res.writeHead(304, {
14650
14861
  etag,
14651
- "cache-control": CACHE_CONTROL
14862
+ "cache-control": CACHE_CONTROL,
14863
+ "server-timing": serverTiming
14652
14864
  }).end();
14653
14865
  return;
14654
14866
  }
@@ -14656,7 +14868,8 @@ function createEventMediaHandler(deps) {
14656
14868
  "content-type": "image/jpeg",
14657
14869
  "cache-control": CACHE_CONTROL,
14658
14870
  etag,
14659
- "content-length": String(media.bytes.byteLength)
14871
+ "content-length": String(media.bytes.byteLength),
14872
+ "server-timing": serverTiming
14660
14873
  });
14661
14874
  if (req.method === "HEAD") res.end();
14662
14875
  else res.end(Buffer.from(media.bytes));
@@ -27383,9 +27596,9 @@ var EventMediaDispatcher = class {
27383
27596
  * the runner's retained native surface with NO `maxWidth` (uncapped TRUE
27384
27597
  * native). It is composed into the full 16:9 window (lateral letterbox only for
27385
27598
  * the part outside the frame) → the `thumbnail`. The `thumbnailSmall` is
27386
- * DERIVED by downscaling that SAME window to the 854 long-side cap (854×480) —
27599
+ * DERIVED by downscaling that SAME window to the 480 long-side cap (480×270) —
27387
27600
  * never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
27388
- * returns the window as-is when it is already ≤ 854).
27601
+ * never upscales a smaller window).
27389
27602
  *
27390
27603
  * Both variants share the SAME framing + central-square containment guarantee:
27391
27604
  * wide consumers use the 16:9 image as-is (subject centered, no cut); square
@@ -30644,6 +30857,7 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
30644
30857
  var CLEAN_MEDIA_KINDS = [
30645
30858
  "crop",
30646
30859
  "thumbnail",
30860
+ "thumbnailSmall",
30647
30861
  "fullFrame",
30648
30862
  "keyFrame",
30649
30863
  "keyFrameSmall"
@@ -31495,7 +31709,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31495
31709
  let storage = this.ctx.kernel.storage;
31496
31710
  const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
31497
31711
  if (mediaRoot) {
31498
- const { FilesystemStorageProvider } = await import("../node-Co0YpEFr.mjs");
31712
+ const { FilesystemStorageProvider } = await Promise.resolve().then(() => node_exports);
31499
31713
  storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
31500
31714
  logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
31501
31715
  }
@@ -35982,6 +36196,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
35982
36196
  * failure the full blob is served (a thumb must never 500 / blank a tile).
35983
36197
  */
35984
36198
  async applyThumbVariant(media, variant) {
36199
+ if (media.key.includes(":thumbnailSmall") && media.bytes.byteLength <= 64 * 1024) return media;
35985
36200
  try {
35986
36201
  return {
35987
36202
  bytes: await makeSquareThumb(media.bytes, variant.size),
@@ -36000,7 +36215,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
36000
36215
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
36001
36216
  if (preferKind !== void 0 && preferKind.length > 0) {
36002
36217
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
36003
- const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
36218
+ const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
36219
+ const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
36220
+ const chosen = resolveCropForcedMedia([
36221
+ ...eventFiles,
36222
+ ...trackFiles,
36223
+ ...owningTrackFiles
36224
+ ], (files) => pickCleanMedia(files, preferKind));
36004
36225
  if (!chosen) return null;
36005
36226
  return {
36006
36227
  bytes: Buffer.from(chosen.base64, "base64"),
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BuCI6peK.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CUxRQmzz.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.55",
3
+ "version": "1.2.58",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -1,148 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path$1 from "node:path";
3
- import "node:crypto";
4
- import "node:child_process";
5
- var STORAGE_LOCATION_TYPES = [
6
- "data",
7
- "media",
8
- "recordings",
9
- "recordings-high",
10
- "recordings-low",
11
- "recordings-clips",
12
- "event-images",
13
- "models",
14
- "addons-data",
15
- "cache",
16
- "logs",
17
- "backups"
18
- ];
19
- var DEFAULT_LOCATION_SUBDIRS = {
20
- data: "db",
21
- media: "media",
22
- recordings: "recordings",
23
- "recordings-high": "recordings-high",
24
- "recordings-low": "recordings-low",
25
- "recordings-clips": "recordings-clips",
26
- "event-images": "event-images",
27
- models: "models",
28
- "addons-data": "addons-data",
29
- cache: "/tmp/camstack-cache",
30
- logs: "logs",
31
- backups: "backups"
32
- };
33
- /**
34
- * Filesystem storage provider — serves all location types from a local directory tree.
35
- *
36
- * Default layout:
37
- * {rootPath}/recordings-high/
38
- * {rootPath}/recordings-low/
39
- * {rootPath}/recordings-clips/
40
- * {rootPath}/event-images/
41
- * {rootPath}/models/
42
- * {rootPath}/addons-data/
43
- * {rootPath}/logs/
44
- * /tmp/camstack-cache/ (cache is always local)
45
- *
46
- * Individual location paths can be overridden.
47
- */
48
- var FilesystemStorageProvider = class {
49
- id = "local";
50
- name = "Local Filesystem";
51
- supportedLocations = [...STORAGE_LOCATION_TYPES];
52
- rootPath;
53
- locationPaths;
54
- constructor(rootPath, overrides) {
55
- this.rootPath = path$1.resolve(rootPath);
56
- this.locationPaths = /* @__PURE__ */ new Map();
57
- for (const loc of STORAGE_LOCATION_TYPES) {
58
- const override = overrides?.[loc];
59
- if (override) this.locationPaths.set(loc, path$1.resolve(override));
60
- else {
61
- const subdir = DEFAULT_LOCATION_SUBDIRS[loc] ?? loc;
62
- this.locationPaths.set(loc, path$1.isAbsolute(subdir) ? subdir : path$1.join(this.rootPath, subdir));
63
- }
64
- }
65
- for (const [loc, override] of Object.entries(overrides ?? {})) if (typeof override === "string" && !this.locationPaths.has(loc)) this.locationPaths.set(loc, path$1.resolve(override));
66
- }
67
- async resolve({ location, relativePath }) {
68
- const base = this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
69
- return path$1.join(base, relativePath);
70
- }
71
- async write({ location, relativePath, data }) {
72
- const filePath = await this.resolve({
73
- location,
74
- relativePath
75
- });
76
- await fs.promises.mkdir(path$1.dirname(filePath), { recursive: true });
77
- if (Buffer.isBuffer(data)) await fs.promises.writeFile(filePath, data);
78
- else {
79
- const writeStream = fs.createWriteStream(filePath);
80
- await new Promise((resolve, reject) => {
81
- data.pipe(writeStream);
82
- writeStream.on("finish", resolve);
83
- writeStream.on("error", reject);
84
- });
85
- }
86
- }
87
- async read({ location, relativePath }) {
88
- return fs.promises.readFile(await this.resolve({
89
- location,
90
- relativePath
91
- }));
92
- }
93
- async exists({ location, relativePath }) {
94
- try {
95
- await fs.promises.access(await this.resolve({
96
- location,
97
- relativePath
98
- }));
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
- async list({ location, prefix }) {
105
- const base = this.locationPaths.get(location);
106
- if (!base) return [];
107
- const dir = prefix ? path$1.join(base, prefix) : base;
108
- try {
109
- return (await fs.promises.readdir(dir, { withFileTypes: true })).map((e) => prefix ? `${prefix}/${e.name}` : e.name);
110
- } catch {
111
- return [];
112
- }
113
- }
114
- async delete({ location, relativePath }) {
115
- const filePath = await this.resolve({
116
- location,
117
- relativePath
118
- });
119
- await fs.promises.rm(filePath, { force: true });
120
- }
121
- async getAvailableSpace({ location }) {
122
- const base = this.locationPaths.get(location);
123
- if (!base) return null;
124
- try {
125
- let target = base;
126
- while (!fs.existsSync(target)) {
127
- const parent = path$1.dirname(target);
128
- if (!parent || parent === target) return null;
129
- target = parent;
130
- }
131
- const stats = await fs.promises.statfs(target);
132
- return stats.bavail * stats.bsize;
133
- } catch {
134
- return null;
135
- }
136
- }
137
- /** Get the resolved path for a location type (addon-declared ids fall back
138
- * to `<rootPath>/<id>`). */
139
- getLocationPath(location) {
140
- return this.locationPaths.get(location) ?? path$1.join(this.rootPath, location);
141
- }
142
- /** Get the root path */
143
- getRootPath() {
144
- return this.rootPath;
145
- }
146
- };
147
- //#endregion
148
- export { FilesystemStorageProvider };