Spaces:
Running
Running
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>
- 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
|
| 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>(
|
| 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 |
-
|
| 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 |
-
|
| 86 |
-
setVideoSrc(EVE_TALK_VIDEO);
|
| 87 |
};
|
| 88 |
audio.onerror = () => {
|
| 89 |
-
stopVisemeLoop(); setAvatarState("idle"); setVideoSrc(
|
| 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(
|
| 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(
|
| 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(
|
| 113 |
}
|
| 114 |
setLoading(false);
|
| 115 |
}, [startVisemeLoop, stopVisemeLoop]);
|
| 116 |
|
| 117 |
-
// On mount —
|
| 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 —
|
| 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}
|