@voqalize/avatar 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +65 -154
  2. package/dist/index.d.ts +37 -26
  3. package/dist/index.js +48 -25
  4. package/dist/pin.d.ts +2 -0
  5. package/dist/pin.js +3 -0
  6. package/dist/react.d.ts +21 -6
  7. package/dist/react.js +24 -6
  8. package/package.json +8 -67
  9. package/LICENSE-APACHE-2.0 +0 -201
  10. package/LICENSE-CC-BY-4.0 +0 -396
  11. package/assets/README.md +0 -60
  12. package/assets/tanvi.glb +0 -0
  13. package/assets/tanya.glb +0 -0
  14. package/assets/tara.glb +0 -0
  15. package/assets/tess.glb +0 -0
  16. package/assets/tushar.glb +0 -0
  17. package/client/Avatar.tsx +0 -57
  18. package/client/AvatarClient.ts +0 -818
  19. package/client/createAvatar.ts +0 -177
  20. package/client/index.ts +0 -44
  21. package/client/internal.ts +0 -132
  22. package/client/playout.ts +0 -95
  23. package/client/react.ts +0 -13
  24. package/client/supports.ts +0 -27
  25. package/client/three/assets.ts +0 -26
  26. package/client/three/budgets.ts +0 -27
  27. package/client/three/character-rig.ts +0 -1746
  28. package/client/three/createCharacter.ts +0 -109
  29. package/client/three/holds.ts +0 -69
  30. package/client/three/internal.ts +0 -55
  31. package/client/three/motion-limits.json +0 -124
  32. package/client/three/sequences.ts +0 -281
  33. package/client/three/tanvi-asset.ts +0 -6
  34. package/client/three/tanvi.ts +0 -30
  35. package/client/three/tanya-asset.ts +0 -6
  36. package/client/three/tanya.ts +0 -29
  37. package/client/three/tara-asset.ts +0 -25
  38. package/client/three/tara.ts +0 -36
  39. package/client/three/tess-asset.ts +0 -6
  40. package/client/three/tess.ts +0 -30
  41. package/client/three/tushar-asset.ts +0 -6
  42. package/client/three/tushar.ts +0 -28
  43. package/client/types.ts +0 -235
  44. package/client/useAvatar.ts +0 -74
  45. package/dist/Avatar.d.ts +0 -44
  46. package/dist/Avatar.d.ts.map +0 -1
  47. package/dist/Avatar.js +0 -7
  48. package/dist/Avatar.js.map +0 -1
  49. package/dist/AvatarClient.d.ts +0 -274
  50. package/dist/AvatarClient.d.ts.map +0 -1
  51. package/dist/AvatarClient.js +0 -712
  52. package/dist/AvatarClient.js.map +0 -1
  53. package/dist/createAvatar.d.ts +0 -135
  54. package/dist/createAvatar.d.ts.map +0 -1
  55. package/dist/createAvatar.js +0 -75
  56. package/dist/createAvatar.js.map +0 -1
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/internal.d.ts +0 -34
  60. package/dist/internal.d.ts.map +0 -1
  61. package/dist/internal.js +0 -56
  62. package/dist/internal.js.map +0 -1
  63. package/dist/playout.d.ts +0 -42
  64. package/dist/playout.d.ts.map +0 -1
  65. package/dist/playout.js +0 -85
  66. package/dist/playout.js.map +0 -1
  67. package/dist/react.d.ts.map +0 -1
  68. package/dist/react.js.map +0 -1
  69. package/dist/supports.d.ts +0 -19
  70. package/dist/supports.d.ts.map +0 -1
  71. package/dist/supports.js +0 -25
  72. package/dist/supports.js.map +0 -1
  73. package/dist/three/assets.d.ts +0 -10
  74. package/dist/three/assets.d.ts.map +0 -1
  75. package/dist/three/assets.js +0 -23
  76. package/dist/three/assets.js.map +0 -1
  77. package/dist/three/budgets.d.ts +0 -17
  78. package/dist/three/budgets.d.ts.map +0 -1
  79. package/dist/three/budgets.js +0 -21
  80. package/dist/three/budgets.js.map +0 -1
  81. package/dist/three/character-rig.d.ts +0 -366
  82. package/dist/three/character-rig.d.ts.map +0 -1
  83. package/dist/three/character-rig.js +0 -1644
  84. package/dist/three/character-rig.js.map +0 -1
  85. package/dist/three/createCharacter.d.ts +0 -60
  86. package/dist/three/createCharacter.d.ts.map +0 -1
  87. package/dist/three/createCharacter.js +0 -84
  88. package/dist/three/createCharacter.js.map +0 -1
  89. package/dist/three/holds.d.ts +0 -38
  90. package/dist/three/holds.d.ts.map +0 -1
  91. package/dist/three/holds.js +0 -61
  92. package/dist/three/holds.js.map +0 -1
  93. package/dist/three/internal.d.ts +0 -52
  94. package/dist/three/internal.d.ts.map +0 -1
  95. package/dist/three/internal.js +0 -49
  96. package/dist/three/internal.js.map +0 -1
  97. package/dist/three/motion-limits.json +0 -124
  98. package/dist/three/sequences.d.ts +0 -71
  99. package/dist/three/sequences.d.ts.map +0 -1
  100. package/dist/three/sequences.js +0 -262
  101. package/dist/three/sequences.js.map +0 -1
  102. package/dist/three/tanvi-asset.d.ts +0 -7
  103. package/dist/three/tanvi-asset.d.ts.map +0 -1
  104. package/dist/three/tanvi-asset.js +0 -7
  105. package/dist/three/tanvi-asset.js.map +0 -1
  106. package/dist/three/tanvi.d.ts +0 -24
  107. package/dist/three/tanvi.d.ts.map +0 -1
  108. package/dist/three/tanvi.js +0 -24
  109. package/dist/three/tanvi.js.map +0 -1
  110. package/dist/three/tanya-asset.d.ts +0 -7
  111. package/dist/three/tanya-asset.d.ts.map +0 -1
  112. package/dist/three/tanya-asset.js +0 -7
  113. package/dist/three/tanya-asset.js.map +0 -1
  114. package/dist/three/tanya.d.ts +0 -23
  115. package/dist/three/tanya.d.ts.map +0 -1
  116. package/dist/three/tanya.js +0 -23
  117. package/dist/three/tanya.js.map +0 -1
  118. package/dist/three/tara-asset.d.ts +0 -26
  119. package/dist/three/tara-asset.d.ts.map +0 -1
  120. package/dist/three/tara-asset.js +0 -26
  121. package/dist/three/tara-asset.js.map +0 -1
  122. package/dist/three/tara.d.ts +0 -30
  123. package/dist/three/tara.d.ts.map +0 -1
  124. package/dist/three/tara.js +0 -30
  125. package/dist/three/tara.js.map +0 -1
  126. package/dist/three/tess-asset.d.ts +0 -7
  127. package/dist/three/tess-asset.d.ts.map +0 -1
  128. package/dist/three/tess-asset.js +0 -7
  129. package/dist/three/tess-asset.js.map +0 -1
  130. package/dist/three/tess.d.ts +0 -24
  131. package/dist/three/tess.d.ts.map +0 -1
  132. package/dist/three/tess.js +0 -24
  133. package/dist/three/tess.js.map +0 -1
  134. package/dist/three/tushar-asset.d.ts +0 -7
  135. package/dist/three/tushar-asset.d.ts.map +0 -1
  136. package/dist/three/tushar-asset.js +0 -7
  137. package/dist/three/tushar-asset.js.map +0 -1
  138. package/dist/three/tushar.d.ts +0 -22
  139. package/dist/three/tushar.d.ts.map +0 -1
  140. package/dist/three/tushar.js +0 -22
  141. package/dist/three/tushar.js.map +0 -1
  142. package/dist/types.d.ts +0 -149
  143. package/dist/types.d.ts.map +0 -1
  144. package/dist/types.js +0 -140
  145. package/dist/types.js.map +0 -1
  146. package/dist/useAvatar.d.ts +0 -39
  147. package/dist/useAvatar.d.ts.map +0 -1
  148. package/dist/useAvatar.js +0 -35
  149. package/dist/useAvatar.js.map +0 -1
  150. package/src/avatar.d.ts +0 -436
  151. package/src/avatar.js +0 -1378
  152. package/src/behavior.d.ts +0 -42
  153. package/src/behavior.js +0 -114
  154. package/src/camera.js +0 -29
  155. package/src/clips.js +0 -155
  156. package/src/conformance.js +0 -119
  157. package/src/emotions.js +0 -58
  158. package/src/face-core.js +0 -180
  159. package/src/face-myna.d.ts +0 -7
  160. package/src/face-myna.js +0 -786
  161. package/src/face-peep-control-plane.js +0 -167
  162. package/src/face-peep.d.ts +0 -7
  163. package/src/face-peep.js +0 -925
  164. package/src/face-wren.d.ts +0 -7
  165. package/src/face-wren.js +0 -468
  166. package/src/faces.d.ts +0 -12
  167. package/src/faces.js +0 -53
  168. package/src/gaze.js +0 -504
  169. package/src/hand.js +0 -709
  170. package/src/head.js +0 -134
  171. package/src/idle.js +0 -668
  172. package/src/interjections.js +0 -747
  173. package/src/line-art.js +0 -137
  174. package/src/params.js +0 -180
  175. package/src/perform.js +0 -107
  176. package/src/prosody.js +0 -741
  177. package/src/rig.d.ts +0 -31
  178. package/src/rig.js +0 -32
  179. package/src/speech-timing.js +0 -51
  180. package/src/visemes.js +0 -410
package/src/behavior.d.ts DELETED
@@ -1,42 +0,0 @@
1
- import type { AvatarStateName } from "./avatar.js";
2
-
3
- /** The two renderer calls behavior resolution actually needs. */
4
- export interface BehaviorAvatar {
5
- setState(name: AvatarStateName): unknown;
6
- /**
7
- * One motion by name. The vocabulary is open — two core intents plus
8
- * whatever this avatar publishes — so a name it does not have must be a
9
- * no-op rather than a throw.
10
- */
11
- action(id: string): unknown;
12
- }
13
-
14
- /**
15
- * The whole behaviour vocabulary — the nine. Not an alias list; see behavior.js.
16
- *
17
- * Every one of these is also an `AvatarStateName`, which is why there is no
18
- * mapping table any more.
19
- */
20
- export type BehaviorStateId =
21
- | "IDLE" | "LISTENING" | "CANT_HEAR" | "THINKING" | "WORKING"
22
- | "MUTED" | "SPEAKING" | "DEGRADED" | "OFFLINE";
23
-
24
- export type BehaviorActionId = "ack" | "turn.interrupted";
25
-
26
- export const BEHAVIOR_STATE_IDS: readonly BehaviorStateId[];
27
- export const BEHAVIOR_ACTIONS: Readonly<Record<BehaviorActionId, { renderAction: string }>>;
28
- export const BEHAVIOR_ACTION_IDS: readonly BehaviorActionId[];
29
- export const WIRE_ACTION_TO_BEHAVIOR: Readonly<Record<string, BehaviorActionId>>;
30
-
31
- export class BehaviorController {
32
- constructor(avatar: BehaviorAvatar);
33
- state: BehaviorStateId | null;
34
- setState(id: BehaviorStateId, options?: { force?: boolean }): this;
35
- action(id: BehaviorActionId): this;
36
- /**
37
- * One id off the wire: a core intent through the catalog, anything else
38
- * straight to the renderer. Never throws — the vocabulary is open.
39
- */
40
- wireAction(id: string): this;
41
- destroy(): void;
42
- }
package/src/behavior.js DELETED
@@ -1,114 +0,0 @@
1
- /**
2
- * Behavior catalog and controller.
3
- *
4
- * This is the layer between factual/wire inputs and the bundled SVG renderer.
5
- * It is not a seam any avatar implementation sees: an implementation receives
6
- * a PipecatClient and decides for itself what a state means.
7
- *
8
- * The vocabulary is the nine core states and two core actions. The states used
9
- * to be seventeen — the core
10
- * set plus every SVG render state (TYPING, SEARCHING_SCREEN, WANTS_IN, …)
11
- * passed straight through, so that tooling could drive one of *those* from
12
- * here. That made the mixer's private state list look like part of the
13
- * behaviour vocabulary. Tooling that wants a render state calls
14
- * `avatar.setState` on the mixer, which is whose state it is.
15
- */
16
-
17
- /**
18
- * The durable states this layer resolves. Each one *is* the render state it
19
- * asks the bundled SVG mixer for, so there is nothing here to map.
20
- *
21
- * There used to be a second column — a per-state `renderState`, so a renderer
22
- * could draw `WORKING` as anything it liked and the divergence would land here
23
- * rather than leak. In nine rows it had exactly one non-identity entry,
24
- * `STRAINING -> CANT_HEAR`, and the wire redesign deleted that entry by
25
- * renaming the state: a state names the bot's *situation*, never how it looks,
26
- * and "straining" was a situation named after one drawing of it. A table whose
27
- * every row reads `X: X` documents nothing and invites a renderer to plug into
28
- * it, so it is a list now. A renderer that genuinely wants to draw one of these
29
- * as something else calls `avatar.setState` with the render state it wants,
30
- * which is whose state that is.
31
- */
32
- export const BEHAVIOR_STATE_IDS = Object.freeze([
33
- 'IDLE', 'LISTENING', 'CANT_HEAR', 'THINKING', 'WORKING',
34
- 'MUTED', 'SPEAKING', 'DEGRADED', 'OFFLINE',
35
- ]);
36
-
37
- const STATE_IDS = new Set(BEHAVIOR_STATE_IDS);
38
-
39
- /**
40
- * Library action IDs: the two intents every renderer owes a server, in this
41
- * layer's readable spelling.
42
- *
43
- * It used to be seven, with a `sequence` command alongside for a renderer's own
44
- * motions. The seven were not the wrong *names* so much as the wrong idea: four
45
- * of them were `GESTURE_*` — a greet, a goodbye, a wave — which are things a
46
- * particular body does and not intents a server can hold every face to, and two
47
- * were the receipt and the nod, which are two *shapes* of acknowledging. So the
48
- * server now says only that an acknowledgement is due, the avatar picks which
49
- * one it makes, and the gestures live in whichever catalogue can draw them,
50
- * reachable by the same open `action` id a sequence used to need its own
51
- * command for.
52
- */
53
- export const BEHAVIOR_ACTIONS = Object.freeze({
54
- ack: { renderAction: 'ACKNOWLEDGE' },
55
- 'turn.interrupted': { renderAction: 'RESPONSE_INTERRUPTED' },
56
- });
57
-
58
- export const BEHAVIOR_ACTION_IDS = Object.freeze(Object.keys(BEHAVIOR_ACTIONS));
59
-
60
- /** The two core wire names map into the behavior catalog here and nowhere else. */
61
- export const WIRE_ACTION_TO_BEHAVIOR = Object.freeze({
62
- ACKNOWLEDGE: 'ack',
63
- RESPONSE_INTERRUPTED: 'turn.interrupted',
64
- });
65
-
66
- /**
67
- * Turns behavior intent into the legacy SVG mixer's calls. A future renderer
68
- * receives the same intent through its own adapter; it does not need to know
69
- * the legacy state/action names used below.
70
- */
71
- export class BehaviorController {
72
- constructor(avatar) {
73
- if (!avatar || typeof avatar.setState !== 'function' || typeof avatar.action !== 'function') {
74
- throw new TypeError('BehaviorController requires an avatar implementation');
75
- }
76
- this.avatar = avatar;
77
- this.state = null;
78
- }
79
-
80
- setState(id, { force = false } = {}) {
81
- if (!STATE_IDS.has(id)) throw new Error(`unknown behavior state: ${id}`);
82
- if (!force && this.state === id) return this;
83
- this.state = id;
84
- this.avatar.setState(id);
85
- return this;
86
- }
87
-
88
- destroy() {}
89
-
90
- action(id) {
91
- const def = BEHAVIOR_ACTIONS[id];
92
- if (!def) throw new Error(`unknown behavior action: ${id}`);
93
- this.avatar.action(def.renderAction);
94
- return this;
95
- }
96
-
97
- /**
98
- * One id off the wire. A core one goes through the behavior catalog; any
99
- * other goes straight to the renderer, and that asymmetry is the point.
100
- * `BEHAVIOR_ACTIONS` exists so a portable intent has a readable name
101
- * independent of how any one face renders it. An avatar's own id has no such
102
- * independence — it *is* the rendering, named by the renderer that owns it —
103
- * so giving it a behavior alias would claim a portability it does not have.
104
- *
105
- * Neither path throws. The wire's action vocabulary is open and an id this
106
- * face cannot draw is the expected case, not somebody's broken build.
107
- */
108
- wireAction(id) {
109
- const behaviorId = WIRE_ACTION_TO_BEHAVIOR[id];
110
- if (behaviorId) return this.action(behaviorId);
111
- this.avatar.action(id);
112
- return this;
113
- }
114
- }
package/src/camera.js DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * The close webcam crop shared by every avatar renderer.
3
- *
4
- * The head gets most of the tile because lip sync and listening expressions
5
- * are the information the avatar is there to carry. The remaining lower band
6
- * leaves enough neck and shoulder for posture to read without turning the
7
- * call tile into a half-body portrait.
8
- */
9
- export const CALL_CAMERA = Object.freeze({
10
- aspect: 4 / 3,
11
- headroom: 0.06,
12
- head: 0.70,
13
- body: 0.24,
14
- });
15
-
16
- /** Derive a camera rectangle from three landmarks in the drawing's own units. */
17
- export function viewBoxForHead({ centerX, crownY, chinY }) {
18
- if (![centerX, crownY, chinY].every(Number.isFinite) || chinY <= crownY) {
19
- throw new TypeError('viewBoxForHead: expected a finite centre and crown above chin');
20
- }
21
- const h = (chinY - crownY) / CALL_CAMERA.head;
22
- const w = h * CALL_CAMERA.aspect;
23
- return {
24
- x: centerX - w / 2,
25
- y: crownY - h * CALL_CAMERA.headroom,
26
- w,
27
- h,
28
- };
29
- }
package/src/clips.js DELETED
@@ -1,155 +0,0 @@
1
- /**
2
- * Gesture clip player.
3
- *
4
- * A clip is a short, multi-channel keyframed timeline. Clip channels are
5
- * *additive deltas*, not absolute poses — a nod adds pitch to whatever the head
6
- * is already doing, which is what makes "nod while speaking" work without any
7
- * special-casing.
8
- *
9
- * A server action is a physical movement, not an abstract state transition.
10
- * Queued actions finish their landing before the next action begins; their
11
- * underlying state may change freely while that happens. `stop()` remains for
12
- * real speech taking mouth ownership.
13
- */
14
-
15
- import { clamp } from './params.js';
16
- import { INTERJECTION_TRACK_TAIL_MS } from './speech-timing.js';
17
- import { VisemeTrack } from './visemes.js';
18
-
19
- const smoothstep = (t) => t * t * (3 - 2 * t);
20
-
21
- function sampleTrack(keys, u) {
22
- if (!keys.length) return 0;
23
- if (u <= keys[0][0]) return keys[0][1];
24
- const last = keys[keys.length - 1];
25
- if (u >= last[0]) return last[1];
26
- for (let i = 1; i < keys.length; i++) {
27
- if (u <= keys[i][0]) {
28
- const [t0, v0] = keys[i - 1];
29
- const [t1, v1] = keys[i];
30
- const k = t1 === t0 ? 1 : smoothstep((u - t0) / (t1 - t0));
31
- return v0 + (v1 - v0) * k;
32
- }
33
- }
34
- return last[1];
35
- }
36
-
37
- const RAMP_IN = 70;
38
- const RAMP_OUT = 150;
39
-
40
- export class ClipPlayer {
41
- /**
42
- * @param {object} hooks
43
- * @param {(name:string|null)=>void} hooks.onGaze clip-scoped gaze override
44
- * @param {()=>void} hooks.onBlink
45
- */
46
- constructor(hooks = {}) {
47
- this.hooks = hooks;
48
- this.clip = null;
49
- this.t = 0;
50
- this.fading = false;
51
- this.fadeT = 0;
52
- this.mouth = new VisemeTrack();
53
- this._blinksDone = 0;
54
- this.onEnd = null;
55
- this.queue = [];
56
- }
57
-
58
- get playing() { return !!this.clip; }
59
- get id() { return this.clip ? this.clip.id : null; }
60
-
61
- /**
62
- * @param {object} clip
63
- * @param {HTMLAudioElement} [audio] if given, the clip's mouth track is
64
- * scheduled against the audio clock instead of the local timer
65
- */
66
- play(clip, audio, { queue = false } = {}) {
67
- if (!clip) return;
68
- if (this.clip && queue) {
69
- if (this.clip.id === clip.id || this.queue.some((item) => item.clip.id === clip.id)) return;
70
- this.queue.push({ clip, audio });
71
- return;
72
- }
73
- this._start(clip, audio);
74
- }
75
-
76
- _start(clip, audio) {
77
- this.clip = clip;
78
- this.t = 0;
79
- this.fading = false;
80
- this.fadeT = 0;
81
- this._blinksDone = 0;
82
- this.audio = audio || null;
83
-
84
- if (clip.mouthCues && clip.mouthCues.length) {
85
- const clock = audio
86
- ? () => audio.currentTime * 1000
87
- : () => this.t;
88
- this.mouth.tailMs = INTERJECTION_TRACK_TAIL_MS;
89
- this.mouth.start(clip.mouthCues, clock);
90
- } else {
91
- this.mouth.stop();
92
- }
93
- if (clip.gaze && this.hooks.onGaze) this.hooks.onGaze(clip.gaze);
94
- if (audio) { audio.currentTime = 0; audio.play().catch(() => {}); }
95
- }
96
-
97
- /** Barge-in. Fades rather than cutting, so the head doesn't snap. */
98
- stop(immediate = false) {
99
- if (!this.clip) return;
100
- if (immediate) return this._end();
101
- this.fading = true;
102
- this.fadeT = 0;
103
- }
104
-
105
- _end() {
106
- const had = this.clip;
107
- this.clip = null;
108
- this.fading = false;
109
- this.mouth.stop();
110
- if (this.audio) { this.audio.pause(); this.audio = null; }
111
- if (had && had.gaze && this.hooks.onGaze) this.hooks.onGaze(null);
112
- if (this.onEnd) this.onEnd(had);
113
- const next = this.queue.shift();
114
- if (next) this._start(next.clip, next.audio);
115
- }
116
-
117
- /**
118
- * @returns {{delta: object, weight: number, mouth: object|null, ownsMouth: boolean}}
119
- */
120
- update(dtMs) {
121
- if (!this.clip) return { delta: null, weight: 0, mouth: null, ownsMouth: false };
122
- this.t += dtMs;
123
- const c = this.clip;
124
- const u = clamp(this.t / c.duration);
125
-
126
- let w = Math.min(1, this.t / RAMP_IN) * Math.min(1, (c.duration - this.t) / RAMP_OUT);
127
- if (this.fading) {
128
- this.fadeT += dtMs;
129
- w *= Math.max(0, 1 - this.fadeT / 110);
130
- if (this.fadeT >= 110) { this._end(); return { delta: null, weight: 0, mouth: null, ownsMouth: false }; }
131
- }
132
- w = clamp(w);
133
-
134
- if (c.blinkAt && this.hooks.onBlink) {
135
- while (this._blinksDone < c.blinkAt.length && u >= c.blinkAt[this._blinksDone]) {
136
- this._blinksDone++;
137
- this.hooks.onBlink();
138
- }
139
- }
140
-
141
- const delta = {};
142
- for (const ch in c.keys) delta[ch] = sampleTrack(c.keys[ch], u) * w;
143
-
144
- let mouth = null;
145
- const ownsMouth = !!(c.mouthCues && c.mouthCues.length);
146
- if (ownsMouth) mouth = this.mouth.sample();
147
-
148
- if (this.t >= c.duration && !this.fading) {
149
- // Let the mouth track finish its tail before tearing down.
150
- if (!ownsMouth || !this.mouth.playing) this._end();
151
- }
152
-
153
- return { delta, weight: w, mouth, ownsMouth };
154
- }
155
- }
@@ -1,119 +0,0 @@
1
- /**
2
- * The rig conformance sweep — the assertions, with no page around them.
3
- *
4
- * Drives every state, emotion, gaze and semantic action through every rig you
5
- * hand it, then a viseme track, and asserts the mixer keeps producing finite,
6
- * in-range parameters and a still-attached drawing. A NaN anywhere in the chain
7
- * shows up as a *frozen face*, which is exactly the defect the eye is worst at
8
- * catching: a still avatar looks calm, not broken.
9
- *
10
- * This is the numeric half of judging a rig, and it is the smaller half —
11
- * passing here says a change did not break the mixer, never that it looks good
12
- * (CLAUDE.md § Verifying). It is also what a face author runs against a new
13
- * drawing before anyone is asked to look at it.
14
- *
15
- * `advance` is the seam that lets one copy of the sweep serve both callers:
16
- * a rig page in the working tree lets real time pass and watches it happen, while
17
- * `packages/avatar/test/conformance.test.ts` steps `{manual: true}` avatars by a fixed
18
- * dt and finishes in milliseconds. The sweep's own clock is handed to `speak`
19
- * so the mouth articulates under both — a stepped avatar reading
20
- * `performance.now()` would sit on the first cue for the whole track.
21
- */
22
- import {
23
- STATE_NAMES,
24
- EMOTION_NAMES,
25
- GAZE_NAMES,
26
- ACTION_IDS,
27
- checkHandFraming,
28
- } from './avatar.js';
29
- import { textToCues } from './visemes.js';
30
-
31
- /** Parameters live in −1..1 by contract; 2 is the slack a gain may legally add. */
32
- const PARAM_LIMIT = 2;
33
-
34
- const realTime = (s) => new Promise((r) => setTimeout(r, s * 1000));
35
-
36
- /**
37
- * @param {Array<{name: string, avatar: object}>} rigs one entry per drawing
38
- * @param {{advance?: (seconds: number) => void|Promise<void>}} [opts]
39
- * @returns {Promise<{ok: boolean, problems: string[], summary: string}>}
40
- */
41
- export async function conformanceSweep(rigs, { advance = realTime } = {}) {
42
- const problems = [];
43
- let ms = 0;
44
- const clock = () => ms;
45
- const tick = async (seconds) => { ms += seconds * 1000; await advance(seconds); };
46
- const all = (fn) => rigs.forEach((r) => fn(r.avatar, r.name));
47
-
48
- function check(label) {
49
- for (const r of rigs) {
50
- const p = r.avatar.params;
51
- for (const k in p) {
52
- if (!Number.isFinite(p[k])) problems.push(`${r.name} ${label}: ${k}=${p[k]}`);
53
- else if (Math.abs(p[k]) > PARAM_LIMIT) {
54
- problems.push(`${r.name} ${label}: ${k} out of range (${p[k].toFixed(2)})`);
55
- }
56
- }
57
- // A renderer-neutral rig has no svg to detach; the SVG one must stay put.
58
- if (r.avatar.svg && !r.avatar.svg.isConnected) problems.push(`${r.name} ${label}: svg detached`);
59
- }
60
- }
61
-
62
- for (const s of STATE_NAMES) { all((k) => k.setState(s)); await tick(0.09); check(`state ${s}`); }
63
- for (const e of EMOTION_NAMES) { all((k) => k.setEmotion(e, 1)); await tick(0.09); check(`emotion ${e}`); }
64
- for (const g of GAZE_NAMES) { all((k) => k.setGaze(g)); await tick(0.09); check(`gaze ${g}`); }
65
- for (const i of ACTION_IDS) { all((k) => k.action(i)); await tick(0.18); check(`action ${i}`); }
66
-
67
- // The hand. Two things are being proved and only one of them is the mixer's.
68
- // Playing each gesture above checked the usual invariants — its face half runs
69
- // through the clip layer like any other interjection. checkHandFraming is the
70
- // one that matters per AVATAR: the hand places itself off META.viewBox, so a
71
- // rig with a different window could push ink through the camera edge or lift
72
- // the wrist into frame, and neither is visible in a resting still.
73
- for (const r of rigs) {
74
- if (!r.avatar.meta) continue;
75
- try { checkHandFraming(r.avatar.meta); } catch (e) { problems.push(`${r.name} ${e.message}`); }
76
- }
77
- all((k) => k.setState('LISTENING'));
78
- await tick(2); // let the longest gesture leave frame before speech starts
79
-
80
- const cues = textToCues('okay, and what happened next');
81
- all((k) => k.speak({ cues, clock }));
82
- for (let i = 0; i < 12; i++) { await tick(0.12); check('speaking'); }
83
- all((k) => k.stopSpeaking().setState('LISTENING'));
84
- await tick(0.2); check('after speech');
85
-
86
- return {
87
- ok: problems.length === 0,
88
- problems,
89
- summary: `${rigs.length} rigs x ${STATE_NAMES.length} states, ${EMOTION_NAMES.length} emotions, `
90
- + `${GAZE_NAMES.length} gazes, ${ACTION_IDS.length} semantic actions `
91
- + `(framing asserted per avatar), plus a viseme track`,
92
- };
93
- }
94
-
95
- /**
96
- * A deterministic `advance` for `{manual: true}` avatars: fixed-dt stepping, no
97
- * wall clock, no rAF. Same rigs and same seed give the same numbers every run,
98
- * which is what makes a failure here reproducible rather than anecdotal.
99
- */
100
- export function stepper(rigs, dt = 1 / 60) {
101
- return (seconds) => {
102
- for (let t = 0; t < seconds; t += dt) rigs.forEach((r) => r.avatar.step(dt));
103
- };
104
- }
105
-
106
- /**
107
- * Replace `Math.random` with a seeded LCG and return the undo.
108
- *
109
- * The sweep's assertions are invariants — they must hold for any draw — but a
110
- * failure you cannot reproduce is a failure you cannot fix, and idle, gaze
111
- * aversion and blink timing are all random. Call this *before* creating the
112
- * avatars: constructors draw their first phases immediately.
113
- */
114
- export function seedRandom(seed = 1) {
115
- const original = Math.random;
116
- let s = seed >>> 0 || 1;
117
- Math.random = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
118
- return () => { Math.random = original; };
119
- }
package/src/emotions.js DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * Affect is a separate axis from state.
3
- *
4
- * If emotion were folded into the state enum you'd need SPEAKING_WARM,
5
- * SPEAKING_CONCERNED, LISTENING_WARM ... and the table would be unmaintainable
6
- * within a week. Instead: `setState('LISTENING', { emotion: 'curious' })`. The
7
- * emotion contributes a base pose that every other layer builds on top of.
8
- */
9
-
10
- export const EMOTIONS = {
11
- // Deliberately empty: REST is genuine availability, rather than a social
12
- // smile baked into every state. Positive affect belongs to a response,
13
- // greeting, success beat, or explicit warm/encouraging emotion.
14
- neutral: {},
15
- warm: {
16
- mouthCornerL: 0.48, mouthCornerR: 0.48,
17
- // A real smile squints. Without this it reads as a mask. From below —
18
- // the cheek pushes the lower lid up (AU6) — with no upper lid dropped on
19
- // top, which on a photographic eye read as sleepy rather than warm.
20
- squintL: 0.30, squintR: 0.30,
21
- browRaiseL: 0.10, browRaiseR: 0.10,
22
- },
23
- curious: {
24
- browRaiseL: 0.34, browRaiseR: 0.12,
25
- browAngleL: 0.10,
26
- headRoll: 0.07,
27
- mouthCornerL: 0.18, mouthCornerR: 0.14,
28
- lidL: -0.10, lidR: -0.10,
29
- },
30
- concerned: {
31
- browInnerL: 0.55, browInnerR: 0.55,
32
- browRaiseL: -0.08, browRaiseR: -0.08,
33
- mouthCornerL: -0.22, mouthCornerR: -0.22,
34
- lidL: 0.06, lidR: 0.06,
35
- },
36
- encouraging: {
37
- mouthCornerL: 0.58, mouthCornerR: 0.58,
38
- browRaiseL: 0.26, browRaiseR: 0.26,
39
- squintL: 0.22, squintR: 0.22,
40
- headPitch: 0.05,
41
- },
42
- thoughtful: {
43
- browRaiseL: -0.14, browRaiseR: -0.06,
44
- browInnerL: 0.18, browInnerR: 0.10,
45
- mouthPress: 0.45, mouthCornerL: -0.05, mouthCornerR: 0.02,
46
- lidL: 0.10, lidR: 0.10,
47
- },
48
- };
49
-
50
- export const EMOTION_NAMES = Object.keys(EMOTIONS);
51
-
52
- /** Blend an emotion toward neutral by `intensity`. */
53
- export function emotionPose(name, intensity = 1) {
54
- const src = EMOTIONS[name] || EMOTIONS.neutral;
55
- const out = {};
56
- for (const k in src) out[k] = src[k] * intensity;
57
- return out;
58
- }