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 +219 -0
- package/README.md +130 -0
- package/index.d.ts +101 -0
- package/index.js +460 -0
- package/package.json +52 -0
- package/react.d.ts +45 -0
- package/react.js +138 -0
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;
|