init: Kira — AI body double with Honcho memory

Full voice pipeline (Whisper STT -> DeepSeek LLM -> OpenAI TTS),
animated SVG avatar (Live2D-ready), girly-pop UI, lofi music,
timer/notes/pets/wardrobe widgets, 10 background scenes with
particle effects, Honcho cross-session memory.
This commit is contained in:
2026-06-04 10:51:38 -04:00
commit 97424cb98f
47 changed files with 5691 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
import { useEffect, useRef, useState } from 'react';
import AnimatedAvatar from './AnimatedAvatar';
interface Props {
isSpeaking: boolean;
isListening: boolean;
outfit: string;
accessory: string | null;
onTalkToggle: () => void;
}
export default function KiraAvatar(props: Props) {
const canvasRef = useRef<HTMLDivElement>(null);
const [live2dReady, setLive2dReady] = useState(false);
const [loadError, setLoadError] = useState(false);
useEffect(() => {
// Try to load Live2D model — if it fails, show animated placeholder
let mounted = true;
const tryLoadLive2D = async () => {
try {
// Check if model files exist
const resp = await fetch('/live2d/models/kira.model3.json', { method: 'HEAD' });
if (!resp.ok) {
if (mounted) setLoadError(true);
return;
}
// Load Cubism core
await loadScript('/live2d/cubism/live2dcubismcore.min.js');
// Model exists — ready for Live2D
// (full Live2D rendering will require the Cubism4 framework bundle)
if (mounted) setLive2dReady(false); // Show placeholder until framework is fully wired
} catch {
if (mounted) setLoadError(true);
}
};
tryLoadLive2D();
return () => { mounted = false; };
}, []);
// Show animated fallback while Live2D model isn't available
return (
<div className="glass-card p-4 flex flex-col items-center" style={{ minHeight: 290 }}>
{/* Live2D canvas area (hidden until model is loaded) */}
{live2dReady && (
<div ref={canvasRef} className="w-36 h-44 relative" id="live2d-canvas" />
)}
{/* Animated SVG placeholder */}
{(!live2dReady || loadError) && (
<AnimatedAvatar
isSpeaking={props.isSpeaking}
isListening={props.isListening}
outfit={props.outfit}
accessory={props.accessory}
onTalkToggle={props.onTalkToggle}
/>
)}
{!live2dReady && !loadError && (
<p className="text-[10px] text-kira-plum/30 mt-1"> Live2D model slot ready</p>
)}
{/* Status info */}
<div className="mt-3 flex items-center gap-3 text-xs text-kira-plum/40">
<span className={`w-2 h-2 rounded-full ${props.isSpeaking ? 'bg-kira-pink animate-pulse' : props.isListening ? 'bg-red-400 animate-pulse' : 'bg-kira-mint'}`} />
<span>
{props.isSpeaking ? 'speaking...' : props.isListening ? 'listening...' : 'here with you'}
</span>
</div>
{/* Outfit + accessory indicator */}
<div className="flex gap-2 mt-1 text-[10px] text-kira-plum/30">
<span>{props.outfit.replace('-', ' ')}</span>
{props.accessory && <span>· {props.accessory}</span>}
</div>
</div>
);
}
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(script);
});
}