@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.
- package/README.md +65 -154
- package/dist/index.d.ts +37 -26
- package/dist/index.js +48 -25
- package/dist/pin.d.ts +2 -0
- package/dist/pin.js +3 -0
- package/dist/react.d.ts +21 -6
- package/dist/react.js +24 -6
- package/package.json +8 -67
- package/LICENSE-APACHE-2.0 +0 -201
- package/LICENSE-CC-BY-4.0 +0 -396
- package/assets/README.md +0 -60
- 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/Avatar.tsx +0 -57
- package/client/AvatarClient.ts +0 -818
- package/client/createAvatar.ts +0 -177
- package/client/index.ts +0 -44
- package/client/internal.ts +0 -132
- package/client/playout.ts +0 -95
- package/client/react.ts +0 -13
- package/client/supports.ts +0 -27
- package/client/three/assets.ts +0 -26
- package/client/three/budgets.ts +0 -27
- package/client/three/character-rig.ts +0 -1746
- package/client/three/createCharacter.ts +0 -109
- package/client/three/holds.ts +0 -69
- package/client/three/internal.ts +0 -55
- package/client/three/motion-limits.json +0 -124
- package/client/three/sequences.ts +0 -281
- package/client/three/tanvi-asset.ts +0 -6
- package/client/three/tanvi.ts +0 -30
- package/client/three/tanya-asset.ts +0 -6
- package/client/three/tanya.ts +0 -29
- package/client/three/tara-asset.ts +0 -25
- package/client/three/tara.ts +0 -36
- package/client/three/tess-asset.ts +0 -6
- package/client/three/tess.ts +0 -30
- package/client/three/tushar-asset.ts +0 -6
- package/client/three/tushar.ts +0 -28
- package/client/types.ts +0 -235
- package/client/useAvatar.ts +0 -74
- package/dist/Avatar.d.ts +0 -44
- package/dist/Avatar.d.ts.map +0 -1
- package/dist/Avatar.js +0 -7
- package/dist/Avatar.js.map +0 -1
- package/dist/AvatarClient.d.ts +0 -274
- package/dist/AvatarClient.d.ts.map +0 -1
- package/dist/AvatarClient.js +0 -712
- package/dist/AvatarClient.js.map +0 -1
- package/dist/createAvatar.d.ts +0 -135
- package/dist/createAvatar.d.ts.map +0 -1
- package/dist/createAvatar.js +0 -75
- package/dist/createAvatar.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts +0 -34
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js +0 -56
- package/dist/internal.js.map +0 -1
- package/dist/playout.d.ts +0 -42
- package/dist/playout.d.ts.map +0 -1
- package/dist/playout.js +0 -85
- package/dist/playout.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/supports.d.ts +0 -19
- package/dist/supports.d.ts.map +0 -1
- package/dist/supports.js +0 -25
- package/dist/supports.js.map +0 -1
- package/dist/three/assets.d.ts +0 -10
- package/dist/three/assets.d.ts.map +0 -1
- package/dist/three/assets.js +0 -23
- package/dist/three/assets.js.map +0 -1
- package/dist/three/budgets.d.ts +0 -17
- package/dist/three/budgets.d.ts.map +0 -1
- package/dist/three/budgets.js +0 -21
- package/dist/three/budgets.js.map +0 -1
- package/dist/three/character-rig.d.ts +0 -366
- package/dist/three/character-rig.d.ts.map +0 -1
- package/dist/three/character-rig.js +0 -1644
- package/dist/three/character-rig.js.map +0 -1
- package/dist/three/createCharacter.d.ts +0 -60
- package/dist/three/createCharacter.d.ts.map +0 -1
- package/dist/three/createCharacter.js +0 -84
- package/dist/three/createCharacter.js.map +0 -1
- package/dist/three/holds.d.ts +0 -38
- package/dist/three/holds.d.ts.map +0 -1
- package/dist/three/holds.js +0 -61
- package/dist/three/holds.js.map +0 -1
- package/dist/three/internal.d.ts +0 -52
- package/dist/three/internal.d.ts.map +0 -1
- package/dist/three/internal.js +0 -49
- package/dist/three/internal.js.map +0 -1
- package/dist/three/motion-limits.json +0 -124
- package/dist/three/sequences.d.ts +0 -71
- package/dist/three/sequences.d.ts.map +0 -1
- package/dist/three/sequences.js +0 -262
- package/dist/three/sequences.js.map +0 -1
- package/dist/three/tanvi-asset.d.ts +0 -7
- package/dist/three/tanvi-asset.d.ts.map +0 -1
- package/dist/three/tanvi-asset.js +0 -7
- package/dist/three/tanvi-asset.js.map +0 -1
- package/dist/three/tanvi.d.ts +0 -24
- package/dist/three/tanvi.d.ts.map +0 -1
- package/dist/three/tanvi.js +0 -24
- package/dist/three/tanvi.js.map +0 -1
- package/dist/three/tanya-asset.d.ts +0 -7
- package/dist/three/tanya-asset.d.ts.map +0 -1
- package/dist/three/tanya-asset.js +0 -7
- package/dist/three/tanya-asset.js.map +0 -1
- package/dist/three/tanya.d.ts +0 -23
- package/dist/three/tanya.d.ts.map +0 -1
- package/dist/three/tanya.js +0 -23
- package/dist/three/tanya.js.map +0 -1
- package/dist/three/tara-asset.d.ts +0 -26
- package/dist/three/tara-asset.d.ts.map +0 -1
- package/dist/three/tara-asset.js +0 -26
- package/dist/three/tara-asset.js.map +0 -1
- package/dist/three/tara.d.ts +0 -30
- package/dist/three/tara.d.ts.map +0 -1
- package/dist/three/tara.js +0 -30
- package/dist/three/tara.js.map +0 -1
- package/dist/three/tess-asset.d.ts +0 -7
- package/dist/three/tess-asset.d.ts.map +0 -1
- package/dist/three/tess-asset.js +0 -7
- package/dist/three/tess-asset.js.map +0 -1
- package/dist/three/tess.d.ts +0 -24
- package/dist/three/tess.d.ts.map +0 -1
- package/dist/three/tess.js +0 -24
- package/dist/three/tess.js.map +0 -1
- package/dist/three/tushar-asset.d.ts +0 -7
- package/dist/three/tushar-asset.d.ts.map +0 -1
- package/dist/three/tushar-asset.js +0 -7
- package/dist/three/tushar-asset.js.map +0 -1
- package/dist/three/tushar.d.ts +0 -22
- package/dist/three/tushar.d.ts.map +0 -1
- package/dist/three/tushar.js +0 -22
- package/dist/three/tushar.js.map +0 -1
- package/dist/types.d.ts +0 -149
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -140
- package/dist/types.js.map +0 -1
- package/dist/useAvatar.d.ts +0 -39
- package/dist/useAvatar.d.ts.map +0 -1
- package/dist/useAvatar.js +0 -35
- package/dist/useAvatar.js.map +0 -1
- package/src/avatar.d.ts +0 -436
- package/src/avatar.js +0 -1378
- package/src/behavior.d.ts +0 -42
- package/src/behavior.js +0 -114
- package/src/camera.js +0 -29
- package/src/clips.js +0 -155
- package/src/conformance.js +0 -119
- package/src/emotions.js +0 -58
- package/src/face-core.js +0 -180
- package/src/face-myna.d.ts +0 -7
- package/src/face-myna.js +0 -786
- package/src/face-peep-control-plane.js +0 -167
- package/src/face-peep.d.ts +0 -7
- package/src/face-peep.js +0 -925
- package/src/face-wren.d.ts +0 -7
- package/src/face-wren.js +0 -468
- package/src/faces.d.ts +0 -12
- package/src/faces.js +0 -53
- package/src/gaze.js +0 -504
- package/src/hand.js +0 -709
- package/src/head.js +0 -134
- package/src/idle.js +0 -668
- package/src/interjections.js +0 -747
- package/src/line-art.js +0 -137
- package/src/params.js +0 -180
- package/src/perform.js +0 -107
- package/src/prosody.js +0 -741
- package/src/rig.d.ts +0 -31
- package/src/rig.js +0 -32
- package/src/speech-timing.js +0 -51
- 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
|
-
}
|
package/src/conformance.js
DELETED
|
@@ -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
|
-
}
|