@trillboards/edge-sdk 0.2.3 → 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.
- package/README.md +4 -3
- package/deploy/docker/Dockerfile.cloud-sense +117 -0
- package/deploy/docker/Dockerfile.cpu +30 -21
- package/deploy/docker/Dockerfile.cuda +30 -21
- package/deploy/docker/Dockerfile.openvino +30 -20
- package/deploy/docker/README.md +9 -4
- package/deploy/docker/connector-entrypoint.sh +122 -0
- package/deploy/helm/README.md +1 -1
- package/deploy/k8s/daemonset.yaml +1 -1
- package/deploy/macos/README.md +161 -0
- package/deploy/macos/TrillboardsSetupApp.m +603 -0
- package/deploy/macos/build-macos-pkg.sh +588 -0
- package/deploy/macos/com.trillboards.edge.plist +62 -0
- package/deploy/macos/com.trillboards.litertlm.plist +45 -0
- package/deploy/macos/pair.command +519 -0
- package/deploy/macos/scripts/ensure-litertlm.sh +62 -0
- package/deploy/macos/scripts/postinstall +153 -0
- package/deploy/macos/scripts/setup-litertlm.sh +272 -0
- package/deploy/macos/test-install-pair.sh +383 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +109 -0
- package/deploy/systemd/install.sh +25 -15
- package/deploy/systemd/trillboards-edge.service +4 -1
- package/deploy/windows/README.md +70 -0
- package/deploy/windows/build-windows.ps1 +225 -0
- package/deploy/windows/bundle-windows-installer.ps1 +153 -0
- package/deploy/windows/check-feeds.js +26 -0
- package/deploy/windows/e2e/fixtures/person.jpg +0 -0
- package/deploy/windows/e2e/test-local-sense.ps1 +185 -0
- package/deploy/windows/install-update.ps1 +101 -0
- package/deploy/windows/installer.nsi +138 -0
- package/deploy/windows/register-litertlm-task.ps1 +42 -0
- package/deploy/windows/register-task.ps1 +44 -0
- package/deploy/windows/run-connector.ps1 +69 -0
- package/deploy/windows/setup-litertlm.ps1 +361 -0
- package/deploy/windows/stop-existing.ps1 +72 -0
- package/deploy/windows/trillboards-edge-task.vbs +25 -0
- package/deploy/windows/trillboards-edge.cmd +33 -0
- package/deploy/windows/trillboards-pair.cmd +276 -0
- package/deploy/windows/trillboards-setup.ps1 +456 -0
- package/deploy/windows/trillboards-setup.vbs +13 -0
- package/dist/AudienceAnalyzeClient.d.ts +136 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -0
- package/dist/AudienceAnalyzeClient.js +246 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -0
- package/dist/ChannelProber.d.ts +150 -0
- package/dist/ChannelProber.d.ts.map +1 -0
- package/dist/ChannelProber.js +887 -0
- package/dist/ChannelProber.js.map +1 -0
- package/dist/CloudEscalationPolicy.d.ts +143 -0
- package/dist/CloudEscalationPolicy.d.ts.map +1 -0
- package/dist/CloudEscalationPolicy.js +137 -0
- package/dist/CloudEscalationPolicy.js.map +1 -0
- package/dist/CloudSensingManager.d.ts +533 -0
- package/dist/CloudSensingManager.d.ts.map +1 -0
- package/dist/CloudSensingManager.js +1254 -0
- package/dist/CloudSensingManager.js.map +1 -0
- package/dist/DiscoveryReportClient.d.ts +66 -0
- package/dist/DiscoveryReportClient.d.ts.map +1 -0
- package/dist/DiscoveryReportClient.js +85 -0
- package/dist/DiscoveryReportClient.js.map +1 -0
- package/dist/EdgeAgent.d.ts +12 -2
- package/dist/EdgeAgent.d.ts.map +1 -1
- package/dist/EdgeAgent.js +122 -65
- package/dist/EdgeAgent.js.map +1 -1
- package/dist/LiteRtVlmProvider.d.ts +193 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -0
- package/dist/LiteRtVlmProvider.js +436 -0
- package/dist/LiteRtVlmProvider.js.map +1 -0
- package/dist/MosaicSource.d.ts +108 -0
- package/dist/MosaicSource.d.ts.map +1 -0
- package/dist/MosaicSource.js +274 -0
- package/dist/MosaicSource.js.map +1 -0
- package/dist/OnvifClient.d.ts +83 -0
- package/dist/OnvifClient.d.ts.map +1 -0
- package/dist/OnvifClient.js +209 -0
- package/dist/OnvifClient.js.map +1 -0
- package/dist/PairClient.d.ts +106 -0
- package/dist/PairClient.d.ts.map +1 -0
- package/dist/PairClient.js +186 -0
- package/dist/PairClient.js.map +1 -0
- package/dist/RecorderAddressRecovery.d.ts +53 -0
- package/dist/RecorderAddressRecovery.d.ts.map +1 -0
- package/dist/RecorderAddressRecovery.js +359 -0
- package/dist/RecorderAddressRecovery.js.map +1 -0
- package/dist/SceneChangeDetector.d.ts +291 -0
- package/dist/SceneChangeDetector.d.ts.map +1 -0
- package/dist/SceneChangeDetector.js +682 -0
- package/dist/SceneChangeDetector.js.map +1 -0
- package/dist/SensingProfileClient.d.ts +59 -0
- package/dist/SensingProfileClient.d.ts.map +1 -0
- package/dist/SensingProfileClient.js +78 -0
- package/dist/SensingProfileClient.js.map +1 -0
- package/dist/SetupSessionClient.d.ts +114 -0
- package/dist/SetupSessionClient.d.ts.map +1 -0
- package/dist/SetupSessionClient.js +95 -0
- package/dist/SetupSessionClient.js.map +1 -0
- package/dist/VlmAdmissionGate.d.ts +126 -0
- package/dist/VlmAdmissionGate.d.ts.map +1 -0
- package/dist/VlmAdmissionGate.js +166 -0
- package/dist/VlmAdmissionGate.js.map +1 -0
- package/dist/autoSense.d.ts +216 -0
- package/dist/autoSense.d.ts.map +1 -0
- package/dist/autoSense.js +521 -0
- package/dist/autoSense.js.map +1 -0
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +511 -21
- package/dist/cli.js.map +1 -1
- package/dist/combinedSense.d.ts +128 -0
- package/dist/combinedSense.d.ts.map +1 -0
- package/dist/combinedSense.js +261 -0
- package/dist/combinedSense.js.map +1 -0
- package/dist/config.d.ts +351 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +99 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorCadence.d.ts +135 -0
- package/dist/connectorCadence.d.ts.map +1 -0
- package/dist/connectorCadence.js +151 -0
- package/dist/connectorCadence.js.map +1 -0
- package/dist/dataDir.d.ts +33 -0
- package/dist/dataDir.d.ts.map +1 -0
- package/dist/dataDir.js +76 -0
- package/dist/dataDir.js.map +1 -0
- package/dist/deployedCadence.d.ts +167 -0
- package/dist/deployedCadence.d.ts.map +1 -0
- package/dist/deployedCadence.js +209 -0
- package/dist/deployedCadence.js.map +1 -0
- package/dist/deployedCadenceSource.d.ts +48 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -0
- package/dist/deployedCadenceSource.js +74 -0
- package/dist/deployedCadenceSource.js.map +1 -0
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.d.ts.map +1 -1
- package/dist/diagnose.js +13 -12
- package/dist/diagnose.js.map +1 -1
- package/dist/discovery.d.ts +131 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +499 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gemmaModelOta.d.ts +118 -0
- package/dist/gemmaModelOta.d.ts.map +1 -0
- package/dist/gemmaModelOta.js +291 -0
- package/dist/gemmaModelOta.js.map +1 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +41 -18
- package/dist/init.js.map +1 -1
- package/dist/keepAwake.d.ts +33 -0
- package/dist/keepAwake.d.ts.map +1 -0
- package/dist/keepAwake.js +54 -0
- package/dist/keepAwake.js.map +1 -0
- package/dist/legacyRecorderProfile.d.ts +20 -0
- package/dist/legacyRecorderProfile.d.ts.map +1 -0
- package/dist/legacyRecorderProfile.js +27 -0
- package/dist/legacyRecorderProfile.js.map +1 -0
- package/dist/localInferenceSchema.d.ts +132 -0
- package/dist/localInferenceSchema.d.ts.map +1 -0
- package/dist/localInferenceSchema.js +274 -0
- package/dist/localInferenceSchema.js.map +1 -0
- package/dist/localSenseE2eHarness.d.ts +101 -0
- package/dist/localSenseE2eHarness.d.ts.map +1 -0
- package/dist/localSenseE2eHarness.js +279 -0
- package/dist/localSenseE2eHarness.js.map +1 -0
- package/dist/localSensing.d.ts +268 -0
- package/dist/localSensing.d.ts.map +1 -0
- package/dist/localSensing.js +492 -0
- package/dist/localSensing.js.map +1 -0
- package/dist/modelIntegrity.d.ts +59 -0
- package/dist/modelIntegrity.d.ts.map +1 -0
- package/dist/modelIntegrity.js +165 -0
- package/dist/modelIntegrity.js.map +1 -0
- package/dist/pairVerify.d.ts +100 -0
- package/dist/pairVerify.d.ts.map +1 -0
- package/dist/pairVerify.js +296 -0
- package/dist/pairVerify.js.map +1 -0
- package/dist/qrScanner.d.ts +33 -0
- package/dist/qrScanner.d.ts.map +1 -0
- package/dist/qrScanner.js +103 -0
- package/dist/qrScanner.js.map +1 -0
- package/dist/runtimePaths.d.ts +10 -0
- package/dist/runtimePaths.d.ts.map +1 -0
- package/dist/runtimePaths.js +79 -0
- package/dist/runtimePaths.js.map +1 -0
- package/dist/sensingRunMode.d.ts +68 -0
- package/dist/sensingRunMode.d.ts.map +1 -0
- package/dist/sensingRunMode.js +346 -0
- package/dist/sensingRunMode.js.map +1 -0
- package/dist/setupMode.d.ts +98 -0
- package/dist/setupMode.d.ts.map +1 -0
- package/dist/setupMode.js +828 -0
- package/dist/setupMode.js.map +1 -0
- package/dist/setupProgress.d.ts +31 -0
- package/dist/setupProgress.d.ts.map +1 -0
- package/dist/setupProgress.js +173 -0
- package/dist/setupProgress.js.map +1 -0
- package/package.json +13 -4
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildStubGemmaResult = buildStubGemmaResult;
|
|
37
|
+
exports.buildStubChatCompletion = buildStubChatCompletion;
|
|
38
|
+
exports.createStubVlmServer = createStubVlmServer;
|
|
39
|
+
exports.createIngestCaptureServer = createIngestCaptureServer;
|
|
40
|
+
exports.assertPreAnalyzedHasPerson = assertPreAnalyzedHasPerson;
|
|
41
|
+
/**
|
|
42
|
+
* localSenseE2eHarness — the reusable, unit-tested pieces of the on-device
|
|
43
|
+
* (LOCAL-sense) connector E2E. Two tiny localhost HTTP servers + one assertion:
|
|
44
|
+
*
|
|
45
|
+
* 1. createStubVlmServer() — stands in for `litert-lm serve` :9379
|
|
46
|
+
* (OpenAI-compatible). Health on GET /v1/models; POST /v1/chat/completions
|
|
47
|
+
* returns a FIXED, schema-conforming Gemma JSON envelope (person_count>=1).
|
|
48
|
+
* This is the "wiring" VLM: it proves the connector → serve → preAnalyzed →
|
|
49
|
+
* assert plumbing on EVERY CI run WITHOUT the ~2.4 GB model download.
|
|
50
|
+
*
|
|
51
|
+
* 2. createIngestCaptureServer() — stands in for the Trillboards API. Returns
|
|
52
|
+
* 200 on POST /v2/earner/audience-analyze and CAPTURES the `preAnalyzed`
|
|
53
|
+
* object the connector POSTs, plus the X-Device-Token. GET
|
|
54
|
+
* /v2/earner/cameras/sensing-profile returns an empty profile so the
|
|
55
|
+
* connector uses its DEFAULT schema (no operator custom fields needed).
|
|
56
|
+
*
|
|
57
|
+
* 3. assertPreAnalyzedHasPerson() — the E2E acceptance: the captured
|
|
58
|
+
* preAnalyzed carries person_count>=1 (or people_present===true).
|
|
59
|
+
*
|
|
60
|
+
* The connector's REAL local-sense path runs against these (built cli.js
|
|
61
|
+
* `local-sense` → LiteRtVlmProvider → LocalSensingClient → AudienceAnalyzeClient),
|
|
62
|
+
* so the wiring job exercises shipped code end-to-end. The REAL-model smoke
|
|
63
|
+
* swaps server (1) for an actual `litert-lm serve` + Gemma 4 E2B and reuses the
|
|
64
|
+
* SAME schema/prompt/provider via scripts/verify-gemma4-vlm.mjs.
|
|
65
|
+
*
|
|
66
|
+
* Node built-ins only (http) — no extra dependency, trivially testable.
|
|
67
|
+
*/
|
|
68
|
+
const http = __importStar(require("http"));
|
|
69
|
+
/**
|
|
70
|
+
* Build a DEFAULT-schema-conforming structured result with a person present.
|
|
71
|
+
* Matches buildLocalSensingSchema()'s required keys + enum members exactly, so
|
|
72
|
+
* LiteRtVlmProvider's client-side validation passes on the first attempt. This
|
|
73
|
+
* is the object the stub VLM "infers" — it must look like real Gemma output.
|
|
74
|
+
*/
|
|
75
|
+
function buildStubGemmaResult(options = {}) {
|
|
76
|
+
const personCount = options.personCount ?? 2;
|
|
77
|
+
const peoplePresent = options.peoplePresent ?? personCount > 0;
|
|
78
|
+
return {
|
|
79
|
+
audience_group_composition: 'friend_group',
|
|
80
|
+
audience_intent_stage: 'browsing',
|
|
81
|
+
audience_activity_macro: 'social_dwell',
|
|
82
|
+
audience_ethnicity: 'mixed',
|
|
83
|
+
audience_engagement_narrative: 'A small group stands near the screen, attentive and likely receptive to a relevant offer.',
|
|
84
|
+
person_count: personCount,
|
|
85
|
+
crowd_density: personCount >= 4 ? 'crowded' : personCount > 0 ? 'sparse' : 'empty',
|
|
86
|
+
safety_concern: false,
|
|
87
|
+
scene_description: 'Two people stand in a well-lit retail space facing the display.',
|
|
88
|
+
ad_receptivity_score: 0.7,
|
|
89
|
+
people_present: peoplePresent,
|
|
90
|
+
confidence: 0.9,
|
|
91
|
+
...(options.extra || {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wrap a structured result in the OpenAI chat-completions envelope litert-lm
|
|
96
|
+
* serve returns (choices[0].message.content is the minified JSON string).
|
|
97
|
+
*/
|
|
98
|
+
function buildStubChatCompletion(result) {
|
|
99
|
+
return {
|
|
100
|
+
id: 'chatcmpl-stub',
|
|
101
|
+
object: 'chat.completion',
|
|
102
|
+
model: 'gemma-4-E2B-it.litertlm',
|
|
103
|
+
choices: [
|
|
104
|
+
{
|
|
105
|
+
index: 0,
|
|
106
|
+
finish_reason: 'stop',
|
|
107
|
+
message: { role: 'assistant', content: JSON.stringify(result) },
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Start a fake `litert-lm serve` (OpenAI-compatible) on loopback. Answers:
|
|
114
|
+
* GET /v1/models → 200 (health probe)
|
|
115
|
+
* POST /v1/chat/completions → 200 {choices:[{message:{content:<json>}}]}
|
|
116
|
+
* Any other path → 404. Resolves once listening (baseUrl/port populated).
|
|
117
|
+
*/
|
|
118
|
+
function createStubVlmServer(options = {}) {
|
|
119
|
+
const host = options.host || '127.0.0.1';
|
|
120
|
+
const result = options.result || buildStubGemmaResult();
|
|
121
|
+
const completion = JSON.stringify(buildStubChatCompletion(result));
|
|
122
|
+
const modelIds = options.modelIds && options.modelIds.length
|
|
123
|
+
? options.modelIds
|
|
124
|
+
: ['gemma-4-E2B-it.litertlm'];
|
|
125
|
+
let chatCalls = 0;
|
|
126
|
+
const chatModels = [];
|
|
127
|
+
const server = http.createServer((req, res) => {
|
|
128
|
+
const url = req.url || '';
|
|
129
|
+
const bodyChunks = [];
|
|
130
|
+
req.on('data', (c) => bodyChunks.push(c));
|
|
131
|
+
req.on('end', () => {
|
|
132
|
+
if (req.method === 'GET' && url.endsWith('/v1/models')) {
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
134
|
+
res.end(JSON.stringify({ object: 'list', data: modelIds.map((id) => ({ id, object: 'model' })) }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (req.method === 'POST' && url.endsWith('/v1/chat/completions')) {
|
|
138
|
+
chatCalls += 1;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(Buffer.concat(bodyChunks).toString('utf-8'));
|
|
141
|
+
if (typeof parsed?.model === 'string')
|
|
142
|
+
chatModels.push(parsed.model);
|
|
143
|
+
}
|
|
144
|
+
catch { /* a malformed body is the caller's problem, not the stub's */ }
|
|
145
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(completion);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
150
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
server.on('error', reject);
|
|
155
|
+
server.listen(options.port ?? 0, host, () => {
|
|
156
|
+
const port = server.address().port;
|
|
157
|
+
resolve({
|
|
158
|
+
baseUrl: `http://${host}:${port}`,
|
|
159
|
+
port,
|
|
160
|
+
getChatCalls: () => chatCalls,
|
|
161
|
+
getChatModels: () => [...chatModels],
|
|
162
|
+
close: () => new Promise((res) => {
|
|
163
|
+
server.close(() => res());
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Start a fake Trillboards API on loopback that the connector's
|
|
171
|
+
* AudienceAnalyzeClient + SensingProfileClient talk to. Captures preAnalyzed
|
|
172
|
+
* ingests; serves an (empty by default) sensing profile.
|
|
173
|
+
* POST /v2/earner/audience-analyze → 200, captures preAnalyzed
|
|
174
|
+
* GET /v2/earner/cameras/sensing-profile → 200 {profile}
|
|
175
|
+
* Any other path → 404.
|
|
176
|
+
*/
|
|
177
|
+
function createIngestCaptureServer(options = {}) {
|
|
178
|
+
const host = options.host || '127.0.0.1';
|
|
179
|
+
const profileBody = options.profileBody ?? { profileName: null, fields: [] };
|
|
180
|
+
const captured = [];
|
|
181
|
+
const server = http.createServer((req, res) => {
|
|
182
|
+
const url = req.url || '';
|
|
183
|
+
const chunks = [];
|
|
184
|
+
req.on('data', (c) => chunks.push(c));
|
|
185
|
+
req.on('end', () => {
|
|
186
|
+
if (req.method === 'GET' && url.includes('/v2/earner/cameras/sensing-profile')) {
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify(profileBody));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (req.method === 'POST' && url.endsWith('/v2/earner/audience-analyze')) {
|
|
192
|
+
let body = null;
|
|
193
|
+
try {
|
|
194
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
body = null;
|
|
198
|
+
}
|
|
199
|
+
const deviceToken = req.headers['x-device-token'] ||
|
|
200
|
+
req.headers['x-trillboard-device-token'] ||
|
|
201
|
+
null;
|
|
202
|
+
const pre = body && typeof body.preAnalyzed === 'object' && body.preAnalyzed !== null
|
|
203
|
+
? body.preAnalyzed
|
|
204
|
+
: null;
|
|
205
|
+
captured.push({
|
|
206
|
+
preAnalyzed: pre,
|
|
207
|
+
deviceToken,
|
|
208
|
+
hasImage: !!(body && typeof body.imageBase64 === 'string' && body.imageBase64.length > 0),
|
|
209
|
+
capturedAt: body && typeof body.capturedAt === 'number' ? body.capturedAt : null,
|
|
210
|
+
raw: body,
|
|
211
|
+
});
|
|
212
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
213
|
+
res.end(JSON.stringify({ status: true, ok: true, profileId: 'p_e2e' }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
217
|
+
res.end(JSON.stringify({ error: 'not found' }));
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
server.on('error', reject);
|
|
222
|
+
server.listen(options.port ?? 0, host, () => {
|
|
223
|
+
const port = server.address().port;
|
|
224
|
+
resolve({
|
|
225
|
+
baseUrl: `http://${host}:${port}`,
|
|
226
|
+
port,
|
|
227
|
+
getCaptured: () => captured,
|
|
228
|
+
close: () => new Promise((res) => {
|
|
229
|
+
server.close(() => res());
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* E2E acceptance over the captured ingests: at least one preAnalyzed result
|
|
237
|
+
* indicates a PERSON — person_count>=1 OR people_present===true. Returns a
|
|
238
|
+
* structured verdict (ok + a human-readable reason) the runner turns into an
|
|
239
|
+
* exit code. The provenance the connector stamps (source='vlm_edge') is also
|
|
240
|
+
* asserted so a result that bypassed the connector can't satisfy the gate.
|
|
241
|
+
*/
|
|
242
|
+
function assertPreAnalyzedHasPerson(captured) {
|
|
243
|
+
if (!Array.isArray(captured) || captured.length === 0) {
|
|
244
|
+
return { ok: false, reason: 'no preAnalyzed ingest was received', personCount: null, peoplePresent: null };
|
|
245
|
+
}
|
|
246
|
+
for (const c of captured) {
|
|
247
|
+
const pre = c.preAnalyzed;
|
|
248
|
+
if (!pre)
|
|
249
|
+
continue;
|
|
250
|
+
const pc = typeof pre.person_count === 'number' ? pre.person_count : null;
|
|
251
|
+
const pp = typeof pre.people_present === 'boolean' ? pre.people_present : null;
|
|
252
|
+
const source = typeof pre.source === 'string' ? pre.source : null;
|
|
253
|
+
const hasPerson = (pc !== null && pc >= 1) || pp === true;
|
|
254
|
+
if (hasPerson) {
|
|
255
|
+
if (source !== 'vlm_edge') {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
reason: `preAnalyzed had a person but source='${source}' (expected connector-stamped 'vlm_edge')`,
|
|
259
|
+
personCount: pc,
|
|
260
|
+
peoplePresent: pp,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
reason: `preAnalyzed person_count=${pc} people_present=${pp} source=${source}`,
|
|
266
|
+
personCount: pc,
|
|
267
|
+
peoplePresent: pp,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const first = captured.find((c) => c.preAnalyzed)?.preAnalyzed ?? null;
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
reason: `received ${captured.length} ingest(s) but none had person_count>=1 / people_present=true (first: ${JSON.stringify(first)?.slice(0, 200)})`,
|
|
275
|
+
personCount: null,
|
|
276
|
+
peoplePresent: null,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=localSenseE2eHarness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localSenseE2eHarness.js","sourceRoot":"","sources":["../src/localSenseE2eHarness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { CameraProvider } from '@trillboards/edge-core';
|
|
2
|
+
import { AudienceAnalyzeClient, type AudienceAnalyzeResponse, type CameraHealthReport, type HttpPoster } from './AudienceAnalyzeClient';
|
|
3
|
+
import { LiteRtVlmProvider, type LiteRtAcceleratorHealth } from './LiteRtVlmProvider';
|
|
4
|
+
import { SensingProfileClient } from './SensingProfileClient';
|
|
5
|
+
import { type ResolvedFeed, type SensingClient, type SensingLoopDeps } from './CloudSensingManager';
|
|
6
|
+
import type { SharedFrameSource } from './MosaicSource';
|
|
7
|
+
import { type LocalSchemaFieldSet } from './localInferenceSchema';
|
|
8
|
+
import { type CloudEscalationPolicyOptions, type CloudEscalationStats } from './CloudEscalationPolicy';
|
|
9
|
+
import type { EdgeAgentConfig } from './config';
|
|
10
|
+
/**
|
|
11
|
+
* Map a deployed profile's SELF-DESCRIBING type to the on-device field set.
|
|
12
|
+
*
|
|
13
|
+
* A Sense-Anything (`custom_intent`) profile asked a specific operational
|
|
14
|
+
* question about the operator's own venue, so the on-device model is asked for
|
|
15
|
+
* that profile's fields plus the presence primitives — NOT the 12 DOOH audience
|
|
16
|
+
* fields, which answer nothing the operator asked and cost 1.09-1.53x the
|
|
17
|
+
* per-frame latency depending on delegate (measured; see localInferenceSchema's
|
|
18
|
+
* header). Every other type — a seeded builtin preset, or NO deployed profile at
|
|
19
|
+
* all — is a screen/audience measurement deployment and keeps the full audience
|
|
20
|
+
* contract.
|
|
21
|
+
*
|
|
22
|
+
* This reads the kind the SERVER already reports rather than an allowlist of
|
|
23
|
+
* profile names maintained on the device.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fieldSetForProfileType(profileType: string | null | undefined): LocalSchemaFieldSet;
|
|
26
|
+
export interface LocalSensingClientOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Fetches the deployed Sense-Anything profile for this feed's screen so the
|
|
29
|
+
* on-device VLM emits the operator's custom fields (cloud-parity). When
|
|
30
|
+
* omitted, the client uses only the default schema/prompt.
|
|
31
|
+
*/
|
|
32
|
+
profileClient?: SensingProfileClient;
|
|
33
|
+
/** Profile cache TTL (re-fetch interval). Default 5 min. */
|
|
34
|
+
profileTtlMs?: number;
|
|
35
|
+
/** Override the base schema (tests). Defaults to buildLocalSensingSchema(). */
|
|
36
|
+
schema?: Record<string, unknown>;
|
|
37
|
+
/** Override the base prompt (tests). Defaults to buildLocalSensingPrompt(). */
|
|
38
|
+
prompt?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Send the analyzed JPEG alongside `preAnalyzed` so the server can attach it as
|
|
41
|
+
* the alert evidence photo. Even when true the frame is sent ONLY for
|
|
42
|
+
* potentially alert-worthy frames (any non-empty/meaningful signal) — see
|
|
43
|
+
* isAlertWorthy(). Default: true.
|
|
44
|
+
*/
|
|
45
|
+
attachEvidenceFrame?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Per-frame resilience: when ON-DEVICE inference THROWS for a single frame,
|
|
48
|
+
* POST that frame to the CLOUD VLM (uploadClient.analyze) instead of losing it.
|
|
49
|
+
* Reuses the SAME upload client (no new provider). Opt-in (default false) so
|
|
50
|
+
* the plain local-sense path is unchanged; auto-sense's local branch enables it.
|
|
51
|
+
*/
|
|
52
|
+
cloudFallbackOnError?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Confidence-based CLOUD ESCALATION (Phase 6d). When the on-device model
|
|
55
|
+
* flags a CANDIDATE — a frame carrying real signal, judged by the SAME
|
|
56
|
+
* `isAlertWorthy` gate that governs the evidence frame — send THAT ONE frame
|
|
57
|
+
* to the cloud VLM for adjudication instead of shipping the local verdict.
|
|
58
|
+
*
|
|
59
|
+
* Distinct from `cloudFallbackOnError` (a per-frame degrade when local
|
|
60
|
+
* inference THROWS) and from `EdgeAgentConfig.localSensing.allowCloudFallback`
|
|
61
|
+
* (a whole-mode switch when the local runtime is unavailable). Those two reach
|
|
62
|
+
* for the cloud because the local path is BROKEN; this one because the local
|
|
63
|
+
* path WORKED and its judgement is worth confirming.
|
|
64
|
+
*
|
|
65
|
+
* Off unless `enabled: true` — see CloudEscalationPolicy.
|
|
66
|
+
*/
|
|
67
|
+
cloudEscalation?: CloudEscalationPolicyOptions;
|
|
68
|
+
/** Feed label used only in the escalation log line. */
|
|
69
|
+
feedName?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether an on-device result is potentially ALERT-WORTHY — the gate that
|
|
73
|
+
* keeps the evidence frame OFF the wire for an EMPTY/uninteresting scene (privacy
|
|
74
|
+
* + bandwidth) while still attaching it for any observation an operator route
|
|
75
|
+
* could fire on. Sense-Anything routes evaluate gt/gte/lt/lte/eq/neq over
|
|
76
|
+
* numbers (counts AND gauges), booleans, and strings/enums (routeEvaluatorService
|
|
77
|
+
* `compare()`), so the gate cannot be limited to "true boolean / positive count"
|
|
78
|
+
* — a gauge route (`avg_wait_minutes gt 5`) or an enum route
|
|
79
|
+
* (`shelf_state eq "empty"`) would otherwise fire WITHOUT a snapshot.
|
|
80
|
+
*
|
|
81
|
+
* A frame is attached when the result carries ANY meaningful signal:
|
|
82
|
+
* - any boolean field that is `true` (safety_concern, people_present, a custom
|
|
83
|
+
* shelf_empty, …);
|
|
84
|
+
* - any positive finite number — a count (person_count / *_count) OR a gauge
|
|
85
|
+
* (avg_wait_minutes, ad_receptivity_score, …);
|
|
86
|
+
* - any non-empty array (a populated `list` custom field), or non-empty object
|
|
87
|
+
* (a `distribution` field);
|
|
88
|
+
* - any string/enum whose value is NOT an empty-scene sentinel
|
|
89
|
+
* (''/none/unclear/empty/unknown) — e.g. shelf_state='low'.
|
|
90
|
+
* Non-discriminative keys (source, captured_at, confidence, scene_description)
|
|
91
|
+
* are ignored — see NON_DISCRIMINATIVE_KEYS for why.
|
|
92
|
+
*
|
|
93
|
+
* KNOWN BOUNDARY (deliberate privacy tradeoff): a route that alerts on ABSENCE
|
|
94
|
+
* — `person_count lte 0`, `people_present eq false`, `shelf_state eq "empty"` —
|
|
95
|
+
* fires on a genuinely empty scene, which this gate suppresses by design (a
|
|
96
|
+
* blank-room frame is exactly the privacy harm the gate exists to prevent). The
|
|
97
|
+
* server owns the route predicates and can request a frame for those routes;
|
|
98
|
+
* operators who want every frame can disable the gate (attachEvidenceFrame).
|
|
99
|
+
*/
|
|
100
|
+
export declare function isAlertWorthy(result: Record<string, unknown>): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* A stable fingerprint of the ALERT-BEARING state of one result — "has the
|
|
103
|
+
* operator-visible situation changed?" in one comparable string.
|
|
104
|
+
*
|
|
105
|
+
* Built from EXACTLY the keys `isAlertWorthy` judges (NON_DISCRIMINATIVE_KEYS
|
|
106
|
+
* excluded), so the two can never drift: the same fields that decide whether a
|
|
107
|
+
* frame carries signal decide whether that signal is NEW. Keys are sorted so
|
|
108
|
+
* the model's property order cannot manufacture a change, and values are
|
|
109
|
+
* compared as emitted — a `queue_length` of 3 and of 4 are genuinely different
|
|
110
|
+
* facts and an operator route may key on exactly that boundary, so this
|
|
111
|
+
* deliberately does NOT bucket numbers into invented bands.
|
|
112
|
+
*
|
|
113
|
+
* Used by CloudEscalationPolicy's novelty bound: an unchanged signature means a
|
|
114
|
+
* verdict the cloud has already adjudicated, so the frame stays on-premises.
|
|
115
|
+
*/
|
|
116
|
+
export declare function alertSignature(result: Record<string, unknown>): string;
|
|
117
|
+
/**
|
|
118
|
+
* Is cloud escalation actually ON for this local-sensing configuration?
|
|
119
|
+
*
|
|
120
|
+
* TWO settings decide it, and this is the ONLY place that combines them:
|
|
121
|
+
* `cloudEscalation.enabled` opts in, and `attachEvidenceFrame: false` — the
|
|
122
|
+
* operator's "no frame ever leaves this machine" switch — outranks it, because
|
|
123
|
+
* escalation sends a frame. `LocalSensingClient` enforces the rule and the
|
|
124
|
+
* startup banner reports it; they must read the same expression or the banner
|
|
125
|
+
* will one day claim an egress posture the client is not running.
|
|
126
|
+
*/
|
|
127
|
+
export declare function isCloudEscalationEnabled(options: {
|
|
128
|
+
attachEvidenceFrame?: boolean;
|
|
129
|
+
cloudEscalation?: {
|
|
130
|
+
enabled?: boolean;
|
|
131
|
+
};
|
|
132
|
+
} | null | undefined): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* LocalSensingClient — the SensingClient the cadence loop uses in LOCAL mode.
|
|
135
|
+
* It runs the VLM ON-DEVICE (LiteRtVlmProvider — Gemma 4 on LiteRT-LM via
|
|
136
|
+
* plain-completion + client-side schema validation, the real CCTV runtime) and
|
|
137
|
+
* POSTs the STRUCTURED result as `preAnalyzed` (uploadClient.analyzePreAnalyzed),
|
|
138
|
+
* skipping the cloud VLM. Implements the same `analyze({jpeg,captureMode})`
|
|
139
|
+
* shape AudienceAnalyzeClient exposes, so CloudSensingManager runs it UNCHANGED.
|
|
140
|
+
*
|
|
141
|
+
* PROFILE-DRIVEN: before analyzing, it fetches (and caches) the screen's active
|
|
142
|
+
* Sense-Anything profile via SensingProfileClient and rebuilds the constrained
|
|
143
|
+
* schema + prompt to include the operator's CUSTOM fields — the on-device
|
|
144
|
+
* equivalent of the cloud `buildProfileDrivenJsonSchema`. So a deployed alert
|
|
145
|
+
* like "queue_length >= 3" makes Gemma emit `queue_length`, which flows through
|
|
146
|
+
* preAnalyzed → OFV → the route → the webhook, exactly like the cloud VLM. The
|
|
147
|
+
* fetch is best-effort: on any failure the last-good (or default) schema is kept,
|
|
148
|
+
* so on-device sensing NEVER goes silent.
|
|
149
|
+
*
|
|
150
|
+
* The connector OVERRIDES `source='vlm_edge'` and `captured_at` (the actual
|
|
151
|
+
* capture time) — never trusting the model for provenance/time. The SERVER
|
|
152
|
+
* additionally forces provenance `local_connector` on the preAnalyzed ingest,
|
|
153
|
+
* gating local-inferred data out of the sold bidstream while still firing the
|
|
154
|
+
* operator's Sense-Anything routes.
|
|
155
|
+
*
|
|
156
|
+
* ── CLOUD ESCALATION (opt-in, Phase 6d) ─────────────────────────────────────
|
|
157
|
+
* Local Gemma is the always-on gate; the CLOUD is the judge for the frames it
|
|
158
|
+
* flags. When `cloudEscalation.enabled` and the policy admits this frame, the
|
|
159
|
+
* SAME JPEG is POSTed through the SAME upload client's `analyze()` — the cloud
|
|
160
|
+
* path the connector already owns — and the cloud VLM's verdict becomes the
|
|
161
|
+
* observation. Three properties are load-bearing:
|
|
162
|
+
*
|
|
163
|
+
* 1. ONE FRAME, ONE OBSERVATION. An escalated frame does NOT also post its
|
|
164
|
+
* local `preAnalyzed` result. Posting both would double-count the same
|
|
165
|
+
* physical frame and put two verdicts for one moment in the stream —
|
|
166
|
+
* exactly the dual write the codebase forbids. The cloud verdict
|
|
167
|
+
* supersedes; the local one was a sampling decision, not a datum.
|
|
168
|
+
* 2. NO PROVENANCE LAUNDERING. What lands is what the CLOUD inferred from a
|
|
169
|
+
* real frame, so the server's normal cloud provenance is the truthful
|
|
170
|
+
* stamp — identical to `cloud-sense` and to the existing
|
|
171
|
+
* `cloudFallbackOnError` frame. Nothing the local model produced enters
|
|
172
|
+
* that record, so `ingest_source='local_connector'` (which exists to keep
|
|
173
|
+
* operator-forgeable structured input out of the sold bidstream) does not
|
|
174
|
+
* apply and is not evaded: there IS no local structured input on this path.
|
|
175
|
+
* 3. THE PRIVACY GATE IS UNCHANGED. Escalation reuses `isAlertWorthy`, so the
|
|
176
|
+
* set of frames that can leave the premises is exactly the set that
|
|
177
|
+
* already leaves as evidence photos — and `attachEvidenceFrame: false`
|
|
178
|
+
* (the operator's "no frame ever leaves" switch) disables escalation
|
|
179
|
+
* outright. An empty scene never escalates.
|
|
180
|
+
*
|
|
181
|
+
* A failed cloud call falls back to posting the local result, so an escalation
|
|
182
|
+
* can degrade the ACCURACY of one frame but can never LOSE it.
|
|
183
|
+
*/
|
|
184
|
+
export declare class LocalSensingClient implements SensingClient {
|
|
185
|
+
private readonly localProvider;
|
|
186
|
+
private readonly uploadClient;
|
|
187
|
+
private readonly profileClient?;
|
|
188
|
+
private readonly profileTtlMs;
|
|
189
|
+
private readonly baseSchema;
|
|
190
|
+
private readonly basePrompt;
|
|
191
|
+
private cachedSchema;
|
|
192
|
+
private cachedPrompt;
|
|
193
|
+
private lastProfileFetchMs;
|
|
194
|
+
/** Send the analyzed JPEG (gated to alert-worthy frames) as evidence. Default true. */
|
|
195
|
+
private readonly attachEvidenceFrame;
|
|
196
|
+
/** On a local-inference throw, POST that one frame to the cloud VLM. Default false. */
|
|
197
|
+
private readonly cloudFallbackOnError;
|
|
198
|
+
/**
|
|
199
|
+
* Rate/novelty bound on cloud escalation for THIS feed. Per-client (not
|
|
200
|
+
* global) on purpose: the budget an operator reasons about is "per camera",
|
|
201
|
+
* and one busy camera must not spend a quiet one's confirmations.
|
|
202
|
+
*/
|
|
203
|
+
private readonly escalationPolicy;
|
|
204
|
+
/** Feed label for the escalation log line. */
|
|
205
|
+
private readonly feedName;
|
|
206
|
+
/** The active profile name (for logging / observability). */
|
|
207
|
+
activeProfileName: string | null;
|
|
208
|
+
/**
|
|
209
|
+
* Which field set the active profile resolved to (for logging / observability):
|
|
210
|
+
* 'operator' for a Sense-Anything profile, 'audience' for a DOOH preset or no
|
|
211
|
+
* profile. See fieldSetForProfileType.
|
|
212
|
+
*/
|
|
213
|
+
activeFieldSet: LocalSchemaFieldSet;
|
|
214
|
+
constructor(localProvider: LiteRtVlmProvider, uploadClient: AudienceAnalyzeClient, options?: LocalSensingClientOptions);
|
|
215
|
+
getEndpoint(): string;
|
|
216
|
+
reportCameraHealth(cameraHealth: CameraHealthReport): Promise<AudienceAnalyzeResponse>;
|
|
217
|
+
/**
|
|
218
|
+
* Re-fetch the active profile if the cache is stale and rebuild the schema +
|
|
219
|
+
* prompt from its custom fields AND its KIND. Best-effort: a fetch failure
|
|
220
|
+
* keeps the existing schema (never disrupts an in-flight profile).
|
|
221
|
+
*
|
|
222
|
+
* The field set follows the profile's own reported type (fieldSetForProfileType):
|
|
223
|
+
* a Sense-Anything profile gets its declared fields plus the presence
|
|
224
|
+
* primitives; a seeded DOOH preset — or a profile that declares nothing —
|
|
225
|
+
* keeps the full audience contract.
|
|
226
|
+
*/
|
|
227
|
+
private refreshProfileIfStale;
|
|
228
|
+
/**
|
|
229
|
+
* What the on-device runtime resolved about this host's accelerator (which
|
|
230
|
+
* litert-lm delegate it selected, and why it is not on the GPU when it is not).
|
|
231
|
+
* Surfaced so an operator/support can see a silent CPU fallback without
|
|
232
|
+
* log-diving. See LiteRtVlmProvider.getAcceleratorHealth.
|
|
233
|
+
*/
|
|
234
|
+
getAcceleratorHealth(): LiteRtAcceleratorHealth;
|
|
235
|
+
/**
|
|
236
|
+
* Cloud-escalation counters for this feed — how many candidate frames were
|
|
237
|
+
* adjudicated by the cloud VLM, and why the rest were not. Read by
|
|
238
|
+
* `CloudSensingManager.getSensingHealth()` so escalation shows up on the
|
|
239
|
+
* operator's periodic capacity line: sending frames off-premises must be
|
|
240
|
+
* VISIBLE, not merely documented.
|
|
241
|
+
*/
|
|
242
|
+
getEscalationStats(): CloudEscalationStats;
|
|
243
|
+
analyze(request: {
|
|
244
|
+
jpeg: Buffer;
|
|
245
|
+
captureMode?: string;
|
|
246
|
+
}): Promise<AudienceAnalyzeResponse>;
|
|
247
|
+
}
|
|
248
|
+
/** Injection points for tests (default to the real providers/clients). */
|
|
249
|
+
export interface BuildLocalSensingDeps {
|
|
250
|
+
rtspProviderFactory?: (feed: ResolvedFeed) => CameraProvider;
|
|
251
|
+
streamUrlResolver?: (feed: ResolvedFeed, currentUrl: string, error: Error) => Promise<string | null>;
|
|
252
|
+
localProviderFactory?: (feed: ResolvedFeed) => LiteRtVlmProvider;
|
|
253
|
+
uploadClientFactory?: (feed: ResolvedFeed) => AudienceAnalyzeClient;
|
|
254
|
+
profileClientFactory?: (feed: ResolvedFeed) => SensingProfileClient | undefined;
|
|
255
|
+
httpPoster?: HttpPoster;
|
|
256
|
+
/** Shared per-mosaic-source pull registry so the local VLM attaches to the SAME
|
|
257
|
+
* SharedFrameSource the on-device zoo uses (one RTSP pull for both rails). */
|
|
258
|
+
sharedSources?: Map<string, SharedFrameSource>;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Build per-feed capture loops for LOCAL sensing — one RTSP provider + one
|
|
262
|
+
* LocalSensingClient (local VLM + preAnalyzed upload) per feed, each under its
|
|
263
|
+
* OWN device token. Each client fetches its screen's active Sense-Anything
|
|
264
|
+
* profile so the on-device VLM emits that screen's custom fields. Mirrors
|
|
265
|
+
* buildSensingLoops but swaps the cloud client for the local one.
|
|
266
|
+
*/
|
|
267
|
+
export declare function buildLocalSensingLoops(config: EdgeAgentConfig, deps?: BuildLocalSensingDeps): SensingLoopDeps[];
|
|
268
|
+
//# sourceMappingURL=localSensing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localSensing.d.ts","sourceRoot":"","sources":["../src/localSensing.ts"],"names":[],"mappings":"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"}
|