@voqalize/avatar 0.2.2 → 0.4.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.
Files changed (265) hide show
  1. package/LICENSE +21 -661
  2. package/LICENSE-CC-BY-4.0 +396 -0
  3. package/README.md +191 -662
  4. package/assets/README.md +48 -0
  5. package/assets/tanya.glb +0 -0
  6. package/assets/tara.glb +0 -0
  7. package/assets/tushar.glb +0 -0
  8. package/client/Avatar.tsx +57 -0
  9. package/client/AvatarClient.ts +818 -0
  10. package/client/arjun.ts +26 -0
  11. package/client/createAvatar.ts +177 -0
  12. package/client/createCanvasAvatar.ts +72 -0
  13. package/client/index.ts +44 -0
  14. package/client/internal.ts +108 -0
  15. package/client/interviewer-female.ts +4 -0
  16. package/client/interviewer-male.ts +4 -0
  17. package/client/ishita.ts +26 -0
  18. package/client/kabir.ts +26 -0
  19. package/client/meera.ts +26 -0
  20. package/client/naina.ts +26 -0
  21. package/client/playout.ts +95 -0
  22. package/client/professional-female-a.ts +4 -0
  23. package/client/professional-female-b.ts +4 -0
  24. package/client/professional-male-a.ts +4 -0
  25. package/client/professional-male-b.ts +4 -0
  26. package/client/react.ts +13 -0
  27. package/client/supports.ts +28 -0
  28. package/client/three/assets.ts +24 -0
  29. package/client/three/budgets.ts +27 -0
  30. package/client/three/holds.ts +64 -0
  31. package/client/three/internal.ts +54 -0
  32. package/client/three/motion-limits.json +116 -0
  33. package/client/three/sequences.ts +281 -0
  34. package/client/three/tanya.ts +69 -0
  35. package/client/three/tara-rig.ts +1562 -0
  36. package/client/three/tara.ts +76 -0
  37. package/client/three/tushar.ts +65 -0
  38. package/client/types.ts +235 -0
  39. package/client/useAvatar.ts +74 -0
  40. package/client/vikram.ts +26 -0
  41. package/dist/Avatar.d.ts +44 -0
  42. package/dist/Avatar.d.ts.map +1 -0
  43. package/{client/dist → dist}/Avatar.js +2 -2
  44. package/dist/Avatar.js.map +1 -0
  45. package/dist/AvatarClient.d.ts +274 -0
  46. package/dist/AvatarClient.d.ts.map +1 -0
  47. package/dist/AvatarClient.js +712 -0
  48. package/dist/AvatarClient.js.map +1 -0
  49. package/dist/arjun.d.ts +7 -0
  50. package/dist/arjun.d.ts.map +1 -0
  51. package/dist/arjun.js +20 -0
  52. package/dist/arjun.js.map +1 -0
  53. package/dist/createAvatar.d.ts +135 -0
  54. package/dist/createAvatar.d.ts.map +1 -0
  55. package/dist/createAvatar.js +75 -0
  56. package/dist/createAvatar.js.map +1 -0
  57. package/dist/createCanvasAvatar.d.ts +22 -0
  58. package/dist/createCanvasAvatar.d.ts.map +1 -0
  59. package/dist/createCanvasAvatar.js +47 -0
  60. package/dist/createCanvasAvatar.js.map +1 -0
  61. package/dist/index.d.ts +34 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +33 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/internal.d.ts +34 -0
  66. package/dist/internal.d.ts.map +1 -0
  67. package/dist/internal.js +41 -0
  68. package/dist/internal.js.map +1 -0
  69. package/dist/interviewer-female.d.ts +4 -0
  70. package/dist/interviewer-female.d.ts.map +1 -0
  71. package/dist/interviewer-female.js +3 -0
  72. package/dist/interviewer-female.js.map +1 -0
  73. package/dist/interviewer-male.d.ts +4 -0
  74. package/dist/interviewer-male.d.ts.map +1 -0
  75. package/dist/interviewer-male.js +3 -0
  76. package/dist/interviewer-male.js.map +1 -0
  77. package/dist/ishita.d.ts +7 -0
  78. package/dist/ishita.d.ts.map +1 -0
  79. package/dist/ishita.js +20 -0
  80. package/dist/ishita.js.map +1 -0
  81. package/dist/kabir.d.ts +7 -0
  82. package/dist/kabir.d.ts.map +1 -0
  83. package/dist/kabir.js +20 -0
  84. package/dist/kabir.js.map +1 -0
  85. package/dist/meera.d.ts +7 -0
  86. package/dist/meera.d.ts.map +1 -0
  87. package/dist/meera.js +20 -0
  88. package/dist/meera.js.map +1 -0
  89. package/dist/naina.d.ts +7 -0
  90. package/dist/naina.d.ts.map +1 -0
  91. package/dist/naina.js +20 -0
  92. package/dist/naina.js.map +1 -0
  93. package/dist/playout.d.ts +42 -0
  94. package/dist/playout.d.ts.map +1 -0
  95. package/dist/playout.js +85 -0
  96. package/dist/playout.js.map +1 -0
  97. package/dist/professional-female-a.d.ts +4 -0
  98. package/dist/professional-female-a.d.ts.map +1 -0
  99. package/dist/professional-female-a.js +3 -0
  100. package/dist/professional-female-a.js.map +1 -0
  101. package/dist/professional-female-b.d.ts +4 -0
  102. package/dist/professional-female-b.d.ts.map +1 -0
  103. package/dist/professional-female-b.js +3 -0
  104. package/dist/professional-female-b.js.map +1 -0
  105. package/dist/professional-male-a.d.ts +4 -0
  106. package/dist/professional-male-a.d.ts.map +1 -0
  107. package/dist/professional-male-a.js +3 -0
  108. package/dist/professional-male-a.js.map +1 -0
  109. package/dist/professional-male-b.d.ts +4 -0
  110. package/dist/professional-male-b.d.ts.map +1 -0
  111. package/dist/professional-male-b.js +3 -0
  112. package/dist/professional-male-b.js.map +1 -0
  113. package/dist/react.d.ts +13 -0
  114. package/dist/react.d.ts.map +1 -0
  115. package/dist/react.js +13 -0
  116. package/dist/react.js.map +1 -0
  117. package/dist/supports.d.ts +20 -0
  118. package/dist/supports.d.ts.map +1 -0
  119. package/dist/supports.js +26 -0
  120. package/dist/supports.js.map +1 -0
  121. package/dist/three/assets.d.ts +24 -0
  122. package/dist/three/assets.d.ts.map +1 -0
  123. package/dist/three/assets.js +22 -0
  124. package/dist/three/assets.js.map +1 -0
  125. package/dist/three/budgets.d.ts +17 -0
  126. package/dist/three/budgets.d.ts.map +1 -0
  127. package/dist/three/budgets.js +21 -0
  128. package/dist/three/budgets.js.map +1 -0
  129. package/dist/three/holds.d.ts +33 -0
  130. package/dist/three/holds.d.ts.map +1 -0
  131. package/dist/three/holds.js +56 -0
  132. package/dist/three/holds.js.map +1 -0
  133. package/dist/three/internal.d.ts +52 -0
  134. package/dist/three/internal.d.ts.map +1 -0
  135. package/dist/three/internal.js +49 -0
  136. package/dist/three/internal.js.map +1 -0
  137. package/dist/three/motion-limits.json +116 -0
  138. package/dist/three/sequences.d.ts +71 -0
  139. package/dist/three/sequences.d.ts.map +1 -0
  140. package/dist/three/sequences.js +262 -0
  141. package/dist/three/sequences.js.map +1 -0
  142. package/dist/three/tanya.d.ts +35 -0
  143. package/dist/three/tanya.d.ts.map +1 -0
  144. package/dist/three/tanya.js +51 -0
  145. package/dist/three/tanya.js.map +1 -0
  146. package/dist/three/tara-rig.d.ts +403 -0
  147. package/dist/three/tara-rig.d.ts.map +1 -0
  148. package/dist/three/tara-rig.js +1500 -0
  149. package/dist/three/tara-rig.js.map +1 -0
  150. package/dist/three/tara.d.ts +35 -0
  151. package/dist/three/tara.d.ts.map +1 -0
  152. package/dist/three/tara.js +58 -0
  153. package/dist/three/tara.js.map +1 -0
  154. package/dist/three/tushar.d.ts +31 -0
  155. package/dist/three/tushar.d.ts.map +1 -0
  156. package/dist/three/tushar.js +47 -0
  157. package/dist/three/tushar.js.map +1 -0
  158. package/dist/types.d.ts +149 -0
  159. package/dist/types.d.ts.map +1 -0
  160. package/dist/types.js +140 -0
  161. package/dist/types.js.map +1 -0
  162. package/dist/useAvatar.d.ts +39 -0
  163. package/dist/useAvatar.d.ts.map +1 -0
  164. package/dist/useAvatar.js +35 -0
  165. package/dist/useAvatar.js.map +1 -0
  166. package/dist/vikram.d.ts +7 -0
  167. package/dist/vikram.d.ts.map +1 -0
  168. package/dist/vikram.js +20 -0
  169. package/dist/vikram.js.map +1 -0
  170. package/package.json +110 -25
  171. package/src/avatar.d.ts +218 -120
  172. package/src/avatar.js +950 -213
  173. package/src/behavior.d.ts +42 -0
  174. package/src/behavior.js +114 -0
  175. package/src/camera.js +29 -0
  176. package/src/canvas/author/parts/eye.mjs +722 -0
  177. package/src/canvas/author/parts/hand.mjs +1156 -0
  178. package/src/canvas/author/parts/mouth.mjs +741 -0
  179. package/src/canvas/author/parts/nose.mjs +100 -0
  180. package/src/canvas/author/parts/skin-detail.mjs +67 -0
  181. package/src/canvas/author/path.mjs +283 -0
  182. package/src/canvas/author/rig.mjs +405 -0
  183. package/src/canvas/avatars/round/face.d.mts +3 -0
  184. package/src/canvas/avatars/round/face.mjs +1307 -0
  185. package/src/canvas/create-rig.d.ts +15 -0
  186. package/src/canvas/create-rig.js +100 -0
  187. package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
  188. package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
  189. package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
  190. package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
  191. package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
  192. package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
  193. package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
  194. package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
  195. package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
  196. package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
  197. package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
  198. package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
  199. package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
  200. package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
  201. package/src/canvas/data/img/round-m3-top-body.webp +0 -0
  202. package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
  203. package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
  204. package/src/canvas/data/img/round-w1-top-body.webp +0 -0
  205. package/src/canvas/data/interviewer-female.rig.json +1 -0
  206. package/src/canvas/data/interviewer-male.rig.json +1 -0
  207. package/src/canvas/data/professional-female-a.rig.json +1 -0
  208. package/src/canvas/data/professional-female-b.rig.json +1 -0
  209. package/src/canvas/data/professional-male-a.rig.json +1 -0
  210. package/src/canvas/data/professional-male-b.rig.json +1 -0
  211. package/src/canvas/src/live.js +508 -0
  212. package/src/canvas/src/render2d.js +218 -0
  213. package/src/canvas/src/rig.js +297 -0
  214. package/src/canvas/src/vocab.js +96 -0
  215. package/src/clips.js +18 -7
  216. package/src/conformance.js +119 -0
  217. package/src/emotions.js +8 -5
  218. package/src/face-core.js +27 -1
  219. package/src/face-myna.d.ts +7 -0
  220. package/src/face-myna.js +192 -131
  221. package/src/face-peep-control-plane.js +167 -0
  222. package/src/face-peep.d.ts +7 -0
  223. package/src/face-peep.js +274 -116
  224. package/src/face-wren.d.ts +7 -0
  225. package/src/face-wren.js +17 -19
  226. package/src/faces.d.ts +12 -0
  227. package/src/faces.js +53 -0
  228. package/src/gaze.js +434 -58
  229. package/src/hand.js +120 -91
  230. package/src/head.js +134 -0
  231. package/src/idle.js +227 -119
  232. package/src/interjections.js +200 -31
  233. package/src/params.js +8 -4
  234. package/src/perform.js +7 -9
  235. package/src/prosody.js +647 -0
  236. package/src/rig.d.ts +24 -0
  237. package/src/rig.js +32 -0
  238. package/src/speech-timing.js +23 -0
  239. package/src/visemes.js +110 -24
  240. package/client/dist/Avatar.d.ts +0 -27
  241. package/client/dist/Avatar.d.ts.map +0 -1
  242. package/client/dist/Avatar.js.map +0 -1
  243. package/client/dist/AvatarClient.d.ts +0 -139
  244. package/client/dist/AvatarClient.d.ts.map +0 -1
  245. package/client/dist/AvatarClient.js +0 -223
  246. package/client/dist/AvatarClient.js.map +0 -1
  247. package/client/dist/index.d.ts +0 -22
  248. package/client/dist/index.d.ts.map +0 -1
  249. package/client/dist/index.js +0 -22
  250. package/client/dist/index.js.map +0 -1
  251. package/client/dist/types.d.ts +0 -86
  252. package/client/dist/types.d.ts.map +0 -1
  253. package/client/dist/types.js +0 -31
  254. package/client/dist/types.js.map +0 -1
  255. package/client/dist/useAvatar.d.ts +0 -42
  256. package/client/dist/useAvatar.d.ts.map +0 -1
  257. package/client/dist/useAvatar.js +0 -51
  258. package/client/dist/useAvatar.js.map +0 -1
  259. package/client/src/Avatar.tsx +0 -33
  260. package/client/src/AvatarClient.ts +0 -270
  261. package/client/src/index.ts +0 -22
  262. package/client/src/types.ts +0 -104
  263. package/client/src/useAvatar.ts +0 -85
  264. package/docs/contract-avatar.md +0 -371
  265. package/docs/contract-protocol.md +0 -461
package/README.md CHANGED
@@ -1,75 +1,58 @@
1
- # Avatar — a programmable 2-D talking head
1
+ # @voqalize/avatar
2
2
 
3
- A dependency-free SVG avatar for AI agents on voice calls. The agent can wear
4
- any face and go by any name — the demos call theirs Kiran. The server owns
5
- meaning (what state the agent is in, what it's saying, where it's looking); the
6
- client owns motion (how a face actually moves when it means those things).
3
+ A 2-D talking head for AI voice calls, driven by your pipecat client. The
4
+ avatars are lip-synced to the audio and they are state aware: they know when
5
+ they have been interrupted, when the user is talking versus idle, when a tool
6
+ call has started and stopped.
7
7
 
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.
8
+ No video track, no per-minute avatar vendor, no second media path. Twelve
9
+ avatars ship with it — three SVG faces, six professional Canvas2D identities and
10
+ three 2.5-D characters — one per entry point, so you pay for the one you import,
11
+ and you can author your own.
10
12
 
11
- The two load-bearing interfaces are specified in
12
- [docs/contract-protocol.md](docs/contract-protocol.md) (server ↔ widget) and
13
- [docs/contract-avatar.md](docs/contract-avatar.md) (mixer ↔ face).
13
+ This is the browser half. The pipeline half is
14
+ [`voqalize-avatar`](https://pypi.org/project/voqalize-avatar/) on PyPI; they are
15
+ two ends of one wire format and release independently, kept compatible by the
16
+ wire contract rather than a shared version number
17
+ ([RELEASING.md § Compatibility](https://github.com/voqalize/avatar/blob/main/RELEASING.md#compatibility)).
14
18
 
15
- ```sh
16
- npm install @voqalize/avatar # the browser half
17
- pip install voqalize-avatar # the pipecat half
18
- ```
19
+ **Licence: MIT for the code, CC-BY 4.0 for the three 2.5-D character binaries**
20
+ (`assets/*.glb`, the artwork — see `assets/README.md` for the credit line). The
21
+ code is usable anywhere, including in closed-source products; the characters ask
22
+ for attribution and nothing else. The manifest declares the pair as
23
+ `MIT AND CC-BY-4.0`.
19
24
 
20
- ```jsx
21
- import { Avatar } from '@voqalize/avatar';
22
-
23
- <Avatar client={pipecatClient} className="call-tile" />
24
- ```
25
+ ## Install
25
26
 
26
- ```python
27
- from voqalize_avatar import AvatarProcessor
28
-
29
- pipeline = Pipeline([..., tts, AvatarProcessor(), transport.output()])
27
+ ```sh
28
+ npm install @voqalize/avatar # this package, the browser half
29
+ pip install voqalize-avatar # the pipecat half
30
30
  ```
31
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.
32
+ Node 20+. The package is ESM with **no runtime dependencies** and ships its own
33
+ types. `@pipecat-ai/client-js` (`>=1.4 <2`) and React (`>=18`) are declared as
34
+ *optional* peers: the pipecat import is types-only, so nothing fails to load
35
+ without it, and React is only for `@voqalize/avatar/react`.
43
36
 
44
- ## What's in the box
37
+ ## Getting started
45
38
 
46
- The repo is one system in three pieces, because that is how it gets consumed —
47
- a browser widget alone is not a working avatar, and neither is a lipsync
48
- backend. See [docs/design-library-split.md](docs/design-library-split.md) for
49
- why this is a library rather than a product.
39
+ In the browser, wherever your app already renders the bot's tile:
50
40
 
51
- | piece | where | what it is |
52
- |---|---|---|
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 |
55
- | the backend | `py/` → `voqalize-avatar` (PyPI) | a pipecat `FrameProcessor` that infers state from stock frames and streams visemes |
56
- | the aligner | `native/avatarsync/` | the Rhubarb Lip Sync fork the backend drives — A–H letters from text *and* from audio |
41
+ ```js
42
+ import { createAvatar } from '@voqalize/avatar';
57
43
 
58
- Browser side, one entry point:
44
+ const avatar = createAvatar({ mount: el, client: pipecatClient });
45
+ ```
59
46
 
60
47
  ```jsx
61
- import { Avatar } from '@voqalize/avatar'; // peers: react >= 18, @pipecat-ai/client-js
48
+ import { Avatar } from '@voqalize/avatar/react';
49
+ import { wren } from '@voqalize/avatar/faces/wren'; // `peep` is the default
62
50
 
63
- <Avatar client={pipecatClient} avatar="peep" className="call-tile" />
51
+ <Avatar client={pipecatClient} options={{ face: wren }} className="call-tile" />
64
52
  ```
65
53
 
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
-
71
- Server side, the whole integration is one processor between your TTS and your
72
- output transport — see `py/` and `docs/contract-protocol.md`:
54
+ In the pipeline, between the TTS service and the transport's output — the seat
55
+ where it can see the audio that is about to be spoken, at generation speed:
73
56
 
74
57
  ```python
75
58
  from voqalize_avatar import AvatarProcessor
@@ -77,651 +60,197 @@ from voqalize_avatar import AvatarProcessor
77
60
  pipeline = Pipeline([..., tts, AvatarProcessor(), transport.output()])
78
61
  ```
79
62
 
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
-
84
- That much is inferred from stock pipecat frames, with no application code.
85
- States that depend on what your application is *doing* — a tool call that
86
- should read as *reviewing the screen* rather than *thinking* — are signalled
87
- explicitly with `AvatarControlFrame`.
88
-
89
- ## Running the demo
90
-
91
- ES modules will not load over `file://`. Serve the directory:
92
-
93
- ```
94
- python3 serve.py 8777
95
- open http://localhost:8777/demo/call.html
96
- ```
97
-
98
- Use `serve.py`, not `python3 -m http.server`. The stdlib server sends
99
- `Last-Modified` and no `Cache-Control`, so browsers apply heuristic freshness and
100
- quietly stop revalidating modules you have edited; `serve.py` is the same server
101
- with `Cache-Control: no-store`. Do not work around a stale module with a `?v=`
102
- query string either — that puts two copies of it in the graph and fails worse.
103
-
104
- `demo/call.html` is the page to start with: a two-tile call with mic VAD,
105
- turn-taking and a log of every token the server would have sent. Hold `Space` to
106
- be the human side of the call if you have no microphone.
107
-
108
- The other one is the control harness:
109
-
110
- ```
111
- index.html?avatar=NAME every control maps to a server token; exposes
112
- window.avatar, so the console is a live REPL
113
- ```
114
-
115
- Those two are the entry points. Everything else is rig tooling — for the
116
- occasions when you are *building or repairing an avatar*, which is a different
117
- job — and it lives behind one index:
118
-
119
- ```
120
- demo/rig/index.html rig-check, contact sheet, torso check, clip strip,
121
- expression lab, lipsync eval
122
- ```
123
-
124
- `?avatar=NAME` (or `?face=NAME` on the rig pages) selects the rig; the call
125
- demo also has a live picker that swaps it without dropping the call.
126
-
127
- ---
128
-
129
- ## Design
130
-
131
- ### 1. The face is a vector, not a set of drawings
132
-
133
- Everything the avatar can do is a point in a ~30-dimensional parameter space
134
- (`src/params.js`): `mouthOpen`, `mouthRound`, `lidL`, `browInnerR`, `headYaw`,
135
- and so on. Visemes, emotions, gaze poses and gesture keyframes are all just
136
- named vectors in that space.
137
-
138
- This is the decision the rest of the system rests on. Blending a smile into a
139
- mid-sentence "oh" is arithmetic, not SVG path surgery — and a continuous stream
140
- of parameter updates from the server is the *native* input format rather than
141
- something to be adapted to.
142
-
143
- ### 2. Layers mix in a fixed order
144
-
145
- ```
146
- base pose (state + emotion) → gaze → visemes → clip deltas → idle
147
- ```
148
-
149
- Earlier layers are overwritten by later ones on the channels they touch. Gesture
150
- clips and the idle layer are **additive**, so they compose instead of fighting:
151
- a nod during speech moves the head while the mouth stays on the server's viseme
152
- track, with no special-casing anywhere.
153
-
154
- One hard rule: **while the server viseme track is playing it owns the mouth
155
- outright.** An interjection that fires mid-sentence contributes its head and
156
- brows, and its mouth track is silently dropped. Otherwise the avatar appears to
157
- say two things at once.
158
-
159
- ### 3. Smoothing is the animation
160
-
161
- There is no tweening engine. Every channel chases its target with a
162
- frame-rate-independent exponential approach, at a per-channel time constant:
163
-
164
- | channel group | τ | why |
165
- |---|---|---|
166
- | mouth | 42ms | fast enough to hit consonants, slow enough to blur between them |
167
- | lids | 18ms | blinks must be crisp or they read as a glitch |
168
- | pupils | 32ms | saccades are ballistic |
169
- | brows | 80ms | |
170
- | head | 160ms | the head has real mass |
171
- | smile | 130ms | expressions bloom, they don't snap |
172
- | shoulders | 190ms | the torso has more mass than the head and reads wrong when it hasn't |
173
- | lean | 240ms | |
174
-
175
- This gives the face weight, and it does **viseme co-articulation for free** — we
176
- never blend shapes explicitly, we just retarget and let the mouth chase.
177
-
178
- ### 4. Screen-share hygiene
179
-
180
- The call runs with screen share on. All idle motion is deliberately
181
- low-amplitude and low-frequency; a jittery avatar in the corner of a shared
182
- screen costs the video encoder real bitrate for no communicative gain.
183
-
184
- ---
185
-
186
- ## The viseme protocol
187
-
188
- The wire format is the **Rhubarb Lip Sync alphabet** — A–H plus X — a
189
- condensation of the Preston Blair mouth set. Nine shapes is plenty for a
190
- stylized 2-D face, and it means the server has an obvious open-source reference
191
- implementation to target.
192
-
193
- | letter | mouth | phonemes |
194
- |---|---|---|
195
- | `A` | closed lips | P B M — also the resting closure |
196
- | `B` | slightly open, teeth together | K S T D, consonantal EE |
197
- | `C` | open | EH AE |
198
- | `D` | wide open | AA |
199
- | `E` | slightly rounded | AO ER R |
200
- | `F` | puckered | UW OW W |
201
- | `G` | lower lip to upper teeth | F V |
202
- | `H` | tongue up, visible | L |
203
- | `X` | idle / silence | — |
204
-
205
- A cue is `{ t, v, i? }`:
206
-
207
- ```js
208
- { t: 240, v: 'D', i: 0.8 }
209
- // ^ms into the utterance
210
- // ^letter
211
- // ^optional 0..1 loudness
63
+ That is the integration, both halves of it. Neither takes an argument.
64
+ `createAvatar` returns `{ destroy() }` and nothing else — the avatar is an
65
+ embodiment of your `PipecatClient` and reacts to it, so there is no avatar to
66
+ drive and no state to read back.
67
+
68
+ **Works the same with or without `@pipecat-ai/voice-ui-kit`.** All `createAvatar`
69
+ needs is the `PipecatClient` instance — however you built your call UI, if you
70
+ have that instance you can mount an avatar next to it. voice-ui-kit's
71
+ components (buttons, visualizers, transport controls) are chrome around the
72
+ same client; they neither wrap nor gate it.
73
+
74
+ ## What you get for free
75
+
76
+ Most of the avatar works on any pipecat application without customization — not
77
+ because integrations were enumerated, but because the behaviour is derived from
78
+ frames and events a pipecat pipeline already emits. `SPEAKING`, `LISTENING`,
79
+ `MUTED`, `OFFLINE` and `DEGRADED` come from your `PipecatClient` with no backend
80
+ involvement at all; `THINKING`, `WORKING` and `CANT_HEAR` come from
81
+ `AvatarProcessor` watching turn boundaries, LLM response boundaries and
82
+ function-call frames; lipsync comes from the same karaoke frames pipecat already
83
+ pushes for word-level captions; blink, breath, gaze aversion and idle motion are
84
+ always the renderer's.
85
+
86
+ What is left over is small, specific, and each item is a case the library
87
+ refuses to guess at — a deliberate nod or greeting, a tool whose calls never
88
+ enter your pipeline, a pose richer than the nine states, a backend that is not
89
+ ours. [The architecture
90
+ page](https://github.com/voqalize/avatar/blob/main/docs/architecture.md) is the
91
+ canonical reference for all of it.
92
+
93
+ ## The wire protocol
94
+
95
+ Three commands, one envelope
96
+ ([contract-wire.md](https://github.com/voqalize/avatar/blob/main/docs/contract-wire.md)):
97
+
98
+ ```json
99
+ { "type": "avatar", "cmd": "state", "state": "WORKING" }
100
+ { "type": "avatar", "cmd": "action", "id": "ACKNOWLEDGE" }
101
+ { "type": "avatar", "cmd": "cues", "ctx": "tts-context", "from_ms": 0, "cues": [] }
212
102
  ```
213
103
 
214
- **Send intensity if you can.** It's cheap to derive from TTS energy and it's the
215
- single biggest realism win available — the same viseme shouted and murmured
216
- should not look identical. It scales only the effortful channels, so a quiet `D`
217
- is a small `D`, not a different shape.
104
+ **States are durable and they are prioritised.** A state holds until the facts
105
+ change; it does not complete on a timer. **The state pipecat reports always
106
+ wins** — bot-output lifecycle and user speech are observed Pipecat facts, and a
107
+ state the server sends is a *candidate* underneath them. **Actions are
108
+ point-in-time animations** that land on top of whatever state is effective at
109
+ the time; they are finite, they complete on their own, and they never establish
110
+ state.
218
111
 
219
- ### Sync rules (these matter more than the shapes)
112
+ Emission is overwrite, never merge: a `cues` message says "discard everything
113
+ queued at or after `from_ms`, then append these". The server decides; the client
114
+ has no say and no way to refuse.
220
115
 
221
- - **Schedule against the audio clock, never wall time.** `audioEl.currentTime *
222
- 1000` or `AudioContext.currentTime`. Wall time drifts against playback and you
223
- will spend the rest of your life chasing it. The client does this for you if
224
- you pass `audio` to `speak()`.
225
- - **The mouth leads the sound by 40ms** (`LEAD_MS`). Perceptual tolerance is
226
- asymmetric — roughly −45ms (audio first) to +125ms (video first) — so leading
227
- is the safe side to err on.
228
- - **Cues below 30ms are dropped** by `normalizeCues()`, which also sorts and
229
- merges consecutive repeats. When a sub-30ms cue must be dropped, closures
230
- (`A`/`G`) win over mid-open vowels: they carry more lip-reading information.
231
- - **Streaming is fine.** Start with what you have and `pushCues()` the rest as it
232
- arrives; the track re-normalizes and re-seeks.
116
+ **Not using our backend?** Any server can produce cues, three ways, best first.
117
+ If your TTS emits native viseme events, map the integer ids through
118
+ `AZURE_VISEME_TO_LETTER` and ship `{t, v}` as they stream. Otherwise force-align
119
+ the text against the audio and map ARPAbet through `ARPABET_TO_VISEME`. With no
120
+ server work at all, `textToCues(text)` is a crude grapheme guesser, fit for
121
+ previews only. All three are exported from `@voqalize/avatar/internal`.
233
122
 
234
- ---
123
+ ## The faces
235
124
 
236
- ## Getting mouth shapes out of speech (server side)
237
-
238
- Three tiers. Pick the highest one your TTS supports.
239
-
240
- ### Tier 1 — native TTS viseme events (best, and nearly free)
241
-
242
- Several TTS engines emit viseme events alongside the audio, already aligned.
243
-
244
- **Azure Speech** fires `visemeReceived` with an integer ID 0–21 and an audio
245
- offset in 100ns ticks. `src/visemes.js` exports the mapping:
125
+ Three ship today, all hand-authored line art: **`peep`** (the default),
126
+ **`wren`**, **`myna`**. Each is its own entry point, and you pass the value
127
+ rather than a name:
246
128
 
247
129
  ```js
248
- import { AZURE_VISEME_TO_LETTER } from './src/visemes.js';
249
-
250
- synth.visemeReceived = (_s, e) => {
251
- cues.push({
252
- t: e.audioOffset / 10000, // ticks → ms
253
- v: AZURE_VISEME_TO_LETTER[e.visemeId],
254
- });
255
- };
256
- ```
130
+ import { myna } from '@voqalize/avatar/faces/myna';
257
131
 
258
- **AWS Polly** with `SpeechMarkTypes: ['viseme']` returns a JSON-lines stream of
259
- `{time, type: 'viseme', value}` where `value` is a Polly viseme name (`p`, `t`,
260
- `S`, `T`, `f`, `k`, `i`, `r`, `s`, `u`, `@`, `a`, `e`, `E`, `o`, `O`, `sil`).
261
- Map those onto the letters above — `p→A`, `f→G`, `u/o/O→F`, `a→D`, `E/e→C`,
262
- `i→B`, `r/@→E`, `t/s/S/T/k→B`, `sil→X`.
263
-
264
- **ElevenLabs / OpenAI TTS** don't emit visemes. Use tier 2.
265
-
266
- ### Tier 2 — forced alignment (works with any TTS)
267
-
268
- Take the audio and the text you already have, get phonemes with timestamps, map
269
- phonemes to letters. Open-source options, cheapest first:
270
-
271
- - **[Montreal Forced Aligner](https://montreal-forced-aligner.readthedocs.io/)** —
272
- the standard. Accurate, ~real-time on CPU, Python.
273
- - **[whisper-timestamped](https://github.com/linto-ai/whisper-timestamped)** or
274
- **WhisperX** — word-level timings; interpolate phonemes within each word via
275
- CMUdict. Lower fidelity but you may already be running Whisper.
276
- - **[Rhubarb Lip Sync](https://github.com/DanielSWolf/rhubarb-lip-sync)** — a
277
- single binary that goes straight from WAV (+ optional transcript) to exactly
278
- this A–H alphabet. `rhubarb -f json -d dialog.txt audio.wav`. This is the
279
- reference implementation; if you want a one-command answer, it's this.
280
-
281
- For phoneme→letter, port `ARPABET_TO_VISEME` from `src/visemes.js` — it's the
282
- complete table and it's already tuned against these nine shapes.
283
-
284
- Rough recipe if you're rolling your own:
285
-
286
- 1. G2P the utterance text (CMUdict for known words, `g2p-en` or `phonemizer`
287
- for the rest) → ARPAbet phoneme sequence.
288
- 2. Force-align against the synthesized audio → per-phoneme start times.
289
- 3. Map each phoneme through `ARPABET_TO_VISEME`.
290
- 4. Emit `{t, v}` at each phoneme onset. Add `{t: end, v: 'X'}` at utterance end.
291
- 5. Don't smooth or interpolate — the client's per-channel smoothing does that,
292
- and doing it twice makes the mouth mushy.
293
-
294
- Latency note: alignment needs the whole audio, so for streaming TTS run it per
295
- sentence chunk and `pushCues()` each chunk as it completes.
296
-
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.
302
-
303
- ---
304
-
305
- ## API
306
-
307
- ### States
308
-
309
- `setState(name, { emotion, intensity, gaze, keepGaze })`
310
-
311
- | state | behaviour |
312
- |---|---|
313
- | `IDLE` | neutral, full idle motion |
314
- | `LISTENING` | slightly widened eyes, brows up a touch, ~16 blinks/min, **backchannel nods fire automatically** — timed off the user's voice when one is supplied |
315
- | `THINKING` | gaze breaks away — mostly *down*, sometimes up-left — faster shallow breath, ~25 blinks/min, occasional dead-still holds |
316
- | `SPEAKING` | eye contact, damped idle so it doesn't fight the mouth |
317
- | `REVIEWING_SCREEN` | gaze wanders across screen regions on its own, leisurely |
318
- | `SEARCHING_SCREEN` | the same regions *hunted* — quick saccades, revisits, tiny "not this one" head flicks. A filler that buys the agent time |
319
- | `WAITING_FOR_USER` | head tilt, brows up, encouraging — the "go ahead" pose |
320
- | `CANT_HEAR` | leans right in, ear cheated toward the speaker, eyes holding contact, concentration squint. Send it when the user's audio is soft |
321
- | `TYPING` | gaze down into the work, task-rate blinks (~9/min), burst-pause shoulder rhythm, a brief glance up every few seconds — busy, not absent |
322
- | `TYPING_CHAT` | `TYPING` turned communicative: longer expectant glance-holds, a touch of apology. For when the audio channel is broken and chat is the workaround |
323
- | `DISTRACTED` | attention visibly elsewhere — lateral away-gaze wander, loosened sway, no backchannels |
324
- | `TAKING_FLOOR` | shoulders up, lips parting, head coming up — about to speak |
325
- | `WANTS_IN` | the same inbreath, held and very still — "I'd like to come in" |
326
- | `YIELDED` | shoulders and lean dropped — interrupted, and giving way |
327
- | `DEGRADED` | desaturated, heavy lids — signals a network problem honestly |
328
- | `OFFLINE` | eyes closed, greyscale |
329
-
330
- Setting a state adopts its default emotion and gaze unless you override them.
331
- Every state carries an idle *profile* — blink rate, breath rate and depth, sway,
332
- weight-shift interval, stillness holds — so the liveness itself is a state
333
- signal (the blink-rate spread alone separates listening from thinking from
334
- visually busy; the numbers come from `docs/research-biomechanics.md`).
335
-
336
- The body is part of that, not just the face. The trunk breathes as a chest
337
- swell about the hem rather than sliding up and down; it re-settles its weight
338
- every 9–22 s, discretely and aperiodically, with the head counter-rolling so
339
- the gaze stays on you through the shift; and it follows a sustained head turn
340
- at nearly 3× the head's time constant, which is where follow-through comes
341
- from. Amplitude rides on the state's `sway`, so concentration suppresses the
342
- lot and `OFFLINE` is genuinely still. `avatar.setMotionGain(g)` scales all of
343
- it if a host wants a calmer or livelier body; `demo/call.html` exposes it as
344
- the *Body motion* slider, because where "alive" stops and "fidgety" starts is a
345
- judgement that should be argued against a running rig.
346
-
347
- The three floor-management states exist because turn-taking is what goes wrong
348
- most often in a voice call — the user either talks over the agent or waits
349
- in silence for a signal that never comes. They are states rather than clips
350
- because the floor is a condition, not an event: `WANTS_IN` has to hold for as
351
- long as it takes the other person to notice it. All three lift the shoulders and
352
- part the lips, because that is what an inbreath looks like from outside, and an
353
- inbreath is the cue humans actually use to predict that someone is about to
354
- speak. The head comes *up*, not down — a lowered head reads as yielding.
355
-
356
- ### The user's voice
357
-
358
- Backchannels only create rapport when they are *contingent* — a nod coupled to
359
- the speaker's pauses reads as understanding; the same nod on a random timer
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:
362
-
363
- ```js
364
- avatar.setUserSpeaking(true / false); // the server endpointer's own signal
365
- avatar.on('backchannel', (id) => log(id)); // every autonomous ack, announced
366
- ```
367
-
368
- While the user holds the floor the avatar leans in a touch; at pause onsets it
369
- acknowledges within ~250–600ms (probability, refractory gap and nod choice all
370
- tuned from listening-corpus numbers — long user utterances earn the bigger
371
- nods). With no signal attached, the old plausible random cadence remains as 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
375
- suppress themselves around it.
376
-
377
- ### Action timelines
378
-
379
- The composable vocabulary: a server assembles behaviour from the enums above,
380
- timed against the utterance's own audio clock —
381
-
382
- ```js
383
- avatar.perform([
384
- { t: 0, do: 'state', name: 'SPEAKING' },
385
- { t: 900, do: 'gaze', name: 'SCREEN_WORK' },
386
- { t: 2100, do: 'interject', id: 'NOD_SMALL' },
387
- { t: 3000, do: 'emotion', name: 'warm', i: 0.7 },
388
- ], { audio: audioEl }); // clock: explicit fn > audio > elapsed
132
+ createAvatar({ mount, client, face: myna });
389
133
  ```
390
134
 
391
- Verbs: `state`, `emotion`, `gaze`, `interject`. `normalizeActions()` applies
392
- the same hygiene philosophy as `normalizeCues()` — sort, warn-and-drop
393
- malformed entries, never throw mid-performance. A new `perform()` replaces the
394
- running one; `stop()` on the returned handle cancels; `performEnd` fires when
395
- the last action has dispatched. The demo's scripted turns
396
- (`demo/perf-clips.json`) run through exactly this call.
397
-
398
- ### Emotion
399
-
400
- `setEmotion(name, intensity)` — `neutral`, `warm`, `thoughtful`, `concerned`,
401
- `encouraging`, `curious`.
135
+ A name would need a table, and a table is a dynamic index no bundler can shake —
136
+ three drawings in every consumer's bundle to render one.
402
137
 
403
- ### Avatars
138
+ ## Professional avatars
404
139
 
405
- The rig can wear more than one face. Pick one at construction:
140
+ Six complete, code-authored avatars ship as their own `createAvatar` modules:
406
141
 
407
142
  ```js
408
- createAvatar({ mount, avatar: 'wren' }) // by name, from AVATARS
409
- createAvatar({ mount, face: myCreateFace }) // or pass a factory directly
143
+ import { createAvatar } from '@voqalize/avatar/avatars/arjun';
144
+ // or: @voqalize/avatar/avatars/meera
145
+ // @voqalize/avatar/avatars/vikram
146
+ // @voqalize/avatar/avatars/ishita
147
+ // @voqalize/avatar/avatars/kabir
148
+ // @voqalize/avatar/avatars/naina
149
+
150
+ const avatar = createAvatar({ mount, client: pipecatClient });
410
151
  ```
411
152
 
412
- `AVATAR_NAMES` lists what is registered. Two ship today, both line art:
153
+ `arjun`/`meera` are the original interviewer pair; `vikram`/`ishita` and
154
+ `kabir`/`naina` are two further wardrobe/hair directions on the same rig. The
155
+ entry points `interviewer-male`, `interviewer-female`, `professional-male-a`,
156
+ `professional-female-a`, `professional-male-b` and `professional-female-b`
157
+ still work — they are `@deprecated` aliases for the names above, kept so
158
+ existing imports do not break, and should not be used in new code.
413
159
 
414
- | name | module | what it is |
415
- |---|---|---|
416
- | `peep` | `face-peep.js` | the default. Open Peeps–style black-and-white line art, `#f97415` on the collar edge and two buttons. Hand-authored in a 760x950 space, portrait window `92 76 576 800` |
417
- | `wren` | `face-wren.js` | the second line-art character — curls, round glasses, same construction kit, window `92 50 576 800` |
160
+ All six depict Indian professionals in their late twenties, without caricature
161
+ or regional costume cues. They are calibrated at call-tile size and preserve
162
+ all six expression targets, continuous visemes, gaze, blink, head motion, and
163
+ the frame-edge gesture hand. Each module is a complete identity, not a face
164
+ value: import one instead of the default module and do not pass a `face`
165
+ option.
418
166
 
419
- `DEFAULT_AVATAR` is `peep`, and it is the face under active work.
167
+ Their faces use low-contrast, landmark-driven nose planes rather than an
168
+ outlined nose glyph. A few identity-specific freckles or a small mole add depth
169
+ only in low-motion upper-cheek areas; none are used as demographic cues.
420
170
 
421
- There were two others — `classic`, the original hand-authored rig, and
422
- `blue-shirt`, a cleaned auto-trace. Both were removed on 2026-08-06 after
423
- stakeholder review accepted the line-art pair and rejected them: keeping four
424
- rigs meant maintaining art nobody wanted, and every visual fix had to be
425
- weighed against faces that were never going to ship. What they taught the
426
- abstraction outlived them — `face-core.js` exists because all three of the
427
- first rigs wrote the same `apply()`, and `META` exists because all three needed
428
- the same two rects. Both modules are in git history.
171
+ They use the same public contract and the same Pipecat lifecycle/viseme driver
172
+ as the SVG avatars. Their private renderer is Canvas2D; its rig data and bitmap
173
+ wardrobe assets are implementation details and no Canvas or pose API is added
174
+ to the package surface.
429
175
 
430
- `peep` is worth a paragraph because it is built on a rule the retired rigs did
431
- not follow: **it has no strokes anywhere.** Every line is a filled path, which is
432
- what lets a line swell and taper along its length the way a drawn mark does — a
433
- uniform `stroke-width` is the thing that makes vector line art read as clip art.
434
- Three helpers do all of it: `taper` for an open mark, `taperRing` for a closed
435
- annulus, `region` for an enclosed area. Widths are given as a *profile across the
436
- whole mark* in normalized `s ∈ [0,1]`, not per node, so the same profile survives
437
- re-authoring the points.
176
+ ## The 2.5-D characters
438
177
 
439
- An avatar is any module exporting
178
+ Three characters ship as compiled binaries, each its own `createAvatar` module:
440
179
 
441
180
  ```js
442
- createFace(mount, theme) -> { svg, apply(params), theme, destroy() }
443
- META = { viewBox, mouthCrop }
444
- ```
445
-
446
- Nothing above the renderer knows which face it is driving: visemes, emotions,
447
- gaze, idle, clips and the mixer all work in parameter space, so a new avatar
448
- costs no changes anywhere else. `META` is the avatar descriptor — the little a
449
- host or tool may know about a face without opening it: `viewBox` for framing
450
- (exposed as `api.meta`; the demo pages derive tile aspect from it) and
451
- `mouthCrop` for the contact sheet's viseme close-ups.
452
-
453
- The registry in `src/avatar.js` maps names to `{ create, meta }` records. It
454
- was factories-only for the first three faces, deliberately — a schema guessed
455
- from two rigs would have been wrong, and building the third supplied the
456
- evidence for what is genuinely shared. That evidence now lives in code rather
457
- than prose: `src/face-core.js` owns the pose mechanics (lean, shoulders,
458
- parallax) driven by per-rig scalar specs, the shared eye/brow/teeth fragments,
459
- and the renderer shell, so a face module supplies art, feature geometry and a
460
- handful of named scalars. The full recipe — what a new avatar must supply and
461
- what it gets for free — is in
462
- [docs/contract-avatar.md](docs/contract-avatar.md). The hard-won rules stand:
463
-
464
- - **Art units are per-rig, and copying a magnitude between rigs is silent
465
- breakage.** `peep`'s `yawPx` is 28 against the original rig's 13 because they
466
- are art units of different sizes; travels convert through the spec's `units`
467
- factor, degrees never do.
468
- - **A trace supplies static geometry, not a rig.** Anything the source art does
469
- not contain has to be authored, and two rigs can honour the same channel and
470
- mean visibly different things by it. (This is most of why the traced rig was
471
- the one that read worst, and why new avatars are hand-authored.)
472
- - **Layer sets follow the art, not a standard** — the first rig ran 7 layers;
473
- the line-art pair fuse to 4.
474
-
475
- Verify a new avatar against `demo/rig/rig-check.html` (every registered avatar side
476
- by side through the live mixer, plus `sweep()` — a scripted pass over every
477
- state, emotion, gaze, interjection and a viseme track, asserting the params stay
478
- finite and in range and the SVG stays connected); against
479
- `demo/rig/contact-sheet.html?face=<name>` for static poses, including a mouth
480
- close-up row, since visemes are only judgeable at that magnification; and against
481
- `demo/rig/torso-check.html?face=<name>` for the shoulder, lean and head-pose
482
- *combinations*, which is where a rig leaks background from behind the shirt if it
483
- is going to.
484
-
485
- `sweep()` returning `{ok: true}` is not evidence a change looks good — it catches
486
- dead avatars, NaN leaks and detached SVGs, and nothing else. Every defect this
487
- project has found was found by looking at a rendered page.
488
-
489
- Emotion is a separate axis from state on purpose. Fold it into the state enum
490
- and you need `SPEAKING_WARM`, `SPEAKING_CONCERNED`, `LISTENING_WARM`… and the
491
- table is unmaintainable within a week.
492
-
493
- ### Gaze
494
-
495
- `setGaze(name)` or `setGaze('CUSTOM', { x, y })` with normalized −1..1.
496
-
497
- Named targets: `USER`, `USER_EAR`, `SCREEN_CENTER`, `SCREEN_LEFT`,
498
- `SCREEN_RIGHT`, `SCREEN_TOP`, `SCREEN_BOTTOM`, `SCREEN_WORK`, `NOTES`,
499
- `AWAY_THINKING`, `AWAY_DOWN`, `AWAY_RIGHT`. (`USER_EAR` cheats the head aside
500
- while the eyes hold contact — `CANT_HEAR`'s signature; `AWAY_DOWN` is the
501
- thinking direction, because real cognitive aversion leads down, not up.)
502
-
503
- Send the semantic direction and let the client do the oculomotor work. Three
504
- details do the perceptual heavy lifting, and skipping any one reads as a puppet:
505
- the eyes arrive first (ballistic, ~32ms) while the head ambles after at 340ms;
506
- the head deliberately under-rotates and lets the eyes carry the rest; a blink
507
- fires involuntarily on any large shift. The upper lid also rides with vertical
508
- gaze — without that, looking down bares sclera and reads as alarm.
509
-
510
- ### Speaking
181
+ import { createAvatar } from '@voqalize/avatar/avatars/tara';
182
+ // or: @voqalize/avatar/avatars/tushar
183
+ // @voqalize/avatar/avatars/tanya
511
184
 
512
- ```js
513
- avatar.speak({ audio: audioEl, cues }); // audio element drives the clock
514
- avatar.speak({ cues, clock: () => myPlayer.positionMs });
515
- avatar.pushCues(moreCues); // streaming top-up
516
- avatar.stopSpeaking();
517
- avatar.on('speakEnd', () => avatar.setState('LISTENING'));
185
+ const avatar = createAvatar({ mount, client: pipecatClient });
518
186
  ```
519
187
 
520
- `speak()` switches to `SPEAKING` and kills any in-flight spoken interjection
521
- first — barge-in is the normal case, not an error.
522
-
523
- For previewing without a TTS round-trip there's `textToCues(text, { wpm })`, a
524
- crude grapheme guesser. It exists for the demo. Do not ship it.
525
-
526
- ### Interjections
527
-
528
- `interject(id)`. These are the real-time feedback channel — they're what makes
529
- the avatar feel like a listener rather than a player, so they're the part most
530
- worth getting right.
531
-
532
- Each clip is a gesture timeline (head, brows, lids, smile) plus, where spoken, a
533
- hand-tuned viseme track and a **baked plausible duration**, so every clip plays
534
- convincingly with no audio at all.
535
-
536
- **Spoken:** `MM_HMM`, `OKAY`, `YES`, `SURE`, `RIGHT`, `GOT_IT`, `I_SEE`,
537
- `GO_ON`, `ONE_MOMENT`, `TAKE_YOUR_TIME`, `SORRY`, `HMM`
538
- **Wordless:** `NOD_SMALL`, `NOD_SLOW`, `NOD_UP`, `BROW_ACK`, `HEAD_SHAKE`,
539
- `HEAD_SHAKE_SOFT`, `BLINK_LONG`, `WAVE`, `THUMBS_UP`, `SHRUG`, `GO_ON_ARM`
540
- **Floor management:** `CLAIM_FLOOR`, `YIELD_FLOOR`, `RAISE_HAND`
541
-
542
- The nod family follows the measured taxonomy of human listening: `NOD_SMALL`
543
- is the single-cycle continuer, `NOD_SLOW` the two-beat assessment (first beat
544
- biggest — long nods start big and decay), `NOD_UP` the realization nod with the
545
- upward swing, for "ah, *I see*" moments. `HEAD_SHAKE` is the firm no,
546
- `HEAD_SHAKE_SOFT` the sympathetic "not quite" with a head tilt — neither ever
547
- fires autonomously, and nor does `BLINK_LONG`, the deliberate ~600ms blink that
548
- tells a speaker "that's noted, move on" (it measurably shortens answers, which
549
- is exactly why only the server may send it).
550
-
551
- Notes on a few, because the detail is the point:
552
-
553
- - `MM_HMM` — lips stay shut the whole way; the meaning is entirely in the nod.
554
- - `ONE_MOMENT` — breaks eye contact to `AWAY_RIGHT`. That break, not the words,
555
- is what communicates "hold on".
556
- - `SORRY` — driven by `browInner` (AU1, the inner-brow lift). That single
557
- channel is the whole apology.
558
- - `WAVE`, `THUMBS_UP`, `SHRUG`, `GO_ON_ARM`, `RAISE_HAND` — these were arm and
559
- hand gestures. The rig has neither any more, and the IDs stayed because they
560
- are a wire contract the server targets; each was re-authored to say the same
561
- thing from the face, shoulders and torso. `WAVE` is now the eyebrow flash,
562
- which is the greeting display a face makes when an arm is unavailable;
563
- `THUMBS_UP` is a slow deep approving nod; `SHRUG` runs the shoulders to the
564
- top of their range with the mouth corners pulled *down*, because raised
565
- shoulders over a neutral mouth is a flinch rather than an "I don't know".
566
-
567
- To use your own TTS clips, attach audio and the baked track re-schedules against
568
- the real file's clock:
188
+ A photograph of a face projected onto shallow geometry, with the parts that have
189
+ to move — eyes, teeth, the lip line — built as geometry rather than painted.
190
+ Three.js is an *optional* peer (`three`, `>=0.180 <0.187`) behind these three
191
+ entry points only, so an SVG or Canvas consumer never downloads it, and the `.glb`
192
+ is fetched when the avatar mounts.
569
193
 
570
- ```js
571
- import { attachAudio } from './src/avatar.js';
572
- attachAudio('OKAY', '/audio/agent-okay.mp3');
573
- ```
194
+ Nothing above the renderer changes: the same wire, the same states, the same
195
+ cue-synced mouth, and a server that has never heard of these characters drives one
196
+ correctly. The head turns 15° of yaw and 24° of pitch, which is a measured limit
197
+ rather than an option.
574
198
 
575
- Clips ramp in over 70ms and out over 150ms, are interruptible, and a repeat of
576
- the clip already playing collapses rather than stacking.
199
+ **The three binaries are artwork under CC-BY 4.0**, separately from the MIT code
200
+ around them; the credit line is in `assets/README.md`. Mounting, sizing, the asset
201
+ budget and what the characters can be asked to do:
202
+ [characters.md](https://github.com/voqalize/avatar/blob/main/docs/characters.md).
577
203
 
578
- ### Hand gestures
204
+ ## Shipping your own avatar
579
205
 
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.
206
+ A whole different rendering technology is not a face; it is a different
207
+ `createAvatar`, published as its own module — which is why there is no registry.
208
+ The interface is small enough to state in one line:
584
209
 
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.
210
+ ```ts
211
+ createAvatar({ mount, client, ...yourOptions }) -> { destroy() }
212
+ ```
591
213
 
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()`.
214
+ What such an implementation needs to understand is the *wire*, because states,
215
+ actions and cues are all an avatar is ever told. `VisemeTrack` in
216
+ `@voqalize/avatar/internal` turns a cue array plus a clock into the mouth shape
217
+ for the current frame; every renderer needs that and none should write it twice.
597
218
 
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
- ```
219
+ An implementation may also export `supports` — one object naming the action ids
220
+ it answers to. Nothing in the library reads it; it is there because the wire's
221
+ action id is open and an unknown one is ignored in silence, so a page that
222
+ drives an avatar otherwise cannot tell a face that has no such motion from one
223
+ that did nothing ([design-avatar-interface.md](https://github.com/voqalize/avatar/blob/main/docs/design-avatar-interface.md)).
224
+ Omitting it is conforming.
604
225
 
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.
226
+ **There is deliberately no renderer interface.** The pose channels our SVG mixer
227
+ uses to talk to our faces are internal, and a second public contract stays
228
+ premature until a second renderer says what it needs.
607
229
 
608
- ### Misc
230
+ ## What is in this tarball
609
231
 
610
- ```js
611
- avatar.blink(true); // true = double blink
612
- avatar.setMouthGain(1.2); // articulation: scales the viseme shapes as authored
613
- avatar.setGestureGain(0.8); // scales every clip delta
614
- avatar.setMotionGain(0.8); // scales the whole idle/body liveness layer
615
- avatar.setOverrides({ ... }); // direct param injection, for tuning UIs
616
- avatar.setOverrides(null);
617
- avatar.params; // live smoothed parameter vector (read-only)
618
- avatar.state / .emotion / .gaze / .speaking / .clip / .gesturing / .performing
619
- avatar.mouthGain / .gestureGain / .motionGain / .svg / .meta
620
- avatar.destroy();
621
- ```
232
+ `dist/` is the compiled client — `AvatarClient`, the avatar entry points and the
233
+ React binding. `src/` is the widget itself: the mixer, the SVG rig and drawings,
234
+ plus the private Canvas2D interviewer rigs and their assets, as dependency-free
235
+ ES modules with no build step, imported by `dist/` through ordinary relative
236
+ paths. `client/` is the TypeScript those `dist/` files were compiled from, so
237
+ the source maps resolve. `assets/` is the three compiled characters and their
238
+ licence note — the only non-JavaScript thing here, fetched at runtime by URL.
622
239
 
623
- Every setter returns the instance, so calls chain. `params`, `svg` and `meta`
624
- are properties, not methods. (`meta` is the mounted avatar's descriptor — the
625
- call demo sizes its tile from `meta.viewBox`.)
626
-
627
- Events: `state` (new state name), `speakEnd`, `clipEnd` (clip id),
628
- `backchannel` (autonomous ack id), `gestureEnd` (hand gesture id), `performEnd`.
629
-
630
- ---
631
-
632
- ## Files
633
-
634
- | file | |
635
- |---|---|
636
- | `src/params.js` | the parameter space, smoothing constants, channel groups |
637
- | `src/face-core.js` | what every face shares: the renderer shell, pose mechanics, shared feature fragments |
638
- | `src/face-peep.js` | the `peep` avatar — the default. Open Peeps line art. No strokes anywhere: every line is a filled path, so it swells and tapers |
639
- | `src/face-wren.js` | the `wren` avatar — the second line-art character, same kit |
640
- | `src/face-myna.js` | the `myna` avatar — the first built by the staged process, from a reference asset |
641
- | `src/line-art.js` | the stroke engine every line-art rig shares: `taper`, `taperRing`, `region` |
642
- | `src/avatar.d.ts` | hand-maintained types for the public surface. The widget has no compiler; this is written against the contract |
643
- | `src/visemes.js` | A–H protocol, cue hygiene, audio-clock scheduling, server mapping tables |
644
- | `src/emotions.js` | six affect poses |
645
- | `src/gaze.js` | named targets, saccade + head-follow model, micro-saccades |
646
- | `src/idle.js` | per-state liveness profiles, the `ListeningEngine`, autonomous backchannel |
647
- | `src/clips.js` | keyframe player for gesture timelines |
648
- | `src/interjections.js` | the 26 clips |
649
- | `src/hand.js` | the frame-edge hand: four gestures, placed from `META.viewBox` |
650
- | `src/perform.js` | the action-timeline player behind `perform()` |
651
- | `src/avatar.js` | the driving API, the per-frame mixer, and the `AVATARS` registry |
652
- | `client/src/AvatarClient.ts` | the dispatcher: turn-clock anchoring and the cue splice, framework-free |
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>` |
655
- | `client/src/types.ts` | the wire vocabulary in TypeScript. Kept in step with `contract-protocol.md` and `messages.py` |
656
- | `py/src/voqalize_avatar/` | the pipecat backend: state machine, processor, viseme engine, `avatarsync` pool |
657
- | `native/avatarsync/` | the Rhubarb Lip Sync fork — text leg and audio leg — plus its patch and build script |
658
- | `docs/contract-protocol.md` `docs/contract-avatar.md` | the two binding interface contracts |
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 |
661
- | `docs/research-biomechanics.md` | the citations behind the motion constants |
662
- | `tools/` | headless render / sweep / pixel-diff CLI (dev-only dependencies) |
663
- | `serve.py` | the dev server — `Cache-Control: no-store`. Use this one |
664
- | `index.html` | the full harness, driving one avatar as a host would |
665
- | `demo/call.html` | the Meet-style call: VAD, turn-taking, token log. The page to show people |
666
- | `demo/floor.js` | turn-taking — barge-in, backchannel acks, floor claim. A stand-in for the server |
667
- | `demo/vad.js` | mic voice activity — silero via CDN, RMS fallback |
668
- | `demo/perf-clips.json` `demo/perf-audio/` | 16 scripted turns the call demo plays: audio, cue tracks, gesture beats |
669
- | `demo/eval-clips.json` `demo/eval-audio/` | 24 clips the lipsync eval scores against |
670
- | `demo/rig/index.html` | the index for the rig tooling below — the way in |
671
- | `demo/rig/rig-check.html` | every registered avatar side by side through the live mixer; `sweep()` |
672
- | `demo/rig/contact-sheet.html` | static poses for one avatar: every viseme, emotion, gaze, extreme |
673
- | `demo/rig/torso-check.html` | shoulder / lean / trunk-turn / head combinations, which only fail together |
674
- | `demo/rig/body-lab.html` | the rig stepped by hand, seeded — the driver `tools/motion.mjs` measures |
675
- | `demo/rig/clip-strip.html` | one gesture clip as a filmstrip, with the mixer's own smoothing |
676
- | `demo/rig/expression-lab.html` | clip and beat authoring against real audio |
677
- | `demo/rig/lipsync-eval.html` | cue tracks A/B, sphinx vs phonetic |
678
- | `experiments/rhubarb-textsync/` | server-side experiment: A–H letters from text, before the audio exists. Ships nowhere near the widget |
679
-
680
- `face-peep.js` and `face-wren.js` draw four layers each; the original rig drew
681
- seven. Every layer carries a parallax multiplier (0.1 for the body, up to 1.34
682
- on the rig with a separate front fringe), which fakes a head turn convincingly
683
- without any 3-D. The layer set follows the art rather than a standard — the
684
- line-art rigs fuse pairs that a more detailed drawing keeps separate. If you
685
- restyle the character the parameter contract is unchanged; only the face module
686
- needs to know what a face looks like.
687
-
688
- The clip data lives in `demo/` rather than beside the pages that use it, because
689
- `demo/call.html` and both lab pages play the same wavs and one copy of a
690
- hundred of them is the point. The lab pages sit a directory deeper and carry a
691
- `DATA = '../'` constant for the hop back up.
240
+ The contract documents do not ship here. They live in the repository, which is
241
+ where they are kept current:
242
+ [github.com/voqalize/avatar](https://github.com/voqalize/avatar).
692
243
 
693
244
  ## License
694
245
 
695
- **AGPL-3.0-only.** Open source, and deliberately the restrictive end of it: you
696
- may use, modify and self-host this freely, but a modified version offered to
697
- users over a network has to offer them its source too. That is a starting
698
- position taken while the project is young, not a final one — Voqalize holds the
699
- copyright on all of it, so relicensing to something permissive later is a
700
- decision we can simply make.
701
-
702
- Two consequences worth knowing before you build on it:
703
-
704
- - Embedding the widget in a closed-source product is not what this license
705
- permits. If that is what you need, open an issue — a commercial license is a
706
- conversation we are happy to have.
707
- - The `avatarsync` aligner in `native/avatarsync/` is a fork of
708
- [Rhubarb Lip Sync](https://github.com/DanielSWolf/rhubarb-lip-sync), which is
709
- MIT. The prebuilt binaries there statically link pocketsphinx, sphinxbase,
710
- flite, WebRTC, cppformat, GSL, Boost and the CMU acoustic model; upstream's
711
- own notice file for all of them is committed beside them as
712
- `native/avatarsync/UPSTREAM-LICENSE.md`. Those terms are unchanged and travel
713
- with that directory.
714
-
715
- ### Third-party material
716
-
717
- | what | where | terms |
718
- |---|---|---|
719
- | [Open Peeps](https://www.openpeeps.com/) | the drawing *idiom* `peep` is authored in — no artwork is copied | CC0 |
720
- | Rhubarb Lip Sync 1.14.0 | `native/avatarsync/` (fetched at build time, not vendored) | MIT; see `UPSTREAM-LICENSE.md` |
721
- | [piper](https://github.com/OHF-Voice/piper1-gpl) voices `en_US-ljspeech-high`, `en_US-libritts_r-medium` | spoke every wav in `demo/*-audio/` and the fixtures in `py/tests/fixtures/` | LJSpeech is public domain; LibriTTS-R is CC BY 4.0 |
722
- | [`@ricky0123/vad-web`](https://github.com/ricky0123/vad) + onnxruntime-web (silero-vad) | loaded from jsDelivr by `demo/vad.js`, demo only — nothing in `src/` fetches it | MIT |
723
-
724
- The three avatars are original drawings. All demo audio is synthesised from text
725
- written for this repo.
726
-
727
- Releasing is documented in [RELEASING.md](RELEASING.md).
246
+ **MIT for the code, CC-BY 4.0 for the artwork**, and Voqalize holds the
247
+ copyright on all of it. The manifest declares the pair as `MIT AND CC-BY-4.0`;
248
+ the artwork is `assets/*.glb`, the three 2.5-D characters, and `assets/README.md`
249
+ carries the credit line. Everything else in the tarball is MIT, usable anywhere
250
+ including in closed-source products.
251
+
252
+ The drawing idiom `peep` is authored in is
253
+ [Open Peeps](https://www.openpeeps.com/) (CC0) — no artwork is copied. The
254
+ `avatarsync` aligner that produces the mouth shapes is a fork of
255
+ [Rhubarb Lip Sync](https://github.com/DanielSWolf/rhubarb-lip-sync) (MIT) and
256
+ ships in the Python package, not this one.