@voqalize/avatar 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +65 -154
  2. package/dist/index.d.ts +37 -26
  3. package/dist/index.js +48 -25
  4. package/dist/pin.d.ts +2 -0
  5. package/dist/pin.js +3 -0
  6. package/dist/react.d.ts +21 -6
  7. package/dist/react.js +24 -6
  8. package/package.json +8 -67
  9. package/LICENSE-APACHE-2.0 +0 -201
  10. package/LICENSE-CC-BY-4.0 +0 -396
  11. package/assets/README.md +0 -60
  12. package/assets/tanvi.glb +0 -0
  13. package/assets/tanya.glb +0 -0
  14. package/assets/tara.glb +0 -0
  15. package/assets/tess.glb +0 -0
  16. package/assets/tushar.glb +0 -0
  17. package/client/Avatar.tsx +0 -57
  18. package/client/AvatarClient.ts +0 -818
  19. package/client/createAvatar.ts +0 -177
  20. package/client/index.ts +0 -44
  21. package/client/internal.ts +0 -132
  22. package/client/playout.ts +0 -95
  23. package/client/react.ts +0 -13
  24. package/client/supports.ts +0 -27
  25. package/client/three/assets.ts +0 -26
  26. package/client/three/budgets.ts +0 -27
  27. package/client/three/character-rig.ts +0 -1746
  28. package/client/three/createCharacter.ts +0 -109
  29. package/client/three/holds.ts +0 -69
  30. package/client/three/internal.ts +0 -55
  31. package/client/three/motion-limits.json +0 -124
  32. package/client/three/sequences.ts +0 -281
  33. package/client/three/tanvi-asset.ts +0 -6
  34. package/client/three/tanvi.ts +0 -30
  35. package/client/three/tanya-asset.ts +0 -6
  36. package/client/three/tanya.ts +0 -29
  37. package/client/three/tara-asset.ts +0 -25
  38. package/client/three/tara.ts +0 -36
  39. package/client/three/tess-asset.ts +0 -6
  40. package/client/three/tess.ts +0 -30
  41. package/client/three/tushar-asset.ts +0 -6
  42. package/client/three/tushar.ts +0 -28
  43. package/client/types.ts +0 -235
  44. package/client/useAvatar.ts +0 -74
  45. package/dist/Avatar.d.ts +0 -44
  46. package/dist/Avatar.d.ts.map +0 -1
  47. package/dist/Avatar.js +0 -7
  48. package/dist/Avatar.js.map +0 -1
  49. package/dist/AvatarClient.d.ts +0 -274
  50. package/dist/AvatarClient.d.ts.map +0 -1
  51. package/dist/AvatarClient.js +0 -712
  52. package/dist/AvatarClient.js.map +0 -1
  53. package/dist/createAvatar.d.ts +0 -135
  54. package/dist/createAvatar.d.ts.map +0 -1
  55. package/dist/createAvatar.js +0 -75
  56. package/dist/createAvatar.js.map +0 -1
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/internal.d.ts +0 -34
  60. package/dist/internal.d.ts.map +0 -1
  61. package/dist/internal.js +0 -56
  62. package/dist/internal.js.map +0 -1
  63. package/dist/playout.d.ts +0 -42
  64. package/dist/playout.d.ts.map +0 -1
  65. package/dist/playout.js +0 -85
  66. package/dist/playout.js.map +0 -1
  67. package/dist/react.d.ts.map +0 -1
  68. package/dist/react.js.map +0 -1
  69. package/dist/supports.d.ts +0 -19
  70. package/dist/supports.d.ts.map +0 -1
  71. package/dist/supports.js +0 -25
  72. package/dist/supports.js.map +0 -1
  73. package/dist/three/assets.d.ts +0 -10
  74. package/dist/three/assets.d.ts.map +0 -1
  75. package/dist/three/assets.js +0 -23
  76. package/dist/three/assets.js.map +0 -1
  77. package/dist/three/budgets.d.ts +0 -17
  78. package/dist/three/budgets.d.ts.map +0 -1
  79. package/dist/three/budgets.js +0 -21
  80. package/dist/three/budgets.js.map +0 -1
  81. package/dist/three/character-rig.d.ts +0 -366
  82. package/dist/three/character-rig.d.ts.map +0 -1
  83. package/dist/three/character-rig.js +0 -1644
  84. package/dist/three/character-rig.js.map +0 -1
  85. package/dist/three/createCharacter.d.ts +0 -60
  86. package/dist/three/createCharacter.d.ts.map +0 -1
  87. package/dist/three/createCharacter.js +0 -84
  88. package/dist/three/createCharacter.js.map +0 -1
  89. package/dist/three/holds.d.ts +0 -38
  90. package/dist/three/holds.d.ts.map +0 -1
  91. package/dist/three/holds.js +0 -61
  92. package/dist/three/holds.js.map +0 -1
  93. package/dist/three/internal.d.ts +0 -52
  94. package/dist/three/internal.d.ts.map +0 -1
  95. package/dist/three/internal.js +0 -49
  96. package/dist/three/internal.js.map +0 -1
  97. package/dist/three/motion-limits.json +0 -124
  98. package/dist/three/sequences.d.ts +0 -71
  99. package/dist/three/sequences.d.ts.map +0 -1
  100. package/dist/three/sequences.js +0 -262
  101. package/dist/three/sequences.js.map +0 -1
  102. package/dist/three/tanvi-asset.d.ts +0 -7
  103. package/dist/three/tanvi-asset.d.ts.map +0 -1
  104. package/dist/three/tanvi-asset.js +0 -7
  105. package/dist/three/tanvi-asset.js.map +0 -1
  106. package/dist/three/tanvi.d.ts +0 -24
  107. package/dist/three/tanvi.d.ts.map +0 -1
  108. package/dist/three/tanvi.js +0 -24
  109. package/dist/three/tanvi.js.map +0 -1
  110. package/dist/three/tanya-asset.d.ts +0 -7
  111. package/dist/three/tanya-asset.d.ts.map +0 -1
  112. package/dist/three/tanya-asset.js +0 -7
  113. package/dist/three/tanya-asset.js.map +0 -1
  114. package/dist/three/tanya.d.ts +0 -23
  115. package/dist/three/tanya.d.ts.map +0 -1
  116. package/dist/three/tanya.js +0 -23
  117. package/dist/three/tanya.js.map +0 -1
  118. package/dist/three/tara-asset.d.ts +0 -26
  119. package/dist/three/tara-asset.d.ts.map +0 -1
  120. package/dist/three/tara-asset.js +0 -26
  121. package/dist/three/tara-asset.js.map +0 -1
  122. package/dist/three/tara.d.ts +0 -30
  123. package/dist/three/tara.d.ts.map +0 -1
  124. package/dist/three/tara.js +0 -30
  125. package/dist/three/tara.js.map +0 -1
  126. package/dist/three/tess-asset.d.ts +0 -7
  127. package/dist/three/tess-asset.d.ts.map +0 -1
  128. package/dist/three/tess-asset.js +0 -7
  129. package/dist/three/tess-asset.js.map +0 -1
  130. package/dist/three/tess.d.ts +0 -24
  131. package/dist/three/tess.d.ts.map +0 -1
  132. package/dist/three/tess.js +0 -24
  133. package/dist/three/tess.js.map +0 -1
  134. package/dist/three/tushar-asset.d.ts +0 -7
  135. package/dist/three/tushar-asset.d.ts.map +0 -1
  136. package/dist/three/tushar-asset.js +0 -7
  137. package/dist/three/tushar-asset.js.map +0 -1
  138. package/dist/three/tushar.d.ts +0 -22
  139. package/dist/three/tushar.d.ts.map +0 -1
  140. package/dist/three/tushar.js +0 -22
  141. package/dist/three/tushar.js.map +0 -1
  142. package/dist/types.d.ts +0 -149
  143. package/dist/types.d.ts.map +0 -1
  144. package/dist/types.js +0 -140
  145. package/dist/types.js.map +0 -1
  146. package/dist/useAvatar.d.ts +0 -39
  147. package/dist/useAvatar.d.ts.map +0 -1
  148. package/dist/useAvatar.js +0 -35
  149. package/dist/useAvatar.js.map +0 -1
  150. package/src/avatar.d.ts +0 -436
  151. package/src/avatar.js +0 -1378
  152. package/src/behavior.d.ts +0 -42
  153. package/src/behavior.js +0 -114
  154. package/src/camera.js +0 -29
  155. package/src/clips.js +0 -155
  156. package/src/conformance.js +0 -119
  157. package/src/emotions.js +0 -58
  158. package/src/face-core.js +0 -180
  159. package/src/face-myna.d.ts +0 -7
  160. package/src/face-myna.js +0 -786
  161. package/src/face-peep-control-plane.js +0 -167
  162. package/src/face-peep.d.ts +0 -7
  163. package/src/face-peep.js +0 -925
  164. package/src/face-wren.d.ts +0 -7
  165. package/src/face-wren.js +0 -468
  166. package/src/faces.d.ts +0 -12
  167. package/src/faces.js +0 -53
  168. package/src/gaze.js +0 -504
  169. package/src/hand.js +0 -709
  170. package/src/head.js +0 -134
  171. package/src/idle.js +0 -668
  172. package/src/interjections.js +0 -747
  173. package/src/line-art.js +0 -137
  174. package/src/params.js +0 -180
  175. package/src/perform.js +0 -107
  176. package/src/prosody.js +0 -741
  177. package/src/rig.d.ts +0 -31
  178. package/src/rig.js +0 -32
  179. package/src/speech-timing.js +0 -51
  180. package/src/visemes.js +0 -410
@@ -1,747 +0,0 @@
1
- /**
2
- * Interjection library.
3
- *
4
- * These are the highest-value animations in the whole widget: they're the
5
- * real-time feedback channel, and they fire on a single server token with zero
6
- * generation latency because the phrase set is fixed and everything is
7
- * pre-baked.
8
- *
9
- * Each entry carries three things:
10
- * · a gesture timeline (additive deltas, normalized time 0..1)
11
- * · a baked viseme track with real millisecond timings, tuned so the clip
12
- * plays convincingly even with no audio at all (muted / degraded mode)
13
- * · an `audio` slot for you to drop in a TTS clip in the agent's own voice
14
- *
15
- * If you supply audio, the mouth track re-schedules against the audio clock
16
- * automatically; the timings below are calibrated to a natural, slightly brisk
17
- * delivery, so a matching clip should line up with only small nudges.
18
- *
19
- * Timeline channel names are rig parameters. Values are deltas added on top of
20
- * whatever the base pose, gaze and viseme layers are already doing.
21
- */
22
-
23
- /** @typedef {{id:string,label:string,text:string,duration:number,keys:object,mouthCues?:Array,gaze?:string,blinkAt?:number[],audio?:string}} Clip */
24
-
25
- const nod = (peak, at = 0.24, rebound = 0.56) => [
26
- [0, 0], [at, peak], [rebound, -peak * 0.24], [1, 0],
27
- ];
28
-
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 = {
33
- // ---------------------------------------------------------------------
34
- // Wordless acknowledgement clips. Every one is an explicit backend or
35
- // application decision; the client never fires them from VAD or a timer.
36
- // ---------------------------------------------------------------------
37
- // The nod peaks below are pre-compensated for the head's 160ms time constant.
38
- // Authored at their perceptual value (~0.3) they rendered at ~0.6x and read as
39
- // no gesture at all — the smoothing that gives the head its mass also eats any
40
- // gesture faster than it. Judge these on screen, never as numbers: 0.55 here is
41
- // the same *rendered* nod that ~0.35 would be if the head had no inertia.
42
- //
43
- // The three nods are the mocap taxonomy (docs/research-biomechanics.md §3.3):
44
- // short continuer / long assessment / long-with-upswing realization, in the
45
- // corpus proportions 49/40/12. Their internal structure follows §3.4's three
46
- // laws — a long nod STARTS bigger (the head knows how long the nod will be
47
- // before it begins), each cycle is smaller than the last, and the final cycle
48
- // drops further than the trend — and everything stays under the 1.5 Hz line
49
- // where sustained attention turns into impatience.
50
- NOD_SMALL: {
51
- id: 'NOD_SMALL', label: 'nod (continuer)', text: '', duration: 900,
52
- keys: {
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]],
59
- },
60
- },
61
- NOD_SLOW: {
62
- id: 'NOD_SLOW', label: 'nod (assessment / receipt)', text: '', duration: 1200,
63
- keys: {
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]],
72
- },
73
- },
74
- // The "ah — I see" nod. What separates it from agreement is the upswing:
75
- // the head rises FIRST (the realization arriving), then commits to the
76
- // deep beat. Brows and lids lift with the upswing and decay through the
77
- // rest — the face catches on a half-beat before the head does.
78
- NOD_UP: {
79
- id: 'NOD_UP', label: 'nod (realization, upswing)', text: '', duration: 1750,
80
- keys: {
81
- headPitch: [[0, 0], [0.13, -0.34], [0.34, 0.70], [0.52, -0.08], [0.68, 0.44], [1, 0]],
82
- browRaiseL: [[0, 0], [0.10, 0.42], [0.45, 0.18], [1, 0]],
83
- browRaiseR: [[0, 0], [0.11, 0.38], [0.45, 0.16], [1, 0]],
84
- lidL: [[0, 0], [0.12, -0.10], [0.5, 0], [1, 0]],
85
- lidR: [[0, 0], [0.12, -0.10], [0.5, 0], [1, 0]],
86
- // The settle carries a trace of satisfaction — getting it feels good.
87
- mouthCornerL: [[0, 0], [0.5, 0.10], [0.85, 0.16], [1, 0]],
88
- mouthCornerR: [[0, 0], [0.5, 0.10], [0.85, 0.16], [1, 0]],
89
- },
90
- },
91
- BROW_ACK: {
92
- id: 'BROW_ACK', label: 'brow acknowledge', text: '', duration: 720,
93
- keys: {
94
- browRaiseL: [[0, 0], [0.24, 0.30], [0.7, 0.06], [1, 0]],
95
- browRaiseR: [[0, 0], [0.24, 0.26], [0.7, 0.06], [1, 0]],
96
- mouthCornerL: [[0, 0], [0.35, 0.14], [1, 0]],
97
- mouthCornerR: [[0, 0], [0.35, 0.14], [1, 0]],
98
- },
99
- blinkAt: [0.22],
100
- },
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
216
- // first swing is the biggest, every cycle decays, and the whole gesture
217
- // stays at or under ~1.5 Hz. Both are SERVER-SENT ONLY — an agent must
218
- // never disagree autonomously, so neither is in the listening engine's
219
- // picker.
220
- HEAD_SHAKE: {
221
- id: 'HEAD_SHAKE', label: 'no (firm)', text: '', duration: 1350,
222
- keys: {
223
- // Two decaying cycles at ~1.5 Hz, peaks pre-compensated like the nods
224
- // (the old ±0.20 rendered as ambient drift, not as "no"). The mouth
225
- // firms and the brows drop a touch: a "no" with a resting smile under
226
- // it reads as teasing.
227
- headYaw: [[0, 0], [0.16, -0.55], [0.42, 0.45], [0.68, -0.26], [0.88, 0.10], [1, 0]],
228
- mouthPress: [[0, 0], [0.20, 0.35], [0.80, 0.30], [1, 0]],
229
- mouthCornerL: [[0, 0], [0.25, -0.18], [1, 0]],
230
- mouthCornerR: [[0, 0], [0.25, -0.18], [1, 0]],
231
- browRaiseL: [[0, 0], [0.20, -0.20], [0.85, -0.10], [1, 0]],
232
- browRaiseR: [[0, 0], [0.20, -0.20], [0.85, -0.10], [1, 0]],
233
- },
234
- },
235
- // "Hmm, not quite" — the polite disagreement backchannel. Slower (~1 Hz),
236
- // smaller, a cycle and a half dying away, with the sympathetic head tilt
237
- // and knit brows of someone sorry to be disagreeing. The regretful set is
238
- // what separates not-quite from no.
239
- HEAD_SHAKE_SOFT: {
240
- id: 'HEAD_SHAKE_SOFT', label: 'not quite (soft)', text: '', duration: 1700,
241
- keys: {
242
- // At tile size the amplitude difference from HEAD_SHAKE is thin; what
243
- // actually separates not-quite from no here is the sympathetic head
244
- // TILT (line-face scaled — under 0.3 the roll never survives the 5.5°
245
- // multiplier's floor), the slower swing, and the sorry face.
246
- headYaw: [[0, 0], [0.22, -0.26], [0.55, 0.18], [0.82, -0.09], [1, 0]],
247
- headRoll: [[0, 0], [0.30, 0.50], [0.85, 0.35], [1, 0.06]],
248
- browInnerL: [[0, 0], [0.30, 0.50], [1, 0.08]],
249
- browInnerR: [[0, 0], [0.30, 0.42], [1, 0.06]],
250
- mouthCornerL: [[0, 0], [0.40, -0.24], [1, -0.06]],
251
- mouthCornerR: [[0, 0], [0.40, -0.24], [1, -0.06]],
252
- mouthPress: [[0, 0], [0.35, 0.30], [1, 0]],
253
- },
254
- },
255
- /**
256
- * The deliberate ~600ms blink, paired with a barely-there nod — the studied
257
- * stimulus (docs/research-biomechanics.md §3.6): listeners who got long
258
- * blinks with nods gave measurably shorter answers, with no awareness of
259
- * why. It reads as "that's noted — move on". SERVER-SENT ONLY: nothing
260
- * autonomous may ever pick it, because it genuinely shortens what the user
261
- * says next. Distinct from THINKING's 0.34s slow blink by duration alone.
262
- * The lids close in ~90ms, stay down half a second, and release slower
263
- * than they fell; the mouth is untouched.
264
- */
265
- BLINK_LONG: {
266
- id: 'BLINK_LONG', label: 'long blink (noted)', text: '', duration: 850,
267
- keys: {
268
- lidL: [[0, 0], [0.10, 1.0], [0.68, 1.0], [0.92, 0], [1, 0]],
269
- lidR: [[0, 0], [0.10, 1.0], [0.68, 1.0], [0.92, 0], [1, 0]],
270
- headPitch: [[0, 0], [0.30, 0.30], [0.75, -0.05], [1, 0]],
271
- },
272
- },
273
-
274
- // ---------------------------------------------------------------------
275
- // Floor management.
276
- //
277
- // These carry no words and no meaning about the conversation — they are pure
278
- // turn-taking. Their whole job is to be *predictive*: a claim has to land
279
- // before the audio it predicts, or it is describing the past.
280
- // ---------------------------------------------------------------------
281
-
282
- /**
283
- * The inbreath before speaking. Fire this ~350ms ahead of the first audio
284
- * sample — the server knows it is about to speak long before the samples
285
- * exist, so the lead time is free.
286
- *
287
- * This is the cue that prevents collisions. Everything in it is one gesture
288
- * seen from outside: the shoulders rise, the chest fills, the head comes up,
289
- * the lips part. It ends held rather than resolved, because the speech it
290
- * precedes is what resolves it — the clip's own ramp-out does the blending.
291
- */
292
- CLAIM_FLOOR: {
293
- id: 'CLAIM_FLOOR', label: 'about to speak (inhale)', text: '', duration: 480,
294
- keys: {
295
- breath: [[0, 0], [0.45, 0.55], [1, 0.22]],
296
- shoulderL: [[0, 0], [0.45, 0.36], [1, 0.14]],
297
- shoulderR: [[0, 0], [0.45, 0.34], [1, 0.13]],
298
- torsoLean: [[0, 0], [0.5, 0.22], [1, 0.10]],
299
- browRaiseL: [[0, 0], [0.35, 0.26], [1, 0.10]],
300
- browRaiseR: [[0, 0], [0.35, 0.22], [1, 0.08]],
301
- headPitch: [[0, 0], [0.4, -0.18], [1, -0.06]],
302
- lidL: [[0, 0], [0.4, -0.10], [1, -0.04]],
303
- lidR: [[0, 0], [0.4, -0.10], [1, -0.04]],
304
- mouthOpen: [[0, 0], [0.55, 0.20], [1, 0.12]],
305
- },
306
- },
307
-
308
- /**
309
- * Interrupted mid-word. The mouth shutting *is* the message, and it has to be
310
- * the fastest thing on the face — a yield that takes as long as a nod reads as
311
- * the agent finishing its sentence anyway, which is the opposite of the meaning.
312
- * The peak is at 12% of 420ms, so the lips are closed inside 50ms.
313
- */
314
- YIELD_FLOOR: {
315
- id: 'YIELD_FLOOR', label: 'yield (interrupted)', text: '', duration: 420,
316
- keys: {
317
- mouthOpen: [[0, 0], [0.12, -0.34], [0.5, -0.14], [1, 0]],
318
- mouthPress: [[0, 0], [0.14, 0.30], [0.6, 0.12], [1, 0]],
319
- // Settling back is the second half of it: the body gives the floor up as
320
- // well as the voice. Without this the face just stops, which reads as a
321
- // dropped connection rather than as deference.
322
- torsoLean: [[0, 0], [0.3, -0.24], [1, -0.08]],
323
- shoulderL: [[0, 0], [0.25, -0.20], [1, -0.06]],
324
- shoulderR: [[0, 0], [0.25, -0.20], [1, -0.06]],
325
- browRaiseL: [[0, 0], [0.2, 0.18], [1, 0.04]],
326
- browRaiseR: [[0, 0], [0.2, 0.15], [1, 0.04]],
327
- headPitch: [[0, 0], [0.28, 0.10], [1, 0.02]],
328
- },
329
- blinkAt: [0.1],
330
- },
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
-
350
- /**
351
- * "May I come in." This used to be a raised palm, and it is now the same
352
- * signal without one: the body claims a little space and the face asks. The
353
- * hold is what carries it either way — the signal is the *hold*, and anything
354
- * that goes up and straight back down reads as a twitch.
355
- *
356
- * Distinct from CLAIM_FLOOR by intent rather than by parts. CLAIM_FLOOR is a
357
- * speaker taking a breath and is followed by audio; this is a listener asking
358
- * and is followed by waiting. What separates them on screen is the head: this
359
- * one comes *up* and stays up, holding eye contact, which is a question. The
360
- * inbreath drops the chin, which is a preparation.
361
- */
362
- RAISE_HAND: {
363
- id: 'RAISE_HAND', label: 'may I come in', text: '', duration: 1600,
364
- keys: {
365
- browRaiseL: [[0, 0], [0.2, 0.44], [0.8, 0.38], [1, 0.14]],
366
- browRaiseR: [[0, 0], [0.2, 0.40], [0.8, 0.34], [1, 0.12]],
367
- browInnerL: [[0, 0], [0.24, 0.20], [0.8, 0.16], [1, 0]],
368
- browInnerR: [[0, 0], [0.24, 0.20], [0.8, 0.16], [1, 0]],
369
- shoulderL: [[0, 0], [0.25, 0.36], [0.8, 0.30], [1, 0.10]],
370
- shoulderR: [[0, 0], [0.25, 0.32], [0.8, 0.26], [1, 0.08]],
371
- torsoLean: [[0, 0], [0.3, 0.30], [0.8, 0.24], [1, 0.08]],
372
- // Chin up and held, not a nod. Held is the whole gesture.
373
- headPitch: [[0, 0], [0.3, -0.26], [0.85, -0.22], [1, -0.06]],
374
- lidL: [[0, 0], [0.3, -0.12], [0.85, -0.10], [1, 0]],
375
- lidR: [[0, 0], [0.3, -0.12], [0.85, -0.10], [1, 0]],
376
- mouthOpen: [[0, 0], [0.3, 0.18], [0.85, 0.14], [1, 0]],
377
- },
378
- },
379
-
380
- // ---------------------------------------------------------------------
381
- // Gestures that were arm gestures.
382
- //
383
- // The rig had a forearm and a hand; they were removed (see the note in
384
- // params.js). These four kept their IDs, because the ID set is a wire
385
- // contract the server targets and dropping entries from it is a breaking
386
- // change for a reason the server has no way to know about. What each one
387
- // means is unchanged; only the body part saying it moved.
388
- //
389
- // Re-authoring a gesture off the face is not the same as deleting a track and
390
- // keeping the rest. A wave whose arm is gone is not a quieter wave, it is a
391
- // smile with a stray head-roll on it. Each of these had to be rebuilt around
392
- // whatever channel could carry the meaning on its own.
393
- // ---------------------------------------------------------------------
394
-
395
- /**
396
- * Greeting. What a face does when an arm is not available is the eyebrow
397
- * flash: a fast, high brow raise held under a fifth of a second, which is a
398
- * near-universal human greeting display and is *the* recognition signal at
399
- * conversational distance. It has to be fast — the difference between a
400
- * greeting and surprise is almost entirely duration — so the peak lands at
401
- * 11% of 1300ms and is off again by 40%.
402
- *
403
- * The smile outlasts the brows by design, and the head tilt outlasts both.
404
- * That ordering is the gesture: brows recognise, mouth greets, head settles
405
- * into listening. All three rising and falling together reads as a single
406
- * twitch instead of a greeting with a shape to it.
407
- *
408
- * Shorter than the wave it replaces (1300 vs 2200) because there is no
409
- * oscillation left to fill the time; a held smile past about a second stops
410
- * being a greeting and becomes an expression.
411
- */
412
- GESTURE_GREET: {
413
- id: 'GESTURE_GREET', label: 'gesture: greet', text: '', duration: 1300,
414
- keys: {
415
- browRaiseL: [[0, 0], [0.11, 0.85], [0.4, 0.22], [0.75, 0.12], [1, 0]],
416
- browRaiseR: [[0, 0], [0.11, 0.80], [0.4, 0.20], [0.75, 0.10], [1, 0]],
417
- lidL: [[0, 0], [0.13, -0.20], [0.45, -0.06], [1, 0]],
418
- lidR: [[0, 0], [0.13, -0.20], [0.45, -0.06], [1, 0]],
419
- mouthCornerL: [[0, 0], [0.26, 0.62], [0.72, 0.50], [1, 0]],
420
- mouthCornerR: [[0, 0], [0.26, 0.62], [0.72, 0.50], [1, 0]],
421
- // The squint is what makes it a real smile rather than a social one, and
422
- // it lags the corners on purpose: an orbicularis contraction that arrives
423
- // with the mouth reads as posed, one that arrives just after reads as felt.
424
- squintL: [[0, 0], [0.34, 0.38], [0.78, 0.32], [1, 0]],
425
- squintR: [[0, 0], [0.34, 0.38], [0.78, 0.32], [1, 0]],
426
- // Chin up on the recognition, then a small settle. A greeting head goes
427
- // up first; only an apology starts by going down.
428
- headPitch: [[0, 0], [0.16, -0.24], [0.5, 0.06], [1, 0]],
429
- headRoll: [[0, 0], [0.42, -0.20], [0.85, -0.14], [1, 0]],
430
- },
431
- blinkAt: [0.14],
432
- },
433
-
434
- /**
435
- * Approval. The thumb is gone, so the nod carries it — and a nod that means
436
- * "good" is not the nod that means "yes". It is slower, deeper, and the head
437
- * comes back up to level rather than overshooting, because the rebound is
438
- * what makes a nod read as assent to a question. Here there is no question.
439
- *
440
- * Deep enough to be unmistakable: peak 0.62 against NOD_SMALL's 0.52, over
441
- * nearly three times the duration.
442
- */
443
- GESTURE_APPROVE: {
444
- id: 'GESTURE_APPROVE', label: 'gesture: approve', text: '', duration: 1500,
445
- keys: {
446
- headPitch: [[0, 0], [0.3, 0.62], [0.58, 0.20], [0.78, 0.34], [1, 0]],
447
- mouthCornerL: [[0, 0], [0.36, 0.58], [0.8, 0.48], [1, 0]],
448
- mouthCornerR: [[0, 0], [0.36, 0.58], [0.8, 0.48], [1, 0]],
449
- squintL: [[0, 0], [0.45, 0.40], [0.8, 0.34], [1, 0]],
450
- squintR: [[0, 0], [0.45, 0.40], [0.8, 0.34], [1, 0]],
451
- browRaiseL: [[0, 0], [0.24, 0.30], [0.7, 0.10], [1, 0]],
452
- browRaiseR: [[0, 0], [0.24, 0.28], [0.7, 0.10], [1, 0]],
453
- // Eyes close a little further than the squint alone would take them. A
454
- // wholehearted approval is a slightly shut-eyed expression; wide eyes
455
- // with a big smile is delight, which is a different and stranger thing
456
- // for an attentive agent to be doing.
457
- lidL: [[0, 0], [0.4, 0.14], [0.8, 0.10], [1, 0]],
458
- lidR: [[0, 0], [0.4, 0.14], [0.8, 0.10], [1, 0]],
459
- },
460
- },
461
-
462
- /**
463
- * The shoulders now carry this alone, so they are pushed to the top of their
464
- * range and held. The old comment here noted that shoulders are legible only
465
- * in profile and that face-on the hands carried it; that is true of a small
466
- * shrug and false of a large one. The fix is amplitude plus the face — brows
467
- * up hard, mouth corners pulled *down* rather than up. Raised shoulders with
468
- * a neutral mouth is a flinch; it is the inverted mouth that makes the same
469
- * shoulders mean "I don't know".
470
- */
471
- SHRUG: {
472
- id: 'SHRUG', label: 'shrug', text: '', duration: 1250,
473
- keys: {
474
- shoulderL: [[0, 0], [0.26, 1], [0.66, 0.95], [1, 0]],
475
- shoulderR: [[0, 0], [0.26, 1], [0.66, 0.95], [1, 0]],
476
- browRaiseL: [[0, 0], [0.24, 0.62], [0.66, 0.56], [1, 0]],
477
- browRaiseR: [[0, 0], [0.24, 0.60], [0.66, 0.54], [1, 0]],
478
- browInnerL: [[0, 0], [0.28, 0.24], [0.7, 0.20], [1, 0]],
479
- browInnerR: [[0, 0], [0.28, 0.24], [0.7, 0.20], [1, 0]],
480
- headRoll: [[0, 0], [0.3, 0.18], [0.7, 0.14], [1, 0]],
481
- // The chin tucks with the shoulders — the head sinks a little between
482
- // them, which is most of what a shrug looks like from the front.
483
- headPitch: [[0, 0], [0.28, 0.18], [0.7, 0.14], [1, 0]],
484
- mouthCornerL: [[0, 0], [0.4, -0.30], [0.7, -0.26], [1, 0]],
485
- mouthCornerR: [[0, 0], [0.4, -0.26], [0.7, -0.22], [1, 0]],
486
- mouthPress: [[0, 0], [0.4, 0.30], [0.7, 0.26], [1, 0]],
487
- },
488
- },
489
-
490
- /**
491
- * "Keep going" — the emphatic one, as against the routine GO_ON backchannel.
492
- * The beckon is gone; what is left is the part of "go on" that was never in
493
- * the arm anyway. Eyes widen, brows go up and *stay* up, and the head tilts
494
- * and holds. The hold is the message: a face that returns to neutral has
495
- * stopped waiting, and this clip's whole job is to say the floor is still
496
- * yours.
497
- *
498
- * Two head beats rather than one continuous lift, which is what a listener
499
- * urging someone on actually does — the second beat is the "and?".
500
- */
501
- GO_ON_ARM: {
502
- id: 'GO_ON_ARM', label: 'go on (emphatic)', text: '', duration: 1400,
503
- keys: {
504
- browRaiseL: [[0, 0], [0.18, 0.58], [0.75, 0.44], [1, 0]],
505
- browRaiseR: [[0, 0], [0.18, 0.55], [0.75, 0.42], [1, 0]],
506
- lidL: [[0, 0], [0.22, -0.26], [0.8, -0.20], [1, 0]],
507
- lidR: [[0, 0], [0.22, -0.26], [0.8, -0.20], [1, 0]],
508
- headPitch: [[0, 0], [0.2, 0.26], [0.42, -0.04], [0.6, 0.20], [1, 0]],
509
- headRoll: [[0, 0], [0.35, 0.16], [0.8, 0.12], [1, 0]],
510
- torsoLean: [[0, 0], [0.3, 0.22], [0.8, 0.18], [1, 0]],
511
- mouthCornerL: [[0, 0], [0.5, 0.26], [1, 0]],
512
- mouthCornerR: [[0, 0], [0.5, 0.26], [1, 0]],
513
- mouthOpen: [[0, 0], [0.35, 0.12], [0.8, 0.10], [1, 0]],
514
- },
515
- },
516
-
517
- // ---------------------------------------------------------------------
518
- // Spoken interjections.
519
- // ---------------------------------------------------------------------
520
- MM_HMM: {
521
- id: 'MM_HMM', label: 'mm-hmm', text: 'mm-hmm', duration: 820,
522
- // Lips stay shut the whole way; the meaning is entirely in the nod.
523
- mouthCues: [{ t: 0, v: 'A' }, { t: 620, v: 'X' }],
524
- keys: {
525
- headPitch: [[0, 0], [0.15, 0.34], [0.33, 0.06], [0.50, 0.28], [0.72, 0.02], [1, 0]],
526
- mouthCornerL: [[0, 0], [0.4, 0.16], [1, 0]],
527
- mouthCornerR: [[0, 0], [0.4, 0.16], [1, 0]],
528
- },
529
- },
530
- OKAY: {
531
- id: 'OKAY', label: 'okay', text: 'okay', duration: 860,
532
- // oʊ · k · eɪ
533
- mouthCues: [
534
- { t: 0, v: 'F' }, { t: 140, v: 'B' }, { t: 235, v: 'C' },
535
- { t: 350, v: 'B' }, { t: 480, v: 'X' },
536
- ],
537
- keys: {
538
- headPitch: nod(0.42, 0.22, 0.52),
539
- browRaiseL: [[0, 0], [0.18, 0.16], [0.6, 0], [1, 0]],
540
- browRaiseR: [[0, 0], [0.18, 0.16], [0.6, 0], [1, 0]],
541
- mouthCornerL: [[0, 0], [0.55, 0.20], [1, 0]],
542
- mouthCornerR: [[0, 0], [0.55, 0.20], [1, 0]],
543
- },
544
- },
545
- YES: {
546
- id: 'YES', label: 'yes', text: 'yes', duration: 740,
547
- // j · ɛ · s
548
- mouthCues: [{ t: 0, v: 'B' }, { t: 70, v: 'C' }, { t: 195, v: 'B' }, { t: 330, v: 'X' }],
549
- keys: {
550
- headPitch: [[0, 0], [0.15, 0.55], [0.40, -0.14], [0.68, 0.08], [1, 0]],
551
- browRaiseL: [[0, 0], [0.13, 0.26], [0.5, 0.04], [1, 0]],
552
- browRaiseR: [[0, 0], [0.13, 0.26], [0.5, 0.04], [1, 0]],
553
- mouthCornerL: [[0, 0], [0.5, 0.30], [1, 0]],
554
- mouthCornerR: [[0, 0], [0.5, 0.30], [1, 0]],
555
- },
556
- },
557
- SURE: {
558
- id: 'SURE', label: 'sure', text: 'sure', duration: 860,
559
- // ʃ · ʊ · r
560
- mouthCues: [{ t: 0, v: 'B' }, { t: 85, v: 'F' }, { t: 200, v: 'E' }, { t: 350, v: 'X' }],
561
- keys: {
562
- headPitch: nod(0.34, 0.26, 0.6),
563
- mouthCornerL: [[0, 0], [0.45, 0.42], [1, 0]],
564
- mouthCornerR: [[0, 0], [0.45, 0.42], [1, 0]],
565
- squintL: [[0, 0], [0.45, 0.20], [1, 0]],
566
- squintR: [[0, 0], [0.45, 0.20], [1, 0]],
567
- browRaiseL: [[0, 0], [0.2, 0.14], [1, 0]],
568
- browRaiseR: [[0, 0], [0.2, 0.14], [1, 0]],
569
- },
570
- },
571
- I_SEE: {
572
- id: 'I_SEE', label: 'I see', text: 'I see', duration: 1050,
573
- // aɪ · s · iː
574
- mouthCues: [{ t: 0, v: 'D' }, { t: 120, v: 'C' }, { t: 215, v: 'B' }, { t: 450, v: 'X' }],
575
- keys: {
576
- headPitch: [[0, 0], [0.28, 0.44], [0.66, 0.02], [1, 0]],
577
- browRaiseL: [[0, 0], [0.18, 0.32], [0.55, 0.05], [1, 0]],
578
- browRaiseR: [[0, 0], [0.18, 0.30], [0.55, 0.05], [1, 0]],
579
- },
580
- blinkAt: [0.34],
581
- },
582
- RIGHT: {
583
- id: 'RIGHT', label: 'right', text: 'right', duration: 740,
584
- // r · aɪ · t
585
- mouthCues: [{ t: 0, v: 'E' }, { t: 75, v: 'D' }, { t: 200, v: 'B' }, { t: 330, v: 'X' }],
586
- keys: {
587
- headPitch: nod(0.42, 0.20, 0.5),
588
- browRaiseL: [[0, 0], [0.16, 0.20], [1, 0]],
589
- browRaiseR: [[0, 0], [0.16, 0.20], [1, 0]],
590
- },
591
- },
592
- GO_ON: {
593
- id: 'GO_ON', label: 'go on', text: 'go on', duration: 820,
594
- // g · oʊ · ɒ · n
595
- mouthCues: [{ t: 0, v: 'B' }, { t: 80, v: 'E' }, { t: 290, v: 'A' }, { t: 420, v: 'X' }],
596
- keys: {
597
- headPitch: nod(0.22, 0.25, 0.6),
598
- browRaiseL: [[0, 0], [0.2, 0.36], [0.65, 0.14], [1, 0]],
599
- browRaiseR: [[0, 0], [0.2, 0.36], [0.65, 0.14], [1, 0]],
600
- // Eyes widen — the "keep talking, I'm with you" signal.
601
- lidL: [[0, 0], [0.25, -0.14], [0.8, -0.08], [1, 0]],
602
- lidR: [[0, 0], [0.25, -0.14], [0.8, -0.08], [1, 0]],
603
- mouthCornerL: [[0, 0], [0.5, 0.18], [1, 0]],
604
- mouthCornerR: [[0, 0], [0.5, 0.18], [1, 0]],
605
- // A hint of the lean, deliberately much smaller than GO_ON_ARM. This clip
606
- // fires as routine backchannel, and a full-sized gesture every time the
607
- // user pauses is precisely the fidget that makes an avatar read as a
608
- // gimmick rather than as a listener.
609
- torsoLean: [[0, 0], [0.25, 0.10], [0.7, 0.08], [1, 0]],
610
- },
611
- },
612
- GESTURE_WAIT: {
613
- id: 'GESTURE_WAIT', label: 'gesture: wait', text: 'one moment', duration: 1350,
614
- // w · ʌ · n · m · oʊ · m · ə · n · t
615
- mouthCues: [
616
- { t: 0, v: 'F' }, { t: 110, v: 'C' }, { t: 210, v: 'A' }, { t: 320, v: 'F' },
617
- { t: 420, v: 'C' }, { t: 510, v: 'A' }, { t: 590, v: 'B' }, { t: 730, v: 'X' },
618
- ],
619
- // Breaking eye contact is what actually communicates "hold on".
620
- gaze: 'AWAY_RIGHT',
621
- keys: {
622
- headRoll: [[0, 0], [0.3, 0.12], [0.8, 0.09], [1, 0]],
623
- headYaw: [[0, 0], [0.35, 0.13], [1, 0]],
624
- browRaiseL: [[0, 0], [0.2, 0.34], [0.7, 0.18], [1, 0]],
625
- browRaiseR: [[0, 0], [0.2, 0.30], [0.7, 0.16], [1, 0]],
626
- // This used to be a raised index finger, and that was the one gesture in
627
- // the set a mime did better than the words: legible muted, at thumbnail
628
- // size, across a language barrier. Nothing on the face replaces it, so no
629
- // attempt is made to — what carries the clip now is the gaze break above,
630
- // which is what actually communicates "hold on" and always was. The face
631
- // says "wait" by looking away; it never said it with the finger.
632
- },
633
- blinkAt: [0.12],
634
- },
635
- SORRY: {
636
- id: 'SORRY', label: 'sorry', text: 'sorry', duration: 1050,
637
- // s · ɒ · r · i
638
- mouthCues: [{ t: 0, v: 'B' }, { t: 80, v: 'E' }, { t: 260, v: 'B' }, { t: 420, v: 'X' }],
639
- keys: {
640
- // AU1: the inner-brow lift. This single channel is the whole apology.
641
- browInnerL: [[0, 0], [0.25, 0.62], [0.75, 0.40], [1, 0]],
642
- browInnerR: [[0, 0], [0.25, 0.62], [0.75, 0.40], [1, 0]],
643
- browRaiseL: [[0, 0], [0.3, -0.06], [1, 0]],
644
- browRaiseR: [[0, 0], [0.3, -0.06], [1, 0]],
645
- headPitch: [[0, 0], [0.32, 0.28], [0.8, 0.14], [1, 0]],
646
- headRoll: [[0, 0], [0.35, 0.13], [1, 0]],
647
- mouthCornerL: [[0, 0], [0.4, -0.18], [1, 0]],
648
- mouthCornerR: [[0, 0], [0.4, -0.18], [1, 0]],
649
- },
650
- },
651
- HMM: {
652
- id: 'HMM', label: 'hmm', text: 'hmm', duration: 1250,
653
- mouthCues: [{ t: 0, v: 'A' }, { t: 900, v: 'X' }],
654
- gaze: 'AWAY_THINKING',
655
- keys: {
656
- browRaiseL: [[0, 0], [0.2, -0.24], [0.75, -0.18], [1, 0]],
657
- browRaiseR: [[0, 0], [0.2, -0.18], [0.75, -0.14], [1, 0]],
658
- browInnerL: [[0, 0], [0.25, 0.30], [0.8, 0.20], [1, 0]],
659
- browInnerR: [[0, 0], [0.25, 0.30], [0.8, 0.20], [1, 0]],
660
- headRoll: [[0, 0], [0.4, 0.10], [1, 0]],
661
- mouthCornerL: [[0, 0], [0.4, -0.10], [1, 0]],
662
- mouthCornerR: [[0, 0], [0.4, 0.04], [1, 0]],
663
- },
664
- },
665
- GOT_IT: {
666
- id: 'GOT_IT', label: 'got it', text: 'got it', duration: 820,
667
- // g · ɒ · t · ɪ · t
668
- mouthCues: [{ t: 0, v: 'B' }, { t: 80, v: 'D' }, { t: 210, v: 'B' }, { t: 400, v: 'X' }],
669
- keys: {
670
- headPitch: [[0, 0], [0.16, 0.48], [0.44, -0.10], [1, 0]],
671
- browRaiseL: [[0, 0], [0.15, 0.22], [1, 0]],
672
- browRaiseR: [[0, 0], [0.15, 0.22], [1, 0]],
673
- mouthCornerL: [[0, 0], [0.5, 0.24], [1, 0]],
674
- mouthCornerR: [[0, 0], [0.5, 0.24], [1, 0]],
675
- },
676
- },
677
- TAKE_YOUR_TIME: {
678
- id: 'TAKE_YOUR_TIME', label: 'take your time', text: 'take your time', duration: 1500,
679
- mouthCues: [
680
- { t: 0, v: 'B' }, { t: 90, v: 'C' }, { t: 200, v: 'B' }, { t: 310, v: 'F' },
681
- { t: 430, v: 'E' }, { t: 560, v: 'A' }, { t: 650, v: 'D' }, { t: 800, v: 'A' },
682
- { t: 900, v: 'X' },
683
- ],
684
- keys: {
685
- headPitch: [[0, 0], [0.2, 0.22], [0.45, 0.02], [0.62, 0.16], [1, 0]],
686
- mouthCornerL: [[0, 0], [0.5, 0.40], [1, 0]],
687
- mouthCornerR: [[0, 0], [0.5, 0.40], [1, 0]],
688
- squintL: [[0, 0], [0.5, 0.24], [1, 0]],
689
- squintR: [[0, 0], [0.5, 0.24], [1, 0]],
690
- browRaiseL: [[0, 0], [0.25, 0.16], [1, 0]],
691
- browRaiseR: [[0, 0], [0.25, 0.16], [1, 0]],
692
- },
693
- },
694
- };
695
-
696
- /** Full local authoring library. Not a server action vocabulary. */
697
- export const INTERNAL_CLIPS = CLIPS;
698
-
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
- });
733
-
734
- /**
735
- * Attach audio to an internal authoring clip. Production actions are silent
736
- * sequences; response speech always belongs to Pipecat's audio/viseme track.
737
- * The baked viseme track is then scheduled against that file's clock instead of
738
- * the local timer, so any timing drift resolves in the audio's favour.
739
- */
740
- export function attachAudio(id, url) {
741
- const clip = CLIPS[id];
742
- if (!clip) throw new Error(`unknown interjection: ${id}`);
743
- const el = new Audio(url);
744
- el.preload = 'auto';
745
- clip.audioEl = el;
746
- return el;
747
- }