avatarlayer 0.1.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/dist/index.cjs +2165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +717 -0
- package/dist/index.d.ts +717 -0
- package/dist/index.js +2112 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +332 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.js +308 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react/index.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
AvatarContext: () => AvatarContext,
|
|
24
|
+
AvatarProvider: () => AvatarProvider,
|
|
25
|
+
AvatarView: () => AvatarView,
|
|
26
|
+
useAvatarSession: () => useAvatarSession
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(react_exports);
|
|
29
|
+
|
|
30
|
+
// src/react/AvatarProvider.tsx
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
|
|
33
|
+
// src/core/events.ts
|
|
34
|
+
var TypedEmitter = class {
|
|
35
|
+
listeners = /* @__PURE__ */ new Map();
|
|
36
|
+
on(event, fn) {
|
|
37
|
+
let set = this.listeners.get(event);
|
|
38
|
+
if (!set) {
|
|
39
|
+
set = /* @__PURE__ */ new Set();
|
|
40
|
+
this.listeners.set(event, set);
|
|
41
|
+
}
|
|
42
|
+
set.add(fn);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
off(event, fn) {
|
|
46
|
+
this.listeners.get(event)?.delete(fn);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
emit(event, ...args) {
|
|
50
|
+
const set = this.listeners.get(event);
|
|
51
|
+
if (!set) return;
|
|
52
|
+
for (const fn of set) {
|
|
53
|
+
try {
|
|
54
|
+
fn(...args);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
removeAllListeners() {
|
|
60
|
+
this.listeners.clear();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/core/session.ts
|
|
65
|
+
var SENTENCE_DELIMITERS = /(?<=[.!?。!?\n])\s+/;
|
|
66
|
+
function splitSentences(buffer) {
|
|
67
|
+
const parts = buffer.split(SENTENCE_DELIMITERS);
|
|
68
|
+
if (parts.length <= 1) return [[], buffer];
|
|
69
|
+
const remaining = parts.pop();
|
|
70
|
+
return [parts.filter((s) => s.trim().length > 0), remaining];
|
|
71
|
+
}
|
|
72
|
+
var AvatarSession = class extends TypedEmitter {
|
|
73
|
+
config;
|
|
74
|
+
_state = "idle";
|
|
75
|
+
history = [];
|
|
76
|
+
abortController = null;
|
|
77
|
+
transactionId = 0;
|
|
78
|
+
constructor(config) {
|
|
79
|
+
super();
|
|
80
|
+
this.config = config;
|
|
81
|
+
}
|
|
82
|
+
get state() {
|
|
83
|
+
return this._state;
|
|
84
|
+
}
|
|
85
|
+
get messages() {
|
|
86
|
+
return this.history;
|
|
87
|
+
}
|
|
88
|
+
/** Mount the renderer and transition to ready. */
|
|
89
|
+
async start(container) {
|
|
90
|
+
if (this._state !== "idle" && this._state !== "destroyed") return;
|
|
91
|
+
this.setState("connecting");
|
|
92
|
+
try {
|
|
93
|
+
await this.config.renderer.mount(container);
|
|
94
|
+
this.setState("ready");
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.setState("error");
|
|
97
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Send a user message and run the LLM → TTS → speak pipeline. */
|
|
101
|
+
async sendMessage(text) {
|
|
102
|
+
if (this._state !== "ready" && this._state !== "speaking") return;
|
|
103
|
+
this.interrupt();
|
|
104
|
+
const userMsg = {
|
|
105
|
+
role: "user",
|
|
106
|
+
content: text,
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
timestamp: Date.now()
|
|
109
|
+
};
|
|
110
|
+
this.history.push(userMsg);
|
|
111
|
+
this.emit("message", userMsg);
|
|
112
|
+
const txId = ++this.transactionId;
|
|
113
|
+
const ac = new AbortController();
|
|
114
|
+
this.abortController = ac;
|
|
115
|
+
this.setState("thinking");
|
|
116
|
+
const messagesForLLM = [];
|
|
117
|
+
if (this.config.systemPrompt) {
|
|
118
|
+
messagesForLLM.push({ role: "system", content: this.config.systemPrompt });
|
|
119
|
+
}
|
|
120
|
+
messagesForLLM.push(...this.history);
|
|
121
|
+
let fullText = "";
|
|
122
|
+
let buffer = "";
|
|
123
|
+
const assistantMsg = {
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: "",
|
|
126
|
+
id: crypto.randomUUID(),
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const stream = this.config.llm.chat(messagesForLLM, {
|
|
131
|
+
signal: ac.signal,
|
|
132
|
+
...this.config.reasoningEffort ? { reasoningEffort: this.config.reasoningEffort } : {}
|
|
133
|
+
});
|
|
134
|
+
for await (const chunk of stream) {
|
|
135
|
+
if (ac.signal.aborted || txId !== this.transactionId) return;
|
|
136
|
+
fullText += chunk.text;
|
|
137
|
+
buffer += chunk.text;
|
|
138
|
+
this.emit("chunk", chunk.text, fullText);
|
|
139
|
+
const [sentences, rest] = splitSentences(buffer);
|
|
140
|
+
buffer = rest;
|
|
141
|
+
for (const sentence of sentences) {
|
|
142
|
+
if (ac.signal.aborted || txId !== this.transactionId) return;
|
|
143
|
+
await this.synthesizeAndSpeak(sentence, ac.signal, txId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (buffer.trim() && !ac.signal.aborted && txId === this.transactionId) {
|
|
147
|
+
await this.synthesizeAndSpeak(buffer.trim(), ac.signal, txId);
|
|
148
|
+
}
|
|
149
|
+
if (!ac.signal.aborted && txId === this.transactionId) {
|
|
150
|
+
assistantMsg.content = fullText;
|
|
151
|
+
this.history.push(assistantMsg);
|
|
152
|
+
this.emit("message", assistantMsg);
|
|
153
|
+
this.setState("ready");
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (ac.signal.aborted) return;
|
|
157
|
+
this.setState("error");
|
|
158
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Cancel the current LLM/TTS/speak pipeline. */
|
|
162
|
+
interrupt() {
|
|
163
|
+
if (this.abortController) {
|
|
164
|
+
this.abortController.abort();
|
|
165
|
+
this.abortController = null;
|
|
166
|
+
}
|
|
167
|
+
this.config.renderer.interrupt();
|
|
168
|
+
if (this._state === "thinking" || this._state === "speaking") {
|
|
169
|
+
this.setState("ready");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Update the renderer with new avatar control state. */
|
|
173
|
+
updateControl(control) {
|
|
174
|
+
this.config.renderer.update(control);
|
|
175
|
+
}
|
|
176
|
+
/** Replace the LLM provider at runtime. */
|
|
177
|
+
setLLM(llm) {
|
|
178
|
+
this.config = { ...this.config, llm };
|
|
179
|
+
}
|
|
180
|
+
/** Replace the TTS provider at runtime. */
|
|
181
|
+
setTTS(tts) {
|
|
182
|
+
this.config = { ...this.config, tts };
|
|
183
|
+
}
|
|
184
|
+
/** Replace the renderer at runtime. Unmounts the old one. */
|
|
185
|
+
async setRenderer(renderer, container) {
|
|
186
|
+
this.interrupt();
|
|
187
|
+
this.config.renderer.unmount();
|
|
188
|
+
this.config = { ...this.config, renderer };
|
|
189
|
+
await renderer.mount(container);
|
|
190
|
+
}
|
|
191
|
+
/** Tear down everything. */
|
|
192
|
+
destroy() {
|
|
193
|
+
this.interrupt();
|
|
194
|
+
this.config.renderer.unmount();
|
|
195
|
+
this.removeAllListeners();
|
|
196
|
+
this.setState("destroyed");
|
|
197
|
+
}
|
|
198
|
+
// -- internals --
|
|
199
|
+
async synthesizeAndSpeak(text, signal, txId) {
|
|
200
|
+
if (signal.aborted || txId !== this.transactionId) return;
|
|
201
|
+
if (this.config.renderer.speakText) {
|
|
202
|
+
this.setState("speaking");
|
|
203
|
+
this.emit("speech-start");
|
|
204
|
+
try {
|
|
205
|
+
await this.config.renderer.speakText(text, signal);
|
|
206
|
+
} finally {
|
|
207
|
+
if (!signal.aborted && txId === this.transactionId) {
|
|
208
|
+
this.emit("speech-end");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!this.config.tts) {
|
|
214
|
+
throw new Error("TTSProvider is required when the renderer does not implement speakText");
|
|
215
|
+
}
|
|
216
|
+
const blob = await this.config.tts.synthesize(text, { signal });
|
|
217
|
+
if (signal.aborted || txId !== this.transactionId) return;
|
|
218
|
+
this.setState("speaking");
|
|
219
|
+
this.emit("speech-start");
|
|
220
|
+
try {
|
|
221
|
+
await this.config.renderer.speak(blob);
|
|
222
|
+
} finally {
|
|
223
|
+
if (!signal.aborted && txId === this.transactionId) {
|
|
224
|
+
this.emit("speech-end");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
setState(next) {
|
|
229
|
+
if (this._state === next) return;
|
|
230
|
+
this._state = next;
|
|
231
|
+
this.emit("state-change", next);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/react/AvatarProvider.tsx
|
|
236
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
237
|
+
var AvatarContext = (0, import_react.createContext)(null);
|
|
238
|
+
function AvatarProvider({ config, children }) {
|
|
239
|
+
const [state, setState] = (0, import_react.useState)("idle");
|
|
240
|
+
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
241
|
+
const sessionRef = (0, import_react.useRef)(null);
|
|
242
|
+
const configRef = (0, import_react.useRef)(config);
|
|
243
|
+
configRef.current = config;
|
|
244
|
+
(0, import_react.useEffect)(() => {
|
|
245
|
+
const s = new AvatarSession(config);
|
|
246
|
+
sessionRef.current = s;
|
|
247
|
+
s.on("state-change", (next) => setState(next));
|
|
248
|
+
s.on("message", () => setMessages([...s.messages]));
|
|
249
|
+
return () => {
|
|
250
|
+
s.destroy();
|
|
251
|
+
sessionRef.current = null;
|
|
252
|
+
};
|
|
253
|
+
}, []);
|
|
254
|
+
const mount = (0, import_react.useCallback)((container) => {
|
|
255
|
+
sessionRef.current?.start(container);
|
|
256
|
+
}, []);
|
|
257
|
+
const sendMessage = (0, import_react.useCallback)((text) => {
|
|
258
|
+
sessionRef.current?.sendMessage(text);
|
|
259
|
+
}, []);
|
|
260
|
+
const interrupt = (0, import_react.useCallback)(() => {
|
|
261
|
+
sessionRef.current?.interrupt();
|
|
262
|
+
}, []);
|
|
263
|
+
const setLLM = (0, import_react.useCallback)((llm) => {
|
|
264
|
+
sessionRef.current?.setLLM(llm);
|
|
265
|
+
}, []);
|
|
266
|
+
const setTTS = (0, import_react.useCallback)((tts) => {
|
|
267
|
+
sessionRef.current?.setTTS(tts);
|
|
268
|
+
}, []);
|
|
269
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
270
|
+
AvatarContext.Provider,
|
|
271
|
+
{
|
|
272
|
+
value: {
|
|
273
|
+
session: sessionRef.current,
|
|
274
|
+
state,
|
|
275
|
+
messages,
|
|
276
|
+
sendMessage,
|
|
277
|
+
interrupt,
|
|
278
|
+
mount,
|
|
279
|
+
setLLM,
|
|
280
|
+
setTTS
|
|
281
|
+
},
|
|
282
|
+
children
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/react/useAvatarSession.ts
|
|
288
|
+
var import_react2 = require("react");
|
|
289
|
+
function useAvatarSession() {
|
|
290
|
+
const ctx = (0, import_react2.useContext)(AvatarContext);
|
|
291
|
+
if (!ctx) {
|
|
292
|
+
throw new Error("useAvatarSession must be used within an <AvatarProvider>");
|
|
293
|
+
}
|
|
294
|
+
return ctx;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/react/AvatarView.tsx
|
|
298
|
+
var import_react3 = require("react");
|
|
299
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
300
|
+
function AvatarView(props) {
|
|
301
|
+
const { mount } = useAvatarSession();
|
|
302
|
+
const containerRef = (0, import_react3.useRef)(null);
|
|
303
|
+
const mountedRef = (0, import_react3.useRef)(false);
|
|
304
|
+
(0, import_react3.useEffect)(() => {
|
|
305
|
+
if (containerRef.current && !mountedRef.current) {
|
|
306
|
+
mountedRef.current = true;
|
|
307
|
+
mount(containerRef.current);
|
|
308
|
+
}
|
|
309
|
+
}, [mount]);
|
|
310
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
ref: containerRef,
|
|
314
|
+
...props,
|
|
315
|
+
style: {
|
|
316
|
+
width: "100%",
|
|
317
|
+
height: "100%",
|
|
318
|
+
position: "relative",
|
|
319
|
+
overflow: "hidden",
|
|
320
|
+
...props.style
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
326
|
+
0 && (module.exports = {
|
|
327
|
+
AvatarContext,
|
|
328
|
+
AvatarProvider,
|
|
329
|
+
AvatarView,
|
|
330
|
+
useAvatarSession
|
|
331
|
+
});
|
|
332
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/AvatarProvider.tsx","../../src/core/events.ts","../../src/core/session.ts","../../src/react/useAvatarSession.ts","../../src/react/AvatarView.tsx"],"sourcesContent":["export { AvatarProvider, AvatarContext, type AvatarContextValue } from \"./AvatarProvider.js\";\nexport { useAvatarSession } from \"./useAvatarSession.js\";\nexport { AvatarView } from \"./AvatarView.js\";\n","\"use client\";\n\nimport {\n createContext,\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { AvatarSession } from \"../core/session.js\";\nimport type {\n AvatarSessionConfig,\n ChatMessage,\n SessionState,\n} from \"../types.js\";\n\nexport interface AvatarContextValue {\n session: AvatarSession | null;\n state: SessionState;\n messages: ChatMessage[];\n sendMessage: (text: string) => void;\n interrupt: () => void;\n /** Mount the renderer into the given container (called by <AvatarView>). */\n mount: (container: HTMLElement) => void;\n setLLM: (llm: AvatarSessionConfig[\"llm\"]) => void;\n setTTS: (tts: AvatarSessionConfig[\"tts\"]) => void;\n}\n\nexport const AvatarContext = createContext<AvatarContextValue | null>(null);\n\ninterface AvatarProviderProps {\n config: AvatarSessionConfig;\n children: ReactNode;\n}\n\nexport function AvatarProvider({ config, children }: AvatarProviderProps) {\n const [state, setState] = useState<SessionState>(\"idle\");\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const sessionRef = useRef<AvatarSession | null>(null);\n const configRef = useRef(config);\n configRef.current = config;\n\n useEffect(() => {\n const s = new AvatarSession(config);\n sessionRef.current = s;\n\n s.on(\"state-change\", (next) => setState(next));\n s.on(\"message\", () => setMessages([...s.messages]));\n\n return () => {\n s.destroy();\n sessionRef.current = null;\n };\n // Intentionally only created once; provider swaps use setLLM/setTTS.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const mount = useCallback((container: HTMLElement) => {\n sessionRef.current?.start(container);\n }, []);\n\n const sendMessage = useCallback((text: string) => {\n sessionRef.current?.sendMessage(text);\n }, []);\n\n const interrupt = useCallback(() => {\n sessionRef.current?.interrupt();\n }, []);\n\n const setLLM = useCallback((llm: AvatarSessionConfig[\"llm\"]) => {\n sessionRef.current?.setLLM(llm);\n }, []);\n\n const setTTS = useCallback((tts: AvatarSessionConfig[\"tts\"]) => {\n sessionRef.current?.setTTS(tts);\n }, []);\n\n return (\n <AvatarContext.Provider\n value={{\n session: sessionRef.current,\n state,\n messages,\n sendMessage,\n interrupt,\n mount,\n setLLM,\n setTTS,\n }}\n >\n {children}\n </AvatarContext.Provider>\n );\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Listener = (...args: any[]) => void;\n\n/**\n * Minimal typed event emitter. The generic parameter maps event names to\n * their handler signatures so callers get full type safety.\n */\nexport class TypedEmitter<\n EventMap extends Record<string, Listener>,\n> {\n private listeners = new Map<keyof EventMap, Set<Listener>>();\n\n on<K extends keyof EventMap>(event: K, fn: EventMap[K]): this {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(fn as Listener);\n return this;\n }\n\n off<K extends keyof EventMap>(event: K, fn: EventMap[K]): this {\n this.listeners.get(event)?.delete(fn as Listener);\n return this;\n }\n\n protected emit<K extends keyof EventMap>(\n event: K,\n ...args: Parameters<EventMap[K]>\n ): void {\n const set = this.listeners.get(event);\n if (!set) return;\n for (const fn of set) {\n try {\n fn(...args);\n } catch {\n // swallow listener errors to avoid breaking the emitter loop\n }\n }\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","import { TypedEmitter } from \"./events.js\";\nimport type {\n AvatarSessionConfig,\n AvatarSessionEventMap,\n ChatMessage,\n SessionState,\n} from \"../types.js\";\n\nconst SENTENCE_DELIMITERS = /(?<=[.!?。!?\\n])\\s+/;\n\n/**\n * Split streaming text into sentence-sized segments suitable for TTS.\n * Returns [completed segments, remaining buffer].\n */\nfunction splitSentences(buffer: string): [string[], string] {\n const parts = buffer.split(SENTENCE_DELIMITERS);\n if (parts.length <= 1) return [[], buffer];\n const remaining = parts.pop()!;\n return [parts.filter((s) => s.trim().length > 0), remaining];\n}\n\n/**\n * AvatarSession orchestrates the realtime conversational avatar pipeline:\n * user text ──▶ LLM stream ──▶ sentence split ──▶ TTS ──▶ renderer.speak\n *\n * Supports interruption at every stage via AbortController.\n */\nexport class AvatarSession extends TypedEmitter<AvatarSessionEventMap> {\n private config: AvatarSessionConfig;\n private _state: SessionState = \"idle\";\n private history: ChatMessage[] = [];\n private abortController: AbortController | null = null;\n private transactionId = 0;\n\n constructor(config: AvatarSessionConfig) {\n super();\n this.config = config;\n }\n\n get state(): SessionState {\n return this._state;\n }\n\n get messages(): readonly ChatMessage[] {\n return this.history;\n }\n\n /** Mount the renderer and transition to ready. */\n async start(container: HTMLElement): Promise<void> {\n if (this._state !== \"idle\" && this._state !== \"destroyed\") return;\n this.setState(\"connecting\");\n try {\n await this.config.renderer.mount(container);\n this.setState(\"ready\");\n } catch (err) {\n this.setState(\"error\");\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n /** Send a user message and run the LLM → TTS → speak pipeline. */\n async sendMessage(text: string): Promise<void> {\n if (this._state !== \"ready\" && this._state !== \"speaking\") return;\n\n this.interrupt();\n\n const userMsg: ChatMessage = {\n role: \"user\",\n content: text,\n id: crypto.randomUUID(),\n timestamp: Date.now(),\n };\n this.history.push(userMsg);\n this.emit(\"message\", userMsg);\n\n const txId = ++this.transactionId;\n const ac = new AbortController();\n this.abortController = ac;\n\n this.setState(\"thinking\");\n\n const messagesForLLM: ChatMessage[] = [];\n if (this.config.systemPrompt) {\n messagesForLLM.push({ role: \"system\", content: this.config.systemPrompt });\n }\n messagesForLLM.push(...this.history);\n\n let fullText = \"\";\n let buffer = \"\";\n const assistantMsg: ChatMessage = {\n role: \"assistant\",\n content: \"\",\n id: crypto.randomUUID(),\n timestamp: Date.now(),\n };\n\n try {\n const stream = this.config.llm.chat(messagesForLLM, {\n signal: ac.signal,\n ...(this.config.reasoningEffort\n ? { reasoningEffort: this.config.reasoningEffort }\n : {}),\n });\n\n for await (const chunk of stream) {\n if (ac.signal.aborted || txId !== this.transactionId) return;\n\n fullText += chunk.text;\n buffer += chunk.text;\n this.emit(\"chunk\", chunk.text, fullText);\n\n const [sentences, rest] = splitSentences(buffer);\n buffer = rest;\n\n for (const sentence of sentences) {\n if (ac.signal.aborted || txId !== this.transactionId) return;\n await this.synthesizeAndSpeak(sentence, ac.signal, txId);\n }\n }\n\n // Flush remaining buffer\n if (buffer.trim() && !ac.signal.aborted && txId === this.transactionId) {\n await this.synthesizeAndSpeak(buffer.trim(), ac.signal, txId);\n }\n\n if (!ac.signal.aborted && txId === this.transactionId) {\n assistantMsg.content = fullText;\n this.history.push(assistantMsg);\n this.emit(\"message\", assistantMsg);\n this.setState(\"ready\");\n }\n } catch (err) {\n if (ac.signal.aborted) return;\n this.setState(\"error\");\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n /** Cancel the current LLM/TTS/speak pipeline. */\n interrupt(): void {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n this.config.renderer.interrupt();\n if (\n this._state === \"thinking\" ||\n this._state === \"speaking\"\n ) {\n this.setState(\"ready\");\n }\n }\n\n /** Update the renderer with new avatar control state. */\n updateControl(control: Parameters<typeof this.config.renderer.update>[0]): void {\n this.config.renderer.update(control);\n }\n\n /** Replace the LLM provider at runtime. */\n setLLM(llm: AvatarSessionConfig[\"llm\"]): void {\n this.config = { ...this.config, llm };\n }\n\n /** Replace the TTS provider at runtime. */\n setTTS(tts: AvatarSessionConfig[\"tts\"]): void {\n this.config = { ...this.config, tts };\n }\n\n /** Replace the renderer at runtime. Unmounts the old one. */\n async setRenderer(\n renderer: AvatarSessionConfig[\"renderer\"],\n container: HTMLElement,\n ): Promise<void> {\n this.interrupt();\n this.config.renderer.unmount();\n this.config = { ...this.config, renderer };\n await renderer.mount(container);\n }\n\n /** Tear down everything. */\n destroy(): void {\n this.interrupt();\n this.config.renderer.unmount();\n this.removeAllListeners();\n this.setState(\"destroyed\");\n }\n\n // -- internals --\n\n private async synthesizeAndSpeak(\n text: string,\n signal: AbortSignal,\n txId: number,\n ): Promise<void> {\n if (signal.aborted || txId !== this.transactionId) return;\n\n if (this.config.renderer.speakText) {\n this.setState(\"speaking\");\n this.emit(\"speech-start\");\n try {\n await this.config.renderer.speakText(text, signal);\n } finally {\n if (!signal.aborted && txId === this.transactionId) {\n this.emit(\"speech-end\");\n }\n }\n return;\n }\n\n if (!this.config.tts) {\n throw new Error(\"TTSProvider is required when the renderer does not implement speakText\");\n }\n\n const blob = await this.config.tts.synthesize(text, { signal });\n if (signal.aborted || txId !== this.transactionId) return;\n\n this.setState(\"speaking\");\n this.emit(\"speech-start\");\n\n try {\n await this.config.renderer.speak(blob);\n } finally {\n if (!signal.aborted && txId === this.transactionId) {\n this.emit(\"speech-end\");\n }\n }\n }\n\n private setState(next: SessionState): void {\n if (this._state === next) return;\n this._state = next;\n this.emit(\"state-change\", next);\n }\n}\n","\"use client\";\n\nimport { useContext } from \"react\";\nimport { AvatarContext, type AvatarContextValue } from \"./AvatarProvider.js\";\n\nexport function useAvatarSession(): AvatarContextValue {\n const ctx = useContext(AvatarContext);\n if (!ctx) {\n throw new Error(\"useAvatarSession must be used within an <AvatarProvider>\");\n }\n return ctx;\n}\n","\"use client\";\n\nimport { useEffect, useRef, type HTMLAttributes } from \"react\";\nimport { useAvatarSession } from \"./useAvatarSession.js\";\n\ntype AvatarViewProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * Renders the avatar by mounting the session's renderer into a div.\n * Automatically calls `mount()` when the component is placed in the tree\n * and the AvatarProvider context is available.\n */\nexport function AvatarView(props: AvatarViewProps) {\n const { mount } = useAvatarSession();\n const containerRef = useRef<HTMLDivElement>(null);\n const mountedRef = useRef(false);\n\n useEffect(() => {\n if (containerRef.current && !mountedRef.current) {\n mountedRef.current = true;\n mount(containerRef.current);\n }\n }, [mount]);\n\n return (\n <div\n ref={containerRef}\n {...props}\n style={{\n width: \"100%\",\n height: \"100%\",\n position: \"relative\",\n overflow: \"hidden\",\n ...props.style,\n }}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAOO;;;ACFA,IAAM,eAAN,MAEL;AAAA,EACQ,YAAY,oBAAI,IAAmC;AAAA,EAE3D,GAA6B,OAAU,IAAuB;AAC5D,QAAI,MAAM,KAAK,UAAU,IAAI,KAAK;AAClC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,UAAU,IAAI,OAAO,GAAG;AAAA,IAC/B;AACA,QAAI,IAAI,EAAc;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,IAA8B,OAAU,IAAuB;AAC7D,SAAK,UAAU,IAAI,KAAK,GAAG,OAAO,EAAc;AAChD,WAAO;AAAA,EACT;AAAA,EAEU,KACR,UACG,MACG;AACN,UAAM,MAAM,KAAK,UAAU,IAAI,KAAK;AACpC,QAAI,CAAC,IAAK;AACV,eAAW,MAAM,KAAK;AACpB,UAAI;AACF,WAAG,GAAG,IAAI;AAAA,MACZ,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,qBAA2B;AACzB,SAAK,UAAU,MAAM;AAAA,EACvB;AACF;;;ACrCA,IAAM,sBAAsB;AAM5B,SAAS,eAAe,QAAoC;AAC1D,QAAM,QAAQ,OAAO,MAAM,mBAAmB;AAC9C,MAAI,MAAM,UAAU,EAAG,QAAO,CAAC,CAAC,GAAG,MAAM;AACzC,QAAM,YAAY,MAAM,IAAI;AAC5B,SAAO,CAAC,MAAM,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,GAAG,SAAS;AAC7D;AAQO,IAAM,gBAAN,cAA4B,aAAoC;AAAA,EAC7D;AAAA,EACA,SAAuB;AAAA,EACvB,UAAyB,CAAC;AAAA,EAC1B,kBAA0C;AAAA,EAC1C,gBAAgB;AAAA,EAExB,YAAY,QAA6B;AACvC,UAAM;AACN,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,IAAI,QAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,WAAmC;AACrC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,MAAM,WAAuC;AACjD,QAAI,KAAK,WAAW,UAAU,KAAK,WAAW,YAAa;AAC3D,SAAK,SAAS,YAAY;AAC1B,QAAI;AACF,YAAM,KAAK,OAAO,SAAS,MAAM,SAAS;AAC1C,WAAK,SAAS,OAAO;AAAA,IACvB,SAAS,KAAK;AACZ,WAAK,SAAS,OAAO;AACrB,WAAK,KAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IACxE;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,YAAY,MAA6B;AAC7C,QAAI,KAAK,WAAW,WAAW,KAAK,WAAW,WAAY;AAE3D,SAAK,UAAU;AAEf,UAAM,UAAuB;AAAA,MAC3B,MAAM;AAAA,MACN,SAAS;AAAA,MACT,IAAI,OAAO,WAAW;AAAA,MACtB,WAAW,KAAK,IAAI;AAAA,IACtB;AACA,SAAK,QAAQ,KAAK,OAAO;AACzB,SAAK,KAAK,WAAW,OAAO;AAE5B,UAAM,OAAO,EAAE,KAAK;AACpB,UAAM,KAAK,IAAI,gBAAgB;AAC/B,SAAK,kBAAkB;AAEvB,SAAK,SAAS,UAAU;AAExB,UAAM,iBAAgC,CAAC;AACvC,QAAI,KAAK,OAAO,cAAc;AAC5B,qBAAe,KAAK,EAAE,MAAM,UAAU,SAAS,KAAK,OAAO,aAAa,CAAC;AAAA,IAC3E;AACA,mBAAe,KAAK,GAAG,KAAK,OAAO;AAEnC,QAAI,WAAW;AACf,QAAI,SAAS;AACb,UAAM,eAA4B;AAAA,MAChC,MAAM;AAAA,MACN,SAAS;AAAA,MACT,IAAI,OAAO,WAAW;AAAA,MACtB,WAAW,KAAK,IAAI;AAAA,IACtB;AAEA,QAAI;AACF,YAAM,SAAS,KAAK,OAAO,IAAI,KAAK,gBAAgB;AAAA,QAClD,QAAQ,GAAG;AAAA,QACX,GAAI,KAAK,OAAO,kBACZ,EAAE,iBAAiB,KAAK,OAAO,gBAAgB,IAC/C,CAAC;AAAA,MACP,CAAC;AAED,uBAAiB,SAAS,QAAQ;AAChC,YAAI,GAAG,OAAO,WAAW,SAAS,KAAK,cAAe;AAEtD,oBAAY,MAAM;AAClB,kBAAU,MAAM;AAChB,aAAK,KAAK,SAAS,MAAM,MAAM,QAAQ;AAEvC,cAAM,CAAC,WAAW,IAAI,IAAI,eAAe,MAAM;AAC/C,iBAAS;AAET,mBAAW,YAAY,WAAW;AAChC,cAAI,GAAG,OAAO,WAAW,SAAS,KAAK,cAAe;AACtD,gBAAM,KAAK,mBAAmB,UAAU,GAAG,QAAQ,IAAI;AAAA,QACzD;AAAA,MACF;AAGA,UAAI,OAAO,KAAK,KAAK,CAAC,GAAG,OAAO,WAAW,SAAS,KAAK,eAAe;AACtE,cAAM,KAAK,mBAAmB,OAAO,KAAK,GAAG,GAAG,QAAQ,IAAI;AAAA,MAC9D;AAEA,UAAI,CAAC,GAAG,OAAO,WAAW,SAAS,KAAK,eAAe;AACrD,qBAAa,UAAU;AACvB,aAAK,QAAQ,KAAK,YAAY;AAC9B,aAAK,KAAK,WAAW,YAAY;AACjC,aAAK,SAAS,OAAO;AAAA,MACvB;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,GAAG,OAAO,QAAS;AACvB,WAAK,SAAS,OAAO;AACrB,WAAK,KAAK,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IACxE;AAAA,EACF;AAAA;AAAA,EAGA,YAAkB;AAChB,QAAI,KAAK,iBAAiB;AACxB,WAAK,gBAAgB,MAAM;AAC3B,WAAK,kBAAkB;AAAA,IACzB;AACA,SAAK,OAAO,SAAS,UAAU;AAC/B,QACE,KAAK,WAAW,cAChB,KAAK,WAAW,YAChB;AACA,WAAK,SAAS,OAAO;AAAA,IACvB;AAAA,EACF;AAAA;AAAA,EAGA,cAAc,SAAkE;AAC9E,SAAK,OAAO,SAAS,OAAO,OAAO;AAAA,EACrC;AAAA;AAAA,EAGA,OAAO,KAAuC;AAC5C,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,IAAI;AAAA,EACtC;AAAA;AAAA,EAGA,OAAO,KAAuC;AAC5C,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,IAAI;AAAA,EACtC;AAAA;AAAA,EAGA,MAAM,YACJ,UACA,WACe;AACf,SAAK,UAAU;AACf,SAAK,OAAO,SAAS,QAAQ;AAC7B,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,SAAS;AACzC,UAAM,SAAS,MAAM,SAAS;AAAA,EAChC;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,UAAU;AACf,SAAK,OAAO,SAAS,QAAQ;AAC7B,SAAK,mBAAmB;AACxB,SAAK,SAAS,WAAW;AAAA,EAC3B;AAAA;AAAA,EAIA,MAAc,mBACZ,MACA,QACA,MACe;AACf,QAAI,OAAO,WAAW,SAAS,KAAK,cAAe;AAEnD,QAAI,KAAK,OAAO,SAAS,WAAW;AAClC,WAAK,SAAS,UAAU;AACxB,WAAK,KAAK,cAAc;AACxB,UAAI;AACF,cAAM,KAAK,OAAO,SAAS,UAAU,MAAM,MAAM;AAAA,MACnD,UAAE;AACA,YAAI,CAAC,OAAO,WAAW,SAAS,KAAK,eAAe;AAClD,eAAK,KAAK,YAAY;AAAA,QACxB;AAAA,MACF;AACA;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,OAAO,KAAK;AACpB,YAAM,IAAI,MAAM,wEAAwE;AAAA,IAC1F;AAEA,UAAM,OAAO,MAAM,KAAK,OAAO,IAAI,WAAW,MAAM,EAAE,OAAO,CAAC;AAC9D,QAAI,OAAO,WAAW,SAAS,KAAK,cAAe;AAEnD,SAAK,SAAS,UAAU;AACxB,SAAK,KAAK,cAAc;AAExB,QAAI;AACF,YAAM,KAAK,OAAO,SAAS,MAAM,IAAI;AAAA,IACvC,UAAE;AACA,UAAI,CAAC,OAAO,WAAW,SAAS,KAAK,eAAe;AAClD,aAAK,KAAK,YAAY;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,SAAS,MAA0B;AACzC,QAAI,KAAK,WAAW,KAAM;AAC1B,SAAK,SAAS;AACd,SAAK,KAAK,gBAAgB,IAAI;AAAA,EAChC;AACF;;;AF1JI;AAlDG,IAAM,oBAAgB,4BAAyC,IAAI;AAOnE,SAAS,eAAe,EAAE,QAAQ,SAAS,GAAwB;AACxE,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAuB,MAAM;AACvD,QAAM,CAAC,UAAU,WAAW,QAAI,uBAAwB,CAAC,CAAC;AAC1D,QAAM,iBAAa,qBAA6B,IAAI;AACpD,QAAM,gBAAY,qBAAO,MAAM;AAC/B,YAAU,UAAU;AAEpB,8BAAU,MAAM;AACd,UAAM,IAAI,IAAI,cAAc,MAAM;AAClC,eAAW,UAAU;AAErB,MAAE,GAAG,gBAAgB,CAAC,SAAS,SAAS,IAAI,CAAC;AAC7C,MAAE,GAAG,WAAW,MAAM,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAElD,WAAO,MAAM;AACX,QAAE,QAAQ;AACV,iBAAW,UAAU;AAAA,IACvB;AAAA,EAGF,GAAG,CAAC,CAAC;AAEL,QAAM,YAAQ,0BAAY,CAAC,cAA2B;AACpD,eAAW,SAAS,MAAM,SAAS;AAAA,EACrC,GAAG,CAAC,CAAC;AAEL,QAAM,kBAAc,0BAAY,CAAC,SAAiB;AAChD,eAAW,SAAS,YAAY,IAAI;AAAA,EACtC,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAY,0BAAY,MAAM;AAClC,eAAW,SAAS,UAAU;AAAA,EAChC,GAAG,CAAC,CAAC;AAEL,QAAM,aAAS,0BAAY,CAAC,QAAoC;AAC9D,eAAW,SAAS,OAAO,GAAG;AAAA,EAChC,GAAG,CAAC,CAAC;AAEL,QAAM,aAAS,0BAAY,CAAC,QAAoC;AAC9D,eAAW,SAAS,OAAO,GAAG;AAAA,EAChC,GAAG,CAAC,CAAC;AAEL,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,OAAO;AAAA,QACL,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;AG5FA,IAAAA,gBAA2B;AAGpB,SAAS,mBAAuC;AACrD,QAAM,UAAM,0BAAW,aAAa;AACpC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,0DAA0D;AAAA,EAC5E;AACA,SAAO;AACT;;;ACTA,IAAAC,gBAAuD;AAuBnD,IAAAC,sBAAA;AAbG,SAAS,WAAW,OAAwB;AACjD,QAAM,EAAE,MAAM,IAAI,iBAAiB;AACnC,QAAM,mBAAe,sBAAuB,IAAI;AAChD,QAAM,iBAAa,sBAAO,KAAK;AAE/B,+BAAU,MAAM;AACd,QAAI,aAAa,WAAW,CAAC,WAAW,SAAS;AAC/C,iBAAW,UAAU;AACrB,YAAM,aAAa,OAAO;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACJ,GAAG;AAAA,MACJ,OAAO;AAAA,QACL,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,UAAU;AAAA,QACV,GAAG,MAAM;AAAA,MACX;AAAA;AAAA,EACF;AAEJ;","names":["import_react","import_react","import_jsx_runtime"]}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// src/react/AvatarProvider.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/core/events.ts
|
|
11
|
+
var TypedEmitter = class {
|
|
12
|
+
listeners = /* @__PURE__ */ new Map();
|
|
13
|
+
on(event, fn) {
|
|
14
|
+
let set = this.listeners.get(event);
|
|
15
|
+
if (!set) {
|
|
16
|
+
set = /* @__PURE__ */ new Set();
|
|
17
|
+
this.listeners.set(event, set);
|
|
18
|
+
}
|
|
19
|
+
set.add(fn);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
off(event, fn) {
|
|
23
|
+
this.listeners.get(event)?.delete(fn);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
emit(event, ...args) {
|
|
27
|
+
const set = this.listeners.get(event);
|
|
28
|
+
if (!set) return;
|
|
29
|
+
for (const fn of set) {
|
|
30
|
+
try {
|
|
31
|
+
fn(...args);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
removeAllListeners() {
|
|
37
|
+
this.listeners.clear();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/core/session.ts
|
|
42
|
+
var SENTENCE_DELIMITERS = /(?<=[.!?。!?\n])\s+/;
|
|
43
|
+
function splitSentences(buffer) {
|
|
44
|
+
const parts = buffer.split(SENTENCE_DELIMITERS);
|
|
45
|
+
if (parts.length <= 1) return [[], buffer];
|
|
46
|
+
const remaining = parts.pop();
|
|
47
|
+
return [parts.filter((s) => s.trim().length > 0), remaining];
|
|
48
|
+
}
|
|
49
|
+
var AvatarSession = class extends TypedEmitter {
|
|
50
|
+
config;
|
|
51
|
+
_state = "idle";
|
|
52
|
+
history = [];
|
|
53
|
+
abortController = null;
|
|
54
|
+
transactionId = 0;
|
|
55
|
+
constructor(config) {
|
|
56
|
+
super();
|
|
57
|
+
this.config = config;
|
|
58
|
+
}
|
|
59
|
+
get state() {
|
|
60
|
+
return this._state;
|
|
61
|
+
}
|
|
62
|
+
get messages() {
|
|
63
|
+
return this.history;
|
|
64
|
+
}
|
|
65
|
+
/** Mount the renderer and transition to ready. */
|
|
66
|
+
async start(container) {
|
|
67
|
+
if (this._state !== "idle" && this._state !== "destroyed") return;
|
|
68
|
+
this.setState("connecting");
|
|
69
|
+
try {
|
|
70
|
+
await this.config.renderer.mount(container);
|
|
71
|
+
this.setState("ready");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.setState("error");
|
|
74
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Send a user message and run the LLM → TTS → speak pipeline. */
|
|
78
|
+
async sendMessage(text) {
|
|
79
|
+
if (this._state !== "ready" && this._state !== "speaking") return;
|
|
80
|
+
this.interrupt();
|
|
81
|
+
const userMsg = {
|
|
82
|
+
role: "user",
|
|
83
|
+
content: text,
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
timestamp: Date.now()
|
|
86
|
+
};
|
|
87
|
+
this.history.push(userMsg);
|
|
88
|
+
this.emit("message", userMsg);
|
|
89
|
+
const txId = ++this.transactionId;
|
|
90
|
+
const ac = new AbortController();
|
|
91
|
+
this.abortController = ac;
|
|
92
|
+
this.setState("thinking");
|
|
93
|
+
const messagesForLLM = [];
|
|
94
|
+
if (this.config.systemPrompt) {
|
|
95
|
+
messagesForLLM.push({ role: "system", content: this.config.systemPrompt });
|
|
96
|
+
}
|
|
97
|
+
messagesForLLM.push(...this.history);
|
|
98
|
+
let fullText = "";
|
|
99
|
+
let buffer = "";
|
|
100
|
+
const assistantMsg = {
|
|
101
|
+
role: "assistant",
|
|
102
|
+
content: "",
|
|
103
|
+
id: crypto.randomUUID(),
|
|
104
|
+
timestamp: Date.now()
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
const stream = this.config.llm.chat(messagesForLLM, {
|
|
108
|
+
signal: ac.signal,
|
|
109
|
+
...this.config.reasoningEffort ? { reasoningEffort: this.config.reasoningEffort } : {}
|
|
110
|
+
});
|
|
111
|
+
for await (const chunk of stream) {
|
|
112
|
+
if (ac.signal.aborted || txId !== this.transactionId) return;
|
|
113
|
+
fullText += chunk.text;
|
|
114
|
+
buffer += chunk.text;
|
|
115
|
+
this.emit("chunk", chunk.text, fullText);
|
|
116
|
+
const [sentences, rest] = splitSentences(buffer);
|
|
117
|
+
buffer = rest;
|
|
118
|
+
for (const sentence of sentences) {
|
|
119
|
+
if (ac.signal.aborted || txId !== this.transactionId) return;
|
|
120
|
+
await this.synthesizeAndSpeak(sentence, ac.signal, txId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (buffer.trim() && !ac.signal.aborted && txId === this.transactionId) {
|
|
124
|
+
await this.synthesizeAndSpeak(buffer.trim(), ac.signal, txId);
|
|
125
|
+
}
|
|
126
|
+
if (!ac.signal.aborted && txId === this.transactionId) {
|
|
127
|
+
assistantMsg.content = fullText;
|
|
128
|
+
this.history.push(assistantMsg);
|
|
129
|
+
this.emit("message", assistantMsg);
|
|
130
|
+
this.setState("ready");
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (ac.signal.aborted) return;
|
|
134
|
+
this.setState("error");
|
|
135
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Cancel the current LLM/TTS/speak pipeline. */
|
|
139
|
+
interrupt() {
|
|
140
|
+
if (this.abortController) {
|
|
141
|
+
this.abortController.abort();
|
|
142
|
+
this.abortController = null;
|
|
143
|
+
}
|
|
144
|
+
this.config.renderer.interrupt();
|
|
145
|
+
if (this._state === "thinking" || this._state === "speaking") {
|
|
146
|
+
this.setState("ready");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Update the renderer with new avatar control state. */
|
|
150
|
+
updateControl(control) {
|
|
151
|
+
this.config.renderer.update(control);
|
|
152
|
+
}
|
|
153
|
+
/** Replace the LLM provider at runtime. */
|
|
154
|
+
setLLM(llm) {
|
|
155
|
+
this.config = { ...this.config, llm };
|
|
156
|
+
}
|
|
157
|
+
/** Replace the TTS provider at runtime. */
|
|
158
|
+
setTTS(tts) {
|
|
159
|
+
this.config = { ...this.config, tts };
|
|
160
|
+
}
|
|
161
|
+
/** Replace the renderer at runtime. Unmounts the old one. */
|
|
162
|
+
async setRenderer(renderer, container) {
|
|
163
|
+
this.interrupt();
|
|
164
|
+
this.config.renderer.unmount();
|
|
165
|
+
this.config = { ...this.config, renderer };
|
|
166
|
+
await renderer.mount(container);
|
|
167
|
+
}
|
|
168
|
+
/** Tear down everything. */
|
|
169
|
+
destroy() {
|
|
170
|
+
this.interrupt();
|
|
171
|
+
this.config.renderer.unmount();
|
|
172
|
+
this.removeAllListeners();
|
|
173
|
+
this.setState("destroyed");
|
|
174
|
+
}
|
|
175
|
+
// -- internals --
|
|
176
|
+
async synthesizeAndSpeak(text, signal, txId) {
|
|
177
|
+
if (signal.aborted || txId !== this.transactionId) return;
|
|
178
|
+
if (this.config.renderer.speakText) {
|
|
179
|
+
this.setState("speaking");
|
|
180
|
+
this.emit("speech-start");
|
|
181
|
+
try {
|
|
182
|
+
await this.config.renderer.speakText(text, signal);
|
|
183
|
+
} finally {
|
|
184
|
+
if (!signal.aborted && txId === this.transactionId) {
|
|
185
|
+
this.emit("speech-end");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!this.config.tts) {
|
|
191
|
+
throw new Error("TTSProvider is required when the renderer does not implement speakText");
|
|
192
|
+
}
|
|
193
|
+
const blob = await this.config.tts.synthesize(text, { signal });
|
|
194
|
+
if (signal.aborted || txId !== this.transactionId) return;
|
|
195
|
+
this.setState("speaking");
|
|
196
|
+
this.emit("speech-start");
|
|
197
|
+
try {
|
|
198
|
+
await this.config.renderer.speak(blob);
|
|
199
|
+
} finally {
|
|
200
|
+
if (!signal.aborted && txId === this.transactionId) {
|
|
201
|
+
this.emit("speech-end");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
setState(next) {
|
|
206
|
+
if (this._state === next) return;
|
|
207
|
+
this._state = next;
|
|
208
|
+
this.emit("state-change", next);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/react/AvatarProvider.tsx
|
|
213
|
+
import { jsx } from "react/jsx-runtime";
|
|
214
|
+
var AvatarContext = createContext(null);
|
|
215
|
+
function AvatarProvider({ config, children }) {
|
|
216
|
+
const [state, setState] = useState("idle");
|
|
217
|
+
const [messages, setMessages] = useState([]);
|
|
218
|
+
const sessionRef = useRef(null);
|
|
219
|
+
const configRef = useRef(config);
|
|
220
|
+
configRef.current = config;
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const s = new AvatarSession(config);
|
|
223
|
+
sessionRef.current = s;
|
|
224
|
+
s.on("state-change", (next) => setState(next));
|
|
225
|
+
s.on("message", () => setMessages([...s.messages]));
|
|
226
|
+
return () => {
|
|
227
|
+
s.destroy();
|
|
228
|
+
sessionRef.current = null;
|
|
229
|
+
};
|
|
230
|
+
}, []);
|
|
231
|
+
const mount = useCallback((container) => {
|
|
232
|
+
sessionRef.current?.start(container);
|
|
233
|
+
}, []);
|
|
234
|
+
const sendMessage = useCallback((text) => {
|
|
235
|
+
sessionRef.current?.sendMessage(text);
|
|
236
|
+
}, []);
|
|
237
|
+
const interrupt = useCallback(() => {
|
|
238
|
+
sessionRef.current?.interrupt();
|
|
239
|
+
}, []);
|
|
240
|
+
const setLLM = useCallback((llm) => {
|
|
241
|
+
sessionRef.current?.setLLM(llm);
|
|
242
|
+
}, []);
|
|
243
|
+
const setTTS = useCallback((tts) => {
|
|
244
|
+
sessionRef.current?.setTTS(tts);
|
|
245
|
+
}, []);
|
|
246
|
+
return /* @__PURE__ */ jsx(
|
|
247
|
+
AvatarContext.Provider,
|
|
248
|
+
{
|
|
249
|
+
value: {
|
|
250
|
+
session: sessionRef.current,
|
|
251
|
+
state,
|
|
252
|
+
messages,
|
|
253
|
+
sendMessage,
|
|
254
|
+
interrupt,
|
|
255
|
+
mount,
|
|
256
|
+
setLLM,
|
|
257
|
+
setTTS
|
|
258
|
+
},
|
|
259
|
+
children
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/react/useAvatarSession.ts
|
|
265
|
+
import { useContext } from "react";
|
|
266
|
+
function useAvatarSession() {
|
|
267
|
+
const ctx = useContext(AvatarContext);
|
|
268
|
+
if (!ctx) {
|
|
269
|
+
throw new Error("useAvatarSession must be used within an <AvatarProvider>");
|
|
270
|
+
}
|
|
271
|
+
return ctx;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/react/AvatarView.tsx
|
|
275
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
276
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
277
|
+
function AvatarView(props) {
|
|
278
|
+
const { mount } = useAvatarSession();
|
|
279
|
+
const containerRef = useRef2(null);
|
|
280
|
+
const mountedRef = useRef2(false);
|
|
281
|
+
useEffect2(() => {
|
|
282
|
+
if (containerRef.current && !mountedRef.current) {
|
|
283
|
+
mountedRef.current = true;
|
|
284
|
+
mount(containerRef.current);
|
|
285
|
+
}
|
|
286
|
+
}, [mount]);
|
|
287
|
+
return /* @__PURE__ */ jsx2(
|
|
288
|
+
"div",
|
|
289
|
+
{
|
|
290
|
+
ref: containerRef,
|
|
291
|
+
...props,
|
|
292
|
+
style: {
|
|
293
|
+
width: "100%",
|
|
294
|
+
height: "100%",
|
|
295
|
+
position: "relative",
|
|
296
|
+
overflow: "hidden",
|
|
297
|
+
...props.style
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
AvatarContext,
|
|
304
|
+
AvatarProvider,
|
|
305
|
+
AvatarView,
|
|
306
|
+
useAvatarSession
|
|
307
|
+
};
|
|
308
|
+
//# sourceMappingURL=index.js.map
|