@surfmate.team/digital-human-video-call 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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @surfmate.team/digital-human-video-call
2
+
3
+ Full-screen 数字人通话 (video call) — the integration feature: LLM (conversation/ChatPort) + TTS (voice) + talking-head clips (speaking) + waiting-video loops (waiting) into a speak→reply→video→loop call.
4
+
5
+ ```sh
6
+ npm i @surfmate.team/digital-human-video-call
7
+ ```
8
+
9
+ Part of the **@surfmate.team digital-human** suite (ports/DI — the package owns its data + UI/logic; the app injects adapters).
10
+
11
+ ## License
12
+
13
+ MIT
@@ -0,0 +1,47 @@
1
+ import { WaitingVideo, WaitingTier } from '@surfmate.team/digital-human-waiting';
2
+ import { SpeakingClip } from '@surfmate.team/digital-human-speaking';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { CharacterPersona, ChatPort } from '@surfmate.team/digital-human-conversation';
5
+ import { VoiceSynthesisPort } from '@surfmate.team/digital-human-voice';
6
+
7
+ /**
8
+ * Pick a waiting-loop clip for the given tier, preferring the current mood.
9
+ * Falls back tier → primary → any. Deterministic (first match) for v1.
10
+ */
11
+ declare function pickWaitingVideo(videos: readonly WaitingVideo[], tier: WaitingTier, mood?: string): WaitingVideo | null;
12
+ /**
13
+ * Pick the talking-head clip that best fits the AI reply, via the pure
14
+ * clip-core: structural shape match → closest char count (longform/uploads fall
15
+ * back as char-count candidates). Replaces the old "just take the first clip".
16
+ */
17
+ declare function selectSpeakingClip(aiText: string, clips: readonly SpeakingClip[]): SpeakingClip | null;
18
+
19
+ type Props = {
20
+ readonly persona: CharacterPersona;
21
+ readonly chatPort?: ChatPort | undefined;
22
+ readonly synth?: VoiceSynthesisPort | undefined;
23
+ readonly voiceId?: string | undefined;
24
+ readonly voiceModelId?: string | undefined;
25
+ readonly waitingVideos?: readonly WaitingVideo[] | undefined;
26
+ readonly speakingClips?: readonly SpeakingClip[] | undefined;
27
+ readonly onClose: () => void;
28
+ readonly speechLang?: string | undefined;
29
+ };
30
+ /**
31
+ * Full-screen video call — playback engine ported from mate's VideoCallModal.
32
+ * SMOOTH ONLY: every clip plays THROUGH to its natural end at NORMAL 1× speed —
33
+ * never cut, never sped up.
34
+ * - waiting layer = two NON-looping <video> slots, preloaded; loop manually via
35
+ * onEnded → swapToFreshWaiting (flip active + preload next from the right pool).
36
+ * - No-cut gate: the waiting clip keeps looping until the TTS audio is ready,
37
+ * then plays to its natural idle-end; on that end we freeze it (suppress the
38
+ * swap) and reveal the speaking clip on that exact idle pose — precise
39
+ * connection, no fresh clip flashing in.
40
+ * - speaking clips all preload="auto"; active revealed after el.play() resolves,
41
+ * then plays THROUGH to its own native 'ended' before fading back to waiting.
42
+ * - thinking switch waits for the current clip's end too (never cut).
43
+ * - No blur, no speedup.
44
+ */
45
+ declare function VideoCall({ persona, chatPort, synth, voiceId, voiceModelId, waitingVideos, speakingClips, onClose, speechLang, }: Props): react_jsx_runtime.JSX.Element;
46
+
47
+ export { VideoCall, pickWaitingVideo, selectSpeakingClip };
package/dist/index.js ADDED
@@ -0,0 +1,577 @@
1
+ // src/calc/select.ts
2
+ import { selectClip } from "@surfmate.team/digital-human-clip-core";
3
+ function pickWaitingVideo(videos, tier2, mood) {
4
+ const inTier = videos.filter((v) => (v.tier ?? "primary") === tier2);
5
+ const pool = inTier.length ? inTier : videos.filter((v) => (v.tier ?? "primary") === "primary");
6
+ const base = pool.length ? pool : [...videos];
7
+ if (!base.length) return null;
8
+ const moodMatch = mood ? base.filter((v) => v.mood === mood) : [];
9
+ const chosen = moodMatch.length ? moodMatch : base;
10
+ return chosen[0] ?? null;
11
+ }
12
+ function selectSpeakingClip(aiText, clips) {
13
+ const result = selectClip(
14
+ aiText,
15
+ clips.map((c) => ({ id: c.id, sourceText: c.sourceText }))
16
+ );
17
+ if (result.tag === "empty") return null;
18
+ return clips.find((c) => c.id === result.id) ?? null;
19
+ }
20
+
21
+ // src/ui/VideoCall.tsx
22
+ import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
23
+ import { Mic, MicOff, Video, VideoOff, Volume2, VolumeX, PhoneOff, Send, Subtitles, Loader2 } from "lucide-react";
24
+ import {
25
+ buildPrompt,
26
+ parseReply
27
+ } from "@surfmate.team/digital-human-conversation";
28
+
29
+ // src/ui/useCallSpeech.ts
30
+ import { useEffect, useRef, useState } from "react";
31
+ function getCtor() {
32
+ if (typeof window === "undefined") return null;
33
+ const w = window;
34
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
35
+ }
36
+ var SILENCE_MS = 2e3;
37
+ function useCallSpeech(onUtterance, lang = "zh-CN") {
38
+ const [listening, setListening] = useState(false);
39
+ const [draft, setDraft] = useState("");
40
+ const recRef = useRef(null);
41
+ const silenceRef = useRef(null);
42
+ const wantRef = useRef(false);
43
+ const supported = getCtor() !== null;
44
+ const clearSilence = () => {
45
+ if (silenceRef.current) {
46
+ clearTimeout(silenceRef.current);
47
+ silenceRef.current = null;
48
+ }
49
+ };
50
+ const stop = () => {
51
+ wantRef.current = false;
52
+ clearSilence();
53
+ recRef.current?.stop();
54
+ recRef.current = null;
55
+ setListening(false);
56
+ setDraft("");
57
+ };
58
+ const start = () => {
59
+ const Ctor = getCtor();
60
+ if (!Ctor || wantRef.current) return;
61
+ const rec = new Ctor();
62
+ rec.lang = lang;
63
+ rec.continuous = true;
64
+ rec.interimResults = true;
65
+ rec.onresult = (e) => {
66
+ let text = "";
67
+ for (let i = 0; i < e.results.length; i++) {
68
+ const r = e.results[i];
69
+ if (r) text += r[0].transcript;
70
+ }
71
+ setDraft(text);
72
+ clearSilence();
73
+ if (text.trim()) {
74
+ silenceRef.current = setTimeout(() => {
75
+ const finalText = text.trim();
76
+ setDraft("");
77
+ onUtterance(finalText);
78
+ }, SILENCE_MS);
79
+ }
80
+ };
81
+ rec.onerror = () => {
82
+ };
83
+ rec.onend = () => {
84
+ if (wantRef.current) {
85
+ try {
86
+ rec.start();
87
+ } catch {
88
+ }
89
+ } else {
90
+ setListening(false);
91
+ }
92
+ };
93
+ try {
94
+ rec.start();
95
+ recRef.current = rec;
96
+ wantRef.current = true;
97
+ setListening(true);
98
+ } catch {
99
+ }
100
+ };
101
+ useEffect(() => () => stop(), []);
102
+ return { supported, listening, draft, start, stop };
103
+ }
104
+
105
+ // src/ui/VideoCall.tsx
106
+ import { jsx, jsxs } from "react/jsx-runtime";
107
+ var STATUS = {
108
+ connecting: "\u63A5\u901A\u4E2D\u2026",
109
+ listening: "\u8046\u542C\u4E2D\u2026",
110
+ thinking: "\u601D\u8003\u4E2D\u2026",
111
+ speaking: "\u8BF4\u8BDD\u4E2D\u2026",
112
+ ended: "\u5DF2\u7ED3\u675F"
113
+ };
114
+ var GATE_SAFETY_CAP_MS = 15e3;
115
+ var TAIL_SAFETY_CAP_MS = 12e3;
116
+ var layerCls = "absolute inset-0 h-full w-full object-contain";
117
+ var ctlBtn = "flex h-12 w-12 items-center justify-center rounded-full transition";
118
+ var ctlOn = "bg-white/90 text-slate-900 hover:bg-white";
119
+ var ctlOff = "bg-red-600/90 text-white hover:bg-red-600";
120
+ var tier = (v) => v.tier ?? "primary";
121
+ var pickRandom = (pool, exclude) => {
122
+ if (!pool.length) return null;
123
+ const opts = pool.filter((v) => v.url !== exclude);
124
+ const arr = opts.length ? opts : pool;
125
+ return arr[Math.floor(Math.random() * arr.length)]?.url ?? null;
126
+ };
127
+ function VideoCall({
128
+ persona,
129
+ chatPort,
130
+ synth,
131
+ voiceId,
132
+ voiceModelId,
133
+ waitingVideos = [],
134
+ speakingClips = [],
135
+ onClose,
136
+ speechLang
137
+ }) {
138
+ const [callState, setCallState] = useState2("connecting");
139
+ const [mood, setMood] = useState2(void 0);
140
+ const [latestAi, setLatestAi] = useState2(persona.greeting ?? "");
141
+ const [latestUser, setLatestUser] = useState2("");
142
+ const [muted, setMuted] = useState2(false);
143
+ const [videoOn, setVideoOn] = useState2(true);
144
+ const [speakerOn, setSpeakerOn] = useState2(true);
145
+ const [showCaptions, setShowCaptions] = useState2(true);
146
+ const [activeIdx, setActiveIdx] = useState2(-1);
147
+ const [speakingOpacity, setSpeakingOpacity] = useState2(0);
148
+ const [greetingShown, setGreetingShown] = useState2(false);
149
+ const [greetingReady, setGreetingReady] = useState2(false);
150
+ const [typed, setTyped] = useState2("");
151
+ const defaultPool = useMemo(() => waitingVideos.filter((v) => tier(v) !== "idle" && tier(v) !== "thinking"), [waitingVideos]);
152
+ const thinkingPool = useMemo(() => waitingVideos.filter((v) => v.tier === "thinking"), [waitingVideos]);
153
+ const poolForMood = (m) => {
154
+ const base = defaultPool.length ? defaultPool : waitingVideos;
155
+ const moodMatch = m ? base.filter((v) => v.mood === m) : [];
156
+ return moodMatch.length ? moodMatch : base;
157
+ };
158
+ const [waitSlots, setWaitSlots] = useState2(() => {
159
+ const base = waitingVideos.filter((v) => tier(v) !== "idle" && tier(v) !== "thinking");
160
+ const pool = base.length ? base : [...waitingVideos];
161
+ const a = pickRandom(pool);
162
+ return [a, pickRandom(pool, a) ?? a];
163
+ });
164
+ const [waitActive, setWaitActive] = useState2(0);
165
+ const historyRef = useRef2([]);
166
+ const audioRef = useRef2(null);
167
+ const stateRef = useRef2("connecting");
168
+ const moodRef = useRef2(void 0);
169
+ const speakerOnRef = useRef2(true);
170
+ const bootedRef = useRef2(false);
171
+ const greetingEndRef = useRef2(null);
172
+ const speakingEls = useRef2([]);
173
+ const waitEls = useRef2([null, null]);
174
+ const waitActiveRef = useRef2(0);
175
+ const speakingPendingRef = useRef2(false);
176
+ const audioReadyRef = useRef2(false);
177
+ stateRef.current = callState;
178
+ moodRef.current = mood;
179
+ speakerOnRef.current = speakerOn;
180
+ waitActiveRef.current = waitActive;
181
+ const thinkingActiveRef = useRef2(false);
182
+ const swapToFreshWaiting = () => {
183
+ const old = waitActiveRef.current;
184
+ const next = old === 0 ? 1 : 0;
185
+ waitActiveRef.current = next;
186
+ setWaitActive(next);
187
+ setWaitSlots((s) => {
188
+ const showing = s[next];
189
+ const pool = (thinkingActiveRef.current || stateRef.current === "thinking") && thinkingPool.length ? thinkingPool : poolForMood(moodRef.current);
190
+ const fresh = pickRandom(pool, showing) ?? showing;
191
+ return old === 0 ? [fresh, s[1]] : [s[0], fresh];
192
+ });
193
+ };
194
+ const enterThinkingWaiting = () => {
195
+ if (thinkingActiveRef.current || !thinkingPool.length) return;
196
+ thinkingActiveRef.current = true;
197
+ };
198
+ const exitThinkingWaiting = () => {
199
+ thinkingActiveRef.current = false;
200
+ };
201
+ const waitForWaitingClipEnd = (audioReady) => new Promise((resolve) => {
202
+ const els = waitEls.current.filter(Boolean);
203
+ const playing = () => els.find((e) => !e.paused && !e.ended);
204
+ if (!els.length || !playing()) {
205
+ resolve();
206
+ return;
207
+ }
208
+ const t0 = performance.now();
209
+ let done = false;
210
+ let audioReadyAt = 0;
211
+ let readyCt = 0;
212
+ let readyDur = 0;
213
+ let readyState = 0;
214
+ let readyBuf = 0;
215
+ const finish = (why) => {
216
+ if (done) return;
217
+ done = true;
218
+ els.forEach((e) => e.removeEventListener("ended", onEnd));
219
+ const waited = audioReadyAt > 0 ? performance.now() - audioReadyAt : -1;
220
+ console.log(`[gate] reveal via ${why} @ ${(performance.now() - t0).toFixed(0)}ms`);
221
+ if (waited >= 0) {
222
+ const remaining = readyDur - readyCt;
223
+ const jitterMs = waited - remaining * 1e3;
224
+ console.log(
225
+ `%c \u23F3 SMOOTH WAIT ${waited.toFixed(0)}ms = \u7B49\u5F85clip ${readyDur.toFixed(2)}s \u2212 \u5C31\u7EEA\u65F6\u5DF2\u64AD ${readyCt.toFixed(2)}s (\u5269 ${remaining.toFixed(2)}s \u2248 ${(remaining * 1e3).toFixed(0)}ms) ${jitterMs >= 0 ? "+" : "\u2212"}${Math.abs(jitterMs).toFixed(0)}ms \u6296\u52A8 \xB7`,
226
+ "background:#ff2d55;color:#fff;font-size:18px;font-weight:900;padding:6px 10px;border-radius:6px;"
227
+ );
228
+ console.log(
229
+ ` \u2191 \u5C31\u7EEA\u77AC\u95F4 clip \u5728 ${(readyDur ? readyCt / readyDur * 100 : 0).toFixed(0)}% \u5904 \xB7 readyState=${readyState} buffered=${readyBuf.toFixed(2)}/${readyDur.toFixed(2)}${readyBuf + 0.01 < readyDur ? " \u26A0\uFE0F\u672A\u7F13\u51B2\u5B8C" : " \u2713\u5DF2\u7F13\u51B2"}`
230
+ );
231
+ }
232
+ resolve();
233
+ };
234
+ const onEnd = () => {
235
+ if (!audioReadyRef.current) return;
236
+ finish("waiting clip ended");
237
+ };
238
+ els.forEach((e) => e.addEventListener("ended", onEnd));
239
+ audioReady.then(() => {
240
+ audioReadyAt = performance.now();
241
+ const el = playing();
242
+ if (el) {
243
+ readyCt = el.currentTime;
244
+ readyDur = isFinite(el.duration) ? el.duration : 0;
245
+ readyState = el.readyState;
246
+ readyBuf = el.buffered.length ? el.buffered.end(el.buffered.length - 1) : 0;
247
+ }
248
+ console.log(`[gate] \u2705 all ready \u2014 waiting clip \u5728 ${readyCt.toFixed(2)}/${readyDur.toFixed(2)}s,\u9884\u8BA1\u8FD8\u9700\u7B49 ${((readyDur - readyCt) * 1e3).toFixed(0)}ms \u5230\u5B83\u64AD\u5B8C`);
249
+ }).catch(() => void 0);
250
+ setTimeout(() => finish("safety cap"), GATE_SAFETY_CAP_MS);
251
+ });
252
+ useEffect2(() => {
253
+ waitEls.current.forEach((el) => {
254
+ if (!el) return;
255
+ el.muted = true;
256
+ el.play().then(() => el.pause()).catch(() => void 0);
257
+ });
258
+ }, []);
259
+ useEffect2(() => {
260
+ if (callState === "connecting") return;
261
+ waitEls.current.forEach((el, i) => {
262
+ if (!el) return;
263
+ if (i === waitActive) {
264
+ if (el.paused) {
265
+ el.currentTime = 0;
266
+ el.play().catch(() => void 0);
267
+ }
268
+ } else {
269
+ el.pause();
270
+ }
271
+ });
272
+ }, [waitActive, waitSlots, callState]);
273
+ const playAudio = (bytes) => new Promise((resolve) => {
274
+ audioRef.current?.pause();
275
+ const url = URL.createObjectURL(new Blob([bytes], { type: "audio/mpeg" }));
276
+ const audio = new Audio(url);
277
+ audio.muted = !speakerOnRef.current;
278
+ audioRef.current = audio;
279
+ const done = () => {
280
+ URL.revokeObjectURL(url);
281
+ resolve();
282
+ };
283
+ audio.onplaying = () => console.log(`[turn] \u{1F50A} audio playing (dur\u2248${audio.duration?.toFixed(2) ?? "?"}s)`);
284
+ audio.onended = () => {
285
+ console.log("[turn] \u{1F507} audio ended");
286
+ done();
287
+ };
288
+ audio.onerror = done;
289
+ void audio.play().catch(done);
290
+ });
291
+ const playGreetingVideo = () => new Promise((resolve) => {
292
+ setCallState("speaking");
293
+ setGreetingShown(true);
294
+ greetingEndRef.current = () => {
295
+ setGreetingShown(false);
296
+ setGreetingReady(false);
297
+ resolve();
298
+ };
299
+ });
300
+ const speakReply = async (text, emotion) => {
301
+ setCallState("speaking");
302
+ const clip = selectSpeakingClip(text, speakingClips);
303
+ const idx = clip ? speakingClips.findIndex((c) => c.id === clip.id) : -1;
304
+ console.log(`[speaking] pick: ${clip ? `id=${clip.id} kind=${clip.kind ?? "preset"} idx=${idx}` : "NO CLIP (TTS over waiting)"} \xB7 ${speakingClips.length} clips \xB7 emotion=${emotion ?? "\u2014"}`);
305
+ speakingPendingRef.current = true;
306
+ audioReadyRef.current = false;
307
+ try {
308
+ const synthP = synth && voiceId ? synth.synthesize({ text, voiceId, ...voiceModelId ? { modelId: voiceModelId } : {}, ...emotion ? { emotion } : {} }) : Promise.resolve(null);
309
+ const audioReady = synthP.then(() => {
310
+ audioReadyRef.current = true;
311
+ }).catch(() => {
312
+ audioReadyRef.current = true;
313
+ });
314
+ await waitForWaitingClipEnd(audioReady);
315
+ const bytes = await synthP.catch(() => null);
316
+ thinkingActiveRef.current = false;
317
+ if (idx >= 0) {
318
+ setActiveIdx(idx);
319
+ const el2 = speakingEls.current[idx];
320
+ if (el2) {
321
+ console.log(`[speaking] el.duration=${isFinite(el2.duration) ? el2.duration.toFixed(2) : "NaN"} \u2192 play()`);
322
+ el2.currentTime = 0;
323
+ el2.loop = true;
324
+ el2.play().then(() => {
325
+ console.log("[speaking] \u25B6 revealed");
326
+ setSpeakingOpacity(1);
327
+ }).catch(() => setSpeakingOpacity(1));
328
+ } else {
329
+ setSpeakingOpacity(1);
330
+ }
331
+ }
332
+ if (bytes) await playAudio(bytes);
333
+ else await new Promise((r) => setTimeout(r, Math.min(6e3, 1200 + text.length * 60)));
334
+ const el = idx >= 0 ? speakingEls.current[idx] : null;
335
+ if (el) {
336
+ el.loop = false;
337
+ if (el.ended || isFinite(el.duration) && el.duration - el.currentTime <= 0.1) {
338
+ console.log(`[speaking] audio ended: clip already at end (ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : "?"}) \u2192 fade`);
339
+ } else {
340
+ const tEnd = performance.now();
341
+ console.log(`[speaking] audio ended: play clip THROUGH to natural end (ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : "?"})`);
342
+ await new Promise((resolve) => {
343
+ let done = false;
344
+ const finish = (why) => {
345
+ if (done) return;
346
+ done = true;
347
+ el.removeEventListener("ended", onNativeEnd);
348
+ console.log(`[speaking] clip end via ${why} @ ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : "?"} (+${((performance.now() - tEnd) / 1e3).toFixed(2)}s) \u2192 fade`);
349
+ resolve();
350
+ };
351
+ const onNativeEnd = () => finish("native ended");
352
+ el.addEventListener("ended", onNativeEnd);
353
+ setTimeout(() => finish("safety cap"), TAIL_SAFETY_CAP_MS);
354
+ });
355
+ }
356
+ }
357
+ } finally {
358
+ speakingPendingRef.current = false;
359
+ audioReadyRef.current = false;
360
+ setSpeakingOpacity(0);
361
+ console.log("[speaking] \u23F9 faded \u2192 waiting");
362
+ }
363
+ };
364
+ const handleUtterance = async (text) => {
365
+ const t = text.trim();
366
+ if (!t || !chatPort) return;
367
+ if (stateRef.current === "thinking" || stateRef.current === "speaking") {
368
+ console.log(`[turn] \u26D4 ignored "${t.slice(0, 30)}" \u2014 busy (${stateRef.current}); LLM NOT called`);
369
+ return;
370
+ }
371
+ const t0 = performance.now();
372
+ console.log("[turn] \u23F1 0ms \u2014 user submitted:", t);
373
+ speech.stop();
374
+ setLatestUser(t);
375
+ setCallState("thinking");
376
+ const history = historyRef.current;
377
+ historyRef.current = [...history, { id: crypto.randomUUID(), role: "user", content: t }];
378
+ try {
379
+ const raw = await chatPort.complete(buildPrompt(persona, history, t));
380
+ const reply = parseReply(raw);
381
+ console.log(`[turn] \u{1F9E0} LLM ${(performance.now() - t0).toFixed(0)}ms \u2192 mood=${reply.mood} text="${reply.text.slice(0, 40)}"`);
382
+ historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: "assistant", content: reply.text }];
383
+ setLatestAi(reply.text);
384
+ setMood(reply.mood);
385
+ await speakReply(reply.text, reply.mood);
386
+ console.log(`[turn] \u{1F3C1} done ${(performance.now() - t0).toFixed(0)}ms`);
387
+ } catch (err) {
388
+ setLatestAi(`(\u51FA\u9519:${err instanceof Error ? err.message : String(err)})`);
389
+ } finally {
390
+ setCallState("listening");
391
+ }
392
+ };
393
+ const speech = useCallSpeech(handleUtterance, speechLang);
394
+ useEffect2(() => {
395
+ if (bootedRef.current) return;
396
+ bootedRef.current = true;
397
+ void (async () => {
398
+ try {
399
+ if (persona.greetingVideoUrl) await playGreetingVideo();
400
+ else if (persona.greeting && synth && voiceId) await speakReply(persona.greeting);
401
+ } finally {
402
+ setCallState("listening");
403
+ }
404
+ })();
405
+ }, []);
406
+ useEffect2(
407
+ () => () => {
408
+ audioRef.current?.pause();
409
+ speech.stop();
410
+ },
411
+ // eslint-disable-next-line react-hooks/exhaustive-deps
412
+ []
413
+ );
414
+ useEffect2(() => {
415
+ if (callState === "listening" && !muted) speech.start();
416
+ else speech.stop();
417
+ }, [callState, muted]);
418
+ const inputActive = typed.trim().length > 0 || speech.draft.trim().length > 0;
419
+ useEffect2(() => {
420
+ if (inputActive) enterThinkingWaiting();
421
+ else if (callState === "listening") exitThinkingWaiting();
422
+ }, [inputActive]);
423
+ const toggleSpeaker = () => {
424
+ setSpeakerOn((on) => {
425
+ const next = !on;
426
+ if (audioRef.current) audioRef.current.muted = !next;
427
+ return next;
428
+ });
429
+ };
430
+ const endCall = () => {
431
+ setCallState("ended");
432
+ audioRef.current?.pause();
433
+ speech.stop();
434
+ onClose();
435
+ };
436
+ const sendTyped = () => {
437
+ const t = typed.trim();
438
+ if (!t) return;
439
+ setTyped("");
440
+ void handleUtterance(t);
441
+ };
442
+ const thinking = callState === "thinking";
443
+ const hasWaiting = !!(waitSlots[0] || waitSlots[1]);
444
+ return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-black text-white", children: [
445
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-0 overflow-hidden bg-black", children: [
446
+ videoOn && hasWaiting ? [0, 1].map((slot) => {
447
+ const url = waitSlots[slot];
448
+ return url ? /* @__PURE__ */ jsx(
449
+ "video",
450
+ {
451
+ ref: (el) => {
452
+ waitEls.current[slot] = el;
453
+ },
454
+ src: url,
455
+ preload: "auto",
456
+ muted: true,
457
+ playsInline: true,
458
+ onEnded: slot === waitActive ? () => {
459
+ if (speakingPendingRef.current && audioReadyRef.current) return;
460
+ swapToFreshWaiting();
461
+ } : void 0,
462
+ className: layerCls,
463
+ style: {
464
+ // Active waiting slot stays a SOLID opacity-1 layer — it does
465
+ // NOT fade with speakingOpacity. The speaking layer (zIndex 2)
466
+ // fades in ON TOP of it, so there's no moment where both are
467
+ // partly transparent and the black stage bleeds through (the
468
+ // old `1 - speakingOpacity` + conditional-transition combo hit a
469
+ // same-frame CSS gotcha: opacity snapped to 0 before the fade
470
+ // registered → a quick black flash). Inactive slot = 0. Swaps
471
+ // stay instant (no transition) — same idle pose, invisible.
472
+ opacity: slot === waitActive ? 1 : 0,
473
+ transition: "none"
474
+ }
475
+ },
476
+ slot
477
+ ) : null;
478
+ }) : !videoOn || !hasWaiting ? persona.avatarUrl ? /* @__PURE__ */ jsx("img", { src: persona.avatarUrl, alt: persona.name, className: `${layerCls} opacity-90` }) : /* @__PURE__ */ jsx("div", { className: "h-full w-full bg-gradient-to-b from-slate-800 to-slate-950" }) : null,
479
+ videoOn ? speakingClips.map((clip, i) => /* @__PURE__ */ jsx(
480
+ "video",
481
+ {
482
+ ref: (el) => {
483
+ speakingEls.current[i] = el;
484
+ },
485
+ src: clip.videoUrl,
486
+ muted: true,
487
+ loop: true,
488
+ playsInline: true,
489
+ preload: "auto",
490
+ className: layerCls,
491
+ style: { opacity: i === activeIdx ? speakingOpacity : 0, transition: i === activeIdx ? "opacity 0.25s ease" : "none", zIndex: 2 }
492
+ },
493
+ clip.id
494
+ )) : null,
495
+ videoOn && greetingShown && persona.greetingVideoUrl ? /* @__PURE__ */ jsx(
496
+ "video",
497
+ {
498
+ src: persona.greetingVideoUrl,
499
+ autoPlay: true,
500
+ playsInline: true,
501
+ onPlaying: () => setGreetingReady(true),
502
+ onEnded: () => greetingEndRef.current?.(),
503
+ onError: () => greetingEndRef.current?.(),
504
+ className: layerCls,
505
+ style: { opacity: greetingReady ? 1 : 0, transition: "opacity 0.25s ease", zIndex: 3 }
506
+ },
507
+ persona.greetingVideoUrl
508
+ ) : null
509
+ ] }),
510
+ /* @__PURE__ */ jsxs(
511
+ "div",
512
+ {
513
+ className: `absolute inset-0 z-20 flex flex-col items-center justify-center gap-5 bg-black transition-opacity duration-500 ${callState === "connecting" ? "opacity-100" : "pointer-events-none opacity-0"}`,
514
+ children: [
515
+ persona.avatarUrl ? /* @__PURE__ */ jsx("img", { src: persona.avatarUrl, alt: persona.name, className: "h-24 w-24 rounded-full object-cover ring-2 ring-white/20" }) : null,
516
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-white/80", children: [
517
+ /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
518
+ /* @__PURE__ */ jsxs("span", { className: "text-sm", children: [
519
+ "\u63A5\u901A ",
520
+ persona.name,
521
+ "\u2026"
522
+ ] })
523
+ ] })
524
+ ]
525
+ }
526
+ ),
527
+ /* @__PURE__ */ jsxs("div", { className: "relative z-10 flex items-center justify-between p-4", children: [
528
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-full bg-black/40 px-3 py-1.5 backdrop-blur", children: [
529
+ /* @__PURE__ */ jsx("span", { className: `h-2 w-2 rounded-full ${thinking ? "animate-pulse bg-amber-400" : callState === "speaking" ? "bg-green-400" : "bg-white/60"}` }),
530
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: persona.name }),
531
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-white/60", children: [
532
+ "\xB7 ",
533
+ STATUS[callState]
534
+ ] })
535
+ ] }),
536
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setShowCaptions((v) => !v), title: "\u5B57\u5E55", className: `rounded-full p-2 backdrop-blur transition ${showCaptions ? "bg-white/20" : "bg-black/40 text-white/60"}`, children: /* @__PURE__ */ jsx(Subtitles, { className: "h-4 w-4" }) })
537
+ ] }),
538
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
539
+ showCaptions ? /* @__PURE__ */ jsxs("div", { className: "relative z-10 space-y-2 px-6 pb-2", children: [
540
+ latestUser ? /* @__PURE__ */ jsx("p", { className: "ml-auto max-w-[80%] text-right text-sm text-white/70", children: /* @__PURE__ */ jsx("span", { className: "rounded-2xl bg-white/10 px-3 py-1.5 backdrop-blur", children: latestUser }) }) : null,
541
+ latestAi ? /* @__PURE__ */ jsx("p", { className: "max-w-[80%] text-sm", children: /* @__PURE__ */ jsx("span", { className: "inline-block rounded-2xl bg-black/50 px-3 py-1.5 leading-relaxed backdrop-blur", children: thinking ? /* @__PURE__ */ jsx(Loader2, { className: "inline h-3.5 w-3.5 animate-spin" }) : `${persona.name}: ${latestAi}` }) }) : null
542
+ ] }) : null,
543
+ /* @__PURE__ */ jsxs("div", { className: "relative z-10 flex flex-col gap-3 bg-gradient-to-t from-black/80 to-transparent px-4 pb-5 pt-4", children: [
544
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-4", children: [
545
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setMuted((v) => !v), title: muted ? "\u53D6\u6D88\u9759\u97F3" : "\u9EA6\u514B\u98CE\u9759\u97F3", className: `${ctlBtn} ${muted ? ctlOff : ctlOn}`, children: muted ? /* @__PURE__ */ jsx(MicOff, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(Mic, { className: "h-5 w-5" }) }),
546
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setVideoOn((v) => !v), title: videoOn ? "\u5173\u95ED\u89C6\u9891" : "\u5F00\u542F\u89C6\u9891", className: `${ctlBtn} ${videoOn ? ctlOn : ctlOff}`, children: videoOn ? /* @__PURE__ */ jsx(Video, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(VideoOff, { className: "h-5 w-5" }) }),
547
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: toggleSpeaker, title: speakerOn ? "\u5173\u95ED\u5916\u653E" : "\u5F00\u542F\u5916\u653E", className: `${ctlBtn} ${speakerOn ? ctlOn : ctlOff}`, children: speakerOn ? /* @__PURE__ */ jsx(Volume2, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(VolumeX, { className: "h-5 w-5" }) }),
548
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: endCall, title: "\u6302\u65AD", className: "flex h-14 w-14 items-center justify-center rounded-full bg-red-600 text-white transition hover:bg-red-700", children: /* @__PURE__ */ jsx(PhoneOff, { className: "h-6 w-6" }) })
549
+ ] }),
550
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
551
+ /* @__PURE__ */ jsx(
552
+ "input",
553
+ {
554
+ value: typed,
555
+ onChange: (e) => setTyped(e.target.value),
556
+ onKeyDown: (e) => {
557
+ if (e.key === "Enter") {
558
+ e.preventDefault();
559
+ sendTyped();
560
+ }
561
+ },
562
+ disabled: !chatPort || thinking,
563
+ placeholder: chatPort ? "\u8BF4\u8BDD,\u6216\u5728\u6B64\u8F93\u5165\u2026" : "\u9700\u6CE8\u5165 ChatPort",
564
+ 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"
565
+ }
566
+ ),
567
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: sendTyped, disabled: !chatPort || thinking || !typed.trim(), className: "flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-900 transition disabled:opacity-40", children: /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" }) })
568
+ ] })
569
+ ] })
570
+ ] });
571
+ }
572
+ export {
573
+ VideoCall,
574
+ pickWaitingVideo,
575
+ selectSpeakingClip
576
+ };
577
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/calc/select.ts","../src/ui/VideoCall.tsx","../src/ui/useCallSpeech.ts"],"sourcesContent":["// Pure selection helpers (calculation) for the call.\n\nimport type { WaitingVideo, WaitingTier } from '@surfmate.team/digital-human-waiting'\nimport type { SpeakingClip } from '@surfmate.team/digital-human-speaking'\nimport { selectClip } from '@surfmate.team/digital-human-clip-core'\n\n/**\n * Pick a waiting-loop clip for the given tier, preferring the current mood.\n * Falls back tier → primary → any. Deterministic (first match) for v1.\n */\nexport function pickWaitingVideo(\n videos: readonly WaitingVideo[],\n tier: WaitingTier,\n mood?: string,\n): WaitingVideo | null {\n const inTier = videos.filter((v) => (v.tier ?? 'primary') === tier)\n const pool = inTier.length ? inTier : videos.filter((v) => (v.tier ?? 'primary') === 'primary')\n const base = pool.length ? pool : [...videos]\n if (!base.length) return null\n const moodMatch = mood ? base.filter((v) => v.mood === mood) : []\n const chosen = moodMatch.length ? moodMatch : base\n return chosen[0] ?? null\n}\n\n/**\n * Pick the talking-head clip that best fits the AI reply, via the pure\n * clip-core: structural shape match → closest char count (longform/uploads fall\n * back as char-count candidates). Replaces the old \"just take the first clip\".\n */\nexport function selectSpeakingClip(aiText: string, clips: readonly SpeakingClip[]): SpeakingClip | null {\n const result = selectClip(\n aiText,\n clips.map((c) => ({ id: c.id, sourceText: c.sourceText })),\n )\n if (result.tag === 'empty') return null\n return clips.find((c) => c.id === result.id) ?? null\n}\n","import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Mic, MicOff, Video, VideoOff, Volume2, VolumeX, PhoneOff, Send, Subtitles, Loader2 } from 'lucide-react'\nimport {\n buildPrompt,\n parseReply,\n type CharacterPersona,\n type ChatMessage,\n type ChatMood,\n type ChatPort,\n} from '@surfmate.team/digital-human-conversation'\nimport type { VoiceSynthesisPort } from '@surfmate.team/digital-human-voice'\nimport type { WaitingVideo } from '@surfmate.team/digital-human-waiting'\nimport type { SpeakingClip } from '@surfmate.team/digital-human-speaking'\nimport { selectSpeakingClip } from '../calc/select'\nimport { useCallSpeech } from './useCallSpeech'\n\ntype CallState = 'connecting' | 'listening' | 'thinking' | 'speaking' | 'ended'\n\ntype Props = {\n readonly persona: CharacterPersona\n readonly chatPort?: ChatPort | undefined\n readonly synth?: VoiceSynthesisPort | undefined\n readonly voiceId?: string | undefined\n readonly voiceModelId?: string | undefined\n readonly waitingVideos?: readonly WaitingVideo[] | undefined\n readonly speakingClips?: readonly SpeakingClip[] | undefined\n readonly onClose: () => 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 GATE_SAFETY_CAP_MS = 15000 // hard backstop so the gate can never hang if 'ended' never fires\nconst TAIL_SAFETY_CAP_MS = 12000 // hard backstop so the speaking tail can never hang\nconst layerCls = 'absolute inset-0 h-full w-full object-contain'\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\nconst tier = (v: WaitingVideo): string => v.tier ?? 'primary'\nconst pickRandom = (pool: readonly WaitingVideo[], exclude?: string | null): string | null => {\n if (!pool.length) return null\n const opts = pool.filter((v) => v.url !== exclude)\n const arr = opts.length ? opts : pool\n return arr[Math.floor(Math.random() * arr.length)]?.url ?? null\n}\n\n/**\n * Full-screen video call — playback engine ported from mate's VideoCallModal.\n * SMOOTH ONLY: every clip plays THROUGH to its natural end at NORMAL 1× speed —\n * never cut, never sped up.\n * - waiting layer = two NON-looping <video> slots, preloaded; loop manually via\n * onEnded → swapToFreshWaiting (flip active + preload next from the right pool).\n * - No-cut gate: the waiting clip keeps looping until the TTS audio is ready,\n * then plays to its natural idle-end; on that end we freeze it (suppress the\n * swap) and reveal the speaking clip on that exact idle pose — precise\n * connection, no fresh clip flashing in.\n * - speaking clips all preload=\"auto\"; active revealed after el.play() resolves,\n * then plays THROUGH to its own native 'ended' before fading back to waiting.\n * - thinking switch waits for the current clip's end too (never cut).\n * - No blur, no speedup.\n */\nexport function VideoCall({\n persona,\n chatPort,\n synth,\n voiceId,\n voiceModelId,\n waitingVideos = [],\n speakingClips = [],\n onClose,\n speechLang,\n}: Props) {\n const [callState, setCallState] = useState<CallState>('connecting')\n const [mood, setMood] = useState<ChatMood | undefined>(undefined)\n const [latestAi, setLatestAi] = useState(persona.greeting ?? '')\n const [latestUser, setLatestUser] = useState('')\n const [muted, setMuted] = useState(false)\n const [videoOn, setVideoOn] = useState(true)\n const [speakerOn, setSpeakerOn] = useState(true)\n const [showCaptions, setShowCaptions] = useState(true)\n const [activeIdx, setActiveIdx] = useState(-1)\n const [speakingOpacity, setSpeakingOpacity] = useState(0)\n const [greetingShown, setGreetingShown] = useState(false)\n const [greetingReady, setGreetingReady] = useState(false)\n const [typed, setTyped] = useState('')\n\n const defaultPool = useMemo(() => waitingVideos.filter((v) => tier(v) !== 'idle' && tier(v) !== 'thinking'), [waitingVideos])\n const thinkingPool = useMemo(() => waitingVideos.filter((v) => v.tier === 'thinking'), [waitingVideos])\n const poolForMood = (m?: string): readonly WaitingVideo[] => {\n const base = defaultPool.length ? defaultPool : waitingVideos\n const moodMatch = m ? base.filter((v) => v.mood === m) : []\n return moodMatch.length ? moodMatch : base\n }\n\n // Two non-looping waiting slots (mate): slot 0 plays, slot 1 preloads next.\n const [waitSlots, setWaitSlots] = useState<[string | null, string | null]>(() => {\n const base = waitingVideos.filter((v) => tier(v) !== 'idle' && tier(v) !== 'thinking')\n const pool = base.length ? base : [...waitingVideos]\n const a = pickRandom(pool)\n return [a, pickRandom(pool, a) ?? a]\n })\n const [waitActive, setWaitActive] = useState<0 | 1>(0)\n\n const historyRef = useRef<ChatMessage[]>([])\n const audioRef = useRef<HTMLAudioElement | null>(null)\n const stateRef = useRef<CallState>('connecting')\n const moodRef = useRef<ChatMood | undefined>(undefined)\n const speakerOnRef = useRef(true)\n const bootedRef = useRef(false)\n const greetingEndRef = useRef<(() => void) | null>(null)\n const speakingEls = useRef<(HTMLVideoElement | null)[]>([])\n const waitEls = useRef<[HTMLVideoElement | null, HTMLVideoElement | null]>([null, null])\n const waitActiveRef = useRef<0 | 1>(0)\n // A speaking turn is staging its reveal. While true AND audio is ready, the\n // waiting clip must NOT swap to a fresh clip on its 'ended' — it freezes on its\n // idle-end frame so the speaking clip (idle-start) reveals on the SAME pose\n // (precise connection). Before audio is ready it keeps looping (swap allowed).\n const speakingPendingRef = useRef(false)\n const audioReadyRef = useRef(false)\n stateRef.current = callState\n moodRef.current = mood\n speakerOnRef.current = speakerOn\n waitActiveRef.current = waitActive\n const thinkingActiveRef = useRef(false)\n\n // Manual loop / swap (mate swapToFreshWaiting): flip to the preloaded inactive\n // slot, preload a fresh clip into the now-inactive slot (thinking pool while\n // thinking, else mood/default pool).\n const swapToFreshWaiting = () => {\n const old = waitActiveRef.current\n const next: 0 | 1 = old === 0 ? 1 : 0\n waitActiveRef.current = next\n setWaitActive(next)\n setWaitSlots((s) => {\n const showing = s[next]\n const pool =\n (thinkingActiveRef.current || stateRef.current === 'thinking') && thinkingPool.length\n ? thinkingPool\n : poolForMood(moodRef.current)\n const fresh = pickRandom(pool, showing) ?? showing\n return old === 0 ? [fresh, s[1]] : [s[0], fresh]\n })\n }\n\n // Input start (typing / speaking) → arm the thinking pool. We NEVER flip the\n // waiting video mid-play (that would cut the current clip). Just set the flag;\n // the current clip plays to completion and the next natural clip-end swap picks\n // the thinking pool (swapToFreshWaiting). callState is untouched, so the submit\n // guard never blocks the LLM. Idempotent; no-op without thinking clips.\n const enterThinkingWaiting = () => {\n if (thinkingActiveRef.current || !thinkingPool.length) return\n thinkingActiveRef.current = true\n }\n // Drop the flag — the next natural clip-end swap reverts to default (so the\n // current thinking clip isn't cut mid-play).\n const exitThinkingWaiting = () => {\n thinkingActiveRef.current = false\n }\n\n // No-cut gate (mate waitForWaitingClipEnd): the waiting clip plays at NORMAL 1×\n // speed (NO speedup, NO cap) and we resolve only on the idle-end that happens\n // AFTER the TTS audio is ready — so the waiting clip is never cut and the reveal\n // lands on a real idle boundary with audio in hand. Until audio is ready the\n // clip just keeps looping (the swap fires). A large safety cap prevents a hang.\n const waitForWaitingClipEnd = (audioReady: Promise<unknown>): Promise<void> =>\n new Promise((resolve) => {\n const els = waitEls.current.filter(Boolean) as HTMLVideoElement[]\n const playing = () => els.find((e) => !e.paused && !e.ended)\n if (!els.length || !playing()) {\n resolve()\n return\n }\n const t0 = performance.now()\n let done = false\n // When everything (LLM + TTS audio) is READY — the timestamp from which we\n // then sit waiting for the current waiting clip to finish playing to its\n // idle-end. THE core cost of smooth mode: the \"empty wait\" the user wants\n // measured, super-highlighted, every turn.\n let audioReadyAt = 0\n // Snapshot of the playing waiting clip AT audio-ready — so SMOOTH WAIT can\n // show its own derivation (clip length − already-played = remaining ≈ wait).\n let readyCt = 0\n let readyDur = 0\n let readyState = 0\n let readyBuf = 0\n const finish = (why: string) => {\n if (done) return\n done = true\n els.forEach((e) => e.removeEventListener('ended', onEnd))\n const waited = audioReadyAt > 0 ? performance.now() - audioReadyAt : -1\n console.log(`[gate] reveal via ${why} @ ${(performance.now() - t0).toFixed(0)}ms`)\n if (waited >= 0) {\n const remaining = readyDur - readyCt // expected wait, from the phase at ready\n const jitterMs = waited - remaining * 1000 // event/decode slack over the expected\n // ── SUPER HIGHLIGHTED, WITH THE MATH: how the number is derived.\n console.log(\n `%c ⏳ SMOOTH WAIT ${waited.toFixed(0)}ms = 等待clip ${readyDur.toFixed(2)}s − 就绪时已播 ${readyCt.toFixed(2)}s (剩 ${remaining.toFixed(2)}s ≈ ${(remaining * 1000).toFixed(0)}ms) ${jitterMs >= 0 ? '+' : '−'}${Math.abs(jitterMs).toFixed(0)}ms 抖动 ·`,\n 'background:#ff2d55;color:#fff;font-size:18px;font-weight:900;padding:6px 10px;border-radius:6px;',\n )\n console.log(\n ` ↑ 就绪瞬间 clip 在 ${(readyDur ? (readyCt / readyDur) * 100 : 0).toFixed(0)}% 处 · readyState=${readyState} buffered=${readyBuf.toFixed(2)}/${readyDur.toFixed(2)}${readyBuf + 0.01 < readyDur ? ' ⚠️未缓冲完' : ' ✓已缓冲'}`,\n )\n }\n resolve()\n }\n const onEnd = () => {\n if (!audioReadyRef.current) return // audio not ready → let it loop (swap fires)\n finish('waiting clip ended')\n }\n els.forEach((e) => e.addEventListener('ended', onEnd))\n audioReady\n .then(() => {\n audioReadyAt = performance.now()\n const el = playing()\n if (el) {\n readyCt = el.currentTime\n readyDur = isFinite(el.duration) ? el.duration : 0\n readyState = el.readyState\n readyBuf = el.buffered.length ? el.buffered.end(el.buffered.length - 1) : 0\n }\n console.log(`[gate] ✅ all ready — waiting clip 在 ${readyCt.toFixed(2)}/${readyDur.toFixed(2)}s,预计还需等 ${((readyDur - readyCt) * 1000).toFixed(0)}ms 到它播完`)\n })\n .catch(() => undefined)\n setTimeout(() => finish('safety cap'), GATE_SAFETY_CAP_MS)\n })\n\n // Prewarm: at call-open, kick BOTH waiting slots (muted) to fetch + decode now,\n // so the FIRST turn's loop doesn't stall on cold media — the main reason turn 1's\n // SMOOTH WAIT is the largest (clips aren't in the browser cache yet). Play→pause\n // forces the decode; the playback owner below takes over once the call connects.\n useEffect(() => {\n waitEls.current.forEach((el) => {\n if (!el) return\n el.muted = true\n el.play().then(() => el.pause()).catch(() => undefined)\n })\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Waiting playback owner: the active slot plays (from 0), the inactive pauses.\n useEffect(() => {\n if (callState === 'connecting') return\n waitEls.current.forEach((el, i) => {\n if (!el) return\n if (i === waitActive) {\n if (el.paused) {\n el.currentTime = 0\n el.play().catch(() => undefined)\n }\n } else {\n el.pause()\n }\n })\n }, [waitActive, waitSlots, callState])\n\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 audio.muted = !speakerOnRef.current\n audioRef.current = audio\n const done = () => {\n URL.revokeObjectURL(url)\n resolve()\n }\n audio.onplaying = () => console.log(`[turn] 🔊 audio playing (dur≈${audio.duration?.toFixed(2) ?? '?'}s)`)\n audio.onended = () => {\n console.log('[turn] 🔇 audio ended')\n done()\n }\n audio.onerror = done\n void audio.play().catch(done)\n })\n\n const playGreetingVideo = (): Promise<void> =>\n new Promise((resolve) => {\n setCallState('speaking')\n setGreetingShown(true)\n greetingEndRef.current = () => {\n setGreetingShown(false)\n setGreetingReady(false)\n resolve()\n }\n })\n\n const speakReply = async (text: string, emotion?: string) => {\n setCallState('speaking')\n const clip = selectSpeakingClip(text, speakingClips)\n const idx = clip ? speakingClips.findIndex((c) => c.id === clip.id) : -1\n console.log(`[speaking] pick: ${clip ? `id=${clip.id} kind=${clip.kind ?? 'preset'} idx=${idx}` : 'NO CLIP (TTS over waiting)'} · ${speakingClips.length} clips · emotion=${emotion ?? '—'}`)\n // Stage the reveal: until audio is ready the waiting clip keeps looping; once\n // ready, its next idle-end freezes (no swap) so speaking reveals on that pose.\n speakingPendingRef.current = true\n audioReadyRef.current = false\n try {\n // Synthesize in parallel with the no-cut gate; flag audio-ready when done.\n const synthP: Promise<ArrayBuffer | null> =\n synth && voiceId\n ? synth.synthesize({ text, voiceId, ...(voiceModelId ? { modelId: voiceModelId } : {}), ...(emotion ? { emotion } : {}) })\n : Promise.resolve(null)\n const audioReady = synthP.then(() => { audioReadyRef.current = true }).catch(() => { audioReadyRef.current = true })\n\n // NO-CUT GATE: let the waiting clip reach its idle end (after audio ready).\n await waitForWaitingClipEnd(audioReady)\n const bytes = await synthP.catch(() => null)\n\n // Reveal ONLY once the clip is actually playing (mate): revealing while\n // play() is still pending can flash a frozen frame-0 for a beat. play()\n // resolve → reveal in lockstep with motion; reject (autoplay race) →\n // reveal anyway (the freeze-watchdog will kick it).\n thinkingActiveRef.current = false // AI is speaking now → thinking phase over\n if (idx >= 0) {\n setActiveIdx(idx)\n const el = speakingEls.current[idx]\n if (el) {\n console.log(`[speaking] el.duration=${isFinite(el.duration) ? el.duration.toFixed(2) : 'NaN'} → play()`)\n el.currentTime = 0\n el.loop = true\n el.play().then(() => { console.log('[speaking] ▶ revealed'); setSpeakingOpacity(1) }).catch(() => setSpeakingOpacity(1))\n } else {\n setSpeakingOpacity(1)\n }\n }\n if (bytes) await playAudio(bytes)\n else await new Promise((r) => setTimeout(r, Math.min(6000, 1200 + text.length * 60)))\n\n // Play the speaking clip THROUGH to its real end, THEN fade — no cut. The\n // end is driven off the native 'ended' event (NOT a duration timer), so a\n // clip whose metadata under-reports its length can't be cut short. Stop\n // looping so it ends naturally; if it already ended at audio-stop, fade now.\n const el = idx >= 0 ? speakingEls.current[idx] : null\n if (el) {\n el.loop = false\n if (el.ended || (isFinite(el.duration) && el.duration - el.currentTime <= 0.1)) {\n console.log(`[speaking] audio ended: clip already at end (ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : '?'}) → fade`)\n } else {\n const tEnd = performance.now()\n console.log(`[speaking] audio ended: play clip THROUGH to natural end (ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : '?'})`)\n await new Promise<void>((resolve) => {\n let done = false\n const finish = (why: string) => {\n if (done) return\n done = true\n el.removeEventListener('ended', onNativeEnd)\n console.log(`[speaking] clip end via ${why} @ ct=${el.currentTime.toFixed(2)}/${isFinite(el.duration) ? el.duration.toFixed(2) : '?'} (+${((performance.now() - tEnd) / 1000).toFixed(2)}s) → fade`)\n resolve()\n }\n const onNativeEnd = () => finish('native ended')\n el.addEventListener('ended', onNativeEnd)\n setTimeout(() => finish('safety cap'), TAIL_SAFETY_CAP_MS)\n })\n }\n }\n } finally {\n speakingPendingRef.current = false\n audioReadyRef.current = false\n setSpeakingOpacity(0)\n console.log('[speaking] ⏹ faded → waiting')\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') {\n console.log(`[turn] ⛔ ignored \"${t.slice(0, 30)}\" — busy (${stateRef.current}); LLM NOT called`)\n return\n }\n const t0 = performance.now()\n console.log('[turn] ⏱ 0ms — user submitted:', t)\n speech.stop()\n setLatestUser(t)\n setCallState('thinking')\n const history = historyRef.current\n historyRef.current = [...history, { id: crypto.randomUUID(), role: 'user', content: t }]\n try {\n const raw = await chatPort.complete(buildPrompt(persona, history, t))\n const reply = parseReply(raw)\n console.log(`[turn] 🧠 LLM ${(performance.now() - t0).toFixed(0)}ms → mood=${reply.mood} text=\"${reply.text.slice(0, 40)}\"`)\n historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: 'assistant', content: reply.text }]\n setLatestAi(reply.text)\n setMood(reply.mood)\n await speakReply(reply.text, reply.mood)\n console.log(`[turn] 🏁 done ${(performance.now() - t0).toFixed(0)}ms`)\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.greetingVideoUrl) await playGreetingVideo()\n else 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 },\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 // The moment the user starts inputting (typing OR live speech transcript),\n // soft-switch the waiting loop to the thinking pool (mate). callState stays\n // 'listening' until submit — only the video changes — so this can never block\n // the LLM call. Clearing the input without sending reverts at the next swap.\n const inputActive = typed.trim().length > 0 || speech.draft.trim().length > 0\n useEffect(() => {\n if (inputActive) enterThinkingWaiting()\n else if (callState === 'listening') exitThinkingWaiting()\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [inputActive])\n\n const toggleSpeaker = () => {\n setSpeakerOn((on) => {\n const next = !on\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()\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 const hasWaiting = !!(waitSlots[0] || waitSlots[1])\n\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col bg-black text-white\">\n <div className=\"absolute inset-0 overflow-hidden bg-black\">\n {/* Waiting — two NON-looping slots (mate); active visible, fades as speaking rises */}\n {videoOn && hasWaiting\n ? ([0, 1] as const).map((slot) => {\n const url = waitSlots[slot]\n return url ? (\n <video\n key={slot}\n ref={(el) => {\n waitEls.current[slot] = el\n }}\n src={url}\n preload=\"auto\"\n muted\n playsInline\n onEnded={\n slot === waitActive\n ? () => {\n // Freeze on this idle-end frame (no fresh swap) once a\n // reveal is staged AND audio is ready, so the speaking\n // clip reveals on this exact idle pose — precise\n // connection, no fresh clip flashing in. Otherwise keep\n // looping the waiting double-buffer.\n if (speakingPendingRef.current && audioReadyRef.current) return\n swapToFreshWaiting()\n }\n : undefined\n }\n className={layerCls}\n style={{\n // Active waiting slot stays a SOLID opacity-1 layer — it does\n // NOT fade with speakingOpacity. The speaking layer (zIndex 2)\n // fades in ON TOP of it, so there's no moment where both are\n // partly transparent and the black stage bleeds through (the\n // old `1 - speakingOpacity` + conditional-transition combo hit a\n // same-frame CSS gotcha: opacity snapped to 0 before the fade\n // registered → a quick black flash). Inactive slot = 0. Swaps\n // stay instant (no transition) — same idle pose, invisible.\n opacity: slot === waitActive ? 1 : 0,\n transition: 'none',\n }}\n />\n ) : null\n })\n : !videoOn || !hasWaiting ? (\n persona.avatarUrl ? (\n <img src={persona.avatarUrl} alt={persona.name} className={`${layerCls} opacity-90`} />\n ) : (\n <div className=\"h-full w-full bg-gradient-to-b from-slate-800 to-slate-950\" />\n )\n ) : null}\n\n {/* Speaking — ALL clips preloaded; active revealed after play() */}\n {videoOn\n ? speakingClips.map((clip, i) => (\n <video\n key={clip.id}\n ref={(el) => {\n speakingEls.current[i] = el\n }}\n src={clip.videoUrl}\n muted\n loop\n playsInline\n preload=\"auto\"\n className={layerCls}\n style={{ opacity: i === activeIdx ? speakingOpacity : 0, transition: i === activeIdx ? 'opacity 0.25s ease' : 'none', zIndex: 2 }}\n />\n ))\n : null}\n\n {/* Greeting video — baked audio, UNMUTED, revealed after playing */}\n {videoOn && greetingShown && persona.greetingVideoUrl ? (\n <video\n key={persona.greetingVideoUrl}\n src={persona.greetingVideoUrl}\n autoPlay\n playsInline\n onPlaying={() => setGreetingReady(true)}\n onEnded={() => greetingEndRef.current?.()}\n onError={() => greetingEndRef.current?.()}\n className={layerCls}\n style={{ opacity: greetingReady ? 1 : 0, transition: 'opacity 0.25s ease', zIndex: 3 }}\n />\n ) : null}\n </div>\n\n {/* Loading scrim */}\n <div\n className={`absolute inset-0 z-20 flex flex-col items-center justify-center gap-5 bg-black transition-opacity duration-500 ${\n callState === 'connecting' ? 'opacity-100' : 'pointer-events-none opacity-0'\n }`}\n >\n {persona.avatarUrl ? <img src={persona.avatarUrl} alt={persona.name} className=\"h-24 w-24 rounded-full object-cover ring-2 ring-white/20\" /> : null}\n <div className=\"flex items-center gap-2 text-white/80\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n <span className=\"text-sm\">接通 {persona.name}…</span>\n </div>\n </div>\n\n {/* Top bar */}\n <div className=\"relative z-10 flex items-center justify-between p-4\">\n <div className=\"flex items-center gap-2 rounded-full bg-black/40 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-black/40 text-white/60'}`}>\n <Subtitles className=\"h-4 w-4\" />\n </button>\n </div>\n\n <div className=\"flex-1\" />\n\n {/* Captions */}\n {showCaptions ? (\n <div className=\"relative z-10 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/50 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=\"relative z-10 flex flex-col gap-3 bg-gradient-to-t from-black/80 to-transparent px-4 pb-5 pt-4\">\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={() => setVideoOn((v) => !v)} title={videoOn ? '关闭视频' : '开启视频'} className={`${ctlBtn} ${videoOn ? ctlOn : ctlOff}`}>\n {videoOn ? <Video className=\"h-5 w-5\" /> : <VideoOff 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}\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 || !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","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"],"mappings":"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","names":["tier","useEffect","useRef","useState","useState","useRef","useEffect","el"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@surfmate.team/digital-human-video-call",
3
+ "version": "0.1.0",
4
+ "description": "Full-screen 数字人通话 (video call) — the integration feature: LLM (conversation/ChatPort) + TTS (voice) + talking-head clips (speaking) + waiting-video loops (waiting) into a speak→reply→video→loop call.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=18"
21
+ },
22
+ "dependencies": {
23
+ "lucide-react": "^0.460.0",
24
+ "@surfmate.team/digital-human-clip-core": "0.1.0",
25
+ "@surfmate.team/digital-human-conversation": "0.1.0",
26
+ "@surfmate.team/digital-human-speaking": "0.1.0",
27
+ "@surfmate.team/digital-human-voice": "0.1.0",
28
+ "@surfmate.team/digital-human-waiting": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^18.3.0",
32
+ "react": "^18.3.1",
33
+ "tsup": "^8.5.0",
34
+ "typescript": "^5.9.0",
35
+ "vitest": "^3.2.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run"
44
+ }
45
+ }