@surfmate.team/digital-human-voice-call 0.1.0 → 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.
package/dist/index.d.ts CHANGED
@@ -24,6 +24,56 @@ type CallReply = {
24
24
  type BuildPrompt = (persona: CallPersona, history: readonly CallMessage[], userMessage: string) => string;
25
25
  /** Pure: the LLM's raw completion → { text, mood? }. */
26
26
  type ParseReply = (raw: string) => CallReply;
27
+ /**
28
+ * A streaming TTS sink. The call enqueues reply SENTENCES as they peel off the
29
+ * LLM token stream; the adapter fetches + schedules each for seamless playback,
30
+ * so audio starts on the first sentence (low latency) instead of after the whole
31
+ * reply. digital-human-voice's `createAzureStreamSpeak()` satisfies this
32
+ * structurally — pass it as the `speechQueue` prop to turn on streaming mode.
33
+ */
34
+ type SpeechQueuePort = {
35
+ /** Unlock/resume the audio output inside a user gesture (call on mount). */
36
+ primeAudio(): void;
37
+ /** Begin a fresh assistant turn (resets the playback timeline). */
38
+ startQueue(): void;
39
+ /** Add one finished sentence; fetched immediately, played in order. */
40
+ enqueueSentence(text: string): void;
41
+ /** No more sentences this turn; `onIdle` fires once playback drains. */
42
+ finishQueue(onIdle: () => void): void;
43
+ /** Interrupt: drop the queue and stop playback now. */
44
+ stopQueue(): void;
45
+ /** Mute the output without stopping (speaker toggle). Optional. */
46
+ setMuted?(muted: boolean): void;
47
+ /** The AnalyserNode the audio flows through, for a reactive waveform. Optional. */
48
+ getAnalyser?(): AnalyserNode | null;
49
+ };
50
+ /** A running mic session: pause while the AI speaks (anti-echo), resume to listen
51
+ * again, stop to release the device. */
52
+ type SttSession = {
53
+ pause(): void;
54
+ resume(): void;
55
+ stop(): void;
56
+ };
57
+ /**
58
+ * Pluggable speech-to-text. Omit → the built-in browser Web Speech API (free but
59
+ * unreliable: stops itself, weak for continuous dialogue, CN-blocked via Google).
60
+ * Inject one (e.g. an Azure STT mic: capture PCM + VAD endpointing → POST a WAV to
61
+ * a recognizer) for robust recognition. `start` opens the mic and calls
62
+ * `onUtterance(text)` after each endpointed segment; the returned session lets the
63
+ * call pause it while the assistant speaks.
64
+ */
65
+ type SttPort = {
66
+ start(handlers: {
67
+ onUtterance: (text: string) => void;
68
+ onError?: (msg: string) => void;
69
+ }): Promise<SttSession> | SttSession;
70
+ };
71
+ /**
72
+ * Streaming prompt builder — PLAIN TEXT output (no JSON wrapper), so reply tokens
73
+ * can be split into speakable sentences as they stream. Use this (not the JSON
74
+ * defaultBuildPrompt) whenever a `speechQueue` is wired.
75
+ */
76
+ declare const defaultStreamPrompt: BuildPrompt;
27
77
  /** Default prompt builder — mirrors mate's grokAI.buildPrompt (JSON {text, mood}). */
28
78
  declare const defaultBuildPrompt: BuildPrompt;
29
79
  /** Default reply parser — pulls the first JSON {text, mood}; falls back to raw + neutral. */
@@ -39,6 +89,18 @@ type Props = {
39
89
  readonly buildPrompt?: BuildPrompt | undefined;
40
90
  /** Override the reply parser (pure). Omit → built-in JSON {text, mood} default. */
41
91
  readonly parseReply?: ParseReply | undefined;
92
+ /**
93
+ * Inject a streaming TTS sink to turn on STREAMING mode: the reply is streamed
94
+ * from the LLM (ChatPort.stream) and spoken sentence-by-sentence as it arrives
95
+ * (low latency) instead of synthesized whole. When set, the default prompt
96
+ * switches to PLAIN-TEXT (defaultStreamPrompt). Omit → classic synth path.
97
+ */
98
+ readonly speechQueue?: SpeechQueuePort | undefined;
99
+ /**
100
+ * Pluggable speech-to-text. Omit → browser Web Speech API (free, but unreliable
101
+ * / CN-blocked). Inject (e.g. an Azure STT mic) for robust recognition.
102
+ */
103
+ readonly stt?: SttPort | undefined;
42
104
  /** Emitted after each completed turn (user said X → assistant replied Y). The
43
105
  * host persists it if it wants — voice-call itself stays in-memory. */
44
106
  readonly onTurn?: ((user: CallMessage, ai: CallReply) => void) | undefined;
@@ -54,6 +116,6 @@ type Props = {
54
116
  * The reply audio is routed through a Web Audio AnalyserNode so the Waveform
55
117
  * pulses with the actual voice. Pure ports/DI — chatPort + synth are injected.
56
118
  */
57
- declare function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, onTurn, onClose, speechLang }: Props): react_jsx_runtime.JSX.Element;
119
+ declare function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, speechQueue, stt, onTurn, onClose, speechLang }: Props): react_jsx_runtime.JSX.Element;
58
120
 
59
- export { type BuildPrompt, type CallMessage, type CallPersona, type CallReply, type ParseReply, VoiceCall, defaultBuildPrompt, defaultParseReply };
121
+ export { type BuildPrompt, type CallMessage, type CallPersona, type CallReply, type ParseReply, type SpeechQueuePort, type SttPort, type SttSession, VoiceCall, defaultBuildPrompt, defaultParseReply, defaultStreamPrompt };
package/dist/index.js CHANGED
@@ -1,9 +1,25 @@
1
1
  // src/ui/VoiceCall.tsx
2
- import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
2
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
3
3
  import { Mic, MicOff, Volume2, VolumeX, PhoneOff, Send, Subtitles, Loader2 } from "lucide-react";
4
+ import { streamOrComplete } from "@surfmate.team/digital-human-llm";
4
5
 
5
6
  // src/chat.ts
6
7
  var HISTORY_WINDOW = 5;
8
+ var defaultStreamPrompt = (persona, history, userMessage) => {
9
+ const historyContext = history.slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : persona.name}: ${m.content}`).join("\n");
10
+ return `You are ${persona.name}.
11
+
12
+ Character background: ${persona.description ?? ""}
13
+
14
+ Personality traits: ${persona.personality ?? ""}
15
+
16
+ Conversation history:
17
+ ${historyContext}
18
+
19
+ User: ${userMessage}
20
+
21
+ Reply as ${persona.name} in short, natural SPOKEN sentences (1-3 sentences unless more is clearly needed). Output PLAIN TEXT only \u2014 no JSON, no markdown, no surrounding quotes.`;
22
+ };
7
23
  var defaultBuildPrompt = (persona, history, userMessage) => {
8
24
  const historyContext = history.slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : persona.name}: ${m.content}`).join("\n");
9
25
  return `You are ${persona.name}.
@@ -41,6 +57,22 @@ var defaultParseReply = (raw) => {
41
57
  return { text: raw, mood: "neutral" };
42
58
  };
43
59
 
60
+ // src/calc/takeSentences.ts
61
+ var BOUNDARY = /([.!?。!?…]+["')\]」』】]*(?=\s))|(\n+)/g;
62
+ function takeSentences(buffer) {
63
+ const sentences = [];
64
+ let last = 0;
65
+ BOUNDARY.lastIndex = 0;
66
+ let m;
67
+ while (m = BOUNDARY.exec(buffer)) {
68
+ const end = m.index + m[0].length;
69
+ const chunk = buffer.slice(last, end).trim();
70
+ if (chunk) sentences.push(chunk);
71
+ last = end;
72
+ }
73
+ return { sentences, rest: buffer.slice(last) };
74
+ }
75
+
44
76
  // src/ui/useCallSpeech.ts
45
77
  import { useEffect, useRef, useState } from "react";
46
78
  function getCtor() {
@@ -117,16 +149,59 @@ function useCallSpeech(onUtterance, lang = "zh-CN") {
117
149
  return { supported, listening, draft, start, stop };
118
150
  }
119
151
 
152
+ // src/ui/useSegmentSpeech.ts
153
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
154
+ function useSegmentSpeech(stt, onUtterance) {
155
+ const [listening, setListening] = useState2(false);
156
+ const sessionRef = useRef2(null);
157
+ const openingRef = useRef2(false);
158
+ const wantRef = useRef2(false);
159
+ const onUttRef = useRef2(onUtterance);
160
+ onUttRef.current = onUtterance;
161
+ const start = () => {
162
+ if (!stt || wantRef.current) return;
163
+ wantRef.current = true;
164
+ setListening(true);
165
+ if (sessionRef.current) {
166
+ sessionRef.current.resume();
167
+ return;
168
+ }
169
+ if (openingRef.current) return;
170
+ openingRef.current = true;
171
+ Promise.resolve(stt.start({ onUtterance: (t) => onUttRef.current(t), onError: () => {
172
+ } })).then((session) => {
173
+ sessionRef.current = session;
174
+ if (!wantRef.current) session.pause();
175
+ }).catch(() => {
176
+ }).finally(() => {
177
+ openingRef.current = false;
178
+ });
179
+ };
180
+ const stop = () => {
181
+ wantRef.current = false;
182
+ setListening(false);
183
+ sessionRef.current?.pause();
184
+ };
185
+ useEffect2(
186
+ () => () => {
187
+ sessionRef.current?.stop();
188
+ sessionRef.current = null;
189
+ },
190
+ []
191
+ );
192
+ return { supported: !!stt, listening, draft: "", start, stop };
193
+ }
194
+
120
195
  // src/ui/Waveform.tsx
121
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
196
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
122
197
  import { jsx } from "react/jsx-runtime";
123
198
  function Waveform({ analyser, phase }) {
124
- const canvasRef = useRef2(null);
125
- const analyserRef = useRef2(analyser);
126
- const phaseRef = useRef2(phase);
199
+ const canvasRef = useRef3(null);
200
+ const analyserRef = useRef3(analyser);
201
+ const phaseRef = useRef3(phase);
127
202
  analyserRef.current = analyser;
128
203
  phaseRef.current = phase;
129
- useEffect2(() => {
204
+ useEffect3(() => {
130
205
  const canvas = canvasRef.current;
131
206
  const ctx = canvas?.getContext("2d");
132
207
  if (!canvas || !ctx) return;
@@ -186,24 +261,25 @@ var STATUS = {
186
261
  var ctlBtn = "flex h-12 w-12 items-center justify-center rounded-full transition";
187
262
  var ctlOn = "bg-white/90 text-slate-900 hover:bg-white";
188
263
  var ctlOff = "bg-red-600/90 text-white hover:bg-red-600";
189
- function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, onTurn, onClose, speechLang }) {
190
- const build = buildPrompt ?? defaultBuildPrompt;
264
+ function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, speechQueue, stt, onTurn, onClose, speechLang }) {
265
+ const streaming = !!speechQueue;
266
+ const build = buildPrompt ?? (streaming ? defaultStreamPrompt : defaultBuildPrompt);
191
267
  const parse = parseReply ?? defaultParseReply;
192
- const [callState, setCallState] = useState2("connecting");
193
- const [latestAi, setLatestAi] = useState2(persona.greeting ?? "");
194
- const [latestUser, setLatestUser] = useState2("");
195
- const [muted, setMuted] = useState2(false);
196
- const [speakerOn, setSpeakerOn] = useState2(true);
197
- const [showCaptions, setShowCaptions] = useState2(true);
198
- const [typed, setTyped] = useState2("");
199
- const [analyser, setAnalyser] = useState2(null);
200
- const historyRef = useRef3([]);
201
- const audioRef = useRef3(null);
202
- const audioCtxRef = useRef3(null);
203
- const gainRef = useRef3(null);
204
- const stateRef = useRef3("connecting");
205
- const speakerOnRef = useRef3(true);
206
- const bootedRef = useRef3(false);
268
+ const [callState, setCallState] = useState3("connecting");
269
+ const [latestAi, setLatestAi] = useState3(persona.greeting ?? "");
270
+ const [latestUser, setLatestUser] = useState3("");
271
+ const [muted, setMuted] = useState3(false);
272
+ const [speakerOn, setSpeakerOn] = useState3(true);
273
+ const [showCaptions, setShowCaptions] = useState3(true);
274
+ const [typed, setTyped] = useState3("");
275
+ const [analyser, setAnalyser] = useState3(null);
276
+ const historyRef = useRef4([]);
277
+ const audioRef = useRef4(null);
278
+ const audioCtxRef = useRef4(null);
279
+ const gainRef = useRef4(null);
280
+ const stateRef = useRef4("connecting");
281
+ const speakerOnRef = useRef4(true);
282
+ const bootedRef = useRef4(false);
207
283
  stateRef.current = callState;
208
284
  speakerOnRef.current = speakerOn;
209
285
  const playAudio = (bytes) => new Promise((resolve) => {
@@ -257,6 +333,38 @@ function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPromp
257
333
  await new Promise((r) => setTimeout(r, Math.min(6e3, 1200 + text.length * 60)));
258
334
  }
259
335
  };
336
+ const streamSpeak = async (prompt, onText) => {
337
+ const q = speechQueue;
338
+ q.startQueue();
339
+ setCallState("speaking");
340
+ let acc = "";
341
+ let buf = "";
342
+ let primed = false;
343
+ const primeWaveform = () => {
344
+ if (primed) return;
345
+ primed = true;
346
+ setTimeout(() => setAnalyser(q.getAnalyser?.() ?? null), 180);
347
+ };
348
+ for await (const delta of streamOrComplete(chatPort, prompt)) {
349
+ acc += delta;
350
+ buf += delta;
351
+ const { sentences, rest } = takeSentences(buf);
352
+ buf = rest;
353
+ for (const s of sentences) {
354
+ q.enqueueSentence(s);
355
+ primeWaveform();
356
+ }
357
+ onText(acc);
358
+ }
359
+ const tail = buf.trim();
360
+ if (tail) {
361
+ q.enqueueSentence(tail);
362
+ primeWaveform();
363
+ }
364
+ await new Promise((res) => q.finishQueue(res));
365
+ setAnalyser(null);
366
+ return acc.trim();
367
+ };
260
368
  const handleUtterance = async (text) => {
261
369
  const t = text.trim();
262
370
  if (!t || !chatPort) return;
@@ -268,40 +376,61 @@ function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPromp
268
376
  const userMsg = { id: crypto.randomUUID(), role: "user", content: t };
269
377
  historyRef.current = [...history, userMsg];
270
378
  try {
271
- const raw = await chatPort.complete(build(persona, history, t));
272
- const reply = parse(raw);
273
- historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: "assistant", content: reply.text }];
274
- setLatestAi(reply.text);
275
- onTurn?.(userMsg, reply);
276
- await speakReply(reply.text, reply.mood);
379
+ if (streaming) {
380
+ const replyText = await streamSpeak(build(persona, history, t), setLatestAi);
381
+ historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: "assistant", content: replyText }];
382
+ onTurn?.(userMsg, { text: replyText });
383
+ } else {
384
+ const raw = await chatPort.complete(build(persona, history, t));
385
+ const reply = parse(raw);
386
+ historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: "assistant", content: reply.text }];
387
+ setLatestAi(reply.text);
388
+ onTurn?.(userMsg, reply);
389
+ await speakReply(reply.text, reply.mood);
390
+ }
277
391
  } catch (err) {
278
392
  setLatestAi(`(\u51FA\u9519:${err instanceof Error ? err.message : String(err)})`);
279
393
  } finally {
280
394
  setCallState("listening");
281
395
  }
282
396
  };
283
- const speech = useCallSpeech(handleUtterance, speechLang);
284
- useEffect3(() => {
397
+ const browserSpeech = useCallSpeech(handleUtterance, speechLang);
398
+ const azureSpeech = useSegmentSpeech(stt, handleUtterance);
399
+ const speech = stt ? azureSpeech : browserSpeech;
400
+ useEffect4(() => {
285
401
  if (bootedRef.current) return;
286
402
  bootedRef.current = true;
287
403
  void (async () => {
288
404
  try {
289
- if (persona.greeting && synth && voiceId) await speakReply(persona.greeting);
405
+ speechQueue?.primeAudio();
406
+ if (streaming && persona.greeting) {
407
+ const q = speechQueue;
408
+ q.startQueue();
409
+ setCallState("speaking");
410
+ const { sentences, rest } = takeSentences(persona.greeting + "\n");
411
+ [...sentences, rest.trim()].filter(Boolean).forEach((s) => q.enqueueSentence(s));
412
+ setTimeout(() => setAnalyser(q.getAnalyser?.() ?? null), 180);
413
+ await new Promise((res) => q.finishQueue(res));
414
+ setAnalyser(null);
415
+ } else if (persona.greeting && synth && voiceId) {
416
+ await speakReply(persona.greeting);
417
+ }
290
418
  } finally {
291
419
  setCallState("listening");
292
420
  }
293
421
  })();
294
422
  }, []);
295
- useEffect3(
423
+ useEffect4(
296
424
  () => () => {
297
425
  audioRef.current?.pause();
426
+ speechQueue?.stopQueue();
298
427
  speech.stop();
299
428
  void audioCtxRef.current?.close();
300
429
  },
301
430
  // eslint-disable-next-line react-hooks/exhaustive-deps
302
431
  []
303
432
  );
304
- useEffect3(() => {
433
+ useEffect4(() => {
305
434
  if (callState === "listening" && !muted) speech.start();
306
435
  else speech.stop();
307
436
  }, [callState, muted]);
@@ -310,12 +439,14 @@ function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPromp
310
439
  const next = !on;
311
440
  if (gainRef.current) gainRef.current.gain.value = next ? 1 : 0;
312
441
  if (audioRef.current) audioRef.current.muted = !next;
442
+ speechQueue?.setMuted?.(!next);
313
443
  return next;
314
444
  });
315
445
  };
316
446
  const endCall = () => {
317
447
  setCallState("ended");
318
448
  audioRef.current?.pause();
449
+ speechQueue?.stopQueue();
319
450
  speech.stop();
320
451
  onClose(historyRef.current);
321
452
  };
@@ -388,6 +519,7 @@ function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPromp
388
519
  export {
389
520
  VoiceCall,
390
521
  defaultBuildPrompt,
391
- defaultParseReply
522
+ defaultParseReply,
523
+ defaultStreamPrompt
392
524
  };
393
525
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ui/VoiceCall.tsx","../src/chat.ts","../src/ui/useCallSpeech.ts","../src/ui/Waveform.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport { Mic, MicOff, Volume2, VolumeX, PhoneOff, Send, Subtitles, Loader2 } from 'lucide-react'\nimport type { ChatPort, VoiceSynthesisPort } from '@surfmate.team/digital-human-ports'\nimport {\n defaultBuildPrompt,\n defaultParseReply,\n type CallPersona,\n type CallMessage,\n type CallReply,\n type BuildPrompt,\n type ParseReply,\n} from '../chat'\nimport { useCallSpeech } from './useCallSpeech'\nimport { Waveform } from './Waveform'\n\ntype CallState = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended'\n\ntype Props = {\n readonly persona: CallPersona\n readonly chatPort?: ChatPort | undefined\n readonly synth?: VoiceSynthesisPort | undefined\n readonly voiceId?: string | undefined\n readonly voiceModelId?: string | undefined\n /** Override the prompt builder (pure). Omit → built-in mate-style default. */\n readonly buildPrompt?: BuildPrompt | undefined\n /** Override the reply parser (pure). Omit → built-in JSON {text, mood} default. */\n readonly parseReply?: ParseReply | undefined\n /** Emitted after each completed turn (user said X → assistant replied Y). The\n * host persists it if it wants — voice-call itself stays in-memory. */\n readonly onTurn?: ((user: CallMessage, ai: CallReply) => void) | undefined\n /** Called when the call ends; receives the full in-memory transcript so the\n * host can persist it in one shot. (Arg is ignorable for UI-only closers.) */\n readonly onClose: (history: readonly CallMessage[]) => void\n readonly speechLang?: string | undefined\n}\n\nconst STATUS: Record<CallState, string> = {\n connecting: '接通中…',\n listening: '聆听中…',\n thinking: '思考中…',\n speaking: '说话中…',\n ended: '已结束',\n}\n\nconst ctlBtn = 'flex h-12 w-12 items-center justify-center rounded-full transition'\nconst ctlOn = 'bg-white/90 text-slate-900 hover:bg-white'\nconst ctlOff = 'bg-red-600/90 text-white hover:bg-red-600'\n\n/**\n * Voice-only call — the lightweight sibling of digital-human-video-call. Same\n * turn loop (speech in → LLM via ChatPort → TTS via synth → back to listening),\n * but NO talking-head video / clip selection / waiting loop / playback engine.\n * The reply audio is routed through a Web Audio AnalyserNode so the Waveform\n * pulses with the actual voice. Pure ports/DI — chatPort + synth are injected.\n */\nexport function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, onTurn, onClose, speechLang }: Props) {\n // Injected calculations (default to the built-in mate-style strategy).\n const build = buildPrompt ?? defaultBuildPrompt\n const parse = parseReply ?? defaultParseReply\n\n const [callState, setCallState] = useState<CallState>('connecting')\n const [latestAi, setLatestAi] = useState(persona.greeting ?? '')\n const [latestUser, setLatestUser] = useState('')\n const [muted, setMuted] = useState(false)\n const [speakerOn, setSpeakerOn] = useState(true)\n const [showCaptions, setShowCaptions] = useState(true)\n const [typed, setTyped] = useState('')\n const [analyser, setAnalyser] = useState<AnalyserNode | null>(null)\n\n const historyRef = useRef<CallMessage[]>([])\n const audioRef = useRef<HTMLAudioElement | null>(null)\n const audioCtxRef = useRef<AudioContext | null>(null)\n const gainRef = useRef<GainNode | null>(null)\n const stateRef = useRef<CallState>('connecting')\n const speakerOnRef = useRef(true)\n const bootedRef = useRef(false)\n stateRef.current = callState\n speakerOnRef.current = speakerOn\n\n // Play reply audio routed through an AnalyserNode (so the waveform reacts) and\n // a GainNode (so the speaker toggle works even inside the Web Audio graph).\n const playAudio = (bytes: ArrayBuffer): Promise<void> =>\n new Promise((resolve) => {\n audioRef.current?.pause()\n const url = URL.createObjectURL(new Blob([bytes], { type: 'audio/mpeg' }))\n const audio = new Audio(url)\n audioRef.current = audio\n try {\n const Ctx = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n const ctx = (audioCtxRef.current ??= new Ctx())\n if (ctx.state === 'suspended') void ctx.resume()\n const src = ctx.createMediaElementSource(audio)\n const an = ctx.createAnalyser()\n an.fftSize = 128\n const gain = ctx.createGain()\n gain.gain.value = speakerOnRef.current ? 1 : 0\n gainRef.current = gain\n src.connect(an)\n an.connect(gain)\n gain.connect(ctx.destination)\n setAnalyser(an)\n } catch {\n audio.muted = !speakerOnRef.current\n }\n const done = () => {\n URL.revokeObjectURL(url)\n setAnalyser(null)\n gainRef.current = null\n resolve()\n }\n audio.onended = done\n audio.onerror = done\n void audio.play().catch(done)\n })\n\n const speakReply = async (text: string, emotion?: string) => {\n setCallState('speaking')\n if (synth && voiceId) {\n try {\n const bytes = await synth.synthesize({\n text,\n voiceId,\n ...(voiceModelId ? { modelId: voiceModelId } : {}),\n ...(emotion ? { emotion } : {}),\n })\n await playAudio(bytes)\n } catch (err) {\n console.warn('[voice-call] synth failed:', err)\n await new Promise((r) => setTimeout(r, Math.min(6000, 1200 + text.length * 60)))\n }\n } else {\n await new Promise((r) => setTimeout(r, Math.min(6000, 1200 + text.length * 60)))\n }\n }\n\n const handleUtterance = async (text: string) => {\n const t = text.trim()\n if (!t || !chatPort) return\n if (stateRef.current === 'thinking' || stateRef.current === 'speaking') return\n speech.stop()\n setLatestUser(t)\n setCallState('thinking')\n const history = historyRef.current\n const userMsg: CallMessage = { id: crypto.randomUUID(), role: 'user', content: t }\n historyRef.current = [...history, userMsg]\n try {\n const raw = await chatPort.complete(build(persona, history, t))\n const reply = parse(raw)\n historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: 'assistant', content: reply.text }]\n setLatestAi(reply.text)\n onTurn?.(userMsg, reply)\n await speakReply(reply.text, reply.mood)\n } catch (err) {\n setLatestAi(`(出错:${err instanceof Error ? err.message : String(err)})`)\n } finally {\n setCallState('listening')\n }\n }\n\n const speech = useCallSpeech(handleUtterance, speechLang)\n\n useEffect(() => {\n if (bootedRef.current) return\n bootedRef.current = true\n void (async () => {\n try {\n if (persona.greeting && synth && voiceId) await speakReply(persona.greeting)\n } finally {\n setCallState('listening')\n }\n })()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n useEffect(\n () => () => {\n audioRef.current?.pause()\n speech.stop()\n void audioCtxRef.current?.close()\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n\n useEffect(() => {\n if (callState === 'listening' && !muted) speech.start()\n else speech.stop()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [callState, muted])\n\n const toggleSpeaker = () => {\n setSpeakerOn((on) => {\n const next = !on\n if (gainRef.current) gainRef.current.gain.value = next ? 1 : 0\n if (audioRef.current) audioRef.current.muted = !next\n return next\n })\n }\n\n const endCall = () => {\n setCallState('ended')\n audioRef.current?.pause()\n speech.stop()\n onClose(historyRef.current)\n }\n\n const sendTyped = () => {\n const t = typed.trim()\n if (!t) return\n setTyped('')\n void handleUtterance(t)\n }\n\n const thinking = callState === 'thinking'\n\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col bg-gradient-to-b from-slate-900 via-slate-950 to-black text-white\">\n {/* Top bar */}\n <div className=\"flex items-center justify-between p-4\">\n <div className=\"flex items-center gap-2 rounded-full bg-white/10 px-3 py-1.5 backdrop-blur\">\n <span className={`h-2 w-2 rounded-full ${thinking ? 'animate-pulse bg-amber-400' : callState === 'speaking' ? 'bg-green-400' : 'bg-white/60'}`} />\n <span className=\"text-sm font-medium\">{persona.name}</span>\n <span className=\"text-xs text-white/60\">· 语音通话 · {STATUS[callState]}</span>\n </div>\n <button type=\"button\" onClick={() => setShowCaptions((v) => !v)} title=\"字幕\" className={`rounded-full p-2 backdrop-blur transition ${showCaptions ? 'bg-white/20' : 'bg-white/5 text-white/60'}`}>\n <Subtitles className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Center: avatar + waveform */}\n <div className=\"flex flex-1 flex-col items-center justify-center gap-6 px-6\">\n <div className=\"relative\">\n {persona.avatarUrl ? (\n <img src={persona.avatarUrl} alt={persona.name} className={`h-32 w-32 rounded-full object-cover ring-4 transition ${callState === 'speaking' ? 'ring-purple-500/70' : 'ring-white/15'}`} />\n ) : (\n <div className=\"flex h-32 w-32 items-center justify-center rounded-full bg-gradient-to-br from-purple-500/40 to-slate-700 text-4xl font-bold\">{persona.name.slice(0, 1)}</div>\n )}\n {callState === 'speaking' ? <span className=\"absolute inset-0 animate-ping rounded-full ring-2 ring-purple-500/40\" /> : null}\n </div>\n <div className=\"w-full max-w-md\">\n <Waveform analyser={analyser} phase={callState} />\n </div>\n {callState === 'connecting' ? (\n <div className=\"flex items-center gap-2 text-white/70\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n <span className=\"text-sm\">接通 {persona.name}…</span>\n </div>\n ) : null}\n </div>\n\n {/* Captions */}\n {showCaptions ? (\n <div className=\"space-y-2 px-6 pb-2\">\n {latestUser ? (\n <p className=\"ml-auto max-w-[80%] text-right text-sm text-white/70\">\n <span className=\"rounded-2xl bg-white/10 px-3 py-1.5 backdrop-blur\">{latestUser}</span>\n </p>\n ) : null}\n {latestAi ? (\n <p className=\"max-w-[80%] text-sm\">\n <span className=\"inline-block rounded-2xl bg-black/40 px-3 py-1.5 leading-relaxed backdrop-blur\">\n {thinking ? <Loader2 className=\"inline h-3.5 w-3.5 animate-spin\" /> : `${persona.name}: ${latestAi}`}\n </span>\n </p>\n ) : null}\n </div>\n ) : null}\n\n {/* Controls */}\n <div className=\"flex flex-col gap-3 px-4 pb-5 pt-2\">\n <div className=\"flex items-center justify-center gap-4\">\n <button type=\"button\" onClick={() => setMuted((v) => !v)} title={muted ? '取消静音' : '麦克风静音'} className={`${ctlBtn} ${muted ? ctlOff : ctlOn}`}>\n {muted ? <MicOff className=\"h-5 w-5\" /> : <Mic className=\"h-5 w-5\" />}\n </button>\n <button type=\"button\" onClick={toggleSpeaker} title={speakerOn ? '关闭外放' : '开启外放'} className={`${ctlBtn} ${speakerOn ? ctlOn : ctlOff}`}>\n {speakerOn ? <Volume2 className=\"h-5 w-5\" /> : <VolumeX className=\"h-5 w-5\" />}\n </button>\n <button type=\"button\" onClick={endCall} title=\"挂断\" className=\"flex h-14 w-14 items-center justify-center rounded-full bg-red-600 text-white transition hover:bg-red-700\">\n <PhoneOff className=\"h-6 w-6\" />\n </button>\n </div>\n <div className=\"flex items-center gap-2\">\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n sendTyped()\n }\n }}\n disabled={!chatPort || thinking || callState === 'speaking'}\n placeholder={chatPort ? '说话,或在此输入…' : '需注入 ChatPort'}\n className=\"h-10 flex-1 rounded-full border border-white/20 bg-white/10 px-4 text-sm text-white placeholder-white/50 outline-none backdrop-blur focus:border-white/40 disabled:opacity-50\"\n />\n <button type=\"button\" onClick={sendTyped} disabled={!chatPort || thinking || callState === 'speaking' || !typed.trim()} className=\"flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-900 transition disabled:opacity-40\">\n <Send className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n )\n}\n","// Voice-call's OWN chat contracts + sensible default calculations. The package\n// depends only on the `ports` contract package — the prompt builder and reply\n// parser are pure strategy you can OVERRIDE via props; these defaults (mate's\n// JSON {text, mood} format) just make it work out of the box.\n//\n// Consumer-defined interfaces (Go-style): a host can pass any structurally\n// matching persona / message — e.g. conversation's CharacterPersona / ChatMessage\n// satisfy these without voice-call ever importing that package.\n\n/** The character context that shapes the prompt + the call header/avatar. */\nexport type CallPersona = {\n readonly name: string\n readonly description?: string | undefined\n readonly personality?: string | undefined\n readonly greeting?: string | undefined\n readonly avatarUrl?: string | undefined\n}\n\n/** One role-tagged turn of history. */\nexport type CallMessage = {\n readonly id: string\n readonly role: 'user' | 'assistant'\n readonly content: string\n}\n\n/** A parsed assistant reply — text + an optional mood (used as the TTS emotion). */\nexport type CallReply = {\n readonly text: string\n readonly mood?: string | undefined\n}\n\n/** Pure: persona + recent history + new message → the single prompt string. */\nexport type BuildPrompt = (persona: CallPersona, history: readonly CallMessage[], userMessage: string) => string\n\n/** Pure: the LLM's raw completion → { text, mood? }. */\nexport type ParseReply = (raw: string) => CallReply\n\n/** How many trailing messages of history the default prompt includes. */\nconst HISTORY_WINDOW = 5\n\n/** Default prompt builder — mirrors mate's grokAI.buildPrompt (JSON {text, mood}). */\nexport const defaultBuildPrompt: BuildPrompt = (persona, history, userMessage) => {\n const historyContext = history\n .slice(-HISTORY_WINDOW)\n .map((m) => `${m.role === 'user' ? 'User' : persona.name}: ${m.content}`)\n .join('\\n')\n\n return `You are ${persona.name}.\n\nCharacter background: ${persona.description ?? ''}\n\nPersonality traits: ${persona.personality ?? ''}\n\nYour greeting message: \"${persona.greeting ?? ''}\"\n\nConversation history:\n${historyContext}\n\nUser: ${userMessage}\n\nRespond as ${persona.name} would, staying in character with the background and personality described above. Keep responses natural, engaging, and consistent. Use 1-3 sentences unless a longer response is clearly needed.\n\nIMPORTANT: Respond in JSON format with your reply text and current mood:\n{\"text\": \"your response here\", \"mood\": \"happy|calm|angry|sad\"}\n\nChoose the mood that best matches the emotional tone of your response. Only use: happy, calm, angry, or sad.`\n}\n\nconst VALID_MOODS = ['happy', 'calm', 'angry', 'sad']\n\n/** Default reply parser — pulls the first JSON {text, mood}; falls back to raw + neutral. */\nexport const defaultParseReply: ParseReply = (raw) => {\n const match = raw.match(/\\{[\\s\\S]*\"text\"[\\s\\S]*\\}/)\n if (match) {\n try {\n const parsed = JSON.parse(match[0]) as { text?: unknown; mood?: unknown }\n const text = typeof parsed.text === 'string' && parsed.text ? parsed.text : raw\n const mood = typeof parsed.mood === 'string' && VALID_MOODS.includes(parsed.mood) ? parsed.mood : 'neutral'\n return { text, mood }\n } catch {\n /* malformed JSON — fall through */\n }\n }\n return { text: raw, mood: 'neutral' }\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Continuous speech recognition for the call: transcribes live, and after a\n// short silence fires onUtterance(text) — mate's \"2s silence = user finished\"\n// turn detection. Browser SpeechRecognition (Chrome/Edge), no backend.\n\ntype RecognitionLike = {\n lang: string\n continuous: boolean\n interimResults: boolean\n onresult: ((e: { results: ArrayLike<{ 0: { transcript: string }; isFinal: boolean }> }) => void) | null\n onend: (() => void) | null\n onerror: (() => void) | null\n start(): void\n stop(): void\n}\n\nfunction getCtor(): (new () => RecognitionLike) | null {\n if (typeof window === 'undefined') return null\n const w = window as unknown as Record<string, unknown>\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as (new () => RecognitionLike) | null\n}\n\nconst SILENCE_MS = 2000\n\nexport function useCallSpeech(onUtterance: (text: string) => void, lang = 'zh-CN') {\n const [listening, setListening] = useState(false)\n const [draft, setDraft] = useState('')\n const recRef = useRef<RecognitionLike | null>(null)\n const silenceRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const wantRef = useRef(false)\n const supported = getCtor() !== null\n\n const clearSilence = () => {\n if (silenceRef.current) {\n clearTimeout(silenceRef.current)\n silenceRef.current = null\n }\n }\n\n const stop = () => {\n wantRef.current = false\n clearSilence()\n recRef.current?.stop()\n recRef.current = null\n setListening(false)\n setDraft('')\n }\n\n const start = () => {\n const Ctor = getCtor()\n if (!Ctor || wantRef.current) return\n const rec = new Ctor()\n rec.lang = lang\n rec.continuous = true\n rec.interimResults = true\n rec.onresult = (e) => {\n let text = ''\n for (let i = 0; i < e.results.length; i++) {\n const r = e.results[i]\n if (r) text += r[0].transcript\n }\n setDraft(text)\n clearSilence()\n if (text.trim()) {\n silenceRef.current = setTimeout(() => {\n const finalText = text.trim()\n setDraft('')\n onUtterance(finalText)\n }, SILENCE_MS)\n }\n }\n rec.onerror = () => {}\n rec.onend = () => {\n // Auto-restart while we still want to listen (recognition stops itself).\n if (wantRef.current) {\n try {\n rec.start()\n } catch {\n /* already started */\n }\n } else {\n setListening(false)\n }\n }\n try {\n rec.start()\n recRef.current = rec\n wantRef.current = true\n setListening(true)\n } catch {\n /* ignore */\n }\n }\n\n useEffect(() => () => stop(), [])\n\n return { supported, listening, draft, start, stop }\n}\n","import { useEffect, useRef } from 'react'\n\ntype Phase = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended'\n\n/**\n * Audio-reactive waveform. When the reply audio is playing it draws the live\n * frequency spectrum from the injected AnalyserNode; otherwise it shows a gentle\n * idle pulse tuned per call phase (listening brighter, thinking calmer). Pure\n * canvas — no per-bar React state.\n */\nexport function Waveform({ analyser, phase }: { readonly analyser: AnalyserNode | null; readonly phase: Phase }) {\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const analyserRef = useRef(analyser)\n const phaseRef = useRef(phase)\n analyserRef.current = analyser\n phaseRef.current = phase\n\n useEffect(() => {\n const canvas = canvasRef.current\n const ctx = canvas?.getContext('2d')\n if (!canvas || !ctx) return\n const BARS = 40\n let raf = 0\n let t = 0\n let freq: Uint8Array<ArrayBuffer> | null = null\n\n const draw = () => {\n raf = requestAnimationFrame(draw)\n t += 0.06\n const a = analyserRef.current\n const p = phaseRef.current\n const W = canvas.width\n const H = canvas.height\n ctx.clearRect(0, 0, W, H)\n\n const live = a && p === 'speaking'\n if (live) {\n if (!freq || freq.length !== a.frequencyBinCount) freq = new Uint8Array(a.frequencyBinCount)\n a.getByteFrequencyData(freq)\n }\n const bw = W / BARS\n const idleBase = p === 'listening' ? 0.22 : p === 'thinking' ? 0.12 : p === 'connecting' ? 0.08 : 0.05\n\n for (let i = 0; i < BARS; i++) {\n let amp: number\n if (live && freq) {\n // sample low→mid frequencies (where speech energy is), mirror outward\n const m = Math.abs(i - BARS / 2) / (BARS / 2) // 0 center → 1 edges\n const idx = Math.floor((1 - m) * (freq.length * 0.6))\n amp = ((freq[idx] ?? 0) / 255) * (1 - m * 0.35)\n } else {\n amp = idleBase * (0.6 + 0.4 * Math.sin(t + i * 0.4))\n }\n const h = Math.max(3, amp * H)\n const x = i * bw + bw * 0.22\n const y = (H - h) / 2\n ctx.fillStyle = `rgba(168, 85, 247, ${0.45 + amp * 0.55})`\n const r = Math.min(bw * 0.28, h / 2)\n ctx.beginPath()\n ctx.roundRect(x, y, bw * 0.56, h, r)\n ctx.fill()\n }\n }\n draw()\n return () => cancelAnimationFrame(raf)\n }, [])\n\n return <canvas ref={canvasRef} width={640} height={160} className=\"h-40 w-full\" />\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useEffect","useRef","jsx","useState","useRef","useEffect"]}
1
+ {"version":3,"sources":["../src/ui/VoiceCall.tsx","../src/chat.ts","../src/calc/takeSentences.ts","../src/ui/useCallSpeech.ts","../src/ui/useSegmentSpeech.ts","../src/ui/Waveform.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react'\nimport { Mic, MicOff, Volume2, VolumeX, PhoneOff, Send, Subtitles, Loader2 } from 'lucide-react'\nimport type { ChatPort, VoiceSynthesisPort } from '@surfmate.team/digital-human-ports'\nimport { streamOrComplete } from '@surfmate.team/digital-human-llm'\nimport {\n defaultBuildPrompt,\n defaultParseReply,\n defaultStreamPrompt,\n type CallPersona,\n type CallMessage,\n type CallReply,\n type BuildPrompt,\n type ParseReply,\n type SpeechQueuePort,\n type SttPort,\n} from '../chat'\nimport { takeSentences } from '../calc/takeSentences'\nimport { useCallSpeech } from './useCallSpeech'\nimport { useSegmentSpeech } from './useSegmentSpeech'\nimport { Waveform } from './Waveform'\n\ntype CallState = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended'\n\ntype Props = {\n readonly persona: CallPersona\n readonly chatPort?: ChatPort | undefined\n readonly synth?: VoiceSynthesisPort | undefined\n readonly voiceId?: string | undefined\n readonly voiceModelId?: string | undefined\n /** Override the prompt builder (pure). Omit → built-in mate-style default. */\n readonly buildPrompt?: BuildPrompt | undefined\n /** Override the reply parser (pure). Omit → built-in JSON {text, mood} default. */\n readonly parseReply?: ParseReply | undefined\n /**\n * Inject a streaming TTS sink to turn on STREAMING mode: the reply is streamed\n * from the LLM (ChatPort.stream) and spoken sentence-by-sentence as it arrives\n * (low latency) instead of synthesized whole. When set, the default prompt\n * switches to PLAIN-TEXT (defaultStreamPrompt). Omit → classic synth path.\n */\n readonly speechQueue?: SpeechQueuePort | undefined\n /**\n * Pluggable speech-to-text. Omit → browser Web Speech API (free, but unreliable\n * / CN-blocked). Inject (e.g. an Azure STT mic) for robust recognition.\n */\n readonly stt?: SttPort | undefined\n /** Emitted after each completed turn (user said X → assistant replied Y). The\n * host persists it if it wants — voice-call itself stays in-memory. */\n readonly onTurn?: ((user: CallMessage, ai: CallReply) => void) | undefined\n /** Called when the call ends; receives the full in-memory transcript so the\n * host can persist it in one shot. (Arg is ignorable for UI-only closers.) */\n readonly onClose: (history: readonly CallMessage[]) => void\n readonly speechLang?: string | undefined\n}\n\nconst STATUS: Record<CallState, string> = {\n connecting: '接通中…',\n listening: '聆听中…',\n thinking: '思考中…',\n speaking: '说话中…',\n ended: '已结束',\n}\n\nconst ctlBtn = 'flex h-12 w-12 items-center justify-center rounded-full transition'\nconst ctlOn = 'bg-white/90 text-slate-900 hover:bg-white'\nconst ctlOff = 'bg-red-600/90 text-white hover:bg-red-600'\n\n/**\n * Voice-only call — the lightweight sibling of digital-human-video-call. Same\n * turn loop (speech in → LLM via ChatPort → TTS via synth → back to listening),\n * but NO talking-head video / clip selection / waiting loop / playback engine.\n * The reply audio is routed through a Web Audio AnalyserNode so the Waveform\n * pulses with the actual voice. Pure ports/DI — chatPort + synth are injected.\n */\nexport function VoiceCall({ persona, chatPort, synth, voiceId, voiceModelId, buildPrompt, parseReply, speechQueue, stt, onTurn, onClose, speechLang }: Props) {\n const streaming = !!speechQueue\n // Injected calculations. Streaming needs PLAIN-TEXT replies (split into spoken\n // sentences), so its default prompt differs from the classic JSON one.\n const build = buildPrompt ?? (streaming ? defaultStreamPrompt : defaultBuildPrompt)\n const parse = parseReply ?? defaultParseReply\n\n const [callState, setCallState] = useState<CallState>('connecting')\n const [latestAi, setLatestAi] = useState(persona.greeting ?? '')\n const [latestUser, setLatestUser] = useState('')\n const [muted, setMuted] = useState(false)\n const [speakerOn, setSpeakerOn] = useState(true)\n const [showCaptions, setShowCaptions] = useState(true)\n const [typed, setTyped] = useState('')\n const [analyser, setAnalyser] = useState<AnalyserNode | null>(null)\n\n const historyRef = useRef<CallMessage[]>([])\n const audioRef = useRef<HTMLAudioElement | null>(null)\n const audioCtxRef = useRef<AudioContext | null>(null)\n const gainRef = useRef<GainNode | null>(null)\n const stateRef = useRef<CallState>('connecting')\n const speakerOnRef = useRef(true)\n const bootedRef = useRef(false)\n stateRef.current = callState\n speakerOnRef.current = speakerOn\n\n // Play reply audio routed through an AnalyserNode (so the waveform reacts) and\n // a GainNode (so the speaker toggle works even inside the Web Audio graph).\n const playAudio = (bytes: ArrayBuffer): Promise<void> =>\n new Promise((resolve) => {\n audioRef.current?.pause()\n const url = URL.createObjectURL(new Blob([bytes], { type: 'audio/mpeg' }))\n const audio = new Audio(url)\n audioRef.current = audio\n try {\n const Ctx = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n const ctx = (audioCtxRef.current ??= new Ctx())\n if (ctx.state === 'suspended') void ctx.resume()\n const src = ctx.createMediaElementSource(audio)\n const an = ctx.createAnalyser()\n an.fftSize = 128\n const gain = ctx.createGain()\n gain.gain.value = speakerOnRef.current ? 1 : 0\n gainRef.current = gain\n src.connect(an)\n an.connect(gain)\n gain.connect(ctx.destination)\n setAnalyser(an)\n } catch {\n audio.muted = !speakerOnRef.current\n }\n const done = () => {\n URL.revokeObjectURL(url)\n setAnalyser(null)\n gainRef.current = null\n resolve()\n }\n audio.onended = done\n audio.onerror = done\n void audio.play().catch(done)\n })\n\n const speakReply = async (text: string, emotion?: string) => {\n setCallState('speaking')\n if (synth && voiceId) {\n try {\n const bytes = await synth.synthesize({\n text,\n voiceId,\n ...(voiceModelId ? { modelId: voiceModelId } : {}),\n ...(emotion ? { emotion } : {}),\n })\n await playAudio(bytes)\n } catch (err) {\n console.warn('[voice-call] synth failed:', err)\n await new Promise((r) => setTimeout(r, Math.min(6000, 1200 + text.length * 60)))\n }\n } else {\n await new Promise((r) => setTimeout(r, Math.min(6000, 1200 + text.length * 60)))\n }\n }\n\n // STREAMING turn: LLM tokens → split into sentences → enqueue to the TTS queue,\n // which fetches+plays each as it arrives (audio starts on sentence 1). Returns\n // the full reply text. Only used when speechQueue is wired.\n const streamSpeak = async (prompt: string, onText: (full: string) => void): Promise<string> => {\n const q = speechQueue!\n q.startQueue()\n setCallState('speaking')\n let acc = ''\n let buf = ''\n let primed = false\n const primeWaveform = () => {\n if (primed) return\n primed = true\n setTimeout(() => setAnalyser(q.getAnalyser?.() ?? null), 180)\n }\n for await (const delta of streamOrComplete(chatPort!, prompt)) {\n acc += delta\n buf += delta\n const { sentences, rest } = takeSentences(buf)\n buf = rest\n for (const s of sentences) {\n q.enqueueSentence(s)\n primeWaveform()\n }\n onText(acc)\n }\n const tail = buf.trim()\n if (tail) {\n q.enqueueSentence(tail)\n primeWaveform()\n }\n await new Promise<void>((res) => q.finishQueue(res))\n setAnalyser(null)\n return acc.trim()\n }\n\n const handleUtterance = async (text: string) => {\n const t = text.trim()\n if (!t || !chatPort) return\n if (stateRef.current === 'thinking' || stateRef.current === 'speaking') return\n speech.stop()\n setLatestUser(t)\n setCallState('thinking')\n const history = historyRef.current\n const userMsg: CallMessage = { id: crypto.randomUUID(), role: 'user', content: t }\n historyRef.current = [...history, userMsg]\n try {\n if (streaming) {\n const replyText = await streamSpeak(build(persona, history, t), setLatestAi)\n historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: 'assistant', content: replyText }]\n onTurn?.(userMsg, { text: replyText })\n } else {\n const raw = await chatPort.complete(build(persona, history, t))\n const reply = parse(raw)\n historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: 'assistant', content: reply.text }]\n setLatestAi(reply.text)\n onTurn?.(userMsg, reply)\n await speakReply(reply.text, reply.mood)\n }\n } catch (err) {\n setLatestAi(`(出错:${err instanceof Error ? err.message : String(err)})`)\n } finally {\n setCallState('listening')\n }\n }\n\n // Both hooks are always called (no conditional hooks); we drive only the chosen\n // one (the unused stays inert until its start()). Inject `stt` → Azure mic;\n // otherwise the browser Web Speech API.\n const browserSpeech = useCallSpeech(handleUtterance, speechLang)\n const azureSpeech = useSegmentSpeech(stt, handleUtterance)\n const speech = stt ? azureSpeech : browserSpeech\n\n useEffect(() => {\n if (bootedRef.current) return\n bootedRef.current = true\n void (async () => {\n try {\n speechQueue?.primeAudio()\n if (streaming && persona.greeting) {\n const q = speechQueue!\n q.startQueue()\n setCallState('speaking')\n const { sentences, rest } = takeSentences(persona.greeting + '\\n')\n ;[...sentences, rest.trim()].filter(Boolean).forEach((s) => q.enqueueSentence(s))\n setTimeout(() => setAnalyser(q.getAnalyser?.() ?? null), 180)\n await new Promise<void>((res) => q.finishQueue(res))\n setAnalyser(null)\n } else if (persona.greeting && synth && voiceId) {\n await speakReply(persona.greeting)\n }\n } finally {\n setCallState('listening')\n }\n })()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n useEffect(\n () => () => {\n audioRef.current?.pause()\n speechQueue?.stopQueue()\n speech.stop()\n void audioCtxRef.current?.close()\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n\n useEffect(() => {\n if (callState === 'listening' && !muted) speech.start()\n else speech.stop()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [callState, muted])\n\n const toggleSpeaker = () => {\n setSpeakerOn((on) => {\n const next = !on\n if (gainRef.current) gainRef.current.gain.value = next ? 1 : 0\n if (audioRef.current) audioRef.current.muted = !next\n speechQueue?.setMuted?.(!next)\n return next\n })\n }\n\n const endCall = () => {\n setCallState('ended')\n audioRef.current?.pause()\n speechQueue?.stopQueue()\n speech.stop()\n onClose(historyRef.current)\n }\n\n const sendTyped = () => {\n const t = typed.trim()\n if (!t) return\n setTyped('')\n void handleUtterance(t)\n }\n\n const thinking = callState === 'thinking'\n\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col bg-gradient-to-b from-slate-900 via-slate-950 to-black text-white\">\n {/* Top bar */}\n <div className=\"flex items-center justify-between p-4\">\n <div className=\"flex items-center gap-2 rounded-full bg-white/10 px-3 py-1.5 backdrop-blur\">\n <span className={`h-2 w-2 rounded-full ${thinking ? 'animate-pulse bg-amber-400' : callState === 'speaking' ? 'bg-green-400' : 'bg-white/60'}`} />\n <span className=\"text-sm font-medium\">{persona.name}</span>\n <span className=\"text-xs text-white/60\">· 语音通话 · {STATUS[callState]}</span>\n </div>\n <button type=\"button\" onClick={() => setShowCaptions((v) => !v)} title=\"字幕\" className={`rounded-full p-2 backdrop-blur transition ${showCaptions ? 'bg-white/20' : 'bg-white/5 text-white/60'}`}>\n <Subtitles className=\"h-4 w-4\" />\n </button>\n </div>\n\n {/* Center: avatar + waveform */}\n <div className=\"flex flex-1 flex-col items-center justify-center gap-6 px-6\">\n <div className=\"relative\">\n {persona.avatarUrl ? (\n <img src={persona.avatarUrl} alt={persona.name} className={`h-32 w-32 rounded-full object-cover ring-4 transition ${callState === 'speaking' ? 'ring-purple-500/70' : 'ring-white/15'}`} />\n ) : (\n <div className=\"flex h-32 w-32 items-center justify-center rounded-full bg-gradient-to-br from-purple-500/40 to-slate-700 text-4xl font-bold\">{persona.name.slice(0, 1)}</div>\n )}\n {callState === 'speaking' ? <span className=\"absolute inset-0 animate-ping rounded-full ring-2 ring-purple-500/40\" /> : null}\n </div>\n <div className=\"w-full max-w-md\">\n <Waveform analyser={analyser} phase={callState} />\n </div>\n {callState === 'connecting' ? (\n <div className=\"flex items-center gap-2 text-white/70\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n <span className=\"text-sm\">接通 {persona.name}…</span>\n </div>\n ) : null}\n </div>\n\n {/* Captions */}\n {showCaptions ? (\n <div className=\"space-y-2 px-6 pb-2\">\n {latestUser ? (\n <p className=\"ml-auto max-w-[80%] text-right text-sm text-white/70\">\n <span className=\"rounded-2xl bg-white/10 px-3 py-1.5 backdrop-blur\">{latestUser}</span>\n </p>\n ) : null}\n {latestAi ? (\n <p className=\"max-w-[80%] text-sm\">\n <span className=\"inline-block rounded-2xl bg-black/40 px-3 py-1.5 leading-relaxed backdrop-blur\">\n {thinking ? <Loader2 className=\"inline h-3.5 w-3.5 animate-spin\" /> : `${persona.name}: ${latestAi}`}\n </span>\n </p>\n ) : null}\n </div>\n ) : null}\n\n {/* Controls */}\n <div className=\"flex flex-col gap-3 px-4 pb-5 pt-2\">\n <div className=\"flex items-center justify-center gap-4\">\n <button type=\"button\" onClick={() => setMuted((v) => !v)} title={muted ? '取消静音' : '麦克风静音'} className={`${ctlBtn} ${muted ? ctlOff : ctlOn}`}>\n {muted ? <MicOff className=\"h-5 w-5\" /> : <Mic className=\"h-5 w-5\" />}\n </button>\n <button type=\"button\" onClick={toggleSpeaker} title={speakerOn ? '关闭外放' : '开启外放'} className={`${ctlBtn} ${speakerOn ? ctlOn : ctlOff}`}>\n {speakerOn ? <Volume2 className=\"h-5 w-5\" /> : <VolumeX className=\"h-5 w-5\" />}\n </button>\n <button type=\"button\" onClick={endCall} title=\"挂断\" className=\"flex h-14 w-14 items-center justify-center rounded-full bg-red-600 text-white transition hover:bg-red-700\">\n <PhoneOff className=\"h-6 w-6\" />\n </button>\n </div>\n <div className=\"flex items-center gap-2\">\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n sendTyped()\n }\n }}\n disabled={!chatPort || thinking || callState === 'speaking'}\n placeholder={chatPort ? '说话,或在此输入…' : '需注入 ChatPort'}\n className=\"h-10 flex-1 rounded-full border border-white/20 bg-white/10 px-4 text-sm text-white placeholder-white/50 outline-none backdrop-blur focus:border-white/40 disabled:opacity-50\"\n />\n <button type=\"button\" onClick={sendTyped} disabled={!chatPort || thinking || callState === 'speaking' || !typed.trim()} className=\"flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-900 transition disabled:opacity-40\">\n <Send className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n )\n}\n","// Voice-call's OWN chat contracts + sensible default calculations. The package\n// depends only on the `ports` contract package — the prompt builder and reply\n// parser are pure strategy you can OVERRIDE via props; these defaults (mate's\n// JSON {text, mood} format) just make it work out of the box.\n//\n// Consumer-defined interfaces (Go-style): a host can pass any structurally\n// matching persona / message — e.g. conversation's CharacterPersona / ChatMessage\n// satisfy these without voice-call ever importing that package.\n\n/** The character context that shapes the prompt + the call header/avatar. */\nexport type CallPersona = {\n readonly name: string\n readonly description?: string | undefined\n readonly personality?: string | undefined\n readonly greeting?: string | undefined\n readonly avatarUrl?: string | undefined\n}\n\n/** One role-tagged turn of history. */\nexport type CallMessage = {\n readonly id: string\n readonly role: 'user' | 'assistant'\n readonly content: string\n}\n\n/** A parsed assistant reply — text + an optional mood (used as the TTS emotion). */\nexport type CallReply = {\n readonly text: string\n readonly mood?: string | undefined\n}\n\n/** Pure: persona + recent history + new message → the single prompt string. */\nexport type BuildPrompt = (persona: CallPersona, history: readonly CallMessage[], userMessage: string) => string\n\n/** Pure: the LLM's raw completion → { text, mood? }. */\nexport type ParseReply = (raw: string) => CallReply\n\n/**\n * A streaming TTS sink. The call enqueues reply SENTENCES as they peel off the\n * LLM token stream; the adapter fetches + schedules each for seamless playback,\n * so audio starts on the first sentence (low latency) instead of after the whole\n * reply. digital-human-voice's `createAzureStreamSpeak()` satisfies this\n * structurally — pass it as the `speechQueue` prop to turn on streaming mode.\n */\nexport type SpeechQueuePort = {\n /** Unlock/resume the audio output inside a user gesture (call on mount). */\n primeAudio(): void\n /** Begin a fresh assistant turn (resets the playback timeline). */\n startQueue(): void\n /** Add one finished sentence; fetched immediately, played in order. */\n enqueueSentence(text: string): void\n /** No more sentences this turn; `onIdle` fires once playback drains. */\n finishQueue(onIdle: () => void): void\n /** Interrupt: drop the queue and stop playback now. */\n stopQueue(): void\n /** Mute the output without stopping (speaker toggle). Optional. */\n setMuted?(muted: boolean): void\n /** The AnalyserNode the audio flows through, for a reactive waveform. Optional. */\n getAnalyser?(): AnalyserNode | null\n}\n\n/** A running mic session: pause while the AI speaks (anti-echo), resume to listen\n * again, stop to release the device. */\nexport type SttSession = {\n pause(): void\n resume(): void\n stop(): void\n}\n\n/**\n * Pluggable speech-to-text. Omit → the built-in browser Web Speech API (free but\n * unreliable: stops itself, weak for continuous dialogue, CN-blocked via Google).\n * Inject one (e.g. an Azure STT mic: capture PCM + VAD endpointing → POST a WAV to\n * a recognizer) for robust recognition. `start` opens the mic and calls\n * `onUtterance(text)` after each endpointed segment; the returned session lets the\n * call pause it while the assistant speaks.\n */\nexport type SttPort = {\n start(handlers: {\n onUtterance: (text: string) => void\n onError?: (msg: string) => void\n }): Promise<SttSession> | SttSession\n}\n\n/** How many trailing messages of history the default prompt includes. */\nconst HISTORY_WINDOW = 5\n\n/**\n * Streaming prompt builder — PLAIN TEXT output (no JSON wrapper), so reply tokens\n * can be split into speakable sentences as they stream. Use this (not the JSON\n * defaultBuildPrompt) whenever a `speechQueue` is wired.\n */\nexport const defaultStreamPrompt: BuildPrompt = (persona, history, userMessage) => {\n const historyContext = history\n .slice(-HISTORY_WINDOW)\n .map((m) => `${m.role === 'user' ? 'User' : persona.name}: ${m.content}`)\n .join('\\n')\n\n return `You are ${persona.name}.\n\nCharacter background: ${persona.description ?? ''}\n\nPersonality traits: ${persona.personality ?? ''}\n\nConversation history:\n${historyContext}\n\nUser: ${userMessage}\n\nReply as ${persona.name} in short, natural SPOKEN sentences (1-3 sentences unless more is clearly needed). Output PLAIN TEXT only — no JSON, no markdown, no surrounding quotes.`\n}\n\n/** Default prompt builder — mirrors mate's grokAI.buildPrompt (JSON {text, mood}). */\nexport const defaultBuildPrompt: BuildPrompt = (persona, history, userMessage) => {\n const historyContext = history\n .slice(-HISTORY_WINDOW)\n .map((m) => `${m.role === 'user' ? 'User' : persona.name}: ${m.content}`)\n .join('\\n')\n\n return `You are ${persona.name}.\n\nCharacter background: ${persona.description ?? ''}\n\nPersonality traits: ${persona.personality ?? ''}\n\nYour greeting message: \"${persona.greeting ?? ''}\"\n\nConversation history:\n${historyContext}\n\nUser: ${userMessage}\n\nRespond as ${persona.name} would, staying in character with the background and personality described above. Keep responses natural, engaging, and consistent. Use 1-3 sentences unless a longer response is clearly needed.\n\nIMPORTANT: Respond in JSON format with your reply text and current mood:\n{\"text\": \"your response here\", \"mood\": \"happy|calm|angry|sad\"}\n\nChoose the mood that best matches the emotional tone of your response. Only use: happy, calm, angry, or sad.`\n}\n\nconst VALID_MOODS = ['happy', 'calm', 'angry', 'sad']\n\n/** Default reply parser — pulls the first JSON {text, mood}; falls back to raw + neutral. */\nexport const defaultParseReply: ParseReply = (raw) => {\n const match = raw.match(/\\{[\\s\\S]*\"text\"[\\s\\S]*\\}/)\n if (match) {\n try {\n const parsed = JSON.parse(match[0]) as { text?: unknown; mood?: unknown }\n const text = typeof parsed.text === 'string' && parsed.text ? parsed.text : raw\n const mood = typeof parsed.mood === 'string' && VALID_MOODS.includes(parsed.mood) ? parsed.mood : 'neutral'\n return { text, mood }\n } catch {\n /* malformed JSON — fall through */\n }\n }\n return { text: raw, mood: 'neutral' }\n}\n","// Pure: peel complete sentences off the front of a streaming buffer, keeping the\n// incomplete tail in `rest`. Drives sentence-at-a-time TTS as LLM tokens arrive.\n//\n// A boundary is sentence punctuation (. ! ? 。!?…) plus any trailing closing\n// quote/bracket, FOLLOWED BY WHITESPACE — not end-of-buffer. Requiring a space\n// after means \"3.5\" / \"Mr.\" mid-token don't split, and a sentence whose final\n// \".\" is the last char so far waits in `rest` until the next delta proves it's a\n// real boundary (the caller flushes the final `rest` once the stream ends).\n// Newlines always split.\n\nconst BOUNDARY = /([.!?。!?…]+[\"')\\]」』】]*(?=\\s))|(\\n+)/g\n\nexport function takeSentences(buffer: string): { sentences: string[]; rest: string } {\n const sentences: string[] = []\n let last = 0\n BOUNDARY.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = BOUNDARY.exec(buffer))) {\n const end = m.index + m[0].length\n const chunk = buffer.slice(last, end).trim()\n if (chunk) sentences.push(chunk)\n last = end\n }\n return { sentences, rest: buffer.slice(last) }\n}\n","import { useEffect, useRef, useState } from 'react'\n\n// Continuous speech recognition for the call: transcribes live, and after a\n// short silence fires onUtterance(text) — mate's \"2s silence = user finished\"\n// turn detection. Browser SpeechRecognition (Chrome/Edge), no backend.\n\ntype RecognitionLike = {\n lang: string\n continuous: boolean\n interimResults: boolean\n onresult: ((e: { results: ArrayLike<{ 0: { transcript: string }; isFinal: boolean }> }) => void) | null\n onend: (() => void) | null\n onerror: (() => void) | null\n start(): void\n stop(): void\n}\n\nfunction getCtor(): (new () => RecognitionLike) | null {\n if (typeof window === 'undefined') return null\n const w = window as unknown as Record<string, unknown>\n return (w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null) as (new () => RecognitionLike) | null\n}\n\nconst SILENCE_MS = 2000\n\nexport function useCallSpeech(onUtterance: (text: string) => void, lang = 'zh-CN') {\n const [listening, setListening] = useState(false)\n const [draft, setDraft] = useState('')\n const recRef = useRef<RecognitionLike | null>(null)\n const silenceRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n const wantRef = useRef(false)\n const supported = getCtor() !== null\n\n const clearSilence = () => {\n if (silenceRef.current) {\n clearTimeout(silenceRef.current)\n silenceRef.current = null\n }\n }\n\n const stop = () => {\n wantRef.current = false\n clearSilence()\n recRef.current?.stop()\n recRef.current = null\n setListening(false)\n setDraft('')\n }\n\n const start = () => {\n const Ctor = getCtor()\n if (!Ctor || wantRef.current) return\n const rec = new Ctor()\n rec.lang = lang\n rec.continuous = true\n rec.interimResults = true\n rec.onresult = (e) => {\n let text = ''\n for (let i = 0; i < e.results.length; i++) {\n const r = e.results[i]\n if (r) text += r[0].transcript\n }\n setDraft(text)\n clearSilence()\n if (text.trim()) {\n silenceRef.current = setTimeout(() => {\n const finalText = text.trim()\n setDraft('')\n onUtterance(finalText)\n }, SILENCE_MS)\n }\n }\n rec.onerror = () => {}\n rec.onend = () => {\n // Auto-restart while we still want to listen (recognition stops itself).\n if (wantRef.current) {\n try {\n rec.start()\n } catch {\n /* already started */\n }\n } else {\n setListening(false)\n }\n }\n try {\n rec.start()\n recRef.current = rec\n wantRef.current = true\n setListening(true)\n } catch {\n /* ignore */\n }\n }\n\n useEffect(() => () => stop(), [])\n\n return { supported, listening, draft, start, stop }\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { SttPort, SttSession } from '../chat'\n\n// The Azure-style sibling of useCallSpeech: same {start, stop, listening, draft}\n// surface, but driven by an injected SttPort (mic capture + VAD endpointing →\n// recognizer) instead of the browser's Web Speech API. The mic is opened ONCE and\n// kept alive across turns — start()=resume, stop()=pause (anti-echo while the AI\n// speaks) — so there's no per-turn getUserMedia re-prompt or clipped onset. Full\n// release happens on unmount. A no-op (supported=false) when no port is injected.\nexport function useSegmentSpeech(stt: SttPort | undefined, onUtterance: (text: string) => void) {\n const [listening, setListening] = useState(false)\n const sessionRef = useRef<SttSession | null>(null)\n const openingRef = useRef(false)\n const wantRef = useRef(false)\n const onUttRef = useRef(onUtterance)\n onUttRef.current = onUtterance\n\n const start = () => {\n if (!stt || wantRef.current) return\n wantRef.current = true\n setListening(true)\n if (sessionRef.current) {\n sessionRef.current.resume()\n return\n }\n if (openingRef.current) return\n openingRef.current = true\n Promise.resolve(stt.start({ onUtterance: (t) => onUttRef.current(t), onError: () => {} }))\n .then((session) => {\n sessionRef.current = session\n // If stop() was called before the async open finished, don't leave it live.\n if (!wantRef.current) session.pause()\n })\n .catch(() => {})\n .finally(() => {\n openingRef.current = false\n })\n }\n\n const stop = () => {\n wantRef.current = false\n setListening(false)\n sessionRef.current?.pause()\n }\n\n useEffect(\n () => () => {\n sessionRef.current?.stop()\n sessionRef.current = null\n },\n [],\n )\n\n return { supported: !!stt, listening, draft: '', start, stop }\n}\n","import { useEffect, useRef } from 'react'\n\ntype Phase = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended'\n\n/**\n * Audio-reactive waveform. When the reply audio is playing it draws the live\n * frequency spectrum from the injected AnalyserNode; otherwise it shows a gentle\n * idle pulse tuned per call phase (listening brighter, thinking calmer). Pure\n * canvas — no per-bar React state.\n */\nexport function Waveform({ analyser, phase }: { readonly analyser: AnalyserNode | null; readonly phase: Phase }) {\n const canvasRef = useRef<HTMLCanvasElement>(null)\n const analyserRef = useRef(analyser)\n const phaseRef = useRef(phase)\n analyserRef.current = analyser\n phaseRef.current = phase\n\n useEffect(() => {\n const canvas = canvasRef.current\n const ctx = canvas?.getContext('2d')\n if (!canvas || !ctx) return\n const BARS = 40\n let raf = 0\n let t = 0\n let freq: Uint8Array<ArrayBuffer> | null = null\n\n const draw = () => {\n raf = requestAnimationFrame(draw)\n t += 0.06\n const a = analyserRef.current\n const p = phaseRef.current\n const W = canvas.width\n const H = canvas.height\n ctx.clearRect(0, 0, W, H)\n\n const live = a && p === 'speaking'\n if (live) {\n if (!freq || freq.length !== a.frequencyBinCount) freq = new Uint8Array(a.frequencyBinCount)\n a.getByteFrequencyData(freq)\n }\n const bw = W / BARS\n const idleBase = p === 'listening' ? 0.22 : p === 'thinking' ? 0.12 : p === 'connecting' ? 0.08 : 0.05\n\n for (let i = 0; i < BARS; i++) {\n let amp: number\n if (live && freq) {\n // sample low→mid frequencies (where speech energy is), mirror outward\n const m = Math.abs(i - BARS / 2) / (BARS / 2) // 0 center → 1 edges\n const idx = Math.floor((1 - m) * (freq.length * 0.6))\n amp = ((freq[idx] ?? 0) / 255) * (1 - m * 0.35)\n } else {\n amp = idleBase * (0.6 + 0.4 * Math.sin(t + i * 0.4))\n }\n const h = Math.max(3, amp * H)\n const x = i * bw + bw * 0.22\n const y = (H - h) / 2\n ctx.fillStyle = `rgba(168, 85, 247, ${0.45 + amp * 0.55})`\n const r = Math.min(bw * 0.28, h / 2)\n ctx.beginPath()\n ctx.roundRect(x, y, bw * 0.56, h, r)\n ctx.fill()\n }\n }\n draw()\n return () => cancelAnimationFrame(raf)\n }, [])\n\n return <canvas ref={canvasRef} width={640} height={160} className=\"h-40 w-full\" />\n}\n"],"mappings":"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","names":["useEffect","useRef","useState","useEffect","useRef","useState","useEffect","useRef","jsx","useState","useRef","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surfmate.team/digital-human-voice-call",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Voice-only call (no talking-head video): speech in → LLM (ChatPort) → TTS, with an audio-reactive waveform UI. The lightweight sibling of digital-human-video-call — no clip/waiting/playback engine.",
5
5
  "keywords": [
6
6
  "voice",
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "lucide-react": "^0.460.0",
36
- "@surfmate.team/digital-human-ports": "0.1.0"
36
+ "@surfmate.team/digital-human-ports": "0.2.0",
37
+ "@surfmate.team/digital-human-llm": "0.3.1"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/react": "^18.3.0",