@voqalize/avatar 0.2.2 → 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
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* `apps/authoring/rig-check.html` 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
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export const EMOTIONS = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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: {},
|
|
14
15
|
warm: {
|
|
15
16
|
mouthCornerL: 0.48, mouthCornerR: 0.48,
|
|
16
17
|
// A real smile squints. Without this it reads as a mask.
|
package/src/face-core.js
CHANGED
|
@@ -65,6 +65,13 @@ export function faceApi(mount, svg, apply, theme) {
|
|
|
65
65
|
* shrugPivot {x, y} the sternum
|
|
66
66
|
* yawPx px of head travel at headYaw = 1
|
|
67
67
|
* pitchPx px at headPitch = 1
|
|
68
|
+
* pitch optional corrective-pose contract for rigs that can nod:
|
|
69
|
+
* `{ headLayers, neckLayer, hinge, headTravel, neckTravel,
|
|
70
|
+
* foreshorten, neckCompress, neckBase }`.
|
|
71
|
+
* `headLayers` must be separate SVG groups for the skull and
|
|
72
|
+
* its attached surface layers; `neckLayer` must be behind it.
|
|
73
|
+
* This is intentionally a small group-level contract, not a
|
|
74
|
+
* second set of authored paths per expression.
|
|
68
75
|
* pivot {x, y} roll pivot — the base of the neck, not the chin:
|
|
69
76
|
* rotating about the chin swings the whole cranium sideways
|
|
70
77
|
* and reads as a puppet on a stick
|
|
@@ -128,17 +135,36 @@ export function poseTransforms(p, set, el, spec) {
|
|
|
128
135
|
|
|
129
136
|
const yx = p.headYaw * spec.yawPx;
|
|
130
137
|
const py = p.headPitch * spec.pitchPx;
|
|
138
|
+
const pitch = spec.pitch;
|
|
131
139
|
for (const key of spec.layers) {
|
|
132
140
|
const k = spec.parallax[key];
|
|
133
141
|
const torso = spec.torsoLayers.includes(key);
|
|
134
142
|
const roll = torso ? p.headRoll * ROLL_TORSO : p.headRoll * ROLL_HEAD;
|
|
143
|
+
const isPitchHead = !!pitch && pitch.headLayers.includes(key);
|
|
144
|
+
const isPitchNeck = !!pitch && pitch.neckLayer === key;
|
|
145
|
+
// A pitch-capable rig makes its skull, features and hair move together:
|
|
146
|
+
// their old parallax values still apply to yaw, but not to the vertical
|
|
147
|
+
// nod. That coherence is the point of separating them from the neck.
|
|
148
|
+
const pitchTravel = isPitchHead ? pitch.headTravel
|
|
149
|
+
: isPitchNeck ? pitch.neckTravel : k;
|
|
150
|
+
const pitchScale = isPitchHead
|
|
151
|
+
? 1 - Math.max(0, p.headPitch) * pitch.foreshorten : 1;
|
|
152
|
+
const neckScale = isPitchNeck
|
|
153
|
+
? 1 - Math.max(0, p.headPitch) * pitch.neckCompress : 1;
|
|
154
|
+
const pitchT = pitchScale !== 1
|
|
155
|
+
? `translate(${f(pitch.hinge.x)} ${f(pitch.hinge.y)}) scale(1 ${f(pitchScale)}) translate(${f(-pitch.hinge.x)} ${f(-pitch.hinge.y)}) `
|
|
156
|
+
: '';
|
|
157
|
+
const neckT = neckScale !== 1
|
|
158
|
+
? `translate(${f(pitch.neckBase.x)} ${f(pitch.neckBase.y)}) scale(1 ${f(neckScale)}) translate(${f(-pitch.neckBase.x)} ${f(-pitch.neckBase.y)}) `
|
|
159
|
+
: '';
|
|
135
160
|
// Torso layers get their breath from the swell inside torsoT; head layers
|
|
136
161
|
// get the matching lift, which is derived from the swell rather than tuned
|
|
137
162
|
// separately — the neck rides on the chest it sits on.
|
|
138
163
|
const bob = torso ? 0 : -neckLift;
|
|
139
164
|
set(el[key], 'transform',
|
|
140
165
|
leanT + (torso ? torsoT : '')
|
|
141
|
-
+
|
|
166
|
+
+ pitchT + neckT
|
|
167
|
+
+ `translate(${f(yx * k)} ${f(py * pitchTravel + bob)}) rotate(${f(roll)} ${f(spec.pivot.x)} ${f(spec.pivot.y)})`);
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AvatarMeta, Face, FaceFactory } from "./avatar.js";
|
|
2
|
+
|
|
3
|
+
export const createFace: FaceFactory;
|
|
4
|
+
export const META: AvatarMeta;
|
|
5
|
+
/** Default palette. Passed as `createAvatar({ theme })`; see authoring-a-face.md. */
|
|
6
|
+
export const THEME: Readonly<Record<string, string>>;
|
|
7
|
+
export const myna: Face;
|