@voqalize/avatar 0.4.0 → 0.4.2
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.
- package/README.md +46 -28
- package/assets/README.md +4 -3
- package/assets/tanya.glb +0 -0
- package/assets/tara.glb +0 -0
- package/assets/tess.glb +0 -0
- package/assets/tushar.glb +0 -0
- package/client/internal.ts +20 -0
- package/client/three/assets.ts +16 -16
- package/client/three/holds.ts +7 -2
- package/client/three/internal.ts +5 -5
- package/client/three/motion-limits.json +5 -1
- package/client/three/sequences.ts +5 -5
- package/client/three/tanya-asset.ts +6 -0
- package/client/three/tanya.ts +2 -2
- package/client/three/tara-asset.ts +25 -0
- package/client/three/tara-rig.ts +37 -14
- package/client/three/tara.ts +2 -1
- package/client/three/tess-asset.ts +6 -0
- package/client/three/tess.ts +72 -0
- package/client/three/tushar-asset.ts +6 -0
- package/client/three/tushar.ts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +12 -1
- package/dist/internal.js.map +1 -1
- package/dist/three/assets.d.ts +1 -16
- package/dist/three/assets.d.ts.map +1 -1
- package/dist/three/assets.js +15 -16
- package/dist/three/assets.js.map +1 -1
- package/dist/three/holds.d.ts +7 -2
- package/dist/three/holds.d.ts.map +1 -1
- package/dist/three/holds.js +7 -2
- package/dist/three/holds.js.map +1 -1
- package/dist/three/internal.d.ts +5 -5
- package/dist/three/internal.js +5 -5
- package/dist/three/motion-limits.json +5 -1
- package/dist/three/sequences.d.ts +5 -5
- package/dist/three/sequences.js +5 -5
- package/dist/three/tanya-asset.d.ts +7 -0
- package/dist/three/tanya-asset.d.ts.map +1 -0
- package/dist/three/tanya-asset.js +7 -0
- package/dist/three/tanya-asset.js.map +1 -0
- package/dist/three/tanya.js +2 -2
- package/dist/three/tanya.js.map +1 -1
- package/dist/three/tara-asset.d.ts +26 -0
- package/dist/three/tara-asset.d.ts.map +1 -0
- package/dist/three/tara-asset.js +26 -0
- package/dist/three/tara-asset.js.map +1 -0
- package/dist/three/tara-rig.d.ts +29 -9
- package/dist/three/tara-rig.d.ts.map +1 -1
- package/dist/three/tara-rig.js +14 -9
- package/dist/three/tara-rig.js.map +1 -1
- package/dist/three/tara.d.ts.map +1 -1
- package/dist/three/tara.js +2 -1
- package/dist/three/tara.js.map +1 -1
- package/dist/three/tess-asset.d.ts +7 -0
- package/dist/three/tess-asset.d.ts.map +1 -0
- package/dist/three/tess-asset.js +7 -0
- package/dist/three/tess-asset.js.map +1 -0
- package/dist/three/tess.d.ts +38 -0
- package/dist/three/tess.d.ts.map +1 -0
- package/dist/three/tess.js +54 -0
- package/dist/three/tess.js.map +1 -0
- package/dist/three/tushar-asset.d.ts +7 -0
- package/dist/three/tushar-asset.d.ts.map +1 -0
- package/dist/three/tushar-asset.js +7 -0
- package/dist/three/tushar-asset.js.map +1 -0
- package/dist/three/tushar.js +2 -2
- package/dist/three/tushar.js.map +1 -1
- package/package.json +5 -1
- package/src/avatar.d.ts +65 -2
- package/src/avatar.js +68 -63
- package/src/faces.js +1 -1
- package/src/gaze.js +19 -44
- package/src/idle.js +60 -26
- package/src/prosody.js +104 -14
- package/src/rig.d.ts +7 -0
package/src/prosody.js
CHANGED
|
@@ -83,10 +83,36 @@ const BLINK_FRESH_MS = 150;
|
|
|
83
83
|
// wobble about the same one. Yaw is the narrowest in degrees because it is the
|
|
84
84
|
// axis a projected photograph survives least; pitch leans slightly chin-up,
|
|
85
85
|
// which is where a person talking to someone holds their head.
|
|
86
|
+
// **Every axis went up on 2026-09-21, and it is paying for something
|
|
87
|
+
// specific.** The speaking face used to get much of its movement from a gaze
|
|
88
|
+
// aversion — a look away that took the neck with it — and that is gone
|
|
89
|
+
// (gaze.js): the eyes now hold the user for the whole turn. Taking it out and
|
|
90
|
+
// changing nothing else left the speaking head measurably *stiller* than
|
|
91
|
+
// before, which is the defect the aversion had been introduced to fix. So the
|
|
92
|
+
// movement comes back as the pose the head holds per phrase, which is what a
|
|
93
|
+
// speaker's head actually does and is communicative rather than evasive.
|
|
94
|
+
//
|
|
95
|
+
// Sized against `presence.test.ts`, which renders a turn through the real
|
|
96
|
+
// mixer and prints the degrees. On tara the widest yaw here is ~4.9° and the
|
|
97
|
+
// widest chin-up ~4.4°, inside the 6° and 5° she was measured at for a *held*
|
|
98
|
+
// pose; what the same table showed before the raise was a speaking head
|
|
99
|
+
// reaching 4.2° of yaw and 2.1° of pitch, the second of which is under half of
|
|
100
|
+
// what the aversion had been supplying. `headHold` softens the sum in any
|
|
101
|
+
// case, so a character measured tighter than tara gets her own angle here for
|
|
102
|
+
// free.
|
|
86
103
|
const POSE = {
|
|
87
|
-
headYaw: [0.
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
headYaw: [0.15, 0.44],
|
|
105
|
+
// **Pitch widens downward only, and the chin-up end is pinned where it was.**
|
|
106
|
+
// Opening it to -0.26 with the rest measured as a speaking face reading
|
|
107
|
+
// *sleepy*: the lids follow the globe, and on a rig with a reflex a chin-up
|
|
108
|
+
// head means eyes rolled down to hold the user, which narrows the aperture
|
|
109
|
+
// for real. The audit's open-lid median went 0.149 -> 0.163 against a 0.15
|
|
110
|
+
// ceiling (research-perception.md § 6) on that change alone. So the chin-up
|
|
111
|
+
// end stays at the -0.20 it was measured safe at, and the extra travel comes
|
|
112
|
+
// off the other end, where the reflex rolls the eyes *up* and the lid opens.
|
|
113
|
+
// A phrase pose returns, so this is not the chin-drift that reads downcast.
|
|
114
|
+
headPitch: [-0.20, 0.22],
|
|
115
|
+
headRoll: [0.12, 0.34],
|
|
90
116
|
// The mouth corners take a small share of each pose: a speaking mouth whose
|
|
91
117
|
// corners never move reads as dubbed. They ease rather than move, since a
|
|
92
118
|
// corner that jumps reads as a twitch.
|
|
@@ -195,16 +221,32 @@ const BEAT = {
|
|
|
195
221
|
// been held a while, so a phrase does not change position twice in a breath.
|
|
196
222
|
// Of the rest most are a nod and some leave the head alone: a beat on every
|
|
197
223
|
// accent is a metronome, and the brows still carry it.
|
|
198
|
-
|
|
199
|
-
|
|
224
|
+
//
|
|
225
|
+
// **Raised on 2026-09-21, and this is where the speaking head's motion went.**
|
|
226
|
+
// Deleting the speaking aversion (gaze.js) took its head share with it, and
|
|
227
|
+
// that share had been most of what moved the head *inside* a sentence: the
|
|
228
|
+
// per-sentence deviation fell from 0.79 deg of yaw to 0.55 measured on the
|
|
229
|
+
// audit's real cue track, because a held phrase pose contributes nothing to a
|
|
230
|
+
// deviation taken about that phrase's own mean. The swing is the honest way
|
|
231
|
+
// to put it back — it is a move to a new pose rather than a look away from
|
|
232
|
+
// the user, which is the whole distinction the removal was about, and Graf
|
|
233
|
+
// has the speaker doing it. Offered more often and after a shorter hold; the
|
|
234
|
+
// hold is still long enough that a phrase does not reposition twice in a
|
|
235
|
+
// breath.
|
|
236
|
+
swingP: 0.42,
|
|
237
|
+
swingAfterMs: 1400,
|
|
200
238
|
swingMs: 320,
|
|
201
239
|
nodP: 0.7,
|
|
202
240
|
// The nod: down and back, over by 0.4 s, and nothing after it. It peaks just
|
|
203
241
|
// ahead of the vowel so that, through the head's 160 ms smoothing, it lands on
|
|
204
242
|
// it. About 2.5° asked of tara, of which the smoothing renders most.
|
|
205
243
|
nod: { pitch: 0.15, attack: 130, release: 240, lead: 170 },
|
|
206
|
-
// A diagonal nod's yaw, on the turn's side.
|
|
207
|
-
|
|
244
|
+
// A diagonal nod's yaw, on the turn's side. Raised with the swing above, and
|
|
245
|
+
// for the same reason — a diagonal beat moves the head across the sentence
|
|
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).
|
|
249
|
+
yawP: 0.45,
|
|
208
250
|
yaw: 0.12,
|
|
209
251
|
};
|
|
210
252
|
|
|
@@ -217,16 +259,21 @@ const BEAT = {
|
|
|
217
259
|
// shoulders at 0.19 s and the lean at 0.24 s. Each stroke returns through
|
|
218
260
|
// neutral: a trunk that only ever lifts is a standing shrug, which is what a
|
|
219
261
|
// filmstrip once showed as the figure sitting larger and lower in frame.
|
|
262
|
+
// Raised on 2026-09-21 with the rest of the body: the trunk has no measured
|
|
263
|
+
// hold budget the way the head does — nothing about a shoulder tells a
|
|
264
|
+
// projected photograph it has been turned too far — so it is the channel where
|
|
265
|
+
// "a bit more movement" is free, and it is the one the head-on-a-stick read
|
|
266
|
+
// comes from.
|
|
220
267
|
const BEAT_BODY = {
|
|
221
268
|
keys: [[-380, 0], [-60, 1], [140, 0.25], [320, -0.30], [600, 0]],
|
|
222
|
-
shoulder: 0.
|
|
269
|
+
shoulder: 0.36, lean: 0.15, spread: 0.35,
|
|
223
270
|
};
|
|
224
271
|
// The lift a phrase carries, over its own span: up on the first stressed
|
|
225
272
|
// syllable and settling below where it started as the breath goes out.
|
|
226
273
|
const PHRASE_BODY = {
|
|
227
274
|
keys: [[-260, 0], [180, 1], [900, 0.5], [1500, -0.30], [2400, 0]],
|
|
228
|
-
shoulder: 0.
|
|
229
|
-
lean: 0.
|
|
275
|
+
shoulder: 0.28,
|
|
276
|
+
lean: 0.21,
|
|
230
277
|
};
|
|
231
278
|
|
|
232
279
|
// The phrase-final settle: chin down this far from the phrase's own pose,
|
|
@@ -257,16 +304,40 @@ const HOME_MS = 700;
|
|
|
257
304
|
// to take the mouth.
|
|
258
305
|
// `ack` rises with the nod and outlasts it, since a smile that ends with the
|
|
259
306
|
// head's last beat reads as part of the gesture rather than as pleasure.
|
|
307
|
+
//
|
|
308
|
+
// `listen` is the fourth, and it is the one episode that is not about speech at
|
|
309
|
+
// all: the floor has arrived and the face receives it. Every other state change
|
|
310
|
+
// the avatar makes is a change of task; this one is a change of *who is
|
|
311
|
+
// talking*, and a face that takes the floor back with no expression at all is
|
|
312
|
+
// where the "not smiling enough" read comes from. Its shape is the owner's
|
|
313
|
+
// (2026-09-21): up, then a long fade to neutral rather than a held level, which
|
|
314
|
+
// is the same argument the other three are built on — a smile that stays is
|
|
315
|
+
// discounted. The full height lasts 1.8 s, inside Ekman's felt-smile window,
|
|
316
|
+
// and the remaining 3.8 s is the fade. **It is the warmest of the four, raised
|
|
317
|
+
// on the owner's read of 2026-09-21 that it was not arriving.** It started at
|
|
318
|
+
// `ack`'s height and reasoned that `close` — the face that hands the floor
|
|
319
|
+
// over — should outrank the face that takes it; on screen that had it landing
|
|
320
|
+
// as a politeness rather than as a welcome, and this is the one episode a user
|
|
321
|
+
// is looking straight at when it fires.
|
|
260
322
|
// Times in ms.
|
|
261
323
|
const WARMTH = {
|
|
262
324
|
onset: { keys: [[0, 0], [350, 1], [1500, 1], [2800, 0]], corner: 0.18, squint: 0.08 },
|
|
263
325
|
close: { keys: [[0, 0], [300, 1], [2600, 1], [3800, 0]], corner: 0.36, squint: 0.14 },
|
|
264
326
|
ack: { keys: [[0, 0], [220, 1], [1100, 1], [2000, 0]], corner: 0.30, squint: 0.12 },
|
|
327
|
+
listen: { keys: [[0, 0], [400, 1], [2200, 1], [6000, 0]], corner: 0.42, squint: 0.17 },
|
|
265
328
|
};
|
|
266
329
|
// A continuer can come every second or two. Each one smiling would hold the
|
|
267
330
|
// smile up for as long as the user talks, which is the fixed smile again, so
|
|
268
331
|
// an acknowledgement inside this long of the last smiling one nods without it.
|
|
269
332
|
const ACK_SMILE_REST_MS = 4000;
|
|
333
|
+
// The listening smile rests behind *any* warmth, not just its own, and this is
|
|
334
|
+
// the whole of what keeps it from becoming the fixed smile. A turn normally
|
|
335
|
+
// ends SPEAKING -> `close` -> LISTENING within a few hundred ms, and `close` is
|
|
336
|
+
// already that handover's smile; a state that flaps back through LISTENING —
|
|
337
|
+
// through THINKING and out again, or a barge-in and a resume — would otherwise
|
|
338
|
+
// re-arm one every time. Longer than the episodes themselves, so two can never
|
|
339
|
+
// run end to end.
|
|
340
|
+
const LISTEN_SMILE_REST_MS = 7000;
|
|
270
341
|
|
|
271
342
|
/**
|
|
272
343
|
* What a rig renders of the head amplitudes above, unless it says otherwise.
|
|
@@ -320,6 +391,7 @@ export class SpeechProsody {
|
|
|
320
391
|
this._moves = [];
|
|
321
392
|
this._warm = [];
|
|
322
393
|
this._lastAckSmile = -Infinity;
|
|
394
|
+
this._lastWarm = -Infinity;
|
|
323
395
|
this._head = new HeadPose();
|
|
324
396
|
this._home = true;
|
|
325
397
|
this._corner = 0;
|
|
@@ -360,7 +432,23 @@ export class SpeechProsody {
|
|
|
360
432
|
|
|
361
433
|
/** The track played out to its end: the turn's closing warmth. */
|
|
362
434
|
closeTurn() {
|
|
363
|
-
this.
|
|
435
|
+
this._smile(WARMTH.close);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The avatar has entered LISTENING: the floor is the user's. A warmth
|
|
440
|
+
* episode, unless something smiled recently — see `LISTEN_SMILE_REST_MS`,
|
|
441
|
+
* which is what stands between this and a face that is always smiling.
|
|
442
|
+
*/
|
|
443
|
+
listen() {
|
|
444
|
+
if (this._ms - this._lastWarm < LISTEN_SMILE_REST_MS) return;
|
|
445
|
+
this._smile(WARMTH.listen);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Start a warmth episode, and remember that the face has just smiled. */
|
|
449
|
+
_smile(w) {
|
|
450
|
+
this._lastWarm = this._ms;
|
|
451
|
+
this._warm.push({ at: this._ms, w });
|
|
364
452
|
}
|
|
365
453
|
|
|
366
454
|
/**
|
|
@@ -371,10 +459,12 @@ export class SpeechProsody {
|
|
|
371
459
|
acknowledge() {
|
|
372
460
|
if (this._ms - this._lastAckSmile < ACK_SMILE_REST_MS) return;
|
|
373
461
|
this._lastAckSmile = this._ms;
|
|
374
|
-
this.
|
|
462
|
+
this._smile(WARMTH.ack);
|
|
375
463
|
}
|
|
376
464
|
|
|
377
|
-
/** Cut off: a smile that survives being interrupted has not noticed.
|
|
465
|
+
/** Cut off: a smile that survives being interrupted has not noticed. The
|
|
466
|
+
* rest period is deliberately *not* cleared — being interrupted is not an
|
|
467
|
+
* occasion to smile a fresh one at the state change that follows. */
|
|
378
468
|
cool() {
|
|
379
469
|
this._warm = [];
|
|
380
470
|
}
|
|
@@ -480,7 +570,7 @@ export class SpeechProsody {
|
|
|
480
570
|
if (cue.v !== SILENT) {
|
|
481
571
|
if (this._onsetPending) {
|
|
482
572
|
this._onsetPending = false;
|
|
483
|
-
this.
|
|
573
|
+
this._smile(WARMTH.onset);
|
|
484
574
|
}
|
|
485
575
|
let a = i;
|
|
486
576
|
while (a > 0 && !isPause(cues, a - 1)) a--;
|
package/src/rig.d.ts
CHANGED
|
@@ -22,3 +22,10 @@ export type AvatarRigFactory = (mount: HTMLElement, options?: unknown) => Avatar
|
|
|
22
22
|
|
|
23
23
|
export const HAND_GESTURE_NAMES: ReadonlyArray<HandFrame["gesture"]>;
|
|
24
24
|
export function avatarFrame(pose: RigPose, hand?: HandFrame): AvatarFrame;
|
|
25
|
+
/** The bundled SVG renderer as a bare rig: what `createAvatar` builds when no
|
|
26
|
+
* `rig` option is passed, and what an instrument mounts when it wants a face
|
|
27
|
+
* it can pose directly. */
|
|
28
|
+
export function createSvgRig(
|
|
29
|
+
face: ReturnType<import("./avatar.js").FaceFactory>,
|
|
30
|
+
hand?: unknown,
|
|
31
|
+
): AvatarRig;
|