@voqalize/avatar 0.4.1 → 0.4.3
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/LICENSE-APACHE-2.0 +201 -0
- package/README.md +26 -95
- package/assets/README.md +30 -18
- package/assets/tanvi.glb +0 -0
- 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 +24 -0
- package/client/supports.ts +6 -7
- package/client/three/assets.ts +7 -3
- package/client/three/budgets.ts +1 -1
- package/client/three/{tara-rig.ts → character-rig.ts} +682 -540
- package/client/three/createCharacter.ts +109 -0
- package/client/three/holds.ts +8 -3
- package/client/three/internal.ts +8 -7
- package/client/three/motion-limits.json +10 -2
- package/client/three/sequences.ts +5 -5
- package/client/three/tanvi-asset.ts +6 -0
- package/client/three/tanvi.ts +30 -0
- package/client/three/tanya.ts +18 -58
- package/client/three/tara.ts +19 -60
- package/client/three/tess-asset.ts +6 -0
- package/client/three/tess.ts +30 -0
- package/client/three/tushar.ts +18 -55
- package/dist/internal.d.ts +2 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +16 -1
- package/dist/internal.js.map +1 -1
- package/dist/supports.d.ts +6 -7
- package/dist/supports.d.ts.map +1 -1
- package/dist/supports.js +6 -7
- package/dist/supports.js.map +1 -1
- package/dist/three/assets.d.ts +2 -0
- package/dist/three/assets.d.ts.map +1 -1
- package/dist/three/assets.js +7 -3
- package/dist/three/assets.js.map +1 -1
- package/dist/three/budgets.d.ts +1 -1
- package/dist/three/budgets.js +1 -1
- package/dist/three/character-rig.d.ts +366 -0
- package/dist/three/character-rig.d.ts.map +1 -0
- package/dist/three/{tara-rig.js → character-rig.js} +633 -526
- package/dist/three/character-rig.js.map +1 -0
- package/dist/three/createCharacter.d.ts +60 -0
- package/dist/three/createCharacter.d.ts.map +1 -0
- package/dist/three/createCharacter.js +84 -0
- package/dist/three/createCharacter.js.map +1 -0
- package/dist/three/holds.d.ts +7 -2
- package/dist/three/holds.d.ts.map +1 -1
- package/dist/three/holds.js +8 -3
- package/dist/three/holds.js.map +1 -1
- package/dist/three/internal.d.ts +7 -7
- package/dist/three/internal.d.ts.map +1 -1
- package/dist/three/internal.js +6 -6
- package/dist/three/internal.js.map +1 -1
- package/dist/three/motion-limits.json +10 -2
- package/dist/three/sequences.d.ts +5 -5
- package/dist/three/sequences.js +5 -5
- package/dist/three/tanvi-asset.d.ts +7 -0
- package/dist/three/tanvi-asset.d.ts.map +1 -0
- package/dist/three/tanvi-asset.js +7 -0
- package/dist/three/tanvi-asset.js.map +1 -0
- package/dist/three/tanvi.d.ts +24 -0
- package/dist/three/tanvi.d.ts.map +1 -0
- package/dist/three/tanvi.js +24 -0
- package/dist/three/tanvi.js.map +1 -0
- package/dist/three/tanya.d.ts +16 -28
- package/dist/three/tanya.d.ts.map +1 -1
- package/dist/three/tanya.js +15 -43
- package/dist/three/tanya.js.map +1 -1
- package/dist/three/tara.d.ts +17 -22
- package/dist/three/tara.d.ts.map +1 -1
- package/dist/three/tara.js +16 -45
- 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 +24 -0
- package/dist/three/tess.d.ts.map +1 -0
- package/dist/three/tess.js +24 -0
- package/dist/three/tess.js.map +1 -0
- package/dist/three/tushar.d.ts +16 -25
- package/dist/three/tushar.d.ts.map +1 -1
- package/dist/three/tushar.js +15 -40
- package/dist/three/tushar.js.map +1 -1
- package/package.json +10 -49
- package/src/avatar.d.ts +67 -2
- package/src/avatar.js +131 -255
- package/src/faces.js +1 -1
- package/src/gaze.js +20 -47
- package/src/idle.js +69 -27
- package/src/prosody.js +109 -15
- package/src/rig.d.ts +7 -0
- package/src/speech-timing.js +9 -0
- package/src/visemes.js +107 -13
- package/client/arjun.ts +0 -26
- package/client/createCanvasAvatar.ts +0 -72
- package/client/interviewer-female.ts +0 -4
- package/client/interviewer-male.ts +0 -4
- package/client/ishita.ts +0 -26
- package/client/kabir.ts +0 -26
- package/client/meera.ts +0 -26
- package/client/naina.ts +0 -26
- package/client/professional-female-a.ts +0 -4
- package/client/professional-female-b.ts +0 -4
- package/client/professional-male-a.ts +0 -4
- package/client/professional-male-b.ts +0 -4
- package/client/vikram.ts +0 -26
- package/dist/arjun.d.ts +0 -7
- package/dist/arjun.d.ts.map +0 -1
- package/dist/arjun.js +0 -20
- package/dist/arjun.js.map +0 -1
- package/dist/createCanvasAvatar.d.ts +0 -22
- package/dist/createCanvasAvatar.d.ts.map +0 -1
- package/dist/createCanvasAvatar.js +0 -47
- package/dist/createCanvasAvatar.js.map +0 -1
- package/dist/interviewer-female.d.ts +0 -4
- package/dist/interviewer-female.d.ts.map +0 -1
- package/dist/interviewer-female.js +0 -3
- package/dist/interviewer-female.js.map +0 -1
- package/dist/interviewer-male.d.ts +0 -4
- package/dist/interviewer-male.d.ts.map +0 -1
- package/dist/interviewer-male.js +0 -3
- package/dist/interviewer-male.js.map +0 -1
- package/dist/ishita.d.ts +0 -7
- package/dist/ishita.d.ts.map +0 -1
- package/dist/ishita.js +0 -20
- package/dist/ishita.js.map +0 -1
- package/dist/kabir.d.ts +0 -7
- package/dist/kabir.d.ts.map +0 -1
- package/dist/kabir.js +0 -20
- package/dist/kabir.js.map +0 -1
- package/dist/meera.d.ts +0 -7
- package/dist/meera.d.ts.map +0 -1
- package/dist/meera.js +0 -20
- package/dist/meera.js.map +0 -1
- package/dist/naina.d.ts +0 -7
- package/dist/naina.d.ts.map +0 -1
- package/dist/naina.js +0 -20
- package/dist/naina.js.map +0 -1
- package/dist/professional-female-a.d.ts +0 -4
- package/dist/professional-female-a.d.ts.map +0 -1
- package/dist/professional-female-a.js +0 -3
- package/dist/professional-female-a.js.map +0 -1
- package/dist/professional-female-b.d.ts +0 -4
- package/dist/professional-female-b.d.ts.map +0 -1
- package/dist/professional-female-b.js +0 -3
- package/dist/professional-female-b.js.map +0 -1
- package/dist/professional-male-a.d.ts +0 -4
- package/dist/professional-male-a.d.ts.map +0 -1
- package/dist/professional-male-a.js +0 -3
- package/dist/professional-male-a.js.map +0 -1
- package/dist/professional-male-b.d.ts +0 -4
- package/dist/professional-male-b.d.ts.map +0 -1
- package/dist/professional-male-b.js +0 -3
- package/dist/professional-male-b.js.map +0 -1
- package/dist/three/tara-rig.d.ts +0 -409
- package/dist/three/tara-rig.d.ts.map +0 -1
- package/dist/three/tara-rig.js.map +0 -1
- package/dist/vikram.d.ts +0 -7
- package/dist/vikram.d.ts.map +0 -1
- package/dist/vikram.js +0 -20
- package/dist/vikram.js.map +0 -1
- package/src/canvas/author/parts/eye.mjs +0 -722
- package/src/canvas/author/parts/hand.mjs +0 -1156
- package/src/canvas/author/parts/mouth.mjs +0 -741
- package/src/canvas/author/parts/nose.mjs +0 -100
- package/src/canvas/author/parts/skin-detail.mjs +0 -67
- package/src/canvas/author/path.mjs +0 -283
- package/src/canvas/author/rig.mjs +0 -405
- package/src/canvas/avatars/round/face.d.mts +0 -3
- package/src/canvas/avatars/round/face.mjs +0 -1307
- package/src/canvas/create-rig.d.ts +0 -15
- package/src/canvas/create-rig.js +0 -100
- package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
- package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
- package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
- package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
- package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
- package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
- package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
- package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
- package/src/canvas/data/img/round-m3-top-body.webp +0 -0
- package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
- package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
- package/src/canvas/data/img/round-w1-top-body.webp +0 -0
- package/src/canvas/data/interviewer-female.rig.json +0 -1
- package/src/canvas/data/interviewer-male.rig.json +0 -1
- package/src/canvas/data/professional-female-a.rig.json +0 -1
- package/src/canvas/data/professional-female-b.rig.json +0 -1
- package/src/canvas/data/professional-male-a.rig.json +0 -1
- package/src/canvas/data/professional-male-b.rig.json +0 -1
- package/src/canvas/src/live.js +0 -508
- package/src/canvas/src/render2d.js +0 -218
- package/src/canvas/src/rig.js +0 -297
- package/src/canvas/src/vocab.js +0 -96
package/src/faces.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The faces we ship, as a table — for tooling that wants every one of them.
|
|
3
3
|
*
|
|
4
4
|
* **Importing this costs all three drawings** (~2k lines of path data). That is
|
|
5
|
-
* the right trade for
|
|
5
|
+
* the right trade for the pose sheet, the sweep and Studio,
|
|
6
6
|
* which exist to compare faces against each other. It is the wrong trade for an
|
|
7
7
|
* application, which renders one: those import a single face module and hand
|
|
8
8
|
* the record to `createAvatar` directly.
|
package/src/gaze.js
CHANGED
|
@@ -84,9 +84,6 @@ const LID_FOLLOW = { down: 0.34, up: 0 };
|
|
|
84
84
|
// peaks mid-turn, is gone when the head lands, and sizes itself on a rig whose
|
|
85
85
|
// head moves four times faster. A 0.75-unit look on tara sinks ~0.8°.
|
|
86
86
|
const HEAD_DIP = 0.06;
|
|
87
|
-
// A gated aversion waits this long past its due time for a phrase boundary,
|
|
88
|
-
// then goes anyway: a run of speech with no pause in it still gets its look.
|
|
89
|
-
const GATE_WAIT = 1.5;
|
|
90
87
|
// How much of a drift the head joins. Little: a drift is a reader's scan or a
|
|
91
88
|
// thinker's gaze moving where it rests, and both are eye movements. At 0.35
|
|
92
89
|
// the head nodded along with every step of a reading scan, which on a
|
|
@@ -190,9 +187,7 @@ function chase(pos, vel, gx, gy, dt, accel, speed) {
|
|
|
190
187
|
* costs η²ₚ = .52–.56 with post-hoc d of 1.09–2.69, with partial inattention
|
|
191
188
|
* costing most of what total inattention costs. So this is not "look away
|
|
192
189
|
* sometimes" — it is a narrow band, and the numbers below are Andrist's measured
|
|
193
|
-
* ones rather than a taste call (docs/research-biomechanics.md §4.2).
|
|
194
|
-
* that arrived at this band, and what it measured on peep, is
|
|
195
|
-
* docs/research-active-listening.md.
|
|
190
|
+
* ones rather than a taste call (docs/research-biomechanics.md §4.2).
|
|
196
191
|
*
|
|
197
192
|
* `every`/`dur` are seconds. `dirs` are unit-ish directions weighted by how
|
|
198
193
|
* often each is taken; magnitude is scaled per-fire.
|
|
@@ -219,30 +214,24 @@ export const AVERSION = {
|
|
|
219
214
|
mag: [0.30, 0.44],
|
|
220
215
|
dirs: [[-1, 0.06], [-1, 0.06], [1, 0.02], [1, 0.02], [-0.7, -0.5], [0.5, 0.35]],
|
|
221
216
|
},
|
|
222
|
-
// While speaking: 1.96 s every 4.75 s mid-turn, a turn-start look on 73% of
|
|
223
|
-
// turns — the planning look, away to find the words and back to deliver
|
|
224
|
-
// them — and none in the last 2.4 s, because the floor is handed back under
|
|
225
|
-
// mutual gaze (§4.2; the mixer owns that hold, since only it can see the
|
|
226
|
-
// cue track). Human speakers hold the listener's eyes for about 41% of a
|
|
227
|
-
// turn. This is set for 70-80% instead — the
|
|
228
|
-
// listener here watches a face on a screen, where 41% reads as evasive — so
|
|
229
|
-
// the looks keep about Andrist's length and come a little less often.
|
|
230
|
-
// `every` sits under Andrist's 4.75 s because the phrase gate and the
|
|
231
|
-
// end-of-turn hold both stretch it: at 3.8-5.8 a turn measured 6-8 looks a
|
|
232
|
-
// minute and 76-82% contact, the top edge of the band rather than its middle.
|
|
233
|
-
// `gated`: a mid-turn look waits for a phrase boundary, because speakers
|
|
234
|
-
// break gaze between clauses, not in the middle of a word.
|
|
235
|
-
SPEAK: {
|
|
236
|
-
every: [3.4, 5.2],
|
|
237
|
-
dur: [1.5, 2.3],
|
|
238
|
-
mag: [0.34, 0.48],
|
|
239
|
-
dirs: [[-1, 0.12], [1, 0.12], [-0.75, 0.55], [0.7, 0.5], [-0.6, -0.45]],
|
|
240
|
-
gated: true,
|
|
241
|
-
start: { p: 0.73, dur: [1.1, 1.8], window: 0.8 },
|
|
242
|
-
},
|
|
243
217
|
};
|
|
244
|
-
// There is deliberately no
|
|
245
|
-
// (
|
|
218
|
+
// **There is deliberately no SPEAK profile, and that is the owner's call
|
|
219
|
+
// (2026-09-21): while the avatar talks, its eyes are on the user.** There was
|
|
220
|
+
// one — Andrist's measured speaker, a planning look away at the start of most
|
|
221
|
+
// turns and a short one every few seconds at a phrase boundary — and in a live
|
|
222
|
+
// call it read as the eyes darting off and coming back. The research it came
|
|
223
|
+
// from is not disputed and the reason it does not transfer is structural: a
|
|
224
|
+
// human speaker's look away is a *head and body* movement that the eyes only
|
|
225
|
+
// lead, and this rig has no such movement to give it. Eye travel on its own is
|
|
226
|
+
// not a smaller version of that, it is a different thing, and on tara the
|
|
227
|
+
// amplitude it needed to be visible at all (`aversionGain` 1.8) is exactly what
|
|
228
|
+
// made it read as a dart. What the speaking face moves instead is the head it
|
|
229
|
+
// holds per phrase and the trunk under it (prosody.js), which is communicative
|
|
230
|
+
// rather than evasive. If the face and body half is ever built, this is where
|
|
231
|
+
// the eye half comes back.
|
|
232
|
+
//
|
|
233
|
+
// There is deliberately no THINK profile either. The *cognitive* aversion —
|
|
234
|
+
// 3.54 s (SD 1.26), splitting 39.3% down / 29.4% up / 31.3% side (§4.2) — is longer
|
|
246
235
|
// and deeper than the listening kind, and THINKING renders it with its own
|
|
247
236
|
// dwell cycle (a `glance` in STATES), which moves the whole gaze target rather
|
|
248
237
|
// than nudging off it. Two mechanisms producing the same look would fight.
|
|
@@ -296,9 +285,6 @@ export class GazeLayer {
|
|
|
296
285
|
this._avHead = { x: 0, y: 0 };
|
|
297
286
|
this._avVel = { x: 0, y: 0 };
|
|
298
287
|
this._avProfileRef = undefined;
|
|
299
|
-
/** Whether now is a phrase boundary, for a `gated` profile. Set by the mixer. */
|
|
300
|
-
this.gate = true;
|
|
301
|
-
this._avStartBy = 0; // a turn-start look may still fire until this time
|
|
302
288
|
// --- drift: a small held offset about the target, set by the mixer
|
|
303
289
|
this.drift = { x: 0, y: 0 };
|
|
304
290
|
this._drift = { x: 0, y: 0 };
|
|
@@ -313,15 +299,6 @@ export class GazeLayer {
|
|
|
313
299
|
// it is entered off a stale timestamp from one that didn't.
|
|
314
300
|
this._avNext = this._t + (p ? p.every[0] + Math.random() * (p.every[1] - p.every[0]) : 0);
|
|
315
301
|
this._avUntil = 0;
|
|
316
|
-
this._avStartBy = 0;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** A turn begins. If the profile has a turn-start look and the dice take it,
|
|
320
|
-
* it fires as soon as `hold` allows inside its window, or not at all — a
|
|
321
|
-
* planning look that arrives a second into the words is just a look. */
|
|
322
|
-
startTurn() {
|
|
323
|
-
const s = this.aversion && this.aversion.start;
|
|
324
|
-
if (s && Math.random() < s.p) this._avStartBy = this._t + s.window;
|
|
325
302
|
}
|
|
326
303
|
|
|
327
304
|
_avert(t, dt) {
|
|
@@ -339,17 +316,13 @@ export class GazeLayer {
|
|
|
339
316
|
// moment the hold lifted — out, back and out again across one turn
|
|
340
317
|
// edge, which is three saccades where the floor wanted none.
|
|
341
318
|
this._avUntil = 0;
|
|
342
|
-
this._avStartBy = 0;
|
|
343
319
|
this._avNext = t + p.every[0] + Math.random() * (p.every[1] - p.every[0]);
|
|
344
320
|
} else if (this._avUntil && t >= this._avUntil) {
|
|
345
321
|
this._avUntil = 0;
|
|
346
322
|
this._nextMicro = 0;
|
|
347
323
|
this._avNext = t + p.every[0] + Math.random() * (p.every[1] - p.every[0]);
|
|
348
|
-
} else if (!this._avUntil && !this.hold &&
|
|
349
|
-
|
|
350
|
-
const dur = t < this._avStartBy ? p.start.dur : p.dur;
|
|
351
|
-
this._avStartBy = 0;
|
|
352
|
-
this._avUntil = t + dur[0] + Math.random() * (dur[1] - dur[0]);
|
|
324
|
+
} else if (!this._avUntil && !this.hold && t >= this._avNext) {
|
|
325
|
+
this._avUntil = t + p.dur[0] + Math.random() * (p.dur[1] - p.dur[0]);
|
|
353
326
|
this._nextMicro = 0;
|
|
354
327
|
const d = p.dirs[(Math.random() * p.dirs.length) | 0];
|
|
355
328
|
const m = (p.mag[0] + Math.random() * (p.mag[1] - p.mag[0])) * this.avertGain;
|
package/src/idle.js
CHANGED
|
@@ -54,7 +54,13 @@ import { HeadPose } from './head.js';
|
|
|
54
54
|
*/
|
|
55
55
|
export const DEFAULT_PROFILE = {
|
|
56
56
|
sway: 1.0,
|
|
57
|
-
|
|
57
|
+
// A head re-positions oftener than it used to (2026-09-21). On a character
|
|
58
|
+
// whose held angle is budgeted this is the half of "more movement" that
|
|
59
|
+
// actually arrives: the angle is capped by `headHold` and the *rate* is not,
|
|
60
|
+
// so a body that re-settles every couple of seconds reads as more alive than
|
|
61
|
+
// one that reaches further every five. Still an event every second or two at
|
|
62
|
+
// its fastest, which is nowhere near the 1.5 Hz line.
|
|
63
|
+
settle: [1.4, 3.4],
|
|
58
64
|
blinkGap: [1.9, 5.4],
|
|
59
65
|
slowBlink: 0,
|
|
60
66
|
breathRate: 1.0,
|
|
@@ -62,7 +68,7 @@ export const DEFAULT_PROFILE = {
|
|
|
62
68
|
hold: null,
|
|
63
69
|
rhythm: null,
|
|
64
70
|
flick: null,
|
|
65
|
-
shift: [
|
|
71
|
+
shift: [7, 16],
|
|
66
72
|
};
|
|
67
73
|
|
|
68
74
|
const rand = ([a, b]) => a + Math.random() * (b - a);
|
|
@@ -71,6 +77,14 @@ const rand = ([a, b]) => a + Math.random() * (b - a);
|
|
|
71
77
|
// is two large gaze shifts a second apart and carries one blink, not two; and
|
|
72
78
|
// a state change that lands on a gaze shift is one event to the eyelids.
|
|
73
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];
|
|
74
88
|
|
|
75
89
|
// How far into the current gap an odds-rolled evoked blink may land, as a
|
|
76
90
|
// fraction of that gap. A state that moves its gaze oftener than it blinks —
|
|
@@ -120,7 +134,7 @@ function nextPosture(prev) {
|
|
|
120
134
|
// a mechanism that is invisible half the time reads as a body that only
|
|
121
135
|
// moves sometimes. Nobody shifts their weight by a millimetre. The 25% that
|
|
122
136
|
// stays on the same side is what keeps it off a left-right metronome.
|
|
123
|
-
const mag = 0.
|
|
137
|
+
const mag = 0.20 + Math.random() * 0.32;
|
|
124
138
|
const away = prev.torsoTurn > 0 ? -1 : prev.torsoTurn < 0 ? 1 : (Math.random() < 0.5 ? -1 : 1);
|
|
125
139
|
const turn = mag * (Math.random() < 0.75 ? away : -away);
|
|
126
140
|
const leanAway = prev.torsoLean > 0 ? -1 : 1;
|
|
@@ -158,11 +172,24 @@ function nextPosture(prev) {
|
|
|
158
172
|
* and not a small number, is what keeps pitch off the wire's vocabulary. It was
|
|
159
173
|
* held to under a degree as well, which measured as a listening head with no
|
|
160
174
|
* pitch in it at all (1.05° peak over thirty seconds, against yaw's 5.5°), and
|
|
161
|
-
* a head that only ever turns is a head on a turntable. The range is
|
|
162
|
-
*
|
|
163
|
-
*
|
|
175
|
+
* a head that only ever turns is a head on a turntable. The range is biased
|
|
176
|
+
* upward: a chin that drifts down and stays there is the downcast read, which
|
|
177
|
+
* on a photographed face arrives long before any other.
|
|
178
|
+
*
|
|
179
|
+
* **Widened again on 2026-09-21, on the owner's read that the whole figure is
|
|
180
|
+
* too restricted.** The ceiling here is not this table — it is `headHold` in
|
|
181
|
+
* the mixer (step 6b), which softens what the layers together ask the head to
|
|
182
|
+
* *hold* against the angle the owner measured on each character by eye
|
|
183
|
+
* (`motion-limits.json`). So these numbers are what the drawing gets and what a
|
|
184
|
+
* budgeted character gets is its own measured angle, which is the arrangement
|
|
185
|
+
* this file should have been sized against all along: on tara the widest settle
|
|
186
|
+
* yaw is 4.9° under a 6° hold budget, the widest pitch 2.6° under 5°, and roll
|
|
187
|
+
* stays the most conservative of the three because her roll *transition* is a
|
|
188
|
+
* recorded defect at any angle. The `dur` is untouched — a wider move over the
|
|
189
|
+
* same half second is a faster move, not a busier one, and the frequency is
|
|
190
|
+
* what the movement budget is about (CLAUDE.md).
|
|
164
191
|
*/
|
|
165
|
-
const SETTLE = { yaw: [0.
|
|
192
|
+
const SETTLE = { yaw: [0.22, 0.46], pitch: [-0.15, 0.11], roll: [0.14, 0.38], dur: [0.45, 0.8], switchP: 0.7 };
|
|
166
193
|
const HOME = { headYaw: 0, headPitch: 0, headRoll: 0 };
|
|
167
194
|
|
|
168
195
|
/**
|
|
@@ -186,7 +213,7 @@ const HOME = { headYaw: 0, headPitch: 0, headRoll: 0 };
|
|
|
186
213
|
* same gate, so a quiet head does not sit on a torso still rocking under it.
|
|
187
214
|
*/
|
|
188
215
|
const LIVE = {
|
|
189
|
-
yaw: 0.
|
|
216
|
+
yaw: 0.07, pitch: 0.028, roll: 0.07, breathPitch: 0.006,
|
|
190
217
|
retarget: [0.6, 2.2], tau: 0.38,
|
|
191
218
|
drift: [2.0, 5.0], still: [1.2, 3.5], stillGain: 0.12,
|
|
192
219
|
};
|
|
@@ -207,8 +234,7 @@ export class IdleLayer {
|
|
|
207
234
|
this.talk = 0;
|
|
208
235
|
this._nextBlink = 2 + Math.random() * 3;
|
|
209
236
|
this._blinkT = -1;
|
|
210
|
-
this._blinkDur = 0
|
|
211
|
-
this._double = false;
|
|
237
|
+
this._blinkDur = BLINK_DUR[0];
|
|
212
238
|
this._lastBlinkAt = -Infinity;
|
|
213
239
|
// Two incommensurate frequencies per axis so the sway never visibly loops.
|
|
214
240
|
this._ph = [Math.random() * 9, Math.random() * 9, Math.random() * 9];
|
|
@@ -294,19 +320,18 @@ export class IdleLayer {
|
|
|
294
320
|
* rate is a decision to revisit in that state, not something for the eyelids
|
|
295
321
|
* to drop silently.
|
|
296
322
|
*/
|
|
297
|
-
blink(
|
|
323
|
+
blink(evoked = false, forced = false) {
|
|
298
324
|
if (evoked) {
|
|
299
325
|
if (this.t - this._lastBlinkAt < BLINK_REFRACTORY) return;
|
|
300
326
|
const gap = this._nextBlink - this._lastBlinkAt;
|
|
301
327
|
if (!forced && this.t - this._lastBlinkAt < EVOKED_EARLIEST * gap) return;
|
|
302
328
|
}
|
|
303
|
-
this._startBlink(
|
|
329
|
+
this._startBlink(rand(BLINK_DUR));
|
|
304
330
|
}
|
|
305
331
|
|
|
306
|
-
_startBlink(dur
|
|
332
|
+
_startBlink(dur) {
|
|
307
333
|
if (this._blinkT >= 0) return;
|
|
308
334
|
this._blinkT = 0;
|
|
309
|
-
this._double = double;
|
|
310
335
|
this._blinkDur = dur;
|
|
311
336
|
this._lastBlinkAt = this.t;
|
|
312
337
|
this._nextBlink = this.t + rand(this.profile.blinkGap);
|
|
@@ -319,12 +344,12 @@ export class IdleLayer {
|
|
|
319
344
|
* of speech with no pauses in it.
|
|
320
345
|
*/
|
|
321
346
|
phraseBlink() {
|
|
322
|
-
this.blink(
|
|
347
|
+
this.blink(true);
|
|
323
348
|
}
|
|
324
349
|
|
|
325
350
|
/** A slow, deliberate blink — reads as thinking or fatigue. */
|
|
326
351
|
slowBlink() {
|
|
327
|
-
this._startBlink(0.34
|
|
352
|
+
this._startBlink(0.34);
|
|
328
353
|
}
|
|
329
354
|
|
|
330
355
|
_blinkValue(dt) {
|
|
@@ -333,7 +358,6 @@ export class IdleLayer {
|
|
|
333
358
|
const d = this._blinkDur;
|
|
334
359
|
const p = this._blinkT / d;
|
|
335
360
|
if (p >= 1) {
|
|
336
|
-
if (this._double) { this._double = false; this._blinkT = 0; return 0; }
|
|
337
361
|
this._blinkT = -1;
|
|
338
362
|
return 0;
|
|
339
363
|
}
|
|
@@ -351,10 +375,15 @@ export class IdleLayer {
|
|
|
351
375
|
// Re-armed here as well as in _startBlink, for the frame the timer comes
|
|
352
376
|
// due in the middle of a blink already running.
|
|
353
377
|
this._nextBlink = t + rand(pr.blinkGap);
|
|
354
|
-
// A share of them slow if the state asks for it
|
|
355
|
-
//
|
|
378
|
+
// A share of them slow if the state asks for it. **The rest are single,
|
|
379
|
+
// and a paired blink is not a thing this layer does** — a share of them
|
|
380
|
+
// used to re-close the instant the lid reached open, which at this
|
|
381
|
+
// duration is a 7-9 Hz flutter and was read by the owner on 2026-09-21 as
|
|
382
|
+
// a dropped frame rather than as a pair. A human doublet is two blinks a
|
|
383
|
+
// few hundred ms apart, which is what the gap already produces when it
|
|
384
|
+
// draws short; nothing in research-biomechanics.md asks for the rest.
|
|
356
385
|
if (Math.random() < pr.slowBlink) this.slowBlink();
|
|
357
|
-
else this.blink(
|
|
386
|
+
else this.blink();
|
|
358
387
|
}
|
|
359
388
|
const blink = this._blinkValue(dt);
|
|
360
389
|
|
|
@@ -524,6 +553,12 @@ export class IdleLayer {
|
|
|
524
553
|
// actually present on screen. `gain` is where a deployment that
|
|
525
554
|
// really is paying for the pixels turns it back down.
|
|
526
555
|
// The head is the exception: it settles rather than sways (SETTLE).
|
|
556
|
+
// The *idle* terms below went up again on 2026-09-21 — the owner read
|
|
557
|
+
// the figure as too restricted — and only the idle terms: the speech
|
|
558
|
+
// share is `prosody.js`'s business and is raised there, beside the
|
|
559
|
+
// words it is timed to. These are all far below 1.5 Hz, so the raise
|
|
560
|
+
// buys excursion and not rate; what it costs the encoder is a slightly
|
|
561
|
+
// larger slow motion, which is the cheap kind.
|
|
527
562
|
headYaw: hs.headYaw + flickYaw + post.headYaw * ps,
|
|
528
563
|
headPitch: hs.headPitch + workPitch,
|
|
529
564
|
headRoll: hs.headRoll + post.headRoll * ps,
|
|
@@ -531,9 +566,16 @@ export class IdleLayer {
|
|
|
531
566
|
// outbreath over the phrase (research-biomechanics.md §6.1), which the
|
|
532
567
|
// prosody layer draws. Quiet breathing steps mostly back under it.
|
|
533
568
|
breath: breath * this.gain * (1 - 0.6 * this.talk),
|
|
534
|
-
// The brows are never quite still either
|
|
535
|
-
|
|
536
|
-
|
|
569
|
+
// The brows are never quite still either — and they were nearly still
|
|
570
|
+
// here, at an amplitude a beat could swallow whole. Two video
|
|
571
|
+
// reviewers named a motionless upper face as the biggest reason these
|
|
572
|
+
// read as uncanny; `prosody.POSE.brow` answered that for a face that
|
|
573
|
+
// is *talking*, and the avatar listens far more than it speaks
|
|
574
|
+
// (CLAUDE.md). The two rates are further apart than they were so the
|
|
575
|
+
// pair drifts out of phase inside one call rather than over several:
|
|
576
|
+
// a matched pair is the drawing, not the face.
|
|
577
|
+
browRaiseL: s(0, 0.089) * 0.032 * a,
|
|
578
|
+
browRaiseR: s(1, 0.071) * 0.032 * a,
|
|
537
579
|
// Shoulders, and the body's share of speech emphasis — a head that
|
|
538
580
|
// moves on its own above a torso that never does is the head-on-a-stick
|
|
539
581
|
// read, and it was the loudest note in the first round of stakeholder
|
|
@@ -559,15 +601,15 @@ export class IdleLayer {
|
|
|
559
601
|
// said should be the louder. What is left here is the part prosody
|
|
560
602
|
// cannot supply: motion in the gaps *between* phrases, where there is
|
|
561
603
|
// no accent to hang anything on.
|
|
562
|
-
shoulderL: (s(1, 0.11) * 0.5 + s(2, 0.22) * 0.5) * (0.
|
|
604
|
+
shoulderL: (s(1, 0.11) * 0.5 + s(2, 0.22) * 0.5) * (0.052 * ad + 0.045 * say * this.gain)
|
|
563
605
|
+ workL + post.shoulderL * ps,
|
|
564
|
-
shoulderR: (s(2, 0.10) * 0.5 + s(0, 0.19) * 0.5) * (0.
|
|
606
|
+
shoulderR: (s(2, 0.10) * 0.5 + s(0, 0.19) * 0.5) * (0.052 * ad + 0.045 * say * this.gain)
|
|
565
607
|
+ workR + post.shoulderR * ps,
|
|
566
|
-
torsoLean: s(0, 0.085) * (0.
|
|
608
|
+
torsoLean: s(0, 0.085) * (0.036 * ad + 0.035 * say * this.gain) + post.torsoLean * ps,
|
|
567
609
|
// The trunk's own drift, small next to the weight shift that dominates
|
|
568
610
|
// this channel. It exists so the body is not perfectly still *between*
|
|
569
611
|
// shifts, which would make each shift read as a discrete event.
|
|
570
|
-
torsoTurn: (s(2, 0.047) * 0.6 + s(1, 0.031) * 0.4) * 0.
|
|
612
|
+
torsoTurn: (s(2, 0.047) * 0.6 + s(1, 0.031) * 0.4) * 0.075 * a
|
|
571
613
|
+ post.torsoTurn * ps,
|
|
572
614
|
},
|
|
573
615
|
};
|
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.
|
|
@@ -106,7 +132,7 @@ const POSE = {
|
|
|
106
132
|
// and a knit below it — so a phrase sitting negative is the only route a
|
|
107
133
|
// real furrow has into a sentence. Pinned at zero, half the asset was
|
|
108
134
|
// unreachable by construction. The floor is `thoughtful`'s own -0.14, which
|
|
109
|
-
//
|
|
135
|
+
// character-rig's knit weight already cites as a third of the crease; the ceiling
|
|
110
136
|
// stays well under a beat's 0.34 so a held level is never read as an accent.
|
|
111
137
|
brow: [-0.14, 0.18],
|
|
112
138
|
browTauS: 0.8,
|
|
@@ -195,16 +221,31 @@ 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
|
+
yawP: 0.45,
|
|
208
249
|
yaw: 0.12,
|
|
209
250
|
};
|
|
210
251
|
|
|
@@ -217,16 +258,21 @@ const BEAT = {
|
|
|
217
258
|
// shoulders at 0.19 s and the lean at 0.24 s. Each stroke returns through
|
|
218
259
|
// neutral: a trunk that only ever lifts is a standing shrug, which is what a
|
|
219
260
|
// filmstrip once showed as the figure sitting larger and lower in frame.
|
|
261
|
+
// Raised on 2026-09-21 with the rest of the body: the trunk has no measured
|
|
262
|
+
// hold budget the way the head does — nothing about a shoulder tells a
|
|
263
|
+
// projected photograph it has been turned too far — so it is the channel where
|
|
264
|
+
// "a bit more movement" is free, and it is the one the head-on-a-stick read
|
|
265
|
+
// comes from.
|
|
220
266
|
const BEAT_BODY = {
|
|
221
267
|
keys: [[-380, 0], [-60, 1], [140, 0.25], [320, -0.30], [600, 0]],
|
|
222
|
-
shoulder: 0.
|
|
268
|
+
shoulder: 0.36, lean: 0.15, spread: 0.35,
|
|
223
269
|
};
|
|
224
270
|
// The lift a phrase carries, over its own span: up on the first stressed
|
|
225
271
|
// syllable and settling below where it started as the breath goes out.
|
|
226
272
|
const PHRASE_BODY = {
|
|
227
273
|
keys: [[-260, 0], [180, 1], [900, 0.5], [1500, -0.30], [2400, 0]],
|
|
228
|
-
shoulder: 0.
|
|
229
|
-
lean: 0.
|
|
274
|
+
shoulder: 0.28,
|
|
275
|
+
lean: 0.21,
|
|
230
276
|
};
|
|
231
277
|
|
|
232
278
|
// The phrase-final settle: chin down this far from the phrase's own pose,
|
|
@@ -257,16 +303,40 @@ const HOME_MS = 700;
|
|
|
257
303
|
// to take the mouth.
|
|
258
304
|
// `ack` rises with the nod and outlasts it, since a smile that ends with the
|
|
259
305
|
// head's last beat reads as part of the gesture rather than as pleasure.
|
|
306
|
+
//
|
|
307
|
+
// `listen` is the fourth, and it is the one episode that is not about speech at
|
|
308
|
+
// all: the floor has arrived and the face receives it. Every other state change
|
|
309
|
+
// the avatar makes is a change of task; this one is a change of *who is
|
|
310
|
+
// talking*, and a face that takes the floor back with no expression at all is
|
|
311
|
+
// where the "not smiling enough" read comes from. Its shape is the owner's
|
|
312
|
+
// (2026-09-21): up, then a long fade to neutral rather than a held level, which
|
|
313
|
+
// is the same argument the other three are built on — a smile that stays is
|
|
314
|
+
// discounted. The full height lasts 1.8 s, inside Ekman's felt-smile window,
|
|
315
|
+
// and the remaining 3.8 s is the fade. **It is the warmest of the four, raised
|
|
316
|
+
// on the owner's read of 2026-09-21 that it was not arriving.** It started at
|
|
317
|
+
// `ack`'s height and reasoned that `close` — the face that hands the floor
|
|
318
|
+
// over — should outrank the face that takes it; on screen that had it landing
|
|
319
|
+
// as a politeness rather than as a welcome, and this is the one episode a user
|
|
320
|
+
// is looking straight at when it fires.
|
|
260
321
|
// Times in ms.
|
|
261
322
|
const WARMTH = {
|
|
262
323
|
onset: { keys: [[0, 0], [350, 1], [1500, 1], [2800, 0]], corner: 0.18, squint: 0.08 },
|
|
263
324
|
close: { keys: [[0, 0], [300, 1], [2600, 1], [3800, 0]], corner: 0.36, squint: 0.14 },
|
|
264
325
|
ack: { keys: [[0, 0], [220, 1], [1100, 1], [2000, 0]], corner: 0.30, squint: 0.12 },
|
|
326
|
+
listen: { keys: [[0, 0], [400, 1], [2200, 1], [6000, 0]], corner: 0.42, squint: 0.17 },
|
|
265
327
|
};
|
|
266
328
|
// A continuer can come every second or two. Each one smiling would hold the
|
|
267
329
|
// smile up for as long as the user talks, which is the fixed smile again, so
|
|
268
330
|
// an acknowledgement inside this long of the last smiling one nods without it.
|
|
269
331
|
const ACK_SMILE_REST_MS = 4000;
|
|
332
|
+
// The listening smile rests behind *any* warmth, not just its own, and this is
|
|
333
|
+
// the whole of what keeps it from becoming the fixed smile. A turn normally
|
|
334
|
+
// ends SPEAKING -> `close` -> LISTENING within a few hundred ms, and `close` is
|
|
335
|
+
// already that handover's smile; a state that flaps back through LISTENING —
|
|
336
|
+
// through THINKING and out again, or a barge-in and a resume — would otherwise
|
|
337
|
+
// re-arm one every time. Longer than the episodes themselves, so two can never
|
|
338
|
+
// run end to end.
|
|
339
|
+
const LISTEN_SMILE_REST_MS = 7000;
|
|
270
340
|
|
|
271
341
|
/**
|
|
272
342
|
* What a rig renders of the head amplitudes above, unless it says otherwise.
|
|
@@ -320,6 +390,7 @@ export class SpeechProsody {
|
|
|
320
390
|
this._moves = [];
|
|
321
391
|
this._warm = [];
|
|
322
392
|
this._lastAckSmile = -Infinity;
|
|
393
|
+
this._lastWarm = -Infinity;
|
|
323
394
|
this._head = new HeadPose();
|
|
324
395
|
this._home = true;
|
|
325
396
|
this._corner = 0;
|
|
@@ -360,7 +431,28 @@ export class SpeechProsody {
|
|
|
360
431
|
|
|
361
432
|
/** The track played out to its end: the turn's closing warmth. */
|
|
362
433
|
closeTurn() {
|
|
363
|
-
this.
|
|
434
|
+
this._smile(WARMTH.close);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The avatar has entered LISTENING: the floor is the user's. A warmth
|
|
439
|
+
* episode, unless something smiled recently — see `LISTEN_SMILE_REST_MS`,
|
|
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.
|
|
446
|
+
*/
|
|
447
|
+
listen() {
|
|
448
|
+
if (this._ms - this._lastWarm < LISTEN_SMILE_REST_MS) return;
|
|
449
|
+
this._smile(WARMTH.listen);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Start a warmth episode, and remember that the face has just smiled. */
|
|
453
|
+
_smile(w) {
|
|
454
|
+
this._lastWarm = this._ms;
|
|
455
|
+
this._warm.push({ at: this._ms, w });
|
|
364
456
|
}
|
|
365
457
|
|
|
366
458
|
/**
|
|
@@ -371,10 +463,12 @@ export class SpeechProsody {
|
|
|
371
463
|
acknowledge() {
|
|
372
464
|
if (this._ms - this._lastAckSmile < ACK_SMILE_REST_MS) return;
|
|
373
465
|
this._lastAckSmile = this._ms;
|
|
374
|
-
this.
|
|
466
|
+
this._smile(WARMTH.ack);
|
|
375
467
|
}
|
|
376
468
|
|
|
377
|
-
/** Cut off: a smile that survives being interrupted has not noticed.
|
|
469
|
+
/** Cut off: a smile that survives being interrupted has not noticed. The
|
|
470
|
+
* rest period is deliberately *not* cleared — being interrupted is not an
|
|
471
|
+
* occasion to smile a fresh one at the state change that follows. */
|
|
378
472
|
cool() {
|
|
379
473
|
this._warm = [];
|
|
380
474
|
}
|
|
@@ -480,7 +574,7 @@ export class SpeechProsody {
|
|
|
480
574
|
if (cue.v !== SILENT) {
|
|
481
575
|
if (this._onsetPending) {
|
|
482
576
|
this._onsetPending = false;
|
|
483
|
-
this.
|
|
577
|
+
this._smile(WARMTH.onset);
|
|
484
578
|
}
|
|
485
579
|
let a = i;
|
|
486
580
|
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;
|
package/src/speech-timing.js
CHANGED
|
@@ -21,3 +21,12 @@ 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 ahead of a rounded vowel the lips begin to round. Anticipatory
|
|
27
|
+
* rounding is among the longest-range coarticulation there is — 100–300 ms
|
|
28
|
+
* before an audible [y]/[u] (Schwartz & Savariaux 2014) — and this is the
|
|
29
|
+
* bottom of that range, because running speech compresses it and a pucker
|
|
30
|
+
* spread across a whole preceding word reads as a pout.
|
|
31
|
+
*/
|
|
32
|
+
export const ROUND_LEAD_MS = 100;
|