@surfmate.team/digital-human-video-call 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WaitingVideo, WaitingTier } from '@surfmate.team/digital-human-waiting';
2
2
  import { SpeakingClip } from '@surfmate.team/digital-human-speaking';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
- import { CharacterPersona, ChatPort } from '@surfmate.team/digital-human-conversation';
4
+ import { CharacterPersona, ChatPort, ChatMessage, ChatReply } from '@surfmate.team/digital-human-conversation';
5
5
  import { VoiceSynthesisPort } from '@surfmate.team/digital-human-voice';
6
6
 
7
7
  /**
@@ -24,7 +24,12 @@ type Props = {
24
24
  readonly voiceModelId?: string | undefined;
25
25
  readonly waitingVideos?: readonly WaitingVideo[] | undefined;
26
26
  readonly speakingClips?: readonly SpeakingClip[] | undefined;
27
- readonly onClose: () => void;
27
+ /** Emitted after each completed turn (user said X → assistant replied Y). The
28
+ * host persists it if it wants — video-call itself stays in-memory. */
29
+ readonly onTurn?: ((user: ChatMessage, ai: ChatReply) => void) | undefined;
30
+ /** Called when the call ends; receives the full in-memory transcript so the
31
+ * host can persist it in one shot. (Arg is ignorable for UI-only closers.) */
32
+ readonly onClose: (history: readonly ChatMessage[]) => void;
28
33
  readonly speechLang?: string | undefined;
29
34
  };
30
35
  /**
@@ -42,6 +47,6 @@ type Props = {
42
47
  * - thinking switch waits for the current clip's end too (never cut).
43
48
  * - No blur, no speedup.
44
49
  */
45
- declare function VideoCall({ persona, chatPort, synth, voiceId, voiceModelId, waitingVideos, speakingClips, onClose, speechLang, }: Props): react_jsx_runtime.JSX.Element;
50
+ declare function VideoCall({ persona, chatPort, synth, voiceId, voiceModelId, waitingVideos, speakingClips, onTurn, onClose, speechLang, }: Props): react_jsx_runtime.JSX.Element;
46
51
 
47
52
  export { VideoCall, pickWaitingVideo, selectSpeakingClip };
package/dist/index.js CHANGED
@@ -132,6 +132,7 @@ function VideoCall({
132
132
  voiceModelId,
133
133
  waitingVideos = [],
134
134
  speakingClips = [],
135
+ onTurn,
135
136
  onClose,
136
137
  speechLang
137
138
  }) {
@@ -374,7 +375,8 @@ function VideoCall({
374
375
  setLatestUser(t);
375
376
  setCallState("thinking");
376
377
  const history = historyRef.current;
377
- historyRef.current = [...history, { id: crypto.randomUUID(), role: "user", content: t }];
378
+ const userMsg = { id: crypto.randomUUID(), role: "user", content: t };
379
+ historyRef.current = [...history, userMsg];
378
380
  try {
379
381
  const raw = await chatPort.complete(buildPrompt(persona, history, t));
380
382
  const reply = parseReply(raw);
@@ -382,6 +384,7 @@ function VideoCall({
382
384
  historyRef.current = [...historyRef.current, { id: crypto.randomUUID(), role: "assistant", content: reply.text }];
383
385
  setLatestAi(reply.text);
384
386
  setMood(reply.mood);
387
+ onTurn?.(userMsg, reply);
385
388
  await speakReply(reply.text, reply.mood);
386
389
  console.log(`[turn] \u{1F3C1} done ${(performance.now() - t0).toFixed(0)}ms`);
387
390
  } catch (err) {
@@ -431,7 +434,7 @@ function VideoCall({
431
434
  setCallState("ended");
432
435
  audioRef.current?.pause();
433
436
  speech.stop();
434
- onClose();
437
+ onClose(historyRef.current);
435
438
  };
436
439
  const sendTyped = () => {
437
440
  const t = typed.trim();
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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 ChatReply,\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 /** Emitted after each completed turn (user said X → assistant replied Y). The\n * host persists it if it wants — video-call itself stays in-memory. */\n readonly onTurn?: ((user: ChatMessage, ai: ChatReply) => 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 ChatMessage[]) => 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 onTurn,\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 const userMsg: ChatMessage = { id: crypto.randomUUID(), role: 'user', content: t }\n historyRef.current = [...history, userMsg]\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 onTurn?.(userMsg, reply)\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(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 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surfmate.team/digital-human-video-call",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
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
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,11 +21,11 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "lucide-react": "^0.460.0",
24
+ "@surfmate.team/digital-human-conversation": "0.2.0",
24
25
  "@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
26
  "@surfmate.team/digital-human-voice": "0.1.0",
28
- "@surfmate.team/digital-human-waiting": "0.1.0"
27
+ "@surfmate.team/digital-human-waiting": "0.1.0",
28
+ "@surfmate.team/digital-human-speaking": "0.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/react": "^18.3.0",