@voqalize/avatar 0.1.0 → 0.2.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 +86 -51
- package/client/dist/Avatar.d.ts +17 -14
- package/client/dist/Avatar.d.ts.map +1 -1
- package/client/dist/Avatar.js +3 -3
- package/client/dist/Avatar.js.map +1 -1
- package/client/dist/AvatarClient.d.ts +23 -57
- package/client/dist/AvatarClient.d.ts.map +1 -1
- package/client/dist/AvatarClient.js +20 -71
- package/client/dist/AvatarClient.js.map +1 -1
- package/client/dist/index.d.ts +22 -0
- package/client/dist/index.d.ts.map +1 -0
- package/client/dist/index.js +22 -0
- package/client/dist/index.js.map +1 -0
- package/client/dist/types.d.ts +22 -37
- package/client/dist/types.d.ts.map +1 -1
- package/client/dist/types.js +12 -12
- package/client/dist/types.js.map +1 -1
- package/client/dist/useAvatar.d.ts +8 -19
- package/client/dist/useAvatar.d.ts.map +1 -1
- package/client/dist/useAvatar.js +15 -32
- package/client/dist/useAvatar.js.map +1 -1
- package/client/src/Avatar.tsx +19 -24
- package/client/src/AvatarClient.ts +38 -111
- package/client/src/index.ts +22 -0
- package/client/src/types.ts +24 -47
- package/client/src/useAvatar.ts +19 -47
- package/docs/contract-avatar.md +37 -3
- package/docs/contract-protocol.md +90 -30
- package/package.json +3 -11
- package/src/avatar.d.ts +38 -6
- package/src/avatar.js +72 -37
- package/src/hand.js +680 -0
- package/src/idle.js +8 -25
- package/src/line-art.js +26 -0
- package/src/perform.js +8 -4
- package/client/dist/pipecat.d.ts +0 -21
- package/client/dist/pipecat.d.ts.map +0 -1
- package/client/dist/pipecat.js +0 -21
- package/client/dist/pipecat.js.map +0 -1
- package/client/dist/react.d.ts +0 -16
- package/client/dist/react.d.ts.map +0 -1
- package/client/dist/react.js +0 -17
- package/client/dist/react.js.map +0 -1
- package/client/src/pipecat.ts +0 -38
- package/client/src/react.ts +0 -34
- package/src/audio-fallback.js +0 -100
package/src/idle.js
CHANGED
|
@@ -392,8 +392,10 @@ export class IdleLayer {
|
|
|
392
392
|
* that acknowledges on a metronome reads as distracting
|
|
393
393
|
* (docs/research-biomechanics.md §3.5). So this engine is contingent first:
|
|
394
394
|
*
|
|
395
|
-
* · The
|
|
396
|
-
* (setUserSpeaking)
|
|
395
|
+
* · The server tells it about the user's voice — a coarse speaking flag
|
|
396
|
+
* (setUserSpeaking), off its own endpointer. With no signal at all the
|
|
397
|
+
* scheduler falls back to a timer, which is the weakest mode and is meant
|
|
398
|
+
* to be: contingency is the whole point.
|
|
397
399
|
* · Acknowledgements fire at PAUSE ONSETS: when the user stops talking, a
|
|
398
400
|
* nod lands 250–600 ms later, about half the time, never more often than
|
|
399
401
|
* every 2.5 s. That timing is where a human listener's nod sits.
|
|
@@ -420,10 +422,7 @@ export class ListeningEngine {
|
|
|
420
422
|
this._next = 0;
|
|
421
423
|
// --- user-signal state
|
|
422
424
|
this._hasSignal = false;
|
|
423
|
-
this._explicit = null; //
|
|
424
|
-
this._derived = false; // level-derived VAD
|
|
425
|
-
this._levelOn = false; // raw hysteresis state behind _derived
|
|
426
|
-
this._flipT = 0; // how long the level has disagreed with _levelOn
|
|
425
|
+
this._explicit = null; // server-declared flag; null = not driven
|
|
427
426
|
this._speaking = false; // merged VAD, after hysteresis
|
|
428
427
|
this._spokeAt = -1e9; // start of the current speech stretch
|
|
429
428
|
this._silentAt = 0; // end of the last one
|
|
@@ -439,29 +438,13 @@ export class ListeningEngine {
|
|
|
439
438
|
this._pending = -1;
|
|
440
439
|
}
|
|
441
440
|
|
|
442
|
-
/**
|
|
441
|
+
/** Server-declared user speech, off the pipeline's own endpointer. Pass null
|
|
442
|
+
* to hand back to the no-signal timer. */
|
|
443
443
|
setUserSpeaking(b) {
|
|
444
444
|
if (b !== null) this._hasSignal = true;
|
|
445
445
|
this._explicit = b === null ? null : !!b;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
/** Feed the smoothed user audio level (an AudioFallback.level). Same scale,
|
|
449
|
-
* thresholds and asymmetry as the demo's RMS VAD: quick in (80 ms), slow
|
|
450
|
-
* out (250 ms) — declaring the turn over early is the expensive mistake,
|
|
451
|
-
* and the 250 ms quiet-hold IS the pause detector the contingent
|
|
452
|
-
* scheduler keys off. */
|
|
453
|
-
observeLevel(level) {
|
|
454
|
-
this._hasSignal = true;
|
|
455
|
-
const on = !!this._levelOn;
|
|
456
|
-
const wants = level > (on ? 0.018 : 0.030);
|
|
457
|
-
if (wants === on) this._flipT = this.t;
|
|
458
|
-
else if (this.t - this._flipT >= (on ? 0.25 : 0.08)) {
|
|
459
|
-
this._levelOn = wants;
|
|
460
|
-
this._flipT = this.t;
|
|
461
|
-
}
|
|
462
|
-
this._derived = !!this._levelOn;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
448
|
/** The one seam for choosing an acknowledgement. Context-aware: what the
|
|
466
449
|
* user just did decides the weight class of the reply
|
|
467
450
|
* (docs/research-biomechanics.md §3.3 — continuers co-occur with ongoing
|
|
@@ -486,7 +469,7 @@ export class ListeningEngine {
|
|
|
486
469
|
return r < 0.55 ? 'NOD_SMALL' : r < 0.8 ? 'BROW_ACK' : 'NOD_SLOW';
|
|
487
470
|
}
|
|
488
471
|
|
|
489
|
-
get speaking() { return this._explicit
|
|
472
|
+
get speaking() { return this._explicit === true; }
|
|
490
473
|
|
|
491
474
|
update(dt) {
|
|
492
475
|
this.t += dt;
|
package/src/line-art.js
CHANGED
|
@@ -104,6 +104,32 @@ export function region(flat) {
|
|
|
104
104
|
return d + 'Z';
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* On-curve points -> the polybezier form above, Catmull-Rom at the standard 1/6
|
|
109
|
+
* tension. The house idiom for a FACE is hand-authored control points: a brow or
|
|
110
|
+
* a lip contour is tuned a handle at a time and interpolation would fight that.
|
|
111
|
+
* This is for marks whose geometry is easier to read as a list of places the
|
|
112
|
+
* line goes through than as three-in-four control points — the hand's contours,
|
|
113
|
+
* where the authoring question is "how far does the thumb clear the knuckles"
|
|
114
|
+
* and every point is measured against another point. Curve control is worth
|
|
115
|
+
* less there than a shape whose numbers can be argued about.
|
|
116
|
+
*/
|
|
117
|
+
export function smooth(pts) {
|
|
118
|
+
const out = [pts[0]];
|
|
119
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
120
|
+
const p0 = pts[i - 1] || pts[i];
|
|
121
|
+
const p1 = pts[i];
|
|
122
|
+
const p2 = pts[i + 1];
|
|
123
|
+
const p3 = pts[i + 2] || p2;
|
|
124
|
+
out.push(
|
|
125
|
+
[p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6],
|
|
126
|
+
[p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6],
|
|
127
|
+
p2
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/** Deterministic jitter, so a drawing is the same every load. */
|
|
108
134
|
export function rng(seed) {
|
|
109
135
|
let s = seed >>> 0;
|
package/src/perform.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A performance is the server's choreography: timed verbs `{t, do, ...}` fired
|
|
5
5
|
* against a clock, where every verb resolves to one of the widget's own enums —
|
|
6
|
-
* states, emotions, gaze targets, interjections. The vocabulary is deliberately
|
|
6
|
+
* states, emotions, gaze targets, interjections, hand gestures. The vocabulary is deliberately
|
|
7
7
|
* closed: the backend sequences what the rig already does well, it cannot
|
|
8
8
|
* invent motion. That constraint is what makes the wire format assemblable by
|
|
9
9
|
* a dialogue manager and reviewable by a human.
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* effects, and replaying a nod is worse than missing one.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
const VERBS = new Set(['state', 'emotion', 'gaze', 'interject']);
|
|
24
|
+
const VERBS = new Set(['state', 'emotion', 'gaze', 'interject', 'gesture']);
|
|
25
|
+
// The verbs addressed by `id` rather than `name`. Both id verbs name a clip the
|
|
26
|
+
// widget already owns; both name-verbs name an enum value.
|
|
27
|
+
const ID_VERBS = new Set(['interject', 'gesture']);
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Shape hygiene for action arrays, in the spirit of normalizeCues: sort by
|
|
@@ -45,8 +48,9 @@ export function normalizeActions(actions) {
|
|
|
45
48
|
console.warn(`perform: dropped unknown verb "${a && a.do}"`, a);
|
|
46
49
|
continue;
|
|
47
50
|
}
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
const idVerb = ID_VERBS.has(a.do);
|
|
52
|
+
if ((idVerb ? a.id : a.name) == null) {
|
|
53
|
+
console.warn(`perform: dropped ${a.do} with no ${idVerb ? 'id' : 'name'}`, a);
|
|
50
54
|
continue;
|
|
51
55
|
}
|
|
52
56
|
out.push(a);
|
package/client/dist/pipecat.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/pipecat` — drive the widget from a pipecat session.
|
|
3
|
-
*
|
|
4
|
-
* Framework-free: everything here is plain TypeScript over the `AvatarApi` the
|
|
5
|
-
* root export returns. `AvatarClient` is the whole surface — construct it
|
|
6
|
-
* around a mounted widget and either `attach()` it to a live `PipecatClient`
|
|
7
|
-
* or feed it messages yourself with `dispatch()`.
|
|
8
|
-
*
|
|
9
|
-
* import { createAvatar } from "@voqalize/avatar";
|
|
10
|
-
* import { AvatarClient } from "@voqalize/avatar/pipecat";
|
|
11
|
-
*
|
|
12
|
-
* const avatar = createAvatar({ mount: "#tile" });
|
|
13
|
-
* const detach = new AvatarClient(avatar).attach(pipecatClient);
|
|
14
|
-
*
|
|
15
|
-
* `@pipecat-ai/client-js` is a peer dependency of this subpath only — the root
|
|
16
|
-
* export has no dependencies at all, and a host that carries avatar commands
|
|
17
|
-
* over its own transport can import this module and never call `attach()`.
|
|
18
|
-
*/
|
|
19
|
-
export { AvatarClient, type AvatarClientOptions } from "./AvatarClient.js";
|
|
20
|
-
export { isAvatarMessage, AVATAR_MESSAGE_TYPE, AVATAR_PROTOCOL_VERSION, type AvatarCommand, type AvatarCue, type AvatarCuesCmd, type AvatarHintCmd, type AvatarInterjectCmd, type AvatarPerformAction, type AvatarPerformCmd, type AvatarServerMessage, type AvatarSpeechCmd, type AvatarStateCmd, type AvatarUnknownCmd, type AvatarUserCmd, } from "./types.js";
|
|
21
|
-
//# sourceMappingURL=pipecat.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pipecat.d.ts","sourceRoot":"","sources":["../src/pipecat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC"}
|
package/client/dist/pipecat.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/pipecat` — drive the widget from a pipecat session.
|
|
3
|
-
*
|
|
4
|
-
* Framework-free: everything here is plain TypeScript over the `AvatarApi` the
|
|
5
|
-
* root export returns. `AvatarClient` is the whole surface — construct it
|
|
6
|
-
* around a mounted widget and either `attach()` it to a live `PipecatClient`
|
|
7
|
-
* or feed it messages yourself with `dispatch()`.
|
|
8
|
-
*
|
|
9
|
-
* import { createAvatar } from "@voqalize/avatar";
|
|
10
|
-
* import { AvatarClient } from "@voqalize/avatar/pipecat";
|
|
11
|
-
*
|
|
12
|
-
* const avatar = createAvatar({ mount: "#tile" });
|
|
13
|
-
* const detach = new AvatarClient(avatar).attach(pipecatClient);
|
|
14
|
-
*
|
|
15
|
-
* `@pipecat-ai/client-js` is a peer dependency of this subpath only — the root
|
|
16
|
-
* export has no dependencies at all, and a host that carries avatar commands
|
|
17
|
-
* over its own transport can import this module and never call `attach()`.
|
|
18
|
-
*/
|
|
19
|
-
export { AvatarClient } from "./AvatarClient.js";
|
|
20
|
-
export { isAvatarMessage, AVATAR_MESSAGE_TYPE, AVATAR_PROTOCOL_VERSION, } from "./types.js";
|
|
21
|
-
//# sourceMappingURL=pipecat.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pipecat.js","sourceRoot":"","sources":["../src/pipecat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAA4B,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,uBAAuB,GAaxB,MAAM,YAAY,CAAC"}
|
package/client/dist/react.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/react` — the React binding.
|
|
3
|
-
*
|
|
4
|
-
* import { Avatar } from "@voqalize/avatar/react";
|
|
5
|
-
*
|
|
6
|
-
* <Avatar client={pipecatClient} className="tile" />
|
|
7
|
-
*
|
|
8
|
-
* Peers: `react >= 18` and `@pipecat-ai/client-js`. Everything a non-React
|
|
9
|
-
* host needs is in `@voqalize/avatar/pipecat`; this module adds a mount
|
|
10
|
-
* lifecycle and nothing else.
|
|
11
|
-
*/
|
|
12
|
-
export { useAvatar, type UseAvatarHandle, type UseAvatarOptions } from "./useAvatar.js";
|
|
13
|
-
export { Avatar, type AvatarProps } from "./Avatar.js";
|
|
14
|
-
export { AvatarClient, type AvatarClientOptions } from "./AvatarClient.js";
|
|
15
|
-
export { isAvatarMessage, AVATAR_MESSAGE_TYPE, AVATAR_PROTOCOL_VERSION, type AvatarCommand, type AvatarCue, type AvatarCuesCmd, type AvatarHintCmd, type AvatarInterjectCmd, type AvatarPerformAction, type AvatarPerformCmd, type AvatarServerMessage, type AvatarSpeechCmd, type AvatarStateCmd, type AvatarUnknownCmd, type AvatarUserCmd, } from "./types.js";
|
|
16
|
-
//# sourceMappingURL=react.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACxF,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC"}
|
package/client/dist/react.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/react` — the React binding.
|
|
3
|
-
*
|
|
4
|
-
* import { Avatar } from "@voqalize/avatar/react";
|
|
5
|
-
*
|
|
6
|
-
* <Avatar client={pipecatClient} className="tile" />
|
|
7
|
-
*
|
|
8
|
-
* Peers: `react >= 18` and `@pipecat-ai/client-js`. Everything a non-React
|
|
9
|
-
* host needs is in `@voqalize/avatar/pipecat`; this module adds a mount
|
|
10
|
-
* lifecycle and nothing else.
|
|
11
|
-
*/
|
|
12
|
-
export { useAvatar } from "./useAvatar.js";
|
|
13
|
-
export { Avatar } from "./Avatar.js";
|
|
14
|
-
// Re-exported so a React consumer needs one import for the common case.
|
|
15
|
-
export { AvatarClient } from "./AvatarClient.js";
|
|
16
|
-
export { isAvatarMessage, AVATAR_MESSAGE_TYPE, AVATAR_PROTOCOL_VERSION, } from "./types.js";
|
|
17
|
-
//# sourceMappingURL=react.js.map
|
package/client/dist/react.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAA+C,MAAM,gBAAgB,CAAC;AACxF,OAAO,EAAE,MAAM,EAAoB,MAAM,aAAa,CAAC;AAEvD,wEAAwE;AACxE,OAAO,EAAE,YAAY,EAA4B,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,uBAAuB,GAaxB,MAAM,YAAY,CAAC"}
|
package/client/src/pipecat.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/pipecat` — drive the widget from a pipecat session.
|
|
3
|
-
*
|
|
4
|
-
* Framework-free: everything here is plain TypeScript over the `AvatarApi` the
|
|
5
|
-
* root export returns. `AvatarClient` is the whole surface — construct it
|
|
6
|
-
* around a mounted widget and either `attach()` it to a live `PipecatClient`
|
|
7
|
-
* or feed it messages yourself with `dispatch()`.
|
|
8
|
-
*
|
|
9
|
-
* import { createAvatar } from "@voqalize/avatar";
|
|
10
|
-
* import { AvatarClient } from "@voqalize/avatar/pipecat";
|
|
11
|
-
*
|
|
12
|
-
* const avatar = createAvatar({ mount: "#tile" });
|
|
13
|
-
* const detach = new AvatarClient(avatar).attach(pipecatClient);
|
|
14
|
-
*
|
|
15
|
-
* `@pipecat-ai/client-js` is a peer dependency of this subpath only — the root
|
|
16
|
-
* export has no dependencies at all, and a host that carries avatar commands
|
|
17
|
-
* over its own transport can import this module and never call `attach()`.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export { AvatarClient, type AvatarClientOptions } from "./AvatarClient.js";
|
|
21
|
-
|
|
22
|
-
export {
|
|
23
|
-
isAvatarMessage,
|
|
24
|
-
AVATAR_MESSAGE_TYPE,
|
|
25
|
-
AVATAR_PROTOCOL_VERSION,
|
|
26
|
-
type AvatarCommand,
|
|
27
|
-
type AvatarCue,
|
|
28
|
-
type AvatarCuesCmd,
|
|
29
|
-
type AvatarHintCmd,
|
|
30
|
-
type AvatarInterjectCmd,
|
|
31
|
-
type AvatarPerformAction,
|
|
32
|
-
type AvatarPerformCmd,
|
|
33
|
-
type AvatarServerMessage,
|
|
34
|
-
type AvatarSpeechCmd,
|
|
35
|
-
type AvatarStateCmd,
|
|
36
|
-
type AvatarUnknownCmd,
|
|
37
|
-
type AvatarUserCmd,
|
|
38
|
-
} from "./types.js";
|
package/client/src/react.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@voqalize/avatar/react` — the React binding.
|
|
3
|
-
*
|
|
4
|
-
* import { Avatar } from "@voqalize/avatar/react";
|
|
5
|
-
*
|
|
6
|
-
* <Avatar client={pipecatClient} className="tile" />
|
|
7
|
-
*
|
|
8
|
-
* Peers: `react >= 18` and `@pipecat-ai/client-js`. Everything a non-React
|
|
9
|
-
* host needs is in `@voqalize/avatar/pipecat`; this module adds a mount
|
|
10
|
-
* lifecycle and nothing else.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export { useAvatar, type UseAvatarHandle, type UseAvatarOptions } from "./useAvatar.js";
|
|
14
|
-
export { Avatar, type AvatarProps } from "./Avatar.js";
|
|
15
|
-
|
|
16
|
-
// Re-exported so a React consumer needs one import for the common case.
|
|
17
|
-
export { AvatarClient, type AvatarClientOptions } from "./AvatarClient.js";
|
|
18
|
-
export {
|
|
19
|
-
isAvatarMessage,
|
|
20
|
-
AVATAR_MESSAGE_TYPE,
|
|
21
|
-
AVATAR_PROTOCOL_VERSION,
|
|
22
|
-
type AvatarCommand,
|
|
23
|
-
type AvatarCue,
|
|
24
|
-
type AvatarCuesCmd,
|
|
25
|
-
type AvatarHintCmd,
|
|
26
|
-
type AvatarInterjectCmd,
|
|
27
|
-
type AvatarPerformAction,
|
|
28
|
-
type AvatarPerformCmd,
|
|
29
|
-
type AvatarServerMessage,
|
|
30
|
-
type AvatarSpeechCmd,
|
|
31
|
-
type AvatarStateCmd,
|
|
32
|
-
type AvatarUnknownCmd,
|
|
33
|
-
type AvatarUserCmd,
|
|
34
|
-
} from "./types.js";
|
package/src/audio-fallback.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Amplitude-driven lipsync — the degradation path.
|
|
3
|
-
*
|
|
4
|
-
* When cues are late, missing, or the TTS vendor can't emit them, we read the
|
|
5
|
-
* audio directly with a WebAudio AnalyserNode and guess. It is obviously worse
|
|
6
|
-
* than real visemes, but it is *far* better than a still mouth, it costs
|
|
7
|
-
* nothing, and it keeps working when the network doesn't.
|
|
8
|
-
*
|
|
9
|
-
* The guess uses two cheap signals:
|
|
10
|
-
* · RMS -> how far the mouth opens
|
|
11
|
-
* · spectral tilt -> which shape family (sibilant / open vowel / rounded)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const GATE = 0.012;
|
|
15
|
-
|
|
16
|
-
export class AudioFallback {
|
|
17
|
-
constructor() {
|
|
18
|
-
this.ctx = null;
|
|
19
|
-
this.analyser = null;
|
|
20
|
-
this.time = null;
|
|
21
|
-
this.freq = null;
|
|
22
|
-
this.level = 0;
|
|
23
|
-
this.active = false;
|
|
24
|
-
this._letter = 'X';
|
|
25
|
-
this._holdUntil = 0;
|
|
26
|
-
this._t = 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** @param {MediaStream|HTMLMediaElement|AudioNode} source */
|
|
30
|
-
attach(source) {
|
|
31
|
-
this.detach();
|
|
32
|
-
const Ctx = window.AudioContext || window.webkitAudioContext;
|
|
33
|
-
this.ctx = new Ctx();
|
|
34
|
-
let node;
|
|
35
|
-
if (source instanceof MediaStream) node = this.ctx.createMediaStreamSource(source);
|
|
36
|
-
else if (source instanceof AudioNode) node = source;
|
|
37
|
-
else node = this.ctx.createMediaElementSource(source);
|
|
38
|
-
|
|
39
|
-
this.analyser = this.ctx.createAnalyser();
|
|
40
|
-
this.analyser.fftSize = 1024;
|
|
41
|
-
this.analyser.smoothingTimeConstant = 0.55;
|
|
42
|
-
node.connect(this.analyser);
|
|
43
|
-
// Media-element sources must still reach the speakers.
|
|
44
|
-
if (!(source instanceof MediaStream)) this.analyser.connect(this.ctx.destination);
|
|
45
|
-
|
|
46
|
-
this.time = new Uint8Array(this.analyser.fftSize);
|
|
47
|
-
this.freq = new Uint8Array(this.analyser.frequencyBinCount);
|
|
48
|
-
this.active = true;
|
|
49
|
-
if (this.ctx.state === 'suspended') this.ctx.resume().catch(() => {});
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
detach() {
|
|
54
|
-
if (this.ctx) { try { this.ctx.close(); } catch (e) { /* already closed */ } }
|
|
55
|
-
this.ctx = null; this.analyser = null; this.active = false; this.level = 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** @returns {{letter:string,intensity:number}|null} */
|
|
59
|
-
sample(dt) {
|
|
60
|
-
if (!this.active) return null;
|
|
61
|
-
this._t += dt;
|
|
62
|
-
this.analyser.getByteTimeDomainData(this.time);
|
|
63
|
-
this.analyser.getByteFrequencyData(this.freq);
|
|
64
|
-
|
|
65
|
-
let sum = 0;
|
|
66
|
-
for (let i = 0; i < this.time.length; i++) {
|
|
67
|
-
const v = (this.time[i] - 128) / 128;
|
|
68
|
-
sum += v * v;
|
|
69
|
-
}
|
|
70
|
-
const rms = Math.sqrt(sum / this.time.length);
|
|
71
|
-
this.level += (rms - this.level) * (1 - Math.exp(-dt / 0.045));
|
|
72
|
-
|
|
73
|
-
if (this.level < GATE) { this._letter = 'X'; return { letter: 'X', intensity: 1 }; }
|
|
74
|
-
|
|
75
|
-
// Bin edges assume ~48kHz; exact boundaries don't matter much here.
|
|
76
|
-
const n = this.freq.length;
|
|
77
|
-
const band = (a, b) => {
|
|
78
|
-
let s = 0;
|
|
79
|
-
const lo = Math.floor(n * a), hi = Math.floor(n * b);
|
|
80
|
-
for (let i = lo; i < hi; i++) s += this.freq[i];
|
|
81
|
-
return s / Math.max(1, hi - lo);
|
|
82
|
-
};
|
|
83
|
-
const low = band(0.00, 0.035); // ~0-800 Hz
|
|
84
|
-
const mid = band(0.035, 0.13); // ~800-3k
|
|
85
|
-
const high = band(0.13, 0.40); // ~3k-9.6k
|
|
86
|
-
const total = low + mid + high + 1e-6;
|
|
87
|
-
|
|
88
|
-
let letter;
|
|
89
|
-
if (high / total > 0.34) letter = 'B'; // sibilant
|
|
90
|
-
else if (low / total > 0.62) letter = this.level > 0.16 ? 'F' : 'E'; // rounded / back
|
|
91
|
-
else letter = this.level > 0.20 ? 'D' : this.level > 0.09 ? 'C' : 'B';
|
|
92
|
-
|
|
93
|
-
// Hold each guess briefly; frame-rate shape churn looks like chattering.
|
|
94
|
-
if (this._t < this._holdUntil) letter = this._letter;
|
|
95
|
-
else { this._letter = letter; this._holdUntil = this._t + 0.055; }
|
|
96
|
-
|
|
97
|
-
const intensity = Math.min(1, this.level / 0.22);
|
|
98
|
-
return { letter, intensity };
|
|
99
|
-
}
|
|
100
|
-
}
|