@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
@@ -26,9 +26,13 @@ const nod = (peak, at = 0.24, rebound = 0.56) => [
26
26
  [0, 0], [at, peak], [rebound, -peak * 0.24], [1, 0],
27
27
  ];
28
28
 
29
- export const INTERJECTIONS = {
29
+ // This is the authoring library. It intentionally contains exploratory clips
30
+ // as well as production ones; only the small `ACTIONS` export below is
31
+ // part of the public action contract.
32
+ const CLIPS = {
30
33
  // ---------------------------------------------------------------------
31
- // Pure backchannel โ€” no words, fired automatically while LISTENING.
34
+ // Wordless acknowledgement clips. Every one is an explicit backend or
35
+ // application decision; the client never fires them from VAD or a timer.
32
36
  // ---------------------------------------------------------------------
33
37
  // The nod peaks below are pre-compensated for the head's 160ms time constant.
34
38
  // Authored at their perceptual value (~0.3) they rendered at ~0.6x and read as
@@ -44,26 +48,27 @@ export const INTERJECTIONS = {
44
48
  // drops further than the trend โ€” and everything stays under the 1.5 Hz line
45
49
  // where sustained attention turns into impatience.
46
50
  NOD_SMALL: {
47
- id: 'NOD_SMALL', label: 'nod (continuer)', text: '', duration: 800,
51
+ id: 'NOD_SMALL', label: 'nod (continuer)', text: '', duration: 900,
48
52
  keys: {
49
- // One cycle: single-cycle nods are 42% of the corpus, and the continuer
50
- // is the quietest citizen of the family โ€” "go on", not "I agree".
51
- headPitch: nod(0.55, 0.22, 0.52),
52
- browRaiseL: [[0, 0], [0.3, 0.14], [1, 0]],
53
- browRaiseR: [[0, 0], [0.3, 0.14], [1, 0]],
53
+ // One readable stroke, not an ambient bob. The peak is deliberately
54
+ // authored above its desired rendered travel because the common head
55
+ // smoothing absorbs a fast impulse before it reaches the SVG.
56
+ headPitch: [[0, 0], [0.25, 0.74], [0.38, 0.70], [0.68, -0.12], [1, 0]],
57
+ browRaiseL: [[0, 0], [0.34, 0.16], [1, 0]],
58
+ browRaiseR: [[0, 0], [0.34, 0.16], [1, 0]],
54
59
  },
55
60
  },
56
61
  NOD_SLOW: {
57
- id: 'NOD_SLOW', label: 'nod (assessment, two-beat)', text: '', duration: 1420,
62
+ id: 'NOD_SLOW', label: 'nod (assessment / receipt)', text: '', duration: 1200,
58
63
  keys: {
59
- // Two cycles at ~1.4 Hz. The first beat is the deepest thing any nod
60
- // does (anticipatory rising) and the second sits well below it โ€”
61
- // declination plus final lowering, which is what makes the gesture
62
- // resolve instead of merely stopping. The lift between beats crosses
63
- // slightly above rest so the cycles read as separate strokes.
64
- headPitch: [[0, 0], [0.16, 0.85], [0.38, -0.10], [0.60, 0.55], [0.85, -0.06], [1, 0]],
65
- mouthCornerL: [[0, 0], [0.45, 0.12], [1, 0]],
66
- mouthCornerR: [[0, 0], [0.45, 0.12], [1, 0]],
64
+ // A receipt needs an arrival. The former two fast peaks registered as
65
+ // idle bobbing at call-tile size, so this is one deep downstroke with a
66
+ // short dwell and a slower recovery. The tiny torso commitment keeps the
67
+ // head from looking like it is sliding independently of the body.
68
+ headPitch: [[0, 0], [0.18, 0.18], [0.39, 1.06], [0.52, 1.02], [0.78, -0.16], [1, 0]],
69
+ torsoLean: [[0, 0], [0.40, 0.09], [0.58, 0.07], [1, 0]],
70
+ browRaiseL: [[0, 0], [0.23, 0.12], [0.62, 0.08], [1, 0]],
71
+ browRaiseR: [[0, 0], [0.23, 0.10], [0.62, 0.07], [1, 0]],
67
72
  },
68
73
  },
69
74
  // The "ah โ€” I see" nod. What separates it from agreement is the upswing:
@@ -88,13 +93,126 @@ export const INTERJECTIONS = {
88
93
  keys: {
89
94
  browRaiseL: [[0, 0], [0.24, 0.30], [0.7, 0.06], [1, 0]],
90
95
  browRaiseR: [[0, 0], [0.24, 0.26], [0.7, 0.06], [1, 0]],
91
- headPitch: [[0, 0], [0.3, 0.08], [1, 0]],
92
96
  mouthCornerL: [[0, 0], [0.35, 0.14], [1, 0]],
93
97
  mouthCornerR: [[0, 0], [0.35, 0.14], [1, 0]],
94
98
  },
95
99
  blinkAt: [0.22],
96
100
  },
97
- // The disagree family follows the same three laws as the nods (ยง3.4): the
101
+ // ---------------------------------------------------------------------
102
+ // Understanding beats โ€” eye/face/body composites, deliberately separate
103
+ // from nods. They are the shared low-authoring-cost alternative when a
104
+ // portrait's pitch geometry is too limited to carry acknowledgement alone.
105
+ // ---------------------------------------------------------------------
106
+ ACK_CONTINUE: {
107
+ id: 'ACK_CONTINUE', label: 'acknowledge: continue', text: '', duration: 620,
108
+ keys: {
109
+ // Eyes acknowledge first; nothing here claims agreement or closes the
110
+ // conversational floor while the user is still talking.
111
+ browRaiseL: [[0, 0], [0.18, 0.14], [0.48, 0.06], [1, 0]],
112
+ browRaiseR: [[0, 0], [0.18, 0.12], [0.48, 0.05], [1, 0]],
113
+ lidL: [[0, 0], [0.20, 0.035], [0.58, 0.01], [1, 0]],
114
+ lidR: [[0, 0], [0.20, 0.035], [0.58, 0.01], [1, 0]],
115
+ },
116
+ },
117
+ ACK_RECEIVE: {
118
+ id: 'ACK_RECEIVE', label: 'acknowledge: received', text: '', duration: 1120,
119
+ keys: {
120
+ // Recognition โ†’ take it in โ†’ settle. The face does not nod; it lands in
121
+ // a quiet, held receipt, leaving room for the user to see the response.
122
+ // Recognition is quick: everything arrives in the first ~150 ms and then
123
+ // holds. It used to build over the first third of the clip with the lids
124
+ // half-lowered and the trunk sinking, and a video reviewer read that as
125
+ // the head drifting down โ€” "looks like losing consciousness". The lids
126
+ // are a flicker now, not a droop, and the lean is a small forward set.
127
+ browRaiseL: [[0, 0], [0.09, 0.17], [0.30, 0.06], [1, 0]],
128
+ browRaiseR: [[0, 0], [0.09, 0.14], [0.30, 0.05], [1, 0]],
129
+ lidL: [[0, 0], [0.10, 0.09], [0.30, 0.04], [0.70, 0.02], [1, 0]],
130
+ lidR: [[0, 0], [0.10, 0.09], [0.30, 0.04], [0.70, 0.02], [1, 0]],
131
+ mouthPress: [[0, 0], [0.13, 0.20], [0.60, 0.15], [1, 0]],
132
+ mouthCornerL: [[0, 0], [0.18, 0.09], [0.80, 0.055], [1, 0]],
133
+ mouthCornerR: [[0, 0], [0.18, 0.09], [0.80, 0.055], [1, 0]],
134
+ torsoLean: [[0, 0], [0.14, 0.07], [0.70, 0.05], [1, 0]],
135
+ shoulderL: [[0, 0], [0.16, 0.060], [0.70, 0.035], [1, 0]],
136
+ shoulderR: [[0, 0], [0.16, 0.060], [0.70, 0.035], [1, 0]],
137
+ },
138
+ },
139
+ ACK_REALIZE: {
140
+ id: 'ACK_REALIZE', label: 'acknowledge: realization', text: '', duration: 980,
141
+ keys: {
142
+ // The brows and widened lid lead: this means "the point connected", not
143
+ // the generic social smile which looks like agreement in a small tile.
144
+ browRaiseL: [[0, 0], [0.15, 0.36], [0.42, 0.20], [1, 0]],
145
+ browRaiseR: [[0, 0], [0.15, 0.32], [0.42, 0.18], [1, 0]],
146
+ lidL: [[0, 0], [0.16, -0.07], [0.48, -0.025], [1, 0]],
147
+ lidR: [[0, 0], [0.16, -0.07], [0.48, -0.025], [1, 0]],
148
+ mouthCornerL: [[0, 0], [0.35, 0.12], [0.80, 0.09], [1, 0]],
149
+ mouthCornerR: [[0, 0], [0.35, 0.12], [0.80, 0.09], [1, 0]],
150
+ torsoLean: [[0, 0], [0.38, 0.07], [0.72, 0.05], [1, 0]],
151
+ },
152
+ },
153
+ ACK_EMPATHIZE: {
154
+ id: 'ACK_EMPATHIZE', label: 'acknowledge: empathy', text: '', duration: 1180,
155
+ keys: {
156
+ // Inner brows and softened lids acknowledge the affect without signalling
157
+ // agreement. Keep the mouth nearly neutral for professional contexts.
158
+ browInnerL: [[0, 0], [0.22, 0.24], [0.68, 0.16], [1, 0]],
159
+ browInnerR: [[0, 0], [0.22, 0.20], [0.68, 0.13], [1, 0]],
160
+ browRaiseL: [[0, 0], [0.22, 0.07], [0.68, 0.05], [1, 0]],
161
+ browRaiseR: [[0, 0], [0.22, 0.06], [0.68, 0.04], [1, 0]],
162
+ lidL: [[0, 0], [0.25, 0.06], [0.72, 0.04], [1, 0]],
163
+ lidR: [[0, 0], [0.25, 0.06], [0.72, 0.04], [1, 0]],
164
+ mouthPress: [[0, 0], [0.34, 0.06], [0.72, 0.045], [1, 0]],
165
+ torsoLean: [[0, 0], [0.40, 0.065], [0.76, 0.045], [1, 0]],
166
+ },
167
+ },
168
+ // A comparison nod, not the director's default. Two quick decaying strokes
169
+ // with a barely-there listening tilt reflect the common conversational form
170
+ // the current single deep receipt does not show. It stays comparison-only:
171
+ // the host can decide whether this faster cadence fits its conversation.
172
+ // **Slower than it was, and that is what made it land.** This nod ran two
173
+ // down-beats 381 ms apart โ€” 2.6 Hz, which is past the 1.5 Hz line CLAUDE.md
174
+ // and research-biomechanics.md ยง 3.4 both draw between "I'm with you" and
175
+ // "hurry up", and which the head's own 160 ms smoothing attenuates to about a
176
+ // third. Authoring it louder was the obvious fix and the wrong one: the
177
+ // gesture was not too small, it was too *fast* to survive the mass of the
178
+ // thing it was moving.
179
+ //
180
+ // At 780 ms between down-beats it is 1.28 Hz, attenuated to about 0.64
181
+ // instead of 0.36, and 1.5 s long โ€” which is where the research puts this
182
+ // kind of nod anyway. ยง 3.3 separates a `short` continuer (0.83 s, small)
183
+ // from a `long` assessment nod (1.42 s, large range), and an acknowledgement
184
+ // of what someone just said is the second. ยง 3.4's two structural laws hold:
185
+ // the first cycle is the biggest, and each one decays.
186
+ //
187
+ // **No opening up-beat, and the corpus is why.** An earlier pass gave this a
188
+ // small preparatory lift on the theory that a nod winds up before it starts.
189
+ // ยง 3.3 separates `long` (large range, *no* swing-up) from `long_p` (large
190
+ // range *with* one, reading as a cognitive shift), so a lift here was quietly
191
+ // making agreement look like realisation. It also broke the rate: the beat
192
+ // sat 330 ms before the first peak, a 1.52 Hz opening half-cycle in a clip
193
+ // whose two down-beats are a comfortable 1.28 Hz apart.
194
+ ACK_NOD: {
195
+ id: 'ACK_NOD', label: 'acknowledge: nod', text: '', duration: 1500,
196
+ keys: {
197
+ // Down, up through neutral, down again, settle. The trailing โˆ’0.08 flourish
198
+ // that used to sit at 0.94 is gone: it put two *return* beats 570 ms apart,
199
+ // a 1.75 Hz oscillation inside a clip whose down-beats are a comfortable
200
+ // 1.28 Hz. A gesture's last move should be releasing, not one more beat.
201
+ // Each down is the stroke and each up the recovery, so the downs are
202
+ // fast and the ups take their time: a symmetric ramp on both sides read
203
+ // as "a very slow, linear dip" on video. The downs still sit 720 ms
204
+ // apart (1.39 Hz), and a short hold at the bottom is what gives the
205
+ // stroke its weight.
206
+ headPitch: [[0, 0], [0.16, 1.10], [0.24, 1.00], [0.52, -0.20], [0.64, 0.66], [0.72, 0.58], [1, 0]],
207
+ headRoll: [[0, 0], [0.18, -0.090], [0.70, -0.070], [1, 0]],
208
+ torsoLean: [[0, 0], [0.32, 0.130], [0.70, 0.085], [1, 0]],
209
+ shoulderL: [[0, 0], [0.44, 0.062], [0.76, 0.036], [1, 0]],
210
+ shoulderR: [[0, 0], [0.44, 0.062], [0.76, 0.036], [1, 0]],
211
+ lidL: [[0, 0], [0.30, 0.070], [0.68, 0.040], [1, 0]],
212
+ lidR: [[0, 0], [0.30, 0.070], [0.68, 0.040], [1, 0]],
213
+ },
214
+ },
215
+ // The disagree family follows the same three laws as explicit nods (ยง3.4): the
98
216
  // first swing is the biggest, every cycle decays, and the whole gesture
99
217
  // stays at or under ~1.5 Hz. Both are SERVER-SENT ONLY โ€” an agent must
100
218
  // never disagree autonomously, so neither is in the listening engine's
@@ -211,6 +329,24 @@ export const INTERJECTIONS = {
211
329
  blinkAt: [0.1],
212
330
  },
213
331
 
332
+ /** Server-confirmed cut-off after playout stopped. This is not a state: the
333
+ * clip explains the abrupt transition, then lands in whatever factual pose
334
+ * Pipecat has resolved underneath it. */
335
+ RESPONSE_INTERRUPTED: {
336
+ id: 'RESPONSE_INTERRUPTED', label: 'response: interrupted', text: '', duration: 1550,
337
+ keys: {
338
+ mouthOpen: [[0, 0], [0.06, 0.36], [0.80, 0.36], [1, 0]],
339
+ jaw: [[0, 0], [0.08, 0.20], [0.80, 0.20], [1, 0]],
340
+ browRaiseL: [[0, 0], [0.12, 0.46], [0.72, 0.34], [1, 0]],
341
+ browRaiseR: [[0, 0], [0.12, 0.40], [0.72, 0.30], [1, 0]],
342
+ lidL: [[0, 0], [0.12, -0.18], [0.72, -0.12], [1, 0]],
343
+ lidR: [[0, 0], [0.12, -0.16], [0.72, -0.10], [1, 0]],
344
+ torsoLean: [[0, 0], [0.18, -0.16], [0.72, -0.10], [1, 0]],
345
+ shoulderL: [[0, 0], [0.18, -0.12], [0.72, -0.08], [1, 0]],
346
+ shoulderR: [[0, 0], [0.18, -0.12], [0.72, -0.08], [1, 0]],
347
+ },
348
+ },
349
+
214
350
  /**
215
351
  * "May I come in." This used to be a raised palm, and it is now the same
216
352
  * signal without one: the body claims a little space and the face asks. The
@@ -273,8 +409,8 @@ export const INTERJECTIONS = {
273
409
  * oscillation left to fill the time; a held smile past about a second stops
274
410
  * being a greeting and becomes an expression.
275
411
  */
276
- WAVE: {
277
- id: 'WAVE', label: 'greet (brow flash)', text: '', duration: 1300,
412
+ GESTURE_GREET: {
413
+ id: 'GESTURE_GREET', label: 'gesture: greet', text: '', duration: 1300,
278
414
  keys: {
279
415
  browRaiseL: [[0, 0], [0.11, 0.85], [0.4, 0.22], [0.75, 0.12], [1, 0]],
280
416
  browRaiseR: [[0, 0], [0.11, 0.80], [0.4, 0.20], [0.75, 0.10], [1, 0]],
@@ -304,8 +440,8 @@ export const INTERJECTIONS = {
304
440
  * Deep enough to be unmistakable: peak 0.62 against NOD_SMALL's 0.52, over
305
441
  * nearly three times the duration.
306
442
  */
307
- THUMBS_UP: {
308
- id: 'THUMBS_UP', label: 'approve', text: '', duration: 1500,
443
+ GESTURE_APPROVE: {
444
+ id: 'GESTURE_APPROVE', label: 'gesture: approve', text: '', duration: 1500,
309
445
  keys: {
310
446
  headPitch: [[0, 0], [0.3, 0.62], [0.58, 0.20], [0.78, 0.34], [1, 0]],
311
447
  mouthCornerL: [[0, 0], [0.36, 0.58], [0.8, 0.48], [1, 0]],
@@ -473,8 +609,8 @@ export const INTERJECTIONS = {
473
609
  torsoLean: [[0, 0], [0.25, 0.10], [0.7, 0.08], [1, 0]],
474
610
  },
475
611
  },
476
- ONE_MOMENT: {
477
- id: 'ONE_MOMENT', label: 'one moment', text: 'one moment', duration: 1350,
612
+ GESTURE_WAIT: {
613
+ id: 'GESTURE_WAIT', label: 'gesture: wait', text: 'one moment', duration: 1350,
478
614
  // w ยท สŒ ยท n ยท m ยท oสŠ ยท m ยท ษ™ ยท n ยท t
479
615
  mouthCues: [
480
616
  { t: 0, v: 'F' }, { t: 110, v: 'C' }, { t: 210, v: 'A' }, { t: 320, v: 'F' },
@@ -557,19 +693,52 @@ export const INTERJECTIONS = {
557
693
  },
558
694
  };
559
695
 
560
- export const INTERJECTION_IDS = Object.keys(INTERJECTIONS);
696
+ /** Full local authoring library. Not a server action vocabulary. */
697
+ export const INTERNAL_CLIPS = CLIPS;
561
698
 
562
- /** Clips that carry words, vs. wordless backchannel. */
563
- export const SPOKEN_IDS = INTERJECTION_IDS.filter((k) => INTERJECTIONS[k].text);
699
+ /**
700
+ * This renderer's own addressable actions โ€” the bundled SVG faces' catalogue,
701
+ * not the wire's vocabulary, which is open and has only two required ids
702
+ * (`docs/contract-wire.md` ยง Action).
703
+ *
704
+ * Every one of these is a name a server may send *once it knows this renderer
705
+ * is mounted*, and nothing here is a name it may send blind. Two of them are
706
+ * what `ACKNOWLEDGE` resolves to on the floor โ€” `ACK_NOD` while the user still
707
+ * has it, `ACK_RECEIVE` once they have stopped โ€” and `RESPONSE_INTERRUPTED` is
708
+ * required of every avatar, so it appears here as this renderer's shape for it
709
+ * rather than as its own idea. The four `GESTURE_*` are things this body does
710
+ * and no protocol asks for.
711
+ *
712
+ * Naming is `CATEGORY_INTENT`, and the list doubles as the conformance sweep's
713
+ * roster: `ACKNOWLEDGE` is absent on purpose, because sweeping both shapes it
714
+ * resolves to covers strictly more.
715
+ */
716
+ export const ACTION_IDS = Object.freeze([
717
+ 'ACK_RECEIVE', 'ACK_NOD',
718
+ 'RESPONSE_INTERRUPTED',
719
+ 'GESTURE_GREET', 'GESTURE_GOODBYE', 'GESTURE_APPROVE', 'GESTURE_WAIT',
720
+ ]);
721
+
722
+ /** Face-capable subset of the action contract. */
723
+ export const ACTIONS = Object.freeze({
724
+ ...Object.fromEntries(ACTION_IDS.filter((id) => CLIPS[id]).map((id) => [id, CLIPS[id]])),
725
+ // The goodbye face intentionally reuses the greeting brow-flash while its
726
+ // hand owns the distinct, longer farewell motion. It still needs metadata so
727
+ // a client can enumerate every public action without knowing that detail.
728
+ GESTURE_GOODBYE: {
729
+ ...CLIPS.GESTURE_GREET,
730
+ id: 'GESTURE_GOODBYE', label: 'gesture: goodbye', duration: 1550,
731
+ },
732
+ });
564
733
 
565
734
  /**
566
- * Attach audio to a clip. Call once at boot with your own TTS renders:
567
- * attachAudio('OKAY', '/audio/agent-okay.mp3')
735
+ * Attach audio to an internal authoring clip. Production actions are silent
736
+ * sequences; response speech always belongs to Pipecat's audio/viseme track.
568
737
  * The baked viseme track is then scheduled against that file's clock instead of
569
738
  * the local timer, so any timing drift resolves in the audio's favour.
570
739
  */
571
740
  export function attachAudio(id, url) {
572
- const clip = INTERJECTIONS[id];
741
+ const clip = CLIPS[id];
573
742
  if (!clip) throw new Error(`unknown interjection: ${id}`);
574
743
  const el = new Audio(url);
575
744
  el.preload = 'auto';
package/src/params.js CHANGED
@@ -14,6 +14,8 @@
14
14
  * mouthCornerL/R + up (smile)
15
15
  */
16
16
 
17
+ import { JAW_RESPONSE_TAU_S, MOUTH_RESPONSE_TAU_S } from './speech-timing.js';
18
+
17
19
  export const REST = {
18
20
  // --- mouth -------------------------------------------------------------
19
21
  mouthOpen: 0.02, // vertical aperture, 0..1
@@ -21,8 +23,10 @@ export const REST = {
21
23
  mouthRound: 0.1, // pucker / lip protrusion
22
24
  mouthPress: 0.15, // lips thinned & pressed together
23
25
  mouthTuck: 0.0, // lower lip drawn under upper teeth (F/V)
24
- mouthCornerL: 0.1, // -1 frown .. +1 smile
25
- mouthCornerR: 0.1,
26
+ // Availability is not a permanent smile. Warmth and encouragement are
27
+ // explicit behavioral choices layered above this neutral resting shape.
28
+ mouthCornerL: 0.0, // -1 frown .. +1 smile
29
+ mouthCornerR: 0.0,
26
30
  teethUpper: 0.0, // how far the upper teeth show, 0..1
27
31
  tongue: 0.0, // tongue raised into the aperture, 0..1
28
32
  jaw: 0.0, // extra chin drop, follows mouthOpen but slower
@@ -118,7 +122,7 @@ export const GROUPS = {
118
122
  export const TAU = (() => {
119
123
  const t = {};
120
124
  for (const c of CHANNELS) t[c] = 0.09;
121
- for (const c of GROUPS.mouth) t[c] = 0.042;
125
+ for (const c of GROUPS.mouth) t[c] = MOUTH_RESPONSE_TAU_S;
122
126
  for (const c of GROUPS.smile) t[c] = 0.13;
123
127
  t.lidL = t.lidR = 0.018; // blinks must be crisp
124
128
  t.squintL = t.squintR = 0.12;
@@ -126,7 +130,7 @@ export const TAU = (() => {
126
130
  for (const c of GROUPS.brows) t[c] = 0.08;
127
131
  for (const c of GROUPS.head) t[c] = 0.16; // the head has real mass
128
132
  t.breath = 0.25;
129
- t.jaw = 0.07; // the jaw lags the lips slightly
133
+ t.jaw = JAW_RESPONSE_TAU_S; // the jaw lags the lips slightly
130
134
  // The torso has more mass than the head and reads wrong when it hasn't.
131
135
  t.shoulderL = t.shoulderR = 0.19;
132
136
  t.torsoLean = 0.24;
package/src/perform.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A performance is the server's choreography: timed verbs `{t, do, ...}` fired
5
5
  * against a clock, where every verb resolves to one of the widget's own enums โ€”
6
- * states, emotions, gaze targets, interjections, hand gestures. The vocabulary is deliberately
6
+ * states, emotions, gaze targets, and semantic actions. The vocabulary is deliberately
7
7
  * closed: the backend sequences what the rig already does well, it cannot
8
8
  * invent motion. That constraint is what makes the wire format assemblable by
9
9
  * a dialogue manager and reviewable by a human.
@@ -14,25 +14,23 @@
14
14
  * tab is backgrounded. One deliberate difference: beat times fire *verbatim*,
15
15
  * with no LEAD_MS. Visemes lead the sound because phoneme sync is
16
16
  * frame-critical; a gesture arrives through its channels' own smoothing lag,
17
- * and any deliberate lead (CLAIM_FLOOR starts ~350ms before the first sample)
18
- * is authored into the times by the composer, not imposed here.
17
+ * and any deliberate lead is authored into the times by the composer, not
18
+ * imposed here.
19
19
  *
20
20
  * Seeking the audio backward does not re-fire earlier actions: verbs have side
21
21
  * effects, and replaying a nod is worse than missing one.
22
22
  */
23
23
 
24
- const VERBS = new Set(['state', 'emotion', 'gaze', 'interject', 'gesture']);
25
- // The verbs addressed by `id` rather than `name`. Both id verbs name a clip the
26
- // widget already owns; both name-verbs name an enum value.
27
- const ID_VERBS = new Set(['interject', 'gesture']);
24
+ const VERBS = new Set(['state', 'emotion', 'gaze', 'action']);
25
+ // `action` is addressed by id; the remaining verbs name an enum value.
26
+ const ID_VERBS = new Set(['action']);
28
27
 
29
28
  /**
30
29
  * Shape hygiene for action arrays, in the spirit of normalizeCues: sort by
31
30
  * time, drop what cannot possibly fire โ€” no finite `t`, an unknown verb, a
32
31
  * missing `name`/`id` โ€” each with a console warning, never a throw.
33
32
  *
34
- * This checks *shape* only. Enum values (is "THINKING" a state? is "NOD_UP"
35
- * an interjection?) are checked when the verb fires, by the dispatcher in
33
+ * This checks *shape* only. Enum values are checked when the verb fires, by the dispatcher in
36
34
  * avatar.js โ€” deliberately, and not just because importing the enums here
37
35
  * would be a dependency cycle: a track half-composed against a newer widget
38
36
  * should lose the verbs the widget doesn't know, not the whole performance.