@archetypeai/ds-cli 0.5.7 → 0.6.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.
@@ -1,624 +0,0 @@
1
- ---
2
- name: newton-camera-frame-analysis
3
- description: Live webcam frame analysis using Newton's vision model via model.query (request/response). Captures frames from a webcam as base64 JPEG and sends them to Newton. Use for live camera analysis, scene description, presence detection, or visual Q&A. NOT for video file uploads — use /activity-monitor-lens-on-video for that.
4
- argument-hint: [question] [camera_index]
5
- allowed-tools: Bash(python *), Read
6
- ---
7
-
8
- # Newton Camera Frame Analysis (Live Webcam → base64 → model.query)
9
-
10
- Capture live webcam frames, encode as base64 JPEG, and send to Newton's vision model via `model.query` (synchronous request/response). Supports Python (OpenCV) and JavaScript (getUserMedia + canvas).
11
-
12
- ## Frontend Architecture
13
-
14
- Decompose the UI into components. See `@rules/frontend-architecture` for conventions.
15
-
16
- ### Recommended decomposition
17
-
18
- | UI Area | Component | Pattern/Primitives | Key Props |
19
- |---------|-----------|-------------------|-----------|
20
- | Camera feed | `WebcamView.svelte` | Card, AspectRatio | `stream`, `status` |
21
- | Status | `ConnectionStatus.svelte` | StatusBadge pattern | `status`, `label` |
22
- | Results | Use FlatLogItem pattern in ScrollArea | FlatLogItem, ScrollArea | `status`, `message`, `detail` |
23
-
24
- - Use `@skills/create-dashboard` for the page layout
25
- - Extract webcam capture and API logic into `$lib/api/camera-analysis.js`
26
-
27
- **This skill is for LIVE WEBCAM input only.** For analyzing uploaded video files, use `/activity-monitor-lens-on-video` instead.
28
-
29
- | | This skill (camera-frame-analysis) | activity-monitor-lens-on-video |
30
- |---|---|---|
31
- | **Input** | Live webcam (base64 JPEG frames) | Uploaded video file |
32
- | **Who captures frames** | Client (Python cv2 / JS canvas) | Server (`video_file_reader`) |
33
- | **Event type** | `model.query` (request/response) | Server-driven, results via SSE |
34
- | **Response** | Direct in POST response | Async via SSE stream |
35
- | **Use case** | Real-time webcam Q&A | Batch video analysis |
36
-
37
- ---
38
-
39
- ## Model Parameters
40
-
41
- | Parameter | Default | Notes |
42
- |---|---|---|
43
- | `model_version` | `Newton::c2_4_7b_251215a172f6d7` | Newton model ID |
44
- | `template_name` | `image_qa_template_task` | Prompt template |
45
- | `instruction` | *(user-provided)* | System prompt guiding output format |
46
- | `focus` | *(user-provided)* | The question or what to look for |
47
- | `max_new_tokens` | `512` | Max response length |
48
- | `camera_buffer_size` | `1` | Single-frame buffer for webcam |
49
- | `min_replicas` / `max_replicas` | `1` / `1` | Scaling config |
50
-
51
- **IMPORTANT:** `instruction` and `focus` must be passed as parameters — not hardcoded. The values in the lens config (registration) and in each `model.query` event must be consistent. Pass the user's values into both.
52
-
53
- ---
54
-
55
- ## Python Implementation
56
-
57
- ### Requirements
58
-
59
- - `archetypeai` Python package
60
- - `opencv-python` (`cv2`), `Pillow`
61
- - Environment variables: `ATAI_API_KEY` or `ARCHETYPE_API_KEY`
62
-
63
- ### Quick Start
64
-
65
- ```bash
66
- export ATAI_API_KEY=your_key_here
67
- python camera_frame_analysis.py "Describe what you see"
68
-
69
- # Custom question
70
- python camera_frame_analysis.py "Is anyone present?"
71
-
72
- # Different camera
73
- python camera_frame_analysis.py "Describe the scene" 1
74
- ```
75
-
76
- ### Parameters
77
-
78
- - **question** (positional, optional): What to analyze (default: "Describe what you see")
79
- - **camera_index** (positional, optional): Camera index (default: 0)
80
-
81
- ### How It Works
82
-
83
- 1. **Capture**: Opens webcam with OpenCV, reads a frame
84
- 2. **Encode**: Converts frame to base64 JPEG (BGR → RGB → PIL → JPEG → base64)
85
- 3. **Setup**: Registers Newton lens, creates session, waits for ready
86
- 4. **Initialize**: Sends `session.modify` to initialize the processor
87
- 5. **Query**: Sends base64 image as `model.query` event, gets response directly
88
- 6. **Cleanup**: Destroys session
89
-
90
- ### Webcam Capture → base64
91
-
92
- ```python
93
- import cv2
94
- import base64
95
- import io
96
- from PIL import Image
97
-
98
- def capture_frame_base64(camera_index=0, jpeg_quality=80, resize=(640, 480)):
99
- """Capture a webcam frame and return as raw base64 JPEG string."""
100
- cap = cv2.VideoCapture(camera_index)
101
- ret, frame = cap.read()
102
- cap.release()
103
-
104
- if not ret:
105
- raise RuntimeError(f"Failed to capture from camera {camera_index}")
106
-
107
- # Resize if needed
108
- h, w = frame.shape[:2]
109
- if (w, h) != resize:
110
- frame = cv2.resize(frame, resize)
111
-
112
- # BGR (OpenCV) → RGB → PIL → JPEG → base64
113
- rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
114
- pil_image = Image.fromarray(rgb_frame)
115
-
116
- buffer = io.BytesIO()
117
- pil_image.save(buffer, format="JPEG", quality=jpeg_quality)
118
- raw_base64 = base64.b64encode(buffer.getvalue()).decode()
119
-
120
- return raw_base64 # No "data:image/jpeg;base64," prefix
121
- ```
122
-
123
- ### Full Python Example
124
-
125
- ```python
126
- import os
127
- import time
128
- from archetypeai.api_client import ArchetypeAI
129
-
130
- api_key = os.getenv("ATAI_API_KEY")
131
- client = ArchetypeAI(api_key)
132
-
133
- # --- User-provided values (NOT hardcoded) ---
134
- instruction = "Answer the following question about the image:"
135
- focus = "Describe what you see in this image."
136
-
137
- def build_lens_config(instruction: str, focus: str) -> dict:
138
- """Build lens config with user-provided instruction and focus."""
139
- return {
140
- "lens_name": "camera-frame-capture-lens",
141
- "lens_config": {
142
- "model_pipeline": [
143
- {"processor_name": "lens_camera_processor", "processor_config": {}}
144
- ],
145
- "model_parameters": {
146
- "model_version": "Newton::c2_4_7b_251215a172f6d7",
147
- "template_name": "image_qa_template_task",
148
- "instruction": instruction,
149
- "focus": focus,
150
- "max_new_tokens": 512,
151
- "camera_buffer_size": 1,
152
- "min_replicas": 1,
153
- "max_replicas": 1,
154
- },
155
- },
156
- }
157
-
158
- def build_query_event(raw_base64: str, instruction: str, focus: str) -> dict:
159
- """Build model.query event with the SAME instruction and focus as the lens config."""
160
- return {
161
- "type": "model.query",
162
- "event_data": {
163
- "model_version": "Newton::c2_4_7b_251215a172f6d7",
164
- "template_name": "image_qa_template_task",
165
- "instruction": instruction,
166
- "focus": focus,
167
- "max_new_tokens": 512,
168
- "data": [{"type": "base64_img", "base64_img": raw_base64}],
169
- },
170
- }
171
-
172
- # 1. Register lens (pass user's instruction + focus)
173
- lens_config = build_lens_config(instruction, focus)
174
- lens = client.lens.register(lens_config)
175
- lens_id = lens["lens_id"]
176
-
177
- # 2. Create session
178
- session = client.lens.sessions.create(lens_id)
179
- session_id = session["session_id"]
180
-
181
- # 3. Wait for session ready
182
- for _ in range(60):
183
- try:
184
- status = client.lens.sessions.process_event(
185
- session_id, {"type": "session.status"}
186
- )
187
- if status.get("session_status") in ["3", "LensSessionStatus.SESSION_STATUS_RUNNING"]:
188
- break
189
- except Exception:
190
- pass
191
- time.sleep(0.5)
192
-
193
- # 4. Initialize processor (REQUIRED)
194
- client.lens.sessions.process_event(session_id, {
195
- "type": "session.modify",
196
- "event_data": {"camera_buffer_size": 1}
197
- })
198
-
199
- # 5. Capture frame and send as model.query (same instruction + focus)
200
- raw_base64 = capture_frame_base64(camera_index=0)
201
- event = build_query_event(raw_base64, instruction, focus)
202
-
203
- response = client.lens.sessions.process_event(session_id, event)
204
-
205
- if response.get("type") == "model.query.response":
206
- result = response["event_data"]["response"]
207
- if isinstance(result, list):
208
- result = result[0]
209
- print(f"Answer: {result}")
210
-
211
- # 6. Cleanup
212
- client.lens.sessions.destroy(session_id)
213
- ```
214
-
215
- ---
216
-
217
- ## Web / JavaScript Implementation
218
-
219
- Uses direct `fetch` calls to the Archetype AI REST API.
220
-
221
- ### Requirements
222
-
223
- - Browser with `getUserMedia` support (webcam access)
224
- - HTTPS (required for camera access, except `localhost`)
225
-
226
- ### API Reference
227
-
228
- | Operation | Method | Endpoint | Body |
229
- |-----------|--------|----------|------|
230
- | List lenses | GET | `/lens/metadata` | — |
231
- | Register lens | POST | `/lens/register` | `{ lens_config: config }` |
232
- | Delete lens | POST | `/lens/delete` | `{ lens_id }` |
233
- | Create session | POST | `/lens/sessions/create` | `{ lens_id }` |
234
- | Process event | POST | `/lens/sessions/events/process` | `{ session_id, event }` |
235
- | Destroy session | POST | `/lens/sessions/destroy` | `{ session_id }` |
236
-
237
- ### Helpers: API wrappers
238
-
239
- ```typescript
240
- const API_ENDPOINT = 'https://api.u1.archetypeai.app/v0.5'
241
-
242
- async function apiGet<T>(path: string, apiKey: string): Promise<T> {
243
- const response = await fetch(`${API_ENDPOINT}${path}`, {
244
- method: 'GET',
245
- headers: { Authorization: `Bearer ${apiKey}` },
246
- })
247
- if (!response.ok) throw new Error(`API GET ${path} failed: ${response.status}`)
248
- return response.json()
249
- }
250
-
251
- async function apiPost<T>(path: string, apiKey: string, body: unknown, timeoutMs = 5000): Promise<T> {
252
- const controller = new AbortController()
253
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
254
-
255
- try {
256
- const response = await fetch(`${API_ENDPOINT}${path}`, {
257
- method: 'POST',
258
- headers: {
259
- Authorization: `Bearer ${apiKey}`,
260
- 'Content-Type': 'application/json',
261
- },
262
- body: JSON.stringify(body),
263
- signal: controller.signal,
264
- })
265
-
266
- if (!response.ok) {
267
- const errorBody = await response.json().catch(() => ({}))
268
- throw new Error(`API POST ${path} failed: ${response.status} - ${JSON.stringify(errorBody)}`)
269
- }
270
-
271
- return response.json()
272
- } finally {
273
- clearTimeout(timeoutId)
274
- }
275
- }
276
- ```
277
-
278
- ### Step 1: Find or create the lens (clean up stale lenses)
279
-
280
- A stale lens from a previous run causes `"Input stream is unhealthy!"` errors. Always check for an existing lens with the same name and delete it before registering a fresh one.
281
-
282
- **Pass the user's `instruction` and `focus` into the lens config** — do not hardcode them.
283
-
284
- ```typescript
285
- const LENS_NAME = 'camera-frame-capture-lens'
286
-
287
- // --- User-provided values (NOT hardcoded) ---
288
- const instruction = 'Answer the following question about the image:'
289
- const focus = 'Describe what you see in this image.'
290
-
291
- function buildLensConfig(instruction: string, focus: string) {
292
- return {
293
- lens_name: LENS_NAME,
294
- lens_config: {
295
- model_pipeline: [
296
- { processor_name: 'lens_camera_processor', processor_config: {} },
297
- ],
298
- model_parameters: {
299
- model_version: 'Newton::c2_4_7b_251215a172f6d7',
300
- template_name: 'image_qa_template_task',
301
- instruction,
302
- focus,
303
- max_new_tokens: 512,
304
- camera_buffer_size: 1,
305
- min_replicas: 1,
306
- max_replicas: 1,
307
- },
308
- },
309
- }
310
- }
311
-
312
- // Delete any existing lens with the same name to avoid stale state
313
- const existingLenses = await apiGet<Array<{ lens_id: string; lens_name: string }>>(
314
- '/lens/metadata', apiKey
315
- )
316
- const staleLens = existingLenses.find(l => l.lens_name === LENS_NAME)
317
- if (staleLens) {
318
- console.log('Deleting stale lens:', staleLens.lens_id)
319
- await apiPost('/lens/delete', apiKey, { lens_id: staleLens.lens_id })
320
- }
321
-
322
- // Register fresh lens with user's instruction + focus
323
- const lensConfig = buildLensConfig(instruction, focus)
324
- const registeredLens = await apiPost<{ lens_id: string }>(
325
- '/lens/register', apiKey, { lens_config: lensConfig }
326
- )
327
- const lensId = registeredLens.lens_id
328
- ```
329
-
330
- ### Step 2: Create session and wait for ready
331
-
332
- ```typescript
333
- const session = await apiPost<{ session_id: string; session_endpoint: string }>(
334
- '/lens/sessions/create', apiKey, { lens_id: lensId }
335
- )
336
- const sessionId = session.session_id
337
-
338
- // Wait for session to be ready (poll until status = running)
339
- async function waitForSessionReady(sessionId: string, maxWaitMs = 30000): Promise<boolean> {
340
- const start = Date.now()
341
- while (Date.now() - start < maxWaitMs) {
342
- const status = await apiPost<{ session_status: string }>(
343
- '/lens/sessions/events/process', apiKey,
344
- { session_id: sessionId, event: { type: 'session.status' } },
345
- 10000
346
- )
347
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_RUNNING' ||
348
- status.session_status === '3') {
349
- return true
350
- }
351
- if (status.session_status === 'LensSessionStatus.SESSION_STATUS_FAILED' ||
352
- status.session_status === '6') {
353
- return false
354
- }
355
- await new Promise(r => setTimeout(r, 500))
356
- }
357
- return false
358
- }
359
-
360
- const isReady = await waitForSessionReady(sessionId)
361
- if (!isReady) throw new Error('Session failed to initialize')
362
- ```
363
-
364
- ### Step 3: Initialize the processor (REQUIRED for lens_camera_processor)
365
-
366
- This sends a `session.modify` event that triggers `update_lens_params()` which initializes `video_narrator_memory`. **Without this step, inference will fail.**
367
-
368
- ```typescript
369
- await apiPost('/lens/sessions/events/process', apiKey, {
370
- session_id: sessionId,
371
- event: {
372
- type: 'session.modify',
373
- event_data: {
374
- camera_buffer_size: 1,
375
- },
376
- },
377
- }, 30000) // 30s timeout for initialization
378
- ```
379
-
380
- ### Step 4: Start webcam and capture frames as base64
381
-
382
- #### 4a. Create a video element
383
-
384
- ```html
385
- <!-- Visible preview (optional) -->
386
- <video id="webcam" autoplay playsinline muted></video>
387
-
388
- <!-- Or create it in JS (no visible preview) -->
389
- ```
390
-
391
- ```typescript
392
- // Option A: Reference an existing <video> element
393
- const video = document.getElementById('webcam') as HTMLVideoElement
394
-
395
- // Option B: Create a hidden video element in JS
396
- const video = document.createElement('video')
397
- video.autoplay = true
398
- video.playsInline = true // Required for iOS
399
- video.muted = true
400
- ```
401
-
402
- #### 4b. Request camera access and start the stream
403
-
404
- ```typescript
405
- async function startCamera(
406
- preferredWidth = 640,
407
- preferredHeight = 480,
408
- facingMode: 'user' | 'environment' = 'user', // 'user' = front, 'environment' = back
409
- ): Promise<MediaStream> {
410
- const stream = await navigator.mediaDevices.getUserMedia({
411
- video: {
412
- width: { ideal: preferredWidth },
413
- height: { ideal: preferredHeight },
414
- facingMode,
415
- },
416
- audio: false,
417
- })
418
-
419
- video.srcObject = stream
420
-
421
- // Wait until video is actually playing and has dimensions
422
- await new Promise<void>((resolve) => {
423
- video.onloadedmetadata = () => {
424
- video.play()
425
- resolve()
426
- }
427
- })
428
-
429
- console.log(`Camera started: ${video.videoWidth}x${video.videoHeight}`)
430
- return stream
431
- }
432
-
433
- const stream = await startCamera()
434
- ```
435
-
436
- **Permission notes:**
437
- - Browser will show a permission prompt on first call
438
- - HTTPS is **required** (except `localhost`)
439
- - On mobile, `facingMode: 'environment'` selects the rear camera
440
-
441
- #### 4c. Capture a frame as base64 JPEG
442
-
443
- The flow is: **video element → canvas → toDataURL → base64 string**.
444
-
445
- ```typescript
446
- function captureFrame(quality = 0.8): string | null {
447
- if (!video.videoWidth || !video.videoHeight) return null
448
-
449
- const canvas = document.createElement('canvas')
450
- canvas.width = video.videoWidth
451
- canvas.height = video.videoHeight
452
-
453
- const ctx = canvas.getContext('2d')
454
- if (!ctx) return null
455
-
456
- // Draw current video frame onto canvas
457
- ctx.drawImage(video, 0, 0)
458
-
459
- // Convert to base64 JPEG — returns "data:image/jpeg;base64,/9j/4AAQ..."
460
- return canvas.toDataURL('image/jpeg', quality)
461
- }
462
- ```
463
-
464
- **Quality vs size tradeoffs:**
465
- | Quality | ~Size (640x480) | Use case |
466
- |---------|-----------------|----------|
467
- | `0.5` | ~20-30 KB | Fast continuous streaming |
468
- | `0.8` | ~40-60 KB | Good balance (recommended) |
469
- | `1.0` | ~80-120 KB | Maximum detail |
470
-
471
- #### 4d. Strip the data URI prefix before sending to the API
472
-
473
- The API expects **raw base64**, not the `data:image/jpeg;base64,` prefix that `toDataURL` produces.
474
-
475
- ```typescript
476
- function captureFrameRaw(quality = 0.8): string | null {
477
- const dataUri = captureFrame(quality)
478
- if (!dataUri) return null
479
-
480
- // Strip "data:image/jpeg;base64," prefix → raw base64
481
- return dataUri.replace(/^data:image\/\w+;base64,/, '')
482
- }
483
- ```
484
-
485
- This raw base64 string is what goes into the `model.query` event's `base64_img` field.
486
-
487
- ### Step 5: Send frames for analysis (model.query)
488
-
489
- This uses **request/response — NOT SSE**. Each frame is sent as a `model.query` event and the response comes back directly in the POST response.
490
-
491
- The `instruction` and `focus` in the `model.query` event **must match** the values used at lens registration. Pass them through — do not hardcode different values.
492
-
493
- ```typescript
494
- function createModelQueryEvent(
495
- rawBase64Images: string[], // Already stripped of data URI prefix
496
- instruction: string, // Same as lens config
497
- focus: string, // Same as lens config
498
- modelVersion = 'Newton::c2_4_7b_251215a172f6d7',
499
- templateName = 'image_qa_template_task',
500
- maxNewTokens = 512,
501
- ) {
502
- return {
503
- type: 'model.query' as const,
504
- event_data: {
505
- model_version: modelVersion,
506
- template_name: templateName,
507
- instruction,
508
- focus,
509
- max_new_tokens: maxNewTokens,
510
- data: rawBase64Images.map(img => ({
511
- type: 'base64_img',
512
- base64_img: img,
513
- })),
514
- },
515
- }
516
- }
517
-
518
- // Send a frame and get the response (uses the same instruction + focus from Step 1)
519
- async function analyzeFrame(instruction: string, focus: string): Promise<string> {
520
- const frame = captureFrameRaw() // Raw base64 (no data URI prefix)
521
- if (!frame) throw new Error('Failed to capture frame')
522
-
523
- const event = createModelQueryEvent([frame], instruction, focus)
524
-
525
- const response = await apiPost<{
526
- type: string
527
- event_data?: { response?: string | string[]; message?: string }
528
- }>(
529
- '/lens/sessions/events/process', apiKey,
530
- { session_id: sessionId, event },
531
- 60000 // 60s timeout for model inference
532
- )
533
-
534
- // Extract text from response
535
- if (response.type === 'model.query.response' && response.event_data) {
536
- const text = response.event_data.response
537
- if (typeof text === 'string') return text
538
- if (Array.isArray(text)) return text.join('\n')
539
- return JSON.stringify(response.event_data)
540
- }
541
-
542
- return JSON.stringify(response)
543
- }
544
- ```
545
-
546
- ### Step 6: Continuous capture loop
547
-
548
- Send the first frame **immediately** after initialization — do not wait for the interval. The processor expects data promptly after `session.modify`.
549
-
550
- ```typescript
551
- let isSending = false
552
-
553
- async function captureAndSend() {
554
- if (isSending) {
555
- console.log('Previous request still in progress, skipping frame')
556
- return
557
- }
558
-
559
- isSending = true
560
- try {
561
- const result = await analyzeFrame(instruction, focus)
562
- console.log('Result:', result)
563
- } catch (error) {
564
- console.error('Frame analysis failed:', error)
565
- } finally {
566
- isSending = false
567
- }
568
- }
569
-
570
- // Send first frame immediately
571
- captureAndSend()
572
-
573
- // Then continue at 1 frame per second
574
- const intervalId = setInterval(captureAndSend, 1000)
575
- ```
576
-
577
- ### Step 7: Cleanup
578
-
579
- ```typescript
580
- // Stop capture loop
581
- clearInterval(intervalId)
582
-
583
- // Stop camera
584
- stream.getTracks().forEach(track => track.stop())
585
-
586
- // Destroy session
587
- await apiPost('/lens/sessions/destroy', apiKey, { session_id: sessionId })
588
- ```
589
-
590
- ### Web Lifecycle Summary
591
-
592
- ```
593
- 1. List existing lenses -> GET /lens/metadata
594
- 2. Delete stale lens -> POST /lens/delete { lens_id } (if same name exists)
595
- 3. Register fresh lens -> POST /lens/register { lens_config: config }
596
- 4. Create session -> POST /lens/sessions/create { lens_id }
597
- 5. Wait for ready -> POST /lens/sessions/events/process (poll session.status)
598
- 6. Initialize processor -> POST /lens/sessions/events/process { session_id, event: session.modify }
599
- 7. Start webcam -> navigator.mediaDevices.getUserMedia()
600
- 8. Send first frame NOW -> POST /lens/sessions/events/process { session_id, event: model.query }
601
- 9. Capture loop (1fps) -> POST /lens/sessions/events/process { session_id, event: model.query }
602
- 10. Stop camera -> stream.getTracks().forEach(t => t.stop())
603
- 11. Destroy session -> POST /lens/sessions/destroy { session_id }
604
- ```
605
-
606
- ---
607
-
608
- ## Use Cases
609
-
610
- - **Quick scene analysis**: Single frame description
611
- - **Presence detection**: Check if someone is at their desk
612
- - **Safety monitoring**: Verify safety equipment usage
613
- - **Object identification**: Identify specific items in view
614
- - **Continuous monitoring**: Stream frames with periodic analysis
615
-
616
- ## Troubleshooting
617
-
618
- - **"Input stream is unhealthy!"**: Stale lens from previous run. Always delete existing lens before registering a new one (see Step 1).
619
- - **Camera not found**: Try different camera indices (Python) or check browser permissions (Web)
620
- - **API errors**: Verify API key is set correctly
621
- - **Session fails**: Ensure `session.modify` (Step 3) is called before sending queries
622
- - **Timeout on inference**: Model queries can take 10-30s; use 60s timeout
623
- - **Frame too large**: Use JPEG encoding with quality 0.8 to reduce payload size
624
- - **Requests overlap**: Gate with `isSending` flag to skip frames while previous request is in-flight