@trillboards/edge-sdk 0.2.4 → 0.2.6

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 (200) hide show
  1. package/README.md +4 -3
  2. package/deploy/docker/Dockerfile.cloud-sense +117 -0
  3. package/deploy/docker/Dockerfile.cpu +30 -21
  4. package/deploy/docker/Dockerfile.cuda +30 -21
  5. package/deploy/docker/Dockerfile.openvino +30 -20
  6. package/deploy/docker/README.md +9 -4
  7. package/deploy/docker/connector-entrypoint.sh +122 -0
  8. package/deploy/helm/README.md +1 -1
  9. package/deploy/k8s/daemonset.yaml +1 -1
  10. package/deploy/macos/README.md +161 -0
  11. package/deploy/macos/TrillboardsSetupApp.m +603 -0
  12. package/deploy/macos/build-macos-pkg.sh +588 -0
  13. package/deploy/macos/com.trillboards.edge.plist +62 -0
  14. package/deploy/macos/com.trillboards.litertlm.plist +45 -0
  15. package/deploy/macos/pair.command +519 -0
  16. package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
  17. package/deploy/macos/scripts/postinstall +153 -0
  18. package/deploy/macos/scripts/setup-litertlm.sh +272 -0
  19. package/deploy/macos/test-install-pair.sh +383 -0
  20. package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
  21. package/deploy/systemd/install.sh +25 -15
  22. package/deploy/systemd/trillboards-edge.service +4 -1
  23. package/deploy/windows/README.md +70 -0
  24. package/deploy/windows/build-windows.ps1 +225 -0
  25. package/deploy/windows/bundle-windows-installer.ps1 +153 -0
  26. package/deploy/windows/check-feeds.js +26 -0
  27. package/deploy/windows/e2e/fixtures/person.jpg +0 -0
  28. package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
  29. package/deploy/windows/install-update.ps1 +101 -0
  30. package/deploy/windows/installer.nsi +138 -0
  31. package/deploy/windows/register-litertlm-task.ps1 +42 -0
  32. package/deploy/windows/register-task.ps1 +44 -0
  33. package/deploy/windows/run-connector.ps1 +69 -0
  34. package/deploy/windows/setup-litertlm.ps1 +361 -0
  35. package/deploy/windows/stop-existing.ps1 +72 -0
  36. package/deploy/windows/trillboards-edge-task.vbs +25 -0
  37. package/deploy/windows/trillboards-edge.cmd +33 -0
  38. package/deploy/windows/trillboards-pair.cmd +276 -0
  39. package/deploy/windows/trillboards-setup.ps1 +456 -0
  40. package/deploy/windows/trillboards-setup.vbs +13 -0
  41. package/dist/AudienceAnalyzeClient.d.ts +136 -0
  42. package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
  43. package/dist/AudienceAnalyzeClient.js +246 -0
  44. package/dist/AudienceAnalyzeClient.js.map +1 -0
  45. package/dist/ChannelProber.d.ts +150 -0
  46. package/dist/ChannelProber.d.ts.map +1 -0
  47. package/dist/ChannelProber.js +887 -0
  48. package/dist/ChannelProber.js.map +1 -0
  49. package/dist/CloudEscalationPolicy.d.ts +143 -0
  50. package/dist/CloudEscalationPolicy.d.ts.map +1 -0
  51. package/dist/CloudEscalationPolicy.js +137 -0
  52. package/dist/CloudEscalationPolicy.js.map +1 -0
  53. package/dist/CloudSensingManager.d.ts +533 -0
  54. package/dist/CloudSensingManager.d.ts.map +1 -0
  55. package/dist/CloudSensingManager.js +1254 -0
  56. package/dist/CloudSensingManager.js.map +1 -0
  57. package/dist/DiscoveryReportClient.d.ts +66 -0
  58. package/dist/DiscoveryReportClient.d.ts.map +1 -0
  59. package/dist/DiscoveryReportClient.js +85 -0
  60. package/dist/DiscoveryReportClient.js.map +1 -0
  61. package/dist/EdgeAgent.d.ts +12 -2
  62. package/dist/EdgeAgent.d.ts.map +1 -1
  63. package/dist/EdgeAgent.js +55 -13
  64. package/dist/EdgeAgent.js.map +1 -1
  65. package/dist/LiteRtVlmProvider.d.ts +193 -0
  66. package/dist/LiteRtVlmProvider.d.ts.map +1 -0
  67. package/dist/LiteRtVlmProvider.js +436 -0
  68. package/dist/LiteRtVlmProvider.js.map +1 -0
  69. package/dist/MosaicSource.d.ts +108 -0
  70. package/dist/MosaicSource.d.ts.map +1 -0
  71. package/dist/MosaicSource.js +274 -0
  72. package/dist/MosaicSource.js.map +1 -0
  73. package/dist/OnvifClient.d.ts +83 -0
  74. package/dist/OnvifClient.d.ts.map +1 -0
  75. package/dist/OnvifClient.js +209 -0
  76. package/dist/OnvifClient.js.map +1 -0
  77. package/dist/PairClient.d.ts +106 -0
  78. package/dist/PairClient.d.ts.map +1 -0
  79. package/dist/PairClient.js +186 -0
  80. package/dist/PairClient.js.map +1 -0
  81. package/dist/RecorderAddressRecovery.d.ts +53 -0
  82. package/dist/RecorderAddressRecovery.d.ts.map +1 -0
  83. package/dist/RecorderAddressRecovery.js +359 -0
  84. package/dist/RecorderAddressRecovery.js.map +1 -0
  85. package/dist/SceneChangeDetector.d.ts +291 -0
  86. package/dist/SceneChangeDetector.d.ts.map +1 -0
  87. package/dist/SceneChangeDetector.js +682 -0
  88. package/dist/SceneChangeDetector.js.map +1 -0
  89. package/dist/SensingProfileClient.d.ts +59 -0
  90. package/dist/SensingProfileClient.d.ts.map +1 -0
  91. package/dist/SensingProfileClient.js +78 -0
  92. package/dist/SensingProfileClient.js.map +1 -0
  93. package/dist/SetupSessionClient.d.ts +114 -0
  94. package/dist/SetupSessionClient.d.ts.map +1 -0
  95. package/dist/SetupSessionClient.js +95 -0
  96. package/dist/SetupSessionClient.js.map +1 -0
  97. package/dist/VlmAdmissionGate.d.ts +126 -0
  98. package/dist/VlmAdmissionGate.d.ts.map +1 -0
  99. package/dist/VlmAdmissionGate.js +166 -0
  100. package/dist/VlmAdmissionGate.js.map +1 -0
  101. package/dist/autoSense.d.ts +216 -0
  102. package/dist/autoSense.d.ts.map +1 -0
  103. package/dist/autoSense.js +521 -0
  104. package/dist/autoSense.js.map +1 -0
  105. package/dist/cli.d.ts +27 -1
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +511 -21
  108. package/dist/cli.js.map +1 -1
  109. package/dist/combinedSense.d.ts +128 -0
  110. package/dist/combinedSense.d.ts.map +1 -0
  111. package/dist/combinedSense.js +261 -0
  112. package/dist/combinedSense.js.map +1 -0
  113. package/dist/config.d.ts +351 -0
  114. package/dist/config.d.ts.map +1 -1
  115. package/dist/config.js +99 -2
  116. package/dist/config.js.map +1 -1
  117. package/dist/connectorCadence.d.ts +135 -0
  118. package/dist/connectorCadence.d.ts.map +1 -0
  119. package/dist/connectorCadence.js +151 -0
  120. package/dist/connectorCadence.js.map +1 -0
  121. package/dist/dataDir.d.ts +33 -0
  122. package/dist/dataDir.d.ts.map +1 -0
  123. package/dist/dataDir.js +76 -0
  124. package/dist/dataDir.js.map +1 -0
  125. package/dist/deployedCadence.d.ts +167 -0
  126. package/dist/deployedCadence.d.ts.map +1 -0
  127. package/dist/deployedCadence.js +209 -0
  128. package/dist/deployedCadence.js.map +1 -0
  129. package/dist/deployedCadenceSource.d.ts +48 -0
  130. package/dist/deployedCadenceSource.d.ts.map +1 -0
  131. package/dist/deployedCadenceSource.js +74 -0
  132. package/dist/deployedCadenceSource.js.map +1 -0
  133. package/dist/diagnose.d.ts +1 -1
  134. package/dist/diagnose.d.ts.map +1 -1
  135. package/dist/diagnose.js +13 -12
  136. package/dist/diagnose.js.map +1 -1
  137. package/dist/discovery.d.ts +131 -0
  138. package/dist/discovery.d.ts.map +1 -0
  139. package/dist/discovery.js +499 -0
  140. package/dist/discovery.js.map +1 -0
  141. package/dist/gemmaModelOta.d.ts +118 -0
  142. package/dist/gemmaModelOta.d.ts.map +1 -0
  143. package/dist/gemmaModelOta.js +291 -0
  144. package/dist/gemmaModelOta.js.map +1 -0
  145. package/dist/index.d.ts +16 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +40 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/init.d.ts.map +1 -1
  150. package/dist/init.js +41 -18
  151. package/dist/init.js.map +1 -1
  152. package/dist/keepAwake.d.ts +33 -0
  153. package/dist/keepAwake.d.ts.map +1 -0
  154. package/dist/keepAwake.js +54 -0
  155. package/dist/keepAwake.js.map +1 -0
  156. package/dist/legacyRecorderProfile.d.ts +20 -0
  157. package/dist/legacyRecorderProfile.d.ts.map +1 -0
  158. package/dist/legacyRecorderProfile.js +27 -0
  159. package/dist/legacyRecorderProfile.js.map +1 -0
  160. package/dist/localInferenceSchema.d.ts +132 -0
  161. package/dist/localInferenceSchema.d.ts.map +1 -0
  162. package/dist/localInferenceSchema.js +274 -0
  163. package/dist/localInferenceSchema.js.map +1 -0
  164. package/dist/localSenseE2eHarness.d.ts +101 -0
  165. package/dist/localSenseE2eHarness.d.ts.map +1 -0
  166. package/dist/localSenseE2eHarness.js +279 -0
  167. package/dist/localSenseE2eHarness.js.map +1 -0
  168. package/dist/localSensing.d.ts +268 -0
  169. package/dist/localSensing.d.ts.map +1 -0
  170. package/dist/localSensing.js +492 -0
  171. package/dist/localSensing.js.map +1 -0
  172. package/dist/modelIntegrity.d.ts +59 -0
  173. package/dist/modelIntegrity.d.ts.map +1 -0
  174. package/dist/modelIntegrity.js +165 -0
  175. package/dist/modelIntegrity.js.map +1 -0
  176. package/dist/pairVerify.d.ts +100 -0
  177. package/dist/pairVerify.d.ts.map +1 -0
  178. package/dist/pairVerify.js +296 -0
  179. package/dist/pairVerify.js.map +1 -0
  180. package/dist/qrScanner.d.ts +33 -0
  181. package/dist/qrScanner.d.ts.map +1 -0
  182. package/dist/qrScanner.js +103 -0
  183. package/dist/qrScanner.js.map +1 -0
  184. package/dist/runtimePaths.d.ts +10 -0
  185. package/dist/runtimePaths.d.ts.map +1 -0
  186. package/dist/runtimePaths.js +79 -0
  187. package/dist/runtimePaths.js.map +1 -0
  188. package/dist/sensingRunMode.d.ts +68 -0
  189. package/dist/sensingRunMode.d.ts.map +1 -0
  190. package/dist/sensingRunMode.js +346 -0
  191. package/dist/sensingRunMode.js.map +1 -0
  192. package/dist/setupMode.d.ts +98 -0
  193. package/dist/setupMode.d.ts.map +1 -0
  194. package/dist/setupMode.js +828 -0
  195. package/dist/setupMode.js.map +1 -0
  196. package/dist/setupProgress.d.ts +31 -0
  197. package/dist/setupProgress.d.ts.map +1 -0
  198. package/dist/setupProgress.js +173 -0
  199. package/dist/setupProgress.js.map +1 -0
  200. package/package.json +13 -4
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keepAwakeCommand = keepAwakeCommand;
4
+ exports.startKeepAwake = startKeepAwake;
5
+ const child_process_1 = require("child_process");
6
+ /** Build the platform command (exported for tests — pure). */
7
+ function keepAwakeCommand(platform, pid) {
8
+ if (platform === 'darwin') {
9
+ return { cmd: 'caffeinate', args: ['-i', '-w', String(pid)] };
10
+ }
11
+ if (platform === 'win32') {
12
+ const script = `$sig='[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);';` +
13
+ `Add-Type -MemberDefinition $sig -Name P -Namespace TrillboardsKeepAwake;` +
14
+ `while (Get-Process -Id ${pid} -ErrorAction SilentlyContinue) {` +
15
+ `[TrillboardsKeepAwake.P]::SetThreadExecutionState([uint32]'0x80000001') | Out-Null;` +
16
+ `Start-Sleep -Seconds 50 }`;
17
+ return { cmd: 'powershell', args: ['-NoProfile', '-NonInteractive', '-Command', script] };
18
+ }
19
+ return null;
20
+ }
21
+ function startKeepAwake(platform = process.platform, pid = process.pid, spawner = child_process_1.spawn) {
22
+ const command = keepAwakeCommand(platform, pid);
23
+ if (!command)
24
+ return { child: null, stop: () => undefined };
25
+ let child = null;
26
+ try {
27
+ child = spawner(command.cmd, command.args, { stdio: 'ignore' });
28
+ // Never let the helper hold the event loop open or crash the connector.
29
+ child.unref();
30
+ child.on('error', () => {
31
+ child = null;
32
+ });
33
+ }
34
+ catch {
35
+ child = null;
36
+ }
37
+ if (child) {
38
+ console.log('Keep-awake active: this computer will not sleep while sensing runs.');
39
+ }
40
+ const stop = () => {
41
+ if (child && !child.killed) {
42
+ try {
43
+ child.kill();
44
+ }
45
+ catch {
46
+ /* already gone */
47
+ }
48
+ }
49
+ };
50
+ // Release on clean exit; the pid-watch (win32) / -w pid (darwin) covers crashes.
51
+ process.once('exit', stop);
52
+ return { child, stop };
53
+ }
54
+ //# sourceMappingURL=keepAwake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keepAwake.js","sourceRoot":"","sources":["../src/keepAwake.ts"],"names":[],"mappings":";;AA8BA,4CAiBC;AAED,wCAmCC;AApFD,iDAAyD;AA6BzD,8DAA8D;AAC9D,SAAgB,gBAAgB,CAC9B,QAAyB,EACzB,GAAW;IAEX,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAChE,CAAC;IACD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,MAAM,GACV,sGAAsG;YACtG,0EAA0E;YAC1E,0BAA0B,GAAG,mCAAmC;YAChE,qFAAqF;YACrF,2BAA2B,CAAC;QAC9B,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5F,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,cAAc,CAC5B,WAA4B,OAAO,CAAC,QAAQ,EAC5C,MAAc,OAAO,CAAC,GAAG,EACzB,UAAwB,qBAAK;IAE7B,MAAM,OAAO,GAAG,gBAAgB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;IAE5D,IAAI,KAAK,GAAwB,IAAI,CAAC;IACtC,IAAI,CAAC;QACH,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChE,wEAAwE;QACxE,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACrB,KAAK,GAAG,IAAI,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,GAAG,CAAC,qEAAqE,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,CAAC;YAAC,MAAM,CAAC;gBACP,kBAAkB;YACpB,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,iFAAiF;IACjF,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Conservative defaults for DVR/NVR feeds. Recorders tend to have fewer RTSP
3
+ * session slots and slower/noisier codec startup than standalone IP cameras,
4
+ * so setup marks them and runtime applies this profile automatically.
5
+ */
6
+ export declare const LEGACY_RECORDER_RUNTIME_PROFILE: {
7
+ readonly timeoutUs: 18000000;
8
+ readonly openValidationAttempts: 6;
9
+ readonly transientRetryDelayMs: 2000;
10
+ readonly feedStaggerMs: 15000;
11
+ readonly maxFeedStaggerMs: 90000;
12
+ readonly cameraHealthFailureGraceCount: 5;
13
+ };
14
+ export type CameraSourceType = 'recorder' | 'direct_url' | undefined;
15
+ export declare function legacyRecorderRtspOptions(sourceType: CameraSourceType): {
16
+ timeoutUs?: number;
17
+ openValidationAttempts?: number;
18
+ transientRetryDelayMs?: number;
19
+ };
20
+ //# sourceMappingURL=legacyRecorderProfile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacyRecorderProfile.d.ts","sourceRoot":"","sources":["../src/legacyRecorderProfile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,+BAA+B;;;;;;;CAOlC,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAErE,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,gBAAgB,GAAG;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAOA"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LEGACY_RECORDER_RUNTIME_PROFILE = void 0;
4
+ exports.legacyRecorderRtspOptions = legacyRecorderRtspOptions;
5
+ /**
6
+ * Conservative defaults for DVR/NVR feeds. Recorders tend to have fewer RTSP
7
+ * session slots and slower/noisier codec startup than standalone IP cameras,
8
+ * so setup marks them and runtime applies this profile automatically.
9
+ */
10
+ exports.LEGACY_RECORDER_RUNTIME_PROFILE = {
11
+ timeoutUs: 18000000,
12
+ openValidationAttempts: 6,
13
+ transientRetryDelayMs: 2000,
14
+ feedStaggerMs: 15000,
15
+ maxFeedStaggerMs: 90000,
16
+ cameraHealthFailureGraceCount: 5,
17
+ };
18
+ function legacyRecorderRtspOptions(sourceType) {
19
+ if (sourceType !== 'recorder')
20
+ return {};
21
+ return {
22
+ timeoutUs: exports.LEGACY_RECORDER_RUNTIME_PROFILE.timeoutUs,
23
+ openValidationAttempts: exports.LEGACY_RECORDER_RUNTIME_PROFILE.openValidationAttempts,
24
+ transientRetryDelayMs: exports.LEGACY_RECORDER_RUNTIME_PROFILE.transientRetryDelayMs,
25
+ };
26
+ }
27
+ //# sourceMappingURL=legacyRecorderProfile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"legacyRecorderProfile.js","sourceRoot":"","sources":["../src/legacyRecorderProfile.ts"],"names":[],"mappings":";;;AAgBA,8DAWC;AA3BD;;;;GAIG;AACU,QAAA,+BAA+B,GAAG;IAC7C,SAAS,EAAE,QAAU;IACrB,sBAAsB,EAAE,CAAC;IACzB,qBAAqB,EAAE,IAAK;IAC5B,aAAa,EAAE,KAAM;IACrB,gBAAgB,EAAE,KAAM;IACxB,6BAA6B,EAAE,CAAC;CACxB,CAAC;AAIX,SAAgB,yBAAyB,CAAC,UAA4B;IAKpE,IAAI,UAAU,KAAK,UAAU;QAAE,OAAO,EAAE,CAAC;IACzC,OAAO;QACL,SAAS,EAAE,uCAA+B,CAAC,SAAS;QACpD,sBAAsB,EAAE,uCAA+B,CAAC,sBAAsB;QAC9E,qBAAqB,EAAE,uCAA+B,CAAC,qBAAqB;KAC7E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * localInferenceSchema — the route-addressable JSON Schema + prompt the
3
+ * on-device VLM (LiteRtVlmProvider) is constrained to. This is the subset of
4
+ * the cloud `buildProfileDrivenJsonSchema` contract that drives OFV →
5
+ * Sense-Anything routes → WhatsApp: the DEFAULT audience profile fields
6
+ * (sensingProfileService CLOUD_EMIT_AUDIENCE_FIELDS) + the scalar scene fields.
7
+ *
8
+ * ── WHY PLAIN-COMPLETION + NON-NULLABLE (the on-device runtime reality):
9
+ * LiteRT-LM (`litert-lm serve` 0.13.1) exposes constrained TOOL-CALLING, but its
10
+ * ANTLR FC tool-call parser 500s whenever Gemma 4 E2B serializes a string with
11
+ * the special `<|"|>` quote token instead of a literal `"`. That happens
12
+ * reproducibly on whole classes of frames — measured 0/5 on an empty-room frame
13
+ * across EVERY tool-call shape (full schema, no-free-text, temp 0.8, even
14
+ * `response_format`). Empty scenes dominate real CCTV, so tool-calling is not
15
+ * viable. The SAME frame returns clean, valid JSON via PLAIN completion (no
16
+ * tools) — measured 8/8 — because the `<|"|>` token is injected only by the
17
+ * tool-call/constrained path. So LiteRtVlmProvider renders THIS schema as a
18
+ * human-readable key list, asks for a bare JSON object, and validates the result
19
+ * client-side (required keys + enum membership) with a retry. That is where the
20
+ * structured-output GUARANTEE comes from now — client validation, not the
21
+ * (broken) server grammar. Keep every field NON-nullable with a sentinel value
22
+ * ('none'/'unclear'/''/0) for "no people"; the localInferenceSchema.test.ts
23
+ * guard fails the build if a nullable type/enum is reintroduced.
24
+ *
25
+ * ── WHY THE FIELD SET IS A PROPERTY OF THE PROFILE (not a global constant):
26
+ * an SOP/operator profile asks an operational question about the operator's OWN
27
+ * venue, and the 12 DOOH audience fields answer none of it — two of them
28
+ * (`scene_description`, `audience_engagement_narrative`) are free-text paragraphs
29
+ * the model must DECODE on every frame. MEASURED through this exact code on an
30
+ * M4 Max, real kitchen frame at production capture params (640x480 JPEG q70),
31
+ * arms INTERLEAVED so machine load lands on both equally:
32
+ *
33
+ * delegate 12 DOOH + 4 operator operator set (7 keys) ratio
34
+ * CPU 39.22 s/frame (n=4) 25.68 s/frame (n=4) 1.53x
35
+ * GPU 10.66 s/frame (n=5) 9.81 s/frame (n=5) 1.09x
36
+ *
37
+ * The GPU win is small because litert-lm pins the VISION encoder to CPU
38
+ * (openai_handler.py `vision_backend = Backend.CPU()`), so image encode is a
39
+ * fixed per-frame floor no schema can shrink; on the CPU delegate — every host
40
+ * with no usable GPU delegate — decode is the bill and the lean set is worth
41
+ * 1.53x. ACCURACY also moved the right way on that run: against the frame's
42
+ * ground truth the operator set scored 16/16 and the 16-key set 12/16, the
43
+ * 16-key arm answering `gloves_worn: true` on a scene where only SOME staff are
44
+ * gloved while the lean arm answered `false`. (On GPU both scored 15/20 — no
45
+ * accuracy difference there.) Latency is therefore a secondary reason to split
46
+ * the sets; the primary ones are asking the operator's question instead of a
47
+ * media buyer's, and keeping `audience_ethnicity` off a camera pointed at the
48
+ * customer's own staff. See LocalSchemaFieldSet.
49
+ *
50
+ * NOTE: the connector SUPPLIES `source='vlm_edge'` and `captured_at` itself
51
+ * (provenance + time are not the model's to decide) — they are deliberately NOT
52
+ * in this schema. The server additionally forces provenance `local_connector`
53
+ * on the preAnalyzed ingest path (gated OUT of the sold bidstream).
54
+ */
55
+ /**
56
+ * A custom operator (Sense-Anything) field from the deployed profile's
57
+ * `metrics_schema`. The connector fetches these from the server
58
+ * (`GET /v2/earner/cameras/sensing-profile`) so the on-device VLM emits the SAME
59
+ * operator-declared fields the cloud VLM does — reaching parity with
60
+ * `buildProfileDrivenJsonSchema` (otherwise an alert keyed on e.g. `queue_length`
61
+ * never fires on-device because Gemma was never told to emit it).
62
+ */
63
+ export interface ProfileField {
64
+ field: string;
65
+ /** Profile field type (sensingIntentService MetricsSchemaEntrySchema). */
66
+ type: 'counter' | 'gauge' | 'boolean' | 'enum' | 'category' | 'string' | 'list' | 'distribution' | 'hidden';
67
+ enum_options?: string[];
68
+ description?: string;
69
+ example_outputs?: Array<string | number | boolean>;
70
+ }
71
+ /**
72
+ * Which DEFAULT field block the on-device schema carries. This is a property of
73
+ * the DEPLOYED PROFILE, resolved from the profile's own self-describing type
74
+ * (`sensing_profiles.profile_type`) — see localSensing.fieldSetForProfileType.
75
+ *
76
+ * - `'audience'` — a DOOH screen/audience-measurement profile (the seeded
77
+ * builtins: retail_full_v1, convenience_store_v1, transit_hub_v1, …), and the
78
+ * default when NO profile is deployed. These exist to describe the AUDIENCE
79
+ * in front of a screen for a media buyer, so they carry the full cloud-parity
80
+ * audience block (the 12 fields below).
81
+ * - `'operator'` — a Sense-Anything profile (`profile_type='custom_intent'`;
82
+ * 147 of the 157 active profiles as of 2026-08-11). The operator asked a
83
+ * specific question about their OWN venue ("is everyone handling food wearing
84
+ * gloves?"). The DOOH audience block answers nothing they asked and costs
85
+ * 1.09-1.53x the per-frame latency depending on delegate (see the file
86
+ * header), so an operator profile emits its OWN declared fields plus the
87
+ * minimal presence primitives — nothing else.
88
+ */
89
+ export type LocalSchemaFieldSet = 'audience' | 'operator';
90
+ export interface LocalSensingSchemaOptions {
91
+ /**
92
+ * Which default field block to emit. Defaults to `'audience'` — the DOOH
93
+ * contract, and the correct answer when no profile is deployed.
94
+ */
95
+ fieldSet?: LocalSchemaFieldSet;
96
+ /** Extra operator (Sense-Anything) field keys to also request, e.g. ['queue_length']. */
97
+ extraBooleanFields?: string[];
98
+ extraCounterFields?: string[];
99
+ /**
100
+ * Typed custom profile fields (preferred) — the active Sense-Anything profile's
101
+ * `metrics_schema`. Each is appended to the constrained schema with the right
102
+ * JSON type so Gemma emits it and the operator's route can evaluate it.
103
+ */
104
+ extraFields?: ProfileField[];
105
+ }
106
+ /**
107
+ * Build the JSON Schema the on-device model is asked to fill (rendered as a
108
+ * readable key list by LiteRtVlmProvider, validated client-side). All fields
109
+ * non-nullable + required — see the file header for why null is forbidden.
110
+ *
111
+ * ONE builder, parameterized by `fieldSet` — an audience/DOOH profile gets the
112
+ * full 12; a Sense-Anything (operator) profile gets the presence primitives plus
113
+ * its own declared fields, and never `audience_ethnicity`.
114
+ */
115
+ export declare function buildLocalSensingSchema(options?: LocalSensingSchemaOptions): Record<string, unknown>;
116
+ /**
117
+ * The analysis prompt (mirrors audienceVisionService buildProfileDrivenPrompt's
118
+ * role). When the active Sense-Anything profile declares custom fields, append a
119
+ * terse instruction for each so Gemma knows what to observe — the on-device
120
+ * equivalent of the cloud profile-driven prompt. Kept lean (the verbose cloud
121
+ * phrasing reliably destabilized the on-device decode — see file header).
122
+ *
123
+ * `fieldSet` must match the schema's: the audience prompt tells Gemma which
124
+ * SENTINEL to use for each audience_* field on an empty scene, and on the
125
+ * operator path those fields do not exist, so repeating that instruction would
126
+ * spend decode budget describing keys the model must not emit.
127
+ */
128
+ export declare function buildLocalSensingPrompt(options?: {
129
+ fieldSet?: LocalSchemaFieldSet;
130
+ extraFields?: ProfileField[];
131
+ }): string;
132
+ //# sourceMappingURL=localInferenceSchema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localInferenceSchema.d.ts","sourceRoot":"","sources":["../src/localInferenceSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,CAAC;IAC5G,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,yFAAyF;IACzF,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;CAC9B;AAqID;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCzB;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAAC,WAAW,CAAC,EAAE,YAAY,EAAE,CAAA;CAAO,GAC7E,MAAM,CAeR"}
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ /**
3
+ * localInferenceSchema — the route-addressable JSON Schema + prompt the
4
+ * on-device VLM (LiteRtVlmProvider) is constrained to. This is the subset of
5
+ * the cloud `buildProfileDrivenJsonSchema` contract that drives OFV →
6
+ * Sense-Anything routes → WhatsApp: the DEFAULT audience profile fields
7
+ * (sensingProfileService CLOUD_EMIT_AUDIENCE_FIELDS) + the scalar scene fields.
8
+ *
9
+ * ── WHY PLAIN-COMPLETION + NON-NULLABLE (the on-device runtime reality):
10
+ * LiteRT-LM (`litert-lm serve` 0.13.1) exposes constrained TOOL-CALLING, but its
11
+ * ANTLR FC tool-call parser 500s whenever Gemma 4 E2B serializes a string with
12
+ * the special `<|"|>` quote token instead of a literal `"`. That happens
13
+ * reproducibly on whole classes of frames — measured 0/5 on an empty-room frame
14
+ * across EVERY tool-call shape (full schema, no-free-text, temp 0.8, even
15
+ * `response_format`). Empty scenes dominate real CCTV, so tool-calling is not
16
+ * viable. The SAME frame returns clean, valid JSON via PLAIN completion (no
17
+ * tools) — measured 8/8 — because the `<|"|>` token is injected only by the
18
+ * tool-call/constrained path. So LiteRtVlmProvider renders THIS schema as a
19
+ * human-readable key list, asks for a bare JSON object, and validates the result
20
+ * client-side (required keys + enum membership) with a retry. That is where the
21
+ * structured-output GUARANTEE comes from now — client validation, not the
22
+ * (broken) server grammar. Keep every field NON-nullable with a sentinel value
23
+ * ('none'/'unclear'/''/0) for "no people"; the localInferenceSchema.test.ts
24
+ * guard fails the build if a nullable type/enum is reintroduced.
25
+ *
26
+ * ── WHY THE FIELD SET IS A PROPERTY OF THE PROFILE (not a global constant):
27
+ * an SOP/operator profile asks an operational question about the operator's OWN
28
+ * venue, and the 12 DOOH audience fields answer none of it — two of them
29
+ * (`scene_description`, `audience_engagement_narrative`) are free-text paragraphs
30
+ * the model must DECODE on every frame. MEASURED through this exact code on an
31
+ * M4 Max, real kitchen frame at production capture params (640x480 JPEG q70),
32
+ * arms INTERLEAVED so machine load lands on both equally:
33
+ *
34
+ * delegate 12 DOOH + 4 operator operator set (7 keys) ratio
35
+ * CPU 39.22 s/frame (n=4) 25.68 s/frame (n=4) 1.53x
36
+ * GPU 10.66 s/frame (n=5) 9.81 s/frame (n=5) 1.09x
37
+ *
38
+ * The GPU win is small because litert-lm pins the VISION encoder to CPU
39
+ * (openai_handler.py `vision_backend = Backend.CPU()`), so image encode is a
40
+ * fixed per-frame floor no schema can shrink; on the CPU delegate — every host
41
+ * with no usable GPU delegate — decode is the bill and the lean set is worth
42
+ * 1.53x. ACCURACY also moved the right way on that run: against the frame's
43
+ * ground truth the operator set scored 16/16 and the 16-key set 12/16, the
44
+ * 16-key arm answering `gloves_worn: true` on a scene where only SOME staff are
45
+ * gloved while the lean arm answered `false`. (On GPU both scored 15/20 — no
46
+ * accuracy difference there.) Latency is therefore a secondary reason to split
47
+ * the sets; the primary ones are asking the operator's question instead of a
48
+ * media buyer's, and keeping `audience_ethnicity` off a camera pointed at the
49
+ * customer's own staff. See LocalSchemaFieldSet.
50
+ *
51
+ * NOTE: the connector SUPPLIES `source='vlm_edge'` and `captured_at` itself
52
+ * (provenance + time are not the model's to decide) — they are deliberately NOT
53
+ * in this schema. The server additionally forces provenance `local_connector`
54
+ * on the preAnalyzed ingest path (gated OUT of the sold bidstream).
55
+ */
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.buildLocalSensingSchema = buildLocalSensingSchema;
58
+ exports.buildLocalSensingPrompt = buildLocalSensingPrompt;
59
+ /**
60
+ * The presence primitives EVERY profile reports, whatever its field set. They
61
+ * are the connector's floor:
62
+ * - `people_present` / `person_count` are what the server's preAnalyzed
63
+ * handler grades the observation on (earner-misc.analyzeAudienceFrame reads
64
+ * `preAnalyzed.people_present === true || Number(preAnalyzed.person_count) >= 1`
65
+ * to mark it decisionable `screen_audience`), so dropping them would
66
+ * silently downgrade every local observation;
67
+ * - `confidence` is the model's own certainty and rides the unified contract.
68
+ * They are also what keeps `localSensing.isAlertWorthy` able to tell an empty
69
+ * scene from a busy one on the lean operator path.
70
+ */
71
+ const PRESENCE_PRIMITIVE_ORDER = ['people_present', 'person_count', 'confidence'];
72
+ /**
73
+ * The DOOH/audience field set, in the exact order the prompt renders it. Order
74
+ * is load-bearing only in that it is the wire order Gemma was benchmarked
75
+ * against — keep it stable unless you re-measure.
76
+ */
77
+ const AUDIENCE_FIELD_ORDER = [
78
+ 'audience_group_composition',
79
+ 'audience_intent_stage',
80
+ 'audience_activity_macro',
81
+ 'audience_ethnicity',
82
+ 'audience_engagement_narrative',
83
+ 'person_count',
84
+ 'crowd_density',
85
+ 'safety_concern',
86
+ 'scene_description',
87
+ 'ad_receptivity_score',
88
+ 'people_present',
89
+ 'confidence',
90
+ ];
91
+ /**
92
+ * The property spec for every DEFAULT field, declared ONCE. `buildLocalSensingSchema`
93
+ * composes a field set out of this table rather than forking into two builders.
94
+ * All non-nullable — see the file header for why null is forbidden.
95
+ */
96
+ const DEFAULT_FIELD_PROPS = {
97
+ // DEFAULT audience profile fields (route-addressable enums). Each enum that
98
+ // had no "no people" member in the cloud contract gains a 'none' sentinel
99
+ // so the model never has to emit null when the scene is empty.
100
+ audience_group_composition: {
101
+ type: 'string',
102
+ enum: ['solo', 'couple', 'family', 'friend_group', 'work_team', 'mixed', 'none'],
103
+ },
104
+ audience_intent_stage: {
105
+ type: 'string',
106
+ enum: ['browsing', 'researching', 'comparing', 'ready_to_buy', 'post_purchase', 'none'],
107
+ },
108
+ audience_activity_macro: {
109
+ type: 'string',
110
+ enum: [
111
+ 'focused_work', 'personal_downtime', 'social_dwell', 'transactional',
112
+ 'decision_making', 'passing_through', 'queueing', 'none',
113
+ ],
114
+ },
115
+ audience_ethnicity: {
116
+ type: 'string',
117
+ enum: [
118
+ 'asian', 'black', 'hispanic', 'middle_eastern', 'south_asian',
119
+ 'southeast_asian', 'white', 'mixed', 'other', 'unclear',
120
+ ],
121
+ },
122
+ // One-sentence advertiser-actionable summary; "" when no people present.
123
+ audience_engagement_narrative: { type: 'string' },
124
+ // Scene-level fields the CLOUD VLM ALWAYS emits (an empty room is
125
+ // scene_description="…", crowd_density="empty", person_count=0).
126
+ person_count: { type: 'integer' },
127
+ crowd_density: {
128
+ type: 'string',
129
+ enum: ['empty', 'sparse', 'moderate', 'crowded', 'very_crowded'],
130
+ },
131
+ safety_concern: { type: 'boolean' },
132
+ scene_description: { type: 'string' },
133
+ // Receptivity in [0,1]; 0 when no people present (non-nullable — see header).
134
+ ad_receptivity_score: { type: 'number' },
135
+ people_present: { type: 'boolean' },
136
+ confidence: { type: 'number' },
137
+ };
138
+ /**
139
+ * NEVER asked of an OPERATOR (Sense-Anything) profile, at any cost — not as a
140
+ * default field and not via a profile's own `metrics_schema`.
141
+ *
142
+ * `audience_ethnicity` is a DOOH REACH metric: it exists so a media buyer can
143
+ * describe the anonymous audience walking past a screen, and it is legal-approved
144
+ * for exactly that. An SOP/compliance deployment points the SAME camera at the
145
+ * customer's OWN EMPLOYEES, where an ethnicity estimate has no operator use and
146
+ * is a straight liability. So the operator path drops it entirely rather than
147
+ * relying on a profile never happening to declare it.
148
+ */
149
+ const OPERATOR_FORBIDDEN_FIELDS = new Set(['audience_ethnicity']);
150
+ /** Map a profile field's type → its JSON-schema property (non-nullable). */
151
+ function profileFieldToSchemaProp(f) {
152
+ switch (f.type) {
153
+ case 'hidden':
154
+ return null; // internal — never asked of the model
155
+ case 'counter':
156
+ return { type: 'integer' };
157
+ case 'gauge':
158
+ return { type: 'number' };
159
+ case 'boolean':
160
+ return { type: 'boolean' };
161
+ case 'enum':
162
+ case 'category':
163
+ if (Array.isArray(f.enum_options) && f.enum_options.length >= 1) {
164
+ return { type: 'string', enum: f.enum_options };
165
+ }
166
+ return { type: 'string' };
167
+ case 'list':
168
+ return { type: 'array', items: { type: 'string' } };
169
+ case 'distribution':
170
+ // Cloud declares distribution as a record-of-number (z.record(string,number)),
171
+ // e.g. age_distribution / gender_split — keep it an object on-device so
172
+ // route/fan-out parity holds (NOT a scalar string).
173
+ return { type: 'object', additionalProperties: { type: 'number' } };
174
+ case 'string':
175
+ default:
176
+ return { type: 'string' };
177
+ }
178
+ }
179
+ /** Fresh copy of a default field spec (callers must never share the enum arrays). */
180
+ function cloneDefaultProp(key) {
181
+ const spec = DEFAULT_FIELD_PROPS[key];
182
+ return Array.isArray(spec.enum) ? { ...spec, enum: [...spec.enum] } : { ...spec };
183
+ }
184
+ /**
185
+ * Build the JSON Schema the on-device model is asked to fill (rendered as a
186
+ * readable key list by LiteRtVlmProvider, validated client-side). All fields
187
+ * non-nullable + required — see the file header for why null is forbidden.
188
+ *
189
+ * ONE builder, parameterized by `fieldSet` — an audience/DOOH profile gets the
190
+ * full 12; a Sense-Anything (operator) profile gets the presence primitives plus
191
+ * its own declared fields, and never `audience_ethnicity`.
192
+ */
193
+ function buildLocalSensingSchema(options = {}) {
194
+ const fieldSet = options.fieldSet === 'operator' ? 'operator' : 'audience';
195
+ const defaults = fieldSet === 'operator' ? PRESENCE_PRIMITIVE_ORDER : AUDIENCE_FIELD_ORDER;
196
+ const properties = {};
197
+ // Every field is required — client-side validation rejects (and retries) any
198
+ // generation that drops a key or emits an out-of-enum value.
199
+ const required = [];
200
+ for (const key of defaults) {
201
+ properties[key] = cloneDefaultProp(key);
202
+ required.push(key);
203
+ }
204
+ // Operator (Sense-Anything) custom fields — also NON-nullable + required so a
205
+ // queue_length / shelf_empty style alert field survives the on-device grammar.
206
+ const addField = (key, prop) => {
207
+ if (!key || key in properties)
208
+ return; // never override a default field
209
+ if (fieldSet === 'operator' && OPERATOR_FORBIDDEN_FIELDS.has(key))
210
+ return;
211
+ properties[key] = prop;
212
+ required.push(key);
213
+ };
214
+ for (const f of options.extraBooleanFields || [])
215
+ addField(f, { type: 'boolean' });
216
+ for (const f of options.extraCounterFields || [])
217
+ addField(f, { type: 'integer' });
218
+ // Typed profile fields (the preferred path — from the deployed profile's metrics_schema).
219
+ for (const f of options.extraFields || []) {
220
+ if (!f || typeof f.field !== 'string')
221
+ continue;
222
+ const prop = profileFieldToSchemaProp(f);
223
+ if (prop)
224
+ addField(f.field, prop);
225
+ }
226
+ return { type: 'object', properties, required };
227
+ }
228
+ /**
229
+ * Render the operator's custom-field instructions for the prompt (shared by both
230
+ * field sets). On the operator path a forbidden field is skipped here too, so the
231
+ * prompt can never ask for a key the schema refuses to carry.
232
+ */
233
+ function renderCustomFieldInstructions(extraFields, fieldSet) {
234
+ const customs = (extraFields || []).filter((f) => f && f.field && f.type !== 'hidden'
235
+ && !(fieldSet === 'operator' && OPERATOR_FORBIDDEN_FIELDS.has(f.field)));
236
+ if (customs.length === 0)
237
+ return null;
238
+ return customs
239
+ .map((f) => {
240
+ const desc = (f.description || '').replace(/\s+/g, ' ').trim().slice(0, 120);
241
+ return `- ${f.field}: ${desc || f.type}`;
242
+ })
243
+ .join(' ');
244
+ }
245
+ /**
246
+ * The analysis prompt (mirrors audienceVisionService buildProfileDrivenPrompt's
247
+ * role). When the active Sense-Anything profile declares custom fields, append a
248
+ * terse instruction for each so Gemma knows what to observe — the on-device
249
+ * equivalent of the cloud profile-driven prompt. Kept lean (the verbose cloud
250
+ * phrasing reliably destabilized the on-device decode — see file header).
251
+ *
252
+ * `fieldSet` must match the schema's: the audience prompt tells Gemma which
253
+ * SENTINEL to use for each audience_* field on an empty scene, and on the
254
+ * operator path those fields do not exist, so repeating that instruction would
255
+ * spend decode budget describing keys the model must not emit.
256
+ */
257
+ function buildLocalSensingPrompt(options = {}) {
258
+ const fieldSet = options.fieldSet === 'operator' ? 'operator' : 'audience';
259
+ const base = fieldSet === 'operator'
260
+ ? 'You are watching one frame from a venue camera. Count distinct people ' +
261
+ '(person_count); people_present=true if any human is visible; confidence is your ' +
262
+ '0-1 certainty. Report only what is VISIBLE in this frame. Anonymous aggregate ' +
263
+ 'measurement only — face DETECTION not recognition.'
264
+ : 'You are a behavioral intelligence analyst observing one frame from a screen at a retail ' +
265
+ 'venue. Count distinct people (person_count); people_present=true if any human is visible. ' +
266
+ 'When NO people are present use "none"/"unclear" for audience_* enums, "" for ' +
267
+ 'audience_engagement_narrative, 0 for ad_receptivity_score. Anonymous aggregate measurement ' +
268
+ 'only — face DETECTION not recognition.';
269
+ const lines = renderCustomFieldInstructions(options.extraFields, fieldSet);
270
+ if (!lines)
271
+ return base;
272
+ return `${base} Also report these operator-requested fields from the image: ${lines}`;
273
+ }
274
+ //# sourceMappingURL=localInferenceSchema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localInferenceSchema.js","sourceRoot":"","sources":["../src/localInferenceSchema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /** A schema-conforming, route-addressable default-profile result with a person. */
2
+ export interface StubGemmaResultOptions {
3
+ personCount?: number;
4
+ peoplePresent?: boolean;
5
+ /** Extra/override keys merged onto the base result (e.g. operator custom fields). */
6
+ extra?: Record<string, unknown>;
7
+ }
8
+ /**
9
+ * Build a DEFAULT-schema-conforming structured result with a person present.
10
+ * Matches buildLocalSensingSchema()'s required keys + enum members exactly, so
11
+ * LiteRtVlmProvider's client-side validation passes on the first attempt. This
12
+ * is the object the stub VLM "infers" — it must look like real Gemma output.
13
+ */
14
+ export declare function buildStubGemmaResult(options?: StubGemmaResultOptions): Record<string, unknown>;
15
+ /**
16
+ * Wrap a structured result in the OpenAI chat-completions envelope litert-lm
17
+ * serve returns (choices[0].message.content is the minified JSON string).
18
+ */
19
+ export declare function buildStubChatCompletion(result: Record<string, unknown>): Record<string, unknown>;
20
+ export interface StubVlmServer {
21
+ /** The base URL the connector should point at (http://127.0.0.1:<port>). */
22
+ baseUrl: string;
23
+ port: number;
24
+ /** Number of /v1/chat/completions calls served (for assertions). */
25
+ getChatCalls(): number;
26
+ /** The `model` id each /v1/chat/completions call was posted under, in order. */
27
+ getChatModels(): string[];
28
+ close(): Promise<void>;
29
+ }
30
+ export interface CreateStubVlmServerOptions {
31
+ /** Result the stub "infers" for every frame. Default: a 2-person scene. */
32
+ result?: Record<string, unknown>;
33
+ /** Bind host. Default 127.0.0.1 (loopback only — never expose a fake model). */
34
+ host?: string;
35
+ /** Port. Default 0 (ephemeral). Pass 9379 to mirror the real serve port. */
36
+ port?: number;
37
+ /**
38
+ * Model ids `GET /v1/models` advertises. Default: the single CPU id. Pass both
39
+ * ids (`['gemma-4-E2B-it.litertlm', 'gemma-4-E2B-it.litertlm,gpu']`) to mirror
40
+ * a real litert-lm 0.13.1 serve, which advertises its GPU delegate as a
41
+ * SEPARATE model id — that is what LiteRtVlmProvider resolves against.
42
+ */
43
+ modelIds?: string[];
44
+ }
45
+ /**
46
+ * Start a fake `litert-lm serve` (OpenAI-compatible) on loopback. Answers:
47
+ * GET /v1/models → 200 (health probe)
48
+ * POST /v1/chat/completions → 200 {choices:[{message:{content:<json>}}]}
49
+ * Any other path → 404. Resolves once listening (baseUrl/port populated).
50
+ */
51
+ export declare function createStubVlmServer(options?: CreateStubVlmServerOptions): Promise<StubVlmServer>;
52
+ /** One captured preAnalyzed ingest POST. */
53
+ export interface CapturedIngest {
54
+ preAnalyzed: Record<string, unknown> | null;
55
+ deviceToken: string | null;
56
+ hasImage: boolean;
57
+ capturedAt: number | null;
58
+ raw: Record<string, unknown> | null;
59
+ }
60
+ export interface IngestCaptureServer {
61
+ /** The apiBaseUrl the connector should use (http://127.0.0.1:<port>). */
62
+ baseUrl: string;
63
+ port: number;
64
+ /** Every captured /v2/earner/audience-analyze POST, in order. */
65
+ getCaptured(): CapturedIngest[];
66
+ close(): Promise<void>;
67
+ }
68
+ export interface CreateIngestCaptureServerOptions {
69
+ host?: string;
70
+ port?: number;
71
+ /**
72
+ * Profile returned by GET /v2/earner/cameras/sensing-profile. Default: a body
73
+ * with no custom fields so the connector keeps the DEFAULT schema. (Shape
74
+ * mirrors SensingProfileClient's expected response.)
75
+ */
76
+ profileBody?: Record<string, unknown>;
77
+ }
78
+ /**
79
+ * Start a fake Trillboards API on loopback that the connector's
80
+ * AudienceAnalyzeClient + SensingProfileClient talk to. Captures preAnalyzed
81
+ * ingests; serves an (empty by default) sensing profile.
82
+ * POST /v2/earner/audience-analyze → 200, captures preAnalyzed
83
+ * GET /v2/earner/cameras/sensing-profile → 200 {profile}
84
+ * Any other path → 404.
85
+ */
86
+ export declare function createIngestCaptureServer(options?: CreateIngestCaptureServerOptions): Promise<IngestCaptureServer>;
87
+ export interface PersonAssertion {
88
+ ok: boolean;
89
+ reason: string;
90
+ personCount: number | null;
91
+ peoplePresent: boolean | null;
92
+ }
93
+ /**
94
+ * E2E acceptance over the captured ingests: at least one preAnalyzed result
95
+ * indicates a PERSON — person_count>=1 OR people_present===true. Returns a
96
+ * structured verdict (ok + a human-readable reason) the runner turns into an
97
+ * exit code. The provenance the connector stamps (source='vlm_edge') is also
98
+ * asserted so a result that bypassed the connector can't satisfy the gate.
99
+ */
100
+ export declare function assertPreAnalyzedHasPerson(captured: CapturedIngest[]): PersonAssertion;
101
+ //# sourceMappingURL=localSenseE2eHarness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localSenseE2eHarness.d.ts","sourceRoot":"","sources":["../src/localSenseE2eHarness.ts"],"names":[],"mappings":"AA8BA,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBzB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAahG;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,YAAY,IAAI,MAAM,CAAC;IACvB,gFAAgF;IAChF,aAAa,IAAI,MAAM,EAAE,CAAC;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,aAAa,CAAC,CAmDxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5C,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,WAAW,IAAI,cAAc,EAAE,CAAC;IAChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,gCAAgC;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,OAAO,CAAC,mBAAmB,CAAC,CA6D9B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,eAAe,CAmCtF"}