@ateam-ai-mobile/expo 0.1.22 → 0.1.24

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,5 +1,6 @@
1
1
  import { AiLiveProvider } from '@ateam-ai-mobile/plugin-runtime';
2
2
 
3
+ declare const PHONE_MAX_PRE_ROLL_SEC = 5;
3
4
  declare const LIVE_KIND = "ai-models.live";
4
5
  interface AiLiveDeps {
5
6
  WebSocketImpl: any;
@@ -7,6 +8,8 @@ interface AiLiveDeps {
7
8
  token(): string | null;
8
9
  tenant(): string | null;
9
10
  jpegById(id: string, maxSide: number, quality: number): Promise<string>;
11
+ /** The frame's signature for the motion gate (signature.ts); null = could not take it (the picture is then sent). */
12
+ signatureById(id: string): Promise<ArrayLike<number> | null>;
10
13
  now(): number;
11
14
  setInterval(fn: () => void, ms: number): any;
12
15
  clearInterval(handle: any): void;
@@ -23,4 +26,4 @@ declare const CLOSE_WAIT_MS = 3000;
23
26
  declare const defaultAiLiveDeps: () => AiLiveDeps;
24
27
  declare function createAiLiveProvider(overrides?: Partial<AiLiveDeps>): AiLiveProvider;
25
28
 
26
- export { type AiLiveDeps, CLOSE_WAIT_MS, LIVE_KIND, OPEN_TIMEOUT_MS, createAiLiveProvider, defaultAiLiveDeps };
29
+ export { type AiLiveDeps, CLOSE_WAIT_MS, LIVE_KIND, OPEN_TIMEOUT_MS, PHONE_MAX_PRE_ROLL_SEC, createAiLiveProvider, defaultAiLiveDeps };
@@ -1,5 +1,6 @@
1
1
  import { AiLiveProvider } from '@ateam-ai-mobile/plugin-runtime';
2
2
 
3
+ declare const PHONE_MAX_PRE_ROLL_SEC = 5;
3
4
  declare const LIVE_KIND = "ai-models.live";
4
5
  interface AiLiveDeps {
5
6
  WebSocketImpl: any;
@@ -7,6 +8,8 @@ interface AiLiveDeps {
7
8
  token(): string | null;
8
9
  tenant(): string | null;
9
10
  jpegById(id: string, maxSide: number, quality: number): Promise<string>;
11
+ /** The frame's signature for the motion gate (signature.ts); null = could not take it (the picture is then sent). */
12
+ signatureById(id: string): Promise<ArrayLike<number> | null>;
10
13
  now(): number;
11
14
  setInterval(fn: () => void, ms: number): any;
12
15
  clearInterval(handle: any): void;
@@ -23,4 +26,4 @@ declare const CLOSE_WAIT_MS = 3000;
23
26
  declare const defaultAiLiveDeps: () => AiLiveDeps;
24
27
  declare function createAiLiveProvider(overrides?: Partial<AiLiveDeps>): AiLiveProvider;
25
28
 
26
- export { type AiLiveDeps, CLOSE_WAIT_MS, LIVE_KIND, OPEN_TIMEOUT_MS, createAiLiveProvider, defaultAiLiveDeps };
29
+ export { type AiLiveDeps, CLOSE_WAIT_MS, LIVE_KIND, OPEN_TIMEOUT_MS, PHONE_MAX_PRE_ROLL_SEC, createAiLiveProvider, defaultAiLiveDeps };
@@ -23,6 +23,7 @@ __export(provider_exports, {
23
23
  CLOSE_WAIT_MS: () => CLOSE_WAIT_MS,
24
24
  LIVE_KIND: () => LIVE_KIND,
25
25
  OPEN_TIMEOUT_MS: () => OPEN_TIMEOUT_MS,
26
+ PHONE_MAX_PRE_ROLL_SEC: () => PHONE_MAX_PRE_ROLL_SEC,
26
27
  createAiLiveProvider: () => createAiLiveProvider,
27
28
  defaultAiLiveDeps: () => defaultAiLiveDeps
28
29
  });
@@ -131,7 +132,103 @@ function base64ToBytes(b64) {
131
132
  return o === out.length ? out : out.subarray(0, o);
132
133
  }
133
134
 
135
+ // src/aiLive/motionGate.ts
136
+ var SIG_W = 16;
137
+ var SIG_H = 12;
138
+ var SLACK_SEC = 0.15;
139
+ var SENSITIVITY = Object.freeze({ low: 0.06, medium: 0.03, high: 0.015 });
140
+ var GATE_DEFAULTS = Object.freeze({ enabled: true, sensitivity: "medium", preRollSec: 1, postRollSec: 1, heartbeatSec: 8 });
141
+ function difference(a, b) {
142
+ if (!a || !b || a.length !== b.length || !a.length) return 1;
143
+ let sum = 0;
144
+ for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
145
+ return sum / (a.length * 255);
146
+ }
147
+ function gateConfigFrom(d) {
148
+ const c = { ...GATE_DEFAULTS };
149
+ if (!d || typeof d !== "object") return c;
150
+ if (typeof d.enabled === "boolean") c.enabled = d.enabled;
151
+ if (typeof d.sensitivity === "string" && Object.prototype.hasOwnProperty.call(SENSITIVITY, d.sensitivity)) c.sensitivity = d.sensitivity;
152
+ for (const k of ["preRollSec", "postRollSec", "heartbeatSec"]) {
153
+ const v = Number(d[k]);
154
+ if (Number.isFinite(v) && v >= 0 && v <= 60) c[k] = v;
155
+ }
156
+ return c;
157
+ }
158
+ function createMotionGate(cfg = {}) {
159
+ const c = { ...GATE_DEFAULTS, ...cfg };
160
+ const threshold = SENSITIVITY[c.sensitivity] ?? SENSITIVITY.medium;
161
+ let prevSig = null;
162
+ let lastChangeT = -Infinity;
163
+ let lastSentT = -Infinity;
164
+ let held = [];
165
+ const n = { offered: 0, sent: 0, held: 0, heartbeats: 0, changes: 0 };
166
+ const sendNow = (t, item, reason, withPreRoll = false) => {
167
+ const out = withPreRoll ? held.filter((h) => t - h.t <= c.preRollSec + SLACK_SEC).map((h) => h.item) : [];
168
+ held = [];
169
+ out.push(item);
170
+ lastSentT = t;
171
+ n.sent += out.length;
172
+ return { send: out, reason };
173
+ };
174
+ return {
175
+ offer({ t, sig, item }) {
176
+ n.offered++;
177
+ if (!c.enabled) {
178
+ lastSentT = t;
179
+ n.sent++;
180
+ return { send: [item], reason: "off" };
181
+ }
182
+ const first = prevSig == null;
183
+ const d = difference(prevSig, sig);
184
+ prevSig = sig;
185
+ if (first) return sendNow(t, item, "first");
186
+ if (d > threshold) {
187
+ lastChangeT = t;
188
+ n.changes++;
189
+ return sendNow(t, item, "change", true);
190
+ }
191
+ if (t - lastChangeT <= c.postRollSec + SLACK_SEC) return sendNow(t, item, "after");
192
+ if (t - lastSentT >= c.heartbeatSec) {
193
+ n.heartbeats++;
194
+ return sendNow(t, item, "heartbeat");
195
+ }
196
+ held.push({ t, item });
197
+ while (held.length && t - held[0].t > c.preRollSec + 1) held.shift();
198
+ n.held++;
199
+ return { send: [], reason: "quiet" };
200
+ },
201
+ counters: () => ({ ...n }),
202
+ config: () => ({ ...c })
203
+ };
204
+ }
205
+
206
+ // src/aiLive/signature.ts
207
+ var import_jpeg_js = require("jpeg-js");
208
+ var SIG_JPEG_SIDE = 64;
209
+ function signatureFromJpeg(b64) {
210
+ const img = (0, import_jpeg_js.decode)(base64ToBytes(b64), { useTArray: true, formatAsRGBA: true, maxResolutionInMP: 1 });
211
+ const { width: w, height: h, data } = img;
212
+ if (!w || !h) throw new Error("empty picture");
213
+ const sum = new Float64Array(SIG_W * SIG_H);
214
+ const cnt = new Uint32Array(SIG_W * SIG_H);
215
+ for (let y = 0; y < h; y++) {
216
+ const cy = Math.min(SIG_H - 1, Math.floor(y * SIG_H / h));
217
+ for (let x = 0; x < w; x++) {
218
+ const cx = Math.min(SIG_W - 1, Math.floor(x * SIG_W / w));
219
+ const i = (y * w + x) * 4;
220
+ const k = cy * SIG_W + cx;
221
+ sum[k] += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
222
+ cnt[k]++;
223
+ }
224
+ }
225
+ const out = new Uint8Array(SIG_W * SIG_H);
226
+ for (let k = 0; k < out.length; k++) out[k] = cnt[k] ? Math.round(sum[k] / cnt[k]) : 0;
227
+ return out;
228
+ }
229
+
134
230
  // src/aiLive/provider.ts
231
+ var PHONE_MAX_PRE_ROLL_SEC = 5;
135
232
  var LIVE_KIND = "ai-models.live";
136
233
  var FRESH_MS = 3e3;
137
234
  var MAX_BUFFERED = 512 * 1024;
@@ -145,13 +242,14 @@ var defaultAiLiveDeps = () => ({
145
242
  token: import_core.getToken,
146
243
  tenant: import_core.getTenant,
147
244
  jpegById,
245
+ signatureById: async (id) => signatureFromJpeg(await jpegById(id, SIG_JPEG_SIDE, 0.6)),
148
246
  now: Date.now,
149
247
  setInterval: (fn, ms) => setInterval(fn, ms),
150
248
  clearInterval: (h) => clearInterval(h),
151
249
  setTimeout: (fn, ms) => setTimeout(fn, ms),
152
250
  clearTimeout: (h) => clearTimeout(h)
153
251
  });
154
- var idle = () => ({ state: "idle", camera: null, sent: 0, skipped: 0, lastEpisode: null, lastError: null });
252
+ var idle = () => ({ state: "idle", camera: null, sent: 0, skipped: 0, heldBack: 0, lastEpisode: null, lastError: null });
155
253
  function createAiLiveProvider(overrides = {}) {
156
254
  const deps = { ...defaultAiLiveDeps(), ...overrides };
157
255
  let owner = null;
@@ -167,9 +265,11 @@ function createAiLiveProvider(overrides = {}) {
167
265
  let closingOnPurpose = false;
168
266
  let seq = 0;
169
267
  let busy = false;
268
+ let owedGaps = 0;
170
269
  let gen = 0;
171
270
  let lastFrameId = null;
172
271
  let camera = DEFAULT_CAMERA;
272
+ let gate = null;
173
273
  let lastOwner = null;
174
274
  const episodeCbs = /* @__PURE__ */ new Map();
175
275
  const snapCbs = /* @__PURE__ */ new Map();
@@ -212,7 +312,6 @@ function createAiLiveProvider(overrides = {}) {
212
312
  }
213
313
  }
214
314
  async function slot() {
215
- const mySeq = seq++;
216
315
  const g = gen;
217
316
  const sock = ws;
218
317
  const current = () => g === gen && ws === sock && sock?.readyState === 1;
@@ -221,13 +320,22 @@ function createAiLiveProvider(overrides = {}) {
221
320
  if (g === gen) void teardown("stopped", import_plugin_runtime.CONSENT_TAKEN_BACK);
222
321
  return true;
223
322
  };
323
+ const gap = (lastError) => {
324
+ seq++;
325
+ set(lastError ? { skipped: snap.skipped + 1, lastError } : { skipped: snap.skipped + 1 });
326
+ };
224
327
  if (consentGone()) return;
225
- if (busy || !current()) {
328
+ if (busy && current()) {
329
+ owedGaps++;
226
330
  set({ skipped: snap.skipped + 1 });
227
331
  return;
228
332
  }
333
+ if (busy || !current()) {
334
+ gap();
335
+ return;
336
+ }
229
337
  if ((sock.bufferedAmount || 0) > MAX_BUFFERED) {
230
- set({ skipped: snap.skipped + 1 });
338
+ gap();
231
339
  return;
232
340
  }
233
341
  busy = true;
@@ -238,19 +346,52 @@ function createAiLiveProvider(overrides = {}) {
238
346
  const id = f?.handle?.id;
239
347
  const t = Number(f?.timestamp);
240
348
  if (!id || id === lastFrameId || !(deps.now() - t < FRESH_MS)) {
241
- set({ skipped: snap.skipped + 1 });
349
+ gap();
242
350
  return;
243
351
  }
244
- const b64 = await deps.jpegById(id, camera.maxPx, 0.7);
245
- if (!current() || consentGone()) return;
246
- const frame = encodeFrame({ seq: mySeq, tCapture: t, payload: base64ToBytes(b64) });
247
- sock.send(frame.buffer);
248
352
  lastFrameId = id;
249
- set({ sent: snap.sent + 1 });
353
+ let toSend = [{ id, t }];
354
+ const myGate = gate;
355
+ if (myGate) {
356
+ let sig = null;
357
+ try {
358
+ sig = await deps.signatureById(id);
359
+ } catch {
360
+ sig = null;
361
+ }
362
+ if (!current() || consentGone()) return;
363
+ toSend = myGate.offer({ t: t / 1e3, sig, item: { id, t } }).send;
364
+ if (!toSend.length) {
365
+ set({ heldBack: (snap.heldBack ?? 0) + 1 });
366
+ return;
367
+ }
368
+ }
369
+ for (const it of toSend) {
370
+ if (!current() || consentGone()) return;
371
+ if ((sock.bufferedAmount || 0) > MAX_BUFFERED) {
372
+ gap();
373
+ continue;
374
+ }
375
+ let b64;
376
+ try {
377
+ b64 = await deps.jpegById(it.id, camera.maxPx, 0.7);
378
+ } catch (e) {
379
+ if (g === gen) gap(String(e?.message || e));
380
+ continue;
381
+ }
382
+ if (!current() || consentGone()) return;
383
+ const frame = encodeFrame({ seq: seq++, tCapture: it.t, payload: base64ToBytes(b64) });
384
+ sock.send(frame.buffer);
385
+ set({ sent: snap.sent + 1 });
386
+ }
250
387
  } catch (e) {
251
- if (g === gen) set({ skipped: snap.skipped + 1, lastError: String(e?.message || e) });
388
+ if (g === gen) gap(String(e?.message || e));
252
389
  } finally {
253
- if (g === gen) busy = false;
390
+ if (g === gen) {
391
+ busy = false;
392
+ seq += owedGaps;
393
+ owedGaps = 0;
394
+ }
254
395
  }
255
396
  }
256
397
  function openSocket() {
@@ -295,6 +436,11 @@ function createAiLiveProvider(overrides = {}) {
295
436
  camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;
296
437
  return;
297
438
  }
439
+ if (ev.event === "motion" && !opened) {
440
+ const c = gateConfigFrom(ev.data);
441
+ gate = c.enabled ? createMotionGate({ ...c, preRollSec: Math.min(c.preRollSec, PHONE_MAX_PRE_ROLL_SEC) }) : null;
442
+ return;
443
+ }
298
444
  if (ev.event === "opened" && !opened) {
299
445
  opened = true;
300
446
  deps.clearTimeout(gaveUp);
@@ -401,8 +547,10 @@ function createAiLiveProvider(overrides = {}) {
401
547
  allowed = typeof ctx.allowed === "function" ? ctx.allowed : () => false;
402
548
  gen++;
403
549
  seq = 0;
550
+ owedGaps = 0;
404
551
  lastFrameId = null;
405
552
  busy = false;
553
+ gate = null;
406
554
  const g = gen;
407
555
  const cameraOfScreen = ctx.visual;
408
556
  snap = { ...idle(), camera: cam };
@@ -470,6 +618,7 @@ function createAiLiveProvider(overrides = {}) {
470
618
  CLOSE_WAIT_MS,
471
619
  LIVE_KIND,
472
620
  OPEN_TIMEOUT_MS,
621
+ PHONE_MAX_PRE_ROLL_SEC,
473
622
  createAiLiveProvider,
474
623
  defaultAiLiveDeps
475
624
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/aiLive/provider.ts","../../src/vision/index.ts","../../src/aiLive/streamProtocol.ts"],"sourcesContent":["/**\n * The AI live provider: this phone's camera → the workspace's live session → the chosen AI,\n * and what the AI saw back to the screen. Installed by the host once at startup:\n * import { createAiLiveProvider } from '@ateam-ai-mobile/expo/ai-live';\n * setAiLiveProvider(createAiLiveProvider());\n * A plugin reaches it only through props.aiLive,\n * whose gate (plugin-runtime aiLiveHost) has already checked the two-sided consent. The gate\n * passes that check as ctx.allowed, and it is asked again right before every picture: once the\n * person takes the yes back, nothing more is sent and the session stops (GPT review 2026-10-30).\n *\n * One camera, one sender: while one screen sends, another's start() is refused by name.\n *\n * ONE CONNECTION IS THE SESSION (owner direction 2026-10-30). start():\n * 1. opens Core's stream endpoint (/ws/stream, kind \"ai-models.live\", the login in the\n * subprotocol) asking for a session of its own (params.session \"own\"). That one round trip\n * IS starting the person's live session: Core starts it with its own $0.50 cap, refuses in\n * plain words when the person already has one running (never joins), and says how to send\n * ({\"event\":\"session\"}: the rate and size the setup was tested at) before \"opened\";\n * 2. turns on the screen's held camera on the requested side;\n * 3. sends one picture per slot at that rate and size, from the newest fresh frame, as a JPEG\n * made by native (jpegById). A slot with no fresh frame, or while the socket is still busy,\n * sends nothing and still takes its sequence number, so Core reports it as a gap;\n * 4. {\"event\":\"episode\"} from Core → the screen's onEpisode callbacks.\n * stop() closes the connection and the camera; Core ends the session when the connection ends,\n * whatever ended it (a stop, the screen closing, the app killed, the network lost).\n */\nimport { getCoreWsUrl, getTenant, getToken } from '@ateam-ai-mobile/core';\nimport { CONSENT_TAKEN_BACK } from '@ateam-ai-mobile/plugin-runtime';\nimport type { AiLiveEpisode, AiLiveProvider, AiLiveSnapshot, AiLiveStartOptions, DeviceState, Subscription } from '@ateam-ai-mobile/plugin-runtime';\nimport { jpegById as nativeJpegById } from '../vision';\nimport { CLOSE_MESSAGE, STREAM_PATH, SUBPROTOCOL, base64ToBytes, describeClose, encodeFrame, openMessage, parseServerEvent } from './streamProtocol';\n\nexport const LIVE_KIND = 'ai-models.live';\n/** A frame older than this is not \"what the camera sees now\" and is not sent. */\nconst FRESH_MS = 3000;\n/** Bytes still waiting in the socket above which a slot is skipped rather than queued. */\nconst MAX_BUFFERED = 512 * 1024;\nconst DEFAULT_CAMERA = { fps: 1, maxPx: 768 };\n\ntype Visual = DeviceState['visual'];\n\nexport interface AiLiveDeps {\n WebSocketImpl: any;\n wsBase(): string;\n token(): string | null;\n tenant(): string | null;\n jpegById(id: string, maxSide: number, quality: number): Promise<string>;\n now(): number;\n setInterval(fn: () => void, ms: number): any;\n clearInterval(handle: any): void;\n setTimeout(fn: () => void, ms: number): any;\n clearTimeout(handle: any): void;\n}\n\n/** Core answers \"opened\" in one round trip (it starts the session first). Past this, the start gives up\n * and closes the connection, so a start can never hang a screen's queue. */\nexport const OPEN_TIMEOUT_MS = 20_000;\n/** A stop waits this long, at most, for its connection to finish closing: Core ends the session when it\n * sees the close, so a restart right after a stop must not open the next one before (Codex on #53).\n * Past this the transport is taken as broken, and Core's own 60 s rule ends that session. */\nexport const CLOSE_WAIT_MS = 3_000;\n\nconst isEpisode = (d: any): d is AiLiveEpisode =>\n !!d && typeof d === 'object' && typeof d.type === 'string' && typeof d.class === 'string' &&\n typeof d.episodeId === 'string' && typeof d.actorId === 'string' && d.actorId.length > 0 && Number.isFinite(d.at);\n\nexport const defaultAiLiveDeps = (): AiLiveDeps => ({\n WebSocketImpl: (globalThis as any).WebSocket,\n wsBase: getCoreWsUrl,\n token: getToken,\n tenant: getTenant,\n jpegById: nativeJpegById,\n now: Date.now,\n setInterval: (fn, ms) => setInterval(fn, ms),\n clearInterval: (h) => clearInterval(h),\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n});\n\nconst idle = (): AiLiveSnapshot => ({ state: 'idle', camera: null, sent: 0, skipped: 0, lastEpisode: null, lastError: null });\n\nexport function createAiLiveProvider(overrides: Partial<AiLiveDeps> = {}): AiLiveProvider {\n const deps: AiLiveDeps = { ...defaultAiLiveDeps(), ...overrides };\n let owner: string | null = null; // the screen sending now\n let snap: AiLiveSnapshot = idle();\n let ws: any = null;\n let loop: any = null;\n let visual: Visual | null = null;\n let allowed: () => boolean = () => false; // the two-sided consent, asked before every picture\n let stopReason: string | null = null; // why the last teardown happened, known before it finishes\n // A teardown still finishing (the camera, the connection): a start waits for it, so a quick restart neither\n // reads the old session as its own nor is cleared by it afterwards (Codex on #53). Every teardown is\n // tracked, the ones the provider starts itself (consent gone, the connection dropped) included.\n let tearingDown: Promise<void> | null = null;\n // Screens whose start is waiting for that teardown, before they own anything: a stop meanwhile cancels\n // the start (it would otherwise go ahead after the screen had stopped or closed; Codex on #53).\n const waitingToStart = new Map<string, { cancelled: boolean }>();\n /** Each connection's \"it has closed\": a stop waits for it (CLOSE_WAIT_MS at most). */\n const closedOf = new WeakMap<object, Promise<void>>();\n let closingOnPurpose = false;\n let seq = 0;\n let busy = false;\n // Which session a slot belongs to. A slot awaits the camera and the JPEG maker; when the screen\n // stops (or another starts) meanwhile, that slot's picture must go nowhere, not into the next one.\n let gen = 0;\n let lastFrameId: string | null = null;\n let camera = DEFAULT_CAMERA;\n let lastOwner: string | null = null; // whose the current snapshot is (it outlives a stop)\n // Each screen hears only its own session: callbacks are kept per holder.\n const episodeCbs = new Map<string, Set<(ev: AiLiveEpisode) => void>>();\n const snapCbs = new Map<string, Set<(s: AiLiveSnapshot) => void>>();\n const sendingCbs = new Set<() => void>();\n let wasSending = false;\n const add = <T>(m: Map<string, Set<T>>, who: string, cb: T): Subscription => {\n let set = m.get(who);\n if (!set) { set = new Set(); m.set(who, set); }\n set.add(cb);\n return { unsubscribe: () => { set!.delete(cb); } };\n };\n\n const set = (patch: Partial<AiLiveSnapshot>) => {\n snap = { ...snap, ...patch };\n for (const cb of (lastOwner && snapCbs.get(lastOwner)) || []) { try { cb(snap); } catch { /* a listener's error is its own */ } }\n const sending = snap.state === 'starting' || snap.state === 'sending';\n if (sending !== wasSending) { wasSending = sending; for (const cb of sendingCbs) { try { cb(); } catch { /* idem */ } } }\n };\n\n function stopTimers() {\n if (loop != null) { deps.clearInterval(loop); loop = null; }\n }\n\n async function slot() {\n const mySeq = seq++; // every slot takes a number, sent or not\n const g = gen;\n const sock = ws;\n const current = () => g === gen && ws === sock && sock?.readyState === 1;\n const consentGone = () => { if (allowed()) return false; if (g === gen) void teardown('stopped', CONSENT_TAKEN_BACK); return true; };\n if (consentGone()) return;\n if (busy || !current()) { set({ skipped: snap.skipped + 1 }); return; }\n if ((sock.bufferedAmount || 0) > MAX_BUFFERED) { set({ skipped: snap.skipped + 1 }); return; }\n busy = true;\n try {\n const v = await visual!.getSnapshot();\n if (!current()) return;\n const f: any = v?.latest;\n const id = f?.handle?.id;\n const t = Number(f?.timestamp);\n if (!id || id === lastFrameId || !(deps.now() - t < FRESH_MS)) { set({ skipped: snap.skipped + 1 }); return; }\n const b64 = await deps.jpegById(id, camera.maxPx, 0.7);\n if (!current() || consentGone()) return;\n const frame = encodeFrame({ seq: mySeq, tCapture: t, payload: base64ToBytes(b64) });\n sock.send(frame.buffer);\n lastFrameId = id;\n set({ sent: snap.sent + 1 });\n } catch (e: any) {\n if (g === gen) set({ skipped: snap.skipped + 1, lastError: String(e?.message || e) });\n } finally {\n if (g === gen) busy = false;\n }\n }\n\n function openSocket(): Promise<void> {\n return new Promise((resolve, reject) => {\n const token = deps.token();\n const tenant = deps.tenant();\n if (!token || !tenant) { reject(new Error('not signed in to a workspace')); return; }\n const url = `${deps.wsBase()}${STREAM_PATH}?tenant=${encodeURIComponent(tenant)}`;\n let opened = false;\n const sock = new deps.WebSocketImpl(url, [SUBPROTOCOL, `adas.bearer.${token}`]);\n ws = sock;\n sock.binaryType = 'arraybuffer';\n let markClosed: () => void = () => {};\n closedOf.set(sock, new Promise<void>((r) => { markClosed = r; }));\n const gaveUp = deps.setTimeout(() => {\n if (opened) return;\n reject(new Error('Core did not answer in time. Try again.'));\n try { sock.close(); } catch { /* closing anyway */ }\n }, OPEN_TIMEOUT_MS);\n // Its own session: opening IS starting it, and Core ends it when this connection ends.\n sock.onopen = () => { try { sock.send(openMessage(LIVE_KIND, { session: 'own', source: 'phone' })); } catch (e) { reject(e); } };\n sock.onmessage = (m: any) => {\n // Only the current connection speaks for the current session. A message from a connection\n // already stopped or replaced (it can still be queued after a stop, a restart or a switch to\n // another screen) is dropped whole: its episode never reaches a newer screen, and its \"session\"\n // or \"opened\" never touches a newer connection (GPT code review v5).\n if (ws !== sock) return;\n const ev = parseServerEvent(m?.data);\n if (!ev) return;\n if (ev.event === 'session' && !opened) {\n const c = ev.data?.camera;\n camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;\n return;\n }\n if (ev.event === 'opened' && !opened) { opened = true; deps.clearTimeout(gaveUp); resolve(); return; }\n // Only a whole episode reaches the screen: what happened, of which class, and whose camera saw it\n // (actorId: live sessions are per person). Anything less is dropped, never passed on half-made.\n if (ev.event === 'episode' && isEpisode(ev.data)) {\n const episode = ev.data;\n set({ lastEpisode: episode });\n for (const cb of (owner && episodeCbs.get(owner)) || []) { try { cb(episode); } catch { /* a listener's error is its own */ } }\n }\n };\n sock.onerror = () => { /* the close that follows says why */ };\n sock.onclose = (c: any) => {\n markClosed();\n const why = describeClose(Number(c?.code), c?.reason);\n if (!opened) { deps.clearTimeout(gaveUp); reject(new Error(why)); return; }\n if (closingOnPurpose || ws !== sock) return;\n void teardown('error', why);\n };\n });\n }\n\n function teardown(state: 'stopped' | 'error', lastError: string | null): Promise<void> {\n // One at a time: a second stop while one is finishing joins it (Codex on #53). Two at once could\n // finish out of order and the first, resuming late, would stop a session started in between.\n if (tearingDown) return tearingDown;\n const p = teardownNow(state, lastError);\n tearingDown = p;\n void p.then(() => {}, () => {}).then(() => { if (tearingDown === p) tearingDown = null; });\n return p;\n }\n\n // Closing the connection ends the session (Core); the camera is this phone's to stop.\n async function teardownNow(state: 'stopped' | 'error', lastError: string | null) {\n stopReason = lastError;\n allowed = () => false;\n gen++;\n stopTimers();\n const sock = ws;\n ws = null;\n closingOnPurpose = true;\n try { if (sock && sock.readyState === 1) sock.send(CLOSE_MESSAGE); } catch { /* closing anyway */ }\n try { sock?.close?.(); } catch { /* already closed */ }\n closingOnPurpose = false;\n const v = visual;\n visual = null;\n // The camera off, and the connection really closed (so Core has ended its session) before this stop\n // is done: a start right after it then never meets the old session (Codex on #53).\n let gaveUp: any = null;\n const closed = sock && closedOf.get(sock);\n await Promise.all([\n (async () => { try { await v?.stop?.(); } catch { /* the camera hold is released by the gate too */ } })(),\n closed ? Promise.race([closed, new Promise<void>((r) => { gaveUp = deps.setTimeout(r, CLOSE_WAIT_MS); })]) : Promise.resolve(),\n ]);\n if (gaveUp != null) deps.clearTimeout(gaveUp);\n owner = null;\n set({ state, lastError });\n }\n\n return {\n async start(opts: AiLiveStartOptions, who: string, ctx: { visual: Visual; allowed: () => boolean }) {\n if (tearingDown) {\n const wait = { cancelled: false };\n waitingToStart.set(who, wait);\n try {\n while (tearingDown) { try { await tearingDown; } catch { /* it stopped either way */ } }\n } finally {\n if (waitingToStart.get(who) === wait) waitingToStart.delete(who);\n }\n if (wait.cancelled) throw Object.assign(new Error('aiLive: stopped before it started.'), { code: 'STOPPED' });\n }\n if (owner && owner !== who) throw Object.assign(new Error('aiLive: another screen is sending to AI right now. Close it first.'), { code: 'CONFLICT' });\n if (owner === who) return snap;\n const cam = opts?.camera === 'front' ? 'front' : 'back';\n if (opts?.camera && opts.camera !== 'front' && opts.camera !== 'back') throw new Error(`aiLive: unknown camera \"${String(opts.camera)}\" — use \"front\" or \"back\"`);\n owner = who;\n lastOwner = who;\n visual = ctx.visual;\n allowed = typeof ctx.allowed === 'function' ? ctx.allowed : () => false;\n gen++; seq = 0; lastFrameId = null; busy = false;\n const g = gen;\n const cameraOfScreen = ctx.visual;\n snap = { ...idle(), camera: cam };\n set({ state: 'starting' });\n // Starting takes several awaits, and the screen may be stopped, or the yes taken back,\n // during any of them (Codex P1 on #53). After each: a stop already tore it down, so say why\n // and go no further; a yes taken back unannounced tears it down here.\n const stillStarting = async () => {\n if (g === gen && !allowed()) await teardown('stopped', CONSENT_TAKEN_BACK);\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n };\n try {\n await stillStarting();\n // One round trip: the connection starts the session (or Core refuses, in its own words), and\n // says how to send. Before the camera, so a refusal never turns the camera on.\n camera = DEFAULT_CAMERA;\n // Checked again with nothing in between: a stop that landed while the check above resolved has\n // already torn this start down, and opening now would start a session nobody closes (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n await openSocket();\n await stillStarting();\n await cameraOfScreen.start({ intent: 'stream', facing: cam } as any);\n if (g !== gen) { try { await cameraOfScreen.stop?.(); } catch { /* released by the gate too */ } }\n await stillStarting();\n // Again with nothing in between: a stop that landed while the last check resolved must not see\n // pictures start or the state flip back to \"sending\" (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n loop = deps.setInterval(() => { void slot(); }, Math.max(200, Math.round(1000 / camera.fps)));\n set({ state: 'sending' });\n return snap;\n } catch (e: any) {\n if (g !== gen) {\n // Already torn down by a stop: report why it stopped, not the closed socket.\n throw e?.code === 'STOPPED' ? e : Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n }\n await teardown('error', String(e?.message || e));\n throw e;\n }\n },\n async stop(who: string, why?: string) {\n const waiting = waitingToStart.get(who);\n if (waiting) waiting.cancelled = true;\n if (owner !== who) return;\n await teardown('stopped', why ?? null);\n },\n getSnapshot: (who: string) => (lastOwner === who ? snap : idle()),\n onEpisode: (cb, who) => add(episodeCbs, who, cb),\n subscribe: (cb, who) => add(snapCbs, who, cb),\n forget(who: string) {\n episodeCbs.delete(who);\n snapCbs.delete(who);\n if (lastOwner === who && owner === null) { lastOwner = null; snap = idle(); }\n },\n isSending: () => snap.state === 'starting' || snap.state === 'sending',\n subscribeSending(cb) {\n sendingCbs.add(cb);\n return { unsubscribe: () => { sendingCbs.delete(cb); } } as Subscription;\n },\n };\n}\n","// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n /** Absent from binaries built before the front camera existed. */\n startVisionCamera?(intervalMs: number, facing: VisionFacing): Promise<void>;\n /** Absent from binaries built before AI live existed. A base64 JPEG. */\n jpegById?(id: string, maxSide: number, quality: number): Promise<string>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** The native function, or a refusal by name: Android's AdaVision has assessQuality only\n * (AI Models F4), so a verb it lacks says so instead of failing as \"not a function\". */\nfunction verb<K extends keyof AdaVisionNative>(name: K): AdaVisionNative[K] {\n if (!native) throw new Error('ada-vision native module not present in this build');\n const fn = native[name];\n if (typeof fn !== 'function') {\n throw Object.assign(new Error(`UNAVAILABLE: ada-vision ${String(name)} is not implemented on this platform (iOS only)`), { code: 'UNAVAILABLE' });\n }\n return (fn as Function).bind(native) as AdaVisionNative[K];\n}\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Which camera the vision session reads. */\nexport type VisionFacing = 'front' | 'back';\n\n/**\n * Start the vision session on `facing` — publishes a timeline frame every\n * `intervalMs`. The back camera works on every binary; the front camera needs a\n * binary built with it, and an older one REFUSES it rather than quietly reading\n * the back camera instead.\n */\nexport async function startVision(intervalMs = 1000, facing: VisionFacing = 'back'): Promise<void> {\n if (facing !== 'front' && facing !== 'back') {\n throw new Error(`Unknown camera \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n if (facing === 'back') return verb('startVision')(intervalMs);\n if (native && typeof native.startVisionCamera !== 'function') {\n throw new Error('the front camera needs a newer app build — this one can read the back camera only');\n }\n return verb('startVisionCamera')!(intervalMs, facing);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n return verb('readText')(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n return verb('describe')(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n return verb('detectObjects')(id, maxItems, minConfidence);\n}\n\n/**\n * A published frame as a base64 JPEG, its longer side at most `maxSide`. FOR THE SDK'S AI LIVE SENDER ONLY\n * (aiLive/provider.ts), which calls it after the two-sided \"Pictures to AI\" consent: this is the one way a\n * picture leaves native, and no plugin-facing API reaches it. An older binary refuses by name.\n */\nexport async function jpegById(id: string, maxSide = 768, quality = 0.7): Promise<string> {\n if (native && typeof native.jpegById !== 'function') {\n throw new Error('sending pictures to AI needs a newer app build — this one cannot make a picture from a camera frame');\n }\n return verb('jpegById')!(id, maxSide, quality);\n}\n","/**\n * The wire protocol of Core's binary stream endpoint (/ws/stream), for the AI live sender.\n *\n * A PORT of Core's `packages/media-stream/src/index.js` (@ateam-ai/media-stream, not on npm\n * yet), limited to what a sender needs. The frame bytes are pinned by\n * tests/aiLiveStreamProtocol.test.mjs against the layout that file documents, so a drift is\n * a red test, not a silent mismatch. DEAD — replace with an import of @ateam-ai/media-stream\n * once Core publishes it.\n *\n * Frame envelope (bytes): 0 version (1) · 1 flags · 2-5 seq uint32 BE (one per sampling slot\n * of the SENDER, taken or skipped) · 6-13 tCapture float64 BE (ms since the epoch) · 14- payload.\n */\nexport const STREAM_PATH = '/ws/stream';\nexport const SUBPROTOCOL = 'adas.v1';\nexport const HEADER_BYTES = 14;\nconst ENVELOPE_VERSION = 1;\n\nexport const CLOSE = Object.freeze({\n AUTH_MISSING: 4001,\n AUTH_FAILED: 4002,\n NO_SUCH_WORKSPACE: 4003,\n NOT_ENTITLED: 4004,\n BAD_REQUEST: 4400,\n UNKNOWN_KIND: 4404,\n TIMEOUT: 4408,\n OVER_LIMIT: 4429,\n TOO_BIG: 1009,\n CONSUMER_FAILING: 1011,\n});\n\nexport function encodeFrame({ seq, tCapture, payload }: { seq: number; tCapture: number; payload: Uint8Array }): Uint8Array {\n const out = new Uint8Array(HEADER_BYTES + payload.length);\n const v = new DataView(out.buffer);\n v.setUint8(0, ENVELOPE_VERSION);\n v.setUint8(1, 0);\n v.setUint32(2, seq >>> 0, false);\n v.setFloat64(6, tCapture, false);\n out.set(payload, HEADER_BYTES);\n return out;\n}\n\nexport function openMessage(kind: string, params?: Record<string, unknown>): string {\n return JSON.stringify(params ? { type: 'open', kind, params } : { type: 'open', kind });\n}\n\nexport const CLOSE_MESSAGE = JSON.stringify({ type: 'close' });\n\n/** A server text frame → { event, data }, or null for anything that is not an event. */\nexport function parseServerEvent(text: unknown): { event: string; data: any } | null {\n if (typeof text !== 'string') return null;\n try {\n const m = JSON.parse(text);\n return m && typeof m.event === 'string' ? { event: m.event, data: m.data } : null;\n } catch {\n return null;\n }\n}\n\nconst WHY: Record<number, string> = {\n [CLOSE.AUTH_MISSING]: 'The login or the workspace was missing. Sign in again and try once more.',\n [CLOSE.AUTH_FAILED]: 'The login was refused. Sign in again and try once more.',\n [CLOSE.NO_SUCH_WORKSPACE]: 'This workspace was not found.',\n [CLOSE.NOT_ENTITLED]: 'This workspace may not stream right now.',\n [CLOSE.TIMEOUT]: 'The connection to the AI timed out.',\n [CLOSE.OVER_LIMIT]: 'Too many cameras are sending to this workspace. Stop another one first.',\n [CLOSE.TOO_BIG]: 'A picture was too big to send.',\n [CLOSE.CONSUMER_FAILING]: 'The AI stopped taking pictures. Check the live session in AI Models.',\n};\n\n/** A close as one plain sentence: the server's own words for a refused open. */\nexport function describeClose(code: number, reason?: string): string {\n if (code === CLOSE.BAD_REQUEST && reason) return reason;\n return WHY[code] || (reason ? reason : `The connection to the AI closed (${code}).`);\n}\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => { const t = new Int16Array(256).fill(-1); for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i; return t; })();\n\n/** Base64 (the native module's JPEG) → bytes. Ignores padding and line breaks. */\nexport function base64ToBytes(b64: string): Uint8Array {\n const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let o = 0, acc = 0, bits = 0;\n for (let i = 0; i < clean.length; i++) {\n acc = (acc << 6) | B64_INDEX[clean.charCodeAt(i)];\n bits += 6;\n if (bits >= 8) { bits -= 8; out[o++] = (acc >> bits) & 0xff; }\n }\n return o === out.length ? out : out.subarray(0, o);\n}\n"],"mappings":"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","names":["set"]}
1
+ {"version":3,"sources":["../../src/aiLive/provider.ts","../../src/vision/index.ts","../../src/aiLive/streamProtocol.ts","../../src/aiLive/motionGate.ts","../../src/aiLive/signature.ts"],"sourcesContent":["/**\n * The AI live provider: this phone's camera → the workspace's live session → the chosen AI,\n * and what the AI saw back to the screen. Installed by the host once at startup:\n * import { createAiLiveProvider } from '@ateam-ai-mobile/expo/ai-live';\n * setAiLiveProvider(createAiLiveProvider());\n * A plugin reaches it only through props.aiLive,\n * whose gate (plugin-runtime aiLiveHost) has already checked the two-sided consent. The gate\n * passes that check as ctx.allowed, and it is asked again right before every picture: once the\n * person takes the yes back, nothing more is sent and the session stops (GPT review 2026-10-30).\n *\n * One camera, one sender: while one screen sends, another's start() is refused by name.\n *\n * ONE CONNECTION IS THE SESSION (owner direction 2026-10-30). start():\n * 1. opens Core's stream endpoint (/ws/stream, kind \"ai-models.live\", the login in the\n * subprotocol) asking for a session of its own (params.session \"own\"). That one round trip\n * IS starting the person's live session: Core starts it with its own $0.50 cap, refuses in\n * plain words when the person already has one running (never joins), and says how to send\n * ({\"event\":\"session\"}: the rate and size the setup was tested at) before \"opened\";\n * 2. turns on the screen's held camera on the requested side;\n * 3. sends one picture per slot at that rate and size, from the newest fresh frame, as a JPEG\n * made by native (jpegById). A slot with no fresh frame, or while the socket is still busy,\n * sends nothing and still takes its sequence number, so Core reports it as a gap;\n * 4. {\"event\":\"episode\"} from Core → the screen's onEpisode callbacks.\n * THE MOTION GATE (owner 2026-11-01, GPT review 2026-11-01): when Core sends {\"event\":\"motion\"} (the workspace's\n * Live tab settings) before \"opened\", each slot first takes the frame's signature (a tiny JPEG decoded here,\n * signature.ts) and the gate (motionGate.ts, the same rule as the computer's camera window) decides: while\n * nothing changes the picture is held back, takes NO sequence number (it is not lost, Core must not count a\n * gap) and costs nothing; one goes anyway every heartbeat so the AI still looks; on a change the held last\n * second goes first, then the change, then one more second after it. Core that sends no \"motion\" gets every\n * picture, as before.\n * stop() closes the connection and the camera; Core ends the session when the connection ends,\n * whatever ended it (a stop, the screen closing, the app killed, the network lost).\n */\nimport { getCoreWsUrl, getTenant, getToken } from '@ateam-ai-mobile/core';\nimport { CONSENT_TAKEN_BACK } from '@ateam-ai-mobile/plugin-runtime';\nimport type { AiLiveEpisode, AiLiveProvider, AiLiveSnapshot, AiLiveStartOptions, DeviceState, Subscription } from '@ateam-ai-mobile/plugin-runtime';\nimport { jpegById as nativeJpegById } from '../vision';\nimport { CLOSE_MESSAGE, STREAM_PATH, SUBPROTOCOL, base64ToBytes, describeClose, encodeFrame, openMessage, parseServerEvent } from './streamProtocol';\nimport { createMotionGate, gateConfigFrom } from './motionGate';\n\n// The gate holds frame ids, not pictures: a held id must still resolve in the native ring when it is sent. The ring\n// keeps 12 frames (AdaVisionModule.swift ringCap), 6 s at the fastest stream rate (500 ms), so a longer pre-roll from\n// Core would send only its tail. 5 s stays inside it with margin.\nexport const PHONE_MAX_PRE_ROLL_SEC = 5;\nimport { SIG_JPEG_SIDE, signatureFromJpeg } from './signature';\n\nexport const LIVE_KIND = 'ai-models.live';\n/** A frame older than this is not \"what the camera sees now\" and is not sent. */\nconst FRESH_MS = 3000;\n/** Bytes still waiting in the socket above which a slot is skipped rather than queued. */\nconst MAX_BUFFERED = 512 * 1024;\nconst DEFAULT_CAMERA = { fps: 1, maxPx: 768 };\n\ntype Visual = DeviceState['visual'];\n\nexport interface AiLiveDeps {\n WebSocketImpl: any;\n wsBase(): string;\n token(): string | null;\n tenant(): string | null;\n jpegById(id: string, maxSide: number, quality: number): Promise<string>;\n /** The frame's signature for the motion gate (signature.ts); null = could not take it (the picture is then sent). */\n signatureById(id: string): Promise<ArrayLike<number> | null>;\n now(): number;\n setInterval(fn: () => void, ms: number): any;\n clearInterval(handle: any): void;\n setTimeout(fn: () => void, ms: number): any;\n clearTimeout(handle: any): void;\n}\n\n/** Core answers \"opened\" in one round trip (it starts the session first). Past this, the start gives up\n * and closes the connection, so a start can never hang a screen's queue. */\nexport const OPEN_TIMEOUT_MS = 20_000;\n/** A stop waits this long, at most, for its connection to finish closing: Core ends the session when it\n * sees the close, so a restart right after a stop must not open the next one before (Codex on #53).\n * Past this the transport is taken as broken, and Core's own 60 s rule ends that session. */\nexport const CLOSE_WAIT_MS = 3_000;\n\nconst isEpisode = (d: any): d is AiLiveEpisode =>\n !!d && typeof d === 'object' && typeof d.type === 'string' && typeof d.class === 'string' &&\n typeof d.episodeId === 'string' && typeof d.actorId === 'string' && d.actorId.length > 0 && Number.isFinite(d.at);\n\nexport const defaultAiLiveDeps = (): AiLiveDeps => ({\n WebSocketImpl: (globalThis as any).WebSocket,\n wsBase: getCoreWsUrl,\n token: getToken,\n tenant: getTenant,\n jpegById: nativeJpegById,\n signatureById: async (id) => signatureFromJpeg(await nativeJpegById(id, SIG_JPEG_SIDE, 0.6)),\n now: Date.now,\n setInterval: (fn, ms) => setInterval(fn, ms),\n clearInterval: (h) => clearInterval(h),\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (h) => clearTimeout(h),\n});\n\nconst idle = (): AiLiveSnapshot => ({ state: 'idle', camera: null, sent: 0, skipped: 0, heldBack: 0, lastEpisode: null, lastError: null });\ntype GateItem = { id: string; t: number };\n\nexport function createAiLiveProvider(overrides: Partial<AiLiveDeps> = {}): AiLiveProvider {\n const deps: AiLiveDeps = { ...defaultAiLiveDeps(), ...overrides };\n let owner: string | null = null; // the screen sending now\n let snap: AiLiveSnapshot = idle();\n let ws: any = null;\n let loop: any = null;\n let visual: Visual | null = null;\n let allowed: () => boolean = () => false; // the two-sided consent, asked before every picture\n let stopReason: string | null = null; // why the last teardown happened, known before it finishes\n // A teardown still finishing (the camera, the connection): a start waits for it, so a quick restart neither\n // reads the old session as its own nor is cleared by it afterwards (Codex on #53). Every teardown is\n // tracked, the ones the provider starts itself (consent gone, the connection dropped) included.\n let tearingDown: Promise<void> | null = null;\n // Screens whose start is waiting for that teardown, before they own anything: a stop meanwhile cancels\n // the start (it would otherwise go ahead after the screen had stopped or closed; Codex on #53).\n const waitingToStart = new Map<string, { cancelled: boolean }>();\n /** Each connection's \"it has closed\": a stop waits for it (CLOSE_WAIT_MS at most). */\n const closedOf = new WeakMap<object, Promise<void>>();\n let closingOnPurpose = false;\n let seq = 0;\n let busy = false;\n let owedGaps = 0; // slots skipped while one was still working: numbered when it ends\n // Which session a slot belongs to. A slot awaits the camera and the JPEG maker; when the screen\n // stops (or another starts) meanwhile, that slot's picture must go nowhere, not into the next one.\n let gen = 0;\n let lastFrameId: string | null = null;\n let camera = DEFAULT_CAMERA;\n let gate: ReturnType<typeof createMotionGate<GateItem>> | null = null; // this session's motion gate, if Core sent one\n let lastOwner: string | null = null; // whose the current snapshot is (it outlives a stop)\n // Each screen hears only its own session: callbacks are kept per holder.\n const episodeCbs = new Map<string, Set<(ev: AiLiveEpisode) => void>>();\n const snapCbs = new Map<string, Set<(s: AiLiveSnapshot) => void>>();\n const sendingCbs = new Set<() => void>();\n let wasSending = false;\n const add = <T>(m: Map<string, Set<T>>, who: string, cb: T): Subscription => {\n let set = m.get(who);\n if (!set) { set = new Set(); m.set(who, set); }\n set.add(cb);\n return { unsubscribe: () => { set!.delete(cb); } };\n };\n\n const set = (patch: Partial<AiLiveSnapshot>) => {\n snap = { ...snap, ...patch };\n for (const cb of (lastOwner && snapCbs.get(lastOwner)) || []) { try { cb(snap); } catch { /* a listener's error is its own */ } }\n const sending = snap.state === 'starting' || snap.state === 'sending';\n if (sending !== wasSending) { wasSending = sending; for (const cb of sendingCbs) { try { cb(); } catch { /* idem */ } } }\n };\n\n function stopTimers() {\n if (loop != null) { deps.clearInterval(loop); loop = null; }\n }\n\n async function slot() {\n const g = gen;\n const sock = ws;\n const current = () => g === gen && ws === sock && sock?.readyState === 1;\n const consentGone = () => { if (allowed()) return false; if (g === gen) void teardown('stopped', CONSENT_TAKEN_BACK); return true; };\n // Every slot that does not send takes a number, so Core sees the gap; a picture the motion gate holds back\n // takes none (it was looked at, nothing changed: not lost).\n const gap = (lastError?: string) => { seq++; set(lastError ? { skipped: snap.skipped + 1, lastError } : { skipped: snap.skipped + 1 }); };\n if (consentGone()) return;\n // A slot that finds the previous one still working is a gap AFTER that one's pictures: its number is taken\n // when the running slot finishes, so numbers stay in capture order even when encoding is slower than the timer.\n if (busy && current()) { owedGaps++; set({ skipped: snap.skipped + 1 }); return; }\n if (busy || !current()) { gap(); return; }\n if ((sock.bufferedAmount || 0) > MAX_BUFFERED) { gap(); return; }\n busy = true;\n try {\n const v = await visual!.getSnapshot();\n if (!current()) return;\n const f: any = v?.latest;\n const id = f?.handle?.id;\n const t = Number(f?.timestamp);\n if (!id || id === lastFrameId || !(deps.now() - t < FRESH_MS)) { gap(); return; }\n lastFrameId = id;\n let toSend: GateItem[] = [{ id, t }];\n const myGate = gate;\n if (myGate) {\n let sig: ArrayLike<number> | null = null;\n try { sig = await deps.signatureById(id); } catch { sig = null; } // no signature: counts as a change, sent\n if (!current() || consentGone()) return;\n toSend = myGate.offer({ t: t / 1000, sig, item: { id, t } }).send;\n if (!toSend.length) { set({ heldBack: (snap.heldBack ?? 0) + 1 }); return; }\n }\n // Oldest first: the held last second (on a change), then this picture.\n for (const it of toSend) {\n if (!current() || consentGone()) return;\n if ((sock.bufferedAmount || 0) > MAX_BUFFERED) { gap(); continue; }\n let b64: string;\n try { b64 = await deps.jpegById(it.id, camera.maxPx, 0.7); } catch (e: any) { if (g === gen) gap(String(e?.message || e)); continue; }\n if (!current() || consentGone()) return;\n const frame = encodeFrame({ seq: seq++, tCapture: it.t, payload: base64ToBytes(b64) });\n sock.send(frame.buffer);\n set({ sent: snap.sent + 1 });\n }\n } catch (e: any) {\n if (g === gen) gap(String(e?.message || e));\n } finally {\n if (g === gen) { busy = false; seq += owedGaps; owedGaps = 0; }\n }\n }\n\n function openSocket(): Promise<void> {\n return new Promise((resolve, reject) => {\n const token = deps.token();\n const tenant = deps.tenant();\n if (!token || !tenant) { reject(new Error('not signed in to a workspace')); return; }\n const url = `${deps.wsBase()}${STREAM_PATH}?tenant=${encodeURIComponent(tenant)}`;\n let opened = false;\n const sock = new deps.WebSocketImpl(url, [SUBPROTOCOL, `adas.bearer.${token}`]);\n ws = sock;\n sock.binaryType = 'arraybuffer';\n let markClosed: () => void = () => {};\n closedOf.set(sock, new Promise<void>((r) => { markClosed = r; }));\n const gaveUp = deps.setTimeout(() => {\n if (opened) return;\n reject(new Error('Core did not answer in time. Try again.'));\n try { sock.close(); } catch { /* closing anyway */ }\n }, OPEN_TIMEOUT_MS);\n // Its own session: opening IS starting it, and Core ends it when this connection ends.\n sock.onopen = () => { try { sock.send(openMessage(LIVE_KIND, { session: 'own', source: 'phone' })); } catch (e) { reject(e); } };\n sock.onmessage = (m: any) => {\n // Only the current connection speaks for the current session. A message from a connection\n // already stopped or replaced (it can still be queued after a stop, a restart or a switch to\n // another screen) is dropped whole: its episode never reaches a newer screen, and its \"session\"\n // or \"opened\" never touches a newer connection (GPT code review v5).\n if (ws !== sock) return;\n const ev = parseServerEvent(m?.data);\n if (!ev) return;\n if (ev.event === 'session' && !opened) {\n const c = ev.data?.camera;\n camera = c && Number(c.fps) > 0 && Number(c.maxPx) > 0 ? { fps: Number(c.fps), maxPx: Number(c.maxPx) } : DEFAULT_CAMERA;\n return;\n }\n if (ev.event === 'motion' && !opened) { const c = gateConfigFrom(ev.data); gate = c.enabled ? createMotionGate<GateItem>({ ...c, preRollSec: Math.min(c.preRollSec, PHONE_MAX_PRE_ROLL_SEC) }) : null; return; }\n if (ev.event === 'opened' && !opened) { opened = true; deps.clearTimeout(gaveUp); resolve(); return; }\n // Only a whole episode reaches the screen: what happened, of which class, and whose camera saw it\n // (actorId: live sessions are per person). Anything less is dropped, never passed on half-made.\n if (ev.event === 'episode' && isEpisode(ev.data)) {\n const episode = ev.data;\n set({ lastEpisode: episode });\n for (const cb of (owner && episodeCbs.get(owner)) || []) { try { cb(episode); } catch { /* a listener's error is its own */ } }\n }\n };\n sock.onerror = () => { /* the close that follows says why */ };\n sock.onclose = (c: any) => {\n markClosed();\n const why = describeClose(Number(c?.code), c?.reason);\n if (!opened) { deps.clearTimeout(gaveUp); reject(new Error(why)); return; }\n if (closingOnPurpose || ws !== sock) return;\n void teardown('error', why);\n };\n });\n }\n\n function teardown(state: 'stopped' | 'error', lastError: string | null): Promise<void> {\n // One at a time: a second stop while one is finishing joins it (Codex on #53). Two at once could\n // finish out of order and the first, resuming late, would stop a session started in between.\n if (tearingDown) return tearingDown;\n const p = teardownNow(state, lastError);\n tearingDown = p;\n void p.then(() => {}, () => {}).then(() => { if (tearingDown === p) tearingDown = null; });\n return p;\n }\n\n // Closing the connection ends the session (Core); the camera is this phone's to stop.\n async function teardownNow(state: 'stopped' | 'error', lastError: string | null) {\n stopReason = lastError;\n allowed = () => false;\n gen++;\n stopTimers();\n const sock = ws;\n ws = null;\n closingOnPurpose = true;\n try { if (sock && sock.readyState === 1) sock.send(CLOSE_MESSAGE); } catch { /* closing anyway */ }\n try { sock?.close?.(); } catch { /* already closed */ }\n closingOnPurpose = false;\n const v = visual;\n visual = null;\n // The camera off, and the connection really closed (so Core has ended its session) before this stop\n // is done: a start right after it then never meets the old session (Codex on #53).\n let gaveUp: any = null;\n const closed = sock && closedOf.get(sock);\n await Promise.all([\n (async () => { try { await v?.stop?.(); } catch { /* the camera hold is released by the gate too */ } })(),\n closed ? Promise.race([closed, new Promise<void>((r) => { gaveUp = deps.setTimeout(r, CLOSE_WAIT_MS); })]) : Promise.resolve(),\n ]);\n if (gaveUp != null) deps.clearTimeout(gaveUp);\n owner = null;\n set({ state, lastError });\n }\n\n return {\n async start(opts: AiLiveStartOptions, who: string, ctx: { visual: Visual; allowed: () => boolean }) {\n if (tearingDown) {\n const wait = { cancelled: false };\n waitingToStart.set(who, wait);\n try {\n while (tearingDown) { try { await tearingDown; } catch { /* it stopped either way */ } }\n } finally {\n if (waitingToStart.get(who) === wait) waitingToStart.delete(who);\n }\n if (wait.cancelled) throw Object.assign(new Error('aiLive: stopped before it started.'), { code: 'STOPPED' });\n }\n if (owner && owner !== who) throw Object.assign(new Error('aiLive: another screen is sending to AI right now. Close it first.'), { code: 'CONFLICT' });\n if (owner === who) return snap;\n const cam = opts?.camera === 'front' ? 'front' : 'back';\n if (opts?.camera && opts.camera !== 'front' && opts.camera !== 'back') throw new Error(`aiLive: unknown camera \"${String(opts.camera)}\" — use \"front\" or \"back\"`);\n owner = who;\n lastOwner = who;\n visual = ctx.visual;\n allowed = typeof ctx.allowed === 'function' ? ctx.allowed : () => false;\n gen++; seq = 0; owedGaps = 0; lastFrameId = null; busy = false; gate = null;\n const g = gen;\n const cameraOfScreen = ctx.visual;\n snap = { ...idle(), camera: cam };\n set({ state: 'starting' });\n // Starting takes several awaits, and the screen may be stopped, or the yes taken back,\n // during any of them (Codex P1 on #53). After each: a stop already tore it down, so say why\n // and go no further; a yes taken back unannounced tears it down here.\n const stillStarting = async () => {\n if (g === gen && !allowed()) await teardown('stopped', CONSENT_TAKEN_BACK);\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n };\n try {\n await stillStarting();\n // One round trip: the connection starts the session (or Core refuses, in its own words), and\n // says how to send. Before the camera, so a refusal never turns the camera on.\n camera = DEFAULT_CAMERA;\n // Checked again with nothing in between: a stop that landed while the check above resolved has\n // already torn this start down, and opening now would start a session nobody closes (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n await openSocket();\n await stillStarting();\n await cameraOfScreen.start({ intent: 'stream', facing: cam } as any);\n if (g !== gen) { try { await cameraOfScreen.stop?.(); } catch { /* released by the gate too */ } }\n await stillStarting();\n // Again with nothing in between: a stop that landed while the last check resolved must not see\n // pictures start or the state flip back to \"sending\" (Codex on #53).\n if (g !== gen) throw Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n loop = deps.setInterval(() => { void slot(); }, Math.max(200, Math.round(1000 / camera.fps)));\n set({ state: 'sending' });\n return snap;\n } catch (e: any) {\n if (g !== gen) {\n // Already torn down by a stop: report why it stopped, not the closed socket.\n throw e?.code === 'STOPPED' ? e : Object.assign(new Error(stopReason || 'aiLive: stopped before it started sending.'), { code: 'STOPPED' });\n }\n await teardown('error', String(e?.message || e));\n throw e;\n }\n },\n async stop(who: string, why?: string) {\n const waiting = waitingToStart.get(who);\n if (waiting) waiting.cancelled = true;\n if (owner !== who) return;\n await teardown('stopped', why ?? null);\n },\n getSnapshot: (who: string) => (lastOwner === who ? snap : idle()),\n onEpisode: (cb, who) => add(episodeCbs, who, cb),\n subscribe: (cb, who) => add(snapCbs, who, cb),\n forget(who: string) {\n episodeCbs.delete(who);\n snapCbs.delete(who);\n if (lastOwner === who && owner === null) { lastOwner = null; snap = idle(); }\n },\n isSending: () => snap.state === 'starting' || snap.state === 'sending',\n subscribeSending(cb) {\n sendingCbs.add(cb);\n return { unsubscribe: () => { sendingCbs.delete(cb); } } as Subscription;\n },\n };\n}\n","// ada-vision — on-device image understanding, our own native code (no camera lib).\n//\n// On-device, frames never leave the phone:\n// assessQuality(uri) — one-shot quality of a LOCAL frame file.\n// startVision()/onVisionFrame — the headless camera session that publishes a\n// timeline frame (id + quality, never pixels) at a\n// cadence; readText/describe/detectObjects resolve\n// a published id ON-DEVICE.\n//\n// The luma math mirrors ./analysis.js (which has the exhaustive unit tests).\n//\n// requireOptionalNativeModule: on a binary WITHOUT the native module, the import\n// is null, isSupported() is false, and the session calls refuse cleanly (no crash).\n\nimport type { ComponentType } from 'react';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\nimport { requireNativeView } from 'expo';\n\n/** TEST-ONLY live preview bound to the same headless session (so a tester can\n * aim). The real Guide runs headless and never mounts this. Null on a binary\n * without the native view, so importing this module never crashes. */\nexport const CameraPreview: ComponentType<any> | null = (() => {\n try { return requireNativeView('AdaVision'); } catch { return null; }\n})();\n\nexport interface RawFrameQuality {\n blur: 'sharp' | 'slight' | 'severe' | 'unknown';\n exposure: 'good' | 'dark' | 'bright' | 'unknown';\n /** true = lens covered; false = clearly not; null = not assessed. */\n obstructed: boolean | null;\n}\n\ninterface Subscription { remove(): void }\n\n/** A published timeline frame from the vision session (metadata only — never\n * pixels; the buffer stays in the native ring, resolved by id). */\nexport interface VisionFrame {\n id: string;\n ts: number;\n expiresAt: number;\n blur: RawFrameQuality['blur'];\n exposure: RawFrameQuality['exposure'];\n obstructed: RawFrameQuality['obstructed'];\n}\n\n/** One OCR result on a frame — matches the SDK's VisionText. */\nexport interface VisionText {\n text: string;\n /** 0..1 recogniser confidence (never a 1.0 filler). */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\n/** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */\nexport interface VisionLabel {\n label: string;\n confidence: number | null;\n}\n\n/** One detected object — a located item with a box, from the bundled detector. */\nexport interface VisionObject {\n /** Open-Images class name (e.g. \"Car\", \"Traffic light\", \"Building\", \"Person\"). */\n label: string;\n /** 0..1 detector confidence. */\n confidence: number | null;\n /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native\n * rotates frames at capture) — x runs from the user's left to their right. */\n box?: { x: number; y: number; width: number; height: number };\n}\n\ninterface AdaVisionNative {\n isSupported(): boolean;\n assessQuality(uri: string): Promise<RawFrameQuality>;\n startVision(intervalMs: number): Promise<void>;\n /** Absent from binaries built before the front camera existed. */\n startVisionCamera?(intervalMs: number, facing: VisionFacing): Promise<void>;\n /** Absent from binaries built before AI live existed. A base64 JPEG. */\n jpegById?(id: string, maxSide: number, quality: number): Promise<string>;\n stopVision(): void;\n visionRetentionMs(): number;\n qualityById(id: string): RawFrameQuality | null;\n readText(id: string, languages: string[] | null): Promise<VisionText[]>;\n describe(id: string, maxLabels: number): Promise<VisionLabel[]>;\n detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;\n addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;\n}\n\nconst native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');\n\n/** The native function, or a refusal by name: Android's AdaVision has assessQuality only\n * (AI Models F4), so a verb it lacks says so instead of failing as \"not a function\". */\nfunction verb<K extends keyof AdaVisionNative>(name: K): AdaVisionNative[K] {\n if (!native) throw new Error('ada-vision native module not present in this build');\n const fn = native[name];\n if (typeof fn !== 'function') {\n throw Object.assign(new Error(`UNAVAILABLE: ada-vision ${String(name)} is not implemented on this platform (iOS only)`), { code: 'UNAVAILABLE' });\n }\n return (fn as Function).bind(native) as AdaVisionNative[K];\n}\n\n/** Is the native vision module present in this binary? */\nexport function isSupported(): boolean {\n try { return native?.isSupported() ?? false; } catch { return false; }\n}\n\n/** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */\nexport async function assessQuality(uri: string): Promise<RawFrameQuality> {\n if (!native) throw new Error('ada-vision native module not present in this build');\n return native.assessQuality(uri);\n}\n\n// ── Vision session (feeds the Visual Engine) ──\n\n/** Which camera the vision session reads. */\nexport type VisionFacing = 'front' | 'back';\n\n/**\n * Start the vision session on `facing` — publishes a timeline frame every\n * `intervalMs`. The back camera works on every binary; the front camera needs a\n * binary built with it, and an older one REFUSES it rather than quietly reading\n * the back camera instead.\n */\nexport async function startVision(intervalMs = 1000, facing: VisionFacing = 'back'): Promise<void> {\n if (facing !== 'front' && facing !== 'back') {\n throw new Error(`Unknown camera \"${String(facing)}\" — use \"front\" or \"back\"`);\n }\n if (facing === 'back') return verb('startVision')(intervalMs);\n if (native && typeof native.startVisionCamera !== 'function') {\n throw new Error('the front camera needs a newer app build — this one can read the back camera only');\n }\n return verb('startVisionCamera')!(intervalMs, facing);\n}\n\n/** Stop the vision session and release the camera. */\nexport function stopVision(): void {\n native?.stopVision?.();\n}\n\n/** Subscribe to published timeline frames (id + quality + expiry; no pixels). */\nexport function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {\n if (!native) return { remove() {} };\n return native.addListener('visionFrame', cb);\n}\n\n/** How far back the native ring can still resolve a frame id (ms). */\nexport function visionRetentionMs(): number {\n return native?.visionRetentionMs?.() ?? 0;\n}\n\n/** Resolve a published frame id → its measured quality (null if evicted). */\nexport function qualityById(id: string): RawFrameQuality | null {\n return native?.qualityById?.(id) ?? null;\n}\n\n/** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */\nexport async function readText(id: string, languages?: string[]): Promise<VisionText[]> {\n return verb('readText')(id, languages ?? null);\n}\n\n/** On-device image classification → labels (not prose). [] = nothing recognised. */\nexport async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {\n return verb('describe')(id, maxLabels);\n}\n\n/** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located\n * items with boxes. [] = looked, found nothing above `minConfidence`; throws =\n * couldn't look (id aged out, or the model isn't in this build). */\nexport async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {\n return verb('detectObjects')(id, maxItems, minConfidence);\n}\n\n/**\n * A published frame as a base64 JPEG, its longer side at most `maxSide`. FOR THE SDK'S AI LIVE SENDER ONLY\n * (aiLive/provider.ts), which calls it after the two-sided \"Pictures to AI\" consent: this is the one way a\n * picture leaves native, and no plugin-facing API reaches it. An older binary refuses by name.\n */\nexport async function jpegById(id: string, maxSide = 768, quality = 0.7): Promise<string> {\n if (native && typeof native.jpegById !== 'function') {\n throw new Error('sending pictures to AI needs a newer app build — this one cannot make a picture from a camera frame');\n }\n return verb('jpegById')!(id, maxSide, quality);\n}\n","/**\n * The wire protocol of Core's binary stream endpoint (/ws/stream), for the AI live sender.\n *\n * A PORT of Core's `packages/media-stream/src/index.js` (@ateam-ai/media-stream, not on npm\n * yet), limited to what a sender needs. The frame bytes are pinned by\n * tests/aiLiveStreamProtocol.test.mjs against the layout that file documents, so a drift is\n * a red test, not a silent mismatch. DEAD — replace with an import of @ateam-ai/media-stream\n * once Core publishes it.\n *\n * Frame envelope (bytes): 0 version (1) · 1 flags · 2-5 seq uint32 BE (one per sampling slot\n * of the SENDER, taken or skipped) · 6-13 tCapture float64 BE (ms since the epoch) · 14- payload.\n */\nexport const STREAM_PATH = '/ws/stream';\nexport const SUBPROTOCOL = 'adas.v1';\nexport const HEADER_BYTES = 14;\nconst ENVELOPE_VERSION = 1;\n\nexport const CLOSE = Object.freeze({\n AUTH_MISSING: 4001,\n AUTH_FAILED: 4002,\n NO_SUCH_WORKSPACE: 4003,\n NOT_ENTITLED: 4004,\n BAD_REQUEST: 4400,\n UNKNOWN_KIND: 4404,\n TIMEOUT: 4408,\n OVER_LIMIT: 4429,\n TOO_BIG: 1009,\n CONSUMER_FAILING: 1011,\n});\n\nexport function encodeFrame({ seq, tCapture, payload }: { seq: number; tCapture: number; payload: Uint8Array }): Uint8Array {\n const out = new Uint8Array(HEADER_BYTES + payload.length);\n const v = new DataView(out.buffer);\n v.setUint8(0, ENVELOPE_VERSION);\n v.setUint8(1, 0);\n v.setUint32(2, seq >>> 0, false);\n v.setFloat64(6, tCapture, false);\n out.set(payload, HEADER_BYTES);\n return out;\n}\n\nexport function openMessage(kind: string, params?: Record<string, unknown>): string {\n return JSON.stringify(params ? { type: 'open', kind, params } : { type: 'open', kind });\n}\n\nexport const CLOSE_MESSAGE = JSON.stringify({ type: 'close' });\n\n/** A server text frame → { event, data }, or null for anything that is not an event. */\nexport function parseServerEvent(text: unknown): { event: string; data: any } | null {\n if (typeof text !== 'string') return null;\n try {\n const m = JSON.parse(text);\n return m && typeof m.event === 'string' ? { event: m.event, data: m.data } : null;\n } catch {\n return null;\n }\n}\n\nconst WHY: Record<number, string> = {\n [CLOSE.AUTH_MISSING]: 'The login or the workspace was missing. Sign in again and try once more.',\n [CLOSE.AUTH_FAILED]: 'The login was refused. Sign in again and try once more.',\n [CLOSE.NO_SUCH_WORKSPACE]: 'This workspace was not found.',\n [CLOSE.NOT_ENTITLED]: 'This workspace may not stream right now.',\n [CLOSE.TIMEOUT]: 'The connection to the AI timed out.',\n [CLOSE.OVER_LIMIT]: 'Too many cameras are sending to this workspace. Stop another one first.',\n [CLOSE.TOO_BIG]: 'A picture was too big to send.',\n [CLOSE.CONSUMER_FAILING]: 'The AI stopped taking pictures. Check the live session in AI Models.',\n};\n\n/** A close as one plain sentence: the server's own words for a refused open. */\nexport function describeClose(code: number, reason?: string): string {\n if (code === CLOSE.BAD_REQUEST && reason) return reason;\n return WHY[code] || (reason ? reason : `The connection to the AI closed (${code}).`);\n}\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => { const t = new Int16Array(256).fill(-1); for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i; return t; })();\n\n/** Base64 (the native module's JPEG) → bytes. Ignores padding and line breaks. */\nexport function base64ToBytes(b64: string): Uint8Array {\n const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');\n const out = new Uint8Array(Math.floor((clean.length * 3) / 4));\n let o = 0, acc = 0, bits = 0;\n for (let i = 0; i < clean.length; i++) {\n acc = (acc << 6) | B64_INDEX[clean.charCodeAt(i)];\n bits += 6;\n if (bits >= 8) { bits -= 8; out[o++] = (acc >> bits) & 0xff; }\n }\n return o === out.length ? out : out.subarray(0, o);\n}\n","/**\n * The motion gate (owner 2026-11-01): the phone decides, for free, whether anything on the camera changed, and only\n * then do pictures go to the AI. The SAME RULE as the computer's camera window and the engine's test replay\n * (ai-dev-assistant connectors/ai-models-mcp/ui-dist/ai-models/motionGate.js), held to it by the same test cases\n * (tests/aiLiveMotionGate.test.mjs is a copy of that repo's tests/motionGate.test.js). Change one, change both.\n *\n * Pure: no clock, no camera. Each sampled picture comes with its capture time and its SIGNATURE: a tiny grey\n * thumbnail (SIG_W × SIG_H bytes, 0..255, signature.ts), and the gate says which pictures to send now.\n * quiet nothing changed: the picture is held (pre-roll), not sent; except one every heartbeatSec, so the AI\n * still LOOKS now and then (GPT review 2026-11-01: someone lying still must still be seen, and an\n * episode stays observed; a quiet stretch never means \"it ended\").\n * change the picture differs from the previous one by more than the sensitivity: the held pictures of the last\n * preRollSec go first, oldest first, then this one.\n * after pictures keep going for postRollSec after the last change.\n * The settings come from Core ({\"event\":\"motion\"}), set on the Live tab of the AI Models page.\n */\nexport const SIG_W = 16;\nexport const SIG_H = 12;\n// A camera's pictures are not exactly 1 s apart (timers drift by milliseconds): \"1 s before / after\" allows this much\n// more, so a picture taken 1.003 s before a change still counts as the second before it (seen live on dev, 2026-11-01).\nexport const SLACK_SEC = 0.15;\nexport const SENSITIVITY = Object.freeze({ low: 0.06, medium: 0.03, high: 0.015 });\nexport type Sensitivity = keyof typeof SENSITIVITY;\nexport interface MotionGateConfig { enabled: boolean; sensitivity: Sensitivity; preRollSec: number; postRollSec: number; heartbeatSec: number }\nexport const GATE_DEFAULTS: Readonly<MotionGateConfig> = Object.freeze({ enabled: true, sensitivity: 'medium', preRollSec: 1, postRollSec: 1, heartbeatSec: 8 });\nexport type GateReason = 'first' | 'change' | 'after' | 'heartbeat' | 'quiet' | 'off';\nexport interface GateCounters { offered: number; sent: number; held: number; heartbeats: number; changes: number }\n\n/** 0..1: how different two signatures are (mean absolute difference). A missing one counts as fully different. */\nexport function difference(a: ArrayLike<number> | null | undefined, b: ArrayLike<number> | null | undefined): number {\n if (!a || !b || a.length !== b.length || !a.length) return 1;\n let sum = 0;\n for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);\n return sum / (a.length * 255);\n}\n\n/** Only the fields Core may set, each checked; anything else keeps the default. */\nexport function gateConfigFrom(d: any): MotionGateConfig {\n const c: MotionGateConfig = { ...GATE_DEFAULTS };\n if (!d || typeof d !== 'object') return c;\n if (typeof d.enabled === 'boolean') c.enabled = d.enabled;\n if (typeof d.sensitivity === 'string' && Object.prototype.hasOwnProperty.call(SENSITIVITY, d.sensitivity)) c.sensitivity = d.sensitivity as Sensitivity;\n for (const k of ['preRollSec', 'postRollSec', 'heartbeatSec'] as const) {\n const v = Number(d[k]);\n if (Number.isFinite(v) && v >= 0 && v <= 60) c[k] = v;\n }\n return c;\n}\n\nexport function createMotionGate<T>(cfg: Partial<MotionGateConfig> = {}) {\n const c: MotionGateConfig = { ...GATE_DEFAULTS, ...cfg };\n const threshold = SENSITIVITY[c.sensitivity] ?? SENSITIVITY.medium;\n let prevSig: ArrayLike<number> | null = null;\n let lastChangeT = -Infinity;\n let lastSentT = -Infinity;\n let held: { t: number; item: T }[] = [];\n const n: GateCounters = { offered: 0, sent: 0, held: 0, heartbeats: 0, changes: 0 };\n\n const sendNow = (t: number, item: T, reason: GateReason, withPreRoll = false) => {\n const out = withPreRoll ? held.filter((h) => t - h.t <= c.preRollSec + SLACK_SEC).map((h) => h.item) : [];\n held = [];\n out.push(item);\n lastSentT = t;\n n.sent += out.length;\n return { send: out, reason };\n };\n\n return {\n offer({ t, sig, item }: { t: number; sig: ArrayLike<number> | null; item: T }): { send: T[]; reason: GateReason } {\n n.offered++;\n if (!c.enabled) { lastSentT = t; n.sent++; return { send: [item], reason: 'off' }; }\n const first = prevSig == null;\n const d = difference(prevSig, sig);\n prevSig = sig;\n if (first) return sendNow(t, item, 'first');\n if (d > threshold) { lastChangeT = t; n.changes++; return sendNow(t, item, 'change', true); }\n if (t - lastChangeT <= c.postRollSec + SLACK_SEC) return sendNow(t, item, 'after');\n if (t - lastSentT >= c.heartbeatSec) { n.heartbeats++; return sendNow(t, item, 'heartbeat'); }\n held.push({ t, item });\n while (held.length && t - held[0].t > c.preRollSec + 1) held.shift();\n n.held++;\n return { send: [], reason: 'quiet' };\n },\n counters: (): GateCounters => ({ ...n }),\n config: (): MotionGateConfig => ({ ...c }),\n };\n}\n","/**\n * A camera frame's SIGNATURE for the motion gate: a SIG_W × SIG_H grey thumbnail (0..255), the same measure the\n * computer's camera window takes from a tiny canvas. The native module gives JS no pixels, only JPEGs (jpegById),\n * so the phone asks native for a very small JPEG of the frame and decodes it here (jpeg-js, pure JS, no new app\n * build). Each cell is the mean luma (Rec. 601) of the pixels that fall in it.\n */\nimport { decode } from 'jpeg-js';\nimport { base64ToBytes } from './streamProtocol';\nimport { SIG_H, SIG_W } from './motionGate';\n\n/** The longer side of the JPEG asked of native for a signature: small enough to be cheap, big enough to average. */\nexport const SIG_JPEG_SIDE = 64;\n\nexport function signatureFromJpeg(b64: string): Uint8Array {\n const img = decode(base64ToBytes(b64), { useTArray: true, formatAsRGBA: true, maxResolutionInMP: 1 });\n const { width: w, height: h, data } = img;\n if (!w || !h) throw new Error('empty picture');\n const sum = new Float64Array(SIG_W * SIG_H);\n const cnt = new Uint32Array(SIG_W * SIG_H);\n for (let y = 0; y < h; y++) {\n const cy = Math.min(SIG_H - 1, Math.floor((y * SIG_H) / h));\n for (let x = 0; x < w; x++) {\n const cx = Math.min(SIG_W - 1, Math.floor((x * SIG_W) / w));\n const i = (y * w + x) * 4;\n const k = cy * SIG_W + cx;\n sum[k] += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];\n cnt[k]++;\n }\n }\n const out = new Uint8Array(SIG_W * SIG_H);\n for (let k = 0; k < out.length; k++) out[k] = cnt[k] ? Math.round(sum[k] / cnt[k]) : 0;\n return out;\n}\n"],"mappings":"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","names":["set"]}
@@ -2,15 +2,17 @@ import {
2
2
  CLOSE_WAIT_MS,
3
3
  LIVE_KIND,
4
4
  OPEN_TIMEOUT_MS,
5
+ PHONE_MAX_PRE_ROLL_SEC,
5
6
  createAiLiveProvider,
6
7
  defaultAiLiveDeps
7
- } from "../chunk-PV2EHH2P.mjs";
8
+ } from "../chunk-PQUTEM3C.mjs";
8
9
  import "../chunk-WQK6PUBN.mjs";
9
10
  import "../chunk-WMJKH4XE.mjs";
10
11
  export {
11
12
  CLOSE_WAIT_MS,
12
13
  LIVE_KIND,
13
14
  OPEN_TIMEOUT_MS,
15
+ PHONE_MAX_PRE_ROLL_SEC,
14
16
  createAiLiveProvider,
15
17
  defaultAiLiveDeps
16
18
  };