@voqalize/avatar 0.2.1 → 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.
Files changed (201) hide show
  1. package/LICENSE +21 -661
  2. package/README.md +144 -663
  3. package/client/Avatar.tsx +57 -0
  4. package/client/AvatarClient.ts +523 -0
  5. package/client/arjun.ts +24 -0
  6. package/client/createAvatar.ts +139 -0
  7. package/client/createCanvasAvatar.ts +72 -0
  8. package/client/index.ts +39 -0
  9. package/client/internal.ts +80 -0
  10. package/client/interviewer-female.ts +4 -0
  11. package/client/interviewer-male.ts +4 -0
  12. package/client/ishita.ts +24 -0
  13. package/client/kabir.ts +24 -0
  14. package/client/meera.ts +24 -0
  15. package/client/naina.ts +24 -0
  16. package/client/professional-female-a.ts +4 -0
  17. package/client/professional-female-b.ts +4 -0
  18. package/client/professional-male-a.ts +4 -0
  19. package/client/professional-male-b.ts +4 -0
  20. package/client/react.ts +13 -0
  21. package/client/types.ts +146 -0
  22. package/client/useAvatar.ts +74 -0
  23. package/client/vikram.ts +24 -0
  24. package/dist/Avatar.d.ts +44 -0
  25. package/dist/Avatar.d.ts.map +1 -0
  26. package/{client/dist → dist}/Avatar.js +2 -2
  27. package/dist/Avatar.js.map +1 -0
  28. package/dist/AvatarClient.d.ts +196 -0
  29. package/dist/AvatarClient.d.ts.map +1 -0
  30. package/dist/AvatarClient.js +450 -0
  31. package/dist/AvatarClient.js.map +1 -0
  32. package/dist/arjun.d.ts +6 -0
  33. package/dist/arjun.d.ts.map +1 -0
  34. package/dist/arjun.js +18 -0
  35. package/dist/arjun.js.map +1 -0
  36. package/dist/createAvatar.d.ts +99 -0
  37. package/dist/createAvatar.d.ts.map +1 -0
  38. package/dist/createAvatar.js +75 -0
  39. package/dist/createAvatar.js.map +1 -0
  40. package/dist/createCanvasAvatar.d.ts +22 -0
  41. package/dist/createCanvasAvatar.d.ts.map +1 -0
  42. package/dist/createCanvasAvatar.js +47 -0
  43. package/dist/createCanvasAvatar.js.map +1 -0
  44. package/dist/index.d.ts +30 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +29 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/internal.d.ts +30 -0
  49. package/dist/internal.d.ts.map +1 -0
  50. package/dist/internal.js +33 -0
  51. package/dist/internal.js.map +1 -0
  52. package/dist/interviewer-female.d.ts +4 -0
  53. package/dist/interviewer-female.d.ts.map +1 -0
  54. package/dist/interviewer-female.js +3 -0
  55. package/dist/interviewer-female.js.map +1 -0
  56. package/dist/interviewer-male.d.ts +4 -0
  57. package/dist/interviewer-male.d.ts.map +1 -0
  58. package/dist/interviewer-male.js +3 -0
  59. package/dist/interviewer-male.js.map +1 -0
  60. package/dist/ishita.d.ts +6 -0
  61. package/dist/ishita.d.ts.map +1 -0
  62. package/dist/ishita.js +18 -0
  63. package/dist/ishita.js.map +1 -0
  64. package/dist/kabir.d.ts +6 -0
  65. package/dist/kabir.d.ts.map +1 -0
  66. package/dist/kabir.js +18 -0
  67. package/dist/kabir.js.map +1 -0
  68. package/dist/meera.d.ts +6 -0
  69. package/dist/meera.d.ts.map +1 -0
  70. package/dist/meera.js +18 -0
  71. package/dist/meera.js.map +1 -0
  72. package/dist/naina.d.ts +6 -0
  73. package/dist/naina.d.ts.map +1 -0
  74. package/dist/naina.js +18 -0
  75. package/dist/naina.js.map +1 -0
  76. package/dist/professional-female-a.d.ts +4 -0
  77. package/dist/professional-female-a.d.ts.map +1 -0
  78. package/dist/professional-female-a.js +3 -0
  79. package/dist/professional-female-a.js.map +1 -0
  80. package/dist/professional-female-b.d.ts +4 -0
  81. package/dist/professional-female-b.d.ts.map +1 -0
  82. package/dist/professional-female-b.js +3 -0
  83. package/dist/professional-female-b.js.map +1 -0
  84. package/dist/professional-male-a.d.ts +4 -0
  85. package/dist/professional-male-a.d.ts.map +1 -0
  86. package/dist/professional-male-a.js +3 -0
  87. package/dist/professional-male-a.js.map +1 -0
  88. package/dist/professional-male-b.d.ts +4 -0
  89. package/dist/professional-male-b.d.ts.map +1 -0
  90. package/dist/professional-male-b.js +3 -0
  91. package/dist/professional-male-b.js.map +1 -0
  92. package/dist/react.d.ts +13 -0
  93. package/dist/react.d.ts.map +1 -0
  94. package/dist/react.js +13 -0
  95. package/dist/react.js.map +1 -0
  96. package/dist/types.d.ts +94 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +93 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/useAvatar.d.ts +39 -0
  101. package/dist/useAvatar.d.ts.map +1 -0
  102. package/dist/useAvatar.js +35 -0
  103. package/dist/useAvatar.js.map +1 -0
  104. package/dist/vikram.d.ts +6 -0
  105. package/dist/vikram.d.ts.map +1 -0
  106. package/dist/vikram.js +18 -0
  107. package/dist/vikram.js.map +1 -0
  108. package/package.json +81 -24
  109. package/src/avatar.d.ts +161 -120
  110. package/src/avatar.js +234 -146
  111. package/src/behavior.d.ts +26 -0
  112. package/src/behavior.js +102 -0
  113. package/src/camera.js +29 -0
  114. package/src/canvas/author/parts/eye.mjs +722 -0
  115. package/src/canvas/author/parts/hand.mjs +1156 -0
  116. package/src/canvas/author/parts/mouth.mjs +741 -0
  117. package/src/canvas/author/parts/nose.mjs +100 -0
  118. package/src/canvas/author/parts/skin-detail.mjs +67 -0
  119. package/src/canvas/author/path.mjs +283 -0
  120. package/src/canvas/author/rig.mjs +405 -0
  121. package/src/canvas/avatars/round/face.d.mts +3 -0
  122. package/src/canvas/avatars/round/face.mjs +1307 -0
  123. package/src/canvas/create-rig.d.ts +15 -0
  124. package/src/canvas/create-rig.js +100 -0
  125. package/src/canvas/data/img/professional-female-a-hair-back.webp +0 -0
  126. package/src/canvas/data/img/professional-female-a-hair-front.webp +0 -0
  127. package/src/canvas/data/img/professional-female-a-top-body.webp +0 -0
  128. package/src/canvas/data/img/professional-female-b-hair-back.webp +0 -0
  129. package/src/canvas/data/img/professional-female-b-hair-front.webp +0 -0
  130. package/src/canvas/data/img/professional-female-b-top-body.webp +0 -0
  131. package/src/canvas/data/img/professional-male-a-hair-back.webp +0 -0
  132. package/src/canvas/data/img/professional-male-a-hair-front.webp +0 -0
  133. package/src/canvas/data/img/professional-male-a-top-body.webp +0 -0
  134. package/src/canvas/data/img/professional-male-b-hair-back.webp +0 -0
  135. package/src/canvas/data/img/professional-male-b-hair-front.webp +0 -0
  136. package/src/canvas/data/img/professional-male-b-top-body.webp +0 -0
  137. package/src/canvas/data/img/round-m3-hair-back.webp +0 -0
  138. package/src/canvas/data/img/round-m3-hair-front.webp +0 -0
  139. package/src/canvas/data/img/round-m3-top-body.webp +0 -0
  140. package/src/canvas/data/img/round-w1-hair-back.webp +0 -0
  141. package/src/canvas/data/img/round-w1-hair-front.webp +0 -0
  142. package/src/canvas/data/img/round-w1-top-body.webp +0 -0
  143. package/src/canvas/data/interviewer-female.rig.json +1 -0
  144. package/src/canvas/data/interviewer-male.rig.json +1 -0
  145. package/src/canvas/data/professional-female-a.rig.json +1 -0
  146. package/src/canvas/data/professional-female-b.rig.json +1 -0
  147. package/src/canvas/data/professional-male-a.rig.json +1 -0
  148. package/src/canvas/data/professional-male-b.rig.json +1 -0
  149. package/src/canvas/src/live.js +508 -0
  150. package/src/canvas/src/render2d.js +218 -0
  151. package/src/canvas/src/rig.js +297 -0
  152. package/src/canvas/src/vocab.js +96 -0
  153. package/src/clips.js +18 -7
  154. package/src/conformance.js +119 -0
  155. package/src/emotions.js +4 -3
  156. package/src/face-core.js +27 -1
  157. package/src/face-myna.d.ts +7 -0
  158. package/src/face-myna.js +192 -131
  159. package/src/face-peep-control-plane.js +167 -0
  160. package/src/face-peep.d.ts +7 -0
  161. package/src/face-peep.js +274 -116
  162. package/src/face-wren.d.ts +7 -0
  163. package/src/face-wren.js +17 -19
  164. package/src/faces.d.ts +12 -0
  165. package/src/faces.js +53 -0
  166. package/src/gaze.js +119 -4
  167. package/src/hand.js +120 -91
  168. package/src/idle.js +18 -101
  169. package/src/interjections.js +156 -31
  170. package/src/params.js +8 -4
  171. package/src/perform.js +7 -9
  172. package/src/rig.d.ts +24 -0
  173. package/src/rig.js +32 -0
  174. package/src/speech-timing.js +23 -0
  175. package/src/visemes.js +28 -12
  176. package/client/dist/Avatar.d.ts +0 -27
  177. package/client/dist/Avatar.d.ts.map +0 -1
  178. package/client/dist/Avatar.js.map +0 -1
  179. package/client/dist/AvatarClient.d.ts +0 -139
  180. package/client/dist/AvatarClient.d.ts.map +0 -1
  181. package/client/dist/AvatarClient.js +0 -223
  182. package/client/dist/AvatarClient.js.map +0 -1
  183. package/client/dist/index.d.ts +0 -22
  184. package/client/dist/index.d.ts.map +0 -1
  185. package/client/dist/index.js +0 -22
  186. package/client/dist/index.js.map +0 -1
  187. package/client/dist/types.d.ts +0 -86
  188. package/client/dist/types.d.ts.map +0 -1
  189. package/client/dist/types.js +0 -31
  190. package/client/dist/types.js.map +0 -1
  191. package/client/dist/useAvatar.d.ts +0 -42
  192. package/client/dist/useAvatar.d.ts.map +0 -1
  193. package/client/dist/useAvatar.js +0 -51
  194. package/client/dist/useAvatar.js.map +0 -1
  195. package/client/src/Avatar.tsx +0 -33
  196. package/client/src/AvatarClient.ts +0 -270
  197. package/client/src/index.ts +0 -22
  198. package/client/src/types.ts +0 -104
  199. package/client/src/useAvatar.ts +0 -85
  200. package/docs/contract-avatar.md +0 -371
  201. package/docs/contract-protocol.md +0 -461
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `createAvatar` — the entire public surface of an avatar.
3
+ *
4
+ * const avatar = createAvatar({ mount: el, client: pipecatClient });
5
+ * // …
6
+ * avatar.destroy();
7
+ *
8
+ * An avatar is an embodiment of a `PipecatClient`. You hand it a mount and a
9
+ * live client; it subscribes and reacts. There is nothing else: no methods to
10
+ * drive it, no callbacks to observe it, no state to read back. Everything it
11
+ * needs to know is already on the wire — Pipecat's factual lifecycle events
12
+ * plus the avatar server-message envelope (docs/contract-wire.md).
13
+ *
14
+ * ## This is also the extension point
15
+ *
16
+ * There is no registry, no loader and no plug-in system. **You add an avatar by
17
+ * publishing a module that exports `createAvatar` with this signature**, and
18
+ * importing yours instead of ours:
19
+ *
20
+ * import { createAvatar } from "@acme/our-mascot";
21
+ *
22
+ * That is the whole mechanism, and it is deliberate. A registry would make us
23
+ * own resolution, versioning and asset paths for code we have never seen; a
24
+ * renderer interface would make us commit to a second public contract before we
25
+ * know what the second renderer actually needs. An ES module export we get for
26
+ * free from the platform.
27
+ *
28
+ * Everything past `mount` and `client` belongs to the implementation. Ours takes
29
+ * `SvgAvatarOptions`; yours takes whatever it likes, and `AvatarFactory<O>` is
30
+ * generic so the caller still gets your options checked.
31
+ *
32
+ * ## What an implementation owes the caller
33
+ *
34
+ * Only that `destroy()` leaves the mount as it found it and unsubscribes from
35
+ * the client. Not returning to rest, not honouring every action, not even
36
+ * having a mouth — a handless, mouthless avatar that only changes colour is a
37
+ * conforming avatar. The obligations that *matter* are perceptual, not typed,
38
+ * and they live in docs/contract-behavior.md.
39
+ */
40
+
41
+ import type { PipecatClient } from "@pipecat-ai/client-js";
42
+ import { createAvatar as createSvgWidget } from "../src/avatar.js";
43
+ import type { Face, FaceTheme, Gain, HandSide } from "../src/avatar.js";
44
+ import { peep } from "../src/face-peep.js";
45
+ import { AvatarClient } from "./AvatarClient.js";
46
+
47
+ export type { Face, FaceTheme, Gain, HandSide };
48
+
49
+ /**
50
+ * What every `createAvatar` takes. Implementations extend it with their own
51
+ * options; the two named here are the only ones the caller can count on.
52
+ */
53
+ export interface AvatarOptions {
54
+ /** The element to render into. The implementation owns its contents. */
55
+ readonly mount: HTMLElement;
56
+ /**
57
+ * A live `PipecatClient`. Required, and not nullable: an avatar with nothing
58
+ * to embody has no reason to exist yet. React callers get the forgiving
59
+ * version — `<Avatar>` waits for a non-null client before mounting.
60
+ */
61
+ readonly client: PipecatClient;
62
+ }
63
+
64
+ /** What every `createAvatar` returns. */
65
+ export interface AvatarInstance {
66
+ /** Unsubscribe from the client and remove everything from the mount. */
67
+ destroy(): void;
68
+ }
69
+
70
+ /**
71
+ * The shape a third-party avatar module exports, parameterised by its own
72
+ * options so a caller passing them gets them checked. `AvatarFactory` bare is
73
+ * the common denominator — the two options everyone shares.
74
+ */
75
+ export type AvatarFactory<O extends AvatarOptions = AvatarOptions> =
76
+ (options: O) => AvatarInstance;
77
+
78
+ /**
79
+ * Options for the bundled SVG avatars — ours alone; nothing outside this
80
+ * package reads them.
81
+ *
82
+ * `face` is a value, not a name: a name needs a table, and a table needs every
83
+ * face imported to answer any lookup. Import the one you want and the other two
84
+ * never enter your bundle.
85
+ *
86
+ * import { createAvatar } from "@voqalize/avatar";
87
+ * import { wren } from "@voqalize/avatar/faces/wren";
88
+ *
89
+ * createAvatar({ mount, client, face: wren });
90
+ */
91
+ export interface SvgAvatarOptions extends AvatarOptions {
92
+ /** Defaults to `peep`, the face this entry point already carries. */
93
+ readonly face?: Face;
94
+ /** Palette for that face. Its keys are the face's own; see `THEME` in its
95
+ * module, and CLAUDE.md on why `peep` has exactly one. */
96
+ readonly theme?: FaceTheme;
97
+ /** Viseme amplitude, 0..2. `1` is as authored. */
98
+ readonly mouthGain?: Gain;
99
+ /** Gesture-clip amplitude, 0..2. */
100
+ readonly gestureGain?: Gain;
101
+ /** Idle/liveness amplitude, 0..2. Low by design — see CLAUDE.md. */
102
+ readonly motionGain?: Gain;
103
+ /** Render the frame-edge hand at all. Default true. */
104
+ readonly hand?: boolean;
105
+ /** `1` puts it on the viewer's right, `-1` the other side. */
106
+ readonly handSide?: HandSide;
107
+ }
108
+
109
+ const GAINS = ["mouthGain", "gestureGain", "motionGain"] as const;
110
+
111
+ export function createAvatar(options: SvgAvatarOptions): AvatarInstance {
112
+ const { mount, client, face = peep, ...rest } = options;
113
+ if (!mount) throw new TypeError("createAvatar: `mount` is required");
114
+ if (!client) throw new TypeError("createAvatar: `client` is required");
115
+ // The one range TypeScript cannot state, checked where it is cheapest to fix:
116
+ // at construction, by the caller who typed the number.
117
+ for (const key of GAINS) {
118
+ const g = rest[key];
119
+ if (g === undefined) continue;
120
+ if (!Number.isFinite(g) || g < 0 || g > 2) {
121
+ throw new RangeError(`createAvatar: \`${key}\` must be 0..2, got ${String(g)}`);
122
+ }
123
+ }
124
+
125
+ const widget = createSvgWidget({ mount, face, ...rest });
126
+ const driver = new AvatarClient(widget);
127
+ const detach = driver.attach(client);
128
+
129
+ let destroyed = false;
130
+ return {
131
+ destroy() {
132
+ if (destroyed) return;
133
+ destroyed = true;
134
+ detach();
135
+ driver.destroy();
136
+ widget.destroy();
137
+ },
138
+ };
139
+ }
@@ -0,0 +1,72 @@
1
+ /** Shared private constructor for the code-authored canvas avatars. */
2
+
3
+ import type { PipecatClient } from '@pipecat-ai/client-js';
4
+ import { createAvatar as createMixer, STATES } from '../src/avatar.js';
5
+ import type { Gain } from '../src/avatar.js';
6
+ import { createCanvasRig } from '../src/canvas/create-rig.js';
7
+ import { AvatarClient } from './AvatarClient.js';
8
+ import type { AvatarInstance, AvatarOptions } from './createAvatar.js';
9
+
10
+ export interface CanvasAvatarOptions extends AvatarOptions {
11
+ /** Viseme amplitude, 0..2. `1` is as authored. */
12
+ readonly mouthGain?: Gain;
13
+ /** Gesture-clip amplitude, 0..2. */
14
+ readonly gestureGain?: Gain;
15
+ /** Idle/liveness amplitude, 0..2. */
16
+ readonly motionGain?: Gain;
17
+ }
18
+
19
+ /** @deprecated Use `CanvasAvatarOptions`; kept for the original entry points. */
20
+ export type InterviewerAvatarOptions = CanvasAvatarOptions;
21
+
22
+ interface CanvasAvatarConfig {
23
+ readonly rigUrl: URL;
24
+ readonly images: Readonly<Record<string, URL>>;
25
+ readonly face: () => Promise<unknown>;
26
+ readonly label: string;
27
+ }
28
+
29
+ const GAINS = ['mouthGain', 'gestureGain', 'motionGain'] as const;
30
+ const PRESENCE_FILTERS: Readonly<Record<string, string>> = {
31
+ DEGRADED: String(STATES.DEGRADED.filter ?? ''),
32
+ OFFLINE: String(STATES.OFFLINE.filter ?? ''),
33
+ };
34
+
35
+ export function createCanvasAvatar(
36
+ options: CanvasAvatarOptions,
37
+ config: CanvasAvatarConfig,
38
+ ): AvatarInstance {
39
+ const { mount, client, ...gains } = options;
40
+ if (!mount) throw new TypeError('createAvatar: `mount` is required');
41
+ if (!client) throw new TypeError('createAvatar: `client` is required');
42
+ for (const key of GAINS) {
43
+ const gain = gains[key];
44
+ if (gain === undefined) continue;
45
+ if (!Number.isFinite(gain) || gain < 0 || gain > 2) {
46
+ throw new RangeError(`createAvatar: \`${key}\` must be 0..2, got ${String(gain)}`);
47
+ }
48
+ }
49
+
50
+ const canvas = createCanvasRig({
51
+ url: config.rigUrl,
52
+ images: config.images,
53
+ face: config.face,
54
+ label: config.label,
55
+ });
56
+ const widget = createMixer({ mount, rig: canvas.factory, ...gains });
57
+ const driver = new AvatarClient(widget, {
58
+ onPresenceChange: (state) => canvas.setPresenceFilter(PRESENCE_FILTERS[state] ?? ''),
59
+ });
60
+ const detach = driver.attach(client as PipecatClient);
61
+
62
+ let destroyed = false;
63
+ return {
64
+ destroy() {
65
+ if (destroyed) return;
66
+ destroyed = true;
67
+ detach();
68
+ driver.destroy();
69
+ widget.destroy();
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `@voqalize/avatar` — a talking head that embodies a `PipecatClient`.
3
+ *
4
+ * import { createAvatar } from "@voqalize/avatar";
5
+ *
6
+ * const avatar = createAvatar({ mount: el, client: pipecatClient });
7
+ *
8
+ * That is the whole public surface. Put an `AvatarProcessor` in the pipecat
9
+ * pipeline (`pip install voqalize-avatar`), mount this in the bot's tile, and
10
+ * the face listens, thinks, claims the floor and lipsyncs what the TTS says.
11
+ *
12
+ * Entry points, and the split is the design rather than packaging taste:
13
+ *
14
+ * - `@voqalize/avatar` — this one. Framework-free. `@pipecat-ai/client-js` is
15
+ * a type-only import, so even that peer is genuinely optional at runtime.
16
+ * It carries exactly one drawing: `peep`, the default face.
17
+ * - `@voqalize/avatar/faces/{peep,wren,myna}` — one drawing each. Import the
18
+ * one you want and pass it as `face`; the others never enter your bundle.
19
+ * - `@voqalize/avatar/react` — `<Avatar>`. Pulls React; nothing here does.
20
+ * - `@voqalize/avatar/internal` — the SVG widget, the behavior catalog and the
21
+ * viseme clock. **No semver promise**: these move in any minor. They are
22
+ * exported because an avatar author building on our renderer needs them, not
23
+ * because they are an interface.
24
+ *
25
+ * To ship your own avatar, publish a module exporting `createAvatar` and import
26
+ * that instead — see `createAvatar.ts` and docs/design-avatar-interface.md.
27
+ */
28
+
29
+ export { createAvatar } from "./createAvatar.js";
30
+ export type {
31
+ AvatarOptions,
32
+ AvatarInstance,
33
+ AvatarFactory,
34
+ SvgAvatarOptions,
35
+ Face,
36
+ FaceTheme,
37
+ Gain,
38
+ HandSide,
39
+ } from "./createAvatar.js";
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `@voqalize/avatar/internal` — the parts of our own implementation that
3
+ * another avatar can build on.
4
+ *
5
+ * **Nothing here is covered by semver.** Names, signatures and semantics move
6
+ * in any minor release. It is exported because an avatar author who wants the
7
+ * SVG mixer, the behavior catalog or — most usefully — the viseme clock should
8
+ * not have to vendor a copy, not because any of it is an interface. The
9
+ * interface is `createAvatar`, and it is one function.
10
+ *
11
+ * The one thing here worth reaching for on purpose is `VisemeTrack`: someone
12
+ * has to turn a cue array plus a clock into "which mouth shape is on screen
13
+ * right now", every renderer needs exactly that, and it is a solved problem.
14
+ *
15
+ * const track = new VisemeTrack();
16
+ * track.start(cues, () => performance.now() - t0);
17
+ * // per frame:
18
+ * const s = track.sample(); // { letter: "D", intensity: 1 } | null
19
+ *
20
+ * `docs/internal-rig.md` describes the pose-channel model the bundled SVG
21
+ * renderer uses internally. It is *not* the seam to implement — see
22
+ * docs/design-avatar-interface.md.
23
+ */
24
+
25
+ export {
26
+ // The bundled SVG widget. Not the faces: importing one from here would put
27
+ // all three in every bundle that wanted the viseme clock.
28
+ // `@voqalize/avatar/faces/<name>` is where a face comes from.
29
+ createAvatar as createSvgAvatar,
30
+ STATES,
31
+ STATE_NAMES,
32
+ ACTIONS,
33
+ ACTION_IDS,
34
+ GAZE_NAMES,
35
+ GAZE_TARGETS,
36
+ EMOTION_NAMES,
37
+ // The viseme clock and its tables.
38
+ VisemeTrack,
39
+ VISEME_LETTERS,
40
+ VISEME_SHAPES,
41
+ SILENT,
42
+ LEAD_MS,
43
+ shapeFor,
44
+ normalizeCues,
45
+ textToCues,
46
+ ARPABET_TO_VISEME,
47
+ AZURE_VISEME_TO_LETTER,
48
+ } from "../src/avatar.js";
49
+
50
+ export type {
51
+ AvatarApi,
52
+ AvatarStateName,
53
+ AvatarActionId,
54
+ AvatarGazeName,
55
+ AvatarEmotionName,
56
+ AvatarAction,
57
+ AvatarMeta,
58
+ VisemeLetter,
59
+ Cue,
60
+ PoseChannel,
61
+ PoseOverrides,
62
+ CreateAvatarOptions as CreateSvgAvatarOptions,
63
+ } from "../src/avatar.js";
64
+
65
+ export {
66
+ BEHAVIOR_STATES,
67
+ BEHAVIOR_STATE_IDS,
68
+ BEHAVIOR_ACTIONS,
69
+ BEHAVIOR_ACTION_IDS,
70
+ WIRE_ACTION_TO_BEHAVIOR,
71
+ } from "../src/behavior.js";
72
+
73
+ export type { BehaviorStateId, BehaviorActionId } from "../src/behavior.js";
74
+
75
+ export { isAvatarMessage } from "./types.js";
76
+ export type {
77
+ AvatarCommand,
78
+ AvatarCue,
79
+ AvatarCuesCmd,
80
+ } from "./types.js";
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `meera`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './meera.js';
4
+ export type { CanvasAvatarOptions, CanvasAvatarOptions as InterviewerAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `arjun`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './arjun.js';
4
+ export type { CanvasAvatarOptions, CanvasAvatarOptions as InterviewerAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,24 @@
1
+ /** Polished professional female avatar, authored to read at call-tile size. */
2
+
3
+ import { createCanvasAvatar } from './createCanvasAvatar.js';
4
+ import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
+ import type { AvatarInstance } from './createAvatar.js';
6
+
7
+ const RIG_URL = new URL('../src/canvas/data/professional-female-a.rig.json', import.meta.url);
8
+ const IMAGES = {
9
+ 'professional-female-a-top-body.webp': new URL('../src/canvas/data/img/professional-female-a-top-body.webp', import.meta.url),
10
+ 'professional-female-a-hair-back.webp': new URL('../src/canvas/data/img/professional-female-a-hair-back.webp', import.meta.url),
11
+ 'professional-female-a-hair-front.webp': new URL('../src/canvas/data/img/professional-female-a-hair-front.webp', import.meta.url),
12
+ } as const;
13
+ const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
+
15
+ export type { CanvasAvatarOptions };
16
+
17
+ export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
18
+ return createCanvasAvatar(options, {
19
+ rigUrl: RIG_URL,
20
+ images: IMAGES,
21
+ face: FACE,
22
+ label: 'Ishita — polished professional female avatar',
23
+ });
24
+ }
@@ -0,0 +1,24 @@
1
+ /** Relaxed professional male avatar, authored to read at call-tile size. */
2
+
3
+ import { createCanvasAvatar } from './createCanvasAvatar.js';
4
+ import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
+ import type { AvatarInstance } from './createAvatar.js';
6
+
7
+ const RIG_URL = new URL('../src/canvas/data/professional-male-b.rig.json', import.meta.url);
8
+ const IMAGES = {
9
+ 'professional-male-b-top-body.webp': new URL('../src/canvas/data/img/professional-male-b-top-body.webp', import.meta.url),
10
+ 'professional-male-b-hair-back.webp': new URL('../src/canvas/data/img/professional-male-b-hair-back.webp', import.meta.url),
11
+ 'professional-male-b-hair-front.webp': new URL('../src/canvas/data/img/professional-male-b-hair-front.webp', import.meta.url),
12
+ } as const;
13
+ const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
+
15
+ export type { CanvasAvatarOptions };
16
+
17
+ export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
18
+ return createCanvasAvatar(options, {
19
+ rigUrl: RIG_URL,
20
+ images: IMAGES,
21
+ face: FACE,
22
+ label: 'Kabir — relaxed professional male avatar',
23
+ });
24
+ }
@@ -0,0 +1,24 @@
1
+ /** Professional Indian female interviewer, authored to read at call-tile size. */
2
+
3
+ import { createCanvasAvatar } from './createCanvasAvatar.js';
4
+ import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
+ import type { AvatarInstance } from './createAvatar.js';
6
+
7
+ const RIG_URL = new URL('../src/canvas/data/interviewer-female.rig.json', import.meta.url);
8
+ const IMAGES = {
9
+ 'round-w1-hair-back.webp': new URL('../src/canvas/data/img/round-w1-hair-back.webp', import.meta.url),
10
+ 'round-w1-top-body.webp': new URL('../src/canvas/data/img/round-w1-top-body.webp', import.meta.url),
11
+ 'round-w1-hair-front.webp': new URL('../src/canvas/data/img/round-w1-hair-front.webp', import.meta.url),
12
+ } as const;
13
+ const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
+
15
+ export type { CanvasAvatarOptions };
16
+
17
+ export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
18
+ return createCanvasAvatar(options, {
19
+ rigUrl: RIG_URL,
20
+ images: IMAGES,
21
+ face: FACE,
22
+ label: 'Meera — professional female interviewer avatar',
23
+ });
24
+ }
@@ -0,0 +1,24 @@
1
+ /** Relaxed professional female avatar, authored to read at call-tile size. */
2
+
3
+ import { createCanvasAvatar } from './createCanvasAvatar.js';
4
+ import type { CanvasAvatarOptions } from './createCanvasAvatar.js';
5
+ import type { AvatarInstance } from './createAvatar.js';
6
+
7
+ const RIG_URL = new URL('../src/canvas/data/professional-female-b.rig.json', import.meta.url);
8
+ const IMAGES = {
9
+ 'professional-female-b-top-body.webp': new URL('../src/canvas/data/img/professional-female-b-top-body.webp', import.meta.url),
10
+ 'professional-female-b-hair-back.webp': new URL('../src/canvas/data/img/professional-female-b-hair-back.webp', import.meta.url),
11
+ 'professional-female-b-hair-front.webp': new URL('../src/canvas/data/img/professional-female-b-hair-front.webp', import.meta.url),
12
+ } as const;
13
+ const FACE = () => import('../src/canvas/avatars/round/face.mjs');
14
+
15
+ export type { CanvasAvatarOptions };
16
+
17
+ export function createAvatar(options: CanvasAvatarOptions): AvatarInstance {
18
+ return createCanvasAvatar(options, {
19
+ rigUrl: RIG_URL,
20
+ images: IMAGES,
21
+ face: FACE,
22
+ label: 'Naina — relaxed professional female avatar',
23
+ });
24
+ }
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `ishita`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './ishita.js';
4
+ export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `naina`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './naina.js';
4
+ export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `vikram`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './vikram.js';
4
+ export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,4 @@
1
+ /** @deprecated Renamed to `kabir`; kept as an alias so existing imports keep working. */
2
+
3
+ export { createAvatar } from './kabir.js';
4
+ export type { CanvasAvatarOptions } from './createCanvasAvatar.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@voqalize/avatar/react` — the avatar as one component.
3
+ *
4
+ * import { Avatar } from "@voqalize/avatar/react";
5
+ *
6
+ * <Avatar client={session.client} className="avatar-tile" />
7
+ *
8
+ * Separate from the main entry so that `createAvatar` costs nothing to a
9
+ * caller who is not on React — React is an optional peer, and a barrel that
10
+ * re-exported this would make it a hard one.
11
+ */
12
+
13
+ export { Avatar, type AvatarProps } from "./Avatar.js";
@@ -0,0 +1,146 @@
1
+ /**
2
+ * types.ts — the avatar wire vocabulary, client side.
3
+ *
4
+ * The binding definition is `docs/contract-wire.md`; this file is its
5
+ * TypeScript restatement and must not drift from it. The Python half of the
6
+ * same vocabulary is `packages/avatar-py/src/voqalize_avatar/messages.py` — the three are
7
+ * maintained together, and a command added to one without the others is
8
+ * incomplete.
9
+ *
10
+ * A server pushes these as RTVI `server-message`s under the envelope
11
+ * `{ type: "avatar", ...cmd-specific fields }`. {@link AvatarCommand} describes
12
+ * what rides inside that envelope; {@link isAvatarMessage} is the envelope
13
+ * itself, and is the only definition of "this message is for the avatar" the
14
+ * client has. There is no protocol version field — an unknown `cmd` is simply
15
+ * ignored, which is the whole forward-compatibility rule.
16
+ */
17
+
18
+ import { ACTION_IDS, VISEME_LETTERS } from "../src/avatar.js";
19
+ import type { AvatarActionId, VisemeLetter } from "../src/avatar.js";
20
+
21
+ /** A viseme cue: `t` is a ms offset into the utterance's clock, `v` is a Rhubarb A–H (or X) letter. */
22
+ export interface AvatarCue {
23
+ t: number;
24
+ v: VisemeLetter;
25
+ /** 0..1 loudness. Absent means full. */
26
+ i?: number;
27
+ }
28
+
29
+ /** A durable, lower-priority server claim. `null` explicitly clears it. */
30
+ export interface AvatarClaimCmd {
31
+ cmd: "claim";
32
+ state: "STRAINING" | "THINKING" | "WORKING" | null;
33
+ }
34
+
35
+ /** A self-completing authored sequence: face, body, and optionally a hand. */
36
+ export interface AvatarActionCmd {
37
+ cmd: "action";
38
+ id: AvatarActionId;
39
+ }
40
+
41
+ export interface AvatarCuesCmd {
42
+ cmd: "cues";
43
+ ctx: string;
44
+ /** Discard queued cues at or after this offset (ms), then append `cues`. */
45
+ from_ms: number;
46
+ cues: AvatarCue[];
47
+ /**
48
+ * True on the one chunk that completes this turn's track: the TTS context is
49
+ * closed, so no further chunk will splice into `ctx`. What a client may
50
+ * assume, exactly — nothing about playout. The audio it describes is still
51
+ * ahead, and Pipecat's `botStoppedSpeaking` remains the end of the turn. It
52
+ * is safe to stop expecting more cue chunks after `final`.
53
+ *
54
+ * Absent on an interrupted turn, deliberately: a turn that was cut never
55
+ * claims to have completed. Absent chunks are the normal case — the widget's
56
+ * own track already completes on the trailing `X`, so ignoring `final`
57
+ * entirely is a correct implementation.
58
+ */
59
+ final?: boolean;
60
+ }
61
+
62
+ export type AvatarCommand =
63
+ | AvatarClaimCmd
64
+ | AvatarActionCmd
65
+ | AvatarCuesCmd;
66
+
67
+ /** The full server-message payload: the envelope plus its command. */
68
+ export type AvatarServerMessage = AvatarCommand & { type: "avatar" };
69
+
70
+ /** An envelope addressed to the avatar, before its payload has been read. */
71
+ export type AvatarEnvelope = { type: "avatar"; cmd: string } & Record<string, unknown>;
72
+
73
+ /**
74
+ * Is this server-message payload the avatar's? The envelope is the whole
75
+ * answer: `{type:"avatar"}` with a string `cmd`. It used to be a per-deployment
76
+ * `accept` predicate on the client, which meant the library could not state
77
+ * what an avatar message *is*.
78
+ *
79
+ * Addressed to us is not the same as understood by us: {@link parseAvatarCommand}
80
+ * is the second half.
81
+ */
82
+ export function isAvatarMessage(msg: unknown): msg is AvatarEnvelope {
83
+ if (typeof msg !== "object" || msg === null) return false;
84
+ const m = msg as Record<string, unknown>;
85
+ return m.type === AVATAR_MESSAGE_TYPE && typeof m.cmd === "string";
86
+ }
87
+
88
+ const ACTIONS = new Set<string>(ACTION_IDS);
89
+ const LETTERS = new Set<string>(VISEME_LETTERS);
90
+
91
+ /**
92
+ * Read an envelope's payload into the wire vocabulary, or `null` if this build
93
+ * cannot act on it — an unknown `cmd`, an id or claim outside the enum, a
94
+ * malformed `cues` chunk.
95
+ *
96
+ * `null` is the forward-compat rule with a type attached: a newer server
97
+ * talking to an older widget is *expected*, and the older widget ignores what
98
+ * it does not know rather than guessing. Doing the check here rather than at
99
+ * each use site is what lets the wire types be closed unions instead of
100
+ * `string` — the boundary is one function, so it can be the only place that
101
+ * has to be honest about untrusted input.
102
+ *
103
+ * Cues survive individually: one unrecognised letter in a chunk drops that cue,
104
+ * not the utterance around it. Losing a frame of articulation is a far smaller
105
+ * regression than losing a sentence of it.
106
+ */
107
+ export function parseAvatarCommand(msg: AvatarEnvelope): AvatarCommand | null {
108
+ switch (msg.cmd) {
109
+ case "claim": {
110
+ const state = msg.state;
111
+ if (state === null || state === "STRAINING" || state === "THINKING" || state === "WORKING") {
112
+ return { cmd: "claim", state };
113
+ }
114
+ return null;
115
+ }
116
+ case "action":
117
+ return typeof msg.id === "string" && ACTIONS.has(msg.id)
118
+ ? { cmd: "action", id: msg.id as AvatarActionId }
119
+ : null;
120
+ case "cues": {
121
+ if (typeof msg.ctx !== "string" || !Number.isFinite(msg.from_ms) || !Array.isArray(msg.cues)) {
122
+ return null;
123
+ }
124
+ const cues: AvatarCue[] = [];
125
+ for (const c of msg.cues as unknown[]) {
126
+ if (typeof c !== "object" || c === null) continue;
127
+ const { t, v, i } = c as Record<string, unknown>;
128
+ if (typeof t !== "number" || !Number.isFinite(t)) continue;
129
+ if (typeof v !== "string" || !LETTERS.has(v)) continue;
130
+ cues.push(typeof i === "number" ? { t, v: v as VisemeLetter, i } : { t, v: v as VisemeLetter });
131
+ }
132
+ return {
133
+ cmd: "cues",
134
+ ctx: msg.ctx,
135
+ from_ms: msg.from_ms as number,
136
+ cues,
137
+ ...(msg.final === true ? { final: true as const } : {}),
138
+ };
139
+ }
140
+ default:
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /** The envelope `type` the protocol reserves for avatar traffic. */
146
+ export const AVATAR_MESSAGE_TYPE = "avatar";