@myzonerocks/gosslens 0.9.0 → 0.10.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/README.md CHANGED
@@ -1,44 +1,79 @@
1
1
  # Gosslens - TypeScript SDK
2
2
 
3
- TypeScript SDK for [Gosslens](../../include/gosslens.h), a camera engine
4
- with a Zig core behind one C ABI, compiled to `wasm32`. Wraps it as
5
- `GossEngine`, `GossSession`, and `Gosslens`, the same names the
6
- [Swift](../swift/README.md) and [Kotlin](../kotlin/README.md) SDKs use.
3
+ TypeScript SDK for [Gosslens](../../include/gosslens.h), a camera engine behind
4
+ one C ABI, compiled to `wasm32`. It wraps the engine as `GossEngine`,
5
+ `GossSession`, and `Gosslens`, the same names the [Swift](../swift/README.md) and
6
+ [Kotlin](../kotlin/README.md) SDKs use.
7
7
 
8
- This SDK owns camera capture through `getUserMedia`, the render loop,
9
- and decoding the PNGs the core has no decoder for. The frame graph, lens
10
- runtime, and effect pipeline live in the core.
11
-
12
- [docs/INTEGRATION-WEB.md](../../docs/INTEGRATION-WEB.md) is the start-to-finish
13
- guide: build and host the wasm and model assets, the render loop, and the
14
- tracking worker.
8
+ This SDK owns camera capture through `getUserMedia`, the render loop, and
9
+ decoding the PNGs the core has no decoder for. The frame graph, lens runtime, and
10
+ effect pipeline live in the core. You write TypeScript; the engine is a prebuilt
11
+ WebAssembly build you host and hand the SDK. The [demo](demo/) is a full working
12
+ page, and the cross-platform SDK overview is in the [root README](../../README.md).
15
13
 
16
14
  ## Install
17
15
 
18
- ```
16
+ The npm package is the JavaScript wrapper you add with bun and write TypeScript
17
+ against. It does not carry the engine. The engine it drives is the emscripten
18
+ `gosslens_web.js`/`.wasm` (a WebGL2 build and a WebGPU build) plus
19
+ `gosslens_tracking.wasm`, which you host next to your app.
20
+
21
+ ```sh
19
22
  bun add @myzonerocks/gosslens
20
23
  ```
21
24
 
22
- Inside this monorepo, consume it from the workspace instead:
25
+ Because the browser cannot fetch a `.wasm` out of `node_modules` the way a native
26
+ app links an archive, you serve the prebuilt engine files from your own static
27
+ host and hand the SDK their URLs. The SDK never guesses a path. Every release
28
+ attaches `gosslens-web-engine.zip` - the two `gosslens_web` builds (WebGPU and
29
+ WebGL2 are separate artifacts, not a runtime toggle) and the tracking wasm - so
30
+ unzip it into your static assets. Grab it from the
31
+ [releases page](https://github.com/myzonerocks/gosslens/releases). `pickEngineUrl`
32
+ picks the WebGL2 or WebGPU build at load time from the URLs you point it at, after
33
+ confirming a real WebGPU adapter.
34
+
35
+ ### Building from source
36
+
37
+ Prefer compiling the engine and the SDK yourself, from a clone or your own
38
+ fork? Build the engine artifacts:
39
+
40
+ ```sh
41
+ zig build wasm-emscripten
42
+ zig build wasm-emscripten-webgpu
43
+ zig build tracking-wasm
44
+ zig build fetch-models
45
+ ```
46
+
47
+ Inside this monorepo, consume the SDK as a workspace dependency instead of the
48
+ published package:
23
49
 
24
50
  ```json
25
51
  { "dependencies": { "@myzonerocks/gosslens": "workspace:*" } }
26
52
  ```
27
53
 
28
- `bun run build` compiles `src/` to `dist/src/` (the package points `main`
29
- and `types` there); run it once so `dist/` exists before a workspace
54
+ `bun run build` compiles `src/` to `dist/src/` (the package points `main` and
55
+ `types` there); run it once in `sdk/ts` so `dist/` exists before a workspace
30
56
  consumer resolves it.
31
57
 
32
- This package is the JS wrapper only. `zig build wasm-emscripten` builds
33
- the WebGL2 `gosslens_web.js`/`.wasm` pair; `zig build
34
- wasm-emscripten-webgpu` builds the WebGPU pair. `pickEngineUrl` picks
35
- between them at load, so `wasmJsUrl` points at whichever one it selects.
36
- They are separate artifacts, not bundled.
58
+ ## The render loop
59
+
60
+ `GossPreviewSession` does the engine, renderer, session, and capture loop in one
61
+ call - most apps want this:
37
62
 
38
- ## Use
63
+ ```typescript
64
+ import { GossPreviewSession, pickEngineUrl } from "@myzonerocks/gosslens";
65
+
66
+ const wasmJsUrl = await pickEngineUrl(webgpuUrl, webgl2Url);
67
+ const preview = await GossPreviewSession.create(canvas, wasmJsUrl);
68
+ preview.activateLens(manifestJson);
69
+ ```
39
70
 
40
- ```ts
41
- import { Gosslens, GossEngine, GossSession, GossPreviewSession } from "@myzonerocks/gosslens";
71
+ `pickEngineUrl` confirms a real WebGPU adapter before choosing, and falls back to
72
+ the WebGL2 URL. `create` takes an optional third `events` argument for the
73
+ capture-loop callbacks. If you drive the loop yourself, the pieces are public too:
74
+
75
+ ```typescript
76
+ import { Gosslens, GossEngine, GossSession } from "@myzonerocks/gosslens";
42
77
 
43
78
  const gosslens = await Gosslens.load(canvas, wasmJsUrl);
44
79
  const engine = GossEngine.create(gosslens);
@@ -47,37 +82,374 @@ const session = GossSession.create(engine);
47
82
 
48
83
  session.submitFrameRgbaCopy(rgba, width * 4, width, height);
49
84
  engine.renderFrame(session);
85
+ ```
86
+
87
+ ## Camera controls
50
88
 
51
- session.setWhiten(0.6);
89
+ The engine never touches the camera. It holds declarative intent you set,
90
+ normalizes it, and hands it back for you to apply as getUserMedia track
91
+ constraints. The controls live on `GossSession`, so reach them through
92
+ `preview.session` if you took the `GossPreviewSession` path:
93
+
94
+ ```typescript
95
+ session.setCameraControls({ ...session.cameraControls(), flashMode: 2, zoomFactor: 2 });
96
+
97
+ const applied = session.cameraControls();
98
+ await track.applyConstraints({ advanced: [{ zoom: applied.zoomFactor,
99
+ torch: applied.torch === 1 }] });
100
+ ```
101
+
102
+ `GossCameraControls` also carries focus and exposure mode and points, the
103
+ exposure bias, and the front-camera mirror-save policy. `setRecordingPolicy`/
104
+ `recordingPolicy` (clip cap, segment and loop mode, speed preset, mic mute,
105
+ save-original, stabilization) round-trips for your `MediaRecorder`, and
106
+ `setCaptureUi`/`captureUi` (grid, level, shutter mode, self-timer, night mode,
107
+ front-screen flash) for the capture chrome you draw. The engine validates and
108
+ clamps every field; you read it back and apply it.
109
+
110
+ ## Lenses
111
+
112
+ A lens is a manifest plus its assets. Activate one from its manifest JSON, and
113
+ drop it again with `deactivateLens`:
114
+
115
+ ```typescript
52
116
  session.activateLens(manifestJson);
117
+ session.deactivateLens();
53
118
  ```
54
119
 
55
- `GossPreviewSession.create(canvas, wasmJsUrl, events)` does all three setup
56
- steps and owns the capture loop too; most app code wants this one.
120
+ The web build activates from the manifest JSON directly. The directory-bundle
121
+ path the native SDKs also take is not wired here: a wasm target has no file IO,
122
+ and the `shader.pass`, `lut.pass`, and `blend.pass` nodes it would carry need
123
+ compiled resources this SDK has no way to hand over yet. A lens built entirely
124
+ from `beauty.*` nodes (the beauty-baseline lens, say) activates and runs for real
125
+ regardless, through the same beauty chain the effects below drive.
57
126
 
58
- WebGPU and WebGL2 are two separate build artifacts, not a runtime
59
- toggle; `pickEngineUrl` picks the right one after confirming a real
60
- adapter.
127
+ A lens that reads face, hand, pose, or segmentation data renders nothing until
128
+ you feed the matching tracker result each frame (see Tracking); it stays silent,
129
+ with no error. A lens's triggers also react to signals you already feed:
130
+ `camera.zoom`, `camera.focus` and `camera.exposure` follow the camera controls
131
+ above, `geo.in_region` follows the geofence below, and `gaze.*` and
132
+ `head.nod`/`head.shake`/`head.tilt` follow the face tracker. The full grammar is
133
+ in [the lens spec](../../lenses/SPEC.md).
61
134
 
62
- Capture reads a PNG back, and world tracking feeds WebXR frames in:
135
+ Advance the lens's own clock, triggers, and script nodes once per display frame
136
+ with `tickLens`, passing the live signals this tick evaluates against:
63
137
 
64
- ```ts
65
- import { GossWebXRWorldSource } from "@myzonerocks/gosslens";
138
+ ```typescript
139
+ session.tickLens(dtUs, {
140
+ hasFace: result.presence >= 0.5,
141
+ blendshapes: result.blendshapes,
142
+ });
143
+ ```
144
+
145
+ Omitted signal fields read as false or zero, so a bare `tickLens(dtUs)` only
146
+ advances triggers with no `when` gate. `fireEvent(name)` delivers a named event
147
+ to the lens's `event('name')` triggers for the next tick, and
148
+ `parameterValue(name)` reads a live lens parameter back, including whatever a
149
+ script node last wrote.
150
+
151
+ ## Beauty and makeup
152
+
153
+ The beauty effects are direct session calls, each an amount in 0..1:
154
+
155
+ ```typescript
156
+ session.setSmooth(0.6);
157
+ session.setWhiten(0.5);
158
+ session.setThinFace(0.3);
159
+ session.setBigEye(0.2);
160
+ session.setLipstick(0.7);
161
+ session.setBlush(0.4);
162
+ ```
66
163
 
67
- const png = await engine.captureFrame(session); // data URL
164
+ Whiten reads four lookup textures, and lipstick and blush their own source
165
+ images, none of which the core decodes. Load them once after setup; the SDK
166
+ decodes the PNGs, and until they resolve the matching setter stays a no-op:
167
+
168
+ ```typescript
169
+ await session.loadWhitenLuts(new URL("./res/", baseUrl)); // the four lookup_*.png
170
+ await session.loadMakeupTextures(new URL("./res/", baseUrl)); // mouth.png, blusher.png
171
+ ```
172
+
173
+ `setMakeupReference` samples a reference photo's makeup color per face part: the
174
+ lips, eyes, brows, and a cheek-and-forehead skin patch, so a lens's `tint.pass`
175
+ with a reference source paints the live face in that color and a foundation over
176
+ `face_skin` matches the reference's skin tone. Pass the reference RGBA and its
177
+ 478-point face landmarks; an empty landmarks array clears it:
178
+
179
+ ```typescript
180
+ session.setMakeupReference(refRgba, refWidth, refHeight, refLandmarks);
181
+ ```
182
+
183
+ ## Tracking
184
+
185
+ Face, hand, pose, and segmentation run in the `gosslens_tracking.wasm` module,
186
+ off the main thread in a Worker. Each pipeline is a class that takes the module
187
+ bytes and a model bundle and runs inference synchronously inside the worker:
188
+
189
+ ```typescript
190
+ // tracking-worker.ts
191
+ import { GossFaceTracker, GossHandTracker, GossPoseTracker } from "@myzonerocks/gosslens";
192
+
193
+ const moduleBytes = await (await fetch(trackingWasmUrl)).arrayBuffer();
194
+ const face = await GossFaceTracker.create(moduleBytes, faceTaskBytes);
195
+ const hands = await GossHandTracker.create(moduleBytes, gestureTaskBytes);
196
+ const pose = await GossPoseTracker.create(moduleBytes, poseTaskBytes);
197
+ // per frame, on RGBA pixels from the camera canvas:
198
+ const faceResult = face.process(rgba, width, height, timestampUs);
199
+ const handResult = hands.process(rgba, width, height, timestampUs);
200
+ const poseResult = pose.process(rgba, width, height, timestampUs);
201
+ ```
202
+
203
+ Standing a tracker up is the web equivalent of the native SDKs' enable calls:
204
+ this build runs no internal engine tracker, so you run the pipeline you need and
205
+ feed its result back. `GossHandTracker` returns up to two hands, each with 21
206
+ landmarks, a handedness score, and a canned gesture when the gesture bundle is
207
+ loaded; `GossPoseTracker` returns the 33-point skeleton.
208
+ [`demo/tracking-worker.ts`](demo/tracking-worker.ts) is the reference worker, and
209
+ [`demo/track-worker.ts`](demo/track-worker.ts) stands all four pipelines up over
210
+ still images.
211
+
212
+ The `.task`/`.tflite` bundles (`face_landmarker.task`, `gesture_recognizer.task`,
213
+ `pose_landmarker_full.task`, `selfie_multiclass.tflite`) are the ones
214
+ `fetch-models` writes; host and fetch the ones your lenses use.
215
+
216
+ Feed the tracked results back to the session for a lens to anchor to. The
217
+ single-face path is `setFaceLandmarks`. To fan a face-anchored lens out across
218
+ every face in frame, hand the engine the faces you tracked this frame:
219
+
220
+ ```typescript
221
+ session.submitFaces(faces); // GossFaceInput[], up to GOSS_FACE_MAX; [] clears
222
+ for (let i = 0; i < session.faceCount(); i++) {
223
+ const face = session.faceResultAt(i);
224
+ }
225
+ ```
226
+
227
+ `submitBodies` is the multi-person equivalent, reaching every tracked figure;
228
+ `setPoseUpperBody(true)` drops the skeleton's legs (knees down) for selfie
229
+ framing where they are out of shot:
230
+
231
+ ```typescript
232
+ session.submitBodies(bodies); // GossPoseInput[], up to GOSS_BODY_MAX; [] clears
233
+ for (let i = 0; i < session.bodyCount(); i++) {
234
+ const body = session.bodyResultAt(i);
235
+ }
236
+ ```
237
+
238
+ `faceRegion` returns the tracked point of a named attach point for pinning
239
+ content - forehead, glabella, nose tip, chin, an eye, a cheek, an ear, or a mouth
240
+ corner - reading the face landmarks you last submitted:
241
+
242
+ ```typescript
243
+ const p = session.faceRegion(GossFaceRegion.Forehead);
244
+ ```
245
+
246
+ For a body or hand joint, read the point off the tracker result directly: the
247
+ pose result carries all 33 landmarks, each hand its 21. The `bodyJoint` and
248
+ `handJoint` session pins read the engine's own internal trackers, which this
249
+ build does not stand up, so they stay empty here; on native they return the named
250
+ joint's point.
251
+
252
+ The face tracker also drives the `gaze.*` and `head.nod`/`head.shake`/`head.tilt`
253
+ lens triggers; `camera.*` follow the camera controls and `geo.in_region` the
254
+ geofence below. The full grammar is in [the lens spec](../../lenses/SPEC.md).
255
+
256
+ ## Segmentation
257
+
258
+ `GossSegmenter` runs a single `.tflite` model through the same tracking module
259
+ and returns a subject mask, `GOSS_SEGMENTATION_MASK_SIDE` squared floats, plus the
260
+ model's own class channels. Feed the subject mask back as the texture the lens's
261
+ blend and mask channels sample:
262
+
263
+ ```typescript
264
+ const segmenter = await GossSegmenter.create(moduleBytes, selfieMulticlassBytes);
265
+ const subject = segmenter.process(rgba, width, height);
266
+ session.setSegmentationMask(subject); // null clears it
267
+ ```
268
+
269
+ `selfie_multiclass.tflite` publishes the seven channels in
270
+ `GOSS_SEGMENTATION_CHANNELS` (person, background, hair, body_skin, face_skin,
271
+ clothes, others); `hair_segmenter.tflite` and `deeplab_v3.tflite` are the other
272
+ shipped models, a single hair mask and another multiclass label set. A lens that
273
+ names class channels reports them as a bitmask from `segmentationChannels`; upload
274
+ exactly those each frame with `setSegmentationClassMask`, after the subject mask,
275
+ since setting the subject clears the classes:
276
+
277
+ ```typescript
278
+ const wanted = session.segmentationChannels();
279
+ for (let channel = 1; channel < GOSS_SEGMENTATION_CHANNELS.length; channel++) {
280
+ if (wanted & (1 << channel)) {
281
+ session.setSegmentationClassMask(channel, segmenter.classMask(channel - 1));
282
+ }
283
+ }
284
+ ```
285
+
286
+ ## World and AR
287
+
288
+ `GossWebXRWorldSource` feeds WebXR frames into the session's world tracker, so a
289
+ world-anchored lens or AR brush stroke stays fixed in the scene. Drive it from the
290
+ XR animation loop:
291
+
292
+ ```typescript
293
+ import { GossWebXRWorldSource } from "@myzonerocks/gosslens";
68
294
 
69
295
  const world = new GossWebXRWorldSource(session);
70
296
  // in the XR animation loop:
71
297
  world.onFrame(xrFrame, referenceSpace, timestampUs);
72
298
  ```
73
299
 
74
- On web, recording and photo capture run through the browser, used by both
75
- this SDK and the demo: `captureFrame()` exports a PNG off the composited
76
- canvas, and `captureStream()` drives a `MediaRecorder` with the
77
- engine-normalized recording policy. The engine's own hardware encoder and
78
- the platform-native HEIC format stay native-only, since the browser owns
79
- encoding on web; see [docs/PARITY.md](../../docs/PARITY.md). The
80
- cross-platform SDK overview is in the [root README](../../README.md).
300
+ ## Depth
301
+
302
+ If the XR session was granted `depth-sensing`, feed each frame's depth so a
303
+ depth-aware lens can occlude content behind real geometry. The map is metres per
304
+ pixel, row major, with the near and far range it spans:
305
+
306
+ ```typescript
307
+ const info = frame.getDepthInformation(view); // WebXR depth-sensing
308
+ // read info into a Float32Array of metres, then:
309
+ session.submitDepth(depth, info.width, info.height, 0.1, 5.0);
310
+ ```
311
+
312
+ An empty array clears it. The engine keeps the latest map for the occlusion pass.
313
+
314
+ ## Camera intrinsics
315
+
316
+ If the camera reports its calibration, feed it once so an `undistort.pass` can
317
+ straighten wide-angle lens distortion. The focal lengths and principal point are
318
+ in pixels of the submitted frame, followed by the radial coefficients (k1, k2):
319
+
320
+ ```typescript
321
+ // fx, fy, cx, cy from the platform's camera calibration, then:
322
+ session.submitCameraIntrinsics(fx, fy, cx, cy, new Float32Array([k1, k2]));
323
+ ```
324
+
325
+ An empty array clears them, leaving an `undistort.pass` inert.
326
+
327
+ ## Geofilters
328
+
329
+ A lens can gate on place. Feed a location fix from the Geolocation API and
330
+ describe the region the lens belongs to; the engine decides membership in wasm
331
+ and the fix never leaves the page:
332
+
333
+ ```typescript
334
+ navigator.geolocation.watchPosition((pos) => {
335
+ session.submitLocation(pos.coords.latitude, pos.coords.longitude,
336
+ pos.coords.accuracy, pos.timestamp * 1000);
337
+ });
338
+ session.setGeofence(lat, lon, 150);
339
+ ```
340
+
341
+ `setGeofenceBBox` and `setGeofencePolygon` describe a box or a ring instead;
342
+ `setGeoAccuracy` sets the worst fix that still counts as inside, and
343
+ `clearGeofence` drops the gate. Membership drives the lens grammar's
344
+ `geo.in_region` trigger.
345
+
346
+ ## Brush
347
+
348
+ Freehand strokes composite over the frame. Open a stroke, push normalized points,
349
+ and close it; the engine keeps the undo/redo stack and hands back the ribbon
350
+ (x, y, r, g, b, a per vertex):
351
+
352
+ ```typescript
353
+ session.setBrushStyle(1, 0.4, 0.6, 1, 0.01);
354
+ session.setBrushMode(3); // 0 pen, 1 highlighter, 2 marker, 3 neon
355
+ session.beginStroke();
356
+ session.addStrokePoint(nx, ny);
357
+ session.endStroke();
358
+ const ribbon = session.brushVertices();
359
+ ```
360
+
361
+ `setARBrushStyle`/`beginARStroke`/`addARStrokePoint(x, y, z)`/`endARStroke` are the
362
+ world-anchored twin: points are pushed in the world frame world tracking reports,
363
+ so a stroke stays fixed in the scene.
364
+
365
+ ## Capture and recording
366
+
367
+ The browser owns encoding on the web, so photo and video capture run through it.
368
+ `captureFrame` reads the composited canvas back and returns a PNG data URL:
369
+
370
+ ```typescript
371
+ const png = await engine.captureFrame(session); // or preview.captureFrame()
372
+ ```
373
+
374
+ `captureStill` is the high-resolution path, decoupled from the preview size: it
375
+ renders the composite at its own or a requested resolution, supersamples, and
376
+ returns the encoded bytes with the format, gamut, and bit depth you tag. It needs
377
+ the wasm renderer, so it is a no-op on the pure WebGL2 fallback:
378
+
379
+ ```typescript
380
+ const jpeg = await engine.captureStill(session, { width: 4032, height: 3024, format: 1, quality: 90 });
381
+ ```
382
+
383
+ For video, drive a `MediaRecorder` off `canvas.captureStream()` yourself.
384
+ `setRecordingPolicy`/`recordingPolicy` round-trips the clip cap, segment and loop
385
+ mode, speed preset, mic mute, save-original, and stabilization the engine
386
+ normalizes, so the recorder and chrome you build read one validated policy. The
387
+ still encoder is the core's own (PNG and JPEG, wide-gamut and 16-bit PNG tagged);
388
+ the live video encoder stays native-only, which is why recording runs through the
389
+ browser. See [PARITY.md](../../docs/PARITY.md).
390
+
391
+ ## Compositing
392
+
393
+ Beyond the camera, a session composites named RGBA sources into one frame, for a
394
+ duet, a stitch, or a live grid. The camera is the implicit source 0; define others
395
+ by name and push a frame into each:
396
+
397
+ ```typescript
398
+ session.defineSource("guest");
399
+ session.submitSourceFrameRgba("guest", rgba, width, height, width * 4);
400
+ session.setLayout(3); // 0 custom, 1 side-by-side, 2 top-bottom, 3 pip, 4 grid, 5 overlay
401
+ ```
402
+
403
+ `setSourceComposite` sets a source's opacity and key mode (none, matte, or a
404
+ chroma key with its color and similarity), so a keyed guest drops onto the base:
405
+
406
+ ```typescript
407
+ session.setSourceComposite("guest", 1, 2, [0, 1, 0], 0.4); // chroma-key green
408
+ ```
409
+
410
+ `defineScreenShare` registers a source whose frame letterboxes to fit its cell,
411
+ `removeSource` drops one, and `clearLayout` returns to the camera alone.
412
+
413
+ ## Lives and calls
414
+
415
+ The web is the easy case: the rendered canvas is already a live video source.
416
+ `canvas.captureStream()` hands you a `MediaStreamTrack` of the composited,
417
+ lens-baked output with no readback and no copy - publish it straight to LiveKit or
418
+ any WebRTC peer:
419
+
420
+ ```typescript
421
+ const track = canvas.captureStream(30).getVideoTracks()[0];
422
+ // publish `track` through your LiveKit room or RTCPeerConnection
423
+ ```
424
+
425
+ Keep the render loop running (`renderFrame` per frame) and the track carries every
426
+ composited frame. Reach for `captureLiveFrame` only when you need the raw pixels
427
+ (BGRA by default) rather than a track. For audio, `mixOutputAudio` folds the
428
+ lens's own sound into the mic block you are about to publish and returns the mixed
429
+ interleaved s16 for your outgoing WebRTC audio track: it resamples the lens sound
430
+ to your track's rate and sums it in, so there is nothing to hand-mix (pass `null`
431
+ for the mic to send the lens sound over silence). `pullAudio` still pulls the lens
432
+ sound alone for local WebAudio playback with no call in progress; `GossAudioOutput`
433
+ wraps that playback (an `AudioWorklet` it owns, `start()` from a gesture, `pump()`
434
+ each frame beside `tickLens`). `GossMicInput` captures the microphone into
435
+ `submitAudio` so level and beat triggers fire in the browser, and
436
+ `GossVideoTexture` plays an MP4 through the browser's decoder into a named
437
+ source a lens composites.
438
+
439
+ When the lens carries an `audio.infer` node with a caption binding, the engine
440
+ runs on-device ASR over the mic and `captionText` reads the decoded text by the
441
+ node's id, for the page to draw as a live subtitle:
442
+
443
+ ```typescript
444
+ const line = session.captionText("caption");
445
+ if (line) subtitleEl.textContent = line;
446
+ ```
447
+
448
+ ## Method names
449
+
450
+ The operation names match the other SDKs: `GossEngine.create(gosslens)`,
451
+ `GossSession.create(engine)`, `submitFrameRgbaCopy`, `renderFrame`. The full table
452
+ is in [API.md](../../docs/API.md).
81
453
 
82
454
  ## Demo app
83
455
 
@@ -89,8 +461,3 @@ cross-platform SDK overview is in the [root README](../../README.md).
89
461
  parsers and the WebGPU/WebGL2 pick. The browser end-to-end proofs live in
90
462
  [`demo/prove.ts`](demo/prove.ts) and [`demo/track-prove.ts`](demo/track-prove.ts),
91
463
  with the host conformance in [`harness/`](../../harness/).
92
-
93
- ## TODO
94
-
95
- - Publish `@myzonerocks/gosslens` to npm; until then `bun add` resolves
96
- only inside the workspace.
@@ -0,0 +1,12 @@
1
+ import type { GossSession } from "./index";
2
+ export declare class GossAudioOutput {
3
+ static readonly sampleRate = 48000;
4
+ private session;
5
+ private context;
6
+ private node;
7
+ constructor(session: GossSession);
8
+ start(): Promise<void>;
9
+ pump(frames?: number): void;
10
+ stop(): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=audio-output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-output.d.ts","sourceRoot":"","sources":["../../src/audio-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgD3C,qBAAa,eAAe;IAE1B,MAAM,CAAC,QAAQ,CAAC,UAAU,SAAU;IAEpC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,IAAI,CAAiC;IAE7C,YAAY,OAAO,EAAE,WAAW,EAE/B;IAIK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB3B;IAMD,IAAI,CAAC,MAAM,SAAM,GAAG,IAAI,CAKvB;IAGK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
@@ -0,0 +1,99 @@
1
+ /// The worklet drains chunks the page posts it, so the audio thread
2
+ /// never touches the wasm heap and the engine's graph-thread pull
3
+ /// contract holds; an underrun renders silence.
4
+ const processorSource = `
5
+ class GossLensAudioProcessor extends AudioWorkletProcessor {
6
+ constructor() {
7
+ super();
8
+ this.queue = [];
9
+ this.offset = 0;
10
+ this.queued = 0;
11
+ this.port.onmessage = (event) => {
12
+ this.queue.push(event.data);
13
+ this.queued += event.data.length;
14
+ while (this.queue.length > 1 && this.queued - this.queue[0].length + this.offset > 16384) {
15
+ this.queued -= this.queue[0].length;
16
+ this.queue.shift();
17
+ this.offset = 0;
18
+ }
19
+ };
20
+ }
21
+ process(_inputs, outputs) {
22
+ const out = outputs[0][0];
23
+ if (!out) return true;
24
+ for (let i = 0; i < out.length; i += 1) {
25
+ const head = this.queue[0];
26
+ if (!head) {
27
+ out[i] = 0;
28
+ continue;
29
+ }
30
+ out[i] = head[this.offset] / 32768;
31
+ this.offset += 1;
32
+ if (this.offset >= head.length) {
33
+ this.queued -= head.length;
34
+ this.queue.shift();
35
+ this.offset = 0;
36
+ }
37
+ }
38
+ return true;
39
+ }
40
+ }
41
+ registerProcessor("goss-lens-audio", GossLensAudioProcessor);
42
+ `;
43
+ /// Routes the lens mixer to the page's speakers. The engine pull is
44
+ /// graph-thread only, so pump() runs in the frame loop on the main
45
+ /// thread and posts each block to an AudioWorklet that plays it.
46
+ export class GossAudioOutput {
47
+ /// The lens mixer's fixed output format.
48
+ static sampleRate = 48_000;
49
+ session;
50
+ context = null;
51
+ node = null;
52
+ constructor(session) {
53
+ this.session = session;
54
+ }
55
+ /// Stands the worklet up; browsers require a user gesture before an
56
+ /// AudioContext runs, so call this from one and await it.
57
+ async start() {
58
+ if (this.context)
59
+ return;
60
+ const context = new AudioContext({ sampleRate: GossAudioOutput.sampleRate });
61
+ const moduleUrl = URL.createObjectURL(new Blob([processorSource], { type: "text/javascript" }));
62
+ try {
63
+ await context.audioWorklet.addModule(moduleUrl);
64
+ }
65
+ finally {
66
+ URL.revokeObjectURL(moduleUrl);
67
+ }
68
+ const node = new AudioWorkletNode(context, "goss-lens-audio", {
69
+ numberOfInputs: 0,
70
+ numberOfOutputs: 1,
71
+ outputChannelCount: [1],
72
+ });
73
+ node.connect(context.destination);
74
+ await context.resume();
75
+ this.context = context;
76
+ this.node = node;
77
+ }
78
+ /// Pulls the next mixer block and hands it to the worklet. Call once
79
+ /// per frame from the same loop that ticks the lens. The post
80
+ /// transfers the block's buffer, which detaches it, so each pump is
81
+ /// one fresh small block by design rather than a reusable buffer.
82
+ pump(frames = 800) {
83
+ const node = this.node;
84
+ if (!node)
85
+ return;
86
+ const block = this.session.pullAudio(frames);
87
+ node.port.postMessage(block, [block.buffer]);
88
+ }
89
+ /// Tears the worklet and context down.
90
+ async stop() {
91
+ this.node?.disconnect();
92
+ this.node = null;
93
+ const context = this.context;
94
+ this.context = null;
95
+ if (context)
96
+ await context.close();
97
+ }
98
+ }
99
+ //# sourceMappingURL=audio-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-output.js","sourceRoot":"","sources":["../../src/audio-output.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,kEAAkE;AAClE,gDAAgD;AAChD,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCvB,CAAC;AAEF,oEAAoE;AACpE,mEAAmE;AACnE,iEAAiE;AACjE,MAAM,OAAO,eAAe;IAC1B,yCAAyC;IACzC,MAAM,CAAU,UAAU,GAAG,MAAM,CAAC;IAE5B,OAAO,CAAc;IACrB,OAAO,GAAwB,IAAI,CAAC;IACpC,IAAI,GAA4B,IAAI,CAAC;IAE7C,YAAY,OAAoB;QAC9B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,oEAAoE;IACpE,0DAA0D;IAC1D,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,eAAe,CAAC,UAAU,EAAE,CAAC,CAAC;QAC7E,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,eAAe,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAClD,CAAC;gBAAS,CAAC;YACT,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,EAAE;YAC5D,cAAc,EAAE,CAAC;YACjB,eAAe,EAAE,CAAC;YAClB,kBAAkB,EAAE,CAAC,CAAC,CAAC;SACxB,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAClC,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,qEAAqE;IACrE,8DAA8D;IAC9D,oEAAoE;IACpE,kEAAkE;IAClE,IAAI,CAAC,MAAM,GAAG,GAAG;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,uCAAuC;IACvC,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,OAAO;YAAE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACrC,CAAC;CACF"}
@@ -526,4 +526,7 @@ export declare class GossPreviewSession {
526
526
  }
527
527
  export { GossWebXRWorldSource } from "./world";
528
528
  export type { GossXRFrameLike } from "./world";
529
+ export { GossAudioOutput } from "./audio-output";
530
+ export { GossMicInput } from "./mic-input";
531
+ export { GossVideoTexture } from "./video-texture";
529
532
  //# sourceMappingURL=index.d.ts.map