@camstack/addon-benchmark 1.2.149 → 1.2.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/MaskShapeCanvas-IMozTYd-.mjs +13806 -0
  2. package/dist/MotionZonesSettings-ZyajfPtv.mjs +310 -0
  3. package/dist/{PrivacyMaskSettings-BOCn8040.mjs → PrivacyMaskSettings-Cg8NOBlj.mjs} +9 -9
  4. package/dist/{SceneMonitorEditor-BbuSg18h.mjs → SceneMonitorEditor-DWlUo1ZM.mjs} +269 -269
  5. package/dist/_stub.js +15741 -28176
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-CHcffIb5.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-Ca2co8UF.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BE5yZLei.mjs +26 -0
  8. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-wTD-CVC7.mjs +25 -0
  9. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BxuRwc3x.mjs +25 -0
  10. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-CQ0VjvSn.mjs +26 -0
  11. package/dist/{_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react__loadShare__.js-GeuxDuzQ.mjs → _virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react__loadShare__.js-DQXGYpip.mjs} +1 -1
  12. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Dh0XkFx5.mjs +26 -0
  13. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom__loadShare__.js-NtWzzhEn.mjs +25 -0
  14. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CFJOcZEH.mjs +25 -0
  15. package/dist/addon-benchmark.css +1 -1
  16. package/dist/{hostInit-CFrADMzo.mjs → hostInit-gynkOlvE.mjs} +2 -2
  17. package/dist/index.js +1474 -1860
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +1477 -1842
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/player-overlays-CbKnZXwO.mjs +44 -0
  22. package/dist/remoteEntry.js +1 -1
  23. package/dist/{responsive-Ltjnw-98.mjs → responsive-CGsBDbD0.mjs} +10 -14
  24. package/dist/{scene-monitor-copy-C2GyQEj3.mjs → scene-monitor-copy-T3Kfl9jG.mjs} +26 -29
  25. package/dist/{square-61mbJhyQ.mjs → square-Cj8CvK_2.mjs} +1 -1
  26. package/dist/{trash-2-DNzC9Oyi.mjs → trash-2-nQzUhmWL.mjs} +1 -1
  27. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-jGvvD5Qp.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-CrQL4i7s.mjs} +390 -399
  28. package/package.json +4 -5
  29. package/assets/fixtures/camera-sim-1080p.h264 +0 -0
  30. package/assets/fixtures/camera-sim-1440p-h265.hevc +0 -0
  31. package/assets/fixtures/camera-sim-360p.h264 +0 -0
  32. package/dist/MaskShapeCanvas-BDvrLf-K.mjs +0 -686
  33. package/dist/MotionZonesSettings-BN7x-JI4.mjs +0 -292
  34. package/dist/ReactKonva-DXzDAU1q.mjs +0 -5416
  35. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CkGpXOBB.mjs +0 -26
  36. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-lv97RBhq.mjs +0 -89
  37. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-DtWmo0vJ.mjs +0 -58
  38. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-Bh7FS7ML.mjs +0 -62
  39. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-SOU_VnTj.mjs +0 -34
  40. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom__loadShare__.js-DQaf3kYb.mjs +0 -45
  41. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-DUzuN6aS.mjs +0 -34
  42. package/dist/grid-2x2-C2zNolUZ.mjs +0 -21
  43. package/dist/hls-R1zGmDT3.mjs +0 -18476
  44. package/dist/lib-CVGFTwe0.mjs +0 -7770
  45. package/dist/player-overlays-p2s8oIiH.mjs +0 -69
package/dist/index.mjs CHANGED
@@ -5,10 +5,14 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/benchmark-addon.ts
8
- import * as fs2 from "fs";
9
- import { fileURLToPath } from "url";
10
- import * as path2 from "path";
11
- import { BaseAddon, addonPagesSourceCapability, parseJsonUnknown, errMsg } from "@camstack/types";
8
+ import {
9
+ BackgroundRunRegistry,
10
+ BaseAddon,
11
+ addonPagesSourceCapability,
12
+ errMsg,
13
+ nodePin,
14
+ parseJsonUnknown
15
+ } from "@camstack/types";
12
16
 
13
17
  // ../../node_modules/zod/v4/classic/external.js
14
18
  var external_exports = {};
@@ -776,10 +780,10 @@ function mergeDefs(...defs) {
776
780
  function cloneDef(schema) {
777
781
  return mergeDefs(schema._zod.def);
778
782
  }
779
- function getElementAtPath(obj, path3) {
780
- if (!path3)
783
+ function getElementAtPath(obj, path) {
784
+ if (!path)
781
785
  return obj;
782
- return path3.reduce((acc, key) => acc?.[key], obj);
786
+ return path.reduce((acc, key) => acc?.[key], obj);
783
787
  }
784
788
  function promiseAllObject(promisesObj) {
785
789
  const keys = Object.keys(promisesObj);
@@ -1188,11 +1192,11 @@ function explicitlyAborted(x, startIndex = 0) {
1188
1192
  }
1189
1193
  return false;
1190
1194
  }
1191
- function prefixIssues(path3, issues) {
1195
+ function prefixIssues(path, issues) {
1192
1196
  return issues.map((iss) => {
1193
1197
  var _a3;
1194
1198
  (_a3 = iss).path ?? (_a3.path = []);
1195
- iss.path.unshift(path3);
1199
+ iss.path.unshift(path);
1196
1200
  return iss;
1197
1201
  });
1198
1202
  }
@@ -1339,16 +1343,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
1339
1343
  }
1340
1344
  function formatError(error51, mapper = (issue2) => issue2.message) {
1341
1345
  const fieldErrors = { _errors: [] };
1342
- const processError = (error52, path3 = []) => {
1346
+ const processError = (error52, path = []) => {
1343
1347
  for (const issue2 of error52.issues) {
1344
1348
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1345
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
1349
+ issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1346
1350
  } else if (issue2.code === "invalid_key") {
1347
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1351
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1348
1352
  } else if (issue2.code === "invalid_element") {
1349
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1353
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1350
1354
  } else {
1351
- const fullpath = [...path3, ...issue2.path];
1355
+ const fullpath = [...path, ...issue2.path];
1352
1356
  if (fullpath.length === 0) {
1353
1357
  fieldErrors._errors.push(mapper(issue2));
1354
1358
  } else {
@@ -1375,17 +1379,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
1375
1379
  }
1376
1380
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
1377
1381
  const result = { errors: [] };
1378
- const processError = (error52, path3 = []) => {
1382
+ const processError = (error52, path = []) => {
1379
1383
  var _a3, _b;
1380
1384
  for (const issue2 of error52.issues) {
1381
1385
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1382
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
1386
+ issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1383
1387
  } else if (issue2.code === "invalid_key") {
1384
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1388
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1385
1389
  } else if (issue2.code === "invalid_element") {
1386
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1390
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1387
1391
  } else {
1388
- const fullpath = [...path3, ...issue2.path];
1392
+ const fullpath = [...path, ...issue2.path];
1389
1393
  if (fullpath.length === 0) {
1390
1394
  result.errors.push(mapper(issue2));
1391
1395
  continue;
@@ -1417,8 +1421,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
1417
1421
  }
1418
1422
  function toDotPath(_path) {
1419
1423
  const segs = [];
1420
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1421
- for (const seg of path3) {
1424
+ const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1425
+ for (const seg of path) {
1422
1426
  if (typeof seg === "number")
1423
1427
  segs.push(`[${seg}]`);
1424
1428
  else if (typeof seg === "symbol")
@@ -11500,42 +11504,42 @@ function extractDefs(ctx, schema) {
11500
11504
  if (!root)
11501
11505
  throw new Error("Unprocessed schema. This is a bug in Zod.");
11502
11506
  const idToSchema = /* @__PURE__ */ new Map();
11503
- for (const entry of ctx.seen.entries()) {
11504
- const id = ctx.metadataRegistry.get(entry[0])?.id;
11507
+ for (const entry2 of ctx.seen.entries()) {
11508
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
11505
11509
  if (id) {
11506
11510
  const existing = idToSchema.get(id);
11507
- if (existing && existing !== entry[0]) {
11511
+ if (existing && existing !== entry2[0]) {
11508
11512
  throw new Error(`Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);
11509
11513
  }
11510
- idToSchema.set(id, entry[0]);
11514
+ idToSchema.set(id, entry2[0]);
11511
11515
  }
11512
11516
  }
11513
- const makeURI = (entry) => {
11517
+ const makeURI = (entry2) => {
11514
11518
  const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
11515
11519
  if (ctx.external) {
11516
- const externalId = ctx.external.registry.get(entry[0])?.id;
11520
+ const externalId = ctx.external.registry.get(entry2[0])?.id;
11517
11521
  const uriGenerator = ctx.external.uri ?? ((id2) => id2);
11518
11522
  if (externalId) {
11519
11523
  return { ref: uriGenerator(externalId) };
11520
11524
  }
11521
- const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
11522
- entry[1].defId = id;
11525
+ const id = entry2[1].defId ?? entry2[1].schema.id ?? `schema${ctx.counter++}`;
11526
+ entry2[1].defId = id;
11523
11527
  return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
11524
11528
  }
11525
- if (entry[1] === root) {
11529
+ if (entry2[1] === root) {
11526
11530
  return { ref: "#" };
11527
11531
  }
11528
11532
  const uriPrefix = `#`;
11529
11533
  const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
11530
- const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
11534
+ const defId = entry2[1].schema.id ?? `__schema${ctx.counter++}`;
11531
11535
  return { defId, ref: defUriPrefix + defId };
11532
11536
  };
11533
- const extractToDef = (entry) => {
11534
- if (entry[1].schema.$ref) {
11537
+ const extractToDef = (entry2) => {
11538
+ if (entry2[1].schema.$ref) {
11535
11539
  return;
11536
11540
  }
11537
- const seen = entry[1];
11538
- const { ref, defId } = makeURI(entry);
11541
+ const seen = entry2[1];
11542
+ const { ref, defId } = makeURI(entry2);
11539
11543
  seen.def = { ...seen.schema };
11540
11544
  if (defId)
11541
11545
  seen.defId = defId;
@@ -11546,8 +11550,8 @@ function extractDefs(ctx, schema) {
11546
11550
  schema2.$ref = ref;
11547
11551
  };
11548
11552
  if (ctx.cycles === "throw") {
11549
- for (const entry of ctx.seen.entries()) {
11550
- const seen = entry[1];
11553
+ for (const entry2 of ctx.seen.entries()) {
11554
+ const seen = entry2[1];
11551
11555
  if (seen.cycle) {
11552
11556
  throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
11553
11557
 
@@ -11555,31 +11559,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
11555
11559
  }
11556
11560
  }
11557
11561
  }
11558
- for (const entry of ctx.seen.entries()) {
11559
- const seen = entry[1];
11560
- if (schema === entry[0]) {
11561
- extractToDef(entry);
11562
+ for (const entry2 of ctx.seen.entries()) {
11563
+ const seen = entry2[1];
11564
+ if (schema === entry2[0]) {
11565
+ extractToDef(entry2);
11562
11566
  continue;
11563
11567
  }
11564
11568
  if (ctx.external) {
11565
- const ext = ctx.external.registry.get(entry[0])?.id;
11566
- if (schema !== entry[0] && ext) {
11567
- extractToDef(entry);
11569
+ const ext = ctx.external.registry.get(entry2[0])?.id;
11570
+ if (schema !== entry2[0] && ext) {
11571
+ extractToDef(entry2);
11568
11572
  continue;
11569
11573
  }
11570
11574
  }
11571
- const id = ctx.metadataRegistry.get(entry[0])?.id;
11575
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
11572
11576
  if (id) {
11573
- extractToDef(entry);
11577
+ extractToDef(entry2);
11574
11578
  continue;
11575
11579
  }
11576
11580
  if (seen.cycle) {
11577
- extractToDef(entry);
11581
+ extractToDef(entry2);
11578
11582
  continue;
11579
11583
  }
11580
11584
  if (seen.count > 1) {
11581
11585
  if (ctx.reused === "ref") {
11582
- extractToDef(entry);
11586
+ extractToDef(entry2);
11583
11587
  continue;
11584
11588
  }
11585
11589
  }
@@ -11651,8 +11655,8 @@ function finalize(ctx, schema) {
11651
11655
  path: seen.path ?? []
11652
11656
  });
11653
11657
  };
11654
- for (const entry of [...ctx.seen.entries()].reverse()) {
11655
- flattenRef(entry[0]);
11658
+ for (const entry2 of [...ctx.seen.entries()].reverse()) {
11659
+ flattenRef(entry2[0]);
11656
11660
  }
11657
11661
  const result = {};
11658
11662
  if (ctx.target === "draft-2020-12") {
@@ -11675,8 +11679,8 @@ function finalize(ctx, schema) {
11675
11679
  if (rootMetaId !== void 0 && result.id === rootMetaId)
11676
11680
  delete result.id;
11677
11681
  const defs = ctx.external?.defs ?? {};
11678
- for (const entry of ctx.seen.entries()) {
11679
- const seen = entry[1];
11682
+ for (const entry2 of ctx.seen.entries()) {
11683
+ const seen = entry2[1];
11680
11684
  if (seen.def && seen.defId) {
11681
11685
  if (seen.def.id === seen.defId)
11682
11686
  delete seen.def.id;
@@ -12292,8 +12296,8 @@ function toJSONSchema(input, params) {
12292
12296
  const registry2 = input;
12293
12297
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
12294
12298
  const defs = {};
12295
- for (const entry of registry2._idmap.entries()) {
12296
- const [_, schema] = entry;
12299
+ for (const entry2 of registry2._idmap.entries()) {
12300
+ const [_, schema] = entry2;
12297
12301
  process2(schema, ctx2);
12298
12302
  }
12299
12303
  const schemas = {};
@@ -12303,8 +12307,8 @@ function toJSONSchema(input, params) {
12303
12307
  defs
12304
12308
  };
12305
12309
  ctx2.external = external;
12306
- for (const entry of registry2._idmap.entries()) {
12307
- const [key, schema] = entry;
12310
+ for (const entry2 of registry2._idmap.entries()) {
12311
+ const [key, schema] = entry2;
12308
12312
  extractDefs(ctx2, schema);
12309
12313
  schemas[key] = finalize(ctx2, schema);
12310
12314
  }
@@ -14110,13 +14114,13 @@ function resolveRef(ref, ctx) {
14110
14114
  if (!ref.startsWith("#")) {
14111
14115
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14112
14116
  }
14113
- const path3 = ref.slice(1).split("/").filter(Boolean);
14114
- if (path3.length === 0) {
14117
+ const path = ref.slice(1).split("/").filter(Boolean);
14118
+ if (path.length === 0) {
14115
14119
  return ctx.rootSchema;
14116
14120
  }
14117
14121
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14118
- if (path3[0] === defsKey) {
14119
- const key = path3[1];
14122
+ if (path[0] === defsKey) {
14123
+ const key = path[1];
14120
14124
  if (!key || !ctx.defs[key]) {
14121
14125
  throw new Error(`Reference not found: ${ref}`);
14122
14126
  }
@@ -14528,14 +14532,159 @@ config(en_default());
14528
14532
  import { customAction, defineCustomActions, PipelineRunResultBridge } from "@camstack/types";
14529
14533
 
14530
14534
  // src/benchmark-schemas.ts
14531
- import { HWACCEL_OPTIONS, PipelineStepInputSchema } from "@camstack/types";
14532
- var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14533
- kind: external_exports.literal("pipeline").default("pipeline"),
14534
- id: external_exports.string(),
14535
- date: external_exports.string(),
14535
+ import { PipelineStepInputSchema as PipelineStepInputSchema2 } from "@camstack/types";
14536
+
14537
+ // src/session-replay/schemas.ts
14538
+ import { PipelineStepInputSchema } from "@camstack/types";
14539
+ var NormBoxSchema = external_exports.object({
14540
+ x: external_exports.number(),
14541
+ y: external_exports.number(),
14542
+ w: external_exports.number(),
14543
+ h: external_exports.number()
14544
+ });
14545
+ var ReplayBoxSchema = external_exports.object({
14546
+ className: external_exports.string(),
14547
+ score: external_exports.number(),
14548
+ box: NormBoxSchema
14549
+ });
14550
+ var StoredPositionSchema = external_exports.object({
14551
+ trackId: external_exports.string(),
14552
+ className: external_exports.string(),
14553
+ t: external_exports.number(),
14554
+ box: NormBoxSchema,
14555
+ score: external_exports.number().nullable()
14556
+ });
14557
+ var ReferenceBasisSchema = external_exports.enum(["exact", "interpolated", "uncertain"]);
14558
+ var ReferenceEntrySchema = StoredPositionSchema.extend({ basis: ReferenceBasisSchema });
14559
+ var TimelineReplayBoxSchema = ReplayBoxSchema.extend({
14560
+ /** `unreferenced`: no stored track was alive at this instant — nothing to contradict. */
14561
+ outcome: external_exports.enum(["matched", "class-changed", "added", "unreferenced"])
14562
+ });
14563
+ var TimelineStoredBoxSchema = ReferenceEntrySchema.extend({
14564
+ outcome: external_exports.enum(["matched", "class-changed", "missed"])
14565
+ });
14566
+ var TimelineFrameSchema = external_exports.object({
14567
+ t: external_exports.number(),
14568
+ /** Some stored track was alive at `t` — the instant HAS a reference. */
14569
+ referenced: external_exports.boolean(),
14570
+ replay: external_exports.array(TimelineReplayBoxSchema).readonly(),
14571
+ stored: external_exports.array(TimelineStoredBoxSchema).readonly(),
14572
+ inferenceMs: external_exports.number().nullable()
14573
+ });
14574
+ var ClassCountsSchema = external_exports.object({
14575
+ replay: external_exports.number(),
14576
+ stored: external_exports.number(),
14577
+ matched: external_exports.number(),
14578
+ added: external_exports.number(),
14579
+ missed: external_exports.number(),
14580
+ unreferenced: external_exports.number(),
14581
+ /** Comparisons (matched / class-changed / missed) made on a non-exact stored box. */
14582
+ interpolated: external_exports.number()
14583
+ });
14584
+ var ReplaySummarySchema = external_exports.object({
14585
+ totals: external_exports.object({
14586
+ matched: external_exports.number(),
14587
+ classChanged: external_exports.number(),
14588
+ added: external_exports.number(),
14589
+ missed: external_exports.number(),
14590
+ unreferenced: external_exports.number(),
14591
+ interpolated: external_exports.number()
14592
+ }),
14593
+ perClass: external_exports.record(external_exports.string(), ClassCountsSchema),
14594
+ scoreDelta: external_exports.object({ mean: external_exports.number(), p10: external_exports.number(), p50: external_exports.number(), p90: external_exports.number() }).nullable(),
14595
+ inferenceMs: external_exports.object({ p50: external_exports.number(), p95: external_exports.number(), count: external_exports.number() }).nullable()
14596
+ });
14597
+ var CadenceSourceSchema = external_exports.enum(["operator", "stored-positions", "default"]);
14598
+ var ReplayCadenceSchema = external_exports.object({ fps: external_exports.number(), source: CadenceSourceSchema });
14599
+ var SessionReplayReportSchema = external_exports.object({
14600
+ deviceId: external_exports.number(),
14601
+ motionEventId: external_exports.number(),
14602
+ window: external_exports.object({
14603
+ startMs: external_exports.number(),
14604
+ endMs: external_exports.number(),
14605
+ motionStartMs: external_exports.number(),
14606
+ motionEndMs: external_exports.number(),
14607
+ /** Where the pre-roll / tail came from: the camera's own settings, or the fleet default. */
14608
+ prerollSource: external_exports.enum(["camera", "default"]),
14609
+ tailSource: external_exports.enum(["camera", "default"])
14610
+ }),
14611
+ pin: external_exports.object({
14612
+ nodeId: external_exports.string(),
14613
+ deviceKey: external_exports.string().nullable(),
14614
+ modelId: external_exports.string(),
14615
+ cameraModelId: external_exports.string().nullable()
14616
+ }),
14617
+ rootStep: PipelineStepInputSchema,
14618
+ cadence: ReplayCadenceSchema,
14619
+ frameSize: external_exports.object({ width: external_exports.number(), source: external_exports.enum(["stored-tracks", "default"]) }),
14620
+ frames: external_exports.object({
14621
+ planned: external_exports.number(),
14622
+ decoded: external_exports.number(),
14623
+ ran: external_exports.number(),
14624
+ failed: external_exports.number(),
14625
+ withoutFootage: external_exports.number(),
14626
+ notDecoded: external_exports.number(),
14627
+ truncated: external_exports.boolean(),
14628
+ truncatedBySafetyCap: external_exports.boolean(),
14629
+ aborted: external_exports.boolean()
14630
+ }),
14631
+ reference: external_exports.object({
14632
+ tracks: external_exports.number(),
14633
+ positionsCompared: external_exports.number(),
14634
+ recoveryExcluded: external_exports.number(),
14635
+ withoutGeometry: external_exports.number(),
14636
+ models: external_exports.record(external_exports.string(), external_exports.number()),
14637
+ /** The stored-track read: how many came back, and whether its limit cut it short. */
14638
+ read: external_exports.object({ tracks: external_exports.number(), truncated: external_exports.boolean() })
14639
+ }),
14640
+ confirmation: external_exports.object({
14641
+ liveVerdicts: external_exports.record(external_exports.string(), external_exports.number()),
14642
+ /** Added or unreferenced detections near a live SUPPRESSED birth of their class. */
14643
+ nearSuppressedBirth: external_exports.number()
14644
+ }),
14645
+ summary: ReplaySummarySchema,
14646
+ timeline: external_exports.array(TimelineFrameSchema).readonly(),
14647
+ failures: external_exports.array(external_exports.object({ t: external_exports.number(), error: external_exports.string() })).readonly()
14648
+ });
14649
+ var SessionReplayRefusalSchema = external_exports.enum([
14650
+ "session-not-found",
14651
+ "session-open",
14652
+ "window-too-long",
14653
+ "no-root-step",
14654
+ "model-unpinned",
14655
+ "model-not-servable",
14656
+ "device-not-servable",
14657
+ "no-recording",
14658
+ "no-footage",
14659
+ "session-length-unknown",
14660
+ "pool-failing"
14661
+ ]);
14662
+ var SessionReplayOutcomeSchema = external_exports.discriminatedUnion("kind", [
14663
+ external_exports.object({
14664
+ kind: external_exports.literal("refused"),
14665
+ reason: SessionReplayRefusalSchema,
14666
+ detail: external_exports.string()
14667
+ }),
14668
+ external_exports.object({ kind: external_exports.literal("ran"), report: SessionReplayReportSchema })
14669
+ ]);
14670
+ var SessionReplayProgressSchema = external_exports.object({
14671
+ phase: external_exports.enum(["preparing", "replaying", "done"]),
14672
+ framesPlanned: external_exports.number(),
14673
+ framesDone: external_exports.number()
14674
+ });
14675
+
14676
+ // src/benchmark-schemas.ts
14677
+ var BenchModeSchema = external_exports.enum(["inference", "full-tree", "live-frame"]);
14678
+ var BenchPoolTuningSchema = external_exports.object({
14679
+ runtime: external_exports.string().nullable(),
14680
+ numWorkers: external_exports.number().nullable(),
14681
+ concurrency: external_exports.number().nullable(),
14682
+ batchMode: external_exports.string().nullable(),
14683
+ maxBatchSize: external_exports.number().nullable()
14684
+ });
14685
+ var HistoryMeasurementFields = {
14536
14686
  stepId: external_exports.string(),
14537
14687
  modelId: external_exports.string(),
14538
- runtime: external_exports.string(),
14539
14688
  iterations: external_exports.number(),
14540
14689
  warmup: external_exports.number(),
14541
14690
  fps: external_exports.number(),
@@ -14544,377 +14693,163 @@ var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14544
14693
  p95Ms: external_exports.number(),
14545
14694
  p99Ms: external_exports.number(),
14546
14695
  /**
14547
- * OPTIONAL, and absent on every entry the page writes.
14548
- * `pipeline-runner.runSyntheticBench` reports mean / p50 / p95 / p99 and
14549
- * measures no per-call min or max. They were required here while nothing
14550
- * in the page ever called `saveBenchmarkResult`, so nothing noticed; the
14551
- * moment it does, a required field can only be satisfied by inventing a
14552
- * number, and a fabricated latency in the history table is worse than a
14553
- * blank one.
14696
+ * OPTIONAL, and absent on every entry the page writes: the runner reports
14697
+ * mean / p50 / p95 / p99 and measures no per-call min or max. A fabricated
14698
+ * latency in the history table is worse than a blank one.
14554
14699
  */
14555
14700
  minMs: external_exports.number().optional(),
14556
14701
  maxMs: external_exports.number().optional(),
14557
14702
  avgDetections: external_exports.number(),
14558
14703
  referenceImage: external_exports.string().optional(),
14559
- /** Number of concurrent runs aggregated into this entry; 1 = single run. */
14704
+ /** Concurrent in-flight requests (simulated cameras) — NOT pool workers. */
14560
14705
  parallelRuns: external_exports.number().min(1).default(1)
14561
- });
14562
- var PipelineBenchmarkHistoryEntryInputSchema = PipelineBenchmarkHistoryEntrySchema.omit({
14563
- kind: true,
14564
- id: true,
14565
- date: true
14566
- });
14567
- var DecoderFixtureCodecSchema = external_exports.enum(["h264", "h265"]);
14568
- var DecoderFixtureSchema = external_exports.object({
14569
- filename: external_exports.string(),
14570
- codec: DecoderFixtureCodecSchema,
14571
- resolutionLabel: external_exports.string(),
14572
- // '360p', '1080p', '1440p'
14573
- sizeBytes: external_exports.number(),
14574
- description: external_exports.string().optional()
14575
- });
14576
- var CamProfileSchema = external_exports.enum(["high", "mid", "low"]);
14577
- var DecoderPerfSourceSchema = external_exports.discriminatedUnion("kind", [
14578
- external_exports.object({
14579
- kind: external_exports.literal("sample"),
14580
- fixtureFilename: external_exports.string()
14581
- }),
14582
- external_exports.object({
14583
- kind: external_exports.literal("stream"),
14584
- deviceId: external_exports.number().int().min(1),
14585
- profile: CamProfileSchema
14586
- })
14587
- ]);
14588
- var DecoderPerfModeSchema = external_exports.discriminatedUnion("kind", [
14589
- external_exports.object({ kind: external_exports.literal("duration"), durationSec: external_exports.number().min(5).max(1800) }),
14590
- external_exports.object({ kind: external_exports.literal("iterations"), iterations: external_exports.number().min(10).max(1e6) })
14591
- ]);
14592
- var DecoderPerfFormatSchema = external_exports.enum(["jpeg", "gray"]);
14593
- var HWACCEL_VALUES = HWACCEL_OPTIONS.map((o) => o.value);
14594
- var DecoderPerfHwaccelSchema = external_exports.enum(HWACCEL_VALUES);
14595
- var DecoderPerfThresholdsSchema = external_exports.object({
14596
- warnMBMin: external_exports.number().min(0).default(5),
14597
- failMBMin: external_exports.number().min(0).default(20)
14598
- });
14599
- var DecoderPerfInputSchema = external_exports.object({
14600
- sessionId: external_exports.string(),
14601
- cameras: external_exports.number().int().min(1).max(16),
14602
- mode: DecoderPerfModeSchema,
14603
- source: DecoderPerfSourceSchema,
14604
- format: DecoderPerfFormatSchema,
14605
- fps: external_exports.number().min(1).max(120),
14606
- toggleMs: external_exports.number().min(0).default(0),
14607
- warmupSec: external_exports.number().min(0).default(15),
14608
- sampleIntervalMs: external_exports.number().min(500).default(2e3),
14609
- hwaccel: DecoderPerfHwaccelSchema.default("none"),
14610
- thresholds: DecoderPerfThresholdsSchema.default({ warnMBMin: 5, failMBMin: 20 }),
14611
- // Pin the decoder sessions to the selected node. createSession is the only
14612
- // decoder call without a node selector (the rest route via the nodeId that
14613
- // createSession returns), so without this the whole test runs on the
14614
- // singleton's elected node (the hub) regardless of the UI selection.
14615
- nodeId: external_exports.string().optional()
14616
- });
14617
- var DecoderPerfSampleSchema = external_exports.object({
14618
- tSec: external_exports.number(),
14619
- rssMB: external_exports.number(),
14620
- heapMB: external_exports.number(),
14621
- externalMB: external_exports.number(),
14622
- arrayBuffersMB: external_exports.number(),
14623
- cpuPct: external_exports.number(),
14624
- framesTotal: external_exports.number()
14625
- });
14626
- var DecoderPerfVerdictSchema = external_exports.enum(["pass", "warn", "fail", "aborted", "error"]);
14627
- var DecoderPerfResultSchema = external_exports.object({
14628
- sessionId: external_exports.string(),
14629
- verdict: DecoderPerfVerdictSchema,
14630
- aborted: external_exports.boolean().default(false),
14631
- errorMessage: external_exports.string().optional(),
14632
- samples: external_exports.array(DecoderPerfSampleSchema).readonly(),
14633
- slopes: external_exports.object({
14634
- rssMBMin: external_exports.number(),
14635
- heapMBMin: external_exports.number(),
14636
- externalMBMin: external_exports.number()
14637
- }),
14638
- totals: external_exports.object({
14639
- wallSec: external_exports.number(),
14640
- framesEmitted: external_exports.number(),
14641
- fpsPerCamera: external_exports.number(),
14642
- cpuAvgPct: external_exports.number(),
14643
- droppedFrames: external_exports.number()
14644
- }),
14645
- config: DecoderPerfInputSchema
14646
- });
14647
- var DecoderPerfProgressSchema = external_exports.object({
14648
- kind: external_exports.literal("decoder-perf"),
14649
- sessionId: external_exports.string(),
14650
- phase: external_exports.enum(["init", "warmup", "running", "complete", "aborted", "error"]),
14651
- tSec: external_exports.number().optional(),
14652
- sample: DecoderPerfSampleSchema.optional(),
14653
- message: external_exports.string().optional()
14654
- });
14655
- var DecoderPerfHistoryEntrySchema = external_exports.object({
14656
- kind: external_exports.literal("decoder-perf"),
14706
+ };
14707
+ var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14708
+ kind: external_exports.literal("pipeline").default("pipeline"),
14657
14709
  id: external_exports.string(),
14658
14710
  date: external_exports.string(),
14659
- sessionId: external_exports.string(),
14660
- verdict: DecoderPerfVerdictSchema,
14661
- cameras: external_exports.number(),
14662
- format: DecoderPerfFormatSchema,
14663
- sourceLabel: external_exports.string(),
14664
- // 'sample:camera-sim-360p' | 'stream:dev-2/low'
14665
- modeLabel: external_exports.string(),
14666
- // 'duration:60s' | 'iterations:5000'
14667
- rssSlopeMBMin: external_exports.number(),
14668
- heapSlopeMBMin: external_exports.number(),
14669
- externalSlopeMBMin: external_exports.number(),
14670
- framesEmitted: external_exports.number(),
14671
- fpsPerCamera: external_exports.number(),
14672
- cpuAvgPct: external_exports.number(),
14673
- droppedFrames: external_exports.number(),
14674
- wallSec: external_exports.number(),
14675
- hwaccel: DecoderPerfHwaccelSchema
14711
+ ...HistoryMeasurementFields,
14712
+ nodeId: external_exports.string().optional(),
14713
+ deviceKey: external_exports.string().nullable().optional(),
14714
+ plane: external_exports.enum(["full", "frame"]).nullable().optional(),
14715
+ mode: BenchModeSchema.optional(),
14716
+ pool: BenchPoolTuningSchema.optional(),
14717
+ runtime: external_exports.string().optional()
14676
14718
  });
14677
- var DecoderPerfHistoryEntryInputSchema = DecoderPerfHistoryEntrySchema.omit({
14678
- kind: true,
14679
- id: true,
14680
- date: true
14719
+ var PipelineBenchmarkHistoryEntryInputSchema = external_exports.object({
14720
+ ...HistoryMeasurementFields,
14721
+ nodeId: external_exports.string(),
14722
+ deviceKey: external_exports.string().nullable(),
14723
+ plane: external_exports.enum(["full", "frame"]).nullable(),
14724
+ mode: BenchModeSchema,
14725
+ pool: BenchPoolTuningSchema
14681
14726
  });
14682
- var HistoryEntrySchema = external_exports.discriminatedUnion("kind", [
14683
- PipelineBenchmarkHistoryEntrySchema,
14684
- DecoderPerfHistoryEntrySchema
14685
- ]);
14727
+ var HistoryEntrySchema = PipelineBenchmarkHistoryEntrySchema;
14686
14728
  var PipelineRunInputSchema = external_exports.object({
14687
- steps: external_exports.array(PipelineStepInputSchema).readonly(),
14729
+ steps: external_exports.array(PipelineStepInputSchema2).readonly(),
14688
14730
  imageBase64: external_exports.string().optional(),
14689
14731
  referenceImage: external_exports.string().optional(),
14690
- frameId: external_exports.string().optional(),
14691
- /**
14692
- * Target node for the pipeline test. Forwarded to
14693
- * `pipelineExecutor.runPipeline`. Omit to run on the hub.
14694
- */
14732
+ /** Target node. Forwarded to `pipelineExecutor.runPipeline`. Omit to run on the hub. */
14695
14733
  nodeId: external_exports.string().optional(),
14696
- /**
14697
- * UI-provided correlation id. When set, every `pipeline.progress`
14698
- * event emitted by the run is tagged with this value so the caller
14699
- * can filter out concurrent camera-runtime dispatches that share the
14700
- * same bus category.
14701
- */
14734
+ /** UI correlation id stamped on every `pipeline.progress` line of the run. */
14702
14735
  sessionId: external_exports.string().optional(),
14703
- engine: external_exports.object({
14704
- runtime: external_exports.enum(["python", "node"]),
14705
- backend: external_exports.string(),
14706
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14707
- device: external_exports.string().optional()
14708
- }).optional(),
14709
14736
  deviceKey: external_exports.string().optional(),
14710
14737
  plane: external_exports.enum(["full", "frame"]).optional()
14711
14738
  });
14712
- var PipelineRunBatchInputSchema = external_exports.object({
14713
- steps: external_exports.array(PipelineStepInputSchema).readonly(),
14714
- imageBase64: external_exports.string(),
14715
- batchSize: external_exports.number().int().min(1).max(255),
14716
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14717
- nodeId: external_exports.string().optional(),
14718
- sessionId: external_exports.string().optional(),
14719
- engine: external_exports.object({
14720
- runtime: external_exports.enum(["python", "node"]),
14721
- backend: external_exports.string(),
14722
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14723
- device: external_exports.string().optional()
14724
- }).optional()
14739
+ var SyntheticBenchPoolSchema = external_exports.object({
14740
+ nodeId: external_exports.string(),
14741
+ deviceKey: external_exports.string().nullable(),
14742
+ runtime: external_exports.string().nullable(),
14743
+ numWorkers: external_exports.number().nullable(),
14744
+ concurrency: external_exports.number().nullable(),
14745
+ batchMode: external_exports.string().nullable(),
14746
+ maxBatchSize: external_exports.number().nullable()
14725
14747
  });
14726
- var PipelineRunBatchResultSchema = external_exports.object({
14727
- results: external_exports.array(PipelineRunResultBridgeRef()).readonly(),
14728
- callMs: external_exports.number(),
14729
- inferMs: external_exports.number()
14748
+ var SyntheticBenchStatsSchema = external_exports.object({
14749
+ runs: external_exports.number(),
14750
+ wallSec: external_exports.number(),
14751
+ fps: external_exports.number(),
14752
+ detectionsPerSec: external_exports.number(),
14753
+ avgDetections: external_exports.number(),
14754
+ callMs: external_exports.object({ mean: external_exports.number(), p50: external_exports.number(), p95: external_exports.number(), p99: external_exports.number() }),
14755
+ inferMs: external_exports.number(),
14756
+ preprocessMs: external_exports.number(),
14757
+ predictMs: external_exports.number(),
14758
+ batchSizeMean: external_exports.number(),
14759
+ batchSizeMax: external_exports.number(),
14760
+ pool: SyntheticBenchPoolSchema,
14761
+ path: external_exports.string()
14730
14762
  });
14731
- function PipelineRunResultBridgeRef() {
14732
- return external_exports.unknown();
14733
- }
14734
- var MultiCameraStressInputSchema = external_exports.object({
14735
- steps: external_exports.array(PipelineStepInputSchema).readonly(),
14736
- imageBase64: external_exports.string(),
14737
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14738
- simulatedCameras: external_exports.number().int().min(1).max(64).optional(),
14739
- batchesPerCamera: external_exports.number().int().min(1).max(2e3).optional(),
14740
- batchSize: external_exports.number().int().min(1).max(64).optional(),
14741
- extraSinglesPerRound: external_exports.number().int().min(0).max(16).optional(),
14742
- nodeId: external_exports.string().optional(),
14743
- sessionId: external_exports.string().optional()
14763
+ var SyntheticBenchStatusSchema = external_exports.object({
14764
+ sessionId: external_exports.string(),
14765
+ state: external_exports.enum(["running", "done", "failed", "aborted"]),
14766
+ startedAtMs: external_exports.number(),
14767
+ finishedAtMs: external_exports.number().nullable(),
14768
+ progress: external_exports.object({ runs: external_exports.number(), totalRuns: external_exports.number() }).nullable(),
14769
+ result: SyntheticBenchStatsSchema.nullable(),
14770
+ error: external_exports.string().nullable()
14771
+ }).nullable();
14772
+ var MotionSessionSchema = external_exports.object({
14773
+ motionEventId: external_exports.number(),
14774
+ deviceId: external_exports.number(),
14775
+ startMs: external_exports.number(),
14776
+ /** `null` while the session is still open. */
14777
+ durationMs: external_exports.number().nullable(),
14778
+ open: external_exports.boolean(),
14779
+ /** Why a replay of this session would be refused (`session-open`, `session-length-unknown`, …); `null` = replayable. */
14780
+ refusal: external_exports.string().nullable(),
14781
+ /** Stored tracks born inside the session window (slim read). */
14782
+ storedTracks: external_exports.number()
14744
14783
  });
14745
- var MultiCameraStressResultSchema = external_exports.object({
14746
- totalMs: external_exports.number(),
14747
- countedDetections: external_exports.number(),
14748
- detsPerSec: external_exports.number(),
14749
- totalCalls: external_exports.number(),
14750
- totalFrames: external_exports.number(),
14751
- callMsP50: external_exports.number(),
14752
- callMsP95: external_exports.number(),
14753
- callMsP99: external_exports.number(),
14754
- inferMsMean: external_exports.number(),
14755
- errors: external_exports.number()
14784
+ var ListMotionSessionsInputSchema = external_exports.object({
14785
+ deviceId: external_exports.number(),
14786
+ /** Default: the last 24 h. */
14787
+ sinceMs: external_exports.number().optional(),
14788
+ untilMs: external_exports.number().optional(),
14789
+ limit: external_exports.number().int().min(1).max(200).default(50)
14756
14790
  });
14757
- var SustainedEngineSchema = external_exports.object({
14758
- runtime: external_exports.enum(["python", "node"]),
14759
- backend: external_exports.string(),
14760
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14761
- device: external_exports.string().optional()
14762
- }).optional();
14763
- var SustainedNodeTargetSchema = external_exports.object({
14764
- nodeId: external_exports.string().optional(),
14765
- engine: SustainedEngineSchema,
14791
+ var SESSION_REPLAY_MAX_FRAMES = 1200;
14792
+ var StartSessionReplayInputSchema = external_exports.object({
14793
+ deviceId: external_exports.number(),
14794
+ motionEventId: external_exports.number(),
14795
+ /** The session's start (`getMotionEvents.timestamp`) — the lookup is windowed on it. */
14796
+ motionStartMs: external_exports.number(),
14797
+ nodeId: external_exports.string(),
14798
+ /** Absent ⇒ the node-default pool. */
14766
14799
  deviceKey: external_exports.string().optional(),
14767
- steps: external_exports.array(PipelineStepInputSchema).readonly(),
14768
- label: external_exports.string().optional()
14769
- });
14770
- var SustainedThroughputInputSchema = external_exports.object({
14771
- frameId: external_exports.string().optional(),
14772
- imageBase64: external_exports.string().optional(),
14773
- durationSec: external_exports.number().min(1).max(3600),
14774
- batchSize: external_exports.number().int().min(1).max(255),
14775
- concurrency: external_exports.number().int().min(1).max(64),
14776
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14777
- sessionId: external_exports.string(),
14778
- nodes: external_exports.array(SustainedNodeTargetSchema).min(1).readonly()
14800
+ /** Absent ⇒ the model the camera's resolved tree names. */
14801
+ modelId: external_exports.string().optional(),
14802
+ /** Absent ⇒ measured from the stored positions (the live cadence). */
14803
+ fps: external_exports.number().min(1).max(30).optional(),
14804
+ maxFrames: external_exports.number().int().min(1).max(SESSION_REPLAY_MAX_FRAMES).default(600)
14779
14805
  });
14780
- var SustainedLatencyStatsSchema = external_exports.object({
14781
- p50: external_exports.number(),
14782
- p95: external_exports.number(),
14783
- p99: external_exports.number(),
14784
- mean: external_exports.number(),
14785
- min: external_exports.number(),
14786
- max: external_exports.number()
14806
+ var StartSessionReplayAcceptedSchema = external_exports.object({
14807
+ accepted: external_exports.literal(true),
14808
+ runId: external_exports.string()
14787
14809
  });
14788
- var SustainedNodeResultSchema = external_exports.object({
14789
- nodeId: external_exports.string().nullable(),
14790
- label: external_exports.string().optional(),
14791
- /** Total frames processed by this node's loop. */
14792
- totalFrames: external_exports.number(),
14793
- /** Total completed batch calls. */
14794
- totalBatches: external_exports.number(),
14795
- /** Measured wall-clock window in ms. */
14796
- wallMs: external_exports.number(),
14797
- /** Sustained throughput. */
14798
- fps: external_exports.number(),
14799
- detsPerSec: external_exports.number(),
14800
- /** Mean detections per frame — stable since the frame is fixed. */
14801
- avgDetections: external_exports.number(),
14802
- latency: SustainedLatencyStatsSchema,
14803
- /** Batch calls that errored (transport / inference). */
14804
- errors: external_exports.number(),
14805
- /** True when this node ended via the stop button rather than the deadline. */
14806
- aborted: external_exports.boolean()
14810
+ var SessionReplayKeySchema = external_exports.object({ runId: external_exports.string() });
14811
+ var SessionReplayStatusSchema = external_exports.object({
14812
+ runId: external_exports.string(),
14813
+ state: external_exports.enum(["running", "done", "failed", "aborted"]),
14814
+ startedAtMs: external_exports.number(),
14815
+ finishedAtMs: external_exports.number().nullable(),
14816
+ progress: SessionReplayProgressSchema.nullable(),
14817
+ outcome: SessionReplayOutcomeSchema.nullable(),
14818
+ error: external_exports.string().nullable()
14819
+ }).nullable();
14820
+ var StoredSessionReplaySummarySchema = external_exports.object({
14821
+ id: external_exports.string(),
14822
+ deviceId: external_exports.number(),
14823
+ nodeId: external_exports.string(),
14824
+ createdAtMs: external_exports.number(),
14825
+ report: SessionReplayReportSchema.omit({ timeline: true })
14807
14826
  });
14808
- var SustainedThroughputResultSchema = external_exports.object({
14809
- perNode: external_exports.array(SustainedNodeResultSchema).readonly(),
14810
- durationSec: external_exports.number(),
14811
- batchSize: external_exports.number(),
14812
- concurrency: external_exports.number()
14827
+ var ListSessionReplaysInputSchema = external_exports.object({ deviceId: external_exports.number() });
14828
+ var ListSessionReplaysResultSchema = external_exports.object({
14829
+ runs: external_exports.array(StoredSessionReplaySummarySchema).readonly(),
14830
+ /** Rows that did not read as a session replay (the removed clip replays). */
14831
+ skipped: external_exports.number()
14813
14832
  });
14814
- var ReplaySourceSchema = external_exports.custom();
14815
- var ReplayOverlaySchema = external_exports.custom();
14816
- var OrchestrateReplayRunResultSchema = external_exports.custom();
14817
- var StoredReplayRunSchema = external_exports.custom();
14818
- var ReplayDiffSchema = external_exports.custom();
14819
- var RunReplayClipInputSchema = external_exports.object({
14833
+ var GetSessionReplayInputSchema = external_exports.object({ id: external_exports.string() });
14834
+ var StoredSessionReplaySchema = external_exports.object({
14835
+ id: external_exports.string(),
14820
14836
  deviceId: external_exports.number(),
14821
14837
  nodeId: external_exports.string(),
14822
- source: ReplaySourceSchema,
14823
- overlay: ReplayOverlaySchema,
14824
- fps: external_exports.number().positive(),
14825
- maxFrames: external_exports.number().int().positive(),
14826
- maxWidth: external_exports.number().int().positive().default(1920),
14827
- deviceKey: external_exports.string().optional()
14828
- });
14829
- var UploadReplayClipInputSchema = external_exports.object({
14830
- dataBase64: external_exports.string(),
14831
- durationMs: external_exports.number().positive()
14838
+ createdAtMs: external_exports.number(),
14839
+ report: SessionReplayReportSchema
14832
14840
  });
14833
- var UploadReplayClipResultSchema = external_exports.object({ clipId: external_exports.string() });
14834
- var ListReplayRunsInputSchema = external_exports.object({ deviceId: external_exports.number() });
14835
- var DiffReplayRunsInputSchema = external_exports.object({ runIdA: external_exports.string(), runIdB: external_exports.string() });
14836
14841
 
14837
14842
  // src/benchmark-actions.ts
14838
14843
  var benchmarkActions = defineCustomActions({
14839
- // Pipeline tester
14844
+ // Detection Pipeline — one run.
14840
14845
  runPipelineTest: customAction(PipelineRunInputSchema, PipelineRunResultBridge, {
14841
14846
  kind: "mutation"
14842
14847
  }),
14843
14848
  /**
14844
- * Decode a JPEG once on the server and cache the resulting raw RGB
14845
- * buffer keyed by a returned `frameId`. Subsequent `runPipelineTest`
14846
- * calls pass `frameId` instead of `imageBase64` — the addon ships the
14847
- * cached raw frame to `pipelineExecutor.runPipeline` via the `frame`
14848
- * field, mirroring the production camera path (stream-broker delivers
14849
- * decoded RGB to pipeline-runner). Eliminates per-call JPEG decode +
14850
- * 265KB base64 transfer that was dominating bench transport time.
14851
- */
14852
- cacheBenchFrame: customAction(
14853
- external_exports.object({ imageBase64: external_exports.string(), ttlSeconds: external_exports.number().int().positive().optional() }),
14854
- external_exports.object({ frameId: external_exports.string(), width: external_exports.number(), height: external_exports.number(), expiresAt: external_exports.number() }),
14855
- { kind: "mutation" }
14856
- ),
14857
- releaseBenchFrame: customAction(
14858
- external_exports.object({ frameId: external_exports.string() }),
14859
- external_exports.object({ released: external_exports.boolean() }),
14860
- { kind: "mutation" }
14861
- ),
14862
- /**
14863
- * Batched pipeline tester. Decodes the JPEG once into RGB on the
14864
- * addon, then forwards N FrameInputs to the target node in a single
14865
- * `pipelineExecutor.runPipelineBatch` cap call. Returns N
14866
- * `FrameResult`s plus aggregate timing.
14867
- *
14868
- * NO UI CALLER — and it is NOT dead. `scripts/bench-pool-ceiling.mts` and
14869
- * `scripts/bench-pipeline-investigation.mts` drive it directly. Do not
14870
- * delete it on the strength of an in-package grep; those scripts break
14871
- * silently and nothing in CI runs them.
14872
- */
14873
- runPipelineBatchTest: customAction(PipelineRunBatchInputSchema, PipelineRunBatchResultSchema, {
14874
- kind: "mutation"
14875
- }),
14876
- /**
14877
- * Multi-camera stress tester — runs the full multi-camera workload
14878
- * loop in-process (decode JPEG once, then N parallel "cameras" each
14879
- * issuing a configurable mix of batched + single cap calls). Returns
14880
- * only aggregate detection-rate stats. Used to measure the pool's
14881
- * true throughput ceiling without per-call tRPC overhead.
14882
- *
14883
- * NO UI CALLER — and it is NOT dead. `scripts/bench-pool-inprocess.mts`
14884
- * drives it directly. Same warning as `runPipelineBatchTest` above.
14885
- */
14886
- runMultiCameraStressTest: customAction(
14887
- MultiCameraStressInputSchema,
14888
- MultiCameraStressResultSchema,
14889
- { kind: "mutation" }
14890
- ),
14891
- /**
14892
- * Sustained-throughput ROOT-detection e2e run. Pins ONE frame (decoded
14893
- * once), then feeds the identical frame to root object-detection for
14894
- * `durationSec` seconds — driven by BATCHES (`runPipelineBatch`,
14895
- * `batchSize` frames/call) with `concurrency` calls in flight, fanned out
14896
- * across `nodes`, each on its resolved runtime/model + root-only steps.
14897
- * Returns per-node sustained fps / det/s + latency percentiles on the
14898
- * identical pinned frame. Bounded by N + `abortSustainedThroughput`.
14899
- */
14900
- runSustainedThroughput: customAction(
14901
- SustainedThroughputInputSchema,
14902
- SustainedThroughputResultSchema,
14903
- { kind: "mutation" }
14904
- ),
14905
- abortSustainedThroughput: customAction(
14906
- external_exports.object({ sessionId: external_exports.string() }),
14907
- external_exports.object({ aborted: external_exports.boolean() }),
14908
- { kind: "mutation" }
14909
- ),
14910
- /**
14911
- * History (shared across pipeline + decoder-perf entries).
14849
+ * History.
14912
14850
  *
14913
14851
  * `z.object({})`, never `z.void()`: the `addons.custom` envelope insists
14914
- * on an `input` object, and a `z.void()` schema REJECTS `{}` — so the
14915
- * action resolved and could never actually be called, not from a settings
14916
- * button and not from `hub.mjs`. The tabs only got away with it by
14917
- * sending `input: undefined`.
14852
+ * on an `input` object, and a `z.void()` schema REJECTS `{}`.
14918
14853
  */
14919
14854
  listBenchmarkHistory: customAction(external_exports.object({}), external_exports.array(HistoryEntrySchema).readonly()),
14920
14855
  saveBenchmarkResult: customAction(
@@ -14922,11 +14857,6 @@ var benchmarkActions = defineCustomActions({
14922
14857
  PipelineBenchmarkHistoryEntrySchema,
14923
14858
  { kind: "mutation" }
14924
14859
  ),
14925
- saveDecoderPerformanceResult: customAction(
14926
- DecoderPerfHistoryEntryInputSchema,
14927
- DecoderPerfHistoryEntrySchema,
14928
- { kind: "mutation" }
14929
- ),
14930
14860
  deleteBenchmarkResult: customAction(
14931
14861
  external_exports.object({ id: external_exports.string() }),
14932
14862
  external_exports.object({ success: external_exports.literal(true) }),
@@ -14935,332 +14865,41 @@ var benchmarkActions = defineCustomActions({
14935
14865
  clearBenchmarkHistory: customAction(external_exports.object({}), external_exports.object({ success: external_exports.literal(true) }), {
14936
14866
  kind: "mutation"
14937
14867
  }),
14938
- // Replay — rigiocare una pipeline vera su una clip fissa. Regia qui,
14939
- // FrameProcessor pass in `addon-post-analysis` via
14940
- // `pipelineAnalytics.runReplayFrameProcessor` (see `replay/replay-wiring.ts`).
14941
- uploadReplayClip: customAction(UploadReplayClipInputSchema, UploadReplayClipResultSchema, {
14942
- kind: "mutation"
14943
- }),
14944
- runReplayClip: customAction(RunReplayClipInputSchema, OrchestrateReplayRunResultSchema, {
14945
- kind: "mutation"
14946
- }),
14947
- listReplayRuns: customAction(
14948
- ListReplayRunsInputSchema,
14949
- external_exports.array(StoredReplayRunSchema).readonly()
14868
+ // Session replay (D646) — the stored sessions to pick from, then start / poll / abort.
14869
+ listMotionSessions: customAction(
14870
+ ListMotionSessionsInputSchema,
14871
+ external_exports.array(MotionSessionSchema).readonly()
14950
14872
  ),
14951
- diffReplayRuns: customAction(DiffReplayRunsInputSchema, ReplayDiffSchema),
14952
- // Decoder perf — fixture discovery + run control.
14953
- // `z.object({})` for the same reason as `listBenchmarkHistory` above.
14954
- listDecoderFixtures: customAction(external_exports.object({}), external_exports.array(DecoderFixtureSchema).readonly()),
14955
- runDecoderPerformanceTest: customAction(DecoderPerfInputSchema, DecoderPerfResultSchema, {
14956
- kind: "mutation"
14873
+ startSessionReplay: customAction(
14874
+ StartSessionReplayInputSchema,
14875
+ StartSessionReplayAcceptedSchema,
14876
+ { kind: "mutation", auth: "admin" }
14877
+ ),
14878
+ getSessionReplayStatus: customAction(SessionReplayKeySchema, SessionReplayStatusSchema),
14879
+ abortSessionReplay: customAction(SessionReplayKeySchema, external_exports.object({ aborted: external_exports.boolean() }), {
14880
+ kind: "mutation",
14881
+ auth: "admin"
14957
14882
  }),
14958
- abortDecoderPerformanceTest: customAction(
14959
- external_exports.object({ sessionId: external_exports.string() }),
14960
- external_exports.object({ aborted: external_exports.boolean() }),
14961
- { kind: "mutation" }
14962
- )
14883
+ listSessionReplays: customAction(ListSessionReplaysInputSchema, ListSessionReplaysResultSchema),
14884
+ getSessionReplay: customAction(GetSessionReplayInputSchema, StoredSessionReplaySchema.nullable())
14963
14885
  });
14964
14886
 
14965
- // src/benchmark-addon.ts
14966
- import { createHash } from "crypto";
14967
-
14968
- // src/decoder-perf-runner.ts
14969
- import * as fs from "fs";
14970
- import * as path from "path";
14971
- import { performance } from "perf_hooks";
14972
- import { cpuUsage, memoryUsage } from "process";
14973
- import { createEvent, nodePin } from "@camstack/types";
14974
- var CHUNK_SIZE = 4096;
14975
- function emitProgress(eventBus, payload) {
14976
- if (!eventBus) return;
14977
- eventBus.emit(
14978
- createEvent(
14979
- "benchmark.progress",
14980
- { type: "benchmark", id: "decoder-perf" },
14981
- {
14982
- kind: payload.kind,
14983
- sessionId: payload.sessionId,
14984
- phase: payload.phase,
14985
- tSec: payload.tSec,
14986
- sample: payload.sample ? { ...payload.sample } : void 0,
14987
- message: payload.message
14988
- }
14989
- )
14990
- );
14991
- }
14992
- function slope(xs, ys) {
14993
- if (xs.length < 2) return 0;
14994
- const n = xs.length;
14995
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
14996
- for (let i = 0; i < n; i++) {
14997
- sx += xs[i];
14998
- sy += ys[i];
14999
- sxx += xs[i] * xs[i];
15000
- sxy += xs[i] * ys[i];
15001
- }
15002
- const denom = n * sxx - sx * sx;
15003
- return denom === 0 ? 0 : (n * sxy - sx * sy) / denom;
15004
- }
15005
- function verdictFor(rssSlopeMBMin, warnMBMin, failMBMin) {
15006
- if (rssSlopeMBMin >= failMBMin) return "fail";
15007
- if (rssSlopeMBMin >= warnMBMin) return "warn";
15008
- return "pass";
15009
- }
15010
- function resolveFixtureCodec(filename) {
15011
- if (filename.endsWith(".hevc") || filename.endsWith(".h265")) return "h265";
15012
- return "h264";
15013
- }
15014
- function readFixtureBytes(fixturesDir, filename) {
15015
- const full = path.join(fixturesDir, filename);
15016
- if (!fs.existsSync(full)) {
15017
- throw new Error(`decoder fixture not found: ${filename}`);
15018
- }
15019
- return fs.readFileSync(full);
15020
- }
15021
- async function createSessions(api, input, codec2) {
15022
- const handles = [];
15023
- const pin = input.nodeId ? nodePin(input.nodeId) : void 0;
15024
- for (let i = 0; i < input.cameras; i++) {
15025
- const created = await api.decoder.createSession.query(
15026
- {
15027
- codec: codec2,
15028
- maxFps: input.fps,
15029
- outputFormat: input.format === "gray" ? "gray" : "jpeg",
15030
- scale: 1
15031
- },
15032
- pin
15033
- );
15034
- handles.push({
15035
- sessionId: created.sessionId,
15036
- nodeId: created.nodeId,
15037
- codec: codec2,
15038
- offsetBytes: 0,
15039
- framesPushed: 0
15040
- });
15041
- }
15042
- return handles;
15043
- }
15044
- async function destroySessions(api, handles, logger) {
15045
- for (const h of handles) {
15046
- try {
15047
- await api.decoder.destroySession.query({ sessionId: h.sessionId, nodeId: h.nodeId });
15048
- } catch (err) {
15049
- logger.warn("destroySession failed", {
15050
- meta: { sessionId: h.sessionId, error: err instanceof Error ? err.message : String(err) }
15051
- });
15052
- }
15053
- }
15054
- }
15055
- async function pushChunk(api, handle, fixture) {
15056
- if (handle.offsetBytes >= fixture.length) handle.offsetBytes = 0;
15057
- const end = Math.min(handle.offsetBytes + CHUNK_SIZE, fixture.length);
15058
- const chunk = fixture.subarray(handle.offsetBytes, end);
15059
- handle.offsetBytes = end;
15060
- const now = Date.now();
15061
- await api.decoder.pushPacket.query({
15062
- sessionId: handle.sessionId,
15063
- nodeId: handle.nodeId,
15064
- packet: {
15065
- type: "video",
15066
- data: new Uint8Array(chunk),
15067
- pts: now,
15068
- dts: now,
15069
- keyframe: handle.offsetBytes <= CHUNK_SIZE,
15070
- codec: handle.codec
15071
- }
15072
- });
15073
- handle.framesPushed += 1;
15074
- }
15075
- async function sumDroppedFrames(api, handles) {
15076
- let dropped = 0;
15077
- for (const h of handles) {
15078
- try {
15079
- const stats = await api.decoder.getStats.query({ sessionId: h.sessionId, nodeId: h.nodeId });
15080
- dropped += stats.droppedFrames;
15081
- } catch {
15082
- }
15083
- }
15084
- return dropped;
15085
- }
15086
- async function runDecoderPerfTest(input, deps) {
15087
- const { api, eventBus, logger, fixturesDir, abortFlag } = deps;
15088
- const sessionId = input.sessionId;
15089
- const samples = [];
15090
- const startWall = performance.now();
15091
- emitProgress(eventBus, {
15092
- kind: "decoder-perf",
15093
- sessionId,
15094
- phase: "init",
15095
- message: "initialising"
15096
- });
15097
- if (input.source.kind !== "sample") {
15098
- const msg = "decoder perf: stream source not yet implemented \u2014 use a sample fixture";
15099
- emitProgress(eventBus, { kind: "decoder-perf", sessionId, phase: "error", message: msg });
15100
- return {
15101
- sessionId,
15102
- verdict: "error",
15103
- aborted: false,
15104
- errorMessage: msg,
15105
- samples: [],
15106
- slopes: { rssMBMin: 0, heapMBMin: 0, externalMBMin: 0 },
15107
- totals: { wallSec: 0, framesEmitted: 0, fpsPerCamera: 0, cpuAvgPct: 0, droppedFrames: 0 },
15108
- config: input
15109
- };
15110
- }
15111
- const fixtureName = input.source.fixtureFilename;
15112
- const codec2 = resolveFixtureCodec(fixtureName);
15113
- const fixture = readFixtureBytes(fixturesDir, fixtureName);
15114
- try {
15115
- const active = await api.decoder.listActiveSessions.query();
15116
- if (active.length > 0) {
15117
- logger.warn("concurrent decoder sessions detected \u2014 measurement may be contaminated", {
15118
- meta: { concurrent: active.length }
15119
- });
15120
- }
15121
- } catch {
15122
- }
15123
- try {
15124
- const instances = await api.metricsProvider.listAddonInstances.query();
15125
- const workerInstances = instances.filter((i) => i.role === "worker");
15126
- if (workerInstances.length === 0) {
15127
- logger.info(
15128
- "decoder perf: no isolated worker detected \u2014 hub-local measurement (process.memoryUsage) mixes decoder + hub workload"
15129
- );
15130
- } else {
15131
- logger.info("decoder perf: worker instances visible", {
15132
- meta: { workers: workerInstances.map((w) => w.addonId) }
15133
- });
15134
- }
15135
- } catch {
15136
- }
15137
- let handles;
15138
- try {
15139
- handles = await createSessions(api, input, codec2);
15140
- } catch (err) {
15141
- const msg = err instanceof Error ? err.message : String(err);
15142
- emitProgress(eventBus, {
15143
- kind: "decoder-perf",
15144
- sessionId,
15145
- phase: "error",
15146
- message: `createSession failed: ${msg}`
15147
- });
15148
- return {
15149
- sessionId,
15150
- verdict: "error",
15151
- aborted: false,
15152
- errorMessage: msg,
15153
- samples: [],
15154
- slopes: { rssMBMin: 0, heapMBMin: 0, externalMBMin: 0 },
15155
- totals: { wallSec: 0, framesEmitted: 0, fpsPerCamera: 0, cpuAvgPct: 0, droppedFrames: 0 },
15156
- config: input
15157
- };
14887
+ // src/replay/mjpeg-split.ts
14888
+ var SOI = Buffer.from([255, 216]);
14889
+ var EOI = Buffer.from([255, 217]);
14890
+ function splitMjpegStream(bytes) {
14891
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
14892
+ const out = [];
14893
+ let pos = 0;
14894
+ for (; ; ) {
14895
+ const soi = buf.indexOf(SOI, pos);
14896
+ if (soi === -1) break;
14897
+ const eoi = buf.indexOf(EOI, soi + SOI.length);
14898
+ if (eoi === -1) break;
14899
+ out.push(buf.subarray(soi, eoi + EOI.length));
14900
+ pos = eoi + EOI.length;
15158
14901
  }
15159
- const frameIntervalMs = 1e3 / input.fps;
15160
- const warmupMs = input.warmupSec * 1e3;
15161
- const durationMs = input.mode.kind === "duration" ? input.mode.durationSec * 1e3 : Number.MAX_SAFE_INTEGER;
15162
- const targetIterations = input.mode.kind === "iterations" ? input.mode.iterations : Number.MAX_SAFE_INTEGER;
15163
- let cpuSnapshot = cpuUsage();
15164
- let cpuWallMs = performance.now();
15165
- const feedTickerStart = performance.now();
15166
- let nextFeedAt = feedTickerStart;
15167
- let nextSampleAt = feedTickerStart + warmupMs;
15168
- let aborted2 = false;
15169
- emitProgress(eventBus, {
15170
- kind: "decoder-perf",
15171
- sessionId,
15172
- phase: "warmup",
15173
- tSec: 0,
15174
- message: `warming up ${input.warmupSec}s`
15175
- });
15176
- const loopUntil = feedTickerStart + warmupMs + durationMs;
15177
- while (performance.now() < loopUntil) {
15178
- if (abortFlag.aborted) {
15179
- aborted2 = true;
15180
- break;
15181
- }
15182
- const totalPushed = handles.reduce((s, h) => s + h.framesPushed, 0);
15183
- if (totalPushed >= targetIterations * handles.length) break;
15184
- const now = performance.now();
15185
- if (now >= nextFeedAt) {
15186
- for (const h of handles) {
15187
- try {
15188
- await pushChunk(api, h, fixture);
15189
- } catch (err) {
15190
- logger.warn("pushPacket failed", {
15191
- meta: {
15192
- sessionId: h.sessionId,
15193
- error: err instanceof Error ? err.message : String(err)
15194
- }
15195
- });
15196
- }
15197
- }
15198
- nextFeedAt += frameIntervalMs;
15199
- }
15200
- if (now >= nextSampleAt) {
15201
- const tSec = (now - feedTickerStart) / 1e3;
15202
- const mem = memoryUsage();
15203
- const cpu = cpuUsage(cpuSnapshot);
15204
- const windowMs = now - cpuWallMs;
15205
- const cpuPct = windowMs > 0 ? (cpu.user + cpu.system) / 1e3 / windowMs * 100 : 0;
15206
- cpuSnapshot = cpuUsage();
15207
- cpuWallMs = now;
15208
- const sample = {
15209
- tSec,
15210
- rssMB: mem.rss / 1048576,
15211
- heapMB: mem.heapUsed / 1048576,
15212
- externalMB: mem.external / 1048576,
15213
- arrayBuffersMB: mem.arrayBuffers / 1048576,
15214
- cpuPct,
15215
- framesTotal: handles.reduce((s, h) => s + h.framesPushed, 0)
15216
- };
15217
- samples.push(sample);
15218
- emitProgress(eventBus, {
15219
- kind: "decoder-perf",
15220
- sessionId,
15221
- phase: tSec < 0 ? "warmup" : "running",
15222
- tSec,
15223
- sample
15224
- });
15225
- nextSampleAt += input.sampleIntervalMs;
15226
- }
15227
- const sleepMs = Math.max(0, Math.min(nextFeedAt, nextSampleAt) - performance.now());
15228
- if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
15229
- }
15230
- const droppedFrames = await sumDroppedFrames(api, handles);
15231
- await destroySessions(api, handles, logger);
15232
- const postWarmup = samples.filter((s) => s.tSec * 1e3 >= warmupMs);
15233
- const xs = postWarmup.map((s) => s.tSec);
15234
- const rssYs = postWarmup.map((s) => s.rssMB);
15235
- const heapYs = postWarmup.map((s) => s.heapMB);
15236
- const extYs = postWarmup.map((s) => s.externalMB);
15237
- const rssSlopeMBMin = slope(xs, rssYs) * 60;
15238
- const heapSlopeMBMin = slope(xs, heapYs) * 60;
15239
- const externalSlopeMBMin = slope(xs, extYs) * 60;
15240
- const wallSec = (performance.now() - startWall) / 1e3;
15241
- const framesEmitted = handles.reduce((s, h) => s + h.framesPushed, 0);
15242
- const fpsPerCamera = wallSec > 0 ? framesEmitted / Math.max(1, handles.length) / wallSec : 0;
15243
- const cpuAvgPct = samples.length > 0 ? samples.reduce((s, x) => s + x.cpuPct, 0) / samples.length : 0;
15244
- const verdict = aborted2 ? "aborted" : verdictFor(rssSlopeMBMin, input.thresholds.warnMBMin, input.thresholds.failMBMin);
15245
- emitProgress(eventBus, {
15246
- kind: "decoder-perf",
15247
- sessionId,
15248
- phase: aborted2 ? "aborted" : "complete",
15249
- message: `done \u2014 rss slope ${rssSlopeMBMin.toFixed(2)} MB/min`
15250
- });
15251
- return {
15252
- sessionId,
15253
- verdict,
15254
- aborted: aborted2,
15255
- samples,
15256
- slopes: {
15257
- rssMBMin: rssSlopeMBMin,
15258
- heapMBMin: heapSlopeMBMin,
15259
- externalMBMin: externalSlopeMBMin
15260
- },
15261
- totals: { wallSec, framesEmitted, fpsPerCamera, cpuAvgPct, droppedFrames },
15262
- config: input
15263
- };
14902
+ return out;
15264
14903
  }
15265
14904
 
15266
14905
  // src/replay/replay-decode.ts
@@ -15289,13 +14928,13 @@ var REPLAY_DECODE_MAX_CONCURRENT = 2;
15289
14928
  var REPLAY_DECODE_TIMEOUT_MS = 15e3;
15290
14929
  var KILL_GRACE_MS = 500;
15291
14930
  var ReplayDecoder = class {
15292
- ffmpegPath;
14931
+ resolveFfmpegPath;
15293
14932
  spawnFn;
15294
14933
  maxConcurrent;
15295
14934
  timeoutMs;
15296
14935
  inFlight = 0;
15297
14936
  constructor(deps) {
15298
- this.ffmpegPath = deps.ffmpegPath;
14937
+ this.resolveFfmpegPath = deps.resolveFfmpegPath;
15299
14938
  this.spawnFn = deps.spawnFn ?? ((bin, args) => spawn(bin, [...args]));
15300
14939
  this.maxConcurrent = deps.maxConcurrent ?? REPLAY_DECODE_MAX_CONCURRENT;
15301
14940
  this.timeoutMs = deps.timeoutMs ?? REPLAY_DECODE_TIMEOUT_MS;
@@ -15312,7 +14951,7 @@ var ReplayDecoder = class {
15312
14951
  * uploaded clip) are fetched through a capability call, not a local path.
15313
14952
  * Resolves with the whole MJPEG stdout stream.
15314
14953
  */
15315
- async decodeSequence(args, input) {
14954
+ async decodeSequence(args, input, opts = {}) {
15316
14955
  if (this.inFlight >= this.maxConcurrent) {
15317
14956
  throw new Error(
15318
14957
  `replay decode busy: node already running ${this.inFlight}/${this.maxConcurrent} decodes`
@@ -15320,19 +14959,20 @@ var ReplayDecoder = class {
15320
14959
  }
15321
14960
  this.inFlight += 1;
15322
14961
  try {
15323
- return await this.runOnce(args, input);
14962
+ const ffmpegPath = await this.resolveFfmpegPath();
14963
+ return await this.runOnce(ffmpegPath, args, input, opts.timeoutMs ?? this.timeoutMs);
15324
14964
  } finally {
15325
14965
  this.inFlight -= 1;
15326
14966
  }
15327
14967
  }
15328
- runOnce(args, input) {
15329
- return new Promise((resolve2, reject) => {
14968
+ runOnce(ffmpegPath, args, input, timeoutMs) {
14969
+ return new Promise((resolve, reject) => {
15330
14970
  const chunks = [];
15331
14971
  const stderrTail = [];
15332
14972
  let settled = false;
15333
14973
  const child = (() => {
15334
14974
  try {
15335
- return this.spawnFn(this.ffmpegPath, args);
14975
+ return this.spawnFn(ffmpegPath, args);
15336
14976
  } catch (err) {
15337
14977
  reject(new Error(`replay decode: ffmpeg spawn failed: ${String(err)}`));
15338
14978
  return null;
@@ -15363,7 +15003,7 @@ var ReplayDecoder = class {
15363
15003
  const timer = setTimeout(() => {
15364
15004
  settle(() => reject(new Error("replay decode: timed out")));
15365
15005
  kill();
15366
- }, this.timeoutMs);
15006
+ }, timeoutMs);
15367
15007
  timer.unref?.();
15368
15008
  child.stdout?.on("data", (chunk) => chunks.push(chunk));
15369
15009
  child.stderr?.on("data", (data) => {
@@ -15379,7 +15019,7 @@ var ReplayDecoder = class {
15379
15019
  child.on("exit", (code, signal) => {
15380
15020
  const stdout = Buffer.concat(chunks);
15381
15021
  if (code === 0) {
15382
- settle(() => resolve2(stdout));
15022
+ settle(() => resolve(stdout));
15383
15023
  return;
15384
15024
  }
15385
15025
  settle(
@@ -15401,6 +15041,7 @@ var ReplayDecoder = class {
15401
15041
 
15402
15042
  // src/replay/replay-store.ts
15403
15043
  var REPLAY_RUNS_COLLECTION = "benchmark:replay-runs";
15044
+ var REPLAY_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
15404
15045
  var REPLAY_RUNS_COLUMNS = [
15405
15046
  { name: "id", type: "TEXT", primaryKey: true, notNull: true },
15406
15047
  { name: "deviceId", type: "INTEGER", notNull: true },
@@ -15415,7 +15056,14 @@ var REPLAY_RUNS_INDEXES = [
15415
15056
  { name: "idx_replay_runs_device_created", columns: ["deviceId", "createdAtMs"] },
15416
15057
  { name: "idx_replay_runs_created", columns: ["createdAtMs"] }
15417
15058
  ];
15418
- var ReplayStore = class {
15059
+ var StoredRowSchema = external_exports.object({
15060
+ deviceId: external_exports.number(),
15061
+ nodeId: external_exports.string(),
15062
+ createdAtMs: external_exports.number(),
15063
+ source: external_exports.object({ kind: external_exports.literal("motion-session") }).loose(),
15064
+ verdict: SessionReplayReportSchema
15065
+ });
15066
+ var ReplayStore = class _ReplayStore {
15419
15067
  constructor(store) {
15420
15068
  this.store = store;
15421
15069
  }
@@ -15430,24 +15078,57 @@ var ReplayStore = class {
15430
15078
  });
15431
15079
  }
15432
15080
  async save(run) {
15433
- const { id, ...rest } = run;
15081
+ const { report } = run;
15434
15082
  await this.store.insert.mutate({
15435
15083
  collection: REPLAY_RUNS_COLLECTION,
15436
- record: { id, data: rest }
15437
- // TECH-DEBT: settings-store boundary, see the comment above.
15084
+ record: {
15085
+ id: run.id,
15086
+ data: {
15087
+ deviceId: run.deviceId,
15088
+ nodeId: run.nodeId,
15089
+ clipId: `session:${report.deviceId}:${report.motionEventId}`,
15090
+ createdAtMs: run.createdAtMs,
15091
+ source: { kind: "motion-session", motionEventId: report.motionEventId, ...report.window },
15092
+ plan: {
15093
+ pin: report.pin,
15094
+ rootStep: report.rootStep,
15095
+ cadence: report.cadence,
15096
+ frameSize: report.frameSize
15097
+ },
15098
+ verdict: report
15099
+ }
15100
+ }
15438
15101
  });
15439
15102
  }
15440
- /** Runs newest-first, optionally scoped to one device. */
15103
+ /** Session replays newest-first, optionally scoped to one camera. */
15441
15104
  async list(query) {
15442
- const filter = { orderBy: { field: "createdAtMs", direction: "desc" } };
15443
- if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
15444
- if (query.limit !== void 0) filter.limit = query.limit;
15445
- const rows = await this.store.query.query({ collection: REPLAY_RUNS_COLLECTION, filter });
15446
- return rows.map((r) => ({ id: r.id, ...r.data }));
15105
+ const rows = await this.store.query.query({
15106
+ collection: REPLAY_RUNS_COLLECTION,
15107
+ filter: {
15108
+ orderBy: { field: "createdAtMs", direction: "desc" },
15109
+ ...query.deviceId !== void 0 ? { where: { deviceId: query.deviceId } } : {},
15110
+ ...query.limit !== void 0 ? { limit: query.limit } : {}
15111
+ }
15112
+ });
15113
+ const runs = [];
15114
+ let skipped = 0;
15115
+ for (const row of rows) {
15116
+ const parsed = _ReplayStore.readRow(row.id, row.data);
15117
+ if (parsed === null) skipped += 1;
15118
+ else runs.push(parsed);
15119
+ }
15120
+ return { runs, skipped };
15121
+ }
15122
+ async get(id) {
15123
+ const rows = await this.store.query.query({
15124
+ collection: REPLAY_RUNS_COLLECTION,
15125
+ filter: { where: { id }, limit: 1 }
15126
+ });
15127
+ const row = rows[0];
15128
+ return row === void 0 ? null : _ReplayStore.readRow(row.id, row.data);
15447
15129
  }
15448
15130
  /** Delete every run created before `cutoffMs`. This store's OWN retention
15449
- * clock — see the file header for why that matters. Returns the count so
15450
- * a caller can log it, the way `OpsLogStore`-adjacent sweeps do. */
15131
+ * clock — see the file header for why that matters. */
15451
15132
  async pruneBefore(cutoffMs) {
15452
15133
  const { deleted } = await this.store.deleteWhere.mutate({
15453
15134
  collection: REPLAY_RUNS_COLLECTION,
@@ -15455,449 +15136,845 @@ var ReplayStore = class {
15455
15136
  });
15456
15137
  return deleted;
15457
15138
  }
15458
- };
15459
-
15460
- // src/replay/replay-plan.ts
15461
- function readPreflight(result) {
15462
- if (!result.ok) {
15463
- const detail = result.issues.map((i) => `${i.addonId}: ${i.detail}`).join("; ");
15464
- return { kind: "refused", reason: "step-unrunnable", detail };
15465
- }
15466
- if (result.substitutions.length > 0) {
15467
- const detail = result.substitutions.map((s) => `${s.addonId}: chose \`${s.chosen}\`, running \`${s.running}\` (${s.format})`).join("; ");
15468
- return { kind: "refused", reason: "model-substituted", detail };
15469
- }
15470
- return { kind: "runnable", format: result.format };
15471
- }
15472
- var PACKAGE_DETECTION_STEP_ID = "package-detection";
15473
- function applyPackageZoneOverlay(steps, overlay) {
15474
- if (overlay.packageDetection === void 0) return steps;
15475
- const existing = steps.find((s) => s.addonId === PACKAGE_DETECTION_STEP_ID);
15476
- if (!overlay.packageDetection) {
15477
- if (!existing) return steps;
15478
- return steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
15479
- }
15480
- if (existing) {
15481
- if (existing.enabled === true) return steps;
15482
- return steps.map((s) => s.addonId === PACKAGE_DETECTION_STEP_ID ? { ...s, enabled: true } : s);
15483
- }
15484
- const node = {
15485
- addonId: PACKAGE_DETECTION_STEP_ID,
15486
- enabled: true,
15487
- children: []
15488
- };
15489
- return [...steps, node];
15490
- }
15491
- function applySettingsAndModels(steps, overlay, ignored) {
15492
- const settingsKeys = new Set(Object.keys(overlay.stepSettings ?? {}));
15493
- const modelKeys = new Set(Object.keys(overlay.stepModels ?? {}));
15494
- const presentIds = new Set(steps.map((s) => s.addonId));
15495
- for (const id of settingsKeys) if (!presentIds.has(id)) ignored.add(id);
15496
- for (const id of modelKeys) if (!presentIds.has(id)) ignored.add(id);
15497
- return steps.map((s) => {
15498
- const settingsPatch = overlay.stepSettings?.[s.addonId];
15499
- const modelOverride = overlay.stepModels?.[s.addonId];
15500
- if (settingsPatch === void 0 && modelOverride === void 0) return s;
15139
+ static readRow(id, data) {
15140
+ const parsed = StoredRowSchema.safeParse(data);
15141
+ if (!parsed.success) return null;
15501
15142
  return {
15502
- ...s,
15503
- ...modelOverride !== void 0 ? { modelId: modelOverride } : {},
15504
- ...settingsPatch !== void 0 ? { settings: { ...s.settings, ...settingsPatch } } : {}
15143
+ id,
15144
+ deviceId: parsed.data.deviceId,
15145
+ nodeId: parsed.data.nodeId,
15146
+ createdAtMs: parsed.data.createdAtMs,
15147
+ report: parsed.data.verdict
15505
15148
  };
15506
- });
15507
- }
15508
- function buildReplayPlan(base, overlay) {
15509
- const ignored = /* @__PURE__ */ new Set();
15510
- const withPackageZone = applyPackageZoneOverlay(base.steps, overlay);
15511
- const steps = applySettingsAndModels(withPackageZone, overlay, ignored);
15512
- return { steps, ignoredOverlays: [...ignored] };
15513
- }
15514
-
15515
- // src/replay/replay-source.ts
15516
- function planReplayFrames(source, fps, maxFrames) {
15517
- const durationMs = source.kind === "recording" ? source.toMs - source.fromMs : source.durationMs;
15518
- if (durationMs <= 0) return { kind: "refused", reason: "empty-window" };
15519
- const stepMs = 1e3 / fps;
15520
- const startAt = source.kind === "recording" ? source.fromMs : 0;
15521
- const rawCount = Math.max(1, Math.floor(durationMs / stepMs));
15522
- const truncated = rawCount > maxFrames;
15523
- const count = Math.min(rawCount, maxFrames);
15524
- const instants = [];
15525
- for (let i = 0; i < count; i++) instants.push(startAt + Math.round(i * stepMs));
15526
- return { kind: "planned", deviceId: source.deviceId, instants, truncated };
15527
- }
15528
-
15529
- // src/replay/mjpeg-split.ts
15530
- var SOI = Buffer.from([255, 216]);
15531
- var EOI = Buffer.from([255, 217]);
15532
- function splitMjpegStream(bytes) {
15533
- const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
15534
- const out = [];
15535
- let pos = 0;
15536
- for (; ; ) {
15537
- const soi = buf.indexOf(SOI, pos);
15538
- if (soi === -1) break;
15539
- const eoi = buf.indexOf(EOI, soi + SOI.length);
15540
- if (eoi === -1) break;
15541
- out.push(buf.subarray(soi, eoi + EOI.length));
15542
- pos = eoi + EOI.length;
15543
15149
  }
15544
- return out;
15545
- }
15150
+ };
15546
15151
 
15547
- // src/replay/replay-run.ts
15548
- var DEFAULT_REPLAY_BUDGET_MS = 12e4;
15549
- var DEFAULT_REPLAY_PACING_MS = 50;
15550
- function sleep(ms) {
15551
- return new Promise((resolve2) => setTimeout(resolve2, ms));
15552
- }
15553
- async function runReplay(deps, frames) {
15554
- const now = deps.now ?? Date.now;
15555
- const budgetMs = deps.budgetMs ?? DEFAULT_REPLAY_BUDGET_MS;
15556
- const pacingMs = deps.pacingMs ?? DEFAULT_REPLAY_PACING_MS;
15557
- const startedAt = now();
15558
- const tags = { deviceId: deps.deviceId };
15559
- deps.logger.info("replay: run starting", {
15560
- tags,
15561
- meta: { framesPlanned: frames.length, nodeId: deps.nodeId, pacingMs, budgetMs }
15562
- });
15563
- const succeeded = [];
15564
- const discarded = [];
15565
- let framesFailed = 0;
15566
- let stoppedOnBudget = false;
15567
- for (let i = 0; i < frames.length; i++) {
15568
- if (now() - startedAt >= budgetMs) {
15569
- stoppedOnBudget = true;
15570
- deps.logger.warn("replay: run stopped on budget \u2014 a partial census, reported as such", {
15571
- tags,
15572
- meta: {
15573
- framesPlanned: frames.length,
15574
- framesRan: succeeded.length,
15575
- framesFailed,
15576
- budgetMs
15577
- }
15578
- });
15579
- break;
15152
+ // src/session-replay/recording-reader.ts
15153
+ async function readRecordingForInstants(deps, instants, stepMs) {
15154
+ const parts = [];
15155
+ let withoutFootage = 0;
15156
+ let truncated = false;
15157
+ let i = 0;
15158
+ while (i < instants.length) {
15159
+ const t = instants[i];
15160
+ if (t === void 0) break;
15161
+ const located = await deps.locateSegment(t);
15162
+ if (located.kind !== "segment") {
15163
+ const edge = located.nearestEdgeMs;
15164
+ let j2 = i + 1;
15165
+ if (edge === null) j2 = instants.length;
15166
+ else if (edge > t) while (j2 < instants.length && (instants[j2] ?? edge) < edge) j2++;
15167
+ withoutFootage += j2 - i;
15168
+ i = j2;
15169
+ continue;
15580
15170
  }
15581
- const f = frames[i];
15582
- if (f === void 0) continue;
15583
- try {
15584
- const frame = await deps.runPipeline({
15585
- timestamp: f.timestamp,
15586
- image: f.image,
15587
- nodeId: deps.nodeId
15588
- });
15589
- succeeded.push({ timestamp: f.timestamp, frame });
15590
- if (frame.discarded !== void 0) discarded.push(...frame.discarded);
15591
- } catch (err) {
15592
- framesFailed += 1;
15593
- deps.logger.warn("replay: frame failed on the node \u2014 counted, run continues", {
15594
- tags,
15595
- meta: { timestamp: f.timestamp, error: err instanceof Error ? err.message : String(err) }
15596
- });
15171
+ const segEnd = located.startMs + located.durMs;
15172
+ let j = i;
15173
+ while (j < instants.length && (instants[j] ?? segEnd) < segEnd) j++;
15174
+ const covered = instants.slice(i, j);
15175
+ const lastCovered = covered[covered.length - 1] ?? t;
15176
+ const window = await deps.readWindowBytes({
15177
+ startMs: located.startMs,
15178
+ fromMs: t,
15179
+ toMs: Math.min(segEnd, lastCovered + Math.round(stepMs))
15180
+ });
15181
+ if (window.kind === "ok") {
15182
+ if (!window.reachesRequestedEnd) truncated = true;
15183
+ parts.push({ bytes: window.data, gopStartMs: window.gopStartMs, instants: covered });
15184
+ } else {
15185
+ withoutFootage += covered.length;
15597
15186
  }
15598
- if (pacingMs > 0 && i < frames.length - 1) await sleep(pacingMs);
15187
+ i = j === i ? i + 1 : j;
15188
+ }
15189
+ if (parts.length === 0) {
15190
+ return {
15191
+ kind: "refused",
15192
+ reason: "no-footage",
15193
+ detail: `no recording covers any of the ${instants.length} planned instants`
15194
+ };
15599
15195
  }
15600
- const tracks = succeeded.length > 0 ? (await deps.runTracking(succeeded)).tracks : [];
15601
- const complete = !stoppedOnBudget && framesFailed === 0;
15602
- deps.logger.info("replay: run finished", {
15603
- tags,
15604
- meta: {
15605
- framesPlanned: frames.length,
15606
- framesRan: succeeded.length,
15607
- framesFailed,
15608
- complete,
15609
- tracks: tracks.length
15610
- }
15611
- });
15612
15196
  return {
15613
- kind: succeeded.length === 0 ? "no-frame-ran" : "ran",
15614
- deviceId: deps.deviceId,
15615
- nodeId: deps.nodeId,
15616
- complete,
15617
- tracks,
15618
- discarded,
15619
- counters: { framesPlanned: frames.length, framesRan: succeeded.length, framesFailed }
15197
+ kind: "ok",
15198
+ parts,
15199
+ instantsWithoutFootage: withoutFootage,
15200
+ truncatedBySafetyCap: truncated
15620
15201
  };
15621
15202
  }
15622
15203
 
15623
- // src/replay/replay-wiring.ts
15624
- function clipIdOf(source) {
15625
- return source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`;
15626
- }
15627
- async function orchestrateReplayRun(deps, input) {
15628
- const tags = { deviceId: input.deviceId };
15629
- const base = await deps.resolveBaseTree(input.deviceId);
15630
- const plan = buildReplayPlan(base, input.overlay);
15631
- if (plan.ignoredOverlays.length > 0) {
15632
- deps.logger.warn("replay: overlay named a step the resolved tree does not run", {
15633
- tags,
15634
- meta: { ignoredOverlays: plan.ignoredOverlays }
15635
- });
15636
- }
15637
- const preflight = await deps.api.pipelineExecutor.validatePipeline.query({
15638
- steps: [...plan.steps]
15639
- });
15640
- const preflightVerdict = readPreflight(preflight);
15641
- if (preflightVerdict.kind === "refused") {
15642
- deps.logger.warn("replay: run refused at preflight", {
15643
- tags,
15644
- meta: { reason: preflightVerdict.reason, detail: preflightVerdict.detail }
15645
- });
15646
- return { kind: "refused", reason: `${preflightVerdict.reason}: ${preflightVerdict.detail}` };
15204
+ // src/session-replay/cadence.ts
15205
+ var DEFAULT_DETECTION_FPS = 10;
15206
+ var MIN_DETECTION_FPS = 1;
15207
+ var MAX_DETECTION_FPS = 30;
15208
+ var MIN_GAPS_TO_MEASURE = 3;
15209
+ function clampFps(fps) {
15210
+ return Math.min(MAX_DETECTION_FPS, Math.max(MIN_DETECTION_FPS, fps));
15211
+ }
15212
+ function resolveReplayCadence(requestedFps, positionTimesByTrack) {
15213
+ if (requestedFps !== void 0) return { fps: clampFps(requestedFps), source: "operator" };
15214
+ const gaps = [];
15215
+ for (const times of positionTimesByTrack) {
15216
+ for (let i = 1; i < times.length; i++) {
15217
+ const prev = times[i - 1];
15218
+ const cur = times[i];
15219
+ if (prev !== void 0 && cur !== void 0 && cur > prev) gaps.push(cur - prev);
15220
+ }
15221
+ }
15222
+ if (gaps.length < MIN_GAPS_TO_MEASURE) return { fps: DEFAULT_DETECTION_FPS, source: "default" };
15223
+ const sorted = gaps.toSorted((a, b) => a - b);
15224
+ const quartile = sorted[Math.floor((sorted.length - 1) / 4)] ?? 0;
15225
+ if (!(quartile > 0)) return { fps: DEFAULT_DETECTION_FPS, source: "default" };
15226
+ return { fps: clampFps(Math.round(1e3 / quartile)), source: "stored-positions" };
15227
+ }
15228
+
15229
+ // src/session-replay/compare.ts
15230
+ var MATCH_IOU_MIN = 0.5;
15231
+ var RELAXED_IOU_MIN = 0.3;
15232
+ var MAX_INTERPOLATION_GAP_MS = 5e3;
15233
+ function iou(a, b) {
15234
+ const x1 = Math.max(a.x, b.x);
15235
+ const y1 = Math.max(a.y, b.y);
15236
+ const x2 = Math.min(a.x + a.w, b.x + b.w);
15237
+ const y2 = Math.min(a.y + a.h, b.y + b.h);
15238
+ const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
15239
+ if (inter === 0) return 0;
15240
+ const union2 = a.w * a.h + b.w * b.h - inter;
15241
+ return union2 > 0 ? inter / union2 : 0;
15242
+ }
15243
+ function lerp(a, b, f) {
15244
+ return a + (b - a) * f;
15245
+ }
15246
+ function entry(p, t, basis) {
15247
+ return { ...p, t, basis };
15248
+ }
15249
+ function trackAt(track, t, halfStepMs) {
15250
+ let prev;
15251
+ let next;
15252
+ let nearest;
15253
+ for (const p of track.positions) {
15254
+ if (p.t <= t) prev = p;
15255
+ if (p.t >= t && next === void 0) next = p;
15256
+ if (nearest === void 0 || Math.abs(p.t - t) < Math.abs(nearest.t - t)) nearest = p;
15647
15257
  }
15648
- const framePlan = planReplayFrames(input.source, input.fps, input.maxFrames);
15649
- if (framePlan.kind === "refused") {
15650
- deps.logger.warn("replay: run refused \u2014 empty window", { tags });
15651
- return { kind: "refused", reason: framePlan.reason };
15258
+ if (nearest !== void 0 && Math.abs(nearest.t - t) <= halfStepMs) {
15259
+ return entry(nearest, nearest.t, "exact");
15652
15260
  }
15653
- if (framePlan.truncated) {
15654
- deps.logger.warn("replay: frame plan truncated by maxFrames", {
15655
- tags,
15656
- meta: { maxFrames: input.maxFrames, planned: framePlan.instants.length }
15657
- });
15261
+ if (t < track.firstSeen || t > track.lastSeen || nearest === void 0) return null;
15262
+ if (prev !== void 0 && next !== void 0 && next.t - prev.t <= MAX_INTERPOLATION_GAP_MS) {
15263
+ const f = next.t === prev.t ? 0 : (t - prev.t) / (next.t - prev.t);
15264
+ return {
15265
+ trackId: track.trackId,
15266
+ className: track.className,
15267
+ t,
15268
+ box: {
15269
+ x: lerp(prev.box.x, next.box.x, f),
15270
+ y: lerp(prev.box.y, next.box.y, f),
15271
+ w: lerp(prev.box.w, next.box.w, f),
15272
+ h: lerp(prev.box.h, next.box.h, f)
15273
+ },
15274
+ score: prev.score !== null && next.score !== null ? lerp(prev.score, next.score, f) : null,
15275
+ basis: "interpolated"
15276
+ };
15658
15277
  }
15659
- const sourceFetch = await deps.fetchSourceBytes(input.source);
15660
- if (sourceFetch.kind === "refused") {
15661
- deps.logger.warn("replay: run refused \u2014 could not fetch source bytes", {
15662
- tags,
15663
- meta: { reason: sourceFetch.reason }
15278
+ return entry(nearest, t, "uncertain");
15279
+ }
15280
+ function referenceAt(tracks, t, halfStepMs) {
15281
+ const entries = [];
15282
+ let live = false;
15283
+ for (const track of tracks) {
15284
+ if (t >= track.firstSeen && t <= track.lastSeen) live = true;
15285
+ const e = trackAt(track, t, halfStepMs);
15286
+ if (e !== null) {
15287
+ entries.push(e);
15288
+ live = true;
15289
+ }
15290
+ }
15291
+ return { live, entries };
15292
+ }
15293
+ function sameClass(a, b) {
15294
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
15295
+ }
15296
+ function barFor(basis, iouMin) {
15297
+ if (basis === "exact") return iouMin;
15298
+ if (basis === "interpolated") return Math.min(iouMin, RELAXED_IOU_MIN);
15299
+ return 0;
15300
+ }
15301
+ function pairUp(replay, stored, accept, bar) {
15302
+ const candidates = [];
15303
+ replay.forEach((r, ri) => {
15304
+ stored.forEach((s, si) => {
15305
+ if (!accept(r, s)) return;
15306
+ const v = iou(r.box, s.box);
15307
+ if (v >= bar(s)) candidates.push({ r: ri, s: si, iou: v });
15664
15308
  });
15665
- return { kind: "refused", reason: sourceFetch.reason };
15666
- }
15667
- const sourceBytes = sourceFetch.bytes;
15668
- const first = framePlan.instants[0] ?? 0;
15669
- const last = framePlan.instants[framePlan.instants.length - 1] ?? first;
15670
- const startSec = (first - sourceFetch.startMs) / 1e3;
15671
- const durationSec = Math.max(1 / input.fps, (last - first) / 1e3 + 1 / input.fps);
15672
- const args = buildReplaySequenceArgs({
15673
- startSec,
15674
- durationSec,
15675
- fps: input.fps,
15676
- maxWidth: input.maxWidth
15677
15309
  });
15678
- const mjpeg = await deps.decoder.decodeSequence(args, sourceBytes);
15679
- const jpegFrames = splitMjpegStream(mjpeg);
15680
- const frames = [];
15681
- for (let i = 0; i < framePlan.instants.length; i++) {
15682
- const timestamp = framePlan.instants[i];
15683
- const image = jpegFrames[i];
15684
- if (timestamp === void 0 || image === void 0) continue;
15685
- frames.push({ timestamp, image });
15686
- }
15687
- if (frames.length < framePlan.instants.length) {
15688
- deps.logger.warn("replay: decode produced fewer frames than planned", {
15689
- tags,
15690
- meta: { planned: framePlan.instants.length, decoded: frames.length }
15310
+ const ranked = candidates.toSorted((a, b) => b.iou - a.iou);
15311
+ const usedR = /* @__PURE__ */ new Set();
15312
+ const usedS = /* @__PURE__ */ new Set();
15313
+ const pairs = [];
15314
+ for (const c of ranked) {
15315
+ if (usedR.has(c.r) || usedS.has(c.s)) continue;
15316
+ const r = replay[c.r];
15317
+ const s = stored[c.s];
15318
+ if (r === void 0 || s === void 0) continue;
15319
+ usedR.add(c.r);
15320
+ usedS.add(c.s);
15321
+ pairs.push({
15322
+ replay: r,
15323
+ stored: s,
15324
+ iou: c.iou,
15325
+ scoreDelta: s.score === null ? null : r.score - s.score
15691
15326
  });
15692
15327
  }
15693
- const verdict = await runReplay(
15694
- {
15695
- deviceId: input.deviceId,
15696
- nodeId: input.nodeId,
15697
- runPipeline: async (call) => deps.api.pipelineExecutor.runPipeline.mutate({
15698
- nodeId: call.nodeId,
15699
- steps: [...plan.steps],
15700
- // Copy into a fresh `Uint8Array<ArrayBuffer>` — `mjpeg-split.ts`
15701
- // hands back a `Buffer` view (`Uint8Array<ArrayBufferLike>`), and
15702
- // the generated cap input is typed against the narrower
15703
- // `ArrayBuffer` backing.
15704
- image: new Uint8Array(call.image),
15705
- deviceId: input.deviceId,
15706
- ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
15707
- }),
15708
- // THE cross-addon call this whole track exists for: the FrameProcessor
15709
- // pass lives in `addon-post-analysis`, reached only through this cap
15710
- // method — see the file header.
15711
- runTracking: async (tracked) => deps.api.pipelineAnalytics.runReplayFrameProcessor.mutate({
15712
- deviceId: input.deviceId,
15713
- source: "pipeline",
15714
- frames: [...tracked]
15715
- }),
15716
- logger: deps.logger
15717
- },
15718
- frames
15328
+ return {
15329
+ pairs,
15330
+ restReplay: replay.filter((_r, i) => !usedR.has(i)),
15331
+ restStored: stored.filter((_s, i) => !usedS.has(i))
15332
+ };
15333
+ }
15334
+ function compareFrame(t, replay, reference, iouMin = MATCH_IOU_MIN) {
15335
+ const same = pairUp(
15336
+ replay,
15337
+ reference.entries,
15338
+ (r, s) => sameClass(r.className, s.className),
15339
+ (s) => barFor(s.basis, iouMin)
15340
+ );
15341
+ const changed = pairUp(
15342
+ same.restReplay,
15343
+ same.restStored,
15344
+ (_r, s) => s.basis !== "uncertain",
15345
+ (s) => barFor(s.basis, iouMin)
15719
15346
  );
15720
- const now = deps.now ?? Date.now;
15721
- const newId = deps.newId ?? (() => `replay-${now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
15722
- const replayRunId = newId();
15723
- const record2 = {
15724
- id: replayRunId,
15725
- deviceId: input.deviceId,
15726
- nodeId: input.nodeId,
15727
- clipId: clipIdOf(input.source),
15728
- createdAtMs: now(),
15729
- source: input.source,
15730
- plan,
15731
- verdict
15732
- };
15733
- await deps.store.save(record2);
15734
- return { kind: "ran", replayRunId, verdict };
15347
+ return {
15348
+ t,
15349
+ referenced: reference.live,
15350
+ matched: same.pairs,
15351
+ classChanged: changed.pairs,
15352
+ added: reference.live ? changed.restReplay : [],
15353
+ unreferenced: reference.live ? [] : changed.restReplay,
15354
+ missed: changed.restStored
15355
+ };
15356
+ }
15357
+ function percentile(sorted, q) {
15358
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1));
15359
+ return sorted[idx] ?? 0;
15360
+ }
15361
+ function summarizeReplay(frames, inferenceMs) {
15362
+ const perClass = /* @__PURE__ */ new Map();
15363
+ const bump = (cls, field) => {
15364
+ const key = cls.trim().toLowerCase();
15365
+ const e = perClass.get(key) ?? {
15366
+ replay: 0,
15367
+ stored: 0,
15368
+ matched: 0,
15369
+ added: 0,
15370
+ missed: 0,
15371
+ unreferenced: 0,
15372
+ interpolated: 0
15373
+ };
15374
+ e[field] += 1;
15375
+ perClass.set(key, e);
15376
+ };
15377
+ const deltas = [];
15378
+ let matched = 0;
15379
+ let classChanged = 0;
15380
+ let added = 0;
15381
+ let missed = 0;
15382
+ let unreferenced = 0;
15383
+ let interpolated = 0;
15384
+ const noteBasis = (s) => {
15385
+ if (s.basis === "exact") return;
15386
+ interpolated += 1;
15387
+ bump(s.className, "interpolated");
15388
+ };
15389
+ for (const f of frames) {
15390
+ for (const p of f.matched) {
15391
+ matched += 1;
15392
+ bump(p.replay.className, "replay");
15393
+ bump(p.stored.className, "stored");
15394
+ bump(p.replay.className, "matched");
15395
+ noteBasis(p.stored);
15396
+ if (p.scoreDelta !== null) deltas.push(p.scoreDelta);
15397
+ }
15398
+ for (const p of f.classChanged) {
15399
+ classChanged += 1;
15400
+ bump(p.replay.className, "replay");
15401
+ bump(p.stored.className, "stored");
15402
+ noteBasis(p.stored);
15403
+ }
15404
+ for (const r of f.added) {
15405
+ added += 1;
15406
+ bump(r.className, "replay");
15407
+ bump(r.className, "added");
15408
+ }
15409
+ for (const r of f.unreferenced) {
15410
+ unreferenced += 1;
15411
+ bump(r.className, "replay");
15412
+ bump(r.className, "unreferenced");
15413
+ }
15414
+ for (const s of f.missed) {
15415
+ missed += 1;
15416
+ bump(s.className, "stored");
15417
+ bump(s.className, "missed");
15418
+ noteBasis(s);
15419
+ }
15420
+ }
15421
+ const sortedDeltas = deltas.toSorted((a, b) => a - b);
15422
+ const sortedInfer = [...inferenceMs].toSorted((a, b) => a - b);
15423
+ return {
15424
+ totals: { matched, classChanged, added, missed, unreferenced, interpolated },
15425
+ perClass: Object.fromEntries(
15426
+ [...perClass.entries()].toSorted(([a], [b]) => a.localeCompare(b))
15427
+ ),
15428
+ scoreDelta: sortedDeltas.length === 0 ? null : {
15429
+ mean: sortedDeltas.reduce((s, v) => s + v, 0) / sortedDeltas.length,
15430
+ p10: percentile(sortedDeltas, 0.1),
15431
+ p50: percentile(sortedDeltas, 0.5),
15432
+ p90: percentile(sortedDeltas, 0.9)
15433
+ },
15434
+ inferenceMs: sortedInfer.length === 0 ? null : {
15435
+ p50: percentile(sortedInfer, 0.5),
15436
+ p95: percentile(sortedInfer, 0.95),
15437
+ count: sortedInfer.length
15438
+ }
15439
+ };
15735
15440
  }
15736
15441
 
15737
- // src/replay/replay-diff.ts
15738
- function iou(a, b) {
15739
- const x1 = Math.max(a.x, b.x);
15740
- const y1 = Math.max(a.y, b.y);
15741
- const x2 = Math.min(a.x + a.w, b.x + b.w);
15742
- const y2 = Math.min(a.y + a.h, b.y + b.h);
15743
- const interW = Math.max(0, x2 - x1);
15744
- const interH = Math.max(0, y2 - y1);
15745
- const inter = interW * interH;
15746
- if (inter <= 0) return 0;
15747
- const union2 = a.w * a.h + b.w * b.h - inter;
15748
- return union2 <= 0 ? 0 : inter / union2;
15749
- }
15750
- function windowsOverlap(a, b) {
15751
- return a.firstSeenMs <= b.lastSeenMs && b.firstSeenMs <= a.lastSeenMs;
15752
- }
15753
- var TRACK_PAIR_MIN_IOU = 0.3;
15754
- function diffTracks(a, b) {
15755
- const bMatched = /* @__PURE__ */ new Set();
15756
- const lost = [];
15757
- for (const ta of a) {
15758
- let bestIdx = -1;
15759
- let bestIou = 0;
15760
- for (let i = 0; i < b.length; i++) {
15761
- if (bMatched.has(i)) continue;
15762
- const tb = b[i];
15763
- if (tb === void 0 || tb.className !== ta.className) continue;
15764
- if (!windowsOverlap(ta, tb)) continue;
15765
- const score = iou(ta.bbox, tb.bbox);
15766
- if (score >= TRACK_PAIR_MIN_IOU && score > bestIou) {
15767
- bestIou = score;
15768
- bestIdx = i;
15769
- }
15770
- }
15771
- if (bestIdx === -1) lost.push(ta);
15772
- else bMatched.add(bestIdx);
15773
- }
15774
- const gained = b.filter((_, i) => !bMatched.has(i));
15775
- return { gained, lost };
15776
- }
15777
- function diffKnobs(a, b) {
15778
- const stepIds = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
15779
- const changed = [];
15780
- for (const stepId of stepIds) {
15781
- const aStep = a[stepId] ?? {};
15782
- const bStep = b[stepId] ?? {};
15783
- const keys = /* @__PURE__ */ new Set([...Object.keys(aStep), ...Object.keys(bStep)]);
15784
- for (const key of keys) {
15785
- const from = aStep[key];
15786
- const to = bStep[key];
15787
- if (from !== to) changed.push({ stepId, key, from, to });
15788
- }
15789
- }
15790
- return changed;
15791
- }
15792
- function diffPromoted(discardedA, tracksB) {
15793
- const promoted = [];
15794
- for (const d of discardedA) {
15795
- const nowTracked = tracksB.some((t) => t.className === d.macroClass);
15796
- if (nowTracked)
15797
- promoted.push({ macroClass: d.macroClass, score: d.score, wasDiscardedBy: d.reason });
15798
- }
15799
- return promoted;
15800
- }
15801
- function diffReplayRuns(a, b) {
15802
- if (a.clipId !== b.clipId) return { kind: "incomparable", reason: "different-input" };
15803
- if (a.nodeId !== b.nodeId) return { kind: "incomparable", reason: "different-node" };
15804
- return {
15805
- kind: "compared",
15806
- changedKnobs: diffKnobs(a.settings, b.settings),
15807
- tracks: diffTracks(a.tracks, b.tracks),
15808
- promoted: diffPromoted(a.discarded, b.tracks)
15442
+ // src/session-replay/frame-plan.ts
15443
+ var REPLAY_CHUNK_MS = 1e4;
15444
+ function planReplayFrames(window, fps, maxFrames, chunkMs = REPLAY_CHUNK_MS) {
15445
+ const stepMs = 1e3 / fps;
15446
+ const instants = [];
15447
+ let truncated = false;
15448
+ for (let k = 0; ; k++) {
15449
+ const t = window.startMs + Math.round(k * stepMs);
15450
+ if (t >= window.endMs) break;
15451
+ if (instants.length >= maxFrames) {
15452
+ truncated = true;
15453
+ break;
15454
+ }
15455
+ instants.push(t);
15456
+ }
15457
+ const chunks = [];
15458
+ let current = [];
15459
+ let chunkStart = window.startMs;
15460
+ const flush = (toMs) => {
15461
+ if (current.length > 0) chunks.push({ fromMs: chunkStart, toMs, instants: current });
15462
+ current = [];
15809
15463
  };
15464
+ for (const t of instants) {
15465
+ while (t >= chunkStart + chunkMs) {
15466
+ flush(chunkStart + chunkMs);
15467
+ chunkStart += chunkMs;
15468
+ }
15469
+ current.push(t);
15470
+ }
15471
+ const last = instants[instants.length - 1];
15472
+ flush(
15473
+ truncated && last !== void 0 ? last + Math.round(stepMs) : Math.min(window.endMs, chunkStart + chunkMs)
15474
+ );
15475
+ return { chunks, planned: instants.length, truncated };
15810
15476
  }
15811
15477
 
15812
- // src/throughput/sustained-runner.ts
15813
- async function runSustainedLoop(opts) {
15814
- const concurrency = Math.max(1, Math.floor(opts.concurrency));
15815
- const start = opts.now();
15816
- const deadline = start + Math.max(0, opts.durationMs);
15817
- const callMsList = [];
15818
- let totalFrames = 0;
15819
- let totalBatches = 0;
15820
- let totalDetections = 0;
15821
- let detectionSamples = 0;
15822
- let errors = 0;
15823
- const worker = async () => {
15824
- while (!opts.shouldStop() && opts.now() < deadline) {
15825
- const t0 = opts.now();
15826
- try {
15827
- const out = await opts.dispatchBatch();
15828
- const t1 = opts.now();
15829
- callMsList.push(t1 - t0);
15830
- totalBatches += 1;
15831
- totalFrames += out.frameCount;
15832
- for (const c of out.detectionCounts) {
15833
- totalDetections += c;
15834
- detectionSamples += 1;
15835
- }
15836
- } catch {
15837
- errors += 1;
15838
- }
15478
+ // src/session-replay/replay-steps.ts
15479
+ var REPLAY_ROOT_STEP = "object-detection";
15480
+ function selectRootReplayStep(tree, requestedModelId) {
15481
+ const root = tree.steps.find((s) => s.addonId === REPLAY_ROOT_STEP);
15482
+ if (root === void 0 || !root.enabled) {
15483
+ return {
15484
+ kind: "refused",
15485
+ reason: "no-root-step",
15486
+ detail: `the camera does not run ${REPLAY_ROOT_STEP} as an enabled root step`
15487
+ };
15488
+ }
15489
+ const cameraModelId = root.modelId ?? null;
15490
+ const modelId = requestedModelId ?? cameraModelId;
15491
+ if (modelId === null || modelId.length === 0) {
15492
+ return {
15493
+ kind: "refused",
15494
+ reason: "model-unpinned",
15495
+ detail: `the camera's ${REPLAY_ROOT_STEP} names no model (it runs the node's format default); pick the model to replay explicitly`
15496
+ };
15497
+ }
15498
+ return {
15499
+ kind: "ok",
15500
+ cameraModelId,
15501
+ step: {
15502
+ addonId: root.addonId,
15503
+ modelId,
15504
+ enabled: true,
15505
+ ...root.settings !== void 0 ? { settings: root.settings } : {},
15506
+ children: []
15839
15507
  }
15840
15508
  };
15841
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
15842
- const wallMs = opts.now() - start;
15509
+ }
15510
+
15511
+ // src/session-replay/session-window.ts
15512
+ var SESSION_PREROLL_MS = 5e3;
15513
+ var SESSION_TAIL_MS = 3e4;
15514
+ var SESSION_MAX_WINDOW_MS = 10 * 6e4;
15515
+ function motionSessionWindow(session, camera = {}, opts = {}) {
15516
+ if (session.durationMs === null) {
15517
+ return {
15518
+ kind: "refused",
15519
+ reason: "session-open",
15520
+ detail: `motion session ${session.id} is still open \u2014 its end is not known yet`
15521
+ };
15522
+ }
15523
+ if (session.durationMs === void 0) {
15524
+ return {
15525
+ kind: "refused",
15526
+ reason: "session-length-unknown",
15527
+ detail: `motion session ${session.id} predates the episode duration column \u2014 its length is unknown`
15528
+ };
15529
+ }
15530
+ const motionEndMs = session.timestamp + session.durationMs;
15531
+ const prerollSource = camera.preRollInferenceEnabled === false ? "camera" : "default";
15532
+ const prerollMs = camera.preRollInferenceEnabled === false ? 0 : SESSION_PREROLL_MS;
15533
+ const tailSource = camera.motionCooldownMs !== void 0 ? "camera" : "default";
15534
+ const tailMs = camera.motionCooldownMs ?? SESSION_TAIL_MS;
15535
+ const startMs = session.timestamp - prerollMs;
15536
+ const endMs = motionEndMs + tailMs;
15537
+ const maxWindowMs = opts.maxWindowMs ?? SESSION_MAX_WINDOW_MS;
15538
+ if (endMs - startMs > maxWindowMs) {
15539
+ return {
15540
+ kind: "refused",
15541
+ reason: "window-too-long",
15542
+ detail: `motion session ${session.id} spans ${Math.round((endMs - startMs) / 1e3)} s \u2014 longer than the ${Math.round(maxWindowMs / 1e3)} s a replay reads`
15543
+ };
15544
+ }
15843
15545
  return {
15844
- callMsList,
15845
- totalFrames,
15846
- totalBatches,
15847
- totalDetections,
15848
- detectionSamples,
15849
- errors,
15850
- wallMs,
15851
- aborted: opts.shouldStop()
15546
+ kind: "window",
15547
+ motionEventId: session.id,
15548
+ motionStartMs: session.timestamp,
15549
+ motionEndMs,
15550
+ startMs,
15551
+ endMs,
15552
+ prerollSource,
15553
+ tailSource
15852
15554
  };
15853
15555
  }
15854
15556
 
15855
- // src/throughput/throughput-stats.ts
15856
- var EMPTY_LATENCY = { p50: 0, p95: 0, p99: 0, mean: 0, min: 0, max: 0 };
15857
- function percentile(sortedAsc, p) {
15858
- const n = sortedAsc.length;
15859
- if (n === 0) return 0;
15860
- const clamped = p < 0 ? 0 : p > 1 ? 1 : p;
15861
- if (clamped <= 0) return sortedAsc[0] ?? 0;
15862
- if (clamped >= 1) return sortedAsc[n - 1] ?? 0;
15863
- const rank = Math.ceil(clamped * n);
15864
- const idx = Math.min(n - 1, Math.max(0, rank - 1));
15865
- return sortedAsc[idx] ?? 0;
15866
- }
15867
- function computeLatencyStats(samplesMs) {
15868
- if (samplesMs.length === 0) return EMPTY_LATENCY;
15869
- const sorted = [...samplesMs].sort((a, b) => a - b);
15870
- const sum = sorted.reduce((acc, x) => acc + x, 0);
15557
+ // src/session-replay/stored-reference.ts
15558
+ var UNKNOWN_MODEL = "unknown";
15559
+ function buildStoredReference(tracks, window) {
15560
+ const refs = [];
15561
+ const models = /* @__PURE__ */ new Map();
15562
+ const sizes = /* @__PURE__ */ new Map();
15563
+ const timesByTrack = [];
15564
+ let recoveryExcluded = 0;
15565
+ let withoutGeometry = 0;
15566
+ let trackCount = 0;
15567
+ for (const track of tracks) {
15568
+ const inWindow = track.positions.filter(
15569
+ (p) => p.timestamp >= window.startMs && p.timestamp < window.endMs
15570
+ );
15571
+ const aliveInWindow = track.firstSeen < window.endMs && track.lastSeen >= window.startMs;
15572
+ if (inWindow.length === 0 && !aliveInWindow) continue;
15573
+ trackCount += 1;
15574
+ const model = track.modelId ?? UNKNOWN_MODEL;
15575
+ models.set(model, (models.get(model) ?? 0) + 1);
15576
+ const fw = track.frameWidth;
15577
+ const fh = track.frameHeight;
15578
+ const hasGeometry = fw !== void 0 && fh !== void 0 && fw > 0 && fh > 0;
15579
+ if (hasGeometry) {
15580
+ const key = `${fw}x${fh}`;
15581
+ const entry2 = sizes.get(key) ?? { width: fw, height: fh, count: 0 };
15582
+ entry2.count += 1;
15583
+ sizes.set(key, entry2);
15584
+ }
15585
+ const times = [];
15586
+ const positions = [];
15587
+ for (const p of inWindow) {
15588
+ if (p.source === "recovery") {
15589
+ recoveryExcluded += 1;
15590
+ continue;
15591
+ }
15592
+ times.push(p.timestamp);
15593
+ if (!hasGeometry) {
15594
+ withoutGeometry += 1;
15595
+ continue;
15596
+ }
15597
+ positions.push({
15598
+ trackId: track.trackId,
15599
+ className: track.className,
15600
+ t: p.timestamp,
15601
+ box: { x: p.bbox.x / fw, y: p.bbox.y / fh, w: p.bbox.w / fw, h: p.bbox.h / fh },
15602
+ score: p.score ?? null
15603
+ });
15604
+ }
15605
+ timesByTrack.push(times.toSorted((a, b) => a - b));
15606
+ if (hasGeometry) {
15607
+ const sorted = positions.toSorted((a, b) => a.t - b.t);
15608
+ refs.push({
15609
+ trackId: track.trackId,
15610
+ className: track.className,
15611
+ firstSeen: Math.min(track.firstSeen, sorted[0]?.t ?? track.firstSeen),
15612
+ lastSeen: Math.max(track.lastSeen, sorted[sorted.length - 1]?.t ?? track.lastSeen),
15613
+ positions: sorted
15614
+ });
15615
+ }
15616
+ }
15617
+ const commonest = [...sizes.values()].toSorted((a, b) => b.count - a.count)[0];
15871
15618
  return {
15872
- p50: percentile(sorted, 0.5),
15873
- p95: percentile(sorted, 0.95),
15874
- p99: percentile(sorted, 0.99),
15875
- mean: sum / sorted.length,
15876
- min: sorted[0] ?? 0,
15877
- max: sorted[sorted.length - 1] ?? 0
15619
+ tracks: refs,
15620
+ trackCount,
15621
+ recoveryExcluded,
15622
+ withoutGeometry,
15623
+ models: Object.fromEntries(models),
15624
+ frameSize: commonest ? { width: commonest.width, height: commonest.height } : null,
15625
+ positionTimesByTrack: timesByTrack
15878
15626
  };
15879
15627
  }
15880
- function computeFps(totalFrames, wallMs) {
15881
- if (wallMs <= 0) return 0;
15882
- return totalFrames * 1e3 / wallMs;
15628
+
15629
+ // src/session-replay/run-session-replay.ts
15630
+ var TRACK_LOOKBACK_MS = 12e4;
15631
+ var DEFAULT_REPLAY_WIDTH = 1280;
15632
+ var SUPPRESSED_BIRTH_PROXIMITY_MS = 2e3;
15633
+ var MAX_REPORTED_FAILURES = 20;
15634
+ var MAX_CONSECUTIVE_FAILURES = 10;
15635
+ var STORED_TRACKS_READ_LIMIT = 1e3;
15636
+ var REAL_PACE = {
15637
+ now: () => Date.now(),
15638
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
15639
+ };
15640
+ var PIN_REFUSALS = ["model-not-servable", "device-not-servable"];
15641
+ function pinRefusalOf(err) {
15642
+ const message = err instanceof Error ? err.message : String(err);
15643
+ for (const reason of PIN_REFUSALS) {
15644
+ if (message.includes(`${reason}:`)) return { reason, detail: message };
15645
+ }
15646
+ return null;
15647
+ }
15648
+ function toReplayBoxes(frame) {
15649
+ const w = frame.width;
15650
+ const h = frame.height;
15651
+ if (!(w > 0 && h > 0)) return [];
15652
+ return frame.detections.filter((d) => d.kind === "first-level").map((d) => ({
15653
+ className: d.macroClass,
15654
+ score: d.score,
15655
+ box: { x: d.bbox.x / w, y: d.bbox.y / h, w: d.bbox.width / w, h: d.bbox.height / h }
15656
+ }));
15883
15657
  }
15884
- function computeDetsPerSec(totalDetections, wallMs) {
15885
- if (wallMs <= 0) return 0;
15886
- return totalDetections * 1e3 / wallMs;
15658
+ function sameClass2(a, b) {
15659
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
15887
15660
  }
15888
- function computeAvgDetections(totalDetections, sampledFrames) {
15889
- if (sampledFrames <= 0) return 0;
15890
- return totalDetections / sampledFrames;
15661
+ async function runSessionReplay(deps, request, run) {
15662
+ const tags = { deviceId: request.deviceId };
15663
+ const refuse = (reason, detail) => {
15664
+ deps.logger.warn("session replay refused", {
15665
+ tags,
15666
+ meta: { reason, detail, motionEventId: request.motionEventId, nodeId: request.nodeId }
15667
+ });
15668
+ return { kind: "refused", reason, detail };
15669
+ };
15670
+ run.report({ phase: "preparing", framesPlanned: 0, framesDone: 0 });
15671
+ const session = await deps.findMotionSession(
15672
+ request.deviceId,
15673
+ request.motionEventId,
15674
+ request.motionStartMs
15675
+ );
15676
+ if (session === null) {
15677
+ return refuse(
15678
+ "session-not-found",
15679
+ `no stored motion session ${request.motionEventId} for camera ${request.deviceId}`
15680
+ );
15681
+ }
15682
+ const window = motionSessionWindow(session, await deps.cameraSessionSettings(request.deviceId));
15683
+ if (window.kind === "refused") return refuse(window.reason, window.detail);
15684
+ const tree = await deps.resolveCameraTree(request.deviceId, request.nodeId);
15685
+ const selection = selectRootReplayStep(tree, request.modelId);
15686
+ if (selection.kind === "refused") return refuse(selection.reason, selection.detail);
15687
+ const step = selection.step;
15688
+ const modelId = step.modelId ?? "";
15689
+ const validation = await deps.validate([step], request.nodeId, request.deviceKey);
15690
+ const pool = `${request.nodeId}/${request.deviceKey ?? "node default"}`;
15691
+ if (!validation.ok) {
15692
+ return refuse(
15693
+ "device-not-servable",
15694
+ `${pool} cannot run ${step.addonId}: ${validation.issues.map((i) => i.detail).join("; ")}`
15695
+ );
15696
+ }
15697
+ const substitution = validation.substitutions[0];
15698
+ if (substitution !== void 0) {
15699
+ return refuse(
15700
+ "model-not-servable",
15701
+ `${pool} would run ${substitution.running} instead of ${substitution.chosen} (${substitution.format})`
15702
+ );
15703
+ }
15704
+ if (!await deps.isRecording(request.deviceId)) {
15705
+ return refuse("no-recording", `camera ${request.deviceId} does not have recording enabled`);
15706
+ }
15707
+ const [tracks, births] = await Promise.all([
15708
+ deps.listStoredTracks(request.deviceId, window.startMs - TRACK_LOOKBACK_MS, window.endMs),
15709
+ deps.listBirthDecisions(request.deviceId, window.startMs)
15710
+ ]);
15711
+ const reference = buildStoredReference(tracks, window);
15712
+ const readTruncated = tracks.length >= STORED_TRACKS_READ_LIMIT;
15713
+ if (readTruncated) {
15714
+ deps.logger.warn(
15715
+ "session replay: stored-track read hit its limit \u2014 the reference may be incomplete",
15716
+ {
15717
+ tags,
15718
+ meta: { limit: STORED_TRACKS_READ_LIMIT, motionEventId: request.motionEventId }
15719
+ }
15720
+ );
15721
+ }
15722
+ const cadence = resolveReplayCadence(request.fps, reference.positionTimesByTrack);
15723
+ const frameSize = reference.frameSize !== null ? { width: reference.frameSize.width, source: "stored-tracks" } : { width: DEFAULT_REPLAY_WIDTH, source: "default" };
15724
+ const windowBirths = births.filter((b) => b.at >= window.startMs && b.at < window.endMs);
15725
+ const suppressedBirths = windowBirths.filter((b) => b.verdict === "suppressed");
15726
+ const liveVerdicts = {};
15727
+ for (const b of windowBirths) liveVerdicts[b.verdict] = (liveVerdicts[b.verdict] ?? 0) + 1;
15728
+ const plan = planReplayFrames(window, cadence.fps, request.maxFrames);
15729
+ if (plan.truncated) {
15730
+ deps.logger.warn("session replay: frame plan truncated by maxFrames", {
15731
+ tags,
15732
+ meta: { maxFrames: request.maxFrames, motionEventId: request.motionEventId }
15733
+ });
15734
+ }
15735
+ const stepMs = 1e3 / cadence.fps;
15736
+ const halfStepMs = stepMs / 2;
15737
+ const timeline = [];
15738
+ const comparisons = [];
15739
+ const inferenceMs = [];
15740
+ const failures = [];
15741
+ let decoded = 0;
15742
+ let ran = 0;
15743
+ let failed = 0;
15744
+ let consecutiveFailures = 0;
15745
+ let lastCallAt = null;
15746
+ const pace = deps.pace ?? REAL_PACE;
15747
+ let withoutFootage = 0;
15748
+ let notDecoded = 0;
15749
+ let truncatedBySafetyCap = false;
15750
+ let nearSuppressedBirth = 0;
15751
+ let positionsCompared = 0;
15752
+ let partsRead = 0;
15753
+ const progress = () => run.report({ phase: "replaying", framesPlanned: plan.planned, framesDone: ran + failed });
15754
+ progress();
15755
+ for (const chunk of plan.chunks) {
15756
+ if (run.signal.aborted) break;
15757
+ const recording = await deps.readRecording(request.deviceId, chunk.instants, stepMs);
15758
+ if (recording.kind === "refused") {
15759
+ withoutFootage += chunk.instants.length;
15760
+ deps.logger.warn("session replay: no footage for a stretch of the session", {
15761
+ tags,
15762
+ meta: { fromMs: chunk.fromMs, toMs: chunk.toMs, detail: recording.detail }
15763
+ });
15764
+ continue;
15765
+ }
15766
+ withoutFootage += recording.instantsWithoutFootage;
15767
+ partsRead += recording.parts.length;
15768
+ if (recording.truncatedBySafetyCap) truncatedBySafetyCap = true;
15769
+ for (const part of recording.parts) {
15770
+ if (run.signal.aborted) break;
15771
+ const first = part.instants[0];
15772
+ if (first === void 0) continue;
15773
+ const frames = await deps.decode(part.bytes, {
15774
+ startSec: (first - part.gopStartMs) / 1e3,
15775
+ durationSec: part.instants.length / cadence.fps,
15776
+ fps: cadence.fps,
15777
+ maxWidth: frameSize.width
15778
+ });
15779
+ decoded += Math.min(frames.length, part.instants.length);
15780
+ if (frames.length < part.instants.length) {
15781
+ notDecoded += part.instants.length - frames.length;
15782
+ deps.logger.warn("session replay: decode produced fewer frames than planned", {
15783
+ tags,
15784
+ meta: { planned: part.instants.length, decoded: frames.length, fromMs: first }
15785
+ });
15786
+ }
15787
+ for (let k = 0; k < part.instants.length; k++) {
15788
+ if (run.signal.aborted) break;
15789
+ const t = part.instants[k];
15790
+ const image = frames[k];
15791
+ if (t === void 0 || image === void 0) break;
15792
+ if (lastCallAt !== null) {
15793
+ const wait = lastCallAt + stepMs - pace.now();
15794
+ if (wait > 0) await pace.sleep(wait);
15795
+ }
15796
+ lastCallAt = pace.now();
15797
+ let result;
15798
+ try {
15799
+ result = await deps.runRootDetector({
15800
+ nodeId: request.nodeId,
15801
+ ...request.deviceKey !== void 0 ? { deviceKey: request.deviceKey } : {},
15802
+ steps: [step],
15803
+ image,
15804
+ deviceId: request.deviceId,
15805
+ t
15806
+ });
15807
+ } catch (err) {
15808
+ const pinRefusal = pinRefusalOf(err);
15809
+ if (pinRefusal !== null) return refuse(pinRefusal.reason, pinRefusal.detail);
15810
+ failed += 1;
15811
+ consecutiveFailures += 1;
15812
+ const error51 = err instanceof Error ? err.message : String(err);
15813
+ if (failures.length < MAX_REPORTED_FAILURES) failures.push({ t, error: error51 });
15814
+ deps.logger.warn("session replay: frame failed \u2014 counted, the run continues", {
15815
+ tags,
15816
+ meta: { t, error: error51, consecutiveFailures }
15817
+ });
15818
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
15819
+ return refuse(
15820
+ "pool-failing",
15821
+ `${pool} failed ${consecutiveFailures} frames in a row (last: ${error51}) \u2014 stopped rather than keep loading a live pool`
15822
+ );
15823
+ }
15824
+ progress();
15825
+ continue;
15826
+ }
15827
+ consecutiveFailures = 0;
15828
+ if (result.rootModelId !== modelId) {
15829
+ return refuse(
15830
+ "model-not-servable",
15831
+ result.rootModelId === void 0 ? `${pool} does not report the model it ran (an executor older than D646) \u2014 the pin cannot be verified` : `${pool} reported ${result.rootModelId} for a frame pinned to ${modelId}`
15832
+ );
15833
+ }
15834
+ ran += 1;
15835
+ const frameInferMs = result.debug?.totalInferenceMs;
15836
+ if (frameInferMs !== void 0) inferenceMs.push(frameInferMs);
15837
+ const replayBoxes = toReplayBoxes(result);
15838
+ const instantRef = referenceAt(reference.tracks, t, halfStepMs);
15839
+ positionsCompared += instantRef.entries.length;
15840
+ const cmp = compareFrame(t, replayBoxes, instantRef);
15841
+ comparisons.push(cmp);
15842
+ for (const unmatched of [...cmp.added, ...cmp.unreferenced]) {
15843
+ if (suppressedBirths.some(
15844
+ (b) => sameClass2(b.className, unmatched.className) && Math.abs(b.at - t) <= SUPPRESSED_BIRTH_PROXIMITY_MS
15845
+ )) {
15846
+ nearSuppressedBirth += 1;
15847
+ }
15848
+ }
15849
+ timeline.push({
15850
+ t,
15851
+ referenced: cmp.referenced,
15852
+ inferenceMs: frameInferMs ?? null,
15853
+ replay: [
15854
+ ...cmp.matched.map((p) => ({ ...p.replay, outcome: "matched" })),
15855
+ ...cmp.classChanged.map((p) => ({ ...p.replay, outcome: "class-changed" })),
15856
+ ...cmp.added.map((r) => ({ ...r, outcome: "added" })),
15857
+ ...cmp.unreferenced.map((r) => ({ ...r, outcome: "unreferenced" }))
15858
+ ],
15859
+ stored: [
15860
+ ...cmp.matched.map((p) => ({ ...p.stored, outcome: "matched" })),
15861
+ ...cmp.classChanged.map((p) => ({ ...p.stored, outcome: "class-changed" })),
15862
+ ...cmp.missed.map((s) => ({ ...s, outcome: "missed" }))
15863
+ ]
15864
+ });
15865
+ progress();
15866
+ }
15867
+ }
15868
+ }
15869
+ const aborted2 = run.signal.aborted;
15870
+ if (partsRead === 0 && !aborted2) {
15871
+ return refuse(
15872
+ "no-footage",
15873
+ `no recording covers any of the ${plan.planned} planned instants of motion session ${request.motionEventId}`
15874
+ );
15875
+ }
15876
+ if (aborted2) {
15877
+ deps.logger.info("session replay aborted \u2014 the report covers the frames that ran", {
15878
+ tags,
15879
+ meta: { ran, planned: plan.planned }
15880
+ });
15881
+ }
15882
+ run.report({ phase: "done", framesPlanned: plan.planned, framesDone: ran + failed });
15883
+ return {
15884
+ kind: "ran",
15885
+ report: {
15886
+ deviceId: request.deviceId,
15887
+ motionEventId: request.motionEventId,
15888
+ window: {
15889
+ startMs: window.startMs,
15890
+ endMs: window.endMs,
15891
+ motionStartMs: window.motionStartMs,
15892
+ motionEndMs: window.motionEndMs,
15893
+ prerollSource: window.prerollSource,
15894
+ tailSource: window.tailSource
15895
+ },
15896
+ pin: {
15897
+ nodeId: request.nodeId,
15898
+ deviceKey: request.deviceKey ?? null,
15899
+ modelId,
15900
+ cameraModelId: selection.cameraModelId
15901
+ },
15902
+ rootStep: step,
15903
+ cadence,
15904
+ frameSize,
15905
+ frames: {
15906
+ planned: plan.planned,
15907
+ decoded,
15908
+ ran,
15909
+ failed,
15910
+ withoutFootage,
15911
+ notDecoded,
15912
+ truncated: plan.truncated,
15913
+ truncatedBySafetyCap,
15914
+ aborted: aborted2
15915
+ },
15916
+ reference: {
15917
+ tracks: reference.trackCount,
15918
+ positionsCompared,
15919
+ recoveryExcluded: reference.recoveryExcluded,
15920
+ withoutGeometry: reference.withoutGeometry,
15921
+ models: reference.models,
15922
+ read: { tracks: tracks.length, truncated: readTruncated }
15923
+ },
15924
+ confirmation: { liveVerdicts, nearSuppressedBirth },
15925
+ summary: summarizeReplay(comparisons, inferenceMs),
15926
+ timeline,
15927
+ failures
15928
+ }
15929
+ };
15930
+ }
15931
+
15932
+ // src/session-replay/camera-settings.ts
15933
+ var FieldSchema = external_exports.object({
15934
+ key: external_exports.string().optional(),
15935
+ value: external_exports.unknown().optional(),
15936
+ fields: external_exports.array(external_exports.unknown()).optional(),
15937
+ tabs: external_exports.array(external_exports.object({ fields: external_exports.array(external_exports.unknown()).optional() }).loose()).optional()
15938
+ }).loose();
15939
+ var FormSchema = external_exports.object({
15940
+ sections: external_exports.array(external_exports.object({ fields: external_exports.array(external_exports.unknown()) }).loose())
15941
+ });
15942
+ function collect(fields, out) {
15943
+ for (const raw of fields) {
15944
+ const field = FieldSchema.safeParse(raw);
15945
+ if (!field.success) continue;
15946
+ if (field.data.key !== void 0 && "value" in field.data)
15947
+ out.set(field.data.key, field.data.value);
15948
+ if (field.data.fields !== void 0) collect(field.data.fields, out);
15949
+ for (const tab of field.data.tabs ?? []) collect(tab.fields ?? [], out);
15950
+ }
15951
+ }
15952
+ function readCameraSessionSettings(raw) {
15953
+ const form = FormSchema.safeParse(raw);
15954
+ if (!form.success) return {};
15955
+ const values = /* @__PURE__ */ new Map();
15956
+ for (const section of form.data.sections) collect(section.fields, values);
15957
+ const cooldown = values.get("motionCooldownMs");
15958
+ const preRoll = values.get("preRollInferenceEnabled");
15959
+ return {
15960
+ ...typeof cooldown === "number" && Number.isFinite(cooldown) && cooldown >= 0 ? { motionCooldownMs: cooldown } : {},
15961
+ ...typeof preRoll === "boolean" ? { preRollInferenceEnabled: preRoll } : {}
15962
+ };
15891
15963
  }
15892
15964
 
15893
15965
  // src/benchmark-addon.ts
15894
15966
  var KEY_BENCHMARK_HISTORY = "benchmarkHistory";
15895
15967
  var MAX_HISTORY_ENTRIES = 100;
15896
15968
  var EMPTY_HISTORY_JSON = "[]";
15897
- var BENCH_RUN_TIMEOUT_MS = Math.max(
15898
- 1e4,
15899
- Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 12e4
15969
+ var BENCH_RUN_TIMEOUT_MS = Math.min(
15970
+ 55e3,
15971
+ Math.max(1e4, Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 55e3)
15900
15972
  );
15973
+ var REPLAY_RECORDING_PROFILE = "high";
15974
+ var REPLAY_BIRTHS_LIMIT = 1e3;
15975
+ var REPLAY_DECODE_TIMEOUT_FLOOR_MS = 15e3;
15976
+ var REPLAY_DECODE_TIMEOUT_PER_SEC_MS = 3e3;
15977
+ var MOTION_SESSIONS_DEFAULT_SPAN_MS = 24 * 60 * 60 * 1e3;
15901
15978
  async function withHardTimeout(promise2, ms, label) {
15902
15979
  let timer;
15903
15980
  const timeout = new Promise((_resolve, reject) => {
@@ -15914,60 +15991,35 @@ async function withHardTimeout(promise2, ms, label) {
15914
15991
  }
15915
15992
  }
15916
15993
  var HistoryArraySchema = external_exports.array(HistoryEntrySchema);
15917
- var FIXTURE_META = {
15918
- "camera-sim-360p.h264": {
15919
- codec: "h264",
15920
- resolutionLabel: "360p",
15921
- description: "Synthetic 360p H.264 \u2014 light workload"
15922
- },
15923
- "camera-sim-1080p.h264": {
15924
- codec: "h264",
15925
- resolutionLabel: "1080p",
15926
- description: "Synthetic 1080p H.264 \u2014 production-like"
15927
- },
15928
- "camera-sim-1440p-h265.hevc": {
15929
- codec: "h265",
15930
- resolutionLabel: "1440p",
15931
- description: "Synthetic 1440p H.265 \u2014 hardware decode stress"
15932
- }
15933
- };
15934
- function resolveFixturesDir() {
15935
- const here = fileURLToPath(import.meta.url);
15936
- const pkgRoot = path2.resolve(path2.dirname(here), "..");
15937
- return path2.join(pkgRoot, "assets", "fixtures");
15938
- }
15939
- var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
15994
+ var BenchmarkAddon = class extends BaseAddon {
15940
15995
  id = "benchmark";
15941
- /** Per-sessionId abort flags for in-flight decoder perf runs. */
15942
- decoderPerfAbortFlags = /* @__PURE__ */ new Map();
15943
- /** Per-sessionId abort flags for in-flight sustained-throughput runs. */
15944
- sustainedAbortFlags = /* @__PURE__ */ new Map();
15945
- /**
15946
- * In-memory cache of original JPEG bytes keyed by `frameId`. Populated by
15947
- * `cacheBenchFrame` (UI uploads JPEG once at bench start). Subsequent
15948
- * `runPipelineTest` calls ship only `frameId` — bench addon forwards the
15949
- * cached JPEG buffer as `image: Uint8Array` to pipelineExecutor.runPipeline
15950
- * (Moleculer MsgPack ships binary efficiently). Why JPEG and not raw RGB:
15951
- * cross-process MsgPack of a 2.7MB raw RGB frame (1280×720×3) is 13×
15952
- * heavier than the 200KB JPEG, and the pipeline addon's per-call sharp
15953
- * decode (~5ms) is cheaper than serialising the raw buffer twice.
15954
- * Production's broker→detection path is in-process (same group) so neither
15955
- * cost matters there.
15956
- */
15957
- frameCache = /* @__PURE__ */ new Map();
15958
- /** Periodic GC handle — clears expired frames every 60s. */
15959
- frameCacheSweeper = null;
15960
- /** The replay run collection — declared in `onInitialize`, before any
15961
- * `runReplayClip` call can save into it. See `replay/replay-store.ts`. */
15962
15996
  replayStore = null;
15963
- /** The replay's one ffmpeg spawn, bounded — see `replay/replay-decode.ts`. */
15997
+ /** The replay's ffmpeg spawns — the PINNED binary (`ctx.deps.ensureFfmpeg`, D540). */
15964
15998
  replayDecoder = new ReplayDecoder({
15965
- ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg"
15999
+ resolveFfmpegPath: () => this.ctx.deps.ensureFfmpeg()
15966
16000
  });
15967
- /** `replay:<StoredReplayRun>` — an in-memory index would drift from the
15968
- * store; this is a thin cache over `replayStore.list`, keyed for the
15969
- * diff action's two-id lookup. */
15970
- replayRunCache = /* @__PURE__ */ new Map();
16001
+ /**
16002
+ * Session replays, start-then-poll (D646). One at a time — a replay drives
16003
+ * one inference call after another on a node that also serves live
16004
+ * cameras — and the last few finished runs kept for a late poll.
16005
+ */
16006
+ replays = new BackgroundRunRegistry(
16007
+ {
16008
+ maxConcurrent: 1,
16009
+ keepFinished: 10,
16010
+ onSettled: (status) => {
16011
+ if (status.state === "failed") {
16012
+ this.ctx.logger.warn("session replay failed", {
16013
+ meta: { runId: status.runId, error: status.error }
16014
+ });
16015
+ return;
16016
+ }
16017
+ const outcome = status.result;
16018
+ if (outcome === null || outcome.kind !== "ran") return;
16019
+ void this.persistReplay(status.runId, outcome.report.pin.nodeId, outcome);
16020
+ }
16021
+ }
16022
+ );
15971
16023
  pages = [
15972
16024
  {
15973
16025
  id: "benchmark",
@@ -15990,99 +16042,50 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
15990
16042
  };
15991
16043
  await ReplayStore.declare(this.api.settingsStore);
15992
16044
  this.replayStore = new ReplayStore(this.api.settingsStore);
16045
+ await this.pruneReplays();
15993
16046
  return {
15994
16047
  providers: [{ capability: addonPagesSourceCapability, provider }],
15995
16048
  customActions: benchmarkActions,
15996
16049
  actionHandlers: {
15997
16050
  runPipelineTest: async (input) => this.runPipelineTest(input),
15998
- runPipelineBatchTest: async (input) => this.runPipelineBatchTest(input),
15999
- runMultiCameraStressTest: async (input) => this.runMultiCameraStressTest(input),
16000
- runSustainedThroughput: async (input) => this.runSustainedThroughput(input),
16001
- abortSustainedThroughput: async (input) => this.abortSustainedThroughput(input),
16002
- cacheBenchFrame: async (input) => this.cacheBenchFrame(input),
16003
- releaseBenchFrame: async (input) => this.releaseBenchFrame(input),
16004
- listBenchmarkHistory: async () => this.listBenchmarkHistory(),
16051
+ listBenchmarkHistory: async () => this.loadHistory(),
16005
16052
  saveBenchmarkResult: async (input) => this.saveBenchmarkResult(input),
16006
- saveDecoderPerformanceResult: async (input) => this.saveDecoderPerformanceResult(input),
16007
16053
  deleteBenchmarkResult: async (input) => this.deleteBenchmarkResult(input),
16008
16054
  clearBenchmarkHistory: async () => this.clearBenchmarkHistory(),
16009
- listDecoderFixtures: async () => this.listDecoderFixtures(),
16010
- runDecoderPerformanceTest: async (input) => this.runDecoderPerformanceTest(input),
16011
- abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input),
16012
- uploadReplayClip: async (input) => this.uploadReplayClip(input),
16013
- runReplayClip: async (input) => this.runReplayClip(input),
16014
- listReplayRuns: async (input) => this.listReplayRuns(input),
16015
- diffReplayRuns: async (input) => this.diffReplayRunsAction(input)
16055
+ listMotionSessions: async (input) => this.listMotionSessions(input),
16056
+ startSessionReplay: async (input) => this.startSessionReplay(input),
16057
+ getSessionReplayStatus: async (input) => this.getSessionReplayStatus(input),
16058
+ abortSessionReplay: async (input) => ({ aborted: this.replays.abort(input.runId) }),
16059
+ listSessionReplays: async (input) => this.listSessionReplays(input),
16060
+ getSessionReplay: async (input) => this.replayStore === null ? null : this.replayStore.get(input.id)
16016
16061
  }
16017
16062
  };
16018
16063
  }
16019
16064
  async onShutdown() {
16020
- if (this.frameCacheSweeper) {
16021
- clearInterval(this.frameCacheSweeper);
16022
- this.frameCacheSweeper = null;
16023
- }
16024
- this.frameCache.clear();
16025
- for (const flag of this.sustainedAbortFlags.values()) flag.aborted = true;
16026
- this.sustainedAbortFlags.clear();
16027
- }
16028
- // ── Frame cache (raw RGB) ────────────────────────────────────────────────
16029
- async cacheBenchFrame(input) {
16030
- const jpeg = Buffer.from(input.imageBase64, "base64");
16031
- const sharp = (await import("sharp")).default;
16032
- const meta3 = await sharp(jpeg).metadata();
16033
- const width = meta3.width ?? 0;
16034
- const height = meta3.height ?? 0;
16035
- if (width <= 0 || height <= 0) {
16036
- throw new Error("cacheBenchFrame: failed to probe JPEG dimensions");
16037
- }
16038
- const ttlMs = Math.max(6e4, (input.ttlSeconds ?? 600) * 1e3);
16039
- const frameId = `bench-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
16040
- const expiresAt = Date.now() + ttlMs;
16041
- this.frameCache.set(frameId, { jpeg, width, height, expiresAt });
16042
- if (!this.frameCacheSweeper) {
16043
- this.frameCacheSweeper = setInterval(() => this.sweepFrameCache(), 6e4);
16044
- this.frameCacheSweeper.unref?.();
16045
- }
16046
- this.ctx.logger.info("cached bench frame", {
16047
- meta: { frameId, width, height, jpegBytes: jpeg.length, ttlMs }
16048
- });
16049
- return { frameId, width, height, expiresAt };
16065
+ this.replays.abortAll();
16050
16066
  }
16051
- async releaseBenchFrame(input) {
16052
- return { released: this.frameCache.delete(input.frameId) };
16053
- }
16054
- sweepFrameCache() {
16055
- const now = Date.now();
16056
- for (const [id, entry] of this.frameCache) {
16057
- if (entry.expiresAt < now) this.frameCache.delete(id);
16058
- }
16059
- }
16060
- // ── Handler implementations ──────────────────────────────────────────────
16061
16067
  get api() {
16062
16068
  const api = this.ctx.api;
16063
16069
  if (!api) throw new Error("BenchmarkAddon: ctx.api not available");
16064
16070
  return api;
16065
16071
  }
16072
+ // ── Detection Pipeline — one run ─────────────────────────────────────────
16066
16073
  async runPipelineTest(input) {
16067
- const { nodeId, steps, imageBase64, referenceImage, frameId, sessionId, engine } = input;
16068
- const { deviceKey, plane } = input;
16074
+ const { nodeId, steps, imageBase64, referenceImage, sessionId, deviceKey, plane } = input;
16069
16075
  this.ctx.logger.debug("runPipelineTest payload", {
16070
16076
  meta: {
16071
16077
  nodeId: nodeId ?? null,
16072
16078
  deviceKey: deviceKey ?? null,
16073
16079
  plane: plane ?? null,
16074
16080
  sessionId: sessionId ?? null,
16075
- engine: engine ?? null,
16076
16081
  stepsCount: steps.length,
16077
- steps: steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`),
16078
- imageSource: frameId ? `frame:${frameId}` : referenceImage ? `ref:${referenceImage}` : imageBase64 ? `base64:${Math.round(imageBase64.length / 1024)}KB` : "none"
16082
+ steps: steps.map(
16083
+ (s) => `${s.addonId}/${s.modelId ?? "(format default)"}${s.enabled ? "" : "[off]"}`
16084
+ ),
16085
+ imageSource: referenceImage ? `ref:${referenceImage}` : imageBase64 ? `base64:${Math.round(imageBase64.length / 1024)}KB` : "none"
16079
16086
  }
16080
16087
  });
16081
- const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
16082
- if (frameId && !cached2)
16083
- throw new Error(`runPipelineTest: frameId ${frameId} not in cache (expired or never cached)`);
16084
- const cachedImage = cached2 ? new Uint8Array(cached2.jpeg) : void 0;
16085
- const image = cachedImage ?? (imageBase64 ? new Uint8Array(Buffer.from(imageBase64, "base64")) : void 0);
16088
+ const image = imageBase64 ? new Uint8Array(Buffer.from(imageBase64, "base64")) : void 0;
16086
16089
  const stepsToRun = steps.length > 0 ? [...steps] : await (async () => {
16087
16090
  const globalSteps = await this.api.pipelineExecutor.getGlobalSteps.query({ nodeId });
16088
16091
  if (!globalSteps || globalSteps.length === 0) {
@@ -16101,10 +16104,8 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16101
16104
  nodeId,
16102
16105
  steps: stepsToRun,
16103
16106
  ...image ? { image } : {},
16104
- ...!image && imageBase64 ? { imageBase64 } : {},
16105
16107
  referenceImage,
16106
16108
  sessionId,
16107
- ...engine ? { engine } : {},
16108
16109
  // Absent, never present-and-undefined: the generated router takes
16109
16110
  // `.loose()` input and an explicit `undefined` is not the same thing
16110
16111
  // as "the node's default pool" / "the cap's default plane".
@@ -16115,579 +16116,235 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16115
16116
  "runPipelineTest"
16116
16117
  );
16117
16118
  }
16118
- // ── Replay ────────────────────────────────────────────────────────────
16119
- /**
16120
- * Storage location a replay upload lands on. `replayClips` is a NEW
16121
- * `StorageLocationType` (an open string, no schema change needed — see
16122
- * `storage-location.ts:19`) but it still needs an actual location
16123
- * REGISTERED for that type before `storage.write`/`.read` can resolve it
16124
- * (`storage.upsertLocation`, `cardinality: 'single'`, its own retention).
16125
- * That registration is an operator/boot-time provisioning step this
16126
- * change does not perform — see the report's open items.
16127
- */
16128
- static REPLAY_CLIPS_LOCATION = "replayClips";
16129
- async uploadReplayClip(input) {
16130
- const bytes = Buffer.from(input.dataBase64, "base64");
16131
- const clipId = `clip:${createHash("sha256").update(bytes).digest("hex").slice(0, 32)}`;
16132
- await this.api.storage.write.mutate({
16133
- location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16134
- relativePath: `${clipId}.bin`,
16135
- data: new Uint8Array(bytes)
16119
+ // ── Session replay (D646) ─────────────────────────────────────────────────
16120
+ async listMotionSessions(input) {
16121
+ const untilMs = input.untilMs ?? Date.now();
16122
+ const sinceMs = input.sinceMs ?? untilMs - MOTION_SESSIONS_DEFAULT_SPAN_MS;
16123
+ const events = await this.api.pipelineAnalytics.getMotionEvents.query({
16124
+ deviceId: input.deviceId,
16125
+ since: sinceMs,
16126
+ until: untilMs,
16127
+ limit: input.limit,
16128
+ projection: "full"
16136
16129
  });
16137
- this.ctx.logger.info("replay: clip uploaded", { meta: { clipId, bytes: bytes.length } });
16138
- return { clipId };
16139
- }
16140
- async runReplayClip(input) {
16141
- if (this.replayStore === null) throw new Error("replay: store not initialized");
16142
- const store = this.replayStore;
16143
- const source = input.source;
16144
- const result = await orchestrateReplayRun(
16145
- {
16146
- api: this.api,
16147
- logger: this.ctx.logger,
16148
- decoder: this.replayDecoder,
16149
- store,
16150
- ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg",
16151
- resolveBaseTree: (deviceId) => this.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }),
16152
- fetchSourceBytes: (src) => this.fetchReplaySourceBytes(src)
16153
- },
16154
- {
16155
- deviceId: input.deviceId,
16156
- nodeId: input.nodeId,
16157
- source,
16158
- overlay: input.overlay,
16159
- fps: input.fps,
16160
- maxFrames: input.maxFrames,
16161
- maxWidth: input.maxWidth,
16162
- ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
16163
- }
16130
+ const camera = await this.cameraSessionSettings(input.deviceId);
16131
+ const windows = events.map((ev) => ({ ev, window: motionSessionWindow(ev, camera) }));
16132
+ const starts = windows.map(
16133
+ ({ ev, window }) => window.kind === "window" ? window.startMs : ev.timestamp
16164
16134
  );
16165
- if (result.kind === "ran")
16166
- this.replayRunCache.set(result.replayRunId, {
16167
- id: result.replayRunId,
16168
- deviceId: input.deviceId,
16169
- nodeId: input.nodeId,
16170
- clipId: source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`,
16171
- createdAtMs: Date.now(),
16172
- source,
16173
- plan: null,
16174
- verdict: result.verdict
16175
- });
16176
- return result;
16177
- }
16178
- /**
16179
- * The profile a replay window is read from. `high` — same choice
16180
- * `RECORDED_STILL_PROFILE` makes in `addon-post-analysis` — because the
16181
- * whole point of `recording` over `upload` is NATIVE pixels (`-c:v copy`
16182
- * means the recorded frame IS the live frame); a window cut from a
16183
- * lower-rate rendition would be a worse version of the thing this source
16184
- * exists to avoid.
16185
- */
16186
- static REPLAY_RECORDING_PROFILE = "high";
16187
- /**
16188
- * Byte source for the replay decode step.
16189
- *
16190
- * `upload` is `storage.read` against the `replayClips` location. `recording`
16191
- * is `recording.getDeviceConfig` → `recording.locateSegment` →
16192
- * `recording.readWindowBytes` — the same "cheap question first" ordering
16193
- * `RecordedStillService` uses (a camera that is not recording costs a
16194
- * settings read, never a disk read), through `ctx.api` because addons never
16195
- * import each other.
16196
- *
16197
- * Every expected failure — not recording, no footage covering `fromMs`, a
16198
- * window that runs past its covering segment — returns a NAMED `refused`,
16199
- * never a thrown error: "the window has no segments" is a RESULT this
16200
- * whole feature exists to make legible (see the plan's stated unknown about
16201
- * whether device 615's footage covers 2026-08-26 18:12), not a failure to
16202
- * hide behind a generic exception.
16203
- */
16204
- async fetchReplaySourceBytes(source) {
16205
- if (source.kind === "upload") {
16206
- try {
16207
- const bytes = await this.api.storage.read.query({
16208
- location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16209
- relativePath: `${source.clipId}.bin`
16210
- });
16211
- return { kind: "ok", bytes, startMs: 0 };
16212
- } catch (err) {
16213
- return { kind: "refused", reason: `clip-not-found: ${errMsg(err)}` };
16214
- }
16215
- }
16216
- const profile = _BenchmarkAddon.REPLAY_RECORDING_PROFILE;
16217
- const tags = { deviceId: source.deviceId };
16218
- const config2 = await this.api.recordingArchive.getDeviceConfig.query({
16219
- deviceId: source.deviceId
16220
- });
16221
- if (config2.enabled === false) {
16222
- this.ctx.logger.warn("replay: recording source refused \u2014 camera is not recording", {
16223
- tags
16224
- });
16225
- return {
16226
- kind: "refused",
16227
- reason: `no-recording: device ${source.deviceId} does not have recording enabled`
16228
- };
16229
- }
16230
- const located = await this.api.recordingArchive.locateSegment.query({
16231
- deviceId: source.deviceId,
16232
- profile,
16233
- epochMs: source.fromMs
16234
- });
16235
- if (located.kind !== "segment") {
16236
- this.ctx.logger.warn(
16237
- "replay: recording source refused \u2014 no footage covers the window start",
16238
- { tags, meta: { fromMs: source.fromMs, toMs: source.toMs, profile } }
16239
- );
16240
- return {
16241
- kind: "refused",
16242
- reason: `no-footage: device ${source.deviceId} has no ${profile} recording covering ${new Date(source.fromMs).toISOString()}`
16243
- };
16244
- }
16245
- const window = await this.api.recordingArchive.readWindowBytes.query({
16246
- deviceId: source.deviceId,
16247
- profile,
16248
- startMs: located.startMs,
16249
- fromMs: source.fromMs,
16250
- toMs: source.toMs
16251
- });
16252
- if (window.kind !== "ok") {
16253
- this.ctx.logger.warn(
16254
- "replay: recording source refused \u2014 window runs past the covering segment",
16255
- {
16256
- tags,
16257
- meta: { fromMs: source.fromMs, toMs: source.toMs, segmentEndMs: window.segmentEndMs }
16258
- }
16259
- );
16260
- return {
16261
- kind: "refused",
16262
- reason: `window-spans-multiple-segments: footage for device ${source.deviceId} changes segment at ${window.segmentEndMs}, before the requested end ${source.toMs} \u2014 shorten the window or align it to one segment`
16263
- };
16264
- }
16265
- if (!window.reachesRequestedEnd) {
16266
- this.ctx.logger.warn(
16267
- "replay: recording window bytes truncated by the recorder read safety cap",
16268
- { tags, meta: { fromMs: source.fromMs, toMs: source.toMs, gopDurMs: window.gopDurMs } }
16269
- );
16270
- }
16271
- return { kind: "ok", bytes: window.data, startMs: window.gopStartMs };
16272
- }
16273
- async listReplayRuns(input) {
16274
- if (this.replayStore === null) return [];
16275
- const rows = await this.replayStore.list({ deviceId: input.deviceId });
16276
- for (const row of rows) this.replayRunCache.set(row.id, row);
16277
- return rows;
16278
- }
16279
- /** `verdict` round-trips through the store as opaque JSON
16280
- * (`StoredReplayRun.verdict: unknown`) — this is the ONE narrowing back
16281
- * to the shape `diffReplayRuns` needs, at the ONE place that reads it
16282
- * for that purpose. Same documented boundary `ReplayStore.list` itself
16283
- * crosses (`replay-store.ts`): this addon is the only reader that knows
16284
- * what it wrote. */
16285
- static verdictTracksAndDiscarded(verdict) {
16286
- return verdict;
16287
- }
16288
- async diffReplayRunsAction(input) {
16289
- const a = this.replayRunCache.get(input.runIdA);
16290
- const b = this.replayRunCache.get(input.runIdB);
16291
- if (a === void 0 || b === void 0) {
16292
- throw new Error(
16293
- "replay: diff \u2014 one or both run ids are not loaded (call listReplayRuns first)"
16294
- );
16295
- }
16296
- return diffReplayRuns(
16297
- {
16298
- clipId: a.clipId,
16299
- nodeId: a.nodeId,
16300
- settings: {},
16301
- ..._BenchmarkAddon.verdictTracksAndDiscarded(a.verdict)
16302
- },
16303
- {
16304
- clipId: b.clipId,
16305
- nodeId: b.nodeId,
16306
- settings: {},
16307
- ..._BenchmarkAddon.verdictTracksAndDiscarded(b.verdict)
16308
- }
16135
+ const ends = windows.map(
16136
+ ({ ev, window }) => window.kind === "window" ? window.endMs : ev.timestamp
16309
16137
  );
16310
- }
16311
- /**
16312
- * Batched pipeline tester.
16313
- *
16314
- * Decodes the JPEG once via sharp into RGB24 at native dimensions,
16315
- * then ships N copies as `FrameInput[]` in a single
16316
- * `pipelineExecutor.runPipelineBatch` cap call. The Python pool
16317
- * collapses the N predicts into one `MSG_INFER_BATCH = 0x03` IPC
16318
- * frame — per-call header + JSON envelope cost is amortised N:1.
16319
- *
16320
- * Returns N `FrameResult`s plus aggregate timing so the UI can
16321
- * compute fps directly from the wall clock.
16322
- */
16323
- async runPipelineBatchTest(input) {
16324
- this.ctx.logger.debug("runPipelineBatchTest payload", {
16325
- meta: {
16326
- nodeId: input.nodeId ?? null,
16327
- sessionId: input.sessionId ?? null,
16328
- engine: input.engine ?? null,
16329
- batchSize: input.batchSize,
16330
- targetSize: input.targetSize ?? null,
16331
- stepsCount: input.steps.length,
16332
- steps: input.steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`),
16333
- imageBytesKB: Math.round(input.imageBase64.length / 1024)
16334
- }
16335
- });
16336
- const { nodeId, steps, imageBase64, batchSize, sessionId, engine } = input;
16337
- const targetSize = input.targetSize ?? 640;
16338
- if (steps.length === 0) {
16339
- throw new Error("runPipelineBatchTest: empty steps");
16340
- }
16341
- const sharp = (await import("sharp")).default;
16342
- const jpeg = Buffer.from(imageBase64, "base64");
16343
- const rgb = await sharp(jpeg).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16344
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16345
- this.ctx.logger.debug("runPipelineBatchTest dispatch", {
16346
- meta: {
16347
- phase: "batch",
16348
- batchSize,
16349
- targetSize,
16350
- rgbBytes: rgb.byteLength,
16351
- totalBytes: rgb.byteLength * batchSize
16352
- }
16138
+ const tracks = events.length === 0 ? [] : await this.api.pipelineAnalytics.listTracks.query({
16139
+ deviceId: input.deviceId,
16140
+ since: Math.min(...starts),
16141
+ until: Math.max(...ends),
16142
+ projection: "slim",
16143
+ excludeSources: ["sensor", "audio"],
16144
+ limit: STORED_TRACKS_READ_LIMIT
16353
16145
  });
16354
- const frames = Array.from({ length: batchSize }, () => ({
16355
- data: rgbBytes,
16356
- format: "rgb",
16357
- width: targetSize,
16358
- height: targetSize,
16359
- timestamp: Date.now()
16146
+ return windows.map(({ ev, window }) => ({
16147
+ motionEventId: ev.id,
16148
+ deviceId: ev.deviceId,
16149
+ startMs: ev.timestamp,
16150
+ durationMs: ev.durationMs ?? null,
16151
+ open: ev.durationMs === null,
16152
+ refusal: window.kind === "refused" ? window.reason : null,
16153
+ storedTracks: window.kind === "window" ? tracks.filter((t) => t.firstSeen >= window.startMs && t.firstSeen < window.endMs).length : 0
16360
16154
  }));
16361
- const callStart = Date.now();
16362
- const out = await withHardTimeout(
16363
- this.api.pipelineExecutor.runPipelineBatch.mutate({
16364
- nodeId,
16365
- steps: [...steps],
16366
- frames,
16367
- sessionId,
16368
- ...engine ? { engine } : {}
16369
- }),
16370
- BENCH_RUN_TIMEOUT_MS,
16371
- "runPipelineBatchTest"
16372
- );
16373
- const callMs = Date.now() - callStart;
16374
- const inferMs = out.results.reduce((acc, r) => Math.max(acc, r.debug?.totalInferenceMs ?? 0), 0);
16375
- return {
16376
- results: out.results,
16377
- callMs,
16378
- inferMs
16379
- };
16380
16155
  }
16381
- /**
16382
- * Multi-camera stress tester.
16383
- *
16384
- * Runs the entire stress workload IN-PROCESS:
16385
- * 1. Decode + sharp-resize the JPEG once.
16386
- * 2. Build pre-fabricated `FrameInput[]` templates for the batched
16387
- * and single-frame call shapes.
16388
- * 3. Spawn `simulatedCameras` parallel async loops; each loop runs
16389
- * `batchesPerCamera` rounds. Per round: fan out 1 batched call
16390
- * + `extraSinglesPerRound` single calls via `Promise.all`.
16391
- * 4. Aggregate detsPerSec from `(simulatedCameras × batchesPerCamera
16392
- * × batchSize) / totalMs` so the metric is comparable to the
16393
- * external detector-engine benchmark we're calibrating against.
16394
- *
16395
- * Because the dispatch goes through `this.api.pipelineExecutor.
16396
- * runPipelineBatch.mutate(...)` and not over the wire, `localProvider
16397
- * Link` short-circuits when detection-pipeline is co-located on the
16398
- * same node. Removes per-call JSON marshalling, JPEG decode, and TLS.
16399
- */
16400
- async runMultiCameraStressTest(input) {
16401
- const targetSize = input.targetSize ?? 640;
16402
- const simulatedCameras = input.simulatedCameras ?? 4;
16403
- const batchesPerCamera = input.batchesPerCamera ?? 125;
16404
- const batchSize = input.batchSize ?? 4;
16405
- const extraSinglesPerRound = input.extraSinglesPerRound ?? 3;
16406
- const { nodeId, steps, sessionId } = input;
16407
- if (steps.length === 0) {
16408
- throw new Error("runMultiCameraStressTest: empty steps");
16409
- }
16410
- this.ctx.logger.info("runMultiCameraStressTest start", {
16156
+ startSessionReplay(input) {
16157
+ const runId = `replay-${input.deviceId}-${input.motionEventId}-${Date.now().toString(36)}`;
16158
+ this.ctx.logger.info("session replay started", {
16159
+ tags: { deviceId: input.deviceId },
16411
16160
  meta: {
16412
- nodeId: nodeId ?? null,
16413
- simulatedCameras,
16414
- batchesPerCamera,
16415
- batchSize,
16416
- extraSinglesPerRound,
16417
- targetSize,
16418
- steps: steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`)
16161
+ runId,
16162
+ motionEventId: input.motionEventId,
16163
+ nodeId: input.nodeId,
16164
+ deviceKey: input.deviceKey ?? null,
16165
+ modelId: input.modelId ?? null,
16166
+ fps: input.fps ?? null,
16167
+ maxFrames: input.maxFrames
16419
16168
  }
16420
16169
  });
16421
- const sharp = (await import("sharp")).default;
16422
- const jpeg = Buffer.from(input.imageBase64, "base64");
16423
- const rgb = await sharp(jpeg).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16424
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16425
- const buildFrames = (count) => Array.from({ length: count }, () => ({
16426
- data: rgbBytes,
16427
- format: "rgb",
16428
- width: targetSize,
16429
- height: targetSize,
16430
- timestamp: Date.now()
16431
- }));
16432
- const batchedFrames = buildFrames(batchSize);
16433
- const singleFrames = buildFrames(1);
16434
- const callMsList = [];
16435
- const inferMsList = [];
16436
- let errors = 0;
16437
- let countedDetections = 0;
16438
- let totalCalls = 0;
16439
- let totalFrames = 0;
16440
- const dispatchOne = async (frames, isCounted, tag) => {
16441
- const callStart = Date.now();
16442
- try {
16443
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
16444
- nodeId,
16445
- steps: [...steps],
16446
- frames: [...frames],
16447
- sessionId: sessionId ? `${sessionId}-${tag}` : tag
16448
- });
16449
- const callMs = Date.now() - callStart;
16450
- if (isCounted) {
16451
- callMsList.push(callMs);
16452
- let maxInfer = 0;
16453
- for (const r of out.results) {
16454
- const ms = r.debug?.totalInferenceMs ?? 0;
16455
- if (ms > maxInfer) maxInfer = ms;
16456
- }
16457
- inferMsList.push(maxInfer);
16458
- }
16459
- } catch (err) {
16460
- errors++;
16461
- this.ctx.logger.warn("runMultiCameraStressTest dispatch error", {
16462
- meta: { tag, error: err instanceof Error ? err.message : String(err) }
16463
- });
16464
- }
16465
- };
16466
- const start = Date.now();
16467
- const cameraTasks = Array.from(
16468
- { length: simulatedCameras },
16469
- (_, camIdx) => (async () => {
16470
- for (let i = 0; i < batchesPerCamera; i++) {
16471
- const dispatches = [
16472
- dispatchOne(batchedFrames, true, `c${camIdx}-r${i}-b`)
16473
- ];
16474
- for (let j = 0; j < extraSinglesPerRound; j++) {
16475
- dispatches.push(dispatchOne(singleFrames, false, `c${camIdx}-r${i}-s${j}`));
16476
- }
16477
- await Promise.all(dispatches);
16478
- countedDetections += batchSize;
16479
- totalCalls += 1 + extraSinglesPerRound;
16480
- totalFrames += batchSize + extraSinglesPerRound;
16481
- }
16482
- })()
16170
+ this.replays.start(
16171
+ runId,
16172
+ (run) => runSessionReplay(
16173
+ this.sessionReplayDeps(),
16174
+ {
16175
+ deviceId: input.deviceId,
16176
+ motionEventId: input.motionEventId,
16177
+ motionStartMs: input.motionStartMs,
16178
+ nodeId: input.nodeId,
16179
+ maxFrames: input.maxFrames,
16180
+ ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {},
16181
+ ...input.modelId !== void 0 ? { modelId: input.modelId } : {},
16182
+ ...input.fps !== void 0 ? { fps: input.fps } : {}
16183
+ },
16184
+ run
16185
+ )
16483
16186
  );
16484
- await Promise.all(cameraTasks);
16485
- const totalMs = Date.now() - start;
16486
- const sortedCall = [...callMsList].toSorted((a, b) => a - b);
16487
- const pct = (xs, p) => {
16488
- if (xs.length === 0) return 0;
16489
- const idx = Math.min(xs.length - 1, Math.floor(xs.length * p));
16490
- return xs[idx] ?? 0;
16187
+ return { accepted: true, runId };
16188
+ }
16189
+ getSessionReplayStatus(input) {
16190
+ const status = this.replays.status(input.runId);
16191
+ if (status === null) return null;
16192
+ return {
16193
+ runId: status.runId,
16194
+ state: status.state,
16195
+ startedAtMs: status.startedAtMs,
16196
+ finishedAtMs: status.finishedAtMs,
16197
+ progress: status.progress,
16198
+ outcome: status.result,
16199
+ error: status.error
16491
16200
  };
16492
- const inferMean = inferMsList.length === 0 ? 0 : inferMsList.reduce((s, x) => s + x, 0) / inferMsList.length;
16493
- const detsPerSec = totalMs > 0 ? countedDetections * 1e3 / totalMs : 0;
16494
- this.ctx.logger.info("runMultiCameraStressTest done", {
16495
- meta: {
16496
- totalMs,
16497
- countedDetections,
16498
- detsPerSec: Number(detsPerSec.toFixed(2)),
16499
- totalCalls,
16500
- totalFrames,
16501
- errors
16502
- }
16503
- });
16201
+ }
16202
+ async listSessionReplays(input) {
16203
+ if (this.replayStore === null) return { runs: [], skipped: 0 };
16204
+ const { runs, skipped } = await this.replayStore.list({ deviceId: input.deviceId, limit: 50 });
16205
+ if (skipped > 0) {
16206
+ this.ctx.logger.info("session replays: rows skipped that are not session replays", {
16207
+ tags: { deviceId: input.deviceId },
16208
+ meta: { skipped }
16209
+ });
16210
+ }
16504
16211
  return {
16505
- totalMs,
16506
- countedDetections,
16507
- detsPerSec,
16508
- totalCalls,
16509
- totalFrames,
16510
- callMsP50: pct(sortedCall, 0.5),
16511
- callMsP95: pct(sortedCall, 0.95),
16512
- callMsP99: pct(sortedCall, 0.99),
16513
- inferMsMean: inferMean,
16514
- errors
16212
+ runs: runs.map((r) => {
16213
+ const { timeline: _timeline, ...report } = r.report;
16214
+ return {
16215
+ id: r.id,
16216
+ deviceId: r.deviceId,
16217
+ nodeId: r.nodeId,
16218
+ createdAtMs: r.createdAtMs,
16219
+ report
16220
+ };
16221
+ }),
16222
+ skipped
16515
16223
  };
16516
16224
  }
16517
- // ── Sustained throughput (root-detection e2e) ────────────────────────────
16518
- /**
16519
- * SUSTAINED THROUGHPUT test of ROOT detection.
16520
- *
16521
- * 1. Pin ONE frame in memory: resolve `frameId` (from `cacheBenchFrame`)
16522
- * or the one-shot `imageBase64`, decode + square-resize ONCE to raw
16523
- * RGB. Every node + every iteration consumes the IDENTICAL buffer — no
16524
- * per-iteration decode or upload.
16525
- * 2. For each target node, run a `concurrency`-bounded loop of
16526
- * `runPipelineBatch` (batchSize frames/call) on the node's ROOT-ONLY
16527
- * steps until `durationSec` elapses (or the stop flag fires). Root-only
16528
- * steps stay on the batch single-root fast path == live `plane:'frame'`
16529
- * root detection.
16530
- * 3. Aggregate sustained fps / det/s + latency percentiles per node on the
16531
- * identical pinned frame for apples-to-apples comparison.
16532
- *
16533
- * Non-destructive: targets the benchmark's own cached frame (never a live
16534
- * camera session), is explicitly started, and is bounded by N + the stop
16535
- * button (`abortSustainedThroughput`).
16536
- */
16537
- async runSustainedThroughput(input) {
16538
- const { frameId, imageBase64, durationSec, batchSize, concurrency, sessionId, nodes } = input;
16539
- const targetSize = input.targetSize ?? 640;
16540
- const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
16541
- if (frameId && !cached2)
16542
- throw new Error(
16543
- `runSustainedThroughput: frameId ${frameId} not in cache (expired or never cached)`
16544
- );
16545
- const jpegSource = cached2 ? cached2.jpeg : imageBase64 ? Buffer.from(imageBase64, "base64") : null;
16546
- if (!jpegSource)
16547
- throw new Error("runSustainedThroughput: no frame source (need frameId or imageBase64)");
16548
- const sharp = (await import("sharp")).default;
16549
- const rgb = await sharp(jpegSource).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16550
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16551
- const batchFrames = Array.from({ length: batchSize }, () => ({
16552
- data: rgbBytes,
16553
- format: "rgb",
16554
- width: targetSize,
16555
- height: targetSize,
16556
- timestamp: Date.now()
16557
- }));
16558
- const benchFrameId = Math.floor(Math.random() * 4294967294) + 1 >>> 0;
16559
- const abortFlag = { aborted: false };
16560
- this.sustainedAbortFlags.set(sessionId, abortFlag);
16561
- this.ctx.logger.info("runSustainedThroughput start", {
16562
- meta: {
16563
- sessionId,
16564
- durationSec,
16565
- batchSize,
16566
- concurrency,
16567
- targetSize,
16568
- nodeCount: nodes.length,
16569
- nodes: nodes.map((n) => n.nodeId ?? "hub"),
16570
- deviceKeys: nodes.map((n) => n.deviceKey ?? "(default)")
16571
- }
16572
- });
16225
+ async persistReplay(runId, nodeId, outcome) {
16226
+ const store = this.replayStore;
16227
+ if (store === null) return;
16228
+ const tags = { deviceId: outcome.report.deviceId };
16573
16229
  try {
16574
- const perNode = await Promise.all(
16575
- nodes.map(async (node) => {
16576
- if (node.steps.length === 0)
16577
- throw new Error(
16578
- `runSustainedThroughput: node ${node.nodeId ?? "hub"} has no root steps`
16579
- );
16580
- const dispatchBatch = async () => {
16581
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
16582
- nodeId: node.nodeId,
16583
- steps: [...node.steps],
16584
- frames: [...batchFrames],
16585
- sessionId,
16586
- frameId: benchFrameId,
16587
- ...node.engine ? { engine: node.engine } : {},
16588
- ...node.deviceKey ? { deviceKey: node.deviceKey } : {}
16589
- });
16590
- return {
16591
- frameCount: out.results.length,
16592
- detectionCounts: out.results.map((r) => r.detections.length)
16593
- };
16594
- };
16595
- let raw;
16596
- try {
16597
- raw = await runSustainedLoop({
16598
- dispatchBatch,
16599
- concurrency,
16600
- durationMs: durationSec * 1e3,
16601
- now: () => Date.now(),
16602
- shouldStop: () => abortFlag.aborted
16603
- });
16604
- } finally {
16605
- await this.api.pipelineExecutor.uncacheFrame.mutate({ nodeId: node.nodeId, frameId: benchFrameId }).catch(() => {
16606
- });
16607
- }
16608
- const summary = {
16609
- nodeId: node.nodeId ?? null,
16610
- ...node.label ? { label: node.label } : {},
16611
- totalFrames: raw.totalFrames,
16612
- totalBatches: raw.totalBatches,
16613
- wallMs: raw.wallMs,
16614
- fps: computeFps(raw.totalFrames, raw.wallMs),
16615
- detsPerSec: computeDetsPerSec(raw.totalDetections, raw.wallMs),
16616
- avgDetections: computeAvgDetections(raw.totalDetections, raw.detectionSamples),
16617
- latency: computeLatencyStats(raw.callMsList),
16618
- errors: raw.errors,
16619
- aborted: raw.aborted
16620
- };
16621
- this.ctx.logger.info("runSustainedThroughput node done", {
16622
- meta: {
16623
- nodeId: summary.nodeId,
16624
- fps: Number(summary.fps.toFixed(2)),
16625
- detsPerSec: Number(summary.detsPerSec.toFixed(2)),
16626
- p95Ms: Number(summary.latency.p95.toFixed(2)),
16627
- totalFrames: summary.totalFrames,
16628
- errors: summary.errors,
16629
- aborted: summary.aborted
16630
- }
16631
- });
16632
- return summary;
16633
- })
16634
- );
16635
- return { perNode, durationSec, batchSize, concurrency };
16636
- } finally {
16637
- this.sustainedAbortFlags.delete(sessionId);
16230
+ await store.save({
16231
+ id: runId,
16232
+ deviceId: outcome.report.deviceId,
16233
+ nodeId,
16234
+ createdAtMs: Date.now(),
16235
+ report: outcome.report
16236
+ });
16237
+ await this.pruneReplays();
16238
+ } catch (err) {
16239
+ this.ctx.logger.warn("session replay: report NOT saved", {
16240
+ tags,
16241
+ meta: { runId, error: errMsg(err) }
16242
+ });
16638
16243
  }
16639
16244
  }
16640
- async abortSustainedThroughput(input) {
16641
- const flag = this.sustainedAbortFlags.get(input.sessionId);
16642
- if (!flag) return { aborted: false };
16643
- flag.aborted = true;
16644
- return { aborted: true };
16645
- }
16646
- // ── Decoder perf — fixture discovery + run orchestration ────────────────
16647
- async listDecoderFixtures() {
16648
- const dir = resolveFixturesDir();
16649
- if (!fs2.existsSync(dir)) {
16650
- this.ctx.logger.warn("decoder fixtures directory missing", { meta: { dir } });
16651
- return [];
16652
- }
16653
- const out = [];
16654
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
16655
- if (!entry.isFile()) continue;
16656
- const meta3 = FIXTURE_META[entry.name];
16657
- if (!meta3) continue;
16658
- const full = path2.join(dir, entry.name);
16659
- const stat = fs2.statSync(full);
16660
- out.push({
16661
- filename: entry.name,
16662
- codec: meta3.codec,
16663
- resolutionLabel: meta3.resolutionLabel,
16664
- sizeBytes: stat.size,
16665
- description: meta3.description
16666
- });
16245
+ async pruneReplays() {
16246
+ const store = this.replayStore;
16247
+ if (store === null) return;
16248
+ try {
16249
+ const deleted = await store.pruneBefore(Date.now() - REPLAY_RETENTION_MS);
16250
+ if (deleted > 0) this.ctx.logger.info("session replays pruned", { meta: { deleted } });
16251
+ } catch (err) {
16252
+ this.ctx.logger.warn("session replays: prune failed", { meta: { error: errMsg(err) } });
16667
16253
  }
16668
- out.sort((a, b) => a.sizeBytes - b.sizeBytes);
16669
- return out;
16670
16254
  }
16671
- async runDecoderPerformanceTest(input) {
16672
- const abortFlag = { aborted: false };
16673
- this.decoderPerfAbortFlags.set(input.sessionId, abortFlag);
16255
+ /**
16256
+ * The camera's own cooldown + pre-roll switch, read from the orchestrator's
16257
+ * hydrated device settings (the authority, not a copy). A failed read is
16258
+ * `{}` — the fleet defaults apply and the report says `default`.
16259
+ */
16260
+ async cameraSessionSettings(deviceId) {
16674
16261
  try {
16675
- return await runDecoderPerfTest(input, {
16676
- api: this.api,
16677
- eventBus: this.ctx.eventBus,
16678
- logger: this.ctx.logger,
16679
- fixturesDir: resolveFixturesDir(),
16680
- abortFlag
16262
+ const raw = await this.api.addonSettings.getDeviceSettings.query({
16263
+ addonId: "pipeline-orchestrator",
16264
+ deviceId
16681
16265
  });
16682
- } finally {
16683
- this.decoderPerfAbortFlags.delete(input.sessionId);
16266
+ return readCameraSessionSettings(raw);
16267
+ } catch (err) {
16268
+ this.ctx.logger.warn("session replay: camera session settings unreadable \u2014 fleet defaults", {
16269
+ tags: { deviceId },
16270
+ meta: { error: errMsg(err) }
16271
+ });
16272
+ return {};
16684
16273
  }
16685
16274
  }
16686
- async abortDecoderPerformanceTest(input) {
16687
- const flag = this.decoderPerfAbortFlags.get(input.sessionId);
16688
- if (!flag) return { aborted: false };
16689
- flag.aborted = true;
16690
- return { aborted: true };
16275
+ /** The replay's outside world — every call through `ctx.api`, node-pinned where it matters. */
16276
+ sessionReplayDeps() {
16277
+ const api = this.api;
16278
+ const profile = REPLAY_RECORDING_PROFILE;
16279
+ return {
16280
+ logger: this.ctx.logger,
16281
+ cameraSessionSettings: (deviceId) => this.cameraSessionSettings(deviceId),
16282
+ findMotionSession: async (deviceId, motionEventId, motionStartMs) => {
16283
+ const events = await api.pipelineAnalytics.getMotionEvents.query({
16284
+ deviceId,
16285
+ since: motionStartMs - 1,
16286
+ until: motionStartMs + 1,
16287
+ limit: 50,
16288
+ projection: "full"
16289
+ });
16290
+ return events.find((e) => e.id === motionEventId) ?? null;
16291
+ },
16292
+ resolveCameraTree: (deviceId, nodeId) => api.pipelineOrchestrator.resolvePipeline.query({ deviceId, agentNodeId: nodeId }),
16293
+ validate: (steps, nodeId, deviceKey) => api.pipelineExecutor.validatePipeline.query(
16294
+ { steps: [...steps], ...deviceKey !== void 0 ? { deviceKey } : {} },
16295
+ nodePin(nodeId)
16296
+ ),
16297
+ isRecording: async (deviceId) => {
16298
+ const config2 = await api.recordingArchive.getDeviceConfig.query({ deviceId });
16299
+ return config2.enabled !== false;
16300
+ },
16301
+ listStoredTracks: async (deviceId, sinceMs, untilMs) => api.pipelineAnalytics.listTracks.query({
16302
+ deviceId,
16303
+ since: sinceMs,
16304
+ until: untilMs,
16305
+ projection: "full",
16306
+ excludeSources: ["sensor", "audio"],
16307
+ limit: STORED_TRACKS_READ_LIMIT
16308
+ }),
16309
+ listBirthDecisions: (deviceId, sinceMs) => api.pipelineAnalytics.listBirthDecisions.query({
16310
+ deviceId,
16311
+ since: sinceMs,
16312
+ limit: REPLAY_BIRTHS_LIMIT
16313
+ }),
16314
+ readRecording: (deviceId, instants, stepMs) => readRecordingForInstants(
16315
+ {
16316
+ locateSegment: (epochMs) => api.recordingArchive.locateSegment.query({ deviceId, profile, epochMs }),
16317
+ readWindowBytes: (q) => api.recordingArchive.readWindowBytes.query({ deviceId, profile, ...q })
16318
+ },
16319
+ instants,
16320
+ stepMs
16321
+ ),
16322
+ decode: async (bytes, args) => {
16323
+ const mjpeg = await this.replayDecoder.decodeSequence(
16324
+ buildReplaySequenceArgs(args),
16325
+ bytes,
16326
+ {
16327
+ timeoutMs: Math.max(
16328
+ REPLAY_DECODE_TIMEOUT_FLOOR_MS,
16329
+ Math.round(args.durationSec * REPLAY_DECODE_TIMEOUT_PER_SEC_MS)
16330
+ )
16331
+ }
16332
+ );
16333
+ return splitMjpegStream(mjpeg);
16334
+ },
16335
+ runRootDetector: (call) => api.pipelineExecutor.runPipeline.mutate({
16336
+ nodeId: call.nodeId,
16337
+ steps: [...call.steps],
16338
+ // A fresh `Uint8Array<ArrayBuffer>`: `mjpeg-split` hands back a
16339
+ // `Buffer` view, and the generated cap input is typed against the
16340
+ // narrower `ArrayBuffer` backing.
16341
+ image: new Uint8Array(call.image),
16342
+ deviceId: call.deviceId,
16343
+ plane: "frame",
16344
+ replay: true,
16345
+ ...call.deviceKey !== void 0 ? { deviceKey: call.deviceKey } : {}
16346
+ })
16347
+ };
16691
16348
  }
16692
16349
  // ── History persistence (per-addon store on the hub) ──────────────────
16693
16350
  /**
@@ -16695,9 +16352,7 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16695
16352
  * VERBATIM: the value under `benchmarkHistory` is a JSON **string** (not
16696
16353
  * the array itself), so the handle is typed over the string — the
16697
16354
  * encode/decode + entry-level recovery below stays the codec, the handle
16698
- * owns store access (retry budget + validated fallback). Absent key or a
16699
- * non-string value falls back to the empty-history encoding, exactly the
16700
- * `[]` the raw read produced.
16355
+ * owns store access (retry budget + validated fallback).
16701
16356
  */
16702
16357
  historyState() {
16703
16358
  return this.state(KEY_BENCHMARK_HISTORY, external_exports.string(), EMPTY_HISTORY_JSON);
@@ -16711,14 +16366,10 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16711
16366
  const arr = Array.isArray(parsed) ? parsed : [];
16712
16367
  const accepted = [];
16713
16368
  let rejected = 0;
16714
- for (const entry of arr) {
16715
- const normalized = entry !== null && typeof entry === "object" && !("kind" in entry) ? { ...entry, kind: "pipeline" } : entry;
16716
- const parsedEntry = HistoryEntrySchema.safeParse(normalized);
16717
- if (parsedEntry.success) {
16718
- accepted.push(parsedEntry.data);
16719
- } else {
16720
- rejected += 1;
16721
- }
16369
+ for (const entry2 of arr) {
16370
+ const parsedEntry = HistoryEntrySchema.safeParse(entry2);
16371
+ if (parsedEntry.success) accepted.push(parsedEntry.data);
16372
+ else rejected += 1;
16722
16373
  }
16723
16374
  if (rejected > 0) {
16724
16375
  this.ctx.logger.warn("benchmark history: dropped unparseable entries", {
@@ -16734,13 +16385,10 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16734
16385
  async writeHistory(history) {
16735
16386
  await this.historyState().set(JSON.stringify(history));
16736
16387
  }
16737
- async listBenchmarkHistory() {
16738
- return this.loadHistory();
16739
- }
16740
- async saveBenchmarkResult(entry) {
16388
+ async saveBenchmarkResult(entry2) {
16741
16389
  const history = await this.loadHistory();
16742
16390
  const saved = {
16743
- ...entry,
16391
+ ...entry2,
16744
16392
  kind: "pipeline",
16745
16393
  id: `bench-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
16746
16394
  date: (/* @__PURE__ */ new Date()).toISOString()
@@ -16750,19 +16398,6 @@ var BenchmarkAddon = class _BenchmarkAddon extends BaseAddon {
16750
16398
  await this.writeHistory(next);
16751
16399
  return saved;
16752
16400
  }
16753
- async saveDecoderPerformanceResult(entry) {
16754
- const history = await this.loadHistory();
16755
- const saved = {
16756
- kind: "decoder-perf",
16757
- id: `decoder-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
16758
- date: (/* @__PURE__ */ new Date()).toISOString(),
16759
- ...entry
16760
- };
16761
- const next = [saved, ...history];
16762
- if (next.length > MAX_HISTORY_ENTRIES) next.length = MAX_HISTORY_ENTRIES;
16763
- await this.writeHistory(next);
16764
- return saved;
16765
- }
16766
16401
  async deleteBenchmarkResult(input) {
16767
16402
  const history = (await this.loadHistory()).filter((e) => e.id !== input.id);
16768
16403
  await this.writeHistory(history);