@convai/embody 1.0.1-beta.0 → 1.1.0-beta.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/CHANGELOG.md +38 -0
- package/README.md +12 -8
- package/dist/character/createCharacter.d.ts +66 -5
- package/dist/character/createCharacter.js +93 -22
- package/dist/character/pipeline.d.ts +9 -5
- package/dist/clips/animations.d.ts +13 -1
- package/dist/clips/animations.js +28 -6
- package/dist/clips/retarget.d.ts +168 -0
- package/dist/clips/retarget.js +856 -0
- package/dist/clips/sources.d.ts +70 -0
- package/dist/clips/sources.js +119 -0
- package/dist/connect/session.d.ts +11 -0
- package/dist/connect/session.js +22 -0
- package/dist/index.d.ts +22 -1
- package/dist/index.js +12 -0
- package/dist/inspect/inspectModel.d.ts +14 -0
- package/dist/inspect/inspectModel.js +26 -0
- package/dist/inspect/morphMap.d.ts +54 -0
- package/dist/inspect/morphMap.js +140 -0
- package/dist/inspect/report.d.ts +88 -0
- package/dist/inspect/report.js +193 -0
- package/dist/motion/eyeAim.d.ts +94 -0
- package/dist/motion/eyeAim.js +276 -0
- package/dist/motion/eyeGaze.d.ts +17 -0
- package/dist/motion/eyeGaze.js +42 -0
- package/dist/motion/footLock.d.ts +67 -0
- package/dist/motion/footLock.js +176 -0
- package/dist/motion/gaze.d.ts +135 -180
- package/dist/motion/gaze.js +257 -491
- package/dist/motion/hair.d.ts +27 -0
- package/dist/motion/hair.js +127 -0
- package/dist/motion/hairClearance.d.ts +39 -0
- package/dist/motion/hairClearance.js +125 -0
- package/dist/motion/morphNormals.d.ts +38 -0
- package/dist/motion/morphNormals.js +237 -0
- package/dist/motion/types.d.ts +15 -0
- package/dist/react/CharacterModel.d.ts +3 -1
- package/dist/react/CharacterModel.js +9 -1
- package/dist/react/ConvaiCharacter.js +4 -1
- package/dist/react/PortraitLights.d.ts +3 -0
- package/dist/react/PortraitLights.js +44 -0
- package/dist/render/faceMaterials.d.ts +60 -3
- package/dist/render/faceMaterials.js +320 -96
- package/dist/render/portraitRig.d.ts +28 -0
- package/dist/render/portraitRig.js +55 -0
- package/dist/render/surfaces.d.ts +181 -0
- package/dist/render/surfaces.js +1020 -0
- package/dist/rig/resolve.d.ts +8 -2
- package/dist/rig/resolve.js +7 -3
- package/dist/rig/units.d.ts +35 -16
- package/dist/rig/units.js +143 -42
- package/dist/speech/expression.d.ts +20 -0
- package/dist/speech/expression.js +118 -0
- package/dist/speech/lipsync.d.ts +68 -2
- package/dist/speech/lipsync.js +205 -5
- package/dist/speech/mouthFinish.d.ts +46 -0
- package/dist/speech/mouthFinish.js +235 -0
- package/dist/speech/voiceClock.d.ts +94 -0
- package/dist/speech/voiceClock.js +176 -0
- package/package.json +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The voice's own clock, for lipsync.
|
|
3
|
+
*
|
|
4
|
+
* Mouth frames arrive paced for the wall clock (60 per second from the moment
|
|
5
|
+
* the character starts speaking). WebRTC does not play the voice on the wall
|
|
6
|
+
* clock: its jitter buffer holds it (~180 ms measured) and stretches and
|
|
7
|
+
* squeezes it to absorb network jitter (a 25.25 s reply played in 24.9 s).
|
|
8
|
+
* Against the pauses in the voice, a wall-clocked mouth started ~230 ms late
|
|
9
|
+
* and ended ~250 ms early in a 20 s reply: it moved through silences.
|
|
10
|
+
*
|
|
11
|
+
* So the mouth follows the voice. This reads the received audio track's RTP
|
|
12
|
+
* synchronization source — the timestamp of the sample playing now (48 kHz)
|
|
13
|
+
* and its level — and reports how much voice has played since the first
|
|
14
|
+
* audible sample:
|
|
15
|
+
*
|
|
16
|
+
* · until the voice is audible it reports null (the mouth holds);
|
|
17
|
+
* · after that it advances by the voice's own RTP steps, so stretching and
|
|
18
|
+
* squeezing move the mouth with the voice;
|
|
19
|
+
* · while no packets arrive (a network break, a server stall, silence
|
|
20
|
+
* suppression) it holds, and when they return advances by the voice time
|
|
21
|
+
* that actually passed — a wall-clocked mouth stayed ahead of the voice by
|
|
22
|
+
* every break's length for the rest of the reply.
|
|
23
|
+
*
|
|
24
|
+
* The level comes from the source when the sender negotiated the RTP
|
|
25
|
+
* audio-level extension. Convai's voice does not (measured on staging:
|
|
26
|
+
* sources carry no `audioLevel`), so otherwise it comes from the receiver's
|
|
27
|
+
* inbound-rtp stats: the level of the decoded audio, refreshed every
|
|
28
|
+
* ~100–200 ms in Chrome. Coarse, but breaks are 150 ms and longer. One stats
|
|
29
|
+
* request is in flight at a time; a stats level older than 0.5 s is not used.
|
|
30
|
+
*
|
|
31
|
+
* A level is believed only once it has read above zero. A voice the page
|
|
32
|
+
* does not play out (autoplay blocked before a user gesture, a muted or
|
|
33
|
+
* detached element) still arrives and still advances the clock, but Chrome
|
|
34
|
+
* measures its level as exactly 0 throughout (measured: 306 speaking frames,
|
|
35
|
+
* peak 0, totalAudioEnergy 0). Taken at its word, that silence held every
|
|
36
|
+
* break gate shut and pulled the mouth to rest against the stream every
|
|
37
|
+
* 150 ms of a reply. Until the voice is shown to be playing, the level is
|
|
38
|
+
* unknown (null), and the gates that depend on it stay off.
|
|
39
|
+
*
|
|
40
|
+
* No AudioContext: neither level needs a user gesture.
|
|
41
|
+
*/
|
|
42
|
+
/** RTP clock rate of Opus, the voice codec. */
|
|
43
|
+
const RATE = 48000;
|
|
44
|
+
/** Audio level (0–1) that counts as audible. */
|
|
45
|
+
const ONSET = 0.01;
|
|
46
|
+
/** A source older than this has stopped playing, s. */
|
|
47
|
+
const STALE = 0.06;
|
|
48
|
+
/** Longest the clock waits for the voice before starting anyway, s. */
|
|
49
|
+
const ANCHOR_TIMEOUT = 15;
|
|
50
|
+
/** Packets for this long with no level ever above zero: the voice is not played out, s. */
|
|
51
|
+
const SILENT_PLAYOUT = 1;
|
|
52
|
+
/** A stats level older than this is not used, s. */
|
|
53
|
+
const STATS_STALE = 0.5;
|
|
54
|
+
export class VoiceClock {
|
|
55
|
+
constructor(receiver, now = () => Date.now()) {
|
|
56
|
+
this.receiver = receiver;
|
|
57
|
+
this.now = now;
|
|
58
|
+
this.started = false;
|
|
59
|
+
this.played = 0;
|
|
60
|
+
this.lastRtp = null;
|
|
61
|
+
this.lastGoodRtp = null;
|
|
62
|
+
this.held = 0;
|
|
63
|
+
this.waited = 0;
|
|
64
|
+
/** Seconds of packets seen while waiting for the voice. */
|
|
65
|
+
this.packets = 0;
|
|
66
|
+
this.stats = null;
|
|
67
|
+
this.polling = false;
|
|
68
|
+
/** Whether any level has read above zero: the voice is being played out. */
|
|
69
|
+
this.playing = false;
|
|
70
|
+
/** Audio level of the voice this tick (0–1), or null without a receiver. */
|
|
71
|
+
this.level = null;
|
|
72
|
+
/** True while no packets arrive: the clock is holding. */
|
|
73
|
+
this.holding = false;
|
|
74
|
+
/** Seconds the clock jumped on the tick packets came back; 0 on every other tick. */
|
|
75
|
+
this.resumedBy = 0;
|
|
76
|
+
}
|
|
77
|
+
/** Whether a receiver is available to clock against. */
|
|
78
|
+
get available() {
|
|
79
|
+
return this.receiver() !== null;
|
|
80
|
+
}
|
|
81
|
+
/** Start of a turn: forget the last one. */
|
|
82
|
+
reset() {
|
|
83
|
+
this.started = false;
|
|
84
|
+
this.played = 0;
|
|
85
|
+
this.lastRtp = this.lastGoodRtp = null;
|
|
86
|
+
this.held = 0;
|
|
87
|
+
this.waited = 0;
|
|
88
|
+
this.packets = 0;
|
|
89
|
+
this.holding = false;
|
|
90
|
+
this.resumedBy = 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The latest inbound-rtp level, and ask for the next one. `undefined` while
|
|
94
|
+
* the first answer is still coming (a level is readable, just not yet);
|
|
95
|
+
* null when the receiver has no stats at all.
|
|
96
|
+
*/
|
|
97
|
+
statsLevel(r) {
|
|
98
|
+
if (r.getStats && !this.polling) {
|
|
99
|
+
this.polling = true;
|
|
100
|
+
r.getStats()
|
|
101
|
+
.then((report) => {
|
|
102
|
+
report.forEach((s) => {
|
|
103
|
+
if (s.type === 'inbound-rtp' && s.kind === 'audio' && typeof s.audioLevel === 'number')
|
|
104
|
+
this.stats = { level: s.audioLevel, at: this.now() };
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
.catch(() => { })
|
|
108
|
+
.finally(() => { this.polling = false; });
|
|
109
|
+
}
|
|
110
|
+
if (!r.getStats)
|
|
111
|
+
return null;
|
|
112
|
+
const st = this.stats;
|
|
113
|
+
return st && (this.now() - st.at) / 1000 <= STATS_STALE ? st.level : undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Advance by `wallStep` seconds of render time. Returns seconds of voice
|
|
117
|
+
* played since it became audible, or null while it is not audible yet.
|
|
118
|
+
*/
|
|
119
|
+
update(wallStep) {
|
|
120
|
+
const r = this.receiver();
|
|
121
|
+
const src = r?.getSynchronizationSources()[0];
|
|
122
|
+
const age = src ? (this.now() - src.timestamp) / 1000 : Infinity;
|
|
123
|
+
const rtp = src && age <= STALE ? src.rtpTimestamp + Math.max(0, age) * RATE : null;
|
|
124
|
+
const read = src ? (age <= STALE ? src.audioLevel ?? this.statsLevel(r) : 0) : null;
|
|
125
|
+
const level = read === undefined ? null : read;
|
|
126
|
+
if (level !== null && level > 0)
|
|
127
|
+
this.playing = true;
|
|
128
|
+
this.level = this.playing ? level : null;
|
|
129
|
+
if (!this.started) {
|
|
130
|
+
this.waited += wallStep;
|
|
131
|
+
if (rtp !== null)
|
|
132
|
+
this.packets += wallStep;
|
|
133
|
+
// The onset reads the raw level. A level still being fetched is not
|
|
134
|
+
// "no level": between replies the stats go stale, so the first packet
|
|
135
|
+
// of every reply found none and started the clock on it, 75–200 ms
|
|
136
|
+
// before the voice was audible, a different amount each reply. Only a
|
|
137
|
+
// receiver that cannot report a level starts on packets. A voice never
|
|
138
|
+
// played out reads 0 throughout; after a second of packets with no
|
|
139
|
+
// level ever seen, start on them rather than wait out the timeout.
|
|
140
|
+
const audible = read === undefined ? false
|
|
141
|
+
: read === null ? rtp !== null
|
|
142
|
+
: read > ONSET || (!this.playing && this.packets > SILENT_PLAYOUT);
|
|
143
|
+
if (!audible && this.waited <= ANCHOR_TIMEOUT)
|
|
144
|
+
return null;
|
|
145
|
+
this.started = true;
|
|
146
|
+
this.played = 0;
|
|
147
|
+
this.lastRtp = this.lastGoodRtp = rtp;
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
let step;
|
|
151
|
+
this.resumedBy = 0;
|
|
152
|
+
if (rtp === null) {
|
|
153
|
+
// No packets: hold.
|
|
154
|
+
this.held += wallStep;
|
|
155
|
+
step = 0;
|
|
156
|
+
}
|
|
157
|
+
else if (this.held > 0) {
|
|
158
|
+
// Packets are back: advance by the voice time that passed, or by the
|
|
159
|
+
// time held when the RTP jump is not usable (a new audio segment).
|
|
160
|
+
const d = this.lastGoodRtp === null ? -1 : (rtp - this.lastGoodRtp) / RATE;
|
|
161
|
+
step = d >= 0 && d <= this.held + 1 ? d : this.held;
|
|
162
|
+
this.resumedBy = step;
|
|
163
|
+
this.held = 0;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const d = this.lastRtp === null ? -1 : (rtp - this.lastRtp) / RATE;
|
|
167
|
+
step = d >= 0 && d <= 0.25 ? d : wallStep;
|
|
168
|
+
}
|
|
169
|
+
this.played += step;
|
|
170
|
+
this.holding = rtp === null;
|
|
171
|
+
if (rtp !== null)
|
|
172
|
+
this.lastGoodRtp = rtp;
|
|
173
|
+
this.lastRtp = rtp;
|
|
174
|
+
return this.played;
|
|
175
|
+
}
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@convai/embody",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0-beta.0",
|
|
4
4
|
"description": "Give a Convai character a body: resolve 3D characters by characterId and drive them with lipsync and animation. Layered on @convai/web-sdk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Convai <support@convai.com>",
|