@voqalize/avatar 0.3.0 → 0.4.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 (147) hide show
  1. package/LICENSE-CC-BY-4.0 +396 -0
  2. package/README.md +65 -17
  3. package/assets/README.md +48 -0
  4. package/assets/tanya.glb +0 -0
  5. package/assets/tara.glb +0 -0
  6. package/assets/tushar.glb +0 -0
  7. package/client/AvatarClient.ts +337 -42
  8. package/client/arjun.ts +2 -0
  9. package/client/createAvatar.ts +38 -0
  10. package/client/index.ts +6 -1
  11. package/client/internal.ts +31 -3
  12. package/client/ishita.ts +2 -0
  13. package/client/kabir.ts +2 -0
  14. package/client/meera.ts +2 -0
  15. package/client/naina.ts +2 -0
  16. package/client/playout.ts +95 -0
  17. package/client/supports.ts +28 -0
  18. package/client/three/assets.ts +22 -0
  19. package/client/three/budgets.ts +27 -0
  20. package/client/three/holds.ts +64 -0
  21. package/client/three/internal.ts +54 -0
  22. package/client/three/motion-limits.json +116 -0
  23. package/client/three/sequences.ts +281 -0
  24. package/client/three/tanya-asset.ts +6 -0
  25. package/client/three/tanya.ts +69 -0
  26. package/client/three/tara-asset.ts +25 -0
  27. package/client/three/tara-rig.ts +1571 -0
  28. package/client/three/tara.ts +77 -0
  29. package/client/three/tushar-asset.ts +6 -0
  30. package/client/three/tushar.ts +65 -0
  31. package/client/types.ts +109 -20
  32. package/client/vikram.ts +2 -0
  33. package/dist/AvatarClient.d.ts +89 -11
  34. package/dist/AvatarClient.d.ts.map +1 -1
  35. package/dist/AvatarClient.js +297 -35
  36. package/dist/AvatarClient.js.map +1 -1
  37. package/dist/arjun.d.ts +1 -0
  38. package/dist/arjun.d.ts.map +1 -1
  39. package/dist/arjun.js +2 -0
  40. package/dist/arjun.js.map +1 -1
  41. package/dist/createAvatar.d.ts +36 -0
  42. package/dist/createAvatar.d.ts.map +1 -1
  43. package/dist/createAvatar.js.map +1 -1
  44. package/dist/index.d.ts +6 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +5 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/internal.d.ts +9 -5
  49. package/dist/internal.d.ts.map +1 -1
  50. package/dist/internal.js +13 -5
  51. package/dist/internal.js.map +1 -1
  52. package/dist/ishita.d.ts +1 -0
  53. package/dist/ishita.d.ts.map +1 -1
  54. package/dist/ishita.js +2 -0
  55. package/dist/ishita.js.map +1 -1
  56. package/dist/kabir.d.ts +1 -0
  57. package/dist/kabir.d.ts.map +1 -1
  58. package/dist/kabir.js +2 -0
  59. package/dist/kabir.js.map +1 -1
  60. package/dist/meera.d.ts +1 -0
  61. package/dist/meera.d.ts.map +1 -1
  62. package/dist/meera.js +2 -0
  63. package/dist/meera.js.map +1 -1
  64. package/dist/naina.d.ts +1 -0
  65. package/dist/naina.d.ts.map +1 -1
  66. package/dist/naina.js +2 -0
  67. package/dist/naina.js.map +1 -1
  68. package/dist/playout.d.ts +42 -0
  69. package/dist/playout.d.ts.map +1 -0
  70. package/dist/playout.js +85 -0
  71. package/dist/playout.js.map +1 -0
  72. package/dist/supports.d.ts +20 -0
  73. package/dist/supports.d.ts.map +1 -0
  74. package/dist/supports.js +26 -0
  75. package/dist/supports.js.map +1 -0
  76. package/dist/three/assets.d.ts +8 -0
  77. package/dist/three/assets.d.ts.map +1 -0
  78. package/dist/three/assets.js +19 -0
  79. package/dist/three/assets.js.map +1 -0
  80. package/dist/three/budgets.d.ts +17 -0
  81. package/dist/three/budgets.d.ts.map +1 -0
  82. package/dist/three/budgets.js +21 -0
  83. package/dist/three/budgets.js.map +1 -0
  84. package/dist/three/holds.d.ts +33 -0
  85. package/dist/three/holds.d.ts.map +1 -0
  86. package/dist/three/holds.js +56 -0
  87. package/dist/three/holds.js.map +1 -0
  88. package/dist/three/internal.d.ts +52 -0
  89. package/dist/three/internal.d.ts.map +1 -0
  90. package/dist/three/internal.js +49 -0
  91. package/dist/three/internal.js.map +1 -0
  92. package/dist/three/motion-limits.json +116 -0
  93. package/dist/three/sequences.d.ts +71 -0
  94. package/dist/three/sequences.d.ts.map +1 -0
  95. package/dist/three/sequences.js +262 -0
  96. package/dist/three/sequences.js.map +1 -0
  97. package/dist/three/tanya-asset.d.ts +7 -0
  98. package/dist/three/tanya-asset.d.ts.map +1 -0
  99. package/dist/three/tanya-asset.js +7 -0
  100. package/dist/three/tanya-asset.js.map +1 -0
  101. package/dist/three/tanya.d.ts +35 -0
  102. package/dist/three/tanya.d.ts.map +1 -0
  103. package/dist/three/tanya.js +51 -0
  104. package/dist/three/tanya.js.map +1 -0
  105. package/dist/three/tara-asset.d.ts +26 -0
  106. package/dist/three/tara-asset.d.ts.map +1 -0
  107. package/dist/three/tara-asset.js +26 -0
  108. package/dist/three/tara-asset.js.map +1 -0
  109. package/dist/three/tara-rig.d.ts +409 -0
  110. package/dist/three/tara-rig.d.ts.map +1 -0
  111. package/dist/three/tara-rig.js +1504 -0
  112. package/dist/three/tara-rig.js.map +1 -0
  113. package/dist/three/tara.d.ts +35 -0
  114. package/dist/three/tara.d.ts.map +1 -0
  115. package/dist/three/tara.js +59 -0
  116. package/dist/three/tara.js.map +1 -0
  117. package/dist/three/tushar-asset.d.ts +7 -0
  118. package/dist/three/tushar-asset.d.ts.map +1 -0
  119. package/dist/three/tushar-asset.js +7 -0
  120. package/dist/three/tushar-asset.js.map +1 -0
  121. package/dist/three/tushar.d.ts +31 -0
  122. package/dist/three/tushar.d.ts.map +1 -0
  123. package/dist/three/tushar.js +47 -0
  124. package/dist/three/tushar.js.map +1 -0
  125. package/dist/types.d.ts +65 -10
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/types.js +58 -11
  128. package/dist/types.js.map +1 -1
  129. package/dist/vikram.d.ts +1 -0
  130. package/dist/vikram.d.ts.map +1 -1
  131. package/dist/vikram.js +2 -0
  132. package/dist/vikram.js.map +1 -1
  133. package/package.json +34 -6
  134. package/src/avatar.d.ts +59 -2
  135. package/src/avatar.js +743 -94
  136. package/src/behavior.d.ts +28 -12
  137. package/src/behavior.js +54 -42
  138. package/src/canvas/create-rig.js +1 -1
  139. package/src/canvas/src/live.js +2 -2
  140. package/src/conformance.js +1 -1
  141. package/src/emotions.js +4 -2
  142. package/src/gaze.js +334 -73
  143. package/src/head.js +134 -0
  144. package/src/idle.js +210 -19
  145. package/src/interjections.js +70 -26
  146. package/src/prosody.js +647 -0
  147. package/src/visemes.js +85 -15
package/src/visemes.js CHANGED
@@ -17,10 +17,20 @@
17
17
  * H tongue up L
18
18
  * X idle / silence
19
19
  *
20
- * A cue is `{ t, v, i? }` — millisecond offset into the utterance, the letter,
21
- * and an optional 0..1 intensity (loudness). Intensity is cheap for the server
22
- * to derive from TTS energy and is the single biggest realism win available:
23
- * the same viseme shouted and murmured should not look identical.
20
+ * A cue is `{ t, v, i?, p? }` — millisecond offset into the utterance, the
21
+ * letter, an optional 0..1 intensity (loudness): the same viseme shouted and
22
+ * murmured should not look identical, and an optional phone label. The backend
23
+ * measures `i` from the RMS under each cue, but only on the leg that has audio
24
+ * to measure — a predicted cue describes speech nobody has generated yet, so it
25
+ * arrives without one and is read here as 1.
26
+ *
27
+ * `p` is the phone being articulated under the letter, and the nine letters are
28
+ * a lossy projection of it: `B` above lists four phone families and absorbs
29
+ * fourteen more. Nothing in this file reads it — `VISEME_SHAPES` is the mouth
30
+ * every face owes the server and it is keyed by letter. It rides through
31
+ * normalization and out of `sample()` so that a rig with a finer mouth than
32
+ * nine shapes can use it without the server sending a second track, and so that
33
+ * a rig without one is unaffected.
24
34
  */
25
35
 
26
36
  import {
@@ -70,18 +80,51 @@ export function shapeFor(letter, intensity = 1) {
70
80
  // Cue track hygiene
71
81
  // ---------------------------------------------------------------------------
72
82
 
83
+ /**
84
+ * Group a time-ordered cue list into runs that share a mouth shape.
85
+ *
86
+ * A backend emits one cue per intersection of its shape and phone timelines, so
87
+ * a held shape arrives as several cues differing only in `p`. Grouping them is
88
+ * what lets the visibility rules stay rules about shapes.
89
+ */
90
+ function shapeRuns(cues) {
91
+ const runs = [];
92
+ for (const c of cues) {
93
+ const v = VISEME_SHAPES[c.v] ? c.v : SILENT;
94
+ const clean = { t: c.t, v, i: c.i == null ? 1 : c.i, p: c.p ?? null };
95
+ if (runs.length && runs[runs.length - 1][0].v === v) runs[runs.length - 1].push(clean);
96
+ else runs.push([clean]);
97
+ }
98
+ return runs;
99
+ }
100
+
73
101
  /**
74
102
  * Sort, merge consecutive duplicates, and drop sub-perceptual cues. Servers
75
103
  * emit noisy tracks; this makes them watchable.
104
+ *
105
+ * **Every decision here is a decision about shapes**, taken over the run heads
106
+ * and nothing else. MIN_VISIBLE_CUE_MS and the closure swap are rules about what
107
+ * the face can be *seen* to do, and a phone transition inside a held shape is
108
+ * not something the face does at all — letting one participate would mean a
109
+ * phone changing 15 ms before a real shape change could swallow that change,
110
+ * which is a lipsync regression bought with a field this file does not read.
111
+ * Phone detail is re-attached afterwards, bounded by the mouth positions either
112
+ * side of it. Mirrors `normalize_cues` server-side.
76
113
  */
77
114
  export function normalizeCues(cues) {
115
+ const runs = shapeRuns([...cues].sort((a, b) => a.t - b.t));
116
+
117
+ // Pass one: the shape track, by exactly the rule that predates phone detail.
118
+ // `kept` maps each surviving cue back to the run it speaks for, which the
119
+ // closure swap can change without moving the cue's timestamp.
78
120
  const out = [];
79
- const sorted = [...cues].sort((a, b) => a.t - b.t);
80
- for (const c of sorted) {
81
- const v = VISEME_SHAPES[c.v] ? c.v : SILENT;
121
+ const kept = [];
122
+ for (let index = 0; index < runs.length; index++) {
123
+ const head = runs[index][0];
124
+ const v = head.v;
82
125
  const prev = out[out.length - 1];
83
126
  if (prev && prev.v === v) continue; // merge repeats
84
- if (prev && c.t - prev.t < MIN_VISIBLE_CUE_MS) {
127
+ if (prev && head.t - prev.t < MIN_VISIBLE_CUE_MS) {
85
128
  // Too short to read. Keep whichever is more visually salient: a closure
86
129
  // (A/G) carries more lip-reading information than a mid-open vowel.
87
130
  if (v === 'A' || v === 'G') {
@@ -89,17 +132,38 @@ export function normalizeCues(cues) {
89
132
  // Its replacement would otherwise create a duplicate visible shape;
90
133
  // preserving the first G is both the stable wire form and the face the
91
134
  // viewer actually saw.
92
- if (out.length > 1 && out[out.length - 2].v === v) out.pop();
135
+ if (out.length > 1 && out[out.length - 2].v === v) { out.pop(); kept.pop(); }
93
136
  // A winning closure replaces the preceding shape for the entire
94
137
  // sub-perceptual interval. Preserve that cue's timestamp while taking
95
138
  // the closure's intensity, matching the server-side wire normalizer.
96
- else out[out.length - 1] = { ...c, t: prev.t, v };
139
+ else {
140
+ out[out.length - 1] = { ...head, t: prev.t, v };
141
+ kept[kept.length - 1] = index;
142
+ }
97
143
  }
98
144
  continue;
99
145
  }
100
- out.push({ t: c.t, v, i: c.i == null ? 1 : c.i });
146
+ out.push({ t: head.t, v, i: head.i, p: head.p });
147
+ kept.push(index);
101
148
  }
102
- return out;
149
+
150
+ // Pass two: the phone detail inside each surviving mouth position. Silence is
151
+ // never split — nothing is being articulated under a rest.
152
+ const result = [];
153
+ for (let slot = 0; slot < out.length; slot++) {
154
+ const cue = out[slot];
155
+ result.push(cue);
156
+ if (cue.v === SILENT) continue;
157
+ const limit = slot + 1 < out.length ? out[slot + 1].t : Infinity;
158
+ for (const sub of runs[kept[slot]].slice(1)) {
159
+ // Strictly after whatever was last emitted, not merely after the head: a
160
+ // splice joins two legs' tracks and both can name the same millisecond.
161
+ if (sub.t <= result[result.length - 1].t || sub.t >= limit) continue;
162
+ if (sub.p === result[result.length - 1].p) continue;
163
+ result.push({ t: sub.t, v: cue.v, i: sub.i, p: sub.p });
164
+ }
165
+ }
166
+ return result;
103
167
  }
104
168
 
105
169
  /**
@@ -124,8 +188,13 @@ export class VisemeTrack {
124
188
  this._idx = 0;
125
189
  this.onEnd = null;
126
190
  this.tailMs = SPEECH_TRACK_TAIL_MS;
191
+ // Where the last sample() landed, for the layers that read the track's
192
+ // future (prosody.js looks ahead to the end of a pause).
193
+ this.now = 0;
127
194
  }
128
195
 
196
+ get index() { return this._idx; }
197
+
129
198
  /** @param {() => number} clock returns elapsed ms of the audio being played */
130
199
  start(cues, clock) {
131
200
  this.cues = normalizeCues(cues);
@@ -149,10 +218,11 @@ export class VisemeTrack {
149
218
  this._idx = 0;
150
219
  }
151
220
 
152
- /** @returns {{letter: string, intensity: number} | null} */
221
+ /** @returns {{letter: string, intensity: number, phone: string | null} | null} */
153
222
  sample() {
154
223
  if (!this.playing || !this.cues.length || !this.clock) return null;
155
224
  const now = this.clock() + LEAD_MS;
225
+ this.now = now;
156
226
 
157
227
  // Cues are time-ordered and `now` is mostly monotonic, so this walk is O(1)
158
228
  // amortized. Reset on seek-backward.
@@ -167,8 +237,8 @@ export class VisemeTrack {
167
237
  }
168
238
 
169
239
  const cue = this.cues[this._idx];
170
- if (cue.t > now) return { letter: SILENT, intensity: 1 };
171
- return { letter: cue.v, intensity: cue.i == null ? 1 : cue.i };
240
+ if (cue.t > now) return { letter: SILENT, intensity: 1, phone: null };
241
+ return { letter: cue.v, intensity: cue.i == null ? 1 : cue.i, phone: cue.p ?? null };
172
242
  }
173
243
  }
174
244