@archetypeai/ds-cli 0.5.7 → 0.5.9

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.
@@ -1,721 +0,0 @@
1
- ---
2
- name: newton-machine-state-from-sensor
3
- description: Run a Machine State Lens by streaming real-time data from a physical sensor (BLE, USB, UDP, or recording playback). Use when doing real-time machine state classification from live sensor hardware.
4
- argument-hint: [source-type]
5
- ---
6
-
7
- # Newton Machine State Lens — Stream from Sensor
8
-
9
- Generate a script that streams real-time IMU sensor data to the Archetype AI Machine State Lens for live n-shot state classification. Supports both Python and JavaScript/Web.
10
-
11
- ## Frontend Architecture
12
-
13
- Decompose the UI into components. See `@rules/frontend-architecture` for conventions.
14
-
15
- ### Recommended decomposition
16
-
17
- | UI Area | Component | Pattern/Primitives | Key Props |
18
- |---------|-----------|-------------------|-----------|
19
- | Sensor input | `DataInput.svelte` | BackgroundCard, Button, Input | `onselect`, `status` |
20
- | Classification | `StateDisplay.svelte` | BackgroundCard, Badge | `currentState`, `confidence` |
21
- | Time series | Reuse SensorChart pattern | BackgroundCard, Chart | `data[]`, `signals` |
22
- | Results | Use FlatLogItem pattern in ScrollArea | FlatLogItem, ScrollArea | `status`, `message`, `detail` |
23
-
24
- - Use `@skills/create-dashboard` for the page layout
25
- - Extract streaming and session logic into `$lib/api/machine-state.js`
26
-
27
- ---
28
-
29
- ## Python Implementation
30
-
31
- ### Requirements
32
-
33
- - `archetypeai` Python package
34
- - `numpy`
35
- - `bleak` (for BLE sources)
36
- - `pyserial` (for USB sources)
37
- - Environment variables: `ATAI_API_KEY`, optionally `ATAI_API_ENDPOINT`
38
-
39
- ### Supported Source Types
40
-
41
- | Source | Description | Extra args |
42
- |--------|-------------|------------|
43
- | `ble` | Bluetooth Low Energy IMU device | None (auto-discovers) |
44
- | `usb` | USB serial IMU device | `--sensor-port` (default `/dev/tty.usbmodem1101`) |
45
- | `udp` | UDP relay (from BLE relay server) | `--udp-port` (default `5556`) |
46
- | `recording` | Replay a CSV recording | `--file-path` |
47
-
48
- ### Architecture
49
-
50
- #### 1. API Client & N-Shot Setup
51
-
52
- ```python
53
- from archetypeai.api_client import ArchetypeAI
54
-
55
- client = ArchetypeAI(api_key, api_endpoint=api_endpoint)
56
-
57
- # Upload n-shot files, derive class names from filenames
58
- n_shot_files = {}
59
- for file_path in args.n_shot_files:
60
- class_name = Path(file_path).stem.upper()
61
- resp = client.files.local.upload(file_path)
62
- n_shot_files[class_name] = resp["file_id"]
63
- ```
64
-
65
- #### 2. Lens YAML Config
66
-
67
- Same YAML structure as file-based, with dynamic `input_n_shot` built from uploaded files:
68
-
69
- ```python
70
- n_shot_yaml_lines = []
71
- for class_name, file_id in n_shot_files.items():
72
- n_shot_yaml_lines.append(f" {class_name}: {file_id}")
73
- n_shot_yaml = "\n".join(n_shot_yaml_lines)
74
- ```
75
-
76
- Insert into the YAML template under `model_parameters.input_n_shot`.
77
-
78
- #### 3. ImuReceiver — Multi-Source Data Acquisition
79
-
80
- Create an `ImuReceiver` class that handles all source types with a unified interface:
81
-
82
- ```python
83
- class ImuReceiver:
84
- def __init__(self, incoming_data, num_samples_per_packet=10,
85
- num_sensor_packets_per_packets_out=10):
86
- self.packet_queue = queue.Queue()
87
-
88
- if 'recording' in incoming_data:
89
- self.source = 'recording'
90
- self.recording = incoming_data['recording']
91
- elif 'sensor' in incoming_data:
92
- self.source = 'sensor'
93
- self.port = incoming_data['sensor']
94
- elif 'ble' in incoming_data:
95
- self.source = 'ble'
96
-
97
- def get_data(self):
98
- """Returns (packet_out, timestamp) or (None, None)"""
99
- if self.packet_queue.qsize() >= self.num_sensor_packets_per_packets_out:
100
- packets = [self.packet_queue.get()
101
- for _ in range(self.num_sensor_packets_per_packets_out)]
102
- packet_out = np.vstack([p['data'] for p in packets]).tolist()
103
- return packet_out, packets[-1]['sensor_timestamp']
104
- return None, None
105
- ```
106
-
107
- ##### BLE Acquisition (async)
108
-
109
- ```python
110
- async def acquire_ble(self, exception_holder):
111
- scanner = bleak.BleakScanner(
112
- detection_callback=self.detection_callback,
113
- service_uuids=[IMU_SERVICE_UUID]
114
- )
115
- await scanner.start()
116
- await asyncio.sleep(5)
117
- await scanner.stop()
118
-
119
- async with bleak.BleakClient(self.the_device) as client:
120
- await client.start_notify(IMU_CHARACTERISTIC_UUID, self.notify_callback)
121
- while client.is_connected:
122
- await asyncio.sleep(1)
123
-
124
- def notify_callback(self, handle, data):
125
- samples = np.frombuffer(data, dtype=np.int16)
126
- header = samples[0]
127
- payload = samples[1:]
128
- imu = payload.reshape(-1, 3) # (n, 3) — ax, ay, az
129
- self.packet_queue.put({"data": imu, "sensor_timestamp": time.time()})
130
- ```
131
-
132
- ##### USB Acquisition (threaded)
133
-
134
- ```python
135
- def acquire_usb(self, exception_holder):
136
- port = serial.Serial(self.port, 115200, timeout=5.0)
137
- port.read(2) # device ID
138
- port.write(bytearray([0x47])) # Go signal
139
-
140
- while True:
141
- raw = port.read(self.num_samples_per_packet * 2)
142
- samples = list(array.array('h', raw))
143
- self.packet_queue.put({
144
- "data": np.array(samples),
145
- "sensor_timestamp": time.time()
146
- })
147
- ```
148
-
149
- ##### UDP Acquisition (threaded)
150
-
151
- ```python
152
- class UdpImuReceiver:
153
- """Drop-in replacement for ImuReceiver using UDP relay data"""
154
- def __init__(self, port=5556):
155
- self.port = port
156
- self.packet_queue = queue.Queue()
157
-
158
- def _receive_loop(self):
159
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
160
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
161
- sock.bind(('', self.port))
162
- sock.settimeout(0.5)
163
-
164
- while self.running:
165
- try:
166
- data, _ = sock.recvfrom(4096)
167
- packet = json.loads(data.decode('utf-8'))
168
- imu = np.array(packet['data'], dtype=np.int16)
169
- self.packet_queue.put({
170
- "data": imu,
171
- "sensor_timestamp": packet['timestamp']
172
- })
173
- except socket.timeout:
174
- continue
175
- ```
176
-
177
- #### 4. Real-Time Streaming with Buffering
178
-
179
- Use a `deque` to buffer incoming sensor data and stream windows to the API:
180
-
181
- ```python
182
- from collections import deque
183
-
184
- data_buffer = deque(maxlen=window_size * 2)
185
-
186
- while not stop_event.is_set():
187
- packet_out, packet_timestamp = imu_receiver.get_data()
188
-
189
- if packet_out is not None:
190
- for row in packet_out:
191
- data_buffer.append((int(row[0]), int(row[1]), int(row[2])))
192
-
193
- if len(data_buffer) >= window_size:
194
- window_rows = list(data_buffer)[-window_size:]
195
-
196
- a1 = [r[0] for r in window_rows]
197
- a2 = [r[1] for r in window_rows]
198
- a3 = [r[2] for r in window_rows]
199
- a4 = [int((ax**2 + ay**2 + az**2) ** 0.5)
200
- for ax, ay, az in window_rows]
201
-
202
- payload = {
203
- "type": "session.update",
204
- "event_data": {
205
- "type": "data.json",
206
- "event_data": {
207
- "sensor_data": [a1, a2, a3, a4],
208
- "sensor_metadata": {
209
- "sensor_timestamp": packet_timestamp,
210
- "sensor_id": f"imu_sensor_{counter}"
211
- }
212
- }
213
- }
214
- }
215
- client.lens.sessions.process_event(session_id, payload)
216
-
217
- # Advance by step_size
218
- for _ in range(min(step_size, len(data_buffer))):
219
- data_buffer.popleft()
220
- ```
221
-
222
- #### 5. SSE Event Listening
223
-
224
- ```python
225
- sse_reader = client.lens.sessions.create_sse_consumer(
226
- session_id, max_read_time_sec=max_run_time_sec
227
- )
228
-
229
- for event in sse_reader.read(block=True):
230
- if event.get("type") == "inference.result":
231
- result = event["event_data"].get("response")
232
- print(f"Predicted: {result}")
233
- ```
234
-
235
- #### 6. Threading Model
236
-
237
- ```
238
- Main Thread: session_callback → starts SSE listener
239
- Thread 1: ImuReceiver (BLE async / USB serial / UDP socket)
240
- Thread 2: Streaming loop (buffer → API)
241
- Optional: CSV recording of session data
242
- ```
243
-
244
- - BLE uses `asyncio.run()` in a daemon thread
245
- - USB/recording use `threading.Thread(target=..., daemon=True)`
246
- - Graceful shutdown via `signal.SIGINT` → `stop_event.set()`
247
-
248
- #### 7. Optional: Record Session Data
249
-
250
- Save streamed data to CSV for later replay or analysis:
251
-
252
- ```python
253
- csv_filename = f"sessions/session_data_{timestamp}.csv"
254
- writer.writerow(['timestamp', 'a1', 'a2', 'a3', 'a4'])
255
-
256
- for ax, ay, az in window_rows:
257
- mag = int((ax**2 + ay**2 + az**2) ** 0.5)
258
- writer.writerow([time.time(), ax, ay, az, mag])
259
- ```
260
-
261
- ### CLI Arguments to Include
262
-
263
- ```
264
- --api-key API key (fallback to ATAI_API_KEY env var)
265
- --api-endpoint API endpoint (default from SDK)
266
- --source-type {ble, usb, recording, udp} (required)
267
- --file-path Recording file path (for recording mode)
268
- --sensor-port USB serial port (default: /dev/tty.usbmodem1101)
269
- --udp-port UDP relay port (default: 5556)
270
- --n-shot-files Paths to n-shot example CSVs (required, nargs='+')
271
- --window-size Window size in samples (default: 100)
272
- --step-size-n-shot Training step size (default: 100)
273
- --step-size-inference Inference step size (default: 100)
274
- --max-run-time-sec Max runtime (default: 500)
275
- ```
276
-
277
- ### Example Usage
278
-
279
- ```bash
280
- # From UDP relay
281
- python stream_from_sensor.py --source-type udp \
282
- --n-shot-files healthy.csv broken.csv
283
-
284
- # From BLE device
285
- python stream_from_sensor.py --source-type ble \
286
- --n-shot-files holding.csv walking.csv sitting.csv
287
-
288
- # Replay a recording
289
- python stream_from_sensor.py --source-type recording \
290
- --file-path data.csv --n-shot-files healthy.csv broken.csv
291
- ```
292
-
293
- ---
294
-
295
- ## Web / JavaScript Implementation
296
-
297
- Uses direct `fetch` calls to the Archetype AI REST API with Web Bluetooth API or WebSocket for sensor data. Based on the working pattern from `test-stream/src/lib/atai-client.ts`.
298
-
299
- ### Requirements
300
-
301
- - `@microsoft/fetch-event-source` for SSE consumption
302
- - Web Bluetooth API (Chrome/Edge) for BLE sensors
303
- - WebSocket support for UDP relay via WebSocket bridge
304
-
305
- ### Supported Web Source Types
306
-
307
- | Source | Web API | Notes |
308
- |--------|---------|-------|
309
- | `ble` | Web Bluetooth API | Chrome/Edge only, requires HTTPS |
310
- | `websocket` | WebSocket | Connect to a UDP-to-WebSocket bridge |
311
- | `file` | File API | Replay a CSV recording from file input |
312
-
313
- ### API Reference
314
-
315
- | Operation | Method | Endpoint | Body |
316
- |-----------|--------|----------|------|
317
- | Upload file | POST | `/files` | `FormData` |
318
- | Register lens | POST | `/lens/register` | `{ lens_config: config }` |
319
- | Delete lens | POST | `/lens/delete` | `{ lens_id }` |
320
- | Create session | POST | `/lens/sessions/create` | `{ lens_id }` |
321
- | Process event | POST | `/lens/sessions/events/process` | `{ session_id, event }` |
322
- | Destroy session | POST | `/lens/sessions/destroy` | `{ session_id }` |
323
- | SSE consumer | GET | `/lens/sessions/consumer/{sessionId}` | — |
324
-
325
- ### Helper: API fetch wrapper
326
-
327
- ```typescript
328
- const API_ENDPOINT = 'https://api.u1.archetypeai.app/v0.5'
329
-
330
- async function apiPost<T>(path: string, apiKey: string, body: unknown, timeoutMs = 5000): Promise<T> {
331
- const controller = new AbortController()
332
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
333
-
334
- try {
335
- const response = await fetch(`${API_ENDPOINT}${path}`, {
336
- method: 'POST',
337
- headers: {
338
- Authorization: `Bearer ${apiKey}`,
339
- 'Content-Type': 'application/json',
340
- },
341
- body: JSON.stringify(body),
342
- signal: controller.signal,
343
- })
344
-
345
- if (!response.ok) {
346
- const errorBody = await response.json().catch(() => ({}))
347
- throw new Error(`API POST ${path} failed: ${response.status} - ${JSON.stringify(errorBody)}`)
348
- }
349
-
350
- return response.json()
351
- } finally {
352
- clearTimeout(timeoutId)
353
- }
354
- }
355
- ```
356
-
357
- ### Step 1: Upload n-shot CSV files
358
-
359
- ```typescript
360
- const nShotMap: Record<string, string> = {}
361
-
362
- for (const { file, className } of nShotFiles) {
363
- const formData = new FormData()
364
- formData.append('file', file)
365
-
366
- const response = await fetch(`${API_ENDPOINT}/files`, {
367
- method: 'POST',
368
- headers: { Authorization: `Bearer ${apiKey}` },
369
- body: formData,
370
- })
371
- const result = await response.json()
372
- nShotMap[className.toUpperCase()] = result.file_id
373
- }
374
- ```
375
-
376
- ### Step 2: Register lens, create session, wait for ready
377
-
378
- ```typescript
379
- const windowSize = 100
380
- const stepSize = 100
381
-
382
- const lensConfig = {
383
- lens_name: 'machine_state_lens',
384
- lens_config: {
385
- model_pipeline: [
386
- { processor_name: 'lens_timeseries_state_processor', processor_config: {} },
387
- ],
388
- model_parameters: {
389
- model_name: 'OmegaEncoder',
390
- model_version: 'OmegaEncoder::omega_embeddings_01',
391
- normalize_input: true,
392
- buffer_size: windowSize,
393
- input_n_shot: nShotMap,
394
- csv_configs: {
395
- timestamp_column: 'timestamp',
396
- data_columns: ['a1', 'a2', 'a3', 'a4'],
397
- window_size: windowSize,
398
- step_size: stepSize,
399
- },
400
- knn_configs: {
401
- n_neighbors: 5,
402
- metric: 'manhattan',
403
- weights: 'distance',
404
- algorithm: 'ball_tree',
405
- normalize_embeddings: false,
406
- },
407
- },
408
- output_streams: [
409
- { stream_type: 'server_sent_events_writer' },
410
- ],
411
- },
412
- }
413
-
414
- // Register lens — NOTE: body wraps config as { lens_config: config }
415
- const registeredLens = await apiPost<{ lens_id: string }>(
416
- '/lens/register', apiKey, { lens_config: lensConfig }
417
- )
418
- const lensId = registeredLens.lens_id
419
-
420
- // Create session
421
- const session = await apiPost<{ session_id: string; session_endpoint: string }>(
422
- '/lens/sessions/create', apiKey, { lens_id: lensId }
423
- )
424
- const sessionId = session.session_id
425
-
426
- await apiPost('/lens/delete', apiKey, { lens_id: lensId })
427
-
428
- // Wait for session to be ready
429
- async function waitForSessionReady(sessionId: string, maxWaitMs = 30000): Promise<boolean> {
430
- const start = Date.now()
431
- while (Date.now() - start < maxWaitMs) {
432
- const status = await apiPost<{ session_status: string }>(
433
- '/lens/sessions/events/process', apiKey,
434
- { session_id: sessionId, event: { type: 'session.status' } },
435
- 10000
436
- )
437
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_RUNNING' ||
438
- status.session_status === '3') {
439
- return true
440
- }
441
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_FAILED' ||
442
- status.session_status === '6') {
443
- return false
444
- }
445
- await new Promise(r => setTimeout(r, 500))
446
- }
447
- return false
448
- }
449
-
450
- const isReady = await waitForSessionReady(sessionId)
451
- if (!isReady) throw new Error('Session failed to start')
452
- ```
453
-
454
- ### Step 3: Acquire sensor data (Web Bluetooth)
455
-
456
- ```typescript
457
- const IMU_SERVICE = '0000fff0-0000-1000-8000-00805f9b34fb'
458
- const IMU_CHARACTERISTIC = '0000fff1-0000-1000-8000-00805f9b34fb'
459
-
460
- // Request BLE device (requires user gesture)
461
- const device = await navigator.bluetooth.requestDevice({
462
- filters: [{ services: [IMU_SERVICE] }],
463
- })
464
- const server = await device.gatt.connect()
465
- const service = await server.getPrimaryService(IMU_SERVICE)
466
- const characteristic = await service.getCharacteristic(IMU_CHARACTERISTIC)
467
-
468
- // Buffer for incoming samples
469
- const dataBuffer: [number, number, number][] = []
470
-
471
- characteristic.addEventListener('characteristicvaluechanged', (event) => {
472
- const value = (event.target as BluetoothRemoteGATTCharacteristic).value!
473
- const samples = new Int16Array(value.buffer)
474
-
475
- // Skip header byte, parse (ax, ay, az) triplets
476
- const payload = samples.slice(1)
477
- for (let i = 0; i + 2 < payload.length; i += 3) {
478
- dataBuffer.push([payload[i], payload[i + 1], payload[i + 2]])
479
- }
480
- })
481
-
482
- await characteristic.startNotifications()
483
- ```
484
-
485
- ### Step 4: Stream buffered data in windows
486
-
487
- ```typescript
488
- let counter = 0
489
-
490
- const streamLoop = setInterval(async () => {
491
- if (dataBuffer.length < windowSize) return
492
-
493
- const window = dataBuffer.splice(0, windowSize)
494
-
495
- const a1 = window.map(r => r[0])
496
- const a2 = window.map(r => r[1])
497
- const a3 = window.map(r => r[2])
498
- const a4 = window.map(([ax, ay, az]) =>
499
- Math.floor(Math.sqrt(ax * ax + ay * ay + az * az))
500
- )
501
-
502
- await apiPost('/lens/sessions/events/process', apiKey, {
503
- session_id: sessionId,
504
- event: {
505
- type: 'session.update',
506
- event_data: {
507
- type: 'data.json',
508
- event_data: {
509
- sensor_data: [a1, a2, a3, a4],
510
- sensor_metadata: {
511
- sensor_timestamp: Date.now() / 1000,
512
- sensor_id: `web_ble_sensor_${counter++}`,
513
- },
514
- },
515
- },
516
- },
517
- }, 10000)
518
- }, 200) // check every 200ms
519
- ```
520
-
521
- ### Step 5: Acquire sensor data (WebSocket bridge)
522
-
523
- Alternative for UDP relay — connect to a WebSocket bridge that forwards UDP packets:
524
-
525
- ```typescript
526
- const ws = new WebSocket('ws://localhost:8765')
527
-
528
- ws.onmessage = (event) => {
529
- const packet = JSON.parse(event.data)
530
- // packet.data is [[ax, ay, az], ...] from UDP relay
531
- for (const [ax, ay, az] of packet.data) {
532
- dataBuffer.push([ax, ay, az])
533
- }
534
- }
535
- ```
536
-
537
- ### Step 6: Consume SSE results
538
-
539
- ```typescript
540
- import { fetchEventSource } from '@microsoft/fetch-event-source'
541
-
542
- const abortController = new AbortController()
543
-
544
- fetchEventSource(`${API_ENDPOINT}/lens/sessions/consumer/${sessionId}`, {
545
- headers: { Authorization: `Bearer ${apiKey}` },
546
- signal: abortController.signal,
547
- onmessage(event) {
548
- const parsed = JSON.parse(event.data)
549
-
550
- if (parsed.type === 'inference.result') {
551
- const result = parsed.event_data.response
552
- const meta = parsed.event_data.query_metadata
553
- console.log(`Predicted: ${result}`)
554
- }
555
-
556
- if (parsed.type === 'sse.stream.end') {
557
- console.log('Stream complete')
558
- }
559
- },
560
- })
561
- ```
562
-
563
- ### Step 7: Cleanup
564
-
565
- ```typescript
566
- clearInterval(streamLoop)
567
- abortController.abort()
568
- await device.gatt.disconnect()
569
- await apiPost('/lens/sessions/destroy', apiKey, { session_id: sessionId })
570
- ```
571
-
572
- ### Web Lifecycle Summary
573
-
574
- ```
575
- 1. Upload n-shot CSVs -> POST /files (FormData, one per class)
576
- 2. Register lens -> POST /lens/register { lens_config: config }
577
- 3. Create session -> POST /lens/sessions/create { lens_id }
578
- 4. Wait for ready -> POST /lens/sessions/events/process { session_id, event: { type: 'session.status' } }
579
- 5. (Optional) Delete lens -> POST /lens/delete { lens_id }
580
- 6. Connect sensor (BLE / WS) -> Web Bluetooth API or WebSocket
581
- 7. Buffer + stream windows -> POST /lens/sessions/events/process { session_id, event } (loop)
582
- 8. Consume SSE results -> GET /lens/sessions/consumer/{sessionId}
583
- 9. Disconnect + destroy -> POST /lens/sessions/destroy { session_id }
584
- ```
585
-
586
- ---
587
-
588
- ## CSV Format Expected
589
-
590
- ```csv
591
- timestamp,a1,a2,a3,a4
592
- 1700000000.0,100,200,300,374
593
- ```
594
-
595
- - `timestamp`: UNIX epoch float
596
- - `a1, a2, a3`: Sensor axes (e.g., accelerometer x, y, z)
597
- - `a4`: Magnitude (sqrt(a1² + a2² + a3²))
598
-
599
- ## Optional: Results Logging
600
-
601
- Save predictions to a timestamped CSV for analysis or visualization.
602
-
603
- ### Python — Results CSV
604
-
605
- ```python
606
- import csv
607
- from pathlib import Path
608
- from datetime import datetime
609
-
610
- # Create results directory and timestamped filename
611
- results_dir = Path("results")
612
- results_dir.mkdir(exist_ok=True)
613
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
614
- results_file = results_dir / f"sensor_{args.source_type}_{timestamp}.csv"
615
-
616
- # Write CSV header
617
- with open(results_file, 'w', newline='', encoding='utf-8') as f:
618
- writer = csv.writer(f)
619
- writer.writerow(['read_index', 'predicted_class', 'confidence_scores',
620
- 'file_id', 'window_size', 'total_rows'])
621
-
622
- # Inside the SSE event loop, when handling inference.result:
623
- if event.get("type") == "inference.result":
624
- ed = event.get("event_data", {})
625
- result = ed.get("response")
626
- meta = ed.get("query_metadata", {})
627
- query_meta = meta.get("query_metadata", {})
628
-
629
- predicted_class = result[0] if isinstance(result, list) and len(result) > 0 else "unknown"
630
- confidence_scores = result[1] if isinstance(result, list) and len(result) > 1 else {}
631
- read_index = query_meta.get("read_index", "N/A")
632
- file_id = query_meta.get("file_id", "N/A")
633
- window_size_val = query_meta.get("window_size", "N/A")
634
- total_rows = query_meta.get("total_rows", "N/A")
635
-
636
- print(f"[{read_index}] Predicted: {predicted_class} | Scores: {confidence_scores}")
637
-
638
- with open(results_file, 'a', newline='', encoding='utf-8') as f:
639
- writer = csv.writer(f)
640
- writer.writerow([read_index, predicted_class, str(confidence_scores),
641
- file_id, window_size_val, total_rows])
642
- ```
643
-
644
- ### Response Structure
645
-
646
- The `inference.result` response contains:
647
- - `response[0]`: predicted class name (string, e.g. `"HEALTHY"`)
648
- - `response[1]`: confidence scores dict (e.g. `{"HEALTHY": 0.95, "BROKEN": 0.05}`)
649
- - `query_metadata.query_metadata.read_index`: window position in the data
650
- - `query_metadata.query_metadata.file_id`: reference file ID
651
- - `query_metadata.query_metadata.window_size`: window size used
652
- - `query_metadata.query_metadata.total_rows`: total rows processed
653
-
654
- ### Web/JS — Results Array + CSV Download
655
-
656
- ```typescript
657
- interface PredictionResult {
658
- readIndex: number | string
659
- predictedClass: string
660
- confidenceScores: Record<string, number>
661
- fileId: string
662
- windowSize: number
663
- totalRows: number
664
- }
665
-
666
- const results: PredictionResult[] = []
667
-
668
- // Inside the SSE onmessage handler:
669
- if (parsed.type === 'inference.result') {
670
- const result = parsed.event_data.response
671
- const meta = parsed.event_data.query_metadata
672
- const queryMeta = meta?.query_metadata ?? {}
673
-
674
- const prediction: PredictionResult = {
675
- readIndex: queryMeta.read_index ?? 'N/A',
676
- predictedClass: Array.isArray(result) && result.length > 0 ? result[0] : 'unknown',
677
- confidenceScores: Array.isArray(result) && result.length > 1 ? result[1] : {},
678
- fileId: queryMeta.file_id ?? 'N/A',
679
- windowSize: queryMeta.window_size ?? 0,
680
- totalRows: queryMeta.total_rows ?? 0,
681
- }
682
-
683
- results.push(prediction)
684
- console.log(`[${prediction.readIndex}] ${prediction.predictedClass}`, prediction.confidenceScores)
685
- }
686
-
687
- // Download results as CSV
688
- function downloadResultsCsv(results: PredictionResult[], filename: string) {
689
- const header = 'read_index,predicted_class,confidence_scores,file_id,window_size,total_rows\n'
690
- const rows = results.map(r =>
691
- `${r.readIndex},${r.predictedClass},"${JSON.stringify(r.confidenceScores)}",${r.fileId},${r.windowSize},${r.totalRows}`
692
- ).join('\n')
693
-
694
- const blob = new Blob([header + rows], { type: 'text/csv' })
695
- const url = URL.createObjectURL(blob)
696
- const a = document.createElement('a')
697
- a.href = url
698
- a.download = filename
699
- a.click()
700
- URL.revokeObjectURL(url)
701
- }
702
- ```
703
-
704
- ### CLI Flag
705
-
706
- Add `--save-results` flag (default: `True`) to enable/disable results logging:
707
-
708
- ```
709
- --save-results Save predictions to CSV in results/ directory (default: True)
710
- ```
711
-
712
- ---
713
-
714
- ## Key Implementation Notes
715
-
716
- - Default `window_size` and `step_size`: **100**
717
- - N-shot class names derived from filename stems (e.g., `healthy.csv` → `HEALTHY`)
718
- - Python: `signal.SIGINT` for graceful shutdown
719
- - Web: `AbortController` for SSE, `clearInterval` for stream loop, `gatt.disconnect()` for BLE
720
- - Web Bluetooth requires HTTPS and a user gesture to initiate pairing
721
- - For WebSocket bridge, run a small relay server that forwards UDP broadcast to WebSocket clients