@voqalize/avatar 0.4.2 → 0.4.4

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 (191) hide show
  1. package/LICENSE-APACHE-2.0 +201 -0
  2. package/README.md +11 -98
  3. package/assets/README.md +28 -17
  4. package/assets/tanvi.glb +0 -0
  5. package/assets/tanya.glb +0 -0
  6. package/assets/tara.glb +0 -0
  7. package/assets/tess.glb +0 -0
  8. package/assets/tushar.glb +0 -0
  9. package/client/internal.ts +4 -0
  10. package/client/supports.ts +6 -7
  11. package/client/three/assets.ts +2 -0
  12. package/client/three/budgets.ts +1 -1
  13. package/client/three/{tara-rig.ts → character-rig.ts} +708 -547
  14. package/client/three/createCharacter.ts +109 -0
  15. package/client/three/holds.ts +1 -1
  16. package/client/three/internal.ts +4 -3
  17. package/client/three/motion-limits.json +5 -1
  18. package/client/three/tanvi-asset.ts +6 -0
  19. package/client/three/tanvi.ts +30 -0
  20. package/client/three/tanya.ts +18 -58
  21. package/client/three/tara.ts +19 -60
  22. package/client/three/tess.ts +17 -59
  23. package/client/three/tushar.ts +18 -55
  24. package/dist/internal.d.ts +1 -1
  25. package/dist/internal.d.ts.map +1 -1
  26. package/dist/internal.js +5 -1
  27. package/dist/internal.js.map +1 -1
  28. package/dist/supports.d.ts +6 -7
  29. package/dist/supports.d.ts.map +1 -1
  30. package/dist/supports.js +6 -7
  31. package/dist/supports.js.map +1 -1
  32. package/dist/three/assets.d.ts +1 -0
  33. package/dist/three/assets.d.ts.map +1 -1
  34. package/dist/three/assets.js +2 -0
  35. package/dist/three/assets.js.map +1 -1
  36. package/dist/three/budgets.d.ts +1 -1
  37. package/dist/three/budgets.js +1 -1
  38. package/dist/three/character-rig.d.ts +366 -0
  39. package/dist/three/character-rig.d.ts.map +1 -0
  40. package/dist/three/{tara-rig.js → character-rig.js} +664 -525
  41. package/dist/three/character-rig.js.map +1 -0
  42. package/dist/three/createCharacter.d.ts +60 -0
  43. package/dist/three/createCharacter.d.ts.map +1 -0
  44. package/dist/three/createCharacter.js +84 -0
  45. package/dist/three/createCharacter.js.map +1 -0
  46. package/dist/three/holds.js +1 -1
  47. package/dist/three/holds.js.map +1 -1
  48. package/dist/three/internal.d.ts +3 -3
  49. package/dist/three/internal.d.ts.map +1 -1
  50. package/dist/three/internal.js +2 -2
  51. package/dist/three/internal.js.map +1 -1
  52. package/dist/three/motion-limits.json +5 -1
  53. package/dist/three/tanvi-asset.d.ts +7 -0
  54. package/dist/three/tanvi-asset.d.ts.map +1 -0
  55. package/dist/three/tanvi-asset.js +7 -0
  56. package/dist/three/tanvi-asset.js.map +1 -0
  57. package/dist/three/tanvi.d.ts +24 -0
  58. package/dist/three/tanvi.d.ts.map +1 -0
  59. package/dist/three/tanvi.js +24 -0
  60. package/dist/three/tanvi.js.map +1 -0
  61. package/dist/three/tanya.d.ts +16 -28
  62. package/dist/three/tanya.d.ts.map +1 -1
  63. package/dist/three/tanya.js +15 -43
  64. package/dist/three/tanya.js.map +1 -1
  65. package/dist/three/tara.d.ts +17 -22
  66. package/dist/three/tara.d.ts.map +1 -1
  67. package/dist/three/tara.js +16 -45
  68. package/dist/three/tara.js.map +1 -1
  69. package/dist/three/tess.d.ts +15 -29
  70. package/dist/three/tess.d.ts.map +1 -1
  71. package/dist/three/tess.js +14 -44
  72. package/dist/three/tess.js.map +1 -1
  73. package/dist/three/tushar.d.ts +16 -25
  74. package/dist/three/tushar.d.ts.map +1 -1
  75. package/dist/three/tushar.js +15 -40
  76. package/dist/three/tushar.js.map +1 -1
  77. package/package.json +7 -50
  78. package/src/avatar.d.ts +2 -0
  79. package/src/avatar.js +99 -220
  80. package/src/gaze.js +1 -3
  81. package/src/idle.js +10 -2
  82. package/src/prosody.js +7 -3
  83. package/src/speech-timing.js +28 -0
  84. package/src/visemes.js +107 -13
  85. package/client/arjun.ts +0 -26
  86. package/client/createCanvasAvatar.ts +0 -72
  87. package/client/interviewer-female.ts +0 -4
  88. package/client/interviewer-male.ts +0 -4
  89. package/client/ishita.ts +0 -26
  90. package/client/kabir.ts +0 -26
  91. package/client/meera.ts +0 -26
  92. package/client/naina.ts +0 -26
  93. package/client/professional-female-a.ts +0 -4
  94. package/client/professional-female-b.ts +0 -4
  95. package/client/professional-male-a.ts +0 -4
  96. package/client/professional-male-b.ts +0 -4
  97. package/client/vikram.ts +0 -26
  98. package/dist/arjun.d.ts +0 -7
  99. package/dist/arjun.d.ts.map +0 -1
  100. package/dist/arjun.js +0 -20
  101. package/dist/arjun.js.map +0 -1
  102. package/dist/createCanvasAvatar.d.ts +0 -22
  103. package/dist/createCanvasAvatar.d.ts.map +0 -1
  104. package/dist/createCanvasAvatar.js +0 -47
  105. package/dist/createCanvasAvatar.js.map +0 -1
  106. package/dist/interviewer-female.d.ts +0 -4
  107. package/dist/interviewer-female.d.ts.map +0 -1
  108. package/dist/interviewer-female.js +0 -3
  109. package/dist/interviewer-female.js.map +0 -1
  110. package/dist/interviewer-male.d.ts +0 -4
  111. package/dist/interviewer-male.d.ts.map +0 -1
  112. package/dist/interviewer-male.js +0 -3
  113. package/dist/interviewer-male.js.map +0 -1
  114. package/dist/ishita.d.ts +0 -7
  115. package/dist/ishita.d.ts.map +0 -1
  116. package/dist/ishita.js +0 -20
  117. package/dist/ishita.js.map +0 -1
  118. package/dist/kabir.d.ts +0 -7
  119. package/dist/kabir.d.ts.map +0 -1
  120. package/dist/kabir.js +0 -20
  121. package/dist/kabir.js.map +0 -1
  122. package/dist/meera.d.ts +0 -7
  123. package/dist/meera.d.ts.map +0 -1
  124. package/dist/meera.js +0 -20
  125. package/dist/meera.js.map +0 -1
  126. package/dist/naina.d.ts +0 -7
  127. package/dist/naina.d.ts.map +0 -1
  128. package/dist/naina.js +0 -20
  129. package/dist/naina.js.map +0 -1
  130. package/dist/professional-female-a.d.ts +0 -4
  131. package/dist/professional-female-a.d.ts.map +0 -1
  132. package/dist/professional-female-a.js +0 -3
  133. package/dist/professional-female-a.js.map +0 -1
  134. package/dist/professional-female-b.d.ts +0 -4
  135. package/dist/professional-female-b.d.ts.map +0 -1
  136. package/dist/professional-female-b.js +0 -3
  137. package/dist/professional-female-b.js.map +0 -1
  138. package/dist/professional-male-a.d.ts +0 -4
  139. package/dist/professional-male-a.d.ts.map +0 -1
  140. package/dist/professional-male-a.js +0 -3
  141. package/dist/professional-male-a.js.map +0 -1
  142. package/dist/professional-male-b.d.ts +0 -4
  143. package/dist/professional-male-b.d.ts.map +0 -1
  144. package/dist/professional-male-b.js +0 -3
  145. package/dist/professional-male-b.js.map +0 -1
  146. package/dist/three/tara-rig.d.ts +0 -423
  147. package/dist/three/tara-rig.d.ts.map +0 -1
  148. package/dist/three/tara-rig.js.map +0 -1
  149. package/dist/vikram.d.ts +0 -7
  150. package/dist/vikram.d.ts.map +0 -1
  151. package/dist/vikram.js +0 -20
  152. package/dist/vikram.js.map +0 -1
  153. package/src/canvas/author/parts/eye.mjs +0 -722
  154. package/src/canvas/author/parts/hand.mjs +0 -1156
  155. package/src/canvas/author/parts/mouth.mjs +0 -741
  156. package/src/canvas/author/parts/nose.mjs +0 -100
  157. package/src/canvas/author/parts/skin-detail.mjs +0 -67
  158. package/src/canvas/author/path.mjs +0 -283
  159. package/src/canvas/author/rig.mjs +0 -405
  160. package/src/canvas/avatars/round/face.d.mts +0 -3
  161. package/src/canvas/avatars/round/face.mjs +0 -1307
  162. package/src/canvas/create-rig.d.ts +0 -15
  163. package/src/canvas/create-rig.js +0 -100
  164. package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
  165. package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
  166. package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
  167. package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
  168. package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
  169. package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
  170. package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
  171. package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
  172. package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
  173. package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
  174. package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
  175. package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
  176. package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
  177. package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
  178. package/src/canvas/data/img/round-m3-top-body.webp +0 -0
  179. package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
  180. package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
  181. package/src/canvas/data/img/round-w1-top-body.webp +0 -0
  182. package/src/canvas/data/interviewer-female.rig.json +0 -1
  183. package/src/canvas/data/interviewer-male.rig.json +0 -1
  184. package/src/canvas/data/professional-female-a.rig.json +0 -1
  185. package/src/canvas/data/professional-female-b.rig.json +0 -1
  186. package/src/canvas/data/professional-male-a.rig.json +0 -1
  187. package/src/canvas/data/professional-male-b.rig.json +0 -1
  188. package/src/canvas/src/live.js +0 -508
  189. package/src/canvas/src/render2d.js +0 -218
  190. package/src/canvas/src/rig.js +0 -297
  191. package/src/canvas/src/vocab.js +0 -96
package/src/idle.js CHANGED
@@ -77,6 +77,14 @@ const rand = ([a, b]) => a + Math.random() * (b - a);
77
77
  // is two large gaze shifts a second apart and carries one blink, not two; and
78
78
  // a state change that lands on a gaze shift is one event to the eyelids.
79
79
  const BLINK_REFRACTORY = 1.5;
80
+ // How long an ordinary blink takes, open to open, in seconds. Centred on the
81
+ // 208 ms "short" blink Hömke et al. gave their avatar listener
82
+ // (research-biomechanics.md §3.6) — the one their listeners read as nothing
83
+ // but a blink. It was 0.11–0.15 s, and in a recorded call on 2026-09-24 a
84
+ // photographic lid covered that in two or three frames: open, shut, open, a
85
+ // pop rather than a lid travelling, which is the flicker a reviewer calls
86
+ // ghosting. A line face swallowed it; a photograph shows every frame.
87
+ const BLINK_DUR = [0.19, 0.23];
80
88
 
81
89
  // How far into the current gap an odds-rolled evoked blink may land, as a
82
90
  // fraction of that gap. A state that moves its gaze oftener than it blinks —
@@ -226,7 +234,7 @@ export class IdleLayer {
226
234
  this.talk = 0;
227
235
  this._nextBlink = 2 + Math.random() * 3;
228
236
  this._blinkT = -1;
229
- this._blinkDur = 0.13;
237
+ this._blinkDur = BLINK_DUR[0];
230
238
  this._lastBlinkAt = -Infinity;
231
239
  // Two incommensurate frequencies per axis so the sway never visibly loops.
232
240
  this._ph = [Math.random() * 9, Math.random() * 9, Math.random() * 9];
@@ -318,7 +326,7 @@ export class IdleLayer {
318
326
  const gap = this._nextBlink - this._lastBlinkAt;
319
327
  if (!forced && this.t - this._lastBlinkAt < EVOKED_EARLIEST * gap) return;
320
328
  }
321
- this._startBlink(0.11 + Math.random() * 0.04);
329
+ this._startBlink(rand(BLINK_DUR));
322
330
  }
323
331
 
324
332
  _startBlink(dur) {
package/src/prosody.js CHANGED
@@ -132,7 +132,7 @@ const POSE = {
132
132
  // and a knit below it — so a phrase sitting negative is the only route a
133
133
  // real furrow has into a sentence. Pinned at zero, half the asset was
134
134
  // unreachable by construction. The floor is `thoughtful`'s own -0.14, which
135
- // tara-rig's knit weight already cites as a third of the crease; the ceiling
135
+ // character-rig's knit weight already cites as a third of the crease; the ceiling
136
136
  // stays well under a beat's 0.34 so a held level is never read as an accent.
137
137
  brow: [-0.14, 0.18],
138
138
  browTauS: 0.8,
@@ -244,8 +244,7 @@ const BEAT = {
244
244
  // A diagonal nod's yaw, on the turn's side. Raised with the swing above, and
245
245
  // for the same reason — a diagonal beat moves the head across the sentence
246
246
  // where a pure pitch nod only moves it down and back. The amplitude is
247
- // untouched: the nod's read defect was its geometry, never its size
248
- // (3d-avatar-motion-audit.md, 2026-09-14).
247
+ // untouched: the nod's read defect was its geometry, never its size.
249
248
  yawP: 0.45,
250
249
  yaw: 0.12,
251
250
  };
@@ -439,6 +438,11 @@ export class SpeechProsody {
439
438
  * The avatar has entered LISTENING: the floor is the user's. A warmth
440
439
  * episode, unless something smiled recently — see `LISTEN_SMILE_REST_MS`,
441
440
  * which is what stands between this and a face that is always smiling.
441
+ *
442
+ * Nothing smiles when the user *starts* talking, on purpose. The client has
443
+ * no event for that moment, and a smile the renderer timed for itself would
444
+ * be an acknowledgement nobody sent. The lever for more warmth while
445
+ * listening is the server sending more `ACK_NOD`s, never a timer here.
442
446
  */
443
447
  listen() {
444
448
  if (this._ms - this._lastWarm < LISTEN_SMILE_REST_MS) return;
@@ -21,3 +21,31 @@ export const INTERJECTION_TRACK_TAIL_MS = 60;
21
21
  /** First-order response constants: lips settle before the jaw does. */
22
22
  export const MOUTH_RESPONSE_TAU_S = 0.042;
23
23
  export const JAW_RESPONSE_TAU_S = 0.07;
24
+
25
+ /**
26
+ * How far past touching a bilabial aims the lips, in `mouthOpen`.
27
+ *
28
+ * Lips do not ease into a closure; they arrive near their peak speed and
29
+ * compress, because the target they are sent to is a *negative* aperture —
30
+ * the lips would pass through each other if they could (Löfqvist & Gracco
31
+ * 1997, JSLHR 40:877). A first-order chase aimed at zero never touches: it
32
+ * slows to nothing on the way in, and a short [b] in running speech ends with
33
+ * the lips still apart and the teeth showing through the gap. Aimed past
34
+ * zero, the chase crosses it at speed and the drawn aperture stops at shut.
35
+ *
36
+ * The literature gives the direction, not a size in these units. This is the
37
+ * knee measured on the Studio lipsync takes (both engines, the mouth modelled
38
+ * at `MOUTH_RESPONSE_TAU_S`): at 0.1 some bilabials still stopped short, at
39
+ * 0.2 all but a stray one met, and 0.3 met no more. From an ordinary open
40
+ * vowel it touches in τ·ln((0.44 + 0.2) / 0.2) ≈ 49 ms.
41
+ */
42
+ export const LIP_CONTACT_AIM = 0.2;
43
+
44
+ /**
45
+ * How far ahead of a rounded vowel the lips begin to round. Anticipatory
46
+ * rounding is among the longest-range coarticulation there is — 100–300 ms
47
+ * before an audible [y]/[u] (Schwartz & Savariaux 2014) — and this is the
48
+ * bottom of that range, because running speech compresses it and a pucker
49
+ * spread across a whole preceding word reads as a pout.
50
+ */
51
+ export const ROUND_LEAD_MS = 100;
package/src/visemes.js CHANGED
@@ -36,19 +36,50 @@
36
36
  import {
37
37
  CUE_TRACK_LEAD_MS,
38
38
  MIN_VISIBLE_CUE_MS,
39
+ ROUND_LEAD_MS,
39
40
  SPEECH_TRACK_TAIL_MS,
40
41
  } from './speech-timing.js';
41
42
 
43
+ /**
44
+ * The mouth every face owes the server, at the loudest a cue can say (`i` 1).
45
+ *
46
+ * **The apertures are measured speech, not taste.** The anchor is the open
47
+ * vowel: Huber & Chandrasekaran 2006 (JSLHR 49:1368, Table 2; Optotrak, 30
48
+ * adults, a sentence read aloud) open "Bobby"'s [ɑ] by 7.84 mm of lower lip
49
+ * over the jaw, 7.32 of jaw and 2.49 of upper lip — 17.65 mm of inter-lip
50
+ * opening, pooled over one comfortable and three loud conditions. Their Table 1
51
+ * puts comfortable at 0.83 of that pool (4.86 mm against a loud 6.06–6.35), so
52
+ * ordinary speech opens an [ɑ] **14.6 mm**, and `D` is sized so that it does at
53
+ * `i` 0.5 — which is where PyGato puts an engine's ordinary speaking level
54
+ * (`pygato.lipsync.intensity`). The same paper is the check on the parts this
55
+ * does not set: the jaw's share comes out at 6.6 mm against their ~6.1, and the
56
+ * upper lip at 1.7 mm against ~2.1 — the upper lip is the stable one, a third
57
+ * of the lower's travel, and the rig already had it so.
58
+ *
59
+ * Millimetres are face units at **176.7 mm** a unit (chin to hairline): Lin &
60
+ * Chen 2017 (PLoS ONE 12:e0188638) give women's menton–sellion as 107.8 mm,
61
+ * and sellion sits at 0.61 of the unit on every character here. Their mouth
62
+ * width (44.9 mm) would say 157 — these faces are drawn with slightly wide
63
+ * mouths, and the vertical measure is the one an aperture is.
64
+ *
65
+ * No source reachable gave per-vowel apertures in millimetres, so the other
66
+ * letters keep the ratios to `D` they were authored with and are scaled by the
67
+ * same factor (0.617 of their excursion from rest). Upper-teeth show is part
68
+ * of the opening and scales with it — except on `G`, where teeth on the lip
69
+ * *is* the shape. A mouth this size is what "the mouth is moving a lot" was
70
+ * asking for: the table it replaced opened an ordinary [ɑ] 26 mm, as far as the
71
+ * loud speech in that study opens a shout.
72
+ */
42
73
  export const VISEME_SHAPES = {
43
74
  X: { mouthOpen: 0.02, mouthWidth: 0.42, mouthRound: 0.10, mouthPress: 0.15, mouthTuck: 0, teethUpper: 0.00, tongue: 0.0 },
44
75
  A: { mouthOpen: 0.00, mouthWidth: 0.40, mouthRound: 0.18, mouthPress: 0.55, mouthTuck: 0, teethUpper: 0.00, tongue: 0.0 },
45
- B: { mouthOpen: 0.16, mouthWidth: 0.54, mouthRound: 0.05, mouthPress: 0.10, mouthTuck: 0, teethUpper: 0.75, tongue: 0.0 },
46
- C: { mouthOpen: 0.45, mouthWidth: 0.58, mouthRound: 0.05, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.45, tongue: 0.0 },
47
- D: { mouthOpen: 0.85, mouthWidth: 0.52, mouthRound: 0.02, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.25, tongue: 0.15 },
48
- E: { mouthOpen: 0.34, mouthWidth: 0.28, mouthRound: 0.55, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.15, tongue: 0.0 },
49
- F: { mouthOpen: 0.22, mouthWidth: 0.10, mouthRound: 0.95, mouthPress: 0.10, mouthTuck: 0, teethUpper: 0.00, tongue: 0.0 },
50
- G: { mouthOpen: 0.20, mouthWidth: 0.46, mouthRound: 0.10, mouthPress: 0.40, mouthTuck: 1.00, teethUpper: 1.00, tongue: 0.0 },
51
- H: { mouthOpen: 0.40, mouthWidth: 0.48, mouthRound: 0.05, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.35, tongue: 0.90 },
76
+ B: { mouthOpen: 0.11, mouthWidth: 0.54, mouthRound: 0.05, mouthPress: 0.10, mouthTuck: 0, teethUpper: 0.46, tongue: 0.0 },
77
+ C: { mouthOpen: 0.29, mouthWidth: 0.58, mouthRound: 0.05, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.28, tongue: 0.0 },
78
+ D: { mouthOpen: 0.53, mouthWidth: 0.52, mouthRound: 0.02, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.15, tongue: 0.15 },
79
+ E: { mouthOpen: 0.22, mouthWidth: 0.28, mouthRound: 0.55, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.09, tongue: 0.0 },
80
+ F: { mouthOpen: 0.14, mouthWidth: 0.10, mouthRound: 0.95, mouthPress: 0.10, mouthTuck: 0, teethUpper: 0.00, tongue: 0.0 },
81
+ G: { mouthOpen: 0.13, mouthWidth: 0.46, mouthRound: 0.10, mouthPress: 0.40, mouthTuck: 1.00, teethUpper: 1.00, tongue: 0.0 },
82
+ H: { mouthOpen: 0.25, mouthWidth: 0.48, mouthRound: 0.05, mouthPress: 0.00, mouthTuck: 0, teethUpper: 0.22, tongue: 0.90 },
52
83
  };
53
84
 
54
85
  export const VISEME_LETTERS = Object.keys(VISEME_SHAPES);
@@ -56,6 +87,40 @@ export const VISEME_LETTERS = Object.keys(VISEME_SHAPES);
56
87
  /** Resting mouth used when nothing is speaking. */
57
88
  export const SILENT = 'X';
58
89
 
90
+ /** The shapes that are a contact, not an aperture: lips together, lip to teeth. */
91
+ const CLOSURES = new Set(['A', 'G']);
92
+
93
+ /**
94
+ * How far the mandible falls for a given aperture — `jaw` is "follows
95
+ * mouthOpen but slower" (`params.js`) and this is the slower.
96
+ *
97
+ * It is exported because it is not a preference: **every amplitude behind the
98
+ * lips was calibrated at this ratio**, so a driver that picks its own moves the
99
+ * mandible relative to the lip that hides it. The lower arch is the surface
100
+ * that shows it, because it rides `jaw` alone while the lip peels off it on
101
+ * `mouthOpen`: what is seen of the arch is the difference between two much
102
+ * larger travels (`avatar-3d/scripts/morphs.ARCH_DROP`), so it goes to nothing
103
+ * at a ratio only a little above this one. A face driven at 1:1 opens wide and
104
+ * shows no lower teeth at any aperture — the arch descends as fast as the lip
105
+ * uncovering it, which is what mocap did until it read this number instead of
106
+ * ARKit's `jawOpen` directly.
107
+ */
108
+ export const JAW_OF_OPEN = 0.7;
109
+
110
+ /**
111
+ * How much of a shape's excursion a cue's loudness buys, as `k = LOUDNESS_FLOOR
112
+ * + (1 - LOUDNESS_FLOOR) * i`.
113
+ *
114
+ * Louder speech is the same movement made bigger, and not by much: +10 dB
115
+ * opens the lips and jaw 1.28x as far (Huber & Chandrasekaran 2006, Table 1:
116
+ * 4.86 mm comfortable, 6.06–6.35 loud; Schulman 1989 calls it "amplification
117
+ * of normal movement patterns"). `i` spans 16 dB with ordinary speech at 0.5,
118
+ * so 0.5 either side is 8 dB and 1.22x — k runs 0.776 : 1 : 1.224 across it,
119
+ * which normalised to the loud end is this floor. The quiet side is the loud
120
+ * side's slope extended; nothing measured it.
121
+ */
122
+ const LOUDNESS_FLOOR = 0.634;
123
+
59
124
  /**
60
125
  * Scale a shape by loudness. Only the "effortful" channels scale — a quiet 'D'
61
126
  * is a small D, not a different shape.
@@ -63,7 +128,7 @@ export const SILENT = 'X';
63
128
  export function shapeFor(letter, intensity = 1) {
64
129
  const base = VISEME_SHAPES[letter] || VISEME_SHAPES[SILENT];
65
130
  const rest = VISEME_SHAPES.X;
66
- const k = 0.45 + 0.55 * Math.max(0, Math.min(1, intensity));
131
+ const k = LOUDNESS_FLOOR + (1 - LOUDNESS_FLOOR) * Math.max(0, Math.min(1, intensity));
67
132
  return {
68
133
  mouthOpen: rest.mouthOpen + (base.mouthOpen - rest.mouthOpen) * k,
69
134
  mouthWidth: rest.mouthWidth + (base.mouthWidth - rest.mouthWidth) * k,
@@ -72,7 +137,7 @@ export function shapeFor(letter, intensity = 1) {
72
137
  mouthTuck: base.mouthTuck,
73
138
  teethUpper: base.teethUpper * k,
74
139
  tongue: base.tongue,
75
- jaw: (rest.mouthOpen + (base.mouthOpen - rest.mouthOpen) * k) * 0.7,
140
+ jaw: (rest.mouthOpen + (base.mouthOpen - rest.mouthOpen) * k) * JAW_OF_OPEN,
76
141
  };
77
142
  }
78
143
 
@@ -127,7 +192,7 @@ export function normalizeCues(cues) {
127
192
  if (prev && head.t - prev.t < MIN_VISIBLE_CUE_MS) {
128
193
  // Too short to read. Keep whichever is more visually salient: a closure
129
194
  // (A/G) carries more lip-reading information than a mid-open vowel.
130
- if (v === 'A' || v === 'G') {
195
+ if (CLOSURES.has(v)) {
131
196
  // The short cue can sit between two identical closures (G → F → G).
132
197
  // Its replacement would otherwise create a duplicate visible shape;
133
198
  // preserving the first G is both the stable wire form and the face the
@@ -209,6 +274,14 @@ export class VisemeTrack {
209
274
  this.cues = merged;
210
275
  // Re-seek rather than trusting the old index against a re-normalized array.
211
276
  this._idx = 0;
277
+ // A sentence can arrive after the one before it has played out — a TTS
278
+ // whose next sentence lags its own audio does this routinely — and the
279
+ // track had stopped at that sentence's tail. Resume on the same clock, or
280
+ // the face holds still through audio the user can hear.
281
+ if (!this.playing && this.clock && merged.length) {
282
+ const last = merged[merged.length - 1];
283
+ this.playing = last.v !== SILENT || this.clock() + LEAD_MS <= last.t + this.tailMs;
284
+ }
212
285
  }
213
286
 
214
287
  stop() {
@@ -218,7 +291,19 @@ export class VisemeTrack {
218
291
  this._idx = 0;
219
292
  }
220
293
 
221
- /** @returns {{letter: string, intensity: number, phone: string | null} | null} */
294
+ /**
295
+ * The shape to aim at now, which is not always the cue under the clock.
296
+ *
297
+ * Rounding starts before its sound, because it is the kind of gesture a
298
+ * viewer reads a word by (speech-timing.js has the measurements): `round`
299
+ * carries the pucker of a rounded vowel coming up within `ROUND_LEAD_MS`, for
300
+ * the mixer to hold against the current shape's own. It does not reach
301
+ * across a rest. A closure's early onset is not drawn here — the server
302
+ * already starts every closure cue ahead of its phone, and a second lead on
303
+ * top of that closed the lips a whole vowel early.
304
+ *
305
+ * @returns {{letter: string, intensity: number, phone: string | null, round: number} | null}
306
+ */
222
307
  sample() {
223
308
  if (!this.playing || !this.cues.length || !this.clock) return null;
224
309
  const now = this.clock() + LEAD_MS;
@@ -237,8 +322,17 @@ export class VisemeTrack {
237
322
  }
238
323
 
239
324
  const cue = this.cues[this._idx];
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 };
325
+ if (cue.t > now) return { letter: SILENT, intensity: 1, phone: null, round: 0 };
326
+ let round = 0;
327
+ for (let j = this._idx + 1; j < this.cues.length; j++) {
328
+ const ahead = this.cues[j];
329
+ if (ahead.v === SILENT || ahead.t - now > ROUND_LEAD_MS) break;
330
+ // Across consonants too: protrusion before a rounded vowel grows with the
331
+ // consonants in front of it (Noiray et al. 2011, JASA 129:340), so the
332
+ // lips of "sm-" in "smooth" are already on their way.
333
+ round = Math.max(round, VISEME_SHAPES[ahead.v].mouthRound);
334
+ }
335
+ return { letter: cue.v, intensity: cue.i == null ? 1 : cue.i, phone: cue.p ?? null, round };
242
336
  }
243
337
  }
244
338
 
package/client/arjun.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Professional Indian male interviewer, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/interviewer-male.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'round-m3-hair-back.webp': new URL('../src/canvas/data/img/round-m3-hair-back.webp', import.meta.url),
10
- 'round-m3-top-body.webp': new URL('../src/canvas/data/img/round-m3-top-body.webp', import.meta.url),
11
- 'round-m3-hair-front.webp': new URL('../src/canvas/data/img/round-m3-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Arjun — professional male interviewer avatar',
25
- });
26
- }
@@ -1,72 +0,0 @@
1
- /** Shared private constructor for the code-authored canvas avatars. */
2
-
3
- import type { PipecatClient } from '@pipecat-ai/client-js';
4
- import { createAvatar as createMixer, STATES } from '../src/avatar.js';
5
- import type { Gain } from '../src/avatar.js';
6
- import { createCanvasRig } from '../src/canvas/create-rig.js';
7
- import { AvatarClient } from './AvatarClient.js';
8
- import type { AvatarInstance, AvatarOptions } from './createAvatar.js';
9
-
10
- export interface CanvasAvatarOptions extends AvatarOptions {
11
- /** Viseme amplitude, 0..2. `1` is as authored. */
12
- readonly mouthGain?: Gain;
13
- /** Gesture-clip amplitude, 0..2. */
14
- readonly gestureGain?: Gain;
15
- /** Idle/liveness amplitude, 0..2. */
16
- readonly motionGain?: Gain;
17
- }
18
-
19
- /** @deprecated Use `CanvasAvatarOptions`; kept for the original entry points. */
20
- export type InterviewerAvatarOptions = CanvasAvatarOptions;
21
-
22
- interface CanvasAvatarConfig {
23
- readonly rigUrl: URL;
24
- readonly images: Readonly<Record<string, URL>>;
25
- readonly face: () => Promise<unknown>;
26
- readonly label: string;
27
- }
28
-
29
- const GAINS = ['mouthGain', 'gestureGain', 'motionGain'] as const;
30
- const PRESENCE_FILTERS: Readonly<Record<string, string>> = {
31
- DEGRADED: String(STATES.DEGRADED.filter ?? ''),
32
- OFFLINE: String(STATES.OFFLINE.filter ?? ''),
33
- };
34
-
35
- export function createCanvasAvatar(
36
- options: CanvasAvatarOptions,
37
- config: CanvasAvatarConfig,
38
- ): AvatarInstance {
39
- const { mount, client, ...gains } = options;
40
- if (!mount) throw new TypeError('createAvatar: `mount` is required');
41
- if (!client) throw new TypeError('createAvatar: `client` is required');
42
- for (const key of GAINS) {
43
- const gain = gains[key];
44
- if (gain === undefined) continue;
45
- if (!Number.isFinite(gain) || gain < 0 || gain > 2) {
46
- throw new RangeError(`createAvatar: \`${key}\` must be 0..2, got ${String(gain)}`);
47
- }
48
- }
49
-
50
- const canvas = createCanvasRig({
51
- url: config.rigUrl,
52
- images: config.images,
53
- face: config.face,
54
- label: config.label,
55
- });
56
- const widget = createMixer({ mount, rig: canvas.factory, ...gains });
57
- const driver = new AvatarClient(widget, {
58
- onPresenceChange: (state) => canvas.setPresenceFilter(PRESENCE_FILTERS[state] ?? ''),
59
- });
60
- const detach = driver.attach(client as PipecatClient);
61
-
62
- let destroyed = false;
63
- return {
64
- destroy() {
65
- if (destroyed) return;
66
- destroyed = true;
67
- detach();
68
- driver.destroy();
69
- widget.destroy();
70
- },
71
- };
72
- }
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `meera`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './meera.js';
4
- export type { CanvasAvatarOptions, CanvasAvatarOptions as InterviewerAvatarOptions } from './createCanvasAvatar.js';
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `arjun`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './arjun.js';
4
- export type { CanvasAvatarOptions, CanvasAvatarOptions as InterviewerAvatarOptions } from './createCanvasAvatar.js';
package/client/ishita.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Polished professional female avatar, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/professional-female-a.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'professional-female-a-top-body.webp': new URL('../src/canvas/data/img/professional-female-a-top-body.webp', import.meta.url),
10
- 'professional-female-a-hair-back.webp': new URL('../src/canvas/data/img/professional-female-a-hair-back.webp', import.meta.url),
11
- 'professional-female-a-hair-front.webp': new URL('../src/canvas/data/img/professional-female-a-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Ishita — polished professional female avatar',
25
- });
26
- }
package/client/kabir.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Relaxed professional male avatar, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/professional-male-b.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'professional-male-b-top-body.webp': new URL('../src/canvas/data/img/professional-male-b-top-body.webp', import.meta.url),
10
- 'professional-male-b-hair-back.webp': new URL('../src/canvas/data/img/professional-male-b-hair-back.webp', import.meta.url),
11
- 'professional-male-b-hair-front.webp': new URL('../src/canvas/data/img/professional-male-b-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Kabir — relaxed professional male avatar',
25
- });
26
- }
package/client/meera.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Professional Indian female interviewer, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/interviewer-female.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'round-w1-hair-back.webp': new URL('../src/canvas/data/img/round-w1-hair-back.webp', import.meta.url),
10
- 'round-w1-top-body.webp': new URL('../src/canvas/data/img/round-w1-top-body.webp', import.meta.url),
11
- 'round-w1-hair-front.webp': new URL('../src/canvas/data/img/round-w1-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Meera — professional female interviewer avatar',
25
- });
26
- }
package/client/naina.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Relaxed professional female avatar, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/professional-female-b.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'professional-female-b-top-body.webp': new URL('../src/canvas/data/img/professional-female-b-top-body.webp', import.meta.url),
10
- 'professional-female-b-hair-back.webp': new URL('../src/canvas/data/img/professional-female-b-hair-back.webp', import.meta.url),
11
- 'professional-female-b-hair-front.webp': new URL('../src/canvas/data/img/professional-female-b-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Naina — relaxed professional female avatar',
25
- });
26
- }
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `ishita`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './ishita.js';
4
- export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `naina`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './naina.js';
4
- export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `vikram`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './vikram.js';
4
- export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -1,4 +0,0 @@
1
- /** @deprecated Renamed to `kabir`; kept as an alias so existing imports keep working. */
2
-
3
- export { createAvatar } from './kabir.js';
4
- export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
package/client/vikram.ts DELETED
@@ -1,26 +0,0 @@
1
- /** Polished professional male avatar, authored to read at call-tile size. */
2
-
3
- import { createCanvasAvatar } from './createCanvasAvatar.js';
4
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
- import type { AvatarInstance } from './createAvatar.js';
6
-
7
- const RIG_URL = new URL('../src/canvas/data/professional-male-a.rig.json', import.meta.url);
8
- const IMAGES = {
9
- 'professional-male-a-top-body.webp': new URL('../src/canvas/data/img/professional-male-a-top-body.webp', import.meta.url),
10
- 'professional-male-a-hair-back.webp': new URL('../src/canvas/data/img/professional-male-a-hair-back.webp', import.meta.url),
11
- 'professional-male-a-hair-front.webp': new URL('../src/canvas/data/img/professional-male-a-hair-front.webp', import.meta.url),
12
- } as const;
13
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
-
15
- export type { CanvasAvatarOptions };
16
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
17
- export { supports } from './supports.js';
18
-
19
- export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
20
- return createCanvasAvatar(options, {
21
- rigUrl: RIG_URL,
22
- images: IMAGES,
23
- face: FACE,
24
- label: 'Vikram — polished professional male avatar',
25
- });
26
- }
package/dist/arjun.d.ts DELETED
@@ -1,7 +0,0 @@
1
- /** Professional Indian male interviewer, authored to read at call-tile size. */
2
- import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
3
- import type { AvatarInstance } from './createAvatar.js';
4
- export type { CanvasAvatarOptions };
5
- export { supports } from './supports.js';
6
- export declare function createAvatar(options: CanvasAvatarOptions): AvatarInstance;
7
- //# sourceMappingURL=arjun.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"arjun.d.ts","sourceRoot":"","sources":["../client/arjun.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAGhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAUxD,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,cAAc,CAOzE"}
package/dist/arjun.js DELETED
@@ -1,20 +0,0 @@
1
- /** Professional Indian male interviewer, authored to read at call-tile size. */
2
- import { createCanvasAvatar } from './createCanvasAvatar.js';
3
- const RIG_URL = new URL('../src/canvas/data/interviewer-male.rig.json', import.meta.url);
4
- const IMAGES = {
5
- 'round-m3-hair-back.webp': new URL('../src/canvas/data/img/round-m3-hair-back.webp', import.meta.url),
6
- 'round-m3-top-body.webp': new URL('../src/canvas/data/img/round-m3-top-body.webp', import.meta.url),
7
- 'round-m3-hair-front.webp': new URL('../src/canvas/data/img/round-m3-hair-front.webp', import.meta.url),
8
- };
9
- const FACE = () => import('../src/canvas/avatars/round/face.mjs');
10
- // Same mixer, same clip library, so the same declaration (`supports.ts`).
11
- export { supports } from './supports.js';
12
- export function createAvatar(options) {
13
- return createCanvasAvatar(options, {
14
- rigUrl: RIG_URL,
15
- images: IMAGES,
16
- face: FACE,
17
- label: 'Arjun — professional male interviewer avatar',
18
- });
19
- }
20
- //# sourceMappingURL=arjun.js.map
package/dist/arjun.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"arjun.js","sourceRoot":"","sources":["../client/arjun.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI7D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,8CAA8C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzF,MAAM,MAAM,GAAG;IACb,yBAAyB,EAAE,IAAI,GAAG,CAAC,gDAAgD,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;IACrG,wBAAwB,EAAE,IAAI,GAAG,CAAC,+CAA+C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;IACnG,0BAA0B,EAAE,IAAI,GAAG,CAAC,iDAAiD,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;CAC/F,CAAC;AACX,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,sCAAsC,CAAC,CAAC;AAGlE,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,OAAO,kBAAkB,CAAC,OAAO,EAAE;QACjC,MAAM,EAAE,OAAO;QACf,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,8CAA8C;KACtD,CAAC,CAAC;AACL,CAAC"}
@@ -1,22 +0,0 @@
1
- /** Shared private constructor for the code-authored canvas avatars. */
2
- import type { Gain } from '../src/avatar.js';
3
- import type { AvatarInstance, AvatarOptions } from './createAvatar.js';
4
- export interface CanvasAvatarOptions extends AvatarOptions {
5
- /** Viseme amplitude, 0..2. `1` is as authored. */
6
- readonly mouthGain?: Gain;
7
- /** Gesture-clip amplitude, 0..2. */
8
- readonly gestureGain?: Gain;
9
- /** Idle/liveness amplitude, 0..2. */
10
- readonly motionGain?: Gain;
11
- }
12
- /** @deprecated Use `CanvasAvatarOptions`; kept for the original entry points. */
13
- export type InterviewerAvatarOptions = CanvasAvatarOptions;
14
- interface CanvasAvatarConfig {
15
- readonly rigUrl: URL;
16
- readonly images: Readonly<Record<string, URL>>;
17
- readonly face: () => Promise<unknown>;
18
- readonly label: string;
19
- }
20
- export declare function createCanvasAvatar(options: CanvasAvatarOptions, config: CanvasAvatarConfig): AvatarInstance;
21
- export {};
22
- //# sourceMappingURL=createCanvasAvatar.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"createCanvasAvatar.d.ts","sourceRoot":"","sources":["../client/createCanvasAvatar.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAIvE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG7C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvE,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,kDAAkD;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC;CAC5B;AAED,iFAAiF;AACjF,MAAM,MAAM,wBAAwB,GAAG,mBAAmB,CAAC;AAE3D,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAQD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,kBAAkB,GACzB,cAAc,CAkChB"}