@camstack/addon-post-analysis 1.2.272 → 1.2.273

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BkBZcyAK.js");
5
+ const require_dist = require("../dist-m5QdWXqR.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -47714,391 +47714,6 @@ async function retentionScopeDeviceIds(deps, onError) {
47714
47714
  return [...ids];
47715
47715
  }
47716
47716
  //#endregion
47717
- //#region src/pipeline-analytics/videoclip-visits.ts
47718
- /**
47719
- * Videoclip visits — the grain a clip should have on an events-mode camera.
47720
- *
47721
- * Disk writes one fMP4 per ffmpeg roll (~8–12 s). The recorder calendar merges
47722
- * only gaps ≤ 5 s, so a single pantry walk shows up as several availability
47723
- * ranges with ~8 s GOP holes between them. Analytics then mints one 15 s clip
47724
- * per motion heartbeat. A *visit* is the keep-window those fragments belong
47725
- * to: cluster availability with {@link VISIT_MERGE_GAP_MS}, annotate with
47726
- * overlapping events. No second copy of the media; the files stay the source
47727
- * of truth.
47728
- */
47729
- /** Synthetic event-id slot in `analytics:<start>:<end>:<id>` for a visit clip. */
47730
- var VISIT_EVENT_ID = "visit";
47731
- function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
47732
- if (ranges.length === 0) return [];
47733
- const sorted = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
47734
- const visits = [];
47735
- let pieces = [sorted[0]];
47736
- let endMs = sorted[0].endMs;
47737
- for (let i = 1; i < sorted.length; i++) {
47738
- const next = sorted[i];
47739
- if (next.startMs <= endMs + gapMs) {
47740
- pieces.push(next);
47741
- endMs = Math.max(endMs, next.endMs);
47742
- continue;
47743
- }
47744
- visits.push(finishVisit(pieces, endMs));
47745
- pieces = [next];
47746
- endMs = next.endMs;
47747
- }
47748
- visits.push(finishVisit(pieces, endMs));
47749
- return visits;
47750
- }
47751
- function finishVisit(pieces, endMs) {
47752
- const holes = [];
47753
- for (let i = 1; i < pieces.length; i++) {
47754
- const prevEnd = pieces[i - 1].endMs;
47755
- const nextStart = pieces[i].startMs;
47756
- if (nextStart > prevEnd) holes.push({
47757
- startMs: prevEnd,
47758
- endMs: nextStart
47759
- });
47760
- }
47761
- return {
47762
- startMs: pieces[0].startMs,
47763
- endMs,
47764
- ranges: pieces,
47765
- holes
47766
- };
47767
- }
47768
- var KIND_ORDER = [
47769
- "object",
47770
- "motion",
47771
- "audio"
47772
- ];
47773
- function eventOverlapsVisit(event, visit, pad) {
47774
- if (event.timestamp >= visit.startMs && event.timestamp <= visit.endMs) return true;
47775
- const windowStart = event.timestamp - pad.preMs;
47776
- const windowEnd = event.timestamp + pad.postMs;
47777
- return windowStart < visit.endMs && windowEnd > visit.startMs;
47778
- }
47779
- function dominantKind(attached) {
47780
- for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
47781
- return "motion";
47782
- }
47783
- /**
47784
- * The instant a recorded still can actually be decoded inside a visit: the
47785
- * midpoint of its LONGEST contiguous range.
47786
- *
47787
- * NOT the midpoint of the visit. A visit spans its holes by construction (it is
47788
- * ranges glued across gaps up to `VISIT_MERGE_GAP_MS`), so the naive midpoint
47789
- * lands in dead air roughly as often as not and the recorder's still route
47790
- * answers 204 for a visit with plenty of footage.
47791
- */
47792
- function stillInstantMs(ranges) {
47793
- let best;
47794
- for (const range of ranges) if (best === void 0 || range.endMs - range.startMs > best.endMs - best.startMs) best = range;
47795
- if (best === void 0) return void 0;
47796
- return Math.floor((best.startMs + best.endMs) / 2);
47797
- }
47798
- /**
47799
- * The distinct object classes attached to a visit, most frequent first,
47800
- * capped at {@link MAX_CLIP_LABELS}. Empty when nothing classified overlapped
47801
- * — the caller then omits the field so a motion/audio-only visit keeps its
47802
- * kind label instead of carrying an empty array every consumer length-checks.
47803
- */
47804
- function dominantLabels(attached) {
47805
- const counts = /* @__PURE__ */ new Map();
47806
- for (const a of attached) {
47807
- if (a.kind !== "object") continue;
47808
- const name = a.event.className;
47809
- if (typeof name !== "string" || name.length === 0) continue;
47810
- counts.set(name, (counts.get(name) ?? 0) + 1);
47811
- }
47812
- return [...counts.entries()].toSorted((x, y) => y[1] - x[1]).slice(0, 3).map(([name]) => name);
47813
- }
47814
- function visitsToClips(visits, events, pad, vouched) {
47815
- const tagged = [
47816
- ...events.object.map((event) => ({
47817
- kind: "object",
47818
- event
47819
- })),
47820
- ...events.motion.map((event) => ({
47821
- kind: "motion",
47822
- event
47823
- })),
47824
- ...events.audio.map((event) => ({
47825
- kind: "audio",
47826
- event
47827
- }))
47828
- ];
47829
- return visits.map((visit) => {
47830
- const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
47831
- const kind = dominantKind(attached);
47832
- const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
47833
- const stillAtMs = stillInstantMs(visit.ranges);
47834
- const labels = dominantLabels(attached);
47835
- return {
47836
- id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
47837
- source: "analytics",
47838
- kind,
47839
- timeRange: {
47840
- startMs: visit.startMs,
47841
- endMs: visit.endMs
47842
- },
47843
- ...withThumb?.event.mediaUrl !== void 0 ? { thumbnail: withThumb.event.mediaUrl } : {},
47844
- ...stillAtMs !== void 0 ? { stillAtMs } : {},
47845
- eventIds: attached.slice(0, 24).map((a) => a.event.id),
47846
- eventCount: attached.length,
47847
- holes: [...visit.holes],
47848
- ...labels.length > 0 ? { labels } : {}
47849
- };
47850
- }).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
47851
- }
47852
- //#endregion
47853
- //#region src/pipeline-analytics/videoclips-provider.ts
47854
- /**
47855
- * Default `videoclips` provider — composes recorded footage visits with the
47856
- * analytics event log. A clip is a time-WINDOW over existing footage, never a
47857
- * separate file: `getClipPlayback` resolves to
47858
- * `recording.getPlaybackManifest(startMs, endMs)`.
47859
- *
47860
- * When availability is present the grain is a *visit* (events-mode keep-window
47861
- * clustered at `VISIT_MERGE_GAP_MS`), not one clip per motion heartbeat. The
47862
- * clip id still encodes the window (`analytics:<startMs>:<endMs>:visit`) so
47863
- * playback is self-contained. Without availability (or when that read fails)
47864
- * the provider falls back to per-event padded windows.
47865
- *
47866
- * Pure helpers (`eventsToClips` / `clusterFootageVisits` / `clipIdFor` /
47867
- * `parseClipId`) are unit-tested in isolation; the provider factory wires
47868
- * injected fetch/resolve deps so the compose logic is testable without I/O.
47869
- */
47870
- var SOURCE = "analytics";
47871
- /** The addon that registers this provider — stamped on its source row. */
47872
- var ANALYTICS_ADDON_ID = "pipeline-analytics";
47873
- /**
47874
- * Backstop on the ANNOTATION read — never on the clip list.
47875
- *
47876
- * A visit is annotated by every event inside it, so the clip `limit` must not
47877
- * reach this fetch (one 615 visit legitimately spans 2 345 motion rows). This
47878
- * bound exists only so a pathological camera-day cannot pull an unbounded page:
47879
- * the store orders `timestamp DESC`, so a truncation drops the OLDEST rows and
47880
- * the EARLIEST visits silently lose their annotations — which is why hitting it
47881
- * is logged, per camera, rather than absorbed.
47882
- */
47883
- var VISIT_EVENT_FETCH_LIMIT = 5e3;
47884
- function clipIdFor(eventId, startMs, endMs) {
47885
- return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
47886
- }
47887
- /** Parse a default-provider clip id back into its window + event id. Returns
47888
- * `null` for any id this provider did not mint (e.g. a native clip id). */
47889
- function parseClipId(clipId) {
47890
- const parts = clipId.split(":");
47891
- if (parts.length < 4 || parts[0] !== SOURCE) return null;
47892
- const startMs = Number(parts[1]);
47893
- const endMs = Number(parts[2]);
47894
- if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return null;
47895
- const eventId = parts.slice(3).join(":");
47896
- if (!eventId) return null;
47897
- return {
47898
- eventId,
47899
- startMs,
47900
- endMs
47901
- };
47902
- }
47903
- /** Map per-kind events into time-ranged clips, newest-first. Pure. */
47904
- function eventsToClips(events, pad, vouched) {
47905
- const make = (e, kind) => {
47906
- const startMs = e.timestamp - pad.preMs;
47907
- const endMs = e.timestamp + pad.postMs;
47908
- return {
47909
- id: clipIdFor(e.id, startMs, endMs),
47910
- source: SOURCE,
47911
- kind,
47912
- timeRange: {
47913
- startMs,
47914
- endMs
47915
- },
47916
- ...e.mediaUrl !== void 0 && vouched.has(e.id) ? { thumbnail: e.mediaUrl } : {}
47917
- };
47918
- };
47919
- return [
47920
- ...events.motion.map((e) => make(e, "motion")),
47921
- ...events.object.map((e) => make(e, "object")),
47922
- ...events.audio.map((e) => make(e, "audio"))
47923
- ].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
47924
- }
47925
- /** Every id in the three buckets, in the order the composers tag them. */
47926
- function allEventIds(events) {
47927
- return [
47928
- ...events.motion,
47929
- ...events.object,
47930
- ...events.audio
47931
- ].map((e) => e.id);
47932
- }
47933
- /**
47934
- * Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
47935
- * `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
47936
- * owning TRACK's. Up to four batched index reads — one PER EVENT KIND that has
47937
- * any ids (D476: `motion` / `object` / `audio` are separate owner kinds now,
47938
- * so a single combined read can no longer ask "does any of these ids own
47939
- * media" — a motion id and an object id can be the same number post-D474 and
47940
- * are answered by different rows) — plus the track fallback, only over the
47941
- * object events the first pass did not already answer for (motion and audio
47942
- * are trackless roots, so they never need it).
47943
- *
47944
- * THROWS on a failed read. The caller fails closed on the CLAIM and open on the
47945
- * LIST; swallowing here would collapse those two opposite directions into one.
47946
- */
47947
- async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
47948
- const ownMedia = /* @__PURE__ */ new Set();
47949
- for (const ownerType of require_dist.EVENT_OWNER_TYPES) {
47950
- const ownerIds = events[ownerType].map((e) => e.id);
47951
- if (ownerIds.length === 0) continue;
47952
- const found = await fetchMediaOwners({
47953
- deviceId,
47954
- ownerKind: ownerType,
47955
- ownerIds
47956
- });
47957
- for (const id of found) ownMedia.add(id);
47958
- }
47959
- const residualTracks = [];
47960
- const seenTracks = /* @__PURE__ */ new Set();
47961
- for (const event of events.object) {
47962
- const trackId = event.trackId;
47963
- if (trackId === void 0 || ownMedia.has(event.id) || seenTracks.has(trackId)) continue;
47964
- seenTracks.add(trackId);
47965
- residualTracks.push(trackId);
47966
- }
47967
- const trackMedia = residualTracks.length === 0 ? /* @__PURE__ */ new Set() : await fetchMediaOwners({
47968
- deviceId,
47969
- ownerKind: "track",
47970
- ownerIds: residualTracks
47971
- });
47972
- const vouched = new Set(ownMedia);
47973
- for (const event of events.object) {
47974
- const trackId = event.trackId;
47975
- if (trackId !== void 0 && trackMedia.has(trackId)) vouched.add(event.id);
47976
- }
47977
- return vouched;
47978
- }
47979
- /** Keep only clips that actually touch the caller's window. The reads are
47980
- * widened by {@link leftOverhangMs}; the ANSWER is not. */
47981
- function intersectsWindow(clip, since, until) {
47982
- return clip.timeRange.startMs < until && clip.timeRange.endMs > since;
47983
- }
47984
- /**
47985
- * How far BEFORE `since` the event + availability reads must reach.
47986
- *
47987
- * A visit straddling `since` keeps its heartbeat rows and loses the transition
47988
- * row — the only one that ever carries a snapshot — so the same visit shows a
47989
- * picture or doesn't depending on where the operator scrolled. One wider range
47990
- * scan on an indexed column buys a list that stops changing under the cursor.
47991
- */
47992
- function leftOverhangMs(pad) {
47993
- return require_dist.VISIT_MERGE_GAP_MS + pad.preMs;
47994
- }
47995
- function createVideoclipsProvider(deps) {
47996
- return {
47997
- listClips: async ({ deviceId, since, until, limit }) => {
47998
- const readSince = since - leftOverhangMs(deps.defaultPad);
47999
- const events = await deps.fetchEvents({
48000
- deviceId,
48001
- since: readSince,
48002
- until,
48003
- limit: deps.fetchAvailability ? VISIT_EVENT_FETCH_LIMIT : limit
48004
- });
48005
- warnOnTruncatedFetch(deps, deviceId, events);
48006
- let vouched = /* @__PURE__ */ new Set();
48007
- try {
48008
- vouched = await resolveVouchedEvents(deviceId, events, deps.fetchMediaOwners);
48009
- } catch (err) {
48010
- deps.logWarn?.("videoclips: media presence read failed — listing without thumbnails", {
48011
- deviceId,
48012
- events: allEventIds(events).length,
48013
- error: err instanceof Error ? err.message : String(err)
48014
- });
48015
- }
48016
- let clips = eventsToClips(events, deps.defaultPad, vouched);
48017
- if (deps.fetchAvailability) try {
48018
- clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
48019
- deviceId,
48020
- since: readSince,
48021
- until
48022
- }), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
48023
- } catch (err) {
48024
- deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
48025
- deviceId,
48026
- error: err instanceof Error ? err.message : String(err)
48027
- });
48028
- }
48029
- const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
48030
- const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
48031
- reportUnvouchedClips(deps, deviceId, answer);
48032
- return answer;
48033
- },
48034
- /**
48035
- * The one source this provider serves, on every camera: the hub's own
48036
- * analytics visits over recorded footage.
48037
- *
48038
- * Always `ok`, and that is not optimism — this source IS the process
48039
- * answering the call, so there is no reachability question to report. A
48040
- * read that fails INSIDE `listClips` degrades that list (and logs it); a
48041
- * source that cannot be reached at all is reported by the collection
48042
- * dispatcher, which is the only layer that can tell the difference.
48043
- * `label` is what the picker draws.
48044
- */
48045
- listSources: async () => [{
48046
- source: SOURCE,
48047
- addonId: ANALYTICS_ADDON_ID,
48048
- label: "CamStack recordings",
48049
- availability: { state: "ok" }
48050
- }],
48051
- getClipPlayback: async ({ deviceId, clipId }) => {
48052
- const parsed = parseClipId(clipId);
48053
- if (!parsed) throw new Error(`videoclips: unrecognized clipId "${clipId}"`);
48054
- return {
48055
- ...await deps.resolvePlayback({
48056
- deviceId,
48057
- startMs: parsed.startMs,
48058
- endMs: parsed.endMs
48059
- }),
48060
- format: "hls"
48061
- };
48062
- }
48063
- };
48064
- }
48065
- /** The annotation backstop biting is a SILENT loss of the earliest visits'
48066
- * events — say so, per camera, instead of shipping a thinner list. */
48067
- function warnOnTruncatedFetch(deps, deviceId, events) {
48068
- if (!deps.fetchAvailability) return;
48069
- const truncated = [
48070
- "motion",
48071
- "object",
48072
- "audio"
48073
- ].filter((kind) => events[kind].length >= VISIT_EVENT_FETCH_LIMIT);
48074
- if (truncated.length === 0) return;
48075
- deps.logWarn?.("videoclips: annotation fetch truncated — earliest visits lose their events", {
48076
- deviceId,
48077
- kinds: truncated,
48078
- limit: VISIT_EVENT_FETCH_LIMIT
48079
- });
48080
- }
48081
- /** The operator's acceptance criterion as a number: a rising count on one
48082
- * camera is its motion-snapshot capturer failing, not a quiet day. */
48083
- function reportUnvouchedClips(deps, deviceId, clips) {
48084
- const clipsWithoutStill = clips.filter((c) => c.thumbnail === void 0).length;
48085
- if (clipsWithoutStill === 0) return;
48086
- deps.logInfo?.("videoclips: visit has no vouched still", {
48087
- deviceId,
48088
- clipsWithoutStill,
48089
- clips: clips.length
48090
- });
48091
- }
48092
- //#endregion
48093
- //#region src/pipeline-analytics/videoclips-shared.ts
48094
- /** Stringify the numeric event ids of one bucket. Lossless. */
48095
- function toEventLike(events) {
48096
- return events.map((e) => ({
48097
- ...e,
48098
- id: String(e.id)
48099
- }));
48100
- }
48101
- //#endregion
48102
47717
  //#region src/pipeline-analytics/viewer-settings-actions.ts
48103
47718
  /**
48104
47719
  * Viewer settings snapshots — hub-shared named blobs of the viewer's stores.
@@ -78648,7 +78263,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78648
78263
  await step("buildSceneMonitor", () => this.buildSceneMonitor(api, logger));
78649
78264
  this.startSweepTimers();
78650
78265
  this.ctx.logger.info("pipeline-analytics subscribers installed");
78651
- const providers = await step("buildProviderRegistrations", () => this.buildProviderRegistrations(api, stores, capProviders));
78266
+ const providers = await step("buildProviderRegistrations", () => this.buildProviderRegistrations(stores, capProviders));
78652
78267
  const isHub = ownNodeId === "hub";
78653
78268
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
78654
78269
  this.discardJob = createDiscardJob({
@@ -81052,71 +80667,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
81052
80667
  });
81053
80668
  }
81054
80669
  }
81055
- buildProviderRegistrations(api, stores, providers) {
80670
+ buildProviderRegistrations(stores, providers) {
81056
80671
  const widgetsProvider = createAnalyticsWidgetsProvider();
81057
- const videoclipsProvider = createVideoclipsProvider({
81058
- defaultPad: require_dist.EVENT_PAD_MS,
81059
- fetchEvents: async ({ deviceId, since, until, limit }) => {
81060
- const [motion, object, audio] = await Promise.all([
81061
- this.getMotionEvents({
81062
- deviceId,
81063
- since,
81064
- until,
81065
- limit,
81066
- projection: "slim"
81067
- }),
81068
- this.getObjectEvents({
81069
- deviceId,
81070
- since,
81071
- until,
81072
- limit,
81073
- projection: "slim"
81074
- }),
81075
- this.getAudioEvents({
81076
- deviceId,
81077
- since,
81078
- until,
81079
- limit,
81080
- projection: "slim"
81081
- })
81082
- ]);
81083
- return {
81084
- motion: toEventLike(motion),
81085
- object: toEventLike(object),
81086
- audio: toEventLike(audio)
81087
- };
81088
- },
81089
- fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
81090
- return stores.mediaStore.ownersWithMedia(ownerKind, ownerIds, deviceId);
81091
- },
81092
- fetchAvailability: async ({ deviceId, since, until }) => {
81093
- return (await api.recording.getAvailability.query({
81094
- deviceId,
81095
- fromMs: since,
81096
- toMs: until
81097
- })).ranges;
81098
- },
81099
- logWarn: (message, meta) => this.ctx.logger.warn(message, {
81100
- tags: { deviceId: Number(meta["deviceId"]) },
81101
- meta
81102
- }),
81103
- logInfo: (message, meta) => this.ctx.logger.info(message, {
81104
- tags: { deviceId: Number(meta["deviceId"]) },
81105
- meta
81106
- }),
81107
- resolvePlayback: async ({ deviceId, startMs, endMs }) => {
81108
- const manifest = await api.recording.getPlaybackManifest.query({
81109
- deviceId,
81110
- fromMs: startMs,
81111
- toMs: endMs
81112
- });
81113
- if (manifest.playbackUrl == null) throw new Error(`videoclips: no footage for clip window [${startMs},${endMs}] on device ${deviceId}`);
81114
- return {
81115
- playbackUrl: manifest.playbackUrl,
81116
- playbackEndpoints: manifest.playbackEndpoints
81117
- };
81118
- }
81119
- });
81120
80672
  const faceGallery = new FaceGalleryProvider({
81121
80673
  identityStore: stores.identityStore,
81122
80674
  faceStore: stores.faceStore,
@@ -81179,10 +80731,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
81179
80731
  capability: require_dist.addonWidgetsSourceCapability,
81180
80732
  provider: widgetsProvider
81181
80733
  },
81182
- {
81183
- capability: require_dist.videoclipsCapability,
81184
- provider: videoclipsProvider
81185
- },
81186
80734
  {
81187
80735
  capability: require_dist.faceGalleryCapability,
81188
80736
  provider: faceGallery
@@ -85835,6 +85383,24 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
85835
85383
  return this.queryFacade.listEventMedia(input);
85836
85384
  }
85837
85385
  /**
85386
+ * Which of `ownerIds` hold at least ONE media row — the media-PRESENCE probe,
85387
+ * on the wire because the `videoclips` source lives in the addon that owns
85388
+ * the FOOTAGE now (D560).
85389
+ *
85390
+ * A pass-through to {@link MediaStore.ownersWithMedia}, which batches by
85391
+ * `whereIn` and pages: the N+1 this exists to prevent is one query per owner,
85392
+ * and a process boundary in front of it would multiply that, not absorb it.
85393
+ *
85394
+ * It THROWS rather than answering partially, and the empty ask costs no read
85395
+ * at all — a caller with nothing to probe must not pay a round trip for it.
85396
+ */
85397
+ async ownersWithMedia(input) {
85398
+ if (input.ownerIds.length === 0) return [];
85399
+ const store = this.mediaStore;
85400
+ if (store === null) throw new Error(`pipelineAnalytics.ownersWithMedia: the media index is not open on this node — refusing rather than answering "these owners have nothing" (device ${String(input.deviceId)})`);
85401
+ return [...await store.ownersWithMedia(input.ownerKind, input.ownerIds, input.deviceId)];
85402
+ }
85403
+ /**
85838
85404
  * Search object events by text using CLIP cosine similarity.
85839
85405
  *
85840
85406
  * Flow: