AIBRUH Claude Sonnet 4.6 commited on
Commit
8d6bc51
·
1 Parent(s): 7dfa92f

feat: two-state video system — idle loop + talk loop, no wave greeting

Browse files

- eve_idle.mp4 (generating now via Wan2.2): breathing, micro-expressions,
blinking — plays at all times including on load, no overlay
- eve_talking_loop.mp4: lips moving — swaps in only while audio plays,
swaps back to idle on audio.onended
- Removed wave greeting entirely: no competing videos, no unnatural looping
- Eve speaks her greeting through TTS only (no visual state change on open)
- Clean two-state machine: IDLE_VIDEO <-> TALK_VIDEO, driven by audio events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. app/the-gym/page.tsx +11 -14
app/the-gym/page.tsx CHANGED
@@ -2,8 +2,8 @@
2
  import { useState, useEffect, useRef, useCallback } from "react";
3
  import Nav from "@/components/Nav";
4
 
5
- const EVE_WAVE_VIDEO = "/eve_wave_greeting.mp4";
6
- const EVE_TALK_VIDEO = "/eve_talking_loop.mp4";
7
 
8
  type Msg = { role: "eve" | "user"; text: string };
9
  type AvatarState = "idle" | "thinking" | "speaking";
@@ -27,7 +27,7 @@ export default function TheGymPage() {
27
  const [pipelineTag, setPipelineTag] = useState("Grok-3 · Eve-TTS · Live");
28
  const [notice, setNotice] = useState("");
29
  // videoSrc drives which video plays — wave for opener, talk loop for replies, null = idle loop
30
- const [videoSrc, setVideoSrc] = useState<string>(EVE_WAVE_VIDEO);
31
 
32
  const audioRef = useRef<HTMLAudioElement>(null);
33
  const chatRef = useRef<HTMLDivElement>(null);
@@ -72,8 +72,7 @@ export default function TheGymPage() {
72
 
73
  if (data.audioUrl) {
74
  visemesRef.current = data.visemes ?? [];
75
- // Opener keeps the wave video; every other turn switches to talk loop
76
- if (userText !== "__OPEN__") setVideoSrc(EVE_TALK_VIDEO);
77
  setAvatarState("speaking");
78
 
79
  const audio = audioRef.current ?? new Audio();
@@ -82,24 +81,23 @@ export default function TheGymPage() {
82
  audio.onended = () => {
83
  stopVisemeLoop();
84
  setAvatarState("idle");
85
- // After speaking, keep the talk loop running so she stays alive
86
- setVideoSrc(EVE_TALK_VIDEO);
87
  };
88
  audio.onerror = () => {
89
- stopVisemeLoop(); setAvatarState("idle"); setVideoSrc(EVE_TALK_VIDEO);
90
  const utt = new SpeechSynthesisUtterance(reply);
91
  utt.onend = () => setAvatarState("idle");
92
  window.speechSynthesis.speak(utt);
93
  };
94
  audio.play().catch(() => {
95
- stopVisemeLoop(); setAvatarState("idle"); setVideoSrc(EVE_TALK_VIDEO);
96
  const utt = new SpeechSynthesisUtterance(reply);
97
  utt.onend = () => setAvatarState("idle");
98
  window.speechSynthesis.speak(utt);
99
  });
100
  setPipelineTag("✓ Grok-3 · Eve-TTS (Ava) · Live");
101
  } else {
102
- setVideoSrc(EVE_TALK_VIDEO);
103
  const utt = new SpeechSynthesisUtterance(reply);
104
  utt.onstart = () => setAvatarState("speaking");
105
  utt.onend = () => { setAvatarState("idle"); stopVisemeLoop(); };
@@ -109,16 +107,15 @@ export default function TheGymPage() {
109
  } catch (e) {
110
  setNotice(`${String(e).slice(0, 80)}`);
111
  setAvatarState("idle");
112
- setVideoSrc(EVE_TALK_VIDEO);
113
  }
114
  setLoading(false);
115
  }, [startVisemeLoop, stopVisemeLoop]);
116
 
117
- // On mount — play wave video immediately, then Eve greets TJ
118
  useEffect(() => {
119
  if (didGreet.current) return;
120
  didGreet.current = true;
121
- setVideoSrc(EVE_WAVE_VIDEO);
122
  setTimeout(() => eveRespond("__OPEN__", true), 900);
123
  }, [eveRespond]);
124
 
@@ -187,7 +184,7 @@ export default function TheGymPage() {
187
  transition: "background 1s",
188
  }} />
189
 
190
- {/* Live video — always playing: wave on open, talk loop otherwise */}
191
  <video
192
  key={videoSrc}
193
  src={videoSrc}
 
2
  import { useState, useEffect, useRef, useCallback } from "react";
3
  import Nav from "@/components/Nav";
4
 
5
+ const EVE_IDLE_VIDEO = "/eve_idle.mp4"; // breathing, micro-movements, blinking — always on
6
+ const EVE_TALK_VIDEO = "/eve_talking_loop.mp4"; // lips moving — only while she's speaking
7
 
8
  type Msg = { role: "eve" | "user"; text: string };
9
  type AvatarState = "idle" | "thinking" | "speaking";
 
27
  const [pipelineTag, setPipelineTag] = useState("Grok-3 · Eve-TTS · Live");
28
  const [notice, setNotice] = useState("");
29
  // videoSrc drives which video plays — wave for opener, talk loop for replies, null = idle loop
30
+ const [videoSrc, setVideoSrc] = useState<string>(EVE_IDLE_VIDEO);
31
 
32
  const audioRef = useRef<HTMLAudioElement>(null);
33
  const chatRef = useRef<HTMLDivElement>(null);
 
72
 
73
  if (data.audioUrl) {
74
  visemesRef.current = data.visemes ?? [];
75
+ setVideoSrc(EVE_TALK_VIDEO);
 
76
  setAvatarState("speaking");
77
 
78
  const audio = audioRef.current ?? new Audio();
 
81
  audio.onended = () => {
82
  stopVisemeLoop();
83
  setAvatarState("idle");
84
+ setVideoSrc(EVE_IDLE_VIDEO);
 
85
  };
86
  audio.onerror = () => {
87
+ stopVisemeLoop(); setAvatarState("idle"); setVideoSrc(EVE_IDLE_VIDEO);
88
  const utt = new SpeechSynthesisUtterance(reply);
89
  utt.onend = () => setAvatarState("idle");
90
  window.speechSynthesis.speak(utt);
91
  };
92
  audio.play().catch(() => {
93
+ stopVisemeLoop(); setAvatarState("idle"); setVideoSrc(EVE_IDLE_VIDEO);
94
  const utt = new SpeechSynthesisUtterance(reply);
95
  utt.onend = () => setAvatarState("idle");
96
  window.speechSynthesis.speak(utt);
97
  });
98
  setPipelineTag("✓ Grok-3 · Eve-TTS (Ava) · Live");
99
  } else {
100
+ setVideoSrc(EVE_IDLE_VIDEO);
101
  const utt = new SpeechSynthesisUtterance(reply);
102
  utt.onstart = () => setAvatarState("speaking");
103
  utt.onend = () => { setAvatarState("idle"); stopVisemeLoop(); };
 
107
  } catch (e) {
108
  setNotice(`${String(e).slice(0, 80)}`);
109
  setAvatarState("idle");
110
+ setVideoSrc(EVE_IDLE_VIDEO);
111
  }
112
  setLoading(false);
113
  }, [startVisemeLoop, stopVisemeLoop]);
114
 
115
+ // On mount — idle video is already set; Eve speaks her greeting after a short delay
116
  useEffect(() => {
117
  if (didGreet.current) return;
118
  didGreet.current = true;
 
119
  setTimeout(() => eveRespond("__OPEN__", true), 900);
120
  }, [eveRespond]);
121
 
 
184
  transition: "background 1s",
185
  }} />
186
 
187
+ {/* Live video — idle breathing loop always on; swaps to talk loop while speaking */}
188
  <video
189
  key={videoSrc}
190
  src={videoSrc}