@voqalize/avatar 0.3.0 → 0.4.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-CC-BY-4.0 +396 -0
- package/README.md +65 -17
- package/assets/README.md +48 -0
- package/assets/tanya.glb +0 -0
- package/assets/tara.glb +0 -0
- package/assets/tushar.glb +0 -0
- package/client/AvatarClient.ts +337 -42
- package/client/arjun.ts +2 -0
- package/client/createAvatar.ts +38 -0
- package/client/index.ts +6 -1
- package/client/internal.ts +31 -3
- package/client/ishita.ts +2 -0
- package/client/kabir.ts +2 -0
- package/client/meera.ts +2 -0
- package/client/naina.ts +2 -0
- package/client/playout.ts +95 -0
- package/client/supports.ts +28 -0
- package/client/three/assets.ts +24 -0
- package/client/three/budgets.ts +27 -0
- package/client/three/holds.ts +64 -0
- package/client/three/internal.ts +54 -0
- package/client/three/motion-limits.json +116 -0
- package/client/three/sequences.ts +281 -0
- package/client/three/tanya.ts +69 -0
- package/client/three/tara-rig.ts +1562 -0
- package/client/three/tara.ts +76 -0
- package/client/three/tushar.ts +65 -0
- package/client/types.ts +109 -20
- package/client/vikram.ts +2 -0
- package/dist/AvatarClient.d.ts +89 -11
- package/dist/AvatarClient.d.ts.map +1 -1
- package/dist/AvatarClient.js +297 -35
- package/dist/AvatarClient.js.map +1 -1
- package/dist/arjun.d.ts +1 -0
- package/dist/arjun.d.ts.map +1 -1
- package/dist/arjun.js +2 -0
- package/dist/arjun.js.map +1 -1
- package/dist/createAvatar.d.ts +36 -0
- package/dist/createAvatar.d.ts.map +1 -1
- package/dist/createAvatar.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +9 -5
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +13 -5
- package/dist/internal.js.map +1 -1
- package/dist/ishita.d.ts +1 -0
- package/dist/ishita.d.ts.map +1 -1
- package/dist/ishita.js +2 -0
- package/dist/ishita.js.map +1 -1
- package/dist/kabir.d.ts +1 -0
- package/dist/kabir.d.ts.map +1 -1
- package/dist/kabir.js +2 -0
- package/dist/kabir.js.map +1 -1
- package/dist/meera.d.ts +1 -0
- package/dist/meera.d.ts.map +1 -1
- package/dist/meera.js +2 -0
- package/dist/meera.js.map +1 -1
- package/dist/naina.d.ts +1 -0
- package/dist/naina.d.ts.map +1 -1
- package/dist/naina.js +2 -0
- package/dist/naina.js.map +1 -1
- package/dist/playout.d.ts +42 -0
- package/dist/playout.d.ts.map +1 -0
- package/dist/playout.js +85 -0
- package/dist/playout.js.map +1 -0
- package/dist/supports.d.ts +20 -0
- package/dist/supports.d.ts.map +1 -0
- package/dist/supports.js +26 -0
- package/dist/supports.js.map +1 -0
- package/dist/three/assets.d.ts +24 -0
- package/dist/three/assets.d.ts.map +1 -0
- package/dist/three/assets.js +22 -0
- package/dist/three/assets.js.map +1 -0
- package/dist/three/budgets.d.ts +17 -0
- package/dist/three/budgets.d.ts.map +1 -0
- package/dist/three/budgets.js +21 -0
- package/dist/three/budgets.js.map +1 -0
- package/dist/three/holds.d.ts +33 -0
- package/dist/three/holds.d.ts.map +1 -0
- package/dist/three/holds.js +56 -0
- package/dist/three/holds.js.map +1 -0
- package/dist/three/internal.d.ts +52 -0
- package/dist/three/internal.d.ts.map +1 -0
- package/dist/three/internal.js +49 -0
- package/dist/three/internal.js.map +1 -0
- package/dist/three/motion-limits.json +116 -0
- package/dist/three/sequences.d.ts +71 -0
- package/dist/three/sequences.d.ts.map +1 -0
- package/dist/three/sequences.js +262 -0
- package/dist/three/sequences.js.map +1 -0
- package/dist/three/tanya.d.ts +35 -0
- package/dist/three/tanya.d.ts.map +1 -0
- package/dist/three/tanya.js +51 -0
- package/dist/three/tanya.js.map +1 -0
- package/dist/three/tara-rig.d.ts +403 -0
- package/dist/three/tara-rig.d.ts.map +1 -0
- package/dist/three/tara-rig.js +1500 -0
- package/dist/three/tara-rig.js.map +1 -0
- package/dist/three/tara.d.ts +35 -0
- package/dist/three/tara.d.ts.map +1 -0
- package/dist/three/tara.js +58 -0
- package/dist/three/tara.js.map +1 -0
- package/dist/three/tushar.d.ts +31 -0
- package/dist/three/tushar.d.ts.map +1 -0
- package/dist/three/tushar.js +47 -0
- package/dist/three/tushar.js.map +1 -0
- package/dist/types.d.ts +65 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +58 -11
- package/dist/types.js.map +1 -1
- package/dist/vikram.d.ts +1 -0
- package/dist/vikram.d.ts.map +1 -1
- package/dist/vikram.js +2 -0
- package/dist/vikram.js.map +1 -1
- package/package.json +34 -6
- package/src/avatar.d.ts +59 -2
- package/src/avatar.js +743 -94
- package/src/behavior.d.ts +28 -12
- package/src/behavior.js +54 -42
- package/src/canvas/create-rig.js +1 -1
- package/src/canvas/src/live.js +2 -2
- package/src/conformance.js +1 -1
- package/src/emotions.js +4 -2
- package/src/gaze.js +334 -73
- package/src/head.js +134 -0
- package/src/idle.js +210 -19
- package/src/interjections.js +70 -26
- package/src/prosody.js +647 -0
- package/src/visemes.js +85 -15
package/src/visemes.js
CHANGED
|
@@ -17,10 +17,20 @@
|
|
|
17
17
|
* H tongue up L
|
|
18
18
|
* X idle / silence
|
|
19
19
|
*
|
|
20
|
-
* A cue is `{ t, v, i? }` — millisecond offset into the utterance, the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
20
|
+
* A cue is `{ t, v, i?, p? }` — millisecond offset into the utterance, the
|
|
21
|
+
* letter, an optional 0..1 intensity (loudness): the same viseme shouted and
|
|
22
|
+
* murmured should not look identical, and an optional phone label. The backend
|
|
23
|
+
* measures `i` from the RMS under each cue, but only on the leg that has audio
|
|
24
|
+
* to measure — a predicted cue describes speech nobody has generated yet, so it
|
|
25
|
+
* arrives without one and is read here as 1.
|
|
26
|
+
*
|
|
27
|
+
* `p` is the phone being articulated under the letter, and the nine letters are
|
|
28
|
+
* a lossy projection of it: `B` above lists four phone families and absorbs
|
|
29
|
+
* fourteen more. Nothing in this file reads it — `VISEME_SHAPES` is the mouth
|
|
30
|
+
* every face owes the server and it is keyed by letter. It rides through
|
|
31
|
+
* normalization and out of `sample()` so that a rig with a finer mouth than
|
|
32
|
+
* nine shapes can use it without the server sending a second track, and so that
|
|
33
|
+
* a rig without one is unaffected.
|
|
24
34
|
*/
|
|
25
35
|
|
|
26
36
|
import {
|
|
@@ -70,18 +80,51 @@ export function shapeFor(letter, intensity = 1) {
|
|
|
70
80
|
// Cue track hygiene
|
|
71
81
|
// ---------------------------------------------------------------------------
|
|
72
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Group a time-ordered cue list into runs that share a mouth shape.
|
|
85
|
+
*
|
|
86
|
+
* A backend emits one cue per intersection of its shape and phone timelines, so
|
|
87
|
+
* a held shape arrives as several cues differing only in `p`. Grouping them is
|
|
88
|
+
* what lets the visibility rules stay rules about shapes.
|
|
89
|
+
*/
|
|
90
|
+
function shapeRuns(cues) {
|
|
91
|
+
const runs = [];
|
|
92
|
+
for (const c of cues) {
|
|
93
|
+
const v = VISEME_SHAPES[c.v] ? c.v : SILENT;
|
|
94
|
+
const clean = { t: c.t, v, i: c.i == null ? 1 : c.i, p: c.p ?? null };
|
|
95
|
+
if (runs.length && runs[runs.length - 1][0].v === v) runs[runs.length - 1].push(clean);
|
|
96
|
+
else runs.push([clean]);
|
|
97
|
+
}
|
|
98
|
+
return runs;
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
/**
|
|
74
102
|
* Sort, merge consecutive duplicates, and drop sub-perceptual cues. Servers
|
|
75
103
|
* emit noisy tracks; this makes them watchable.
|
|
104
|
+
*
|
|
105
|
+
* **Every decision here is a decision about shapes**, taken over the run heads
|
|
106
|
+
* and nothing else. MIN_VISIBLE_CUE_MS and the closure swap are rules about what
|
|
107
|
+
* the face can be *seen* to do, and a phone transition inside a held shape is
|
|
108
|
+
* not something the face does at all — letting one participate would mean a
|
|
109
|
+
* phone changing 15 ms before a real shape change could swallow that change,
|
|
110
|
+
* which is a lipsync regression bought with a field this file does not read.
|
|
111
|
+
* Phone detail is re-attached afterwards, bounded by the mouth positions either
|
|
112
|
+
* side of it. Mirrors `normalize_cues` server-side.
|
|
76
113
|
*/
|
|
77
114
|
export function normalizeCues(cues) {
|
|
115
|
+
const runs = shapeRuns([...cues].sort((a, b) => a.t - b.t));
|
|
116
|
+
|
|
117
|
+
// Pass one: the shape track, by exactly the rule that predates phone detail.
|
|
118
|
+
// `kept` maps each surviving cue back to the run it speaks for, which the
|
|
119
|
+
// closure swap can change without moving the cue's timestamp.
|
|
78
120
|
const out = [];
|
|
79
|
-
const
|
|
80
|
-
for (
|
|
81
|
-
const
|
|
121
|
+
const kept = [];
|
|
122
|
+
for (let index = 0; index < runs.length; index++) {
|
|
123
|
+
const head = runs[index][0];
|
|
124
|
+
const v = head.v;
|
|
82
125
|
const prev = out[out.length - 1];
|
|
83
126
|
if (prev && prev.v === v) continue; // merge repeats
|
|
84
|
-
if (prev &&
|
|
127
|
+
if (prev && head.t - prev.t < MIN_VISIBLE_CUE_MS) {
|
|
85
128
|
// Too short to read. Keep whichever is more visually salient: a closure
|
|
86
129
|
// (A/G) carries more lip-reading information than a mid-open vowel.
|
|
87
130
|
if (v === 'A' || v === 'G') {
|
|
@@ -89,17 +132,38 @@ export function normalizeCues(cues) {
|
|
|
89
132
|
// Its replacement would otherwise create a duplicate visible shape;
|
|
90
133
|
// preserving the first G is both the stable wire form and the face the
|
|
91
134
|
// viewer actually saw.
|
|
92
|
-
if (out.length > 1 && out[out.length - 2].v === v) out.pop();
|
|
135
|
+
if (out.length > 1 && out[out.length - 2].v === v) { out.pop(); kept.pop(); }
|
|
93
136
|
// A winning closure replaces the preceding shape for the entire
|
|
94
137
|
// sub-perceptual interval. Preserve that cue's timestamp while taking
|
|
95
138
|
// the closure's intensity, matching the server-side wire normalizer.
|
|
96
|
-
else
|
|
139
|
+
else {
|
|
140
|
+
out[out.length - 1] = { ...head, t: prev.t, v };
|
|
141
|
+
kept[kept.length - 1] = index;
|
|
142
|
+
}
|
|
97
143
|
}
|
|
98
144
|
continue;
|
|
99
145
|
}
|
|
100
|
-
out.push({ t:
|
|
146
|
+
out.push({ t: head.t, v, i: head.i, p: head.p });
|
|
147
|
+
kept.push(index);
|
|
101
148
|
}
|
|
102
|
-
|
|
149
|
+
|
|
150
|
+
// Pass two: the phone detail inside each surviving mouth position. Silence is
|
|
151
|
+
// never split — nothing is being articulated under a rest.
|
|
152
|
+
const result = [];
|
|
153
|
+
for (let slot = 0; slot < out.length; slot++) {
|
|
154
|
+
const cue = out[slot];
|
|
155
|
+
result.push(cue);
|
|
156
|
+
if (cue.v === SILENT) continue;
|
|
157
|
+
const limit = slot + 1 < out.length ? out[slot + 1].t : Infinity;
|
|
158
|
+
for (const sub of runs[kept[slot]].slice(1)) {
|
|
159
|
+
// Strictly after whatever was last emitted, not merely after the head: a
|
|
160
|
+
// splice joins two legs' tracks and both can name the same millisecond.
|
|
161
|
+
if (sub.t <= result[result.length - 1].t || sub.t >= limit) continue;
|
|
162
|
+
if (sub.p === result[result.length - 1].p) continue;
|
|
163
|
+
result.push({ t: sub.t, v: cue.v, i: sub.i, p: sub.p });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
103
167
|
}
|
|
104
168
|
|
|
105
169
|
/**
|
|
@@ -124,8 +188,13 @@ export class VisemeTrack {
|
|
|
124
188
|
this._idx = 0;
|
|
125
189
|
this.onEnd = null;
|
|
126
190
|
this.tailMs = SPEECH_TRACK_TAIL_MS;
|
|
191
|
+
// Where the last sample() landed, for the layers that read the track's
|
|
192
|
+
// future (prosody.js looks ahead to the end of a pause).
|
|
193
|
+
this.now = 0;
|
|
127
194
|
}
|
|
128
195
|
|
|
196
|
+
get index() { return this._idx; }
|
|
197
|
+
|
|
129
198
|
/** @param {() => number} clock returns elapsed ms of the audio being played */
|
|
130
199
|
start(cues, clock) {
|
|
131
200
|
this.cues = normalizeCues(cues);
|
|
@@ -149,10 +218,11 @@ export class VisemeTrack {
|
|
|
149
218
|
this._idx = 0;
|
|
150
219
|
}
|
|
151
220
|
|
|
152
|
-
/** @returns {{letter: string, intensity: number} | null} */
|
|
221
|
+
/** @returns {{letter: string, intensity: number, phone: string | null} | null} */
|
|
153
222
|
sample() {
|
|
154
223
|
if (!this.playing || !this.cues.length || !this.clock) return null;
|
|
155
224
|
const now = this.clock() + LEAD_MS;
|
|
225
|
+
this.now = now;
|
|
156
226
|
|
|
157
227
|
// Cues are time-ordered and `now` is mostly monotonic, so this walk is O(1)
|
|
158
228
|
// amortized. Reset on seek-backward.
|
|
@@ -167,8 +237,8 @@ export class VisemeTrack {
|
|
|
167
237
|
}
|
|
168
238
|
|
|
169
239
|
const cue = this.cues[this._idx];
|
|
170
|
-
if (cue.t > now) return { letter: SILENT, intensity: 1 };
|
|
171
|
-
return { letter: cue.v, intensity: cue.i == null ? 1 : cue.i };
|
|
240
|
+
if (cue.t > now) return { letter: SILENT, intensity: 1, phone: null };
|
|
241
|
+
return { letter: cue.v, intensity: cue.i == null ? 1 : cue.i, phone: cue.p ?? null };
|
|
172
242
|
}
|
|
173
243
|
}
|
|
174
244
|
|