@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/src/avatar.d.ts CHANGED
@@ -3,28 +3,30 @@
3
3
  *
4
4
  * The widget is dependency-free ES modules with no build step, so there is no
5
5
  * compiler to derive these from; this file is written by hand against
6
- * `docs/contract-protocol.md` (the binding server ↔ widget contract) and a
6
+ * `docs/internal-mixer.md` (the mixer's driving API) and a
7
7
  * reading of `avatar.js`. It lives here rather than in a consumer because it
8
8
  * is only correct next to the code it describes — the previous copy lived in
9
9
  * a vendored tree two repos away and went stale the first time an enum grew.
10
10
  *
11
- * It is deliberately not a conversion of the widget to TypeScript. String-keyed
12
- * enums (state / gaze / emotion / interjection ids) are literal unions for
13
- * editor ergonomics, but every setter also accepts plain `string`, because the
14
- * widget enforces these enums itself at runtime — unknown state and
15
- * interjection ids throw, unknown emotion and gaze fall back silently — and a
16
- * stale `.d.ts` must never claim to be stricter than the code it describes.
11
+ * It is deliberately not a conversion of the widget to TypeScript. The
12
+ * string-keyed enums (state / gaze / emotion / semantic action ids) are closed
13
+ * literal unions, and there is no `| string` escape hatch on any setter: the
14
+ * widget enforces the same enums at runtime — unknown state and action ids
15
+ * throw, unknown emotion and gaze fall back silently — so a caller who wants
16
+ * one of these has a name that is in the union or a bug. Keeping the two in
17
+ * step is this file's whole job; a stale `.d.ts` must never claim to be
18
+ * stricter than the code it describes, so widen the runtime first.
17
19
  */
18
20
 
19
- /** `STATE_NAMES` — see docs/contract-protocol.md § States. */
21
+ /** `STATE_NAMES` — see docs/internal-mixer.md § States. */
20
22
  export type AvatarStateName =
21
23
  | "IDLE"
22
24
  | "LISTENING"
23
25
  | "THINKING"
24
26
  | "SPEAKING"
27
+ | "WORKING"
25
28
  | "REVIEWING_SCREEN"
26
29
  | "WAITING_FOR_USER"
27
- | "TYPING"
28
30
  | "TYPING_CHAT"
29
31
  | "DISTRACTED"
30
32
  | "SEARCHING_SCREEN"
@@ -35,7 +37,7 @@ export type AvatarStateName =
35
37
  | "DEGRADED"
36
38
  | "OFFLINE";
37
39
 
38
- /** `EMOTION_NAMES` — see docs/contract-protocol.md § Emotion. */
40
+ /** `EMOTION_NAMES` — see docs/internal-mixer.md § Emotion. */
39
41
  export type AvatarEmotionName =
40
42
  | "neutral"
41
43
  | "warm"
@@ -44,7 +46,7 @@ export type AvatarEmotionName =
44
46
  | "encouraging"
45
47
  | "thoughtful";
46
48
 
47
- /** `GAZE_NAMES` — see docs/contract-protocol.md § Gaze. `"CUSTOM"` is the escape hatch (any name + a `custom` point works). */
49
+ /** `GAZE_NAMES` — see docs/internal-mixer.md § Gaze. `"CUSTOM"` is the escape hatch (any name + a `custom` point works). */
48
50
  export type AvatarGazeName =
49
51
  | "USER"
50
52
  | "USER_EAR"
@@ -58,73 +60,83 @@ export type AvatarGazeName =
58
60
  | "AWAY_THINKING"
59
61
  | "AWAY_RIGHT"
60
62
  | "AWAY_DOWN"
63
+ | "AWAY_SIDE"
61
64
  | "CUSTOM";
62
65
 
63
- /** `INTERJECTION_IDS` — see docs/contract-protocol.md § Interjections. */
64
- export type AvatarInterjectionId =
65
- | "NOD_SMALL"
66
- | "NOD_SLOW"
67
- | "NOD_UP"
68
- | "BROW_ACK"
69
- | "HEAD_SHAKE"
70
- | "HEAD_SHAKE_SOFT"
71
- | "BLINK_LONG"
72
- | "CLAIM_FLOOR"
73
- | "YIELD_FLOOR"
74
- | "RAISE_HAND"
75
- | "WAVE"
76
- | "THUMBS_UP"
77
- | "SHRUG"
78
- | "GO_ON_ARM"
79
- | "MM_HMM"
80
- | "OKAY"
81
- | "YES"
82
- | "SURE"
83
- | "I_SEE"
84
- | "RIGHT"
85
- | "GO_ON"
86
- | "ONE_MOMENT"
87
- | "SORRY"
88
- | "HMM"
89
- | "GOT_IT"
90
- | "TAKE_YOUR_TIME";
66
+ /** The complete server-addressable action contract. Names are semantic, not
67
+ * anatomical: a future rig may implement `ACK_NOD` without a literal nod. */
68
+ export type AvatarActionId =
69
+ | "ACK_RECEIVE"
70
+ | "ACK_NOD"
71
+ | "RESPONSE_INTERRUPTED"
72
+ | "GESTURE_GREET"
73
+ | "GESTURE_GOODBYE"
74
+ | "GESTURE_APPROVE"
75
+ | "GESTURE_WAIT";
91
76
 
92
- /** `HAND_GESTURE_IDS` — see docs/contract-protocol.md § Hand gestures. A hand at
93
- * the frame edge plus the face half that makes it belong to somebody. Disjoint
94
- * from the interjection ids on purpose: `interject("WAVE")` is still the face
95
- * alone. */
96
- export type AvatarHandGestureId = "HI" | "BYE" | "THUMBS_UP" | "ONE_MOMENT";
97
-
98
- /** Rhubarb Lip Sync letter — see docs/contract-protocol.md § Speech. */
77
+ /** Rhubarb Lip Sync letter — see docs/internal-mixer.md § Speech. */
99
78
  export type VisemeLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "X";
100
79
 
101
80
  /** One viseme cue: `t` is a ms offset into the utterance, `i` is optional 0..1 loudness. */
102
81
  export interface Cue {
103
82
  t: number;
104
- v: string;
83
+ v: VisemeLetter;
84
+ /** 0..1. Absent means full. */
105
85
  i?: number;
106
86
  }
107
87
 
88
+ /** A pose channel from `src/params.js` — see docs/internal-rig.md § The pose channels. */
89
+ export type PoseChannel =
90
+ | "mouthOpen" | "mouthWidth" | "mouthRound" | "mouthPress" | "mouthTuck"
91
+ | "mouthCornerL" | "mouthCornerR" | "teethUpper" | "tongue" | "jaw"
92
+ | "lidL" | "lidR" | "squintL" | "squintR" | "pupilX" | "pupilY"
93
+ | "browRaiseL" | "browRaiseR" | "browAngleL" | "browAngleR"
94
+ | "browInnerL" | "browInnerR"
95
+ | "headYaw" | "headPitch" | "headRoll"
96
+ | "breath" | "shoulderL" | "shoulderR" | "torsoLean" | "torsoTurn";
97
+
98
+ /** A partial pose. Channels are clamped to `RANGE`; absent channels keep the mix. */
99
+ export type PoseOverrides = Partial<Record<PoseChannel, number>>;
100
+
101
+ /** Which hand the character gestures with: `1` the viewer's right (the
102
+ * character's own left), `-1` the other. */
103
+ export type HandSide = 1 | -1;
104
+
105
+ /**
106
+ * A motion scale. `1` is the amplitude the channel was authored at, `0` stills
107
+ * it, and `2` is the documented ceiling — past that the smoothing constants
108
+ * stop holding and clips overshoot their range clamp.
109
+ *
110
+ * TypeScript cannot say "0..2", so this is a named `number` and the check is at
111
+ * the door: the public `createAvatar` throws `RangeError`. The live setters
112
+ * below do not, because they exist for review sliders that already bound
113
+ * themselves and should not throw mid-drag.
114
+ */
115
+ export type Gain = number;
116
+
108
117
  /** Normalized-screen-coordinate escape hatch for `setGaze('CUSTOM', custom)`. */
109
118
  export interface GazeCustom {
110
119
  x: number;
111
120
  y: number;
112
121
  }
113
122
 
114
- /** A `perform()` timeline action — see docs/contract-protocol.md § Composing behavior. */
115
- export interface AvatarAction {
116
- t: number;
117
- do: "state" | "emotion" | "gaze" | "interject" | "gesture";
118
- name?: string;
119
- id?: string;
120
- i?: number;
121
- keepGaze?: boolean;
122
- }
123
+ /**
124
+ * A `perform()` timeline action — see docs/internal-mixer.md § Composing
125
+ * behavior. `t` is a ms offset into the performance. Discriminated on `do`
126
+ * because the verbs do not share their payload: only `action` is addressed by
127
+ * `id`, and only `state` reads `keepGaze`.
128
+ */
129
+ export type AvatarAction =
130
+ | { t: number; do: "state"; name: AvatarStateName; keepGaze?: boolean }
131
+ | { t: number; do: "emotion"; name: AvatarEmotionName; i?: number }
132
+ | { t: number; do: "gaze"; name: AvatarGazeName }
133
+ | { t: number; do: "action"; id: AvatarActionId };
123
134
 
124
135
  export interface SetStateOptions {
125
- emotion?: AvatarEmotionName | (string & {});
136
+ emotion?: AvatarEmotionName;
137
+ /** 0..1 emotion strength. Default 1. */
126
138
  intensity?: number;
127
- gaze?: AvatarGazeName | (string & {});
139
+ gaze?: AvatarGazeName;
128
140
  keepGaze?: boolean;
129
141
  }
130
142
 
@@ -141,12 +153,11 @@ export interface PerformOptions {
141
153
  }
142
154
 
143
155
  export interface PerformHandle {
144
- /** Cancels the *future* of this performance only — see docs/contract-protocol.md. */
156
+ /** Cancels the *future* of this performance only — see docs/internal-mixer.md. */
145
157
  stop: () => void;
146
158
  }
147
159
 
148
- export type AvatarEventName =
149
- | "state" | "speakEnd" | "clipEnd" | "backchannel" | "performEnd" | "gestureEnd";
160
+ export type AvatarEventName = "state" | "speakEnd" | "clipEnd" | "performEnd" | "gestureEnd";
150
161
 
151
162
  /** What a host needs to frame an avatar it has never seen: the drawing's own
152
163
  * window, and where the mouth is inside it. See CLAUDE.md § The two
@@ -158,114 +169,201 @@ export interface AvatarMeta {
158
169
 
159
170
  /** The object `createAvatar()` returns — the whole server-facing surface. */
160
171
  export interface AvatarApi {
161
- setState(name: AvatarStateName | (string & {}), o?: SetStateOptions): AvatarApi;
162
- setEmotion(name: AvatarEmotionName | (string & {}), intensity?: number): AvatarApi;
163
- setGaze(name: AvatarGazeName | (string & {}), custom?: GazeCustom): AvatarApi;
172
+ setState(name: AvatarStateName, o?: SetStateOptions): AvatarApi;
173
+ setEmotion(name: AvatarEmotionName, intensity?: number): AvatarApi;
174
+ setGaze(name: AvatarGazeName, custom?: GazeCustom): AvatarApi;
164
175
  speak(o?: SpeakOptions): AvatarApi;
165
176
  pushCues(cues: Cue[]): AvatarApi;
166
177
  stopSpeaking(): AvatarApi;
167
- interject(id: AvatarInterjectionId | (string & {})): AvatarApi;
168
- gesture(id: AvatarHandGestureId | (string & {})): AvatarApi;
169
- /** +1 the viewer's right (the character's own left hand), -1 the other. */
170
- setHandSide(dir: number): AvatarApi;
178
+ /**
179
+ * Self-completing action: states resolve underneath while motion lands.
180
+ *
181
+ * One core intent, one of `actions`, or one of `sequences` — the vocabulary
182
+ * is open, so an id this face has no word for is a no-op rather than a
183
+ * throw. `ACKNOWLEDGE` resolves to whichever of this body's acknowledgements
184
+ * the moment calls for.
185
+ */
186
+ action(id: string): AvatarApi;
187
+ setHandSide(dir: HandSide): AvatarApi;
171
188
  perform(actions: AvatarAction[], o?: PerformOptions): PerformHandle;
172
189
  setUserSpeaking(speaking: boolean | null): AvatarApi;
173
- setMouthGain(g: number): AvatarApi;
174
- readonly mouthGain: number;
175
- setGestureGain(g: number): AvatarApi;
176
- readonly gestureGain: number;
177
- setMotionGain(g: number): AvatarApi;
178
- readonly motionGain: number;
190
+ /** Hold user gaze for a short interaction window; currently JS-level only. */
191
+ attend(ms?: number): AvatarApi;
192
+ setMouthGain(g: Gain): AvatarApi;
193
+ readonly mouthGain: Gain;
194
+ setGestureGain(g: Gain): AvatarApi;
195
+ readonly gestureGain: Gain;
196
+ setMotionGain(g: Gain): AvatarApi;
197
+ readonly motionGain: Gain;
179
198
  blink(double?: boolean): AvatarApi;
180
199
  /** Advance one frame by hand — only meaningful under `{ manual: true }`. */
181
200
  step(dt: number): AvatarApi;
182
- setOverrides(o: Record<string, number> | null): AvatarApi;
201
+ /** Pin channels to fixed values, above the whole mix. `null` releases. */
202
+ setOverrides(o: PoseOverrides | null): AvatarApi;
183
203
  on(event: "state", fn: (name: AvatarStateName) => void): AvatarApi;
184
204
  on(event: "speakEnd" | "performEnd", fn: () => void): AvatarApi;
185
- on(event: "clipEnd" | "backchannel" | "gestureEnd", fn: (id: string) => void): AvatarApi;
205
+ on(event: "clipEnd" | "gestureEnd", fn: (id: string) => void): AvatarApi;
186
206
  on(event: AvatarEventName, fn: (...args: unknown[]) => void): AvatarApi;
187
207
  readonly state: AvatarStateName;
188
208
  readonly emotion: AvatarEmotionName;
189
209
  readonly gaze: AvatarGazeName;
190
210
  readonly speaking: boolean;
191
211
  readonly performing: boolean;
212
+ /** Internal clip id in flight. Not a contract — the clip catalog is private
213
+ * to this renderer, which is why this is the one open string here. */
192
214
  readonly clip: string | null;
193
- /** The hand gesture in flight, or null — always null under `hand: false`. */
194
- readonly gesturing: AvatarHandGestureId | null;
195
- readonly params: Record<string, number>;
215
+ /** Semantic hand action in flight, including for a non-SVG custom rig. */
216
+ readonly gesturing: AvatarActionId | null;
217
+ readonly params: Readonly<Record<PoseChannel, number>>;
196
218
  readonly userSpeaking: boolean;
197
- readonly svg: SVGSVGElement;
198
- readonly meta: AvatarMeta;
219
+ /** Legacy SVG inspection fields; null for a renderer-neutral AvatarRig. */
220
+ readonly svg: SVGSVGElement | null;
221
+ readonly meta: AvatarMeta | null;
199
222
  destroy(): void;
200
223
  }
201
224
 
202
225
  /** A face module's factory — `createFace(mount, theme)`. See
203
- * docs/contract-avatar.md § Adding a new avatar. */
226
+ * docs/authoring-a-face.md § Adding a new avatar. */
204
227
  export type FaceFactory = (
205
228
  mount: Element,
206
- theme?: unknown,
229
+ theme?: FaceTheme,
207
230
  ) => {
208
231
  svg: SVGSVGElement;
209
- apply: (params: Record<string, number>) => void;
210
- theme: unknown;
232
+ apply: (params: Readonly<Record<PoseChannel, number>>) => void;
233
+ theme: FaceTheme;
211
234
  destroy: () => void;
212
235
  };
213
236
 
237
+ /** A face's palette: CSS colour strings by role. Keys are the face's own
238
+ * (`THEME` in its module) — a shared key set was tried and each drawing wanted
239
+ * different roles. See CLAUDE.md on why `peep` has no second palette. */
240
+ export type FaceTheme = Readonly<Record<string, string>>;
241
+
242
+ /** One drawn face: what to build, and how to frame it. Import one from
243
+ * `@voqalize/avatar/faces/<name>`; nothing resolves a face by string. */
244
+ export interface Face {
245
+ readonly create: FaceFactory;
246
+ readonly meta: AvatarMeta;
247
+ }
248
+
214
249
  export interface CreateAvatarOptions {
215
250
  /** Element, or CSS selector resolved via `document.querySelector`. */
216
251
  mount: string | Element;
217
- /** Name from `AVATAR_NAMES`. Defaults to `DEFAULT_AVATAR`. */
218
- avatar?: string;
219
- /** A bare face factory, for an avatar the registry doesn't know about.
220
- * `meta` then falls back to the svg's own viewBox. */
221
- face?: FaceFactory;
222
- theme?: unknown;
223
- mouthGain?: number;
224
- gestureGain?: number;
225
- motionGain?: number;
226
- /** Withhold the frame-edge hand entirely — for a face drawn in some other
227
- * idiom, or a tile too small to spend the pixels. `gesture()` then degrades
228
- * to the gesture's face half. Default true. */
252
+ /** The face to wear. Required unless `rig` replaces the renderer outright. */
253
+ face?: Face;
254
+ /** Renderer-neutral rig factory. It replaces the SVG face implementation;
255
+ * no SVG or metadata is required. See docs/internal-rig.md. */
256
+ rig?: import("./rig.js").AvatarRigFactory;
257
+ /** Passed to `rig` verbatim. Opaque here on purpose — it belongs to whoever
258
+ * wrote the rig, and this file has no way to know its shape. */
259
+ rigOptions?: unknown;
260
+ theme?: FaceTheme;
261
+ mouthGain?: Gain;
262
+ gestureGain?: Gain;
263
+ motionGain?: Gain;
264
+ /** Speech-rhythm head motion (beats, phrase drift), 0..2. A per-rig
265
+ * calibration: the same pose unit is a different angle on every rig. */
266
+ prosodyHeadGain?: Gain;
267
+ /** Speech-rhythm face motion (brows, lids, turn-edge warmth), 0..2. */
268
+ prosodyFaceGain?: Gain;
269
+ /** The share of a sustained head turn the trunk takes up, 0..1. Default
270
+ * 0.45; a per-rig calibration, like `prosodyHeadGain`. */
271
+ trunkFollow?: number;
272
+ /**
273
+ * How far this face may *hold* its head off centre, per axis, in pose units.
274
+ * The layers that hold a pose — attitude, gaze, a phrase's pose, the idle
275
+ * posture — are folded into this together; strokes, beats and clip deltas
276
+ * are not, so a nod keeps its peak. An axis left out is unbudgeted.
277
+ *
278
+ * A per-rig fact and a measured one: what a face can hold before its
279
+ * rendering gives it away is a property of that drawing or that photograph,
280
+ * and the library has no way to guess it.
281
+ */
282
+ headHold?: Readonly<Partial<Record<"headYaw" | "headPitch" | "headRoll", number>>>;
283
+ /** Disable only the bundled SVG hand renderer. A custom rig still receives
284
+ * first-class `frame.hand` controls for every gesture action. Default true. */
229
285
  hand?: boolean;
230
- /** Which hand the character gestures with: +1 the viewer's right. */
231
- handSide?: number;
286
+ handSide?: HandSide;
232
287
  /** Withhold the rAF loop so a tool can drive frames itself via `step(dt)`. */
233
288
  manual?: boolean;
289
+ /**
290
+ * This avatar's own addressable motions, on top of the core intents. A
291
+ * server names one with the wire's `action` command, same as a core one; a
292
+ * face that does not have the name ignores it. Keyed by id, each value a clip
293
+ * in the same shape `ACTIONS` uses. Cannot shadow one of this renderer's own
294
+ * — `ACTIONS` wins.
295
+ */
296
+ sequences?: Readonly<Record<string, unknown>>;
297
+ /**
298
+ * This avatar's own shape for one of this renderer's actions, keyed by its
299
+ * id. Same id, same intent, a rendering sized for this body — the shared
300
+ * clips are authored for a line face. An id this renderer does not publish
301
+ * throws: this can reshape the vocabulary, never add to it.
302
+ */
303
+ actions?: Readonly<Record<string, unknown>>;
304
+ /**
305
+ * This avatar's own rendering of a state, keyed by state name. Each value
306
+ * replaces that state's fields whole (`pose`, `idle`, …); the rest are the
307
+ * shared table's. A name that is not a state throws: this can re-render the
308
+ * vocabulary, never add to it.
309
+ */
310
+ states?: Readonly<Partial<Record<AvatarStateName, Readonly<Record<string, unknown>>>>>;
234
311
  }
235
312
 
236
313
  export function createAvatar(opts: CreateAvatarOptions): AvatarApi;
237
314
 
238
- /** The registry: `{ create, meta }` per avatar. */
239
- export const AVATARS: Record<string, { create: FaceFactory; meta: AvatarMeta }>;
240
- export const AVATAR_NAMES: string[];
241
- export const DEFAULT_AVATAR: string;
242
-
243
315
  /** Per-state base pose + idle profile. Read-only in practice — the mixer owns it. */
244
- export const STATES: Record<string, Record<string, unknown>>;
245
- export const STATE_NAMES: AvatarStateName[];
246
- export const GAZE_NAMES: AvatarGazeName[];
247
- export const GAZE_TARGETS: Record<string, { x: number; y: number }>;
248
- export const EMOTION_NAMES: AvatarEmotionName[];
249
- export const INTERJECTIONS: Record<string, unknown>;
250
- export const INTERJECTION_IDS: AvatarInterjectionId[];
251
- export const HAND_GESTURES: Record<string, Record<string, unknown>>;
252
- export const HAND_GESTURE_IDS: AvatarHandGestureId[];
316
+ export const STATES: Readonly<Record<AvatarStateName, Readonly<Record<string, unknown>>>>;
317
+ export const STATE_NAMES: readonly AvatarStateName[];
318
+ export const GAZE_NAMES: readonly AvatarGazeName[];
319
+ export const GAZE_TARGETS: Readonly<Record<AvatarGazeName, { x: number; y: number }>>;
320
+ export const EMOTION_NAMES: readonly AvatarEmotionName[];
321
+ export const ACTIONS: Readonly<Record<AvatarActionId, unknown>>;
322
+ export const ACTION_IDS: readonly AvatarActionId[];
253
323
  /** Asserts the two framing rules against a face's own window. Throws on
254
- * violation — `tools/sweep.mjs` runs it for every registered avatar. */
324
+ * violation — `pnpm test` runs it for every registered avatar. */
255
325
  export function checkHandFraming(meta: AvatarMeta): {
256
326
  ok: true;
257
327
  wristDrop: number;
258
328
  outboardLimit: number;
259
329
  worst: Record<string, number>;
260
330
  };
261
- /** The subset that has (or expects) audio — the rest are silent gestures. */
262
- export const SPOKEN_IDS: AvatarInterjectionId[];
263
- export const VISEME_LETTERS: VisemeLetter[];
264
- export const VISEME_SHAPES: Record<string, Record<string, number>>;
331
+ /** The neutral pose: every channel's resting value (`src/params.js`). */
332
+ export const REST: Readonly<Record<PoseChannel, number>>;
333
+ /** Every channel name, in `REST` order. */
334
+ export const CHANNELS: readonly PoseChannel[];
335
+ /** Post-mix clamp per channel, `[min, max]`. */
336
+ export const RANGE: Readonly<Record<PoseChannel, readonly [number, number]>>;
337
+ export const VISEME_LETTERS: readonly VisemeLetter[];
338
+ export const VISEME_SHAPES: Readonly<Record<VisemeLetter, PoseOverrides>>;
265
339
  /** Cues lead the audio by this many ms — perceptual tolerance is asymmetric. */
266
340
  export const LEAD_MS: number;
267
- export const ARPABET_TO_VISEME: Record<string, string>;
268
- export const AZURE_VISEME_TO_LETTER: Record<number, string>;
341
+ /** The shoulder line's share of a held `headRoll`, per pose unit. */
342
+ export const SHOULDER_TILT: number;
343
+ export const ARPABET_TO_VISEME: Readonly<Record<string, VisemeLetter>>;
344
+ export const AZURE_VISEME_TO_LETTER: Readonly<Record<number, VisemeLetter>>;
345
+
346
+ /** The silent/rest letter, `"X"`. */
347
+ export const SILENT: VisemeLetter;
348
+ /** Pose channels for one letter at `intensity` (0..1), ready to merge into a frame. */
349
+ export function shapeFor(letter: VisemeLetter, intensity?: number): PoseOverrides;
350
+
351
+ /**
352
+ * The mouth clock. Someone has to turn a cue array plus a clock into "which
353
+ * letter is on screen right now"; every renderer needs exactly that and none
354
+ * should write it twice, so it is a class to construct rather than a contract
355
+ * to implement. `sample()` returns `null` when the track is done.
356
+ */
357
+ export class VisemeTrack {
358
+ /** @param clock elapsed ms of the audio being played. */
359
+ start(cues: Cue[], clock: () => number): void;
360
+ /** Streaming top-up: append cues that arrive mid-utterance. */
361
+ push(cues: Cue[]): void;
362
+ stop(): void;
363
+ sample(): { letter: VisemeLetter; intensity: number } | null;
364
+ onEnd: (() => void) | null;
365
+ readonly playing: boolean;
366
+ }
269
367
 
270
368
  export function attachAudio(id: string, url: string): void;
271
369
  export function normalizeActions(actions: AvatarAction[]): AvatarAction[];