@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +12 -8
  3. package/dist/character/createCharacter.d.ts +66 -5
  4. package/dist/character/createCharacter.js +93 -22
  5. package/dist/character/pipeline.d.ts +9 -5
  6. package/dist/clips/animations.d.ts +13 -1
  7. package/dist/clips/animations.js +28 -6
  8. package/dist/clips/retarget.d.ts +168 -0
  9. package/dist/clips/retarget.js +856 -0
  10. package/dist/clips/sources.d.ts +70 -0
  11. package/dist/clips/sources.js +119 -0
  12. package/dist/connect/session.d.ts +11 -0
  13. package/dist/connect/session.js +22 -0
  14. package/dist/index.d.ts +22 -1
  15. package/dist/index.js +12 -0
  16. package/dist/inspect/inspectModel.d.ts +14 -0
  17. package/dist/inspect/inspectModel.js +26 -0
  18. package/dist/inspect/morphMap.d.ts +54 -0
  19. package/dist/inspect/morphMap.js +140 -0
  20. package/dist/inspect/report.d.ts +88 -0
  21. package/dist/inspect/report.js +193 -0
  22. package/dist/motion/eyeAim.d.ts +94 -0
  23. package/dist/motion/eyeAim.js +276 -0
  24. package/dist/motion/eyeGaze.d.ts +17 -0
  25. package/dist/motion/eyeGaze.js +42 -0
  26. package/dist/motion/footLock.d.ts +67 -0
  27. package/dist/motion/footLock.js +176 -0
  28. package/dist/motion/gaze.d.ts +135 -180
  29. package/dist/motion/gaze.js +257 -491
  30. package/dist/motion/hair.d.ts +27 -0
  31. package/dist/motion/hair.js +127 -0
  32. package/dist/motion/hairClearance.d.ts +39 -0
  33. package/dist/motion/hairClearance.js +125 -0
  34. package/dist/motion/morphNormals.d.ts +38 -0
  35. package/dist/motion/morphNormals.js +237 -0
  36. package/dist/motion/types.d.ts +15 -0
  37. package/dist/react/CharacterModel.d.ts +3 -1
  38. package/dist/react/CharacterModel.js +9 -1
  39. package/dist/react/ConvaiCharacter.js +4 -1
  40. package/dist/react/PortraitLights.d.ts +3 -0
  41. package/dist/react/PortraitLights.js +44 -0
  42. package/dist/render/faceMaterials.d.ts +60 -3
  43. package/dist/render/faceMaterials.js +320 -96
  44. package/dist/render/portraitRig.d.ts +28 -0
  45. package/dist/render/portraitRig.js +55 -0
  46. package/dist/render/surfaces.d.ts +181 -0
  47. package/dist/render/surfaces.js +1020 -0
  48. package/dist/rig/resolve.d.ts +8 -2
  49. package/dist/rig/resolve.js +7 -3
  50. package/dist/rig/units.d.ts +35 -16
  51. package/dist/rig/units.js +143 -42
  52. package/dist/speech/expression.d.ts +20 -0
  53. package/dist/speech/expression.js +118 -0
  54. package/dist/speech/lipsync.d.ts +68 -2
  55. package/dist/speech/lipsync.js +205 -5
  56. package/dist/speech/mouthFinish.d.ts +46 -0
  57. package/dist/speech/mouthFinish.js +235 -0
  58. package/dist/speech/voiceClock.d.ts +94 -0
  59. package/dist/speech/voiceClock.js +176 -0
  60. 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.1-beta.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>",