@voqalize/avatar 0.1.0 → 0.2.1

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.
Files changed (46) hide show
  1. package/README.md +86 -51
  2. package/client/dist/Avatar.d.ts +17 -14
  3. package/client/dist/Avatar.d.ts.map +1 -1
  4. package/client/dist/Avatar.js +3 -3
  5. package/client/dist/Avatar.js.map +1 -1
  6. package/client/dist/AvatarClient.d.ts +23 -57
  7. package/client/dist/AvatarClient.d.ts.map +1 -1
  8. package/client/dist/AvatarClient.js +20 -71
  9. package/client/dist/AvatarClient.js.map +1 -1
  10. package/client/dist/index.d.ts +22 -0
  11. package/client/dist/index.d.ts.map +1 -0
  12. package/client/dist/index.js +22 -0
  13. package/client/dist/index.js.map +1 -0
  14. package/client/dist/types.d.ts +22 -37
  15. package/client/dist/types.d.ts.map +1 -1
  16. package/client/dist/types.js +12 -12
  17. package/client/dist/types.js.map +1 -1
  18. package/client/dist/useAvatar.d.ts +8 -19
  19. package/client/dist/useAvatar.d.ts.map +1 -1
  20. package/client/dist/useAvatar.js +15 -32
  21. package/client/dist/useAvatar.js.map +1 -1
  22. package/client/src/Avatar.tsx +19 -24
  23. package/client/src/AvatarClient.ts +38 -111
  24. package/client/src/index.ts +22 -0
  25. package/client/src/types.ts +24 -47
  26. package/client/src/useAvatar.ts +19 -47
  27. package/docs/contract-avatar.md +37 -3
  28. package/docs/contract-protocol.md +90 -30
  29. package/package.json +3 -11
  30. package/src/avatar.d.ts +38 -6
  31. package/src/avatar.js +72 -37
  32. package/src/hand.js +680 -0
  33. package/src/idle.js +8 -25
  34. package/src/line-art.js +26 -0
  35. package/src/perform.js +8 -4
  36. package/client/dist/pipecat.d.ts +0 -21
  37. package/client/dist/pipecat.d.ts.map +0 -1
  38. package/client/dist/pipecat.js +0 -21
  39. package/client/dist/pipecat.js.map +0 -1
  40. package/client/dist/react.d.ts +0 -16
  41. package/client/dist/react.d.ts.map +0 -1
  42. package/client/dist/react.js +0 -17
  43. package/client/dist/react.js.map +0 -1
  44. package/client/src/pipecat.ts +0 -38
  45. package/client/src/react.ts +0 -34
  46. package/src/audio-fallback.js +0 -100
package/README.md CHANGED
@@ -5,8 +5,8 @@ any face and go by any name — the demos call theirs Kiran. The server owns
5
5
  meaning (what state the agent is in, what it's saying, where it's looking); the
6
6
  client owns motion (how a face actually moves when it means those things).
7
7
 
8
- No build step, no runtime dependencies, ~200KB of ES modules — about two thirds
9
- of that is the three face rigs, and a host that ships one face pays for one.
8
+ No build step, no runtime dependencies, ~270KB of ES modules — a third of that
9
+ is the three face rigs, and a host that ships one face pays for one.
10
10
 
11
11
  The two load-bearing interfaces are specified in
12
12
  [docs/contract-protocol.md](docs/contract-protocol.md) (server ↔ widget) and
@@ -17,17 +17,30 @@ npm install @voqalize/avatar # the browser half
17
17
  pip install voqalize-avatar # the pipecat half
18
18
  ```
19
19
 
20
- ```js
21
- import { createAvatar } from '@voqalize/avatar';
20
+ ```jsx
21
+ import { Avatar } from '@voqalize/avatar';
22
+
23
+ <Avatar client={pipecatClient} className="call-tile" />
24
+ ```
22
25
 
23
- const avatar = createAvatar({ mount: '#avatar' });
26
+ ```python
27
+ from voqalize_avatar import AvatarProcessor
24
28
 
25
- avatar.setState('LISTENING', { emotion: 'warm' });
26
- avatar.interject('MM_HMM');
27
- avatar.setGaze('SCREEN_WORK');
28
- avatar.speak({ audio: audioEl, cues }); // cues: [{t: 0, v: 'D'}, ...]
29
+ pipeline = Pipeline([..., tts, AvatarProcessor(), transport.output()])
29
30
  ```
30
31
 
32
+ That is the integration, both halves of it. The processor infers the agent's
33
+ state from the frames already flowing past it and streams viseme letters synced
34
+ to the audio; the component renders a face that listens, thinks, claims the
35
+ floor, speaks and yields it. Nothing to configure, no binaries to install.
36
+
37
+ Under that one component is a ~30-channel parameter mixer with a full driving
38
+ API (`setState`, `interject`, `gesture`, `speak`, `perform`, …). It is not an
39
+ npm entrypoint — one component is the whole public surface, deliberately, and
40
+ [docs/removed.md](docs/removed.md) says what that cost and how to undo it. The
41
+ rest of this README documents that machinery, because it is what you read when
42
+ you are authoring a face or debugging a mouth.
43
+
31
44
  ## What's in the box
32
45
 
33
46
  The repo is one system in three pieces, because that is how it gets consumed —
@@ -37,37 +50,37 @@ why this is a library rather than a product.
37
50
 
38
51
  | piece | where | what it is |
39
52
  |---|---|---|
40
- | the widget | `src/` → `@voqalize/avatar` | the face. Dependency-free ES modules, no build step, mounts anywhere |
41
- | the client | `client/` → `@voqalize/avatar/{pipecat,react}` | the dispatcher: turn clock, cue splice, React mount |
53
+ | the widget | `src/` | the face. Dependency-free ES modules, no build step, mounts anywhere. Internal since 0.2 |
54
+ | the client | `client/` → `@voqalize/avatar` | the dispatcher (turn clock, cue splice) and the `<Avatar>` component over it |
42
55
  | the backend | `py/` → `voqalize-avatar` (PyPI) | a pipecat `FrameProcessor` that infers state from stock frames and streams visemes |
43
56
  | the aligner | `native/avatarsync/` | the Rhubarb Lip Sync fork the backend drives — A–H letters from text *and* from audio |
44
57
 
45
- Browser side, three entry points:
46
-
47
- ```js
48
- import { createAvatar, AVATAR_NAMES } from '@voqalize/avatar'; // no deps
49
- import { AvatarClient } from '@voqalize/avatar/pipecat'; // + pipecat client
50
- import { Avatar, useAvatar } from '@voqalize/avatar/react'; // + React >= 18
51
- ```
52
-
53
- The root export is the widget itself and pulls in nothing. `/pipecat` adds
54
- `AvatarClient`, which anchors the turn clock and splices cue tracks; it needs
55
- `@pipecat-ai/client-js` only if you call `attach()`. `/react` adds a mount
56
- lifecycle and nothing else:
58
+ Browser side, one entry point:
57
59
 
58
60
  ```jsx
59
- <Avatar client={pipecatClient} className="call-tile" />
61
+ import { Avatar } from '@voqalize/avatar'; // peers: react >= 18, @pipecat-ai/client-js
62
+
63
+ <Avatar client={pipecatClient} avatar="peep" className="call-tile" />
60
64
  ```
61
65
 
66
+ `client` is the live `PipecatClient` (or `null` before connect); `avatar` picks
67
+ a face and is read once, at mount. Everything else is forwarded to the mount
68
+ `<div>`, so it sizes and styles like the tile it lives in. There is nothing to
69
+ configure because the server already says all of it.
70
+
62
71
  Server side, the whole integration is one processor between your TTS and your
63
72
  output transport — see `py/` and `docs/contract-protocol.md`:
64
73
 
65
74
  ```python
66
- from voqalize_avatar import AvatarProcessor, AvatarStateMachine
75
+ from voqalize_avatar import AvatarProcessor
67
76
 
68
- pipeline = Pipeline([..., tts, AvatarProcessor(state_machine=AvatarStateMachine()), transport.output()])
77
+ pipeline = Pipeline([..., tts, AvatarProcessor(), transport.output()])
69
78
  ```
70
79
 
80
+ It takes no arguments. `StartFrame` tells it the sample rate, the aligner rides
81
+ inside the wheel, and an application that needs to say something the pipeline
82
+ cannot infer subclasses `AvatarStateMachine` — see `py/README.md`.
83
+
71
84
  That much is inferred from stock pipecat frames, with no application code.
72
85
  States that depend on what your application is *doing* — a tool call that
73
86
  should read as *reviewing the screen* rather than *thinking* — are signalled
@@ -281,21 +294,11 @@ Rough recipe if you're rolling your own:
281
294
  Latency note: alignment needs the whole audio, so for streaming TTS run it per
282
295
  sentence chunk and `pushCues()` each chunk as it completes.
283
296
 
284
- ### Tier 3 — amplitude fallback (client-side, zero server work)
285
-
286
- Already implemented in `src/audio-fallback.js`. Attach any `MediaStream`,
287
- `HTMLMediaElement` or WebAudio node and it derives openness from RMS and a
288
- rough shape family from spectral tilt (sibilant → `B`, low-heavy → `F`/`E`,
289
- otherwise open vowels by level).
290
-
291
- ```js
292
- avatar.setAudioFallback(audioElement);
293
- ```
294
-
295
- It is not lip-sync — it's a mouth that moves plausibly with the voice. Use it
296
- when the viseme stream is unavailable or has fallen behind. The mixer engages it
297
- only when nothing better is playing, so you can leave it attached permanently as
298
- a safety net.
297
+ There used to be a Tier 3: a client-side amplitude/spectral guesser
298
+ (`setAudioFallback`) for a server that sends no cues at all. It was removed in
299
+ 0.2 — there is no such server any more, and a second, lower-fidelity mouth
300
+ standing behind the real one mostly made a broken mouth harder to diagnose. See
301
+ [docs/removed.md](docs/removed.md) § Amplitude lipsync.
299
302
 
300
303
  ---
301
304
 
@@ -354,12 +357,11 @@ speak. The head comes *up*, not down — a lowered head reads as yielding.
354
357
 
355
358
  Backchannels only create rapport when they are *contingent* — a nod coupled to
356
359
  the speaker's pauses reads as understanding; the same nod on a random timer
357
- reads as distracting (the research is unambiguous on this). Give the widget the
358
- user's voice and the listening engine does the rest:
360
+ reads as distracting (the research is unambiguous on this). Tell the widget when
361
+ the user holds the floor and the listening engine does the rest:
359
362
 
360
363
  ```js
361
- avatar.setUserAudio(micStreamOrElement); // internal RMS VAD; null to detach
362
- avatar.setUserSpeaking(true / false); // or run your own VAD; this wins
364
+ avatar.setUserSpeaking(true / false); // the server endpointer's own signal
363
365
  avatar.on('backchannel', (id) => log(id)); // every autonomous ack, announced
364
366
  ```
365
367
 
@@ -367,7 +369,9 @@ While the user holds the floor the avatar leans in a touch; at pause onsets it
367
369
  acknowledges within ~250–600ms (probability, refractory gap and nod choice all
368
370
  tuned from listening-corpus numbers — long user utterances earn the bigger
369
371
  nods). With no signal attached, the old plausible random cadence remains as the
370
- fallback. The server can always `interject()` explicitly; autonomous acks
372
+ fallback. (The widget could once derive this itself with its own VAD over the
373
+ mic stream; that raced the server's endpointer and went in 0.2 —
374
+ [docs/removed.md](docs/removed.md) § Client-side VAD.) The server can always `interject()` explicitly; autonomous acks
371
375
  suppress themselves around it.
372
376
 
373
377
  ### Action timelines
@@ -571,6 +575,36 @@ attachAudio('OKAY', '/audio/agent-okay.mp3');
571
575
  Clips ramp in over 70ms and out over 150ms, are interruptible, and a repeat of
572
576
  the clip already playing collapses rather than stacking.
573
577
 
578
+ ### Hand gestures
579
+
580
+ `gesture(id)` — `HI`, `BYE`, `THUMBS_UP`, `ONE_MOMENT`. A hand rises into the
581
+ bottom of the frame, and the matching interjection above plays with it, because
582
+ a hand that arrives while the head sits perfectly still is not attached to
583
+ anybody.
584
+
585
+ There is still no arm. The hand enters the way a webcam sees one — only fingers
586
+ and palm ever clear the edge, the wrist never does — which is what makes it a
587
+ different proposition from the articulated forearm chain this project removed.
588
+ It is not part of the rig either: no parameter channel, no per-face geometry.
589
+ Every avatar gets it from its own `viewBox` and theme, and a face that never
590
+ plays a gesture renders exactly what it rendered before.
591
+
592
+ Two guarantees worth stating, because they are the reasons it could ship at
593
+ all: **nothing but a single digit ever passes the mouth** (lipsync is the
594
+ headline feature — a gesture is free to fire mid-speech), and the hand never
595
+ leaves the frame sideways. `checkHandFraming(meta)` asserts both against the
596
+ real timelines for every registered avatar, in `sweep()`.
597
+
598
+ ```js
599
+ avatar.gesture('HI');
600
+ avatar.setHandSide(-1); // which side it enters from; +1 is the default
601
+ avatar.gesturing; // the id in flight, or null
602
+ createAvatar({ mount, hand: false }); // no hand; gesture() plays the face half alone
603
+ ```
604
+
605
+ `gesture` is a separate verb from `interject` on the wire too — `interject('WAVE')`
606
+ is still the face alone, so a server that upgrades gets no hand until it asks.
607
+
574
608
  ### Misc
575
609
 
576
610
  ```js
@@ -578,11 +612,10 @@ avatar.blink(true); // true = double blink
578
612
  avatar.setMouthGain(1.2); // articulation: scales the viseme shapes as authored
579
613
  avatar.setGestureGain(0.8); // scales every clip delta
580
614
  avatar.setMotionGain(0.8); // scales the whole idle/body liveness layer
581
- avatar.setAudioFallback(el); // null to detach
582
615
  avatar.setOverrides({ ... }); // direct param injection, for tuning UIs
583
616
  avatar.setOverrides(null);
584
617
  avatar.params; // live smoothed parameter vector (read-only)
585
- avatar.state / .emotion / .gaze / .speaking / .clip / .performing / .audioLevel
618
+ avatar.state / .emotion / .gaze / .speaking / .clip / .gesturing / .performing
586
619
  avatar.mouthGain / .gestureGain / .motionGain / .svg / .meta
587
620
  avatar.destroy();
588
621
  ```
@@ -592,7 +625,7 @@ are properties, not methods. (`meta` is the mounted avatar's descriptor — the
592
625
  call demo sizes its tile from `meta.viewBox`.)
593
626
 
594
627
  Events: `state` (new state name), `speakEnd`, `clipEnd` (clip id),
595
- `backchannel` (autonomous ack id), `performEnd`.
628
+ `backchannel` (autonomous ack id), `gestureEnd` (hand gesture id), `performEnd`.
596
629
 
597
630
  ---
598
631
 
@@ -613,16 +646,18 @@ Events: `state` (new state name), `speakEnd`, `clipEnd` (clip id),
613
646
  | `src/idle.js` | per-state liveness profiles, the `ListeningEngine`, autonomous backchannel |
614
647
  | `src/clips.js` | keyframe player for gesture timelines |
615
648
  | `src/interjections.js` | the 26 clips |
649
+ | `src/hand.js` | the frame-edge hand: four gestures, placed from `META.viewBox` |
616
650
  | `src/perform.js` | the action-timeline player behind `perform()` |
617
- | `src/audio-fallback.js` | WebAudio amplitude/spectral lipsync |
618
- | `src/avatar.js` | public API, the per-frame mixer, and the `AVATARS` registry |
651
+ | `src/avatar.js` | the driving API, the per-frame mixer, and the `AVATARS` registry |
619
652
  | `client/src/AvatarClient.ts` | the dispatcher: turn-clock anchoring and the cue splice, framework-free |
620
653
  | `client/src/useAvatar.ts` `client/src/Avatar.tsx` | the React binding — a mount lifecycle over `AvatarClient` |
654
+ | `client/src/index.ts` | the one export: `<Avatar>` |
621
655
  | `client/src/types.ts` | the wire vocabulary in TypeScript. Kept in step with `contract-protocol.md` and `messages.py` |
622
656
  | `py/src/voqalize_avatar/` | the pipecat backend: state machine, processor, viseme engine, `avatarsync` pool |
623
657
  | `native/avatarsync/` | the Rhubarb Lip Sync fork — text leg and audio leg — plus its patch and build script |
624
658
  | `docs/contract-protocol.md` `docs/contract-avatar.md` | the two binding interface contracts |
625
659
  | `docs/design-library-split.md` | why this is a library, and what each published artifact owns |
660
+ | `docs/removed.md` | what 0.2 deleted from the public surface, why, and how to get any of it back |
626
661
  | `docs/research-biomechanics.md` | the citations behind the motion constants |
627
662
  | `tools/` | headless render / sweep / pixel-diff CLI (dev-only dependencies) |
628
663
  | `serve.py` | the dev server — `Cache-Control: no-store`. Use this one |
@@ -1,24 +1,27 @@
1
1
  /**
2
- * Avatar — a call-tile-ready wrapper around the widget.
3
- *
4
- * Thin by design: `useAvatar` does the work; this is the div it mounts into
5
- * plus prop plumbing.
2
+ * Avatar — a call-tile-ready wrapper around the widget, and the package's
3
+ * whole public surface.
6
4
  *
7
5
  * <Avatar client={session.client} className="avatar-tile" />
8
6
  *
7
+ * Two props of its own; everything else is forwarded to the mount `<div>`, so
8
+ * it sizes and styles like the tile it lives in. There is nothing to configure
9
+ * because there is nothing the server does not already say: the
10
+ * `AvatarProcessor` in the pipeline drives the state, the gaze and the mouth.
11
+ *
9
12
  * The widget has no hot-swap-avatar API — `createFace` runs once per mount —
10
- * so `avatar`/`theme`/`mouthGain`/`gestureGain` are read once, at mount. To
11
- * switch avatars at runtime, remount with a `key` prop:
13
+ * so `avatar` is read once, at mount. To switch faces at runtime, remount with
14
+ * a `key` prop:
12
15
  *
13
16
  * <Avatar key={name} avatar={name} client={session.client} />
14
17
  */
15
- import type { CSSProperties } from "react";
16
- import type { UseAvatarOptions } from "./useAvatar.js";
17
- export interface AvatarProps extends UseAvatarOptions {
18
- className?: string;
19
- style?: CSSProperties;
20
- /** Forwarded to the mount `<div>`. */
21
- "aria-label"?: string;
18
+ import type { HTMLAttributes } from "react";
19
+ import type { PipecatClient } from "@pipecat-ai/client-js";
20
+ export interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
21
+ /** The live `PipecatClient`, or `null` before connect. */
22
+ client?: PipecatClient | null;
23
+ /** Which face. Omit for the default. Read at mount only — see above. */
24
+ avatar?: string;
22
25
  }
23
- export declare function Avatar({ className, style, "aria-label": ariaLabel, ...options }: AvatarProps): import("react").JSX.Element;
26
+ export declare function Avatar({ client, avatar, ...rest }: AvatarProps): import("react").JSX.Element;
24
27
  //# sourceMappingURL=Avatar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,MAAM,WAAW,WAAY,SAAQ,gBAAgB;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,EAAE,WAAW,+BAW5F"}
1
+ {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,MAAM,WAAW,WAAY,SAAQ,cAAc,CAAC,cAAc,CAAC;IACjE,0DAA0D;IAC1D,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,+BAG9D"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useAvatar } from "./useAvatar.js";
3
- export function Avatar({ className, style, "aria-label": ariaLabel, ...options }) {
4
- const { containerRef } = useAvatar(options);
5
- return (_jsx("div", { ref: containerRef, className: className, style: style, "aria-label": ariaLabel ?? "avatar", role: "img" }));
3
+ export function Avatar({ client, avatar, ...rest }) {
4
+ const { containerRef } = useAvatar({ client, avatar });
5
+ return _jsx("div", { role: "img", "aria-label": "avatar", ...rest, ref: containerRef });
6
6
  }
7
7
  //# sourceMappingURL=Avatar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.js","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":";AAiBA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAS3C,MAAM,UAAU,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,OAAO,EAAe;IAC3F,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CACL,cACE,GAAG,EAAE,YAAY,EACjB,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,gBACA,SAAS,IAAI,QAAQ,EACjC,IAAI,EAAC,KAAK,GACV,CACH,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"Avatar.js","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":";AAoBA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAS3C,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAe;IAC7D,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,OAAO,cAAK,IAAI,EAAC,KAAK,gBAAY,QAAQ,KAAK,IAAI,EAAE,GAAG,EAAE,YAAY,GAAI,CAAC;AAC7E,CAAC"}
@@ -32,10 +32,10 @@
32
32
  * it would eat into the intentional video-first safety margin rather than
33
33
  * improve it. Left as a documented option, not built.
34
34
  *
35
- * `attach()` still subscribes to both pipecat events, but only to report a
36
- * **diagnostic** drift (`onSpeakingDrift`) between our anchor and pipecat's —
37
- * useful for noticing in logs if the two ever separate by more than jitter,
38
- * never used to move `t0` itself.
35
+ * `attach()` therefore subscribes to exactly one pipecat event,
36
+ * `serverMessage`. It used to also subscribe to both speaking events to report
37
+ * a diagnostic drift between our anchor and pipecat's; that hook is gone with
38
+ * the rest of the observability surface (`docs/removed.md` § Client callbacks).
39
39
  *
40
40
  * ## Cue splice
41
41
  *
@@ -70,48 +70,28 @@
70
70
  */
71
71
  import type { PipecatClient } from "@pipecat-ai/client-js";
72
72
  import type { AvatarApi } from "../../src/avatar.js";
73
- import { type AvatarCommand, type AvatarCue, type AvatarHintCmd, type AvatarUnknownCmd } from "./types.js";
73
+ import { type AvatarCommand, type AvatarCue } from "./types.js";
74
+ /**
75
+ * Internal. Not exported from the package — see `index.ts` for the public
76
+ * surface, which is `<Avatar>` and nothing else.
77
+ *
78
+ * There is deliberately no `accept` predicate here any more. Avatar commands
79
+ * travel in one envelope, `{type:"avatar"}`, in both directions and from every
80
+ * source: a `AvatarProcessor` in the pipeline and a brain driving the face
81
+ * out of band emit the same shape. A per-deployment predicate meant the
82
+ * library could not state what an avatar message *is*, which is the one thing
83
+ * a wire format has to be able to say. See docs/removed.md § The accept
84
+ * predicate.
85
+ */
74
86
  export interface AvatarClientOptions {
75
- /** `{cmd:"hint"}` is a no-op hook today — the widget's listening engine
76
- * already handles acks; a host may still want to know a hint arrived. */
77
- onHint?: (kind: string, msg: AvatarHintCmd) => void;
78
- /** An unrecognized `cmd` (forward compat) — the protocol says ignore
79
- * silently, so this is purely an observability hook, not required. */
80
- onUnknownCmd?: (msg: AvatarUnknownCmd) => void;
81
87
  /** A dispatch threw (e.g. an unknown state or interjection id, which the
82
88
  * widget throws on). Defaults to `console.warn`. */
83
89
  onError?: (err: unknown, msg: AvatarCommand) => void;
84
- /** Diagnostic only (see the class doc's "Turn clock anchoring" section) —
85
- * never moves the anchor, just reports how far pipecat's own
86
- * botStartedSpeaking/botStoppedSpeaking landed from it. */
87
- onSpeakingDrift?: (info: {
88
- event: "start" | "stop";
89
- ctx: string | null;
90
- driftMs: number;
91
- }) => void;
92
- /**
93
- * Which server-messages `attach()` should look inside. Defaults to the
94
- * protocol's own envelope, `type === "avatar"`.
95
- *
96
- * The escape hatch exists because an application may tunnel avatar commands
97
- * inside a message type of its own — one deployment routes them through a
98
- * generic `ui_command` envelope so an LLM tool call can drive the face — and
99
- * teaching this library that envelope would be teaching it one consumer's
100
- * private vocabulary. Widen it here instead:
101
- *
102
- * accept: (m) => m.type === "avatar" ||
103
- * (m.type === "ui_command" && m.action === "avatar")
104
- *
105
- * The predicate only decides *whether to look*; the payload still has to
106
- * carry a string `cmd` to dispatch at all.
107
- */
108
- accept?: (message: Record<string, unknown>) => boolean;
109
90
  /** Override for tests. Defaults to `performance.now`. */
110
91
  now?: () => number;
111
92
  }
112
93
  /**
113
- * The three `RTVIEvent` members `attach()` subscribes to, spelled as their
114
- * values.
94
+ * The one `RTVIEvent` member `attach()` subscribes to, spelled as its value.
115
95
  *
116
96
  * Written out rather than imported because that enum was this module's *only*
117
97
  * runtime reference to `@pipecat-ai/client-js`, and one runtime reference makes
@@ -127,43 +107,29 @@ export interface AvatarClientOptions {
127
107
  */
128
108
  export declare const RTVI_EVENTS: {
129
109
  readonly serverMessage: "serverMessage";
130
- readonly botStartedSpeaking: "botStartedSpeaking";
131
- readonly botStoppedSpeaking: "botStoppedSpeaking";
132
110
  };
133
111
  export declare class AvatarClient {
134
112
  private readonly avatar;
135
113
  private readonly opts;
136
114
  private readonly now;
137
- private readonly accept;
138
115
  private turn;
139
116
  constructor(avatar: AvatarApi, opts?: AvatarClientOptions);
140
117
  /** The active turn's ctx, or `null` between turns. For tests and telemetry. */
141
118
  get turnCtx(): string | null;
142
119
  /** The active turn's canonical (already-spliced) cue track. For tests and telemetry. */
143
120
  get turnCues(): AvatarCue[];
144
- /** Dispatch one avatar command. Accepts anything with a string `cmd` — an
145
- * already-unwrapped `{type:"avatar", cmd, ...}` server message, or a bare
146
- * `{cmd, ...}` payload from whatever else the host is carrying them in.
147
- * Unknown `cmd`s are ignored, per the wire protocol's forward-compat rule. */
121
+ /** Dispatch one server message. Anything that isn't in the avatar envelope
122
+ * is not ours and is ignored; so is an envelope carrying a `cmd` this build
123
+ * has never heard of, per the wire protocol's forward-compat rule. */
148
124
  dispatch(raw: unknown): void;
149
125
  private handleState;
150
- private handlePerform;
151
- /** Ride the named turn's clock if it's the one we're currently anchored to;
152
- * otherwise (no active turn, or `perform` names a ctx we never saw a
153
- * `speech start` for) fall back to a fresh clock anchored at this call — the
154
- * same "elapsed ms since this call" default `avatar.perform()` itself uses
155
- * when given no clock and no audio. */
156
- private resolveClock;
157
126
  private ensureTurn;
158
127
  private handleCues;
159
128
  private handleSpeech;
160
- private reportDrift;
161
129
  /**
162
130
  * Subscribe to a live `PipecatClient`'s server messages and dispatch the
163
- * avatar commands among them. Which messages count is the `accept` option;
164
- * by default, the protocol's own `{type:"avatar"}` envelope.
165
- *
166
- * Also wires the diagnostic drift cross-check described in the class doc.
131
+ * avatar commands among them — the ones in the protocol's own
132
+ * `{type:"avatar"}` envelope, which `isAvatarMessage` is the definition of.
167
133
  * Never throws on a malformed or irrelevant message.
168
134
  *
169
135
  * @returns an unsubscribe function; call it on unmount or disconnect.
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarClient.d.ts","sourceRoot":"","sources":["../src/AvatarClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,SAAS,EAEd,KAAK,aAAa,EAIlB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAYpB,MAAM,WAAW,mBAAmB;IAClC;6EACyE;IACzE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACpD;0EACsE;IACtE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC/C;wDACoD;IACpD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACrD;;+DAE2D;IAC3D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnG;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IACvD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;CAImB,CAAC;AAU5C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgD;IACvE,OAAO,CAAC,IAAI,CAAqB;gBAErB,MAAM,EAAE,SAAS,EAAE,IAAI,GAAE,mBAAwB;IAO7D,+EAA+E;IAC/E,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,wFAAwF;IACxF,IAAI,QAAQ,IAAI,SAAS,EAAE,CAE1B;IAED;;;kFAG8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAsC5B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,aAAa;IAIrB;;;;2CAIuC;IACvC,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,WAAW;IAOnB;;;;;;;;;OASG;IACH,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,IAAI;CAuB1C"}
1
+ {"version":3,"file":"AvatarClient.d.ts","sourceRoot":"","sources":["../src/AvatarClient.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,SAAS,EAIf,MAAM,YAAY,CAAC;AAYpB;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,mBAAmB;IAClC;wDACoD;IACpD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACrD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW;;CAEmB,CAAC;AAU5C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,IAAI,CAAqB;gBAErB,MAAM,EAAE,SAAS,EAAE,IAAI,GAAE,mBAAwB;IAM7D,+EAA+E;IAC/E,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,wFAAwF;IACxF,IAAI,QAAQ,IAAI,SAAS,EAAE,CAE1B;IAED;;0EAEsE;IACtE,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAkC5B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,YAAY;IAkBpB;;;;;;;OAOG;IACH,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,IAAI;CAM1C"}
@@ -32,10 +32,10 @@
32
32
  * it would eat into the intentional video-first safety margin rather than
33
33
  * improve it. Left as a documented option, not built.
34
34
  *
35
- * `attach()` still subscribes to both pipecat events, but only to report a
36
- * **diagnostic** drift (`onSpeakingDrift`) between our anchor and pipecat's —
37
- * useful for noticing in logs if the two ever separate by more than jitter,
38
- * never used to move `t0` itself.
35
+ * `attach()` therefore subscribes to exactly one pipecat event,
36
+ * `serverMessage`. It used to also subscribe to both speaking events to report
37
+ * a diagnostic drift between our anchor and pipecat's; that hook is gone with
38
+ * the rest of the observability surface (`docs/removed.md` § Client callbacks).
39
39
  *
40
40
  * ## Cue splice
41
41
  *
@@ -68,10 +68,9 @@
68
68
  * chunk of a turn starts speak()" means the first *widget* call, not
69
69
  * necessarily the first *message*.
70
70
  */
71
- import { AVATAR_MESSAGE_TYPE, isAvatarMessage, } from "./types.js";
71
+ import { isAvatarMessage, } from "./types.js";
72
72
  /**
73
- * The three `RTVIEvent` members `attach()` subscribes to, spelled as their
74
- * values.
73
+ * The one `RTVIEvent` member `attach()` subscribes to, spelled as its value.
75
74
  *
76
75
  * Written out rather than imported because that enum was this module's *only*
77
76
  * runtime reference to `@pipecat-ai/client-js`, and one runtime reference makes
@@ -87,8 +86,6 @@ import { AVATAR_MESSAGE_TYPE, isAvatarMessage, } from "./types.js";
87
86
  */
88
87
  export const RTVI_EVENTS = {
89
88
  serverMessage: "serverMessage",
90
- botStartedSpeaking: "botStartedSpeaking",
91
- botStoppedSpeaking: "botStoppedSpeaking",
92
89
  };
93
90
  /** Defensive unwrap for the `RTVIEvent.ServerMessage` `{ data }` quirk: some
94
91
  * transports deliver the payload directly and some wrap it once more. */
@@ -101,13 +98,11 @@ export class AvatarClient {
101
98
  avatar;
102
99
  opts;
103
100
  now;
104
- accept;
105
101
  turn = null;
106
102
  constructor(avatar, opts = {}) {
107
103
  this.avatar = avatar;
108
104
  this.opts = opts;
109
105
  this.now = opts.now ?? (() => performance.now());
110
- this.accept = opts.accept ?? ((m) => m.type === AVATAR_MESSAGE_TYPE);
111
106
  }
112
107
  /** The active turn's ctx, or `null` between turns. For tests and telemetry. */
113
108
  get turnCtx() {
@@ -117,10 +112,9 @@ export class AvatarClient {
117
112
  get turnCues() {
118
113
  return this.turn ? [...this.turn.cues] : [];
119
114
  }
120
- /** Dispatch one avatar command. Accepts anything with a string `cmd` — an
121
- * already-unwrapped `{type:"avatar", cmd, ...}` server message, or a bare
122
- * `{cmd, ...}` payload from whatever else the host is carrying them in.
123
- * Unknown `cmd`s are ignored, per the wire protocol's forward-compat rule. */
115
+ /** Dispatch one server message. Anything that isn't in the avatar envelope
116
+ * is not ours and is ignored; so is an envelope carrying a `cmd` this build
117
+ * has never heard of, per the wire protocol's forward-compat rule. */
124
118
  dispatch(raw) {
125
119
  if (!isAvatarMessage(raw))
126
120
  return;
@@ -133,8 +127,8 @@ export class AvatarClient {
133
127
  case "interject":
134
128
  this.avatar.interject(msg.id);
135
129
  break;
136
- case "perform":
137
- this.handlePerform(msg);
130
+ case "gesture":
131
+ this.avatar.gesture(msg.id);
138
132
  break;
139
133
  case "cues":
140
134
  this.handleCues(msg);
@@ -145,14 +139,10 @@ export class AvatarClient {
145
139
  case "user":
146
140
  this.avatar.setUserSpeaking(msg.speaking);
147
141
  break;
148
- case "hint": {
149
- const hint = msg;
150
- this.opts.onHint?.(hint.kind, hint);
151
- break;
152
- }
153
- default:
154
- this.opts.onUnknownCmd?.(msg);
155
- break;
142
+ // No default: an unknown `cmd` is a newer server talking to an older
143
+ // widget, and the protocol's forward-compat rule says ignore it. There
144
+ // is no callback for it — a hook nobody could act on is observability,
145
+ // not an interface (`docs/removed.md` § Client callbacks).
156
146
  }
157
147
  }
158
148
  catch (err) {
@@ -171,28 +161,13 @@ export class AvatarClient {
171
161
  // particular message takes effect.
172
162
  this.avatar.setState(msg.name, { emotion: msg.emotion, gaze: msg.gaze });
173
163
  }
174
- handlePerform(msg) {
175
- this.avatar.perform(msg.actions, { clock: this.resolveClock(msg.ctx) });
176
- }
177
- /** Ride the named turn's clock if it's the one we're currently anchored to;
178
- * otherwise (no active turn, or `perform` names a ctx we never saw a
179
- * `speech start` for) fall back to a fresh clock anchored at this call — the
180
- * same "elapsed ms since this call" default `avatar.perform()` itself uses
181
- * when given no clock and no audio. */
182
- resolveClock(ctx) {
183
- if (ctx && this.turn && this.turn.ctx === ctx && this.turn.clock) {
184
- return this.turn.clock;
185
- }
186
- const start = this.now();
187
- return () => this.now() - start;
188
- }
189
164
  ensureTurn(ctx) {
190
165
  if (!this.turn || this.turn.ctx !== ctx) {
191
166
  // A different ctx supersedes whatever turn we had — a stale trailing
192
167
  // message for the old ctx will find `this.turn.ctx !== ctx` in
193
168
  // handleSpeech's stop-guard and be ignored, rather than cutting off the
194
169
  // new turn.
195
- this.turn = { ctx, cues: [], started: false, clock: null, t0: null };
170
+ this.turn = { ctx, cues: [], started: false, clock: null };
196
171
  }
197
172
  return this.turn;
198
173
  }
@@ -218,7 +193,6 @@ export class AvatarClient {
218
193
  const turn = this.ensureTurn(msg.ctx);
219
194
  const t0 = this.now();
220
195
  const clock = () => this.now() - t0;
221
- turn.t0 = t0;
222
196
  turn.clock = clock;
223
197
  turn.started = true;
224
198
  this.avatar.speak({ cues: turn.cues, clock });
@@ -231,44 +205,19 @@ export class AvatarClient {
231
205
  this.turn = null;
232
206
  }
233
207
  }
234
- reportDrift(event) {
235
- if (!this.opts.onSpeakingDrift)
236
- return;
237
- const t0 = this.turn?.t0;
238
- if (t0 == null)
239
- return;
240
- this.opts.onSpeakingDrift({ event, ctx: this.turn?.ctx ?? null, driftMs: this.now() - t0 });
241
- }
242
208
  /**
243
209
  * Subscribe to a live `PipecatClient`'s server messages and dispatch the
244
- * avatar commands among them. Which messages count is the `accept` option;
245
- * by default, the protocol's own `{type:"avatar"}` envelope.
246
- *
247
- * Also wires the diagnostic drift cross-check described in the class doc.
210
+ * avatar commands among them — the ones in the protocol's own
211
+ * `{type:"avatar"}` envelope, which `isAvatarMessage` is the definition of.
248
212
  * Never throws on a malformed or irrelevant message.
249
213
  *
250
214
  * @returns an unsubscribe function; call it on unmount or disconnect.
251
215
  */
252
216
  attach(client) {
253
- const onServerMessage = (raw) => {
254
- const message = unwrapServerMessage(raw);
255
- if (!this.accept(message))
256
- return;
257
- this.dispatch(message);
258
- };
259
- const onBotStartedSpeaking = () => this.reportDrift("start");
260
- const onBotStoppedSpeaking = () => this.reportDrift("stop");
217
+ const onServerMessage = (raw) => this.dispatch(unwrapServerMessage(raw));
261
218
  const serverMessage = RTVI_EVENTS.serverMessage;
262
- const started = RTVI_EVENTS.botStartedSpeaking;
263
- const stopped = RTVI_EVENTS.botStoppedSpeaking;
264
219
  client.on(serverMessage, onServerMessage);
265
- client.on(started, onBotStartedSpeaking);
266
- client.on(stopped, onBotStoppedSpeaking);
267
- return () => {
268
- client.off(serverMessage, onServerMessage);
269
- client.off(started, onBotStartedSpeaking);
270
- client.off(stopped, onBotStoppedSpeaking);
271
- };
220
+ return () => client.off(serverMessage, onServerMessage);
272
221
  }
273
222
  }
274
223
  //# sourceMappingURL=AvatarClient.js.map