@voqalize/avatar 0.2.1 → 0.3.0
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 +21 -661
- package/README.md +144 -663
- package/client/Avatar.tsx +57 -0
- package/client/AvatarClient.ts +523 -0
- package/client/arjun.ts +24 -0
- package/client/createAvatar.ts +139 -0
- package/client/createCanvasAvatar.ts +72 -0
- package/client/index.ts +39 -0
- package/client/internal.ts +80 -0
- package/client/interviewer-female.ts +4 -0
- package/client/interviewer-male.ts +4 -0
- package/client/ishita.ts +24 -0
- package/client/kabir.ts +24 -0
- package/client/meera.ts +24 -0
- package/client/naina.ts +24 -0
- package/client/professional-female-a.ts +4 -0
- package/client/professional-female-b.ts +4 -0
- package/client/professional-male-a.ts +4 -0
- package/client/professional-male-b.ts +4 -0
- package/client/react.ts +13 -0
- package/client/types.ts +146 -0
- package/client/useAvatar.ts +74 -0
- package/client/vikram.ts +24 -0
- package/dist/Avatar.d.ts +44 -0
- package/dist/Avatar.d.ts.map +1 -0
- package/{client/dist → dist}/Avatar.js +2 -2
- package/dist/Avatar.js.map +1 -0
- package/dist/AvatarClient.d.ts +196 -0
- package/dist/AvatarClient.d.ts.map +1 -0
- package/dist/AvatarClient.js +450 -0
- package/dist/AvatarClient.js.map +1 -0
- package/dist/arjun.d.ts +6 -0
- package/dist/arjun.d.ts.map +1 -0
- package/dist/arjun.js +18 -0
- package/dist/arjun.js.map +1 -0
- package/dist/createAvatar.d.ts +99 -0
- package/dist/createAvatar.d.ts.map +1 -0
- package/dist/createAvatar.js +75 -0
- package/dist/createAvatar.js.map +1 -0
- package/dist/createCanvasAvatar.d.ts +22 -0
- package/dist/createCanvasAvatar.d.ts.map +1 -0
- package/dist/createCanvasAvatar.js +47 -0
- package/dist/createCanvasAvatar.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +33 -0
- package/dist/internal.js.map +1 -0
- package/dist/interviewer-female.d.ts +4 -0
- package/dist/interviewer-female.d.ts.map +1 -0
- package/dist/interviewer-female.js +3 -0
- package/dist/interviewer-female.js.map +1 -0
- package/dist/interviewer-male.d.ts +4 -0
- package/dist/interviewer-male.d.ts.map +1 -0
- package/dist/interviewer-male.js +3 -0
- package/dist/interviewer-male.js.map +1 -0
- package/dist/ishita.d.ts +6 -0
- package/dist/ishita.d.ts.map +1 -0
- package/dist/ishita.js +18 -0
- package/dist/ishita.js.map +1 -0
- package/dist/kabir.d.ts +6 -0
- package/dist/kabir.d.ts.map +1 -0
- package/dist/kabir.js +18 -0
- package/dist/kabir.js.map +1 -0
- package/dist/meera.d.ts +6 -0
- package/dist/meera.d.ts.map +1 -0
- package/dist/meera.js +18 -0
- package/dist/meera.js.map +1 -0
- package/dist/naina.d.ts +6 -0
- package/dist/naina.d.ts.map +1 -0
- package/dist/naina.js +18 -0
- package/dist/naina.js.map +1 -0
- package/dist/professional-female-a.d.ts +4 -0
- package/dist/professional-female-a.d.ts.map +1 -0
- package/dist/professional-female-a.js +3 -0
- package/dist/professional-female-a.js.map +1 -0
- package/dist/professional-female-b.d.ts +4 -0
- package/dist/professional-female-b.d.ts.map +1 -0
- package/dist/professional-female-b.js +3 -0
- package/dist/professional-female-b.js.map +1 -0
- package/dist/professional-male-a.d.ts +4 -0
- package/dist/professional-male-a.d.ts.map +1 -0
- package/dist/professional-male-a.js +3 -0
- package/dist/professional-male-a.js.map +1 -0
- package/dist/professional-male-b.d.ts +4 -0
- package/dist/professional-male-b.d.ts.map +1 -0
- package/dist/professional-male-b.js +3 -0
- package/dist/professional-male-b.js.map +1 -0
- package/dist/react.d.ts +13 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +13 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +93 -0
- package/dist/types.js.map +1 -0
- package/dist/useAvatar.d.ts +39 -0
- package/dist/useAvatar.d.ts.map +1 -0
- package/dist/useAvatar.js +35 -0
- package/dist/useAvatar.js.map +1 -0
- package/dist/vikram.d.ts +6 -0
- package/dist/vikram.d.ts.map +1 -0
- package/dist/vikram.js +18 -0
- package/dist/vikram.js.map +1 -0
- package/package.json +81 -24
- package/src/avatar.d.ts +161 -120
- package/src/avatar.js +234 -146
- package/src/behavior.d.ts +26 -0
- package/src/behavior.js +102 -0
- package/src/camera.js +29 -0
- package/src/canvas/author/parts/eye.mjs +722 -0
- package/src/canvas/author/parts/hand.mjs +1156 -0
- package/src/canvas/author/parts/mouth.mjs +741 -0
- package/src/canvas/author/parts/nose.mjs +100 -0
- package/src/canvas/author/parts/skin-detail.mjs +67 -0
- package/src/canvas/author/path.mjs +283 -0
- package/src/canvas/author/rig.mjs +405 -0
- package/src/canvas/avatars/round/face.d.mts +3 -0
- package/src/canvas/avatars/round/face.mjs +1307 -0
- package/src/canvas/create-rig.d.ts +15 -0
- package/src/canvas/create-rig.js +100 -0
- 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 +1 -0
- package/src/canvas/data/interviewer-male.rig.json +1 -0
- package/src/canvas/data/professional-female-a.rig.json +1 -0
- package/src/canvas/data/professional-female-b.rig.json +1 -0
- package/src/canvas/data/professional-male-a.rig.json +1 -0
- package/src/canvas/data/professional-male-b.rig.json +1 -0
- package/src/canvas/src/live.js +508 -0
- package/src/canvas/src/render2d.js +218 -0
- package/src/canvas/src/rig.js +297 -0
- package/src/canvas/src/vocab.js +96 -0
- package/src/clips.js +18 -7
- package/src/conformance.js +119 -0
- package/src/emotions.js +4 -3
- package/src/face-core.js +27 -1
- package/src/face-myna.d.ts +7 -0
- package/src/face-myna.js +192 -131
- package/src/face-peep-control-plane.js +167 -0
- package/src/face-peep.d.ts +7 -0
- package/src/face-peep.js +274 -116
- package/src/face-wren.d.ts +7 -0
- package/src/face-wren.js +17 -19
- package/src/faces.d.ts +12 -0
- package/src/faces.js +53 -0
- package/src/gaze.js +119 -4
- package/src/hand.js +120 -91
- package/src/idle.js +18 -101
- package/src/interjections.js +156 -31
- package/src/params.js +8 -4
- package/src/perform.js +7 -9
- package/src/rig.d.ts +24 -0
- package/src/rig.js +32 -0
- package/src/speech-timing.js +23 -0
- package/src/visemes.js +28 -12
- package/client/dist/Avatar.d.ts +0 -27
- package/client/dist/Avatar.d.ts.map +0 -1
- package/client/dist/Avatar.js.map +0 -1
- package/client/dist/AvatarClient.d.ts +0 -139
- package/client/dist/AvatarClient.d.ts.map +0 -1
- package/client/dist/AvatarClient.js +0 -223
- package/client/dist/AvatarClient.js.map +0 -1
- package/client/dist/index.d.ts +0 -22
- package/client/dist/index.d.ts.map +0 -1
- package/client/dist/index.js +0 -22
- package/client/dist/index.js.map +0 -1
- package/client/dist/types.d.ts +0 -86
- package/client/dist/types.d.ts.map +0 -1
- package/client/dist/types.js +0 -31
- package/client/dist/types.js.map +0 -1
- package/client/dist/useAvatar.d.ts +0 -42
- package/client/dist/useAvatar.d.ts.map +0 -1
- package/client/dist/useAvatar.js +0 -51
- package/client/dist/useAvatar.js.map +0 -1
- package/client/src/Avatar.tsx +0 -33
- package/client/src/AvatarClient.ts +0 -270
- package/client/src/index.ts +0 -22
- package/client/src/types.ts +0 -104
- package/client/src/useAvatar.ts +0 -85
- package/docs/contract-avatar.md +0 -371
- package/docs/contract-protocol.md +0 -461
package/src/hand.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* The avatar has no arms (CLAUDE.md, constraint 9): two earlier rigs carried a
|
|
5
5
|
* full forearm/hand chain, nine parameter channels and several hundred lines of
|
|
6
6
|
* geometry each, and it was removed on sight. This is the other design. It
|
|
7
|
-
* survived a stakeholder trial
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* survived a stakeholder trial and was promoted on 2026-08-07; what makes it a
|
|
8
|
+
* different proposition from the thing that was cut is that **it is not part of
|
|
9
|
+
* the rig**. No parameter channel, no per-face
|
|
10
10
|
* geometry, no forearm — one drawing, placed by four numbers derived from the
|
|
11
11
|
* mounted avatar's own `META.viewBox`, painted in its own theme, appended over
|
|
12
12
|
* its SVG. A face that never plays a gesture is byte-for-byte what it was.
|
|
@@ -39,37 +39,37 @@
|
|
|
39
39
|
* the waves sit LOWER and further back instead (`sc`), and the face stays
|
|
40
40
|
* entirely clear.
|
|
41
41
|
*
|
|
42
|
-
* 4. THE ONLY EDGE THAT CUTS THE HAND IS THE BOTTOM ONE.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* as a rendering fault. Outboard travel is budgeted against the hand's own
|
|
42
|
+
* 4. THE ONLY EDGE THAT CUTS THE HAND IS THE BOTTOM ONE. Anything past the
|
|
43
|
+
* camera's side edge is sliced by a hard vertical line that reads as a
|
|
44
|
+
* rendering fault. Outboard travel is budgeted against the hand's own
|
|
46
45
|
* width AND its rotation — a hand 440 units tall swings ~110 units sideways
|
|
47
46
|
* at 14 degrees, which is what silently blew the budget the first time.
|
|
48
47
|
* `checkHandFraming()` asserts both rules against the real timelines.
|
|
49
48
|
*
|
|
50
|
-
* See docs/
|
|
51
|
-
* docs/
|
|
49
|
+
* See docs/internal-mixer.md § Hand gestures for the wire surface and
|
|
50
|
+
* docs/authoring-a-face.md § The hand for what a face module owes this module
|
|
52
51
|
* (the answer is: a viewBox and two theme keys).
|
|
53
52
|
*/
|
|
54
53
|
|
|
55
54
|
import { taper, taperRing, region, smooth } from './line-art.js';
|
|
56
55
|
import { f } from './face-core.js';
|
|
56
|
+
import { CALL_CAMERA } from './camera.js';
|
|
57
57
|
|
|
58
58
|
// --- the frame --------------------------------------------------------------
|
|
59
59
|
// Everything the hand needs to place itself comes out of `META.viewBox`, and
|
|
60
|
-
// that is the whole reason this module needed no new META field. The
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// that is the whole reason this module needed no new META field. The shared
|
|
61
|
+
// camera makes 70% of its height a head, so size follows the person rather
|
|
62
|
+
// than growing merely because the new 4:3 camera is wider.
|
|
63
63
|
//
|
|
64
64
|
// The wrist sits below the frame edge by a clear margin, never at it: a wrist
|
|
65
65
|
// exactly on the line renders as a rounded end kissing the border, which reads
|
|
66
66
|
// as a hand stuck to the frame rather than one coming from off-camera.
|
|
67
67
|
const WRIST_DROP = 24; // minimum units the wrist stays below the frame bottom
|
|
68
68
|
const HIDE = 574; // fully off-camera; must clear the tallest shape
|
|
69
|
-
// How far the outermost ink may sit from the frame centre before the
|
|
70
|
-
//
|
|
69
|
+
// How far the outermost ink may sit from the frame centre before the camera
|
|
70
|
+
// slices it (rule 4): half the window, less a hair of margin.
|
|
71
71
|
const SIDE_MARGIN = 8;
|
|
72
|
-
// Author scale at a
|
|
72
|
+
// Author scale at a 480-unit-tall head. The number this design changed most,
|
|
73
73
|
// and both directions were rendered and judged. Earlier cuts drew the hand at
|
|
74
74
|
// FACE depth: a 19 cm hand against a 23 cm head is 0.83 of it, peep's head is
|
|
75
75
|
// 477 art units, so ~394 — which came out looking like a pale tube rising past
|
|
@@ -85,7 +85,8 @@ const SIDE_MARGIN = 8;
|
|
|
85
85
|
// becomes ground, and the tile reads as broken before it reads as a gesture.
|
|
86
86
|
// Optically correct, perceptually wrong. 2.95 puts the palm at ~0.82
|
|
87
87
|
// head-widths — still clearly nearer the lens, still clearly not the subject.
|
|
88
|
-
|
|
88
|
+
// Compact avatar tiles need a hand that reads as a gesture, not as the subject.
|
|
89
|
+
const REACH_AT_480_HEAD = 2.40;
|
|
89
90
|
|
|
90
91
|
/** The four placement numbers, derived. A host never sees these. */
|
|
91
92
|
function frameOf(viewBox) {
|
|
@@ -93,7 +94,7 @@ function frameOf(viewBox) {
|
|
|
93
94
|
return {
|
|
94
95
|
cx: vb.x + vb.w / 2, // frame centre; also where a resting hand starts
|
|
95
96
|
bottom: vb.y + vb.h, // the visible bottom edge — the line the hand rises past
|
|
96
|
-
reach: (
|
|
97
|
+
reach: (REACH_AT_480_HEAD * vb.h * CALL_CAMERA.head) / 480,
|
|
97
98
|
outboardLimit: vb.w / 2 - SIDE_MARGIN,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
@@ -129,17 +130,10 @@ const PALM = [
|
|
|
129
130
|
// bottom edge, which reads as a shape RESTING ON the edge (a bag, a sheet of
|
|
130
131
|
// paper) rather than one continuing past it. Widest at ~a third down from the
|
|
131
132
|
// fingertips, ~65% of that at the cut, and the eye infers the arm for free.
|
|
132
|
-
[
|
|
133
|
-
[
|
|
134
|
-
[
|
|
135
|
-
[
|
|
136
|
-
[13, -134], // notch floor: ~15% of finger length, and a U not a V
|
|
137
|
-
[8, -146], [1, -164], [-8, -166], // middle, the longest
|
|
138
|
-
[-16, -156],
|
|
139
|
-
[-20, -136], // notch floor
|
|
140
|
-
[-25, -148], [-33, -157], [-40, -150], // index, domed
|
|
141
|
-
[-44, -130], [-46, -110],
|
|
142
|
-
[-47, -92], // the thumb web — shallow, so this is a hand not a fork
|
|
133
|
+
[24, 300], [26, 58], [30, 18], [38, -18], [42, -48],
|
|
134
|
+
[39, -72], [31, -86], [21, -84],
|
|
135
|
+
[17, -104], [8, -116], [-4, -114], [-13, -102], [-16, -84],
|
|
136
|
+
[-22, -102], [-34, -111], [-46, -104], [-53, -91], [-53, -76],
|
|
143
137
|
// The thumb is a CONVEX WEDGE, and that is a deletion rather than an
|
|
144
138
|
// addition. An earlier thumb had a re-entrant curl in its outer contour; at
|
|
145
139
|
// 130 px the hook read as a detached ear sitting beside the jaw, which was the
|
|
@@ -147,11 +141,8 @@ const PALM = [
|
|
|
147
141
|
// this contour: it leaves the palm's outer edge, swells to a rounded pad at
|
|
148
142
|
// ~40 degrees off the palm axis, and comes back. A thumb is a wedge, and a
|
|
149
143
|
// wedge has two edges.
|
|
150
|
-
[-
|
|
151
|
-
[-
|
|
152
|
-
[-56, -58], [-49, -46],
|
|
153
|
-
[-46, -26], [-42, 4], // thenar, then the taper
|
|
154
|
-
[-36, 60], [-32, 300],
|
|
144
|
+
[-68, -73], [-80, -64], [-84, -52], [-80, -40],
|
|
145
|
+
[-68, -31], [-56, -22], [-47, -5], [-41, 28], [-34, 300],
|
|
155
146
|
];
|
|
156
147
|
// Where the finger information moved to. An earlier cut put the interdigital
|
|
157
148
|
// valleys 50-60% of the way down the fingers, which makes a SAW: at 130 px a
|
|
@@ -167,8 +158,8 @@ const PALM = [
|
|
|
167
158
|
// size this ships at) and at full size it reads as a scratch. Economy is not a
|
|
168
159
|
// style here, it is the difference between a mark and a blemish.
|
|
169
160
|
const PALM_SEPS = [
|
|
170
|
-
[[
|
|
171
|
-
[[-20, -
|
|
161
|
+
[[10, -94], [10, -89], [11, -84]],
|
|
162
|
+
[[-20, -94], [-21, -89], [-21, -84]],
|
|
172
163
|
];
|
|
173
164
|
// The one interior mark that says PALM rather than back-of-hand — and it is the
|
|
174
165
|
// THENAR crease, the arc around the ball of the thumb, not a line across the
|
|
@@ -177,7 +168,7 @@ const PALM_SEPS = [
|
|
|
177
168
|
// rounded white form, at tile size, beside a face, reads as a MOUTH. The arc
|
|
178
169
|
// runs off the bottom edge instead of ending, so nothing about it says "this
|
|
179
170
|
// mark stopped here".
|
|
180
|
-
const PALM_CREASE = [[-
|
|
171
|
+
const PALM_CREASE = [[-53, -65], [-46, -47], [-34, -27], [-18, -8]];
|
|
181
172
|
|
|
182
173
|
// --- dorsal: the fist, for thumbs-up ----------------------------------------
|
|
183
174
|
// The back of a closed hand: an undulating top edge of metacarpal heads, and a
|
|
@@ -199,13 +190,10 @@ const PALM_CREASE = [[-46, -80], [-41, -54], [-30, -26], [-14, -2], [-8, 14]];
|
|
|
199
190
|
// that is true, but by 16 units across 60 rather than by 30; the mass is 82 wide
|
|
200
191
|
// against 78 visible. The two undulations survive only as inflections in it.
|
|
201
192
|
const FIST = [
|
|
202
|
-
[24, 300], [26,
|
|
203
|
-
[
|
|
204
|
-
[
|
|
205
|
-
[
|
|
206
|
-
[-26, -76], [-36, -64],
|
|
207
|
-
[-42, -42], [-41, -16],
|
|
208
|
-
[-37, 60], [-34, 300],
|
|
193
|
+
[24, 300], [26, 62], [30, 20], [39, -7], [45, -31],
|
|
194
|
+
[42, -51], [31, -64], [14, -70], [-4, -69],
|
|
195
|
+
[-21, -63], [-35, -51], [-42, -31], [-40, -6],
|
|
196
|
+
[-35, 62], [-31, 300],
|
|
209
197
|
];
|
|
210
198
|
// Four small knuckle bumps were drawn at the same scale and rhythm as peep's
|
|
211
199
|
// hair spikes, so at 130 px the row read as a lapel zigzag continuing the
|
|
@@ -223,7 +211,7 @@ const FIST = [
|
|
|
223
211
|
// frame cut and left the knuckle half of the fist empty. An empty top and a
|
|
224
212
|
// marked bottom is upside down: the eye goes to the crest first, and finds
|
|
225
213
|
// nothing there.
|
|
226
|
-
const FIST_CURL = [[
|
|
214
|
+
const FIST_CURL = [[22, -43], [7, -50], [-10, -49], [-25, -41]];
|
|
227
215
|
// The thumb is a SEPARATE CLOSED SHAPE crossing the fist, which is what keeps
|
|
228
216
|
// the middle-finger read dead — the first thumbs-up drawn here was rejected on
|
|
229
217
|
// sight for exactly that. A glyph has tolerances and these are them:
|
|
@@ -248,12 +236,9 @@ const FIST_CURL = [[24, -48], [8, -58], [-8, -60], [-24, -52]];
|
|
|
248
236
|
// its tip and the entire radial column reads as ONE very long digit — the
|
|
249
237
|
// middle-finger silhouette, rebuilt out of correct parts.
|
|
250
238
|
const FIST_THUMB = [
|
|
251
|
-
[
|
|
252
|
-
[
|
|
253
|
-
[-
|
|
254
|
-
[-38, -106], [-38, -88],
|
|
255
|
-
[-36, -70], [-32, -56], [-25, -44], // outer edge, tucking back under the crest
|
|
256
|
-
[10, -48],
|
|
239
|
+
[17, -42], [8, -61], [2, -78], [-8, -90],
|
|
240
|
+
[-21, -90], [-31, -80], [-33, -66], [-27, -52],
|
|
241
|
+
[-16, -41], [17, -42],
|
|
257
242
|
];
|
|
258
243
|
// There is no thumbnail, and that is the clearest single deletion in this
|
|
259
244
|
// drawing. A short curved crease near the top of the thumb, at 130 px, on a
|
|
@@ -270,11 +255,9 @@ const FIST_THUMB = [
|
|
|
270
255
|
// meaning lives in one unambiguous silhouette rather than in notches, and where
|
|
271
256
|
// it crosses the hair, value does the separating.
|
|
272
257
|
const POINT = [
|
|
273
|
-
[24, 300], [26,
|
|
274
|
-
[
|
|
275
|
-
[
|
|
276
|
-
[12, -80], [-2, -84],
|
|
277
|
-
[-8, -90], // the web — a rounded corner, barely a notch
|
|
258
|
+
[24, 300], [26, 62], [30, 20], [39, -7], [44, -29],
|
|
259
|
+
[37, -49], [17, -53], [8, -66], [6, -107],
|
|
260
|
+
[0, -128], [-11, -136], [-23, -130], [-29, -116], [-29, -72],
|
|
278
261
|
// The index. It once cleared the knuckles by 75 units against a visible fist
|
|
279
262
|
// of 90 — anatomically defensible, and at 0.79 of a head-width it was the
|
|
280
263
|
// longest single mark in the tile. Nothing that is not the face gets to be
|
|
@@ -286,15 +269,9 @@ const POINT = [
|
|
|
286
269
|
// It tapers toward the tip and carries no joint pinch. The pinch was drawn in
|
|
287
270
|
// the OUTLINE, and a wobble that small in a contour does not read as a knuckle
|
|
288
271
|
// at any size — it reads as an unsteady line.
|
|
289
|
-
[-12, -106], [-15, -122], // inner edge
|
|
290
|
-
[-18, -136], [-25, -146], [-32, -143], // domed tip, never pointed
|
|
291
|
-
[-35, -129], [-34, -112], // outer edge
|
|
292
|
-
[-36, -94],
|
|
293
272
|
// The thumb, clamped across the curled fingers and showing as a lobe on the
|
|
294
273
|
// flank. An index-up fist with no thumb anywhere in it is quietly impossible.
|
|
295
|
-
[-
|
|
296
|
-
[-42, -38], [-41, -16],
|
|
297
|
-
[-37, 60], [-34, 300],
|
|
274
|
+
[-39, -55], [-42, -31], [-40, -6], [-35, 62], [-31, 300],
|
|
298
275
|
];
|
|
299
276
|
|
|
300
277
|
// Ink weights are in ART units, sampled off peep's own marks: the torso runs
|
|
@@ -318,22 +295,22 @@ const POINT = [
|
|
|
318
295
|
// (compose so it doesn't cross; weight hierarchy; knockout) and it is the one
|
|
319
296
|
// already native to these rigs, whose head contour outweighs the brow which
|
|
320
297
|
// outweighs the ear whorl.
|
|
321
|
-
const W_OUTLINE = [
|
|
298
|
+
const W_OUTLINE = [12, 11, 8, 6, 8, 11, 12];
|
|
322
299
|
// Device 3, the knockout: a white halo that breaks the shirt's seams where the
|
|
323
300
|
// hand crosses them. It is deliberately NARROW across the middle of the mark:
|
|
324
301
|
// the hand's upper half sits against the background, and a white rim there would
|
|
325
302
|
// turn it into a cut-out sticker. Only the tails — the part over the shirt — get
|
|
326
303
|
// the full gap. Where the halo width equals the ink width it is entirely covered
|
|
327
304
|
// and costs nothing, so the profile is scaled with the ink above it.
|
|
328
|
-
const W_HALO = [
|
|
305
|
+
const W_HALO = [26, 22, 13, 6, 13, 22, 26];
|
|
329
306
|
const W_CREASE = [2, 8, 2];
|
|
330
307
|
// The palm's finger separators: thick where they leave the notch floor, gone by
|
|
331
308
|
// the end. This is the ear-whorl mark language, applied to a hand.
|
|
332
|
-
const W_SEP = [
|
|
309
|
+
const W_SEP = [5, 2, 0.5];
|
|
333
310
|
// The fist's one interior mark. Thick-to-thin across an S, so it reads as a form
|
|
334
311
|
// turning rather than as a drawn line.
|
|
335
312
|
const W_CURL = [3, 9, 7, 2];
|
|
336
|
-
const W_THUMB = [
|
|
313
|
+
const W_THUMB = [9, 11, 9, 7, 9, 11, 9];
|
|
337
314
|
|
|
338
315
|
// The shapes, and the interior marks each one carries. Interior marks carry
|
|
339
316
|
// their own width profile rather than sharing one: three of them exist, they do
|
|
@@ -354,7 +331,7 @@ const SHAPES = {
|
|
|
354
331
|
// being an empty slab.
|
|
355
332
|
POINT: {
|
|
356
333
|
outline: POINT,
|
|
357
|
-
marks: [{ pts: [[
|
|
334
|
+
marks: [{ pts: [[20, -42], [5, -48], [-11, -47]], w: W_CURL }],
|
|
358
335
|
rings: [],
|
|
359
336
|
},
|
|
360
337
|
};
|
|
@@ -435,11 +412,11 @@ export const HAND_GESTURES = {
|
|
|
435
412
|
//
|
|
436
413
|
// The swing is deliberately ASYMMETRIC, -2 out and +16 in. A wave rotating
|
|
437
414
|
// about a wrist below the frame throws the fingertips ~110 units sideways,
|
|
438
|
-
// and spending that outboard is what put the thumb through the
|
|
439
|
-
//
|
|
415
|
+
// and spending that outboard is what put the thumb through the camera edge
|
|
416
|
+
// the first time. Swinging further toward the person you are waving at
|
|
440
417
|
// is also, conveniently, what people do.
|
|
441
|
-
|
|
442
|
-
id: '
|
|
418
|
+
GESTURE_GREET: {
|
|
419
|
+
id: 'GESTURE_GREET', label: 'greet', shape: 'PALM', face: 'GESTURE_GREET', dur: 1250, sc: 0.70,
|
|
443
420
|
out: [[0, 0], [150, 74], [300, 114], [1000, 114], [1250, 30]],
|
|
444
421
|
dy: [[0, HIDE], [150, 240], [300, 40], [390, 60], [700, 50], [1000, 58], [1120, 136], [1250, HIDE]],
|
|
445
422
|
rot: [[0, -3], [150, 2], [310, 16], [475, -2], [640, 16], [805, -1], [970, 12], [1000, 8], [1250, -3]],
|
|
@@ -447,8 +424,8 @@ export const HAND_GESTURES = {
|
|
|
447
424
|
// Goodbye: same hand, slower, one more swing, and it lingers at the top
|
|
448
425
|
// before dropping. A wave that leaves as briskly as it arrived reads as a
|
|
449
426
|
// dismissal rather than a farewell.
|
|
450
|
-
|
|
451
|
-
id: '
|
|
427
|
+
GESTURE_GOODBYE: {
|
|
428
|
+
id: 'GESTURE_GOODBYE', label: 'goodbye', shape: 'PALM', face: 'GESTURE_GREET', dur: 1550, sc: 0.70,
|
|
452
429
|
out: [[0, 0], [170, 76], [320, 116], [1300, 116], [1550, 32]],
|
|
453
430
|
dy: [[0, HIDE], [160, 230], [320, 32], [410, 54], [700, 42], [1000, 52], [1300, 46], [1420, 128], [1550, HIDE]],
|
|
454
431
|
rot: [[0, -3], [170, 2], [330, 16], [510, -2], [690, 16], [870, -2], [1050, 16], [1230, -1], [1300, 8], [1550, -3]],
|
|
@@ -461,8 +438,8 @@ export const HAND_GESTURES = {
|
|
|
461
438
|
// exactly why it read the way it did. The wrist spends the entire budget (it
|
|
462
439
|
// sits on the floor), so the fist rides the bottom edge and only the thumb is
|
|
463
440
|
// up near the face.
|
|
464
|
-
|
|
465
|
-
id: '
|
|
441
|
+
GESTURE_APPROVE: {
|
|
442
|
+
id: 'GESTURE_APPROVE', label: 'approve', shape: 'FIST', face: 'GESTURE_APPROVE', dur: 1300, sc: 0.82,
|
|
466
443
|
out: [[0, 0], [160, 54], [300, 84], [1050, 84], [1300, 32]],
|
|
467
444
|
dy: [[0, HIDE], [160, 174], [300, 24], [390, 42], [700, 34], [1050, 40], [1160, 120], [1300, HIDE]],
|
|
468
445
|
rot: [[0, -6], [160, -2], [300, 3], [400, 0], [700, 1.5], [1000, 0], [1050, 0], [1300, -8]],
|
|
@@ -472,8 +449,8 @@ export const HAND_GESTURES = {
|
|
|
472
449
|
// couple of units and a degree, which reads as a held hand rather than as a
|
|
473
450
|
// stopped clock. Held longest of the four, because it is the one gesture
|
|
474
451
|
// whose job is to buy time (research-perception.md §1, latency masking).
|
|
475
|
-
|
|
476
|
-
id: '
|
|
452
|
+
GESTURE_WAIT: {
|
|
453
|
+
id: 'GESTURE_WAIT', label: 'wait', shape: 'POINT', face: 'GESTURE_WAIT', dur: 1700, sc: 0.78,
|
|
477
454
|
out: [[0, 0], [170, 60], [320, 96], [1400, 96], [1700, 34]],
|
|
478
455
|
dy: [[0, HIDE], [170, 244], [320, 32], [410, 54], [700, 46], [1100, 53], [1400, 48], [1520, 136], [1700, HIDE]],
|
|
479
456
|
rot: [[0, -7], [170, -2], [320, 2], [420, 0], [800, 1], [1200, -0.5], [1400, 0], [1700, -9]],
|
|
@@ -482,6 +459,22 @@ export const HAND_GESTURES = {
|
|
|
482
459
|
|
|
483
460
|
export const HAND_GESTURE_IDS = Object.keys(HAND_GESTURES);
|
|
484
461
|
|
|
462
|
+
/** Public renderer-frame gesture names → current SVG timelines. */
|
|
463
|
+
const FRAME_GESTURES = Object.freeze({
|
|
464
|
+
greet: 'GESTURE_GREET',
|
|
465
|
+
farewell: 'GESTURE_GOODBYE',
|
|
466
|
+
approve: 'GESTURE_APPROVE',
|
|
467
|
+
wait: 'GESTURE_WAIT',
|
|
468
|
+
});
|
|
469
|
+
const GESTURE_TO_FRAME = Object.freeze(Object.fromEntries(
|
|
470
|
+
Object.entries(FRAME_GESTURES).map(([name, id]) => [id, name]),
|
|
471
|
+
));
|
|
472
|
+
|
|
473
|
+
/** Stable SVG-action names → renderer-neutral hand controls. */
|
|
474
|
+
export const HAND_ACTION_TO_FRAME_GESTURE = Object.freeze({
|
|
475
|
+
...GESTURE_TO_FRAME,
|
|
476
|
+
});
|
|
477
|
+
|
|
485
478
|
// GO_ON is deliberately absent, and this is the reasoning rather than an
|
|
486
479
|
// oversight. It was drawn as a low splayed open palm rocking at the wrist; the
|
|
487
480
|
// stakeholder's verdict was "go on doesn't work for me" and the review was
|
|
@@ -520,7 +513,7 @@ function sample(keys, t) {
|
|
|
520
513
|
|
|
521
514
|
/**
|
|
522
515
|
* Mount the hand over a face and return the player. Built by `createAvatar`;
|
|
523
|
-
*
|
|
516
|
+
* the public avatar composes it through a semantic `GESTURE_*` action.
|
|
524
517
|
*
|
|
525
518
|
* @param {SVGElement} svg the mounted face's own root — same coordinate space
|
|
526
519
|
* @param {{ink:string, paper:string}} theme the mounted face's palette
|
|
@@ -575,6 +568,7 @@ export function createHand(svg, theme, meta, opts = {}) {
|
|
|
575
568
|
for (const [k, s] of Object.entries(SHAPES)) shapes[k] = build(s);
|
|
576
569
|
|
|
577
570
|
let current = null; // { def, start }
|
|
571
|
+
const queue = [];
|
|
578
572
|
let lastT = 0;
|
|
579
573
|
|
|
580
574
|
function place(x, y, rot, sc) {
|
|
@@ -588,19 +582,50 @@ export function createHand(svg, theme, meta, opts = {}) {
|
|
|
588
582
|
for (const s of Object.values(shapes)) s.style.display = 'none';
|
|
589
583
|
place(fr.cx, fr.bottom + HIDE, 0, 1);
|
|
590
584
|
}
|
|
585
|
+
function show(def) {
|
|
586
|
+
for (const [k, s] of Object.entries(shapes)) s.style.display = k === def.shape ? '' : 'none';
|
|
587
|
+
}
|
|
588
|
+
function applyFrame(frame) {
|
|
589
|
+
if (!frame) { park(); return; }
|
|
590
|
+
const id = FRAME_GESTURES[frame.gesture];
|
|
591
|
+
const def = HAND_GESTURES[id];
|
|
592
|
+
if (!def) { park(); return; }
|
|
593
|
+
dir = frame.side === 'left' ? -1 : 1;
|
|
594
|
+
show(def);
|
|
595
|
+
const local = Math.max(0, Math.min(1, frame.progress)) * def.dur;
|
|
596
|
+
place(
|
|
597
|
+
fr.cx + dir * sample(def.out, local),
|
|
598
|
+
fr.bottom + sample(def.dy, local),
|
|
599
|
+
sample(def.rot, local),
|
|
600
|
+
def.sc || 1,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
591
603
|
park();
|
|
592
604
|
|
|
593
605
|
return {
|
|
594
606
|
get playing() { return !!current; },
|
|
595
607
|
get id() { return current ? current.def.id : null; },
|
|
608
|
+
/** The semantic hand control for the current animation frame, or null. */
|
|
609
|
+
get frame() {
|
|
610
|
+
if (!current) return null;
|
|
611
|
+
return {
|
|
612
|
+
gesture: GESTURE_TO_FRAME[current.def.id],
|
|
613
|
+
progress: Math.max(0, Math.min(1, (lastT - current.start) / current.def.dur)),
|
|
614
|
+
side: dir === -1 ? 'left' : 'right',
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
/** Render a first-class AvatarFrame hand control. */
|
|
618
|
+
applyFrame,
|
|
596
619
|
setDir(d) { dir = d === -1 ? -1 : 1; if (!current) park(); },
|
|
597
620
|
/** @param {string} id @param {number} [atMs] start time on the layer's clock */
|
|
598
|
-
play(id, atMs) {
|
|
621
|
+
play(id, atMs, { queue: shouldQueue = false } = {}) {
|
|
599
622
|
const def = HAND_GESTURES[id];
|
|
600
623
|
if (!def) throw new Error(`unknown hand gesture: ${id}`);
|
|
601
|
-
|
|
602
|
-
|
|
624
|
+
if (current && shouldQueue) {
|
|
625
|
+
if (current.def.id !== id && !queue.some((item) => item.id === id)) queue.push({ id, def });
|
|
626
|
+
return def;
|
|
603
627
|
}
|
|
628
|
+
show(def);
|
|
604
629
|
current = { def, start: atMs !== undefined ? atMs : lastT };
|
|
605
630
|
return def;
|
|
606
631
|
},
|
|
@@ -610,15 +635,19 @@ export function createHand(svg, theme, meta, opts = {}) {
|
|
|
610
635
|
const local = tMs - current.start;
|
|
611
636
|
if (local < 0) return;
|
|
612
637
|
const def = current.def;
|
|
613
|
-
if (local >= def.dur) {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
638
|
+
if (local >= def.dur) {
|
|
639
|
+
const done = def;
|
|
640
|
+
const next = queue.shift();
|
|
641
|
+
if (next) {
|
|
642
|
+
show(next.def);
|
|
643
|
+
current = { def: next.def, start: tMs };
|
|
644
|
+
} else {
|
|
645
|
+
current = null;
|
|
646
|
+
}
|
|
647
|
+
return done;
|
|
648
|
+
}
|
|
620
649
|
},
|
|
621
|
-
stop() { current = null; park(); },
|
|
650
|
+
stop() { current = null; queue.length = 0; park(); },
|
|
622
651
|
destroy() { if (g.parentNode) g.parentNode.removeChild(g); },
|
|
623
652
|
};
|
|
624
653
|
}
|
|
@@ -626,12 +655,12 @@ export function createHand(svg, theme, meta, opts = {}) {
|
|
|
626
655
|
/**
|
|
627
656
|
* Rules 1 and 4, asserted rather than eyeballed — both were violated by an
|
|
628
657
|
* early cut and neither is visible in a still of the resting pose. Run by
|
|
629
|
-
* `
|
|
658
|
+
* `pnpm test` against every registered avatar, because the frame numbers
|
|
630
659
|
* are derived per avatar and a rig with a different window could break the
|
|
631
660
|
* budget without anything else noticing.
|
|
632
661
|
*
|
|
633
662
|
* rule 1 the wrist never rises into the frame;
|
|
634
|
-
* rule 4 no gesture pushes ink through the
|
|
663
|
+
* rule 4 no gesture pushes ink through the camera window's side edge, at
|
|
635
664
|
* any point in its rotation. A hand 440 units tall throws its outer
|
|
636
665
|
* corner ~110 units sideways at 14 degrees, so the budget has to be
|
|
637
666
|
* spent against `out` AND `rot` together.
|
package/src/idle.js
CHANGED
|
@@ -62,7 +62,7 @@ const rand = ([a, b]) => a + Math.random() * (b - a);
|
|
|
62
62
|
* Everything else here is an oscillator, and an oscillator cannot make a body
|
|
63
63
|
* look alive across a thirty-second hold. Fast enough to notice and it reads
|
|
64
64
|
* as rocking; slow enough not to and it is indistinguishable from a still
|
|
65
|
-
* image. A motion map of the listening state (tools/motion.mjs) showed the
|
|
65
|
+
* image. A motion map of the listening state (apps/authoring/tools/motion.mjs) showed the
|
|
66
66
|
* outer edge of the torso travelling zero pixels over 24 seconds — the sway
|
|
67
67
|
* was there in the numbers and rendered as nothing at all.
|
|
68
68
|
*
|
|
@@ -384,91 +384,37 @@ export class IdleLayer {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
* The listening engine
|
|
387
|
+
* The listening engine is now deliberately posture-only.
|
|
388
388
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* (docs/research-biomechanics.md §3.5). So this engine is contingent first:
|
|
394
|
-
*
|
|
395
|
-
* · The server tells it about the user's voice — a coarse speaking flag
|
|
396
|
-
* (setUserSpeaking), off its own endpointer. With no signal at all the
|
|
397
|
-
* scheduler falls back to a timer, which is the weakest mode and is meant
|
|
398
|
-
* to be: contingency is the whole point.
|
|
399
|
-
* · Acknowledgements fire at PAUSE ONSETS: when the user stops talking, a
|
|
400
|
-
* nod lands 250–600 ms later, about half the time, never more often than
|
|
401
|
-
* every 2.5 s. That timing is where a human listener's nod sits.
|
|
402
|
-
* · During a long unbroken stretch of user speech a rare mid-speech nod
|
|
403
|
-
* keeps the face alive (nods fill ~26% of human listening time — we err
|
|
404
|
-
* far quieter, per the screen-share bitrate constraint).
|
|
405
|
-
* · Engagement posture: `engage` rises while the user speaks and relaxes
|
|
406
|
-
* after long silence. The mixer spends it on torsoLean — forward lean is
|
|
407
|
-
* the highest-value listening channel the rig has (§6.3).
|
|
408
|
-
*
|
|
409
|
-
* If the host never supplies any user signal, the old loose random timer runs
|
|
410
|
-
* instead — a worse listener, but never a dead one.
|
|
389
|
+
* `setUserSpeaking()` receives Pipecat's VAD verdict through the client
|
|
390
|
+
* adapter. It may alter the sustained engagement lean, but it never chooses or
|
|
391
|
+
* fires a clip: a nod, brow acknowledgement or any other facial claim of
|
|
392
|
+
* understanding is an explicit backend/application `action()` decision.
|
|
411
393
|
*/
|
|
412
394
|
export class ListeningEngine {
|
|
413
|
-
constructor(
|
|
414
|
-
this.fire = fire;
|
|
395
|
+
constructor() {
|
|
415
396
|
this.enabled = false;
|
|
416
397
|
this.t = 0;
|
|
417
398
|
this.engage = 0;
|
|
418
|
-
this.lastFireAt = -1e9;
|
|
419
|
-
// --- no-signal fallback timer (the pre-contingency behaviour, verbatim)
|
|
420
|
-
this.minGap = 3.4;
|
|
421
|
-
this.maxGap = 8.0;
|
|
422
|
-
this._next = 0;
|
|
423
|
-
// --- user-signal state
|
|
424
399
|
this._hasSignal = false;
|
|
425
|
-
this._explicit = null;
|
|
426
|
-
this._speaking = false;
|
|
427
|
-
this.
|
|
428
|
-
this._silentAt = 0; // end of the last one
|
|
429
|
-
this._pending = -1; // scheduled contingent fire time, <0 = none
|
|
430
|
-
this._midNext = 0;
|
|
400
|
+
this._explicit = null;
|
|
401
|
+
this._speaking = false;
|
|
402
|
+
this._silentAt = 0;
|
|
431
403
|
}
|
|
432
404
|
|
|
433
|
-
/**
|
|
434
|
-
*
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
405
|
+
/** Compatibility no-ops retained for existing hosts that reached these
|
|
406
|
+
* internals indirectly. With no autonomous clips there is nothing to reset
|
|
407
|
+
* or cede. */
|
|
408
|
+
cede() {}
|
|
409
|
+
get ceded() { return false; }
|
|
410
|
+
reset() {}
|
|
440
411
|
|
|
441
|
-
/**
|
|
442
|
-
* to hand back to the no-signal timer. */
|
|
412
|
+
/** Pipecat VAD's user-speaking truth. `null` removes the engagement signal. */
|
|
443
413
|
setUserSpeaking(b) {
|
|
444
414
|
if (b !== null) this._hasSignal = true;
|
|
445
415
|
this._explicit = b === null ? null : !!b;
|
|
446
416
|
}
|
|
447
417
|
|
|
448
|
-
/** The one seam for choosing an acknowledgement. Context-aware: what the
|
|
449
|
-
* user just did decides the weight class of the reply
|
|
450
|
-
* (docs/research-biomechanics.md §3.3 — continuers co-occur with ongoing
|
|
451
|
-
* speech, assessments with completed content; corpus mix 49/40/12,
|
|
452
|
-
* shifted quieter here per the screen-share constraint). */
|
|
453
|
-
pickAck(context) {
|
|
454
|
-
const r = Math.random();
|
|
455
|
-
// Mid-speech nods stay minimal: the user still has the floor.
|
|
456
|
-
if (context === 'midspeech') return r < 0.7 ? 'NOD_SMALL' : 'BROW_ACK';
|
|
457
|
-
// A pause after a LONG stretch earns an assessment-class nod — the user
|
|
458
|
-
// completed a thought, and answering a paragraph with a continuer reads
|
|
459
|
-
// as not having listened to it. NOD_UP is rationed: a realization every
|
|
460
|
-
// few seconds stops meaning realization.
|
|
461
|
-
const utter = this._hasSignal ? this._silentAt - this._spokeAt : 0;
|
|
462
|
-
if (utter >= 4) {
|
|
463
|
-
if (r < 0.45) return 'NOD_SLOW';
|
|
464
|
-
if (r < 0.65) return 'NOD_UP';
|
|
465
|
-
if (r < 0.85) return 'NOD_SMALL';
|
|
466
|
-
return 'BROW_ACK';
|
|
467
|
-
}
|
|
468
|
-
// Short utterance (and the no-signal fallback timer): continuer country.
|
|
469
|
-
return r < 0.55 ? 'NOD_SMALL' : r < 0.8 ? 'BROW_ACK' : 'NOD_SLOW';
|
|
470
|
-
}
|
|
471
|
-
|
|
472
418
|
get speaking() { return this._explicit === true; }
|
|
473
419
|
|
|
474
420
|
update(dt) {
|
|
@@ -480,39 +426,10 @@ export class ListeningEngine {
|
|
|
480
426
|
// relaxing the moment they pause would read as relief that they stopped.
|
|
481
427
|
if (speaking !== this._speaking) {
|
|
482
428
|
this._speaking = speaking;
|
|
483
|
-
if (speaking)
|
|
484
|
-
else {
|
|
485
|
-
this._silentAt = t;
|
|
486
|
-
// Pause onset: the contingent backchannel moment. Half of pauses get
|
|
487
|
-
// an acknowledgement; the other half, keeping still IS the answer.
|
|
488
|
-
if (this.enabled && this._hasSignal
|
|
489
|
-
&& t - this.lastFireAt >= 2.5 && Math.random() < 0.5) {
|
|
490
|
-
this._pending = t + 0.15 + Math.random() * 0.3;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
429
|
+
if (!speaking) this._silentAt = t;
|
|
493
430
|
}
|
|
494
431
|
const engaged = speaking || t - this._silentAt < 8;
|
|
495
432
|
this.engage = approach(this.engage, engaged && this._hasSignal ? 1 : 0,
|
|
496
433
|
speaking ? 1.5 : 6.0, dt);
|
|
497
|
-
|
|
498
|
-
if (!this.enabled) { this._pending = -1; return; }
|
|
499
|
-
|
|
500
|
-
if (this._hasSignal) {
|
|
501
|
-
if (this._pending > 0 && t >= this._pending) {
|
|
502
|
-
this._pending = -1;
|
|
503
|
-
this.lastFireAt = t;
|
|
504
|
-
this.fire(this.pickAck('pause'));
|
|
505
|
-
}
|
|
506
|
-
// A long unbroken stretch of user speech earns a rare mid-speech nod.
|
|
507
|
-
if (speaking && t - this._spokeAt > 5.5 && t - this.lastFireAt > 3.5 && t >= this._midNext) {
|
|
508
|
-
this._midNext = t + 2.6 + Math.random() * 1.8;
|
|
509
|
-
if (Math.random() < 0.35) { this.lastFireAt = t; this.fire(this.pickAck('midspeech')); }
|
|
510
|
-
}
|
|
511
|
-
} else if (t >= this._next) {
|
|
512
|
-
// No user signal was ever supplied: the loose timer, exactly as before.
|
|
513
|
-
this._next = t + this.minGap + Math.random() * (this.maxGap - this.minGap);
|
|
514
|
-
this.lastFireAt = t;
|
|
515
|
-
this.fire(this.pickAck('pause'));
|
|
516
|
-
}
|
|
517
434
|
}
|
|
518
435
|
}
|