@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.
- package/README.md +86 -51
- package/client/dist/Avatar.d.ts +17 -14
- package/client/dist/Avatar.d.ts.map +1 -1
- package/client/dist/Avatar.js +3 -3
- package/client/dist/Avatar.js.map +1 -1
- package/client/dist/AvatarClient.d.ts +23 -57
- package/client/dist/AvatarClient.d.ts.map +1 -1
- package/client/dist/AvatarClient.js +20 -71
- package/client/dist/AvatarClient.js.map +1 -1
- package/client/dist/index.d.ts +22 -0
- package/client/dist/index.d.ts.map +1 -0
- package/client/dist/index.js +22 -0
- package/client/dist/index.js.map +1 -0
- package/client/dist/types.d.ts +22 -37
- package/client/dist/types.d.ts.map +1 -1
- package/client/dist/types.js +12 -12
- package/client/dist/types.js.map +1 -1
- package/client/dist/useAvatar.d.ts +8 -19
- package/client/dist/useAvatar.d.ts.map +1 -1
- package/client/dist/useAvatar.js +15 -32
- package/client/dist/useAvatar.js.map +1 -1
- package/client/src/Avatar.tsx +19 -24
- package/client/src/AvatarClient.ts +38 -111
- package/client/src/index.ts +22 -0
- package/client/src/types.ts +24 -47
- package/client/src/useAvatar.ts +19 -47
- package/docs/contract-avatar.md +37 -3
- package/docs/contract-protocol.md +90 -30
- package/package.json +3 -11
- package/src/avatar.d.ts +38 -6
- package/src/avatar.js +72 -37
- package/src/hand.js +680 -0
- package/src/idle.js +8 -25
- package/src/line-art.js +26 -0
- package/src/perform.js +8 -4
- package/client/dist/pipecat.d.ts +0 -21
- package/client/dist/pipecat.d.ts.map +0 -1
- package/client/dist/pipecat.js +0 -21
- package/client/dist/pipecat.js.map +0 -1
- package/client/dist/react.d.ts +0 -16
- package/client/dist/react.d.ts.map +0 -1
- package/client/dist/react.js +0 -17
- package/client/dist/react.js.map +0 -1
- package/client/src/pipecat.ts +0 -38
- package/client/src/react.ts +0 -34
- 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, ~
|
|
9
|
-
|
|
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
|
-
```
|
|
21
|
-
import {
|
|
20
|
+
```jsx
|
|
21
|
+
import { Avatar } from '@voqalize/avatar';
|
|
22
|
+
|
|
23
|
+
<Avatar client={pipecatClient} className="call-tile" />
|
|
24
|
+
```
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
```python
|
|
27
|
+
from voqalize_avatar import AvatarProcessor
|
|
24
28
|
|
|
25
|
-
|
|
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/`
|
|
41
|
-
| the client | `client/` → `@voqalize/avatar
|
|
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,
|
|
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
|
-
|
|
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
|
|
75
|
+
from voqalize_avatar import AvatarProcessor
|
|
67
76
|
|
|
68
|
-
pipeline = Pipeline([..., tts, AvatarProcessor(
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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).
|
|
358
|
-
user
|
|
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.
|
|
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
|
|
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 / .
|
|
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/
|
|
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 |
|
package/client/dist/Avatar.d.ts
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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 {
|
|
16
|
-
import type {
|
|
17
|
-
export interface AvatarProps extends
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
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({
|
|
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
|
|
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"}
|
package/client/dist/Avatar.js
CHANGED
|
@@ -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({
|
|
4
|
-
const { containerRef } = useAvatar(
|
|
5
|
-
return
|
|
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":";
|
|
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()`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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
|
|
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
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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
|
|
164
|
-
*
|
|
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,
|
|
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()`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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 {
|
|
71
|
+
import { isAvatarMessage, } from "./types.js";
|
|
72
72
|
/**
|
|
73
|
-
* The
|
|
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
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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 "
|
|
137
|
-
this.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
|
245
|
-
*
|
|
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.
|
|
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
|