booya-sdk 1.0.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.
package/PROMPT.md ADDED
@@ -0,0 +1,219 @@
1
+ # PROMPT.md — Booya SDK (AI-Optimized Documentation)
2
+
3
+ > This file is optimized for AI coding assistants (Cursor, Copilot, Lovable, v0, etc.).
4
+ > Paste or reference this file when building apps with the Booya SDK.
5
+
6
+ ## What is Booya SDK?
7
+
8
+ A JavaScript SDK for real-time audience measurement and emotion detection. It uses a WebAssembly engine to process video from the user's camera, detects faces, tracks engagement, and measures emotional response — all client-side. Events are automatically logged to the Booya backend.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install booya-sdk
14
+ ```
15
+
16
+ ## Credentials
17
+
18
+ You need two values from your Booya dashboard:
19
+ - `apiKey` — starts with `bya_`
20
+ - `appId` — your Base44 application ID
21
+
22
+ ## Vanilla JavaScript — Minimal Example
23
+
24
+ ```html
25
+ <div id="camera" style="width:640px;height:480px"></div>
26
+ <button id="start">Start</button>
27
+ <button id="stop">Stop</button>
28
+
29
+ <script type="module">
30
+ import { BooyaSDK } from 'booya-sdk';
31
+
32
+ const booya = new BooyaSDK({
33
+ apiKey: 'bya_YOUR_KEY',
34
+ appId: 'YOUR_APP_ID',
35
+ cdnBase: 'https://cdn.booya.ai/wasm/v1', // or omit to load from /public
36
+ });
37
+
38
+ await booya.init('#camera');
39
+
40
+ document.getElementById('start').onclick = async () => {
41
+ const sessionId = await booya.startSession();
42
+ console.log('Session:', sessionId);
43
+ };
44
+
45
+ document.getElementById('stop').onclick = async () => {
46
+ const summary = await booya.endSession();
47
+ console.log('Summary:', summary);
48
+ };
49
+
50
+ booya.onMetrics((m) => {
51
+ // m.emotions = { happy, sad, surprised, angry, neutral, disgust } (0-100)
52
+ // m.viewerCount = number of people looking at camera
53
+ // m.totalPersons = total faces detected
54
+ // m.engagementScore = 0-100
55
+ // m.dominantEmotion = string
56
+ });
57
+ </script>
58
+ ```
59
+
60
+ ## React — useBooya Hook
61
+
62
+ ```jsx
63
+ import { useBooya } from 'booya-sdk/react';
64
+
65
+ function MeasurementView() {
66
+ const {
67
+ containerRef, // attach to a <div>
68
+ metrics, // real-time BooyaMetrics or null
69
+ isReady, // true when WASM loaded
70
+ isRecording, // true during active session
71
+ sessionId, // current session ID or null
72
+ error, // last Error or null
73
+ start, // () => Promise<sessionId>
74
+ stop, // () => Promise<summary>
75
+ } = useBooya({
76
+ apiKey: 'bya_YOUR_KEY',
77
+ appId: 'YOUR_APP_ID',
78
+ cdnBase: 'https://cdn.booya.ai/wasm/v1',
79
+ });
80
+
81
+ return (
82
+ <div>
83
+ <div ref={containerRef} style={{ width: 640, height: 480 }} />
84
+
85
+ {!isReady && <p>Loading engine...</p>}
86
+
87
+ {metrics && (
88
+ <div>
89
+ <p>Emotion: {metrics.dominantEmotion}</p>
90
+ <p>Viewers: {metrics.viewerCount}</p>
91
+ <p>Engagement: {metrics.engagementScore}%</p>
92
+ </div>
93
+ )}
94
+
95
+ <button onClick={isRecording ? stop : start} disabled={!isReady}>
96
+ {isRecording ? 'End Session' : 'Start Session'}
97
+ </button>
98
+
99
+ {error && <p style={{color:'red'}}>{error.message}</p>}
100
+ </div>
101
+ );
102
+ }
103
+ ```
104
+
105
+ ## Data-Only API (No Camera / No WASM)
106
+
107
+ For dashboards, admin panels, or server-side analysis:
108
+
109
+ ```javascript
110
+ import { BooyaSDK } from 'booya-sdk';
111
+
112
+ // List sessions
113
+ const { data } = await BooyaSDK.api.getSessions('bya_KEY', 'APP_ID', {
114
+ dashboardId: 'optional-dashboard-id',
115
+ limit: 20,
116
+ });
117
+
118
+ // Get single session with events
119
+ const session = await BooyaSDK.api.getSession('bya_KEY', 'APP_ID', 'session-id');
120
+
121
+ // Get analytics summary
122
+ const analytics = await BooyaSDK.api.getAnalytics('bya_KEY', 'APP_ID', {
123
+ startDate: '2026-01-01',
124
+ endDate: '2026-03-01',
125
+ });
126
+ ```
127
+
128
+ ## WASM Assets
129
+
130
+ Three files must be accessible at runtime:
131
+ - `demo.js` (loader, ~200KB)
132
+ - `demo.wasm` (engine, ~5MB)
133
+ - `demo.data` (models, ~30MB)
134
+
135
+ Pass `cdnBase` to load from a CDN, or place them in your `/public` folder.
136
+
137
+ ## Key Configuration
138
+
139
+ | Param | Type | Required | Default | Notes |
140
+ |---|---|---|---|---|
141
+ | apiKey | string | yes | — | Starts with `bya_` |
142
+ | appId | string | yes | — | Base44 app ID |
143
+ | cdnBase | string | no | `''` | CDN URL for WASM files |
144
+ | dashboardId | string | no | null | Scope to a dashboard |
145
+ | skin | string | no | `'default'` | `'default'` / `'minimal'` / `'none'` |
146
+ | metricsInterval | number | no | 200 | ms between metric reads |
147
+ | eventInterval | number | no | 2000 | ms between event logs |
148
+
149
+ ## Metrics Shape
150
+
151
+ ```typescript
152
+ interface BooyaMetrics {
153
+ emotions: {
154
+ happy: number; // 0-100
155
+ sad: number; // 0-100
156
+ surprised: number; // 0-100
157
+ angry: number; // 0-100
158
+ neutral: number; // 0-100
159
+ disgust: number; // 0-100
160
+ };
161
+ viewerCount: number; // actively looking at camera
162
+ totalPersons: number; // faces detected
163
+ engagementScore: number; // 0-100
164
+ dominantEmotion: string; // key with highest score
165
+ }
166
+ ```
167
+
168
+ ## Common Patterns
169
+
170
+ ### Show emotion bars
171
+
172
+ ```jsx
173
+ {metrics && Object.entries(metrics.emotions).map(([name, value]) => (
174
+ <div key={name} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
175
+ <span style={{ width: 80 }}>{name}</span>
176
+ <div style={{ flex: 1, background: '#eee', borderRadius: 4, height: 8 }}>
177
+ <div style={{ width: `${value}%`, background: '#6366f1', borderRadius: 4, height: 8 }} />
178
+ </div>
179
+ <span>{Math.round(value)}%</span>
180
+ </div>
181
+ ))}
182
+ ```
183
+
184
+ ### Auto-start on mount
185
+
186
+ ```jsx
187
+ const { containerRef, isReady, start } = useBooya({ apiKey, appId });
188
+
189
+ useEffect(() => {
190
+ if (isReady) start();
191
+ }, [isReady]);
192
+ ```
193
+
194
+ ### Custom overlay (no built-in skin)
195
+
196
+ ```jsx
197
+ const { containerRef, metrics } = useBooya({ apiKey, appId, skin: 'none' });
198
+
199
+ return (
200
+ <div style={{ position: 'relative' }}>
201
+ <div ref={containerRef} style={{ width: '100%', height: 400 }} />
202
+ {metrics && (
203
+ <div style={{ position: 'absolute', top: 10, right: 10, background: '#000a', color: '#fff', padding: 8, borderRadius: 8 }}>
204
+ {metrics.dominantEmotion} — {metrics.viewerCount} viewers
205
+ </div>
206
+ )}
207
+ </div>
208
+ );
209
+ ```
210
+
211
+ ## Troubleshooting
212
+
213
+ | Issue | Fix |
214
+ |---|---|
215
+ | "container not found" | Pass a valid CSS selector or DOM element to `init()` |
216
+ | Camera permission denied | The browser blocked `getUserMedia` — check HTTPS and permissions |
217
+ | WASM fails to load | Verify `cdnBase` URL or that files exist in `/public` |
218
+ | No metrics | Ensure `startSession()` was called and face is visible to camera |
219
+ | 0 viewers but faces detected | "Viewers" = people looking at camera; check gaze angle |
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # booya-sdk
2
+
3
+ Real-time audience measurement and emotion detection SDK powered by [Booya AI](https://booya.ai).
4
+
5
+ Detect faces, track engagement, and measure emotional response in real-time using WebAssembly — all client-side. Events are logged to the Booya backend for analytics and dashboards.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install booya-sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```javascript
16
+ import { BooyaSDK } from 'booya-sdk';
17
+
18
+ const booya = new BooyaSDK({
19
+ apiKey: 'bya_your_api_key',
20
+ appId: 'your-base44-app-id',
21
+ });
22
+
23
+ await booya.init('#camera');
24
+ const sessionId = await booya.startSession();
25
+
26
+ booya.onMetrics((m) => {
27
+ console.log(m.emotions); // { happy, sad, surprised, angry, neutral, disgust }
28
+ console.log(m.viewerCount); // people actively looking at camera
29
+ console.log(m.totalPersons); // total faces detected
30
+ });
31
+
32
+ // When done:
33
+ const summary = await booya.endSession();
34
+ ```
35
+
36
+ ```html
37
+ <div id="camera" style="width: 640px; height: 480px;"></div>
38
+ ```
39
+
40
+ ## React
41
+
42
+ ```jsx
43
+ import { useBooya } from 'booya-sdk/react';
44
+
45
+ function MeasurementView() {
46
+ const { containerRef, metrics, isRecording, start, stop, error } = useBooya({
47
+ apiKey: 'bya_your_api_key',
48
+ appId: 'your-base44-app-id',
49
+ });
50
+
51
+ return (
52
+ <div>
53
+ <div ref={containerRef} style={{ width: 640, height: 480 }} />
54
+ {metrics && <p>Emotion: {metrics.dominantEmotion}</p>}
55
+ <button onClick={isRecording ? stop : start}>
56
+ {isRecording ? 'Stop' : 'Start'}
57
+ </button>
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## WASM Assets
64
+
65
+ The SDK requires three WASM engine files at runtime:
66
+ - `demo.js` — loader script
67
+ - `demo.wasm` — WebAssembly binary
68
+ - `demo.data` — model data (~30MB)
69
+
70
+ **Option A** — Host on your own CDN and pass `cdnBase`:
71
+
72
+ ```javascript
73
+ new BooyaSDK({ apiKey, appId, cdnBase: 'https://cdn.booya.ai/wasm/v1' });
74
+ ```
75
+
76
+ **Option B** — Place files in your public directory (e.g. `/public/demo.js`).
77
+
78
+ ## Configuration
79
+
80
+ | Option | Type | Default | Description |
81
+ |---|---|---|---|
82
+ | `apiKey` | `string` | *required* | Your Booya API key |
83
+ | `appId` | `string` | *required* | Base44 application ID |
84
+ | `cdnBase` | `string` | `''` | CDN URL for WASM assets |
85
+ | `dashboardId` | `string` | `null` | Scope session to a dashboard |
86
+ | `skin` | `string` | `'default'` | Overlay: `'default'`, `'minimal'`, `'none'` |
87
+ | `metricsInterval` | `number` | `200` | Read metrics every N ms |
88
+ | `eventInterval` | `number` | `2000` | Log events every N ms |
89
+ | `onMetrics` | `function` | `null` | Callback for real-time metrics |
90
+ | `onError` | `function` | `null` | Callback for errors |
91
+
92
+ ## API Methods
93
+
94
+ ### `BooyaSDK`
95
+
96
+ | Method | Returns | Description |
97
+ |---|---|---|
98
+ | `init(container)` | `Promise<void>` | Load WASM and attach to a container element |
99
+ | `startSession()` | `Promise<string>` | Start camera, create session, return session_id |
100
+ | `endSession()` | `Promise<SessionSummary>` | Stop recording and return session summary |
101
+ | `onMetrics(cb)` | `void` | Register real-time metrics callback |
102
+ | `onError(cb)` | `void` | Register error callback |
103
+ | `getMetrics()` | `BooyaMetrics \| null` | Latest metrics snapshot |
104
+ | `isReady()` | `boolean` | Whether the engine is loaded |
105
+ | `isRecording()` | `boolean` | Whether a session is active |
106
+ | `destroy()` | `void` | Clean up all resources |
107
+
108
+ ### Static API (data-only, no WASM)
109
+
110
+ ```javascript
111
+ const sessions = await BooyaSDK.api.getSessions(apiKey, appId, { dashboardId });
112
+ const session = await BooyaSDK.api.getSession(apiKey, appId, sessionId);
113
+ const analytics = await BooyaSDK.api.getAnalytics(apiKey, appId, { startDate, endDate });
114
+ ```
115
+
116
+ ## Metrics Object
117
+
118
+ ```typescript
119
+ {
120
+ emotions: { happy, sad, surprised, angry, neutral, disgust }, // 0-100
121
+ viewerCount: number, // people actively looking at camera
122
+ totalPersons: number, // total faces detected
123
+ engagementScore: number, // 0-100 aggregate score
124
+ dominantEmotion: string // highest-scored emotion
125
+ }
126
+ ```
127
+
128
+ ## License
129
+
130
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ export interface BooyaConfig {
2
+ /** Booya API key (required) */
3
+ apiKey: string;
4
+ /** Base44 app ID (required) */
5
+ appId: string;
6
+ /** Base44 server URL (default: https://base44.app) */
7
+ serverUrl?: string;
8
+ /** CDN base URL for WASM assets (demo.js, demo.wasm, demo.data) */
9
+ cdnBase?: string;
10
+ /** Dashboard ID to scope the session */
11
+ dashboardId?: string;
12
+ /** Overlay skin: 'default' | 'minimal' | 'none' (default: 'default') */
13
+ skin?: 'default' | 'minimal' | 'none';
14
+ /** Custom CSS injected into the container */
15
+ customCss?: string;
16
+ /** Callback for real-time metrics */
17
+ onMetrics?: (metrics: BooyaMetrics) => void;
18
+ /** Callback for errors */
19
+ onError?: (error: Error) => void;
20
+ /** Frequency of metrics reads in ms (default: 200) */
21
+ metricsInterval?: number;
22
+ /** Frequency of event logging in ms (default: 2000) */
23
+ eventInterval?: number;
24
+ }
25
+
26
+ export interface BooyaMetrics {
27
+ emotions: {
28
+ happy: number;
29
+ sad: number;
30
+ surprised: number;
31
+ angry: number;
32
+ neutral: number;
33
+ disgust: number;
34
+ };
35
+ viewerCount: number;
36
+ totalPersons: number;
37
+ engagementScore: number;
38
+ dominantEmotion: string;
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ export interface SessionSummary {
43
+ session_id: string;
44
+ duration: number;
45
+ total_events: number;
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ export declare class BooyaSDK {
50
+ constructor(config: BooyaConfig);
51
+
52
+ /** Initialize: load WASM engine and attach to a container */
53
+ init(container: string | HTMLElement): Promise<void>;
54
+
55
+ /** Register a metrics callback */
56
+ onMetrics(callback: (metrics: BooyaMetrics) => void): void;
57
+
58
+ /** Register an error callback */
59
+ onError(callback: (error: Error) => void): void;
60
+
61
+ /** Start camera, create a session, begin processing */
62
+ startSession(): Promise<string>;
63
+
64
+ /** End the session and return a summary */
65
+ endSession(): Promise<SessionSummary | null>;
66
+
67
+ /** Get the latest metrics snapshot */
68
+ getMetrics(): BooyaMetrics | null;
69
+
70
+ /** Check if the SDK is initialized */
71
+ isReady(): boolean;
72
+
73
+ /** Check if a session is active */
74
+ isRecording(): boolean;
75
+
76
+ /** Clean up all resources */
77
+ destroy(): void;
78
+
79
+ static api: {
80
+ getSessions(
81
+ apiKey: string,
82
+ appId: string,
83
+ options?: { serverUrl?: string; dashboardId?: string; limit?: number; offset?: number }
84
+ ): Promise<any>;
85
+
86
+ getSession(
87
+ apiKey: string,
88
+ appId: string,
89
+ sessionId: string,
90
+ options?: { serverUrl?: string }
91
+ ): Promise<any>;
92
+
93
+ getAnalytics(
94
+ apiKey: string,
95
+ appId: string,
96
+ options?: { serverUrl?: string; dashboardId?: string; startDate?: string; endDate?: string }
97
+ ): Promise<any>;
98
+ };
99
+ }
100
+
101
+ export default BooyaSDK;
package/index.js ADDED
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Booya SDK v1.0
3
+ *
4
+ * Real-time audience measurement and emotion detection.
5
+ *
6
+ * @example
7
+ * import { BooyaSDK } from 'booya-sdk';
8
+ *
9
+ * const booya = new BooyaSDK({
10
+ * apiKey: 'bya_...',
11
+ * appId: 'your-base44-app-id'
12
+ * });
13
+ * await booya.init('#camera-container');
14
+ * const sessionId = await booya.startSession();
15
+ * booya.onMetrics(m => console.log(m.emotions));
16
+ * const summary = await booya.endSession();
17
+ */
18
+
19
+ const DEFAULT_SERVER = 'https://base44.app';
20
+ const PROCESS_SCALE = 0.5;
21
+
22
+ export class BooyaSDK {
23
+ constructor(config = {}) {
24
+ if (!config.apiKey) throw new Error('BooyaSDK: apiKey is required');
25
+ if (!config.appId) throw new Error('BooyaSDK: appId is required');
26
+
27
+ this._apiKey = config.apiKey;
28
+ this._appId = config.appId;
29
+ this._serverUrl = config.serverUrl || DEFAULT_SERVER;
30
+ this._cdnBase = config.cdnBase || '';
31
+ this._skin = config.skin || 'default';
32
+ this._customCss = config.customCss || '';
33
+ this._onMetrics = config.onMetrics || null;
34
+ this._onError = config.onError || null;
35
+ this._metricsInterval = config.metricsInterval || 200;
36
+ this._eventInterval = config.eventInterval || 2000;
37
+
38
+ this._container = null;
39
+ this._videoEl = null;
40
+ this._canvasEl = null;
41
+ this._stream = null;
42
+ this._engine = null;
43
+ this._sessionId = null;
44
+ this._dashboardId = config.dashboardId || null;
45
+ this._running = false;
46
+ this._animFrame = null;
47
+ this._srcMat = null;
48
+ this._dstMat = null;
49
+ this._captureCanvas = null;
50
+ this._captureCtx = null;
51
+ this._outCtx = null;
52
+ this._frameSize = { width: 0, height: 0 };
53
+ this._resultImageData = null;
54
+ this._lastMetricsTime = 0;
55
+ this._lastEventTime = 0;
56
+ this._metricsOverlay = null;
57
+ this._latestMetrics = null;
58
+ this._ready = false;
59
+ }
60
+
61
+ /**
62
+ * Initialize the SDK: load WASM engine and set up the container.
63
+ * @param {string|HTMLElement} container - CSS selector or DOM element
64
+ */
65
+ async init(container) {
66
+ if (typeof container === 'string') {
67
+ this._container = document.querySelector(container);
68
+ } else {
69
+ this._container = container;
70
+ }
71
+
72
+ if (!this._container) throw new Error('BooyaSDK: container not found');
73
+
74
+ this._container.style.position = 'relative';
75
+ this._container.style.overflow = 'hidden';
76
+
77
+ this._videoEl = document.createElement('video');
78
+ this._videoEl.setAttribute('playsinline', '');
79
+ this._videoEl.setAttribute('autoplay', '');
80
+ this._videoEl.setAttribute('muted', '');
81
+ this._videoEl.muted = true;
82
+ this._videoEl.style.display = 'none';
83
+
84
+ this._canvasEl = document.createElement('canvas');
85
+ this._canvasEl.style.width = '100%';
86
+ this._canvasEl.style.height = '100%';
87
+ this._canvasEl.style.objectFit = 'cover';
88
+
89
+ this._container.appendChild(this._videoEl);
90
+ this._container.appendChild(this._canvasEl);
91
+
92
+ if (this._skin !== 'none') {
93
+ this._createOverlay();
94
+ }
95
+
96
+ if (this._customCss) {
97
+ const style = document.createElement('style');
98
+ style.textContent = this._customCss;
99
+ this._container.appendChild(style);
100
+ }
101
+
102
+ await this._loadWasm();
103
+ this._ready = true;
104
+ }
105
+
106
+ /** Register a callback for real-time metrics. */
107
+ onMetrics(callback) {
108
+ this._onMetrics = callback;
109
+ }
110
+
111
+ /** Register a callback for errors. */
112
+ onError(callback) {
113
+ this._onError = callback;
114
+ }
115
+
116
+ /**
117
+ * Start camera and processing. Creates a session via the API.
118
+ * @returns {Promise<string>} session_id
119
+ */
120
+ async startSession() {
121
+ if (!this._ready) throw new Error('BooyaSDK: call init() first');
122
+
123
+ await this._startCamera();
124
+ this._initEngine();
125
+
126
+ const res = await this._apiCall('apiCreateSession', {
127
+ dashboard_id: this._dashboardId,
128
+ metadata: { source: 'sdk', skin: this._skin }
129
+ });
130
+
131
+ this._sessionId = res.data.session_id;
132
+ this._running = true;
133
+ this._processFrame();
134
+
135
+ return this._sessionId;
136
+ }
137
+
138
+ /**
139
+ * End the current session.
140
+ * @returns {Promise<Object>} session summary
141
+ */
142
+ async endSession() {
143
+ this._running = false;
144
+ if (this._animFrame) {
145
+ cancelAnimationFrame(this._animFrame);
146
+ this._animFrame = null;
147
+ }
148
+
149
+ this._stopCamera();
150
+
151
+ if (!this._sessionId) return null;
152
+
153
+ const res = await this._apiCall('apiEndSession', {
154
+ session_id: this._sessionId
155
+ });
156
+
157
+ const sessionId = this._sessionId;
158
+ this._sessionId = null;
159
+
160
+ return { session_id: sessionId, ...res.data };
161
+ }
162
+
163
+ /** Get the latest metrics snapshot. */
164
+ getMetrics() {
165
+ return this._latestMetrics;
166
+ }
167
+
168
+ /** Check if the SDK is initialized and ready. */
169
+ isReady() {
170
+ return this._ready;
171
+ }
172
+
173
+ /** Check if a session is currently active. */
174
+ isRecording() {
175
+ return this._running && !!this._sessionId;
176
+ }
177
+
178
+ /** Destroy the SDK and clean up resources. */
179
+ destroy() {
180
+ this._running = false;
181
+ if (this._animFrame) cancelAnimationFrame(this._animFrame);
182
+ this._stopCamera();
183
+ if (this._srcMat) { try { this._srcMat.delete(); } catch (_) {} }
184
+ if (this._dstMat) { try { this._dstMat.delete(); } catch (_) {} }
185
+ this._engine = null;
186
+ if (this._container) {
187
+ this._container.innerHTML = '';
188
+ }
189
+ }
190
+
191
+ // ── Private ──────────────────────────────────────────────
192
+
193
+ async _loadWasm() {
194
+ return new Promise((resolve, reject) => {
195
+ const Module = window.Module;
196
+
197
+ if (Module && typeof Module.Engine === 'function') {
198
+ resolve();
199
+ return;
200
+ }
201
+
202
+ window.Module = {
203
+ ...(Module || {}),
204
+ preloadResults: (Module && Module.preloadResults) || {},
205
+ dataFileDownloads: (Module && Module.dataFileDownloads) || {},
206
+ expectedDataFileDownloads: (Module && Module.expectedDataFileDownloads) || 0,
207
+ locateFile: (path) => {
208
+ const filename = path.split('/').pop();
209
+ return this._cdnBase ? `${this._cdnBase}/${filename}` : `/${filename}`;
210
+ },
211
+ onRuntimeInitialized: () => resolve()
212
+ };
213
+
214
+ const existing = document.querySelector('script[data-booya-wasm="demo-js"]');
215
+ if (existing) { resolve(); return; }
216
+
217
+ const script = document.createElement('script');
218
+ script.src = this._cdnBase ? `${this._cdnBase}/demo.js` : '/demo.js';
219
+ script.async = true;
220
+ script.dataset.booyaWasm = 'demo-js';
221
+ script.onerror = () => reject(new Error('Failed to load WASM engine'));
222
+ document.head.appendChild(script);
223
+ });
224
+ }
225
+
226
+ _initEngine() {
227
+ if (this._engine) return;
228
+ const Module = window.Module;
229
+ this._engine = new Module.Engine();
230
+ this._engine.init('resources');
231
+ }
232
+
233
+ async _startCamera() {
234
+ const stream = await navigator.mediaDevices.getUserMedia({
235
+ video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } },
236
+ audio: false
237
+ });
238
+ this._stream = stream;
239
+ this._videoEl.srcObject = stream;
240
+ await new Promise((resolve) => { this._videoEl.onloadedmetadata = resolve; });
241
+ await this._videoEl.play();
242
+ }
243
+
244
+ _stopCamera() {
245
+ if (this._stream) {
246
+ this._stream.getTracks().forEach(t => t.stop());
247
+ this._stream = null;
248
+ }
249
+ if (this._videoEl) {
250
+ this._videoEl.srcObject = null;
251
+ }
252
+ }
253
+
254
+ _processFrame() {
255
+ if (!this._running) return;
256
+
257
+ const video = this._videoEl;
258
+ const canvas = this._canvasEl;
259
+ const engine = this._engine;
260
+ const Module = window.Module;
261
+
262
+ if (!video || !canvas || !engine || video.readyState < 4) {
263
+ this._animFrame = requestAnimationFrame(() => this._processFrame());
264
+ return;
265
+ }
266
+
267
+ const width = video.videoWidth;
268
+ const height = video.videoHeight;
269
+ const pw = Math.max(1, Math.floor(width * PROCESS_SCALE));
270
+ const ph = Math.max(1, Math.floor(height * PROCESS_SCALE));
271
+
272
+ if (width === 0 || height === 0) {
273
+ this._animFrame = requestAnimationFrame(() => this._processFrame());
274
+ return;
275
+ }
276
+
277
+ if (!this._captureCanvas) {
278
+ this._captureCanvas = document.createElement('canvas');
279
+ }
280
+ if (this._captureCanvas.width !== pw || this._captureCanvas.height !== ph) {
281
+ this._captureCanvas.width = pw;
282
+ this._captureCanvas.height = ph;
283
+ this._captureCtx = this._captureCanvas.getContext('2d', { willReadFrequently: true });
284
+ }
285
+
286
+ const sizeChanged = this._frameSize.width !== pw || this._frameSize.height !== ph;
287
+ if (!this._srcMat || !this._dstMat || sizeChanged) {
288
+ if (this._srcMat) { try { this._srcMat.delete(); } catch (_) {} }
289
+ if (this._dstMat) { try { this._dstMat.delete(); } catch (_) {} }
290
+ this._srcMat = new Module.Mat(ph, pw);
291
+ this._dstMat = new Module.Mat(ph, pw);
292
+ this._frameSize = { width: pw, height: ph };
293
+ this._resultImageData = null;
294
+ }
295
+
296
+ try {
297
+ this._captureCtx.drawImage(video, 0, 0, pw, ph);
298
+ const imageData = this._captureCtx.getImageData(0, 0, pw, ph);
299
+ this._srcMat.data.set(imageData.data);
300
+
301
+ engine.process(this._srcMat, this._dstMat);
302
+
303
+ if (!this._outCtx || canvas.width !== pw || canvas.height !== ph) {
304
+ canvas.width = pw;
305
+ canvas.height = ph;
306
+ this._outCtx = canvas.getContext('2d');
307
+ this._outCtx.imageSmoothingEnabled = true;
308
+ this._resultImageData = null;
309
+ }
310
+
311
+ if (!this._resultImageData) {
312
+ this._resultImageData = new ImageData(pw, ph);
313
+ }
314
+ this._resultImageData.data.set(this._dstMat.data);
315
+ this._outCtx.putImageData(this._resultImageData, 0, 0);
316
+
317
+ const now = Date.now();
318
+ if (now - this._lastMetricsTime > this._metricsInterval) {
319
+ this._lastMetricsTime = now;
320
+ try {
321
+ const jsonStr = engine.getMetricsJson();
322
+ const parsed = JSON.parse(jsonStr);
323
+ this._latestMetrics = parsed;
324
+
325
+ if (this._onMetrics) this._onMetrics(parsed);
326
+ if (this._metricsOverlay) this._updateOverlay(parsed);
327
+
328
+ if (this._sessionId && (now - this._lastEventTime > this._eventInterval)) {
329
+ this._lastEventTime = now;
330
+ this._logEvent(parsed);
331
+ }
332
+ } catch (_) {}
333
+ }
334
+ } catch (err) {
335
+ if (this._onError) this._onError(err);
336
+ }
337
+
338
+ this._animFrame = requestAnimationFrame(() => this._processFrame());
339
+ }
340
+
341
+ async _logEvent(parsed) {
342
+ if (!this._sessionId) return;
343
+ try {
344
+ await this._apiCall('apiLogEvent', {
345
+ session_id: this._sessionId,
346
+ emotions: parsed.emotions || {},
347
+ viewer_count: parsed.viewerCount || 0,
348
+ attention_score: parsed.engagementScore || 0,
349
+ timestamp: new Date().toISOString()
350
+ });
351
+ } catch (_) {}
352
+ }
353
+
354
+ _buildFunctionUrl(functionName) {
355
+ return `${this._serverUrl}/api/apps/${this._appId}/functions/${functionName}`;
356
+ }
357
+
358
+ async _apiCall(functionName, body = {}) {
359
+ const url = this._buildFunctionUrl(functionName);
360
+ const res = await fetch(url, {
361
+ method: 'POST',
362
+ headers: {
363
+ 'Content-Type': 'application/json',
364
+ 'X-API-Key': this._apiKey
365
+ },
366
+ body: JSON.stringify(body)
367
+ });
368
+ const data = await res.json();
369
+ if (!res.ok) throw new Error(data.error || 'API request failed');
370
+ return data;
371
+ }
372
+
373
+ _createOverlay() {
374
+ this._metricsOverlay = document.createElement('div');
375
+ this._metricsOverlay.className = 'booya-metrics-overlay';
376
+
377
+ const baseStyles = `
378
+ position: absolute; bottom: 16px; left: 16px;
379
+ padding: 12px 16px; border-radius: 12px;
380
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
381
+ font-size: 13px; line-height: 1.5; pointer-events: none; z-index: 10;
382
+ `;
383
+
384
+ if (this._skin === 'default') {
385
+ this._metricsOverlay.style.cssText = baseStyles +
386
+ 'background: rgba(0,0,0,0.75); color: #fff; backdrop-filter: blur(10px);';
387
+ } else if (this._skin === 'minimal') {
388
+ this._metricsOverlay.style.cssText = baseStyles +
389
+ 'background: transparent; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.8);';
390
+ }
391
+
392
+ this._metricsOverlay.innerHTML = '<div class="booya-metrics-content">Initializing...</div>';
393
+ this._container.appendChild(this._metricsOverlay);
394
+ }
395
+
396
+ _updateOverlay(metrics) {
397
+ if (!this._metricsOverlay) return;
398
+
399
+ let dominant = 'neutral';
400
+ let maxVal = 0;
401
+ if (metrics.emotions) {
402
+ for (const [k, v] of Object.entries(metrics.emotions)) {
403
+ if (v > maxVal) { maxVal = v; dominant = k; }
404
+ }
405
+ }
406
+
407
+ const icons = {
408
+ happy: '\u{1F60A}', surprised: '\u{1F632}', angry: '\u{1F621}',
409
+ sad: '\u{1F622}', disgust: '\u{1F922}', neutral: '\u{1F610}'
410
+ };
411
+
412
+ const content = this._metricsOverlay.querySelector('.booya-metrics-content') || this._metricsOverlay;
413
+ content.innerHTML = `
414
+ <div style="font-weight:600;margin-bottom:4px">${icons[dominant] || ''} ${dominant}</div>
415
+ <div>Viewers: ${metrics.viewerCount || 0}</div>
416
+ <div>Engagement: ${Math.round(metrics.engagementScore || 0)}%</div>
417
+ `;
418
+ }
419
+ }
420
+
421
+ /** Static API helpers — data-only access, no WASM needed. */
422
+ BooyaSDK.api = {
423
+ _buildUrl(appId, fn, serverUrl) {
424
+ return `${serverUrl || DEFAULT_SERVER}/api/apps/${appId}/functions/${fn}`;
425
+ },
426
+
427
+ async getSessions(apiKey, appId, options = {}) {
428
+ const { serverUrl, dashboardId, limit, offset } = options;
429
+ const params = new URLSearchParams();
430
+ if (dashboardId) params.set('dashboard_id', dashboardId);
431
+ if (limit) params.set('limit', limit);
432
+ if (offset) params.set('offset', offset);
433
+ const url = this._buildUrl(appId, 'apiGetSessions', serverUrl);
434
+ const res = await fetch(`${url}?${params}`, { headers: { 'X-API-Key': apiKey } });
435
+ return res.json();
436
+ },
437
+
438
+ async getSession(apiKey, appId, sessionId, options = {}) {
439
+ const url = this._buildUrl(appId, 'apiGetSession', options.serverUrl);
440
+ const res = await fetch(url, {
441
+ method: 'POST',
442
+ headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
443
+ body: JSON.stringify({ session_id: sessionId })
444
+ });
445
+ return res.json();
446
+ },
447
+
448
+ async getAnalytics(apiKey, appId, options = {}) {
449
+ const { serverUrl, dashboardId, startDate, endDate } = options;
450
+ const params = new URLSearchParams();
451
+ if (dashboardId) params.set('dashboard_id', dashboardId);
452
+ if (startDate) params.set('start_date', startDate);
453
+ if (endDate) params.set('end_date', endDate);
454
+ const url = this._buildUrl(appId, 'apiGetAnalytics', serverUrl);
455
+ const res = await fetch(`${url}?${params}`, { headers: { 'X-API-Key': apiKey } });
456
+ return res.json();
457
+ }
458
+ };
459
+
460
+ export default BooyaSDK;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "booya-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Real-time audience measurement and emotion detection SDK powered by Booya AI",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "module": "./index.js",
8
+ "types": "./index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./index.js",
12
+ "types": "./index.d.ts"
13
+ },
14
+ "./react": {
15
+ "import": "./react.js",
16
+ "types": "./react.d.ts"
17
+ }
18
+ },
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts",
22
+ "react.js",
23
+ "react.d.ts",
24
+ "PROMPT.md",
25
+ "README.md"
26
+ ],
27
+ "keywords": [
28
+ "booya",
29
+ "emotion-detection",
30
+ "facial-recognition",
31
+ "audience-measurement",
32
+ "engagement-analytics",
33
+ "wasm",
34
+ "real-time",
35
+ "ai"
36
+ ],
37
+ "author": "Booya AI",
38
+ "license": "MIT",
39
+ "homepage": "https://booya.ai",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/booya-ai/booya-sdk"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=16.8.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "react": {
49
+ "optional": true
50
+ }
51
+ }
52
+ }
package/react.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { RefObject } from 'react';
2
+ import { BooyaSDK, BooyaMetrics, SessionSummary } from './index';
3
+
4
+ export interface UseBooyaConfig {
5
+ /** Booya API key (required) */
6
+ apiKey: string;
7
+ /** Base44 app ID (required) */
8
+ appId: string;
9
+ /** CDN base URL for WASM assets */
10
+ cdnBase?: string;
11
+ /** Dashboard ID to scope the session */
12
+ dashboardId?: string;
13
+ /** Overlay skin: 'default' | 'minimal' | 'none' */
14
+ skin?: 'default' | 'minimal' | 'none';
15
+ /** Frequency of metrics reads in ms (default: 200) */
16
+ metricsInterval?: number;
17
+ /** Frequency of event logging in ms (default: 2000) */
18
+ eventInterval?: number;
19
+ /** Automatically initialize on mount (default: true) */
20
+ autoInit?: boolean;
21
+ }
22
+
23
+ export interface UseBooyaResult {
24
+ /** Attach this ref to the container div */
25
+ containerRef: RefObject<HTMLDivElement>;
26
+ /** Latest real-time metrics (null until first frame) */
27
+ metrics: BooyaMetrics | null;
28
+ /** true when WASM engine is loaded and ready */
29
+ isReady: boolean;
30
+ /** true when a session is active */
31
+ isRecording: boolean;
32
+ /** Current session ID, or null */
33
+ sessionId: string | null;
34
+ /** Last error, or null */
35
+ error: Error | null;
36
+ /** Start a new session */
37
+ start(): Promise<string>;
38
+ /** End the current session */
39
+ stop(): Promise<SessionSummary | null>;
40
+ /** Direct access to the underlying SDK instance */
41
+ sdk: BooyaSDK | null;
42
+ }
43
+
44
+ export declare function useBooya(config: UseBooyaConfig): UseBooyaResult;
45
+ export default useBooya;
package/react.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Booya SDK React Hook
3
+ *
4
+ * @example
5
+ * import { useBooya } from 'booya-sdk/react';
6
+ *
7
+ * function MeasurementView() {
8
+ * const { containerRef, metrics, isRecording, start, stop, error } = useBooya({
9
+ * apiKey: 'bya_...',
10
+ * appId: 'your-app-id'
11
+ * });
12
+ *
13
+ * return (
14
+ * <div>
15
+ * <div ref={containerRef} style={{ width: 640, height: 480 }} />
16
+ * {metrics && <p>Dominant: {metrics.dominantEmotion}</p>}
17
+ * <button onClick={isRecording ? stop : start}>
18
+ * {isRecording ? 'Stop' : 'Start'}
19
+ * </button>
20
+ * {error && <p>Error: {error.message}</p>}
21
+ * </div>
22
+ * );
23
+ * }
24
+ */
25
+
26
+ import { useState, useEffect, useRef, useCallback } from 'react';
27
+ import { BooyaSDK } from './index.js';
28
+
29
+ /**
30
+ * @param {Object} config
31
+ * @param {string} config.apiKey - Booya API key
32
+ * @param {string} config.appId - Base44 app ID
33
+ * @param {string} [config.cdnBase] - CDN URL for WASM assets
34
+ * @param {string} [config.dashboardId] - Scope to a dashboard
35
+ * @param {string} [config.skin='default'] - 'default' | 'minimal' | 'none'
36
+ * @param {number} [config.metricsInterval=200] - How often to read metrics (ms)
37
+ * @param {number} [config.eventInterval=2000] - How often to log events (ms)
38
+ * @param {boolean} [config.autoInit=true] - Initialize on mount
39
+ */
40
+ export function useBooya(config) {
41
+ const {
42
+ apiKey,
43
+ appId,
44
+ cdnBase,
45
+ dashboardId,
46
+ skin,
47
+ metricsInterval,
48
+ eventInterval,
49
+ autoInit = true,
50
+ } = config;
51
+
52
+ const containerRef = useRef(null);
53
+ const sdkRef = useRef(null);
54
+ const mountedRef = useRef(true);
55
+
56
+ const [isReady, setIsReady] = useState(false);
57
+ const [isRecording, setIsRecording] = useState(false);
58
+ const [metrics, setMetrics] = useState(null);
59
+ const [sessionId, setSessionId] = useState(null);
60
+ const [error, setError] = useState(null);
61
+
62
+ useEffect(() => {
63
+ mountedRef.current = true;
64
+ return () => { mountedRef.current = false; };
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ if (!autoInit || !containerRef.current || sdkRef.current) return;
69
+
70
+ const sdk = new BooyaSDK({
71
+ apiKey,
72
+ appId,
73
+ cdnBase,
74
+ dashboardId,
75
+ skin,
76
+ metricsInterval,
77
+ eventInterval,
78
+ onMetrics: (m) => { if (mountedRef.current) setMetrics(m); },
79
+ onError: (e) => { if (mountedRef.current) setError(e); },
80
+ });
81
+
82
+ sdkRef.current = sdk;
83
+
84
+ sdk.init(containerRef.current)
85
+ .then(() => { if (mountedRef.current) setIsReady(true); })
86
+ .catch((e) => { if (mountedRef.current) setError(e); });
87
+
88
+ return () => {
89
+ sdk.destroy();
90
+ sdkRef.current = null;
91
+ };
92
+ }, [apiKey, appId, cdnBase, dashboardId, skin, metricsInterval, eventInterval, autoInit]);
93
+
94
+ const start = useCallback(async () => {
95
+ if (!sdkRef.current) return;
96
+ setError(null);
97
+ try {
98
+ const sid = await sdkRef.current.startSession();
99
+ if (mountedRef.current) {
100
+ setSessionId(sid);
101
+ setIsRecording(true);
102
+ }
103
+ return sid;
104
+ } catch (e) {
105
+ if (mountedRef.current) setError(e);
106
+ throw e;
107
+ }
108
+ }, []);
109
+
110
+ const stop = useCallback(async () => {
111
+ if (!sdkRef.current) return;
112
+ try {
113
+ const summary = await sdkRef.current.endSession();
114
+ if (mountedRef.current) {
115
+ setIsRecording(false);
116
+ setSessionId(null);
117
+ }
118
+ return summary;
119
+ } catch (e) {
120
+ if (mountedRef.current) setError(e);
121
+ throw e;
122
+ }
123
+ }, []);
124
+
125
+ return {
126
+ containerRef,
127
+ metrics,
128
+ isReady,
129
+ isRecording,
130
+ sessionId,
131
+ error,
132
+ start,
133
+ stop,
134
+ sdk: sdkRef.current,
135
+ };
136
+ }
137
+
138
+ export default useBooya;