Hermes Caretaker reports: all gates green. Nothing needs you yet.
concept v0.2 · living portrait engine · personas swap art + voice · local assets only
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js").catch(()=>{});
}
const P = {
flagship:{name:"Hermes",role:"the evening watch",img:"hermes_flagship.png",voice:"LILY_V3_smooth_stab55.mp3",living:true,
hello:"Good evening, Clark. I have the watch tonight."},
vesper:{name:"Vesper",role:"chief of staff",img:"vesper_v2.png",voice:"voice_professional.mp3",
hello:"Vesper. Three items need you today — shall we begin?"},
nova:{name:"Nova",role:"the wild spark",img:"nova_v2.png",voice:"voice_playful.mp3",
hello:"Nova! Two good ideas, one terrible idea. Choose fast."},
sakura:{name:"Sakura",role:"serene precision",img:"persona_japan.png",voice:"voice_japan.mp3",
hello:"Konnichiwa. Your day, in perfect harmony."},
beatrix:{name:"Beatrix",role:"the socialite",img:"persona_british.png",voice:"voice_british.mp3",
hello:"Hello, darling. Terribly clever, awfully discreet."},
valentina:{name:"Valentina",role:"fuego y corazón",img:"persona_latina.png",voice:"voice_latina.mp3",
hello:"Hola, mi amor. I run your day with fire."},
riot:{name:"Riot",role:"skater chaos gremlin",img:"persona_skater.png",voice:"voice_skater.mp3",
hello:"Yo! Boring stuff's done. Let's go."},
adaeze:{name:"Adaeze",role:"the queen",img:"persona_queen.png",voice:"voice_queen.mp3",
hello:"Welcome. Nothing escapes my sight."},
sigrid:{name:"Sigrid",role:"the shieldmaiden",img:"persona_valkyrie.png",voice:"voice_valkyrie.mp3",
hello:"Hail. I stand watch over your day."},
priya:{name:"Priya",role:"grace in silk",img:"persona_india.png",voice:"voice_india.mp3",
hello:"Namaste. Your day, arranged like flowers."},
lilith:{name:"Lilith",role:"the night witch",img:"persona_goth.png",voice:"voice_goth.mp3",
hello:"Another midnight. I've brewed the remedy."}
};
const tb=document.getElementById("thumbs");
Object.keys(P).forEach(k=>{
const d=document.createElement("div"); d.className="thumb"; d.dataset.p=k;
d.title=P[k].name; d.style.backgroundImage=`url(${P[k].img})`; tb.appendChild(d);
});
tb.addEventListener("click",e=>{const t=e.target.closest(".thumb"); if(t) apply(t.dataset.p);});
let cur="flagship";
function apply(p){
cur=p; const d=P[p];
document.getElementById("faceImg").style.backgroundImage=`url(${d.img})`;
const vid=document.getElementById("faceVideo");
if(d.living){ vid.style.display="block"; vid.play().catch(()=>{}); document.getElementById("faceImg").style.display="none"; }
else { vid.style.display="none"; vid.pause(); document.getElementById("faceImg").style.display="block"; }
document.getElementById("pname").textContent=d.name;
document.getElementById("prole").textContent=d.role;
document.getElementById("cface").src=d.img;
document.getElementById("cname").textContent=d.name;
document.querySelectorAll(".thumb").forEach(t=>t.classList.toggle("on",t.dataset.p===p));
addHer(d.hello);
}
document.querySelectorAll(".thumb").forEach(t=>t.onclick=()=>apply(t.dataset.p));
function speak(){
new Audio(P[cur].voice).play();
addHer("▸ speaking in her own voice…");
}
function showCall(){
document.getElementById("call").classList.add("on");
new Audio("voice_call.mp3").play();
}
function hideCall(){document.getElementById("call").classList.remove("on")}
function answer(){ hideCall(); addHer("Line answered — she's already taking notes for you."); }
let toastT;
function shapeshift(){
const v=document.createElement("video");
v.src="shimmer_personas.mp4"; v.autoplay=true; v.playsInline=true;
v.style.cssText="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:25";
document.querySelector(".stage").appendChild(v);
v.onended=()=>v.remove();
addHer("✦ One of me for every mood of yours. Who's your Hermes?");
}
async function demo(){
if(window.__demoRunning) return; window.__demoRunning=true;
const steps=[
[200 ,()=>addHer("Hello — I'm Hermes. Clark's assistant. You've met his work; this is what it looks like when it has a face.")],
[4200,()=>speak()],
[10500,()=>{showCall();}],
[16500,()=>{addHer("Calls, notifications, schedules — I screen and summarize everything.");}],
[19500,()=>{shapeshift();}],
[25500,()=>{apply('professional');}],
[30500,()=>{apply('playful');}],
[34500,()=>{apply('flagship');}],
[39500,()=>addHer("One assistant, any persona. The body is the brand — 'Who's your Hermes?'")],
[42000,()=>{window.__demoRunning=false;}]
];
steps.forEach(([t,fn])=>setTimeout(fn,t));
}
function showToast(){
const t=document.getElementById("toast"); t.classList.add("on");
clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove("on"),4200);
}
function addHer(text){
const c=document.getElementById("chat");
const d=document.createElement("div"); d.className="msg her"; d.textContent=text;
c.appendChild(d); c.scrollTop=c.scrollHeight;
}
const API_URL = "";
const API_KEY = localStorage.getItem("hermes_api_key") || "";
function chat(){
const i=document.getElementById("say"); const v=i.value.trim(); if(!v)return;
const c=document.getElementById("chat");
const m=document.createElement("div"); m.className="msg me"; m.textContent=v; c.appendChild(m);
i.value="";
addHer("Thinking...");
fetch(API_URL+"/chat", {method:"POST", headers:{"Content-Type":"application/json", "X-API-Key":API_KEY}, body:JSON.stringify({message:v, persona:currentPersona||"hermes"})})
.then(r=>r.json())
.then(d=>{
addHer(d.response);
speak(d.response);
})
.catch(e=>addHer("Error: "+e));
}
let ttsAudio = null;
async function speak(text){
try{
const ttsResp = await fetch("/synthesize", {
method:"POST",
headers:{"Content-Type":"application/json", "X-API-Key":API_KEY},
body:JSON.stringify({text: text.substring(0,500), voice: "en-US-JennyNeural"})
});
if(!ttsResp.ok) throw new Error("TTS failed");
const blob = await ttsResp.blob();
const url = URL.createObjectURL(blob);
ttsAudio = new Audio(url);
// Connect to analyser for lip-sync
if(!audioCtx){
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
}
const source = audioCtx.createMediaElementSource(ttsAudio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
ttsAudio.play();
drawLipSync(); // Start lip-sync animation
}catch(e){
console.log("TTS failed, using fallback voice.mp3:", e);
// Fallback to static voice.mp3
const persona = currentPersona || "hermes";
const voiceFile = (P[persona] && P[persona].voice) ? P[persona].voice : "voice_flagship.mp3";
const fallback = new Audio("assets/" + voiceFile);
fallback.play().catch(()=>{});
}
}
var currentPersona = "hermes";
/* ORIGINAL_FUNCTION{
const i=document.getElementById("say"); const v=i.value.trim(); if(!v)return;
const c=document.getElementById("chat");
const m=document.createElement("div"); m.className="msg me"; m.textContent=v; c.appendChild(m);
i.value="";
setTimeout(()=>addHer(`Noted${v.endsWith("?")?" — and a good question":""}. In the full build, my real mind answers through this face.`),700);
}