@cef-ai/cli 0.2.6 → 0.3.0

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 (127) hide show
  1. package/README.md +75 -1247
  2. package/dist/build/duration.d.ts +22 -0
  3. package/dist/build/duration.d.ts.map +1 -0
  4. package/dist/build/duration.js +44 -0
  5. package/dist/build/duration.js.map +1 -0
  6. package/dist/build/esbuild-plugin.d.ts +32 -0
  7. package/dist/build/esbuild-plugin.d.ts.map +1 -0
  8. package/dist/build/esbuild-plugin.js +53 -0
  9. package/dist/build/esbuild-plugin.js.map +1 -0
  10. package/dist/build/lints.d.ts +55 -0
  11. package/dist/build/lints.d.ts.map +1 -0
  12. package/dist/build/lints.js +214 -0
  13. package/dist/build/lints.js.map +1 -0
  14. package/dist/build/load-config.d.ts +114 -0
  15. package/dist/build/load-config.d.ts.map +1 -0
  16. package/dist/build/load-config.js +150 -0
  17. package/dist/build/load-config.js.map +1 -0
  18. package/dist/build/manifest.d.ts +156 -0
  19. package/dist/build/manifest.d.ts.map +1 -0
  20. package/dist/build/manifest.js +111 -0
  21. package/dist/build/manifest.js.map +1 -0
  22. package/dist/build/targeting.d.ts +15 -0
  23. package/dist/build/targeting.d.ts.map +1 -0
  24. package/dist/build/targeting.js +69 -0
  25. package/dist/build/targeting.js.map +1 -0
  26. package/dist/cli.d.ts +2 -2
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +107 -62
  29. package/dist/cli.js.map +1 -0
  30. package/dist/commands/build.d.ts +41 -0
  31. package/dist/commands/build.d.ts.map +1 -0
  32. package/dist/commands/build.js +252 -0
  33. package/dist/commands/build.js.map +1 -0
  34. package/dist/commands/inspect.d.ts +49 -0
  35. package/dist/commands/inspect.d.ts.map +1 -0
  36. package/dist/commands/inspect.js +135 -0
  37. package/dist/commands/inspect.js.map +1 -0
  38. package/dist/commands/typegen.d.ts +36 -0
  39. package/dist/commands/typegen.d.ts.map +1 -0
  40. package/dist/commands/typegen.js +210 -0
  41. package/dist/commands/typegen.js.map +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +6 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lib.d.ts +12 -0
  47. package/dist/lib.d.ts.map +1 -0
  48. package/dist/lib.js +9 -0
  49. package/dist/lib.js.map +1 -0
  50. package/dist/typegen/augment.d.ts +57 -0
  51. package/dist/typegen/augment.d.ts.map +1 -0
  52. package/dist/typegen/augment.js +87 -0
  53. package/dist/typegen/augment.js.map +1 -0
  54. package/dist/typegen/fetch.d.ts +30 -0
  55. package/dist/typegen/fetch.d.ts.map +1 -0
  56. package/dist/typegen/fetch.js +38 -0
  57. package/dist/typegen/fetch.js.map +1 -0
  58. package/dist/typegen/lockfile.d.ts +42 -0
  59. package/dist/typegen/lockfile.d.ts.map +1 -0
  60. package/dist/typegen/lockfile.js +58 -0
  61. package/dist/typegen/lockfile.js.map +1 -0
  62. package/dist/typegen/schema-to-ts.d.ts +21 -0
  63. package/dist/typegen/schema-to-ts.d.ts.map +1 -0
  64. package/dist/typegen/schema-to-ts.js +29 -0
  65. package/dist/typegen/schema-to-ts.js.map +1 -0
  66. package/package.json +20 -58
  67. package/dist/commands/auth/index.d.ts +0 -2
  68. package/dist/commands/auth/index.js +0 -162
  69. package/dist/commands/deploy/clone.d.ts +0 -13
  70. package/dist/commands/deploy/clone.js +0 -143
  71. package/dist/commands/deploy/delete.d.ts +0 -14
  72. package/dist/commands/deploy/delete.js +0 -134
  73. package/dist/commands/deploy/deploy.d.ts +0 -24
  74. package/dist/commands/deploy/deploy.js +0 -654
  75. package/dist/commands/deploy/index.d.ts +0 -2
  76. package/dist/commands/deploy/index.js +0 -216
  77. package/dist/commands/deploy/status.d.ts +0 -14
  78. package/dist/commands/deploy/status.js +0 -61
  79. package/dist/commands/dev/index.d.ts +0 -2
  80. package/dist/commands/dev/index.js +0 -134
  81. package/dist/commands/dev/server.d.ts +0 -14
  82. package/dist/commands/dev/server.js +0 -195
  83. package/dist/commands/dev/static/app.js +0 -633
  84. package/dist/commands/dev/static/index.html +0 -205
  85. package/dist/commands/dev/static/static/app.js +0 -633
  86. package/dist/commands/dev/static/static/index.html +0 -205
  87. package/dist/commands/dev/static/static/style.css +0 -184
  88. package/dist/commands/dev/static/style.css +0 -184
  89. package/dist/commands/playground/index.d.ts +0 -2
  90. package/dist/commands/playground/index.js +0 -75
  91. package/dist/commands/playground/test.d.ts +0 -15
  92. package/dist/commands/playground/test.js +0 -166
  93. package/dist/commands/service/index.d.ts +0 -2
  94. package/dist/commands/service/index.js +0 -75
  95. package/dist/lib/api.d.ts +0 -146
  96. package/dist/lib/api.js +0 -330
  97. package/dist/lib/config.d.ts +0 -12
  98. package/dist/lib/config.js +0 -145
  99. package/dist/lib/credentials.d.ts +0 -19
  100. package/dist/lib/credentials.js +0 -68
  101. package/dist/lib/runtime/agent-router.d.ts +0 -12
  102. package/dist/lib/runtime/agent-router.js +0 -67
  103. package/dist/lib/runtime/context-builder.d.ts +0 -27
  104. package/dist/lib/runtime/context-builder.js +0 -166
  105. package/dist/lib/runtime/cubby-manager.d.ts +0 -20
  106. package/dist/lib/runtime/cubby-manager.js +0 -162
  107. package/dist/lib/runtime/index.d.ts +0 -51
  108. package/dist/lib/runtime/index.js +0 -181
  109. package/dist/lib/runtime/log-collector.d.ts +0 -17
  110. package/dist/lib/runtime/log-collector.js +0 -61
  111. package/dist/lib/runtime/stream-manager.d.ts +0 -10
  112. package/dist/lib/runtime/stream-manager.js +0 -77
  113. package/dist/lib/runtime/types.d.ts +0 -76
  114. package/dist/lib/runtime/types.js +0 -2
  115. package/dist/lib/transpile.d.ts +0 -1
  116. package/dist/lib/transpile.js +0 -9
  117. package/dist/lib/typecheck.d.ts +0 -31
  118. package/dist/lib/typecheck.js +0 -175
  119. package/dist/lib/types.d.ts +0 -246
  120. package/dist/lib/types.js +0 -2
  121. package/dist/lib/utils.d.ts +0 -4
  122. package/dist/lib/utils.js +0 -33
  123. package/example/.env.example +0 -6
  124. package/example/agents/object-detection/tasks/yolo.ts +0 -28
  125. package/example/agents/parking-violation-detector/tasks/detect.ts +0 -326
  126. package/example/cef.config.yaml +0 -91
  127. package/example/engagements/engagement.ts +0 -902
@@ -1,902 +0,0 @@
1
- // RAFT: Multi-Stream Synchronization for Drone Data
2
- // Handles: DRONE_TELEMETRY_DATA, VIDEO_STREAM_DATA, THERMAL_STREAM_DATA, VIDEO_KLV_DATA
3
-
4
- const SYNC_WINDOW_MS = 3000;
5
- const BUFFER_TTL_MS = 300000;
6
- const VIOLATION_WINDOW_MS = 30000; //120000; // 2 minutes – aggregation flush window
7
- const VIOLATION_GRID_SIZE = 50; // px – bbox quantization
8
- const MAX_FRAME_REFS = 5;
9
- const VIOLATION_STALE_MS = 60000; // 60s – max gap to consider "same sighting"
10
- const DRONE_POS_PRECISION = 4; // decimal places ≈ ~11m grid
11
- const MIN_DETECTION_CONFIDENCE = 0.80;
12
- const VEHICLE_LABELS = new Set(['car', 'truck', 'van', 'bus', 'vehicle']);
13
-
14
- // ─────────────────────────────────────────────────────────────
15
- // Mission Registry
16
- // ─────────────────────────────────────────────────────────────
17
-
18
- async function registerMission(missionId, droneName, timestamp, cubby, context) {
19
- const t0 = Date.now();
20
- const listKey = 'list_missions';
21
- let missions = [];
22
- try {
23
- missions = await cubby.json.get(listKey);
24
- } catch (_) {}
25
- if (!Array.isArray(missions)) missions = [];
26
-
27
- const idx = missions.findIndex(m => m.missionId === missionId);
28
-
29
- if (idx === -1) {
30
- missions.push({
31
- missionId,
32
- droneName,
33
- firstSeen: timestamp,
34
- lastSeen: timestamp,
35
- });
36
- context.log(`📝 Registered new mission: ${missionId}`);
37
- } else {
38
- missions[idx].lastSeen = timestamp;
39
- if (droneName) missions[idx].droneName = droneName;
40
- }
41
-
42
- await cubby.json.set(listKey, missions);
43
- context.log(`⏱ registerMission ${Date.now() - t0}ms`);
44
- }
45
-
46
-
47
- // ─────────────────────────────────────────────────────────────
48
- // Timestamp Index Helpers
49
- // ─────────────────────────────────────────────────────────────
50
-
51
- async function appendTimestamp(missionId, streamType, timestamp, cubby) {
52
- const indexKey = `mission/${missionId}/${streamType}/_index`;
53
- let timestamps = []
54
- try{
55
- timestamps = await cubby.json.get(indexKey);
56
- }
57
- catch(error){
58
-
59
- }
60
- const cutoff = Date.now() - BUFFER_TTL_MS;
61
- if (timestamps.length > 0 && timestamps[0] < cutoff) {
62
- timestamps = timestamps.filter(ts => ts > cutoff);
63
- }
64
-
65
- timestamps.push(timestamp);
66
- await cubby.json.set(indexKey, timestamps);
67
- }
68
-
69
- async function getTimestamps(missionId, streamType, cubby, context?) {
70
- let data = []
71
- try{
72
- data = await cubby.json.get(`mission/${missionId}/${streamType}/_index`)
73
- }
74
- catch(error){
75
- if (context) context.log(`⚠️ getTimestamps(${streamType}) failed: ${error.message}`);
76
- }
77
- if (!Array.isArray(data)) data = [];
78
- return data
79
- }
80
-
81
- // ─────────────────────────────────────────────────────────────
82
- // Storage Functions
83
- // ─────────────────────────────────────────────────────────────
84
-
85
- async function storeTelemetry(missionId, timestamp, data, context, cubby) {
86
- const t0 = Date.now();
87
- const key = `mission/${missionId}/telemetry/${timestamp}`;
88
- const value = { ...data, storedAt: Date.now() };
89
- await Promise.all([
90
- cubby.json.set(key, value),
91
- appendTimestamp(missionId, 'telemetry', timestamp, cubby),
92
- ]);
93
- context.log(`✅ Stored telemetry for mission ${missionId} at ${timestamp} (${Date.now() - t0}ms)`);
94
- }
95
-
96
- async function storeRgbFrame(missionId, timestamp, data, context, cubby) {
97
- const t0 = Date.now();
98
- const key = `mission/${missionId}/rgb/${timestamp}`;
99
-
100
- let detectedObjects = null;
101
- const tYolo = Date.now();
102
- try {
103
- const result = await context.agents.objectDetection.yolo({
104
- image: data.image,
105
- objectsToFind: ["a car", "a vehicle", "a truck"],
106
- minConfidence: 0.03,
107
- });
108
- if(result?.detections){
109
- detectedObjects = result?.detections.filter(d =>
110
- d.confidence >= MIN_DETECTION_CONFIDENCE && VEHICLE_LABELS.has(d.label.toLowerCase())
111
- )
112
- }
113
- } catch (error) {
114
- context.log(`ObjectDetectionFailedWithError: ${error.message}`);
115
- }
116
- context.log(`⏱ YOLO detection ${Date.now() - tYolo}ms | detections: ${detectedObjects?.length ?? 0}`);
117
-
118
- const value = {
119
- detectedObjects: detectedObjects,
120
- index: data.index,
121
- frameId: data.frameId,
122
- framePath: data.framePath,
123
- frameWidth: data.frameWidth,
124
- timestamp: timestamp,
125
- droneName: data.droneName,
126
- storedAt: Date.now(),
127
- };
128
-
129
- await Promise.all([
130
- cubby.json.set(key, value),
131
- appendTimestamp(missionId, 'rgb', timestamp, cubby),
132
- ]);
133
- context.log(`✅ Stored RGB frame #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
134
- }
135
-
136
- async function storeThermalFrame(missionId, timestamp, data, context, cubby) {
137
- const t0 = Date.now();
138
- const key = `mission/${missionId}/thermal/${timestamp}`;
139
- const value = {
140
- frameId: data.frameId,
141
- framePath: data.framePath,
142
- frameWidth: data.frameWidth,
143
- index: data.index,
144
- timestamp: timestamp,
145
- droneName: data.droneName,
146
- storedAt: Date.now(),
147
- };
148
-
149
- await Promise.all([
150
- cubby.json.set(key, value),
151
- appendTimestamp(missionId, 'thermal', timestamp, cubby),
152
- ]);
153
- context.log(`✅ Stored thermal frame #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
154
- }
155
-
156
- async function storeKlvData(missionId, timestamp, data, context, cubby) {
157
- const t0 = Date.now();
158
- const key = `mission/${missionId}/klv/${timestamp}`;
159
- const value = {
160
- klv: data.klv,
161
- index: data.index,
162
- timestamp: timestamp,
163
- droneName: data.droneName,
164
- storedAt: Date.now(),
165
- };
166
-
167
- await Promise.all([
168
- cubby.json.set(key, value),
169
- appendTimestamp(missionId, 'klv', timestamp, cubby),
170
- ]);
171
- context.log(`✅ Stored KLV data #${data.index} for mission ${missionId} (${Date.now() - t0}ms)`);
172
- }
173
-
174
- // ─────────────────────────────────────────────────────────────
175
- // Query Functions
176
- // ─────────────────────────────────────────────────────────────
177
-
178
- async function getLatestSynced(missionId, context, cubby) {
179
- const key = `mission/${missionId}/synced/latest`;
180
- const data = await cubby.json.get(key);
181
- return { success: true, data: data || null };
182
- }
183
-
184
- async function getAllSynced(missionId, context, cubby) {
185
- const pattern = `mission/${missionId}/synced/*`;
186
- const keys = await cubby.json.keys(pattern);
187
-
188
- if (!keys || keys.length === 0) {
189
- return { success: true, data: [] };
190
- }
191
-
192
- const dataKeys = keys.filter(k => !k.endsWith('/latest'));
193
- if (dataKeys.length === 0) {
194
- return { success: true, data: [] };
195
- }
196
-
197
- const allData = await cubby.json.mget(dataKeys);
198
- const dataArray = Object.values(allData)
199
- .filter(item => item !== null)
200
- //@ts-ignore
201
- .sort((a, b) => a.timestamp - b.timestamp);
202
-
203
- return { success: true, data: dataArray };
204
- }
205
-
206
- async function getSyncedRange(missionId, startTime, endTime, context, cubby) {
207
- const pattern = `mission/${missionId}/synced/*`;
208
- const keys = await cubby.json.keys(pattern);
209
-
210
- if (!keys || keys.length === 0) {
211
- return { success: true, data: [] };
212
- }
213
-
214
- const filteredKeys = keys.filter(k => {
215
- if (k.endsWith('/latest')) return false;
216
- const ts = parseInt(k.split('/').pop(), 10);
217
- return ts >= startTime && ts <= endTime;
218
- });
219
-
220
- if (filteredKeys.length === 0) {
221
- return { success: true, data: [] };
222
- }
223
-
224
- const allData = await cubby.json.mget(filteredKeys);
225
- const dataArray = Object.values(allData)
226
- .filter(item => item !== null)
227
- //@ts-ignore
228
- .sort((a, b) => a.timestamp - b.timestamp);
229
-
230
- return { success: true, data: dataArray };
231
- }
232
-
233
- // ─────────────────────────────────────────────────────────────
234
- // Violation Helpers
235
- // ─────────────────────────────────────────────────────────────
236
-
237
- function computeViolationGridKey(bbox) {
238
- const cx = bbox.x + bbox.w / 2;
239
- const cy = bbox.y + bbox.h / 2;
240
- const gridX = Math.floor(cx / VIOLATION_GRID_SIZE);
241
- const gridY = Math.floor(cy / VIOLATION_GRID_SIZE);
242
- return `${gridX}_${gridY}`;
243
- }
244
-
245
- function quantizeDronePosition(lat, lng) {
246
- const factor = Math.pow(10, DRONE_POS_PRECISION);
247
- return `${Math.floor(lat * factor)}_${Math.floor(lng * factor)}`;
248
- }
249
-
250
- function buildDedupKey(classLabel, bbox, dronePosition) {
251
- const bboxKey = computeViolationGridKey(bbox);
252
- const posKey = dronePosition
253
- ? quantizeDronePosition(dronePosition.lat, dronePosition.lng)
254
- : "nopos";
255
- return `${posKey}/${classLabel}/${bboxKey}`;
256
- }
257
-
258
- // ─────────────────────────────────────────────────────────────
259
- // Violation Buffer + Windowed Flush
260
- // ─────────────────────────────────────────────────────────────
261
-
262
- async function bufferViolations(missionId, processedRGB, dronePosition, cubby, context) {
263
- const t0 = Date.now();
264
- const newEntries = [];
265
-
266
- for (const rgb of processedRGB) {
267
- const violations = rgb.violationDetection?.value;
268
- if (!violations || !violations.length) continue;
269
-
270
- for (const v of violations) {
271
- if (v.violationType !== "outside_slot") continue;
272
-
273
- const classLabel = v.detection?.classLabel;
274
- const bbox = v.detection?.bbox;
275
- if (!classLabel || !bbox) continue;
276
-
277
- newEntries.push({
278
- classLabel,
279
- bbox,
280
- confidence: v.confidence,
281
- trackingId: v.detection.trackingId,
282
- timestamp: v.timestamp,
283
- framePath: rgb.framePath || null,
284
- frameId: rgb.frameId || null,
285
- dronePosition: dronePosition
286
- ? { lat: dronePosition.lat, lng: dronePosition.lng }
287
- : null,
288
- violationLocation:
289
- v.location?.lat != null && v.location?.lng != null
290
- ? { lat: v.location.lat, lng: v.location.lng }
291
- : null,
292
- // Store the original YOLO detection for traceability
293
- sourceDetection: rgb.detectedObjects?.find((d) => {
294
- const bw = d.box.xmax - d.box.xmin;
295
- const bh = d.box.ymax - d.box.ymin;
296
- return (
297
- Math.abs(d.box.xmin - bbox.x) < 1 &&
298
- Math.abs(d.box.ymin - bbox.y) < 1 &&
299
- Math.abs(bw - bbox.w) < 1 &&
300
- Math.abs(bh - bbox.h) < 1
301
- );
302
- }) || null,
303
- });
304
- }
305
- }
306
-
307
- if (newEntries.length === 0) return;
308
-
309
- const bufKey = `mission/${missionId}/violation_buf`;
310
- let buf = null;
311
- try {
312
- buf = await cubby.json.get(bufKey);
313
- } catch (error) {}
314
-
315
- if (!buf || !Array.isArray(buf.entries)) {
316
- buf = { startedAt: Date.now(), entries: [] };
317
- }
318
-
319
- buf.entries = buf.entries.concat(newEntries);
320
- await cubby.json.set(bufKey, buf);
321
-
322
- context.log(
323
- `📥 Buffered ${newEntries.length} violations (total in window: ${buf.entries.length}) (${Date.now() - t0}ms)`
324
- );
325
- }
326
-
327
- async function checkAndFlushViolations(missionId, cubby, context) {
328
- const t0 = Date.now();
329
- const bufKey = `mission/${missionId}/violation_buf`;
330
- let buf = null;
331
- try {
332
- buf = await cubby.json.get(bufKey);
333
- } catch (error) {}
334
-
335
- if (!buf || !Array.isArray(buf.entries) || buf.entries.length === 0) {
336
- return null;
337
- }
338
-
339
- const elapsed = Date.now() - buf.startedAt;
340
- if (elapsed < VIOLATION_WINDOW_MS) {
341
- return null; // Window not expired yet
342
- }
343
-
344
- context.log(
345
- `⏱️ Violation window expired (${Math.round(elapsed / 1000)}s, ${buf.entries.length} entries). Flushing...`
346
- );
347
-
348
- // ── 1. Deduplicate buffer entries in-memory ──────────────
349
- const grouped = {};
350
-
351
- for (const e of buf.entries) {
352
- const dedupKey = buildDedupKey(e.classLabel, e.bbox, e.dronePosition);
353
-
354
- if (!grouped[dedupKey]) {
355
- grouped[dedupKey] = {
356
- missionId,
357
- vehicleType: e.classLabel,
358
- violationType: "outside_slot",
359
- dedupKey,
360
- bboxGridKey: computeViolationGridKey(e.bbox),
361
- sightingsCount: 0,
362
- bestConfidence: 0,
363
- bestDetection: null,
364
- bestSourceDetection: null,
365
- firstSeenAt: e.timestamp,
366
- lastSeenAt: e.timestamp,
367
- dronePosition: e.dronePosition,
368
- violationLocation: e.violationLocation || e.dronePosition,
369
- representativeFramePath: null,
370
- lastFrameReferences: [],
371
- };
372
- }
373
-
374
- const rec = grouped[dedupKey];
375
- rec.sightingsCount += 1;
376
- if (e.timestamp < rec.firstSeenAt) rec.firstSeenAt = e.timestamp;
377
- if (e.timestamp > rec.lastSeenAt) rec.lastSeenAt = e.timestamp;
378
-
379
- if (e.violationLocation) {
380
- rec.violationLocation = e.violationLocation;
381
- }
382
-
383
- if (e.confidence > rec.bestConfidence) {
384
- rec.bestConfidence = e.confidence;
385
- rec.bestDetection = {
386
- classLabel: e.classLabel,
387
- trackingId: e.trackingId,
388
- confidence: e.confidence,
389
- bbox: e.bbox,
390
- };
391
- rec.bestSourceDetection = e.sourceDetection || null;
392
- rec.representativeFramePath = e.framePath;
393
- }
394
-
395
- rec.lastFrameReferences.push({
396
- framePath: e.framePath,
397
- frameId: e.frameId,
398
- timestamp: e.timestamp,
399
- confidence: e.confidence,
400
- violationBbox: e.bbox,
401
- location: e.violationLocation || e.dronePosition,
402
- });
403
- }
404
-
405
- // Cap frame references
406
- const dedupKeys = Object.keys(grouped);
407
- for (const dk of dedupKeys) {
408
- grouped[dk].lastFrameReferences = grouped[dk].lastFrameReferences.slice(
409
- -MAX_FRAME_REFS
410
- );
411
- }
412
-
413
- // ── 2. Merge with existing Redis aggregation records ─────
414
- const redisKeys = dedupKeys.map(
415
- (dk) => `mission/${missionId}/violation_agg/${dk}`
416
- );
417
-
418
- let existingMap = {};
419
- try {
420
- existingMap = await cubby.json.mget(redisKeys);
421
- } catch (error) {}
422
-
423
- const toWrite = {};
424
-
425
- for (const dk of dedupKeys) {
426
- const incoming = grouped[dk];
427
- const redisKey = `mission/${missionId}/violation_agg/${dk}`;
428
- const existing = existingMap[redisKey] || null;
429
-
430
- if (existing) {
431
- const gap = incoming.firstSeenAt - existing.lastSeenAt;
432
- if (gap > VIOLATION_STALE_MS) {
433
- toWrite[redisKey] = incoming;
434
- context.log(
435
- `🔄 Reset stale violation ${dk} (gap ${Math.round(gap / 1000)}s)`
436
- );
437
- continue;
438
- }
439
-
440
- existing.sightingsCount += incoming.sightingsCount;
441
- if (incoming.bestConfidence > existing.bestConfidence) {
442
- existing.bestConfidence = incoming.bestConfidence;
443
- existing.bestDetection = incoming.bestDetection;
444
- existing.bestSourceDetection = incoming.bestSourceDetection;
445
- existing.representativeFramePath = incoming.representativeFramePath;
446
- }
447
- if (incoming.firstSeenAt < existing.firstSeenAt) {
448
- existing.firstSeenAt = incoming.firstSeenAt;
449
- }
450
- if (incoming.lastSeenAt > existing.lastSeenAt) {
451
- existing.lastSeenAt = incoming.lastSeenAt;
452
- }
453
- if (incoming.dronePosition) {
454
- existing.dronePosition = incoming.dronePosition;
455
- }
456
- if (
457
- incoming.violationLocation &&
458
- (!incoming.dronePosition ||
459
- incoming.violationLocation.lat !== incoming.dronePosition.lat ||
460
- incoming.violationLocation.lng !== incoming.dronePosition.lng)
461
- ) {
462
- existing.violationLocation = incoming.violationLocation;
463
- } else if (!existing.violationLocation && incoming.violationLocation) {
464
- existing.violationLocation = incoming.violationLocation;
465
- }
466
-
467
- existing.lastFrameReferences = existing.lastFrameReferences
468
- .concat(incoming.lastFrameReferences)
469
- .slice(-MAX_FRAME_REFS);
470
-
471
- toWrite[redisKey] = existing;
472
- } else {
473
- toWrite[redisKey] = incoming;
474
- }
475
- }
476
-
477
- // ── 3. Batch write aggregated records ────────────────────
478
- await cubby.json.mset(toWrite);
479
-
480
- // ── 4. Update violation_agg index ────────────────────────
481
- const indexKey = `mission/${missionId}/violation_agg/_index`;
482
- let indexArr = [];
483
- try {
484
- indexArr = await cubby.json.get(indexKey);
485
- } catch (error) {}
486
- if (!Array.isArray(indexArr)) indexArr = [];
487
-
488
- const existingSet = new Set(indexArr);
489
- let indexChanged = false;
490
- for (const dk of dedupKeys) {
491
- if (!existingSet.has(dk)) {
492
- indexArr.push(dk);
493
- indexChanged = true;
494
- }
495
- }
496
- if (indexChanged) {
497
- await cubby.json.set(indexKey, indexArr);
498
- }
499
-
500
- // ── 5. Find closest synced packet and patch it ───────────
501
- const lastSeenAt = Math.max(...buf.entries.map((e) => e.timestamp));
502
- const syncedTimestamps = await getTimestamps(missionId, "telemetry", cubby, context);
503
-
504
- let closestSyncTs = null;
505
- let closestDelta = Infinity;
506
- for (const ts of syncedTimestamps) {
507
- const delta = Math.abs(ts - lastSeenAt);
508
- if (delta < closestDelta) {
509
- closestSyncTs = ts;
510
- closestDelta = delta;
511
- }
512
- }
513
-
514
- const aggregatedSummary = Object.values(toWrite).map((r:any) => ({
515
- vehicleType: r.vehicleType,
516
- dedupKey: r.dedupKey,
517
- bboxGridKey: r.bboxGridKey,
518
- sightingsCount: r.sightingsCount,
519
- bestConfidence: r.bestConfidence,
520
- firstSeenAt: r.firstSeenAt,
521
- lastSeenAt: r.lastSeenAt,
522
- violationLocation: r.violationLocation,
523
- dronePosition: r.dronePosition,
524
- representativeFramePath: r.representativeFramePath,
525
- }));
526
-
527
- if (closestSyncTs !== null) {
528
- const syncedKey = `mission/${missionId}/synced/${closestSyncTs}`;
529
- let syncedPacket = null;
530
- try {
531
- syncedPacket = await cubby.json.get(syncedKey);
532
- } catch (error) {}
533
-
534
- if (syncedPacket) {
535
- syncedPacket.aggregatedViolations = aggregatedSummary;
536
- syncedPacket.violationFlushedAt = Date.now();
537
- await cubby.json.set(syncedKey, syncedPacket);
538
- context.log(
539
- `📌 Attached ${dedupKeys.length} aggregated violations to synced packet at ${closestSyncTs}`
540
- );
541
- }
542
- }
543
-
544
- // ── 6. Append to flush log ───────────────────────────────
545
- const flushLogKey = `mission/${missionId}/violation_flush_log/_index`;
546
- let flushLog = [];
547
- try {
548
- flushLog = await cubby.json.get(flushLogKey);
549
- } catch (error) {}
550
- if (!Array.isArray(flushLog)) flushLog = [];
551
-
552
- flushLog.push({
553
- flushedAt: Date.now(),
554
- syncedTimestamp: closestSyncTs,
555
- violationCount: dedupKeys.length,
556
- totalSightings: buf.entries.length,
557
- windowDurationMs: elapsed,
558
- });
559
- await cubby.json.set(flushLogKey, flushLog);
560
-
561
- // ── 7. Clear the buffer ──────────────────────────────────
562
- await cubby.json.set(bufKey, { startedAt: Date.now(), entries: [] });
563
-
564
- context.log(
565
- `📊 Flushed ${dedupKeys.length} unique violations (${buf.entries.length} raw sightings) over ${Math.round(elapsed / 1000)}s window (flush took ${Date.now() - t0}ms)`
566
- );
567
-
568
- return aggregatedSummary;
569
- }
570
-
571
- /**
572
- * Retrieve all aggregated violations for a mission.
573
- */
574
- async function getAggregatedViolations(missionId, cubby) {
575
- const indexKey = `mission/${missionId}/violation_agg/_index`;
576
- let indexArr = [];
577
- try {
578
- indexArr = await cubby.json.get(indexKey);
579
- } catch (error) {}
580
- if (!Array.isArray(indexArr) || indexArr.length === 0) {
581
- return { success: true, data: [] };
582
- }
583
-
584
- const keys = indexArr.map(
585
- (dk) => `mission/${missionId}/violation_agg/${dk}`
586
- );
587
- const records = await cubby.json.mget(keys);
588
-
589
- const dataArray = Object.values(records)
590
- .filter((r) => r !== null)
591
- //@ts-ignore
592
- .sort((a, b) => b.bestConfidence - a.bestConfidence);
593
-
594
- return { success: true, data: dataArray };
595
- }
596
-
597
- // ─────────────────────────────────────────────────────────────
598
- // Synchronization Logic
599
- // ─────────────────────────────────────────────────────────────
600
-
601
- async function detectParkingViloations(mergedRgb, telemetryTimestamp, telemetry, context) {
602
- const t0 = Date.now();
603
- const processedRGB = await Promise.all(
604
- mergedRgb.map(async (rgb) => {
605
- const data = { ...rgb };
606
- if (!rgb.detectedObjects?.length || rgb.violationDetectionProcessed) {
607
- return data;
608
- }
609
-
610
- context.log(`Detected RGB object Detection`);
611
- const objectsToDetect = rgb.detectedObjects.filter((d) =>
612
- VEHICLE_LABELS.has(d.label.toLowerCase())
613
- );
614
-
615
- if (!objectsToDetect.length) return data;
616
-
617
- context.log(`needProcess:${objectsToDetect.length}`);
618
- try {
619
- context.log(`Processing violation detection...`);
620
- const violationDetection =
621
- await context.agents.parkingViolationDetector.detect({
622
- timestamp: telemetryTimestamp,
623
- detections: rgb.detectedObjects.map((item) => ({
624
- classLabel: item.label,
625
- bbox: {
626
- x: item.box.xmin,
627
- y: item.box.ymin,
628
- w: item.box.xmax - item.box.xmin,
629
- h: item.box.ymax - item.box.ymin,
630
- },
631
- trackingId: item.class_id,
632
- confidence: item.confidence,
633
- })),
634
- telemetry: {
635
- timestamp: telemetryTimestamp,
636
- position: { lat: telemetry.lat, lng: telemetry.lon },
637
- altitudeAGL: 65,
638
- heading: 75,
639
- pitch: 0,
640
- roll: 0,
641
- gimbalPitch: -85,
642
- hfov: 84,
643
- vfov: 63,
644
- },
645
- });
646
- // context.log(
647
- // `ProcessedViolationDetection:${JSON.stringify(violationDetection)}`
648
- // );
649
- data.violationDetection = violationDetection;
650
- data.violationDetectionProcessed = true;
651
- } catch (error) {
652
- context.log(`[ERROR]: Failed detect violation: ${error.message}`);
653
- data.violationDetectionProcessed = true;
654
- data.violationDetectionError = error.message;
655
- }
656
-
657
- return data;
658
- })
659
- );
660
-
661
- context.log(`⏱ detectParkingViolations ${Date.now() - t0}ms | ${mergedRgb.length} frames`);
662
- return processedRGB;
663
- }
664
-
665
- // ─── Optimized: index lookup + mget instead of keys() + N×get ───
666
-
667
- async function findAllFramesInWindow(missionId, streamType, targetTimestamp, cubby, context) {
668
- const t0 = Date.now();
669
- const timestamps = await getTimestamps(missionId, streamType, cubby, context);
670
-
671
- if (timestamps.length === 0) return [];
672
-
673
- const matchingTimestamps = timestamps.filter(
674
- ts => Math.abs(ts - targetTimestamp) <= SYNC_WINDOW_MS
675
- );
676
-
677
- if (matchingTimestamps.length === 0) return [];
678
-
679
- const keys = matchingTimestamps.map(ts => `mission/${missionId}/${streamType}/${ts}`);
680
- const framesMap = await cubby.json.mget(keys);
681
-
682
- const matches = [];
683
- for (const key of keys) {
684
- const frame = framesMap[key];
685
- if (frame) {
686
- const ts = parseInt(key.split('/').pop(), 10);
687
- frame.timeDelta = ts - targetTimestamp;
688
- matches.push(frame);
689
- }
690
- }
691
-
692
- matches.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
693
- context.log(`⏱ findFrames(${streamType}) ${Date.now() - t0}ms | ${matches.length} matches`);
694
- return matches;
695
- }
696
-
697
- // ─── Optimized: parallel fetches in attemptSync ───
698
-
699
- async function attemptSync(missionId, telemetryTimestamp, context, cubby) {
700
- const t0 = Date.now();
701
- try {
702
- const telemetryKey = `mission/${missionId}/telemetry/${telemetryTimestamp}`;
703
- const syncedKey = `mission/${missionId}/synced/${telemetryTimestamp}`;
704
- let telemetry
705
- let existing = null
706
- try{
707
- telemetry = await cubby.json.get(telemetryKey)
708
- }
709
- catch(error){
710
-
711
- }
712
- try{
713
- existing = await cubby.json.get(syncedKey)
714
- } catch(error){
715
-
716
- }
717
-
718
- const [rgbFrames, thermalFrames, klvPackets] = await Promise.all([
719
- findAllFramesInWindow(missionId, "rgb", telemetryTimestamp, cubby, context),
720
- findAllFramesInWindow(missionId, "thermal", telemetryTimestamp, cubby, context),
721
- findAllFramesInWindow(missionId, "klv", telemetryTimestamp, cubby, context),
722
- ]);
723
-
724
- if (!telemetry) return;
725
-
726
- const mergedRgb = rgbFrames.length > 0 ? rgbFrames : (existing?.rgb || []);
727
- const mergedThermal = thermalFrames.length > 0 ? thermalFrames : (existing?.thermal || []);
728
- const mergedKlv = klvPackets.length > 0 ? klvPackets : (existing?.klv || []);
729
-
730
- const hasRgb = mergedRgb.length > 0;
731
- const hasThermal = mergedThermal.length > 0;
732
- const hasKlv = mergedKlv.length > 0;
733
- const streamCount = [hasRgb, hasThermal, hasKlv].filter(Boolean).length;
734
-
735
- let syncQuality;
736
- if (streamCount === 3) syncQuality = "full";
737
- else if (streamCount === 0) syncQuality = "telemetry-only";
738
- else syncQuality = "partial";
739
-
740
- const processedRGB = await detectParkingViloations(mergedRgb, telemetryTimestamp, telemetry, context);
741
-
742
- // ── Buffer violations (append only, no aggregation yet) ─
743
- const telData = telemetry.payload || telemetry;
744
- const dronePosition =
745
- telData.lat != null && telData.lon != null
746
- ? { lat: telData.lat, lng: telData.lon }
747
- : null;
748
-
749
- await bufferViolations(missionId, processedRGB, dronePosition, cubby, context);
750
-
751
- // ── Check if 2-minute window expired → flush ────────────
752
- const flushed = await checkAndFlushViolations(missionId, cubby, context);
753
-
754
- const syncedPacket = {
755
- timestamp: telemetryTimestamp,
756
- missionId,
757
- droneName: telemetry.payload?.name || telemetry.droneName,
758
- syncQuality,
759
- syncedAt: Date.now(),
760
- telemetry: telData,
761
- rgb: processedRGB,
762
- thermal: mergedThermal,
763
- klv: mergedKlv.map(f =>
764
- f.data ? f : { data: f.klv, index: f.index, timestamp: f.timestamp, timeDelta: f.timeDelta }
765
- ),
766
- };
767
-
768
- // Only attach aggregatedViolations when a flush actually happened
769
- if (flushed) {
770
- //@ts-ignore
771
- syncedPacket.aggregatedViolations = flushed;
772
- //@ts-ignore
773
- syncedPacket.violationFlushedAt = Date.now();
774
- }
775
-
776
- const latestKey = `mission/${missionId}/synced/latest`;
777
- await cubby.json.set(syncedKey, syncedPacket)
778
- let currentLatest = null
779
- try{
780
- currentLatest = cubby.json.get(latestKey)
781
- } catch(error){
782
-
783
- }
784
-
785
- const qualityRank = { "telemetry-only": 0, partial: 1, full: 2 };
786
- if (
787
- !currentLatest ||
788
- syncedPacket.timestamp >= currentLatest.timestamp ||
789
- qualityRank[syncQuality] >= qualityRank[currentLatest.syncQuality || "telemetry-only"]
790
- ) {
791
- await cubby.json.set(latestKey, syncedPacket);
792
- }
793
-
794
- context.log(
795
- `🔗 Synced for mission ${missionId} [${syncQuality}] ` +
796
- `RGB: ${mergedRgb.length}, Thermal: ${mergedThermal.length}, KLV: ${mergedKlv.length} (${Date.now() - t0}ms)`
797
- );
798
- } catch (error) {
799
- context.log(`❌ Sync failed for mission ${missionId}: ${error.message}`);
800
- }
801
- }
802
-
803
- // ─── Optimized: index lookup instead of keys() scan ───
804
-
805
- async function attemptSyncFromFrame(missionId, frameTimestamp, context, cubby) {
806
- const t0 = Date.now();
807
- try {
808
- const timestamps = await getTimestamps(missionId, 'telemetry', cubby, context);
809
- if (timestamps.length === 0) {
810
- context.log(`⏱ attemptSyncFromFrame: no telemetry timestamps yet for ${missionId} (${Date.now() - t0}ms)`);
811
- return;
812
- }
813
-
814
- let closestTs = null;
815
- let closestDelta = Infinity;
816
-
817
- for (const ts of timestamps) {
818
- const delta = Math.abs(ts - frameTimestamp);
819
- if (delta <= SYNC_WINDOW_MS && delta < closestDelta) {
820
- closestTs = ts;
821
- closestDelta = delta;
822
- }
823
- }
824
- if (closestTs !== null) {
825
- context.log(`⏱ attemptSyncFromFrame: matched telemetry ts=${closestTs} (delta ${closestDelta}ms) from ${timestamps.length} candidates`);
826
- await attemptSync(missionId, closestTs, context, cubby);
827
- } else {
828
- const nearest = timestamps.reduce((best, ts) => {
829
- const d = Math.abs(ts - frameTimestamp);
830
- return d < best.delta ? { ts, delta: d } : best;
831
- }, { ts: null, delta: Infinity });
832
- context.log(`⏱ attemptSyncFromFrame: no telemetry within ${SYNC_WINDOW_MS}ms window | nearest delta=${nearest.delta}ms (ts=${nearest.ts}), frameTs=${frameTimestamp}, ${timestamps.length} candidates`);
833
- }
834
- context.log(`⏱ attemptSyncFromFrame ${Date.now() - t0}ms`);
835
- } catch (error) {
836
- context.log(`❌ Sync from frame failed: ${error.message} (${Date.now() - t0}ms)`);
837
- }
838
- }
839
-
840
- // ─────────────────────────────────────────────────────────────
841
- // Main Handler
842
- // ─────────────────────────────────────────────────────────────
843
-
844
- function bytesToString(bytes) {
845
- var str = "";
846
- for (var i = 0; i < bytes.length; i++) {
847
- str += String.fromCharCode(bytes[i]);
848
- }
849
- return str;
850
- }
851
-
852
- async function handle(event, context) {
853
- const cubby = await context.cubby('syncMission');
854
- const streamId = event.payload?.streamId || event.streamId;
855
-
856
- if (!streamId) {
857
- context.log('❌ Missing streamId in event payload');
858
- return;
859
- }
860
-
861
- const stream = await context.streams.subscribe(streamId);
862
- context.log('✅ Successfully subscribed to Stream');
863
-
864
- for await (const packet of stream) {
865
- const seqNum = packet.sequenceNum;
866
- const ctx = Object.create(context);
867
- ctx.log = (msg) => context.log(`[seq:${seqNum}] ${msg}`);
868
-
869
- try {
870
- const payloadStr = bytesToString(packet.payload);
871
- const data = JSON.parse(payloadStr);
872
- const eventType = data.event_type;
873
- const missionId = data.missionId || data.payload?.mission || "unknown";
874
- const droneName = data.name || data.payload?.name
875
- let timestamp = data.timestamp || Date.now();
876
- if (typeof timestamp === 'string') {
877
- timestamp = new Date(timestamp).getTime();
878
- }
879
-
880
- const tEvent = Date.now();
881
-
882
- if (eventType === "DRONE_TELEMETRY_DATA") {
883
- await registerMission(missionId, droneName, timestamp, cubby, ctx);
884
- await storeTelemetry(missionId, timestamp, data, ctx, cubby);
885
- await attemptSync(missionId, timestamp, ctx, cubby);
886
- } else if (eventType === "VIDEO_STREAM_DATA") {
887
- await storeRgbFrame(missionId, timestamp, data, ctx, cubby);
888
- await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
889
- } else if (eventType === "THERMAL_STREAM_DATA") {
890
- await storeThermalFrame(missionId, timestamp, data, ctx, cubby);
891
- await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
892
- } else if (eventType === "VIDEO_KLV_DATA") {
893
- await storeKlvData(missionId, timestamp, data, ctx, cubby);
894
- await attemptSyncFromFrame(missionId, timestamp, ctx, cubby);
895
- }
896
-
897
- ctx.log(`⏱ [${eventType}] total pipeline ${Date.now() - tEvent}ms`);
898
- } catch (error) {
899
- ctx.log(`Failed to process data: ${error.message}`);
900
- }
901
- }
902
- }