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:
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user