chore: cleanup dead code and unused files

Deleted 7 unused frontend components (ChatBubble, PetZone, Live2DCat,
BackgroundScene, Clock, Toolbar, types/index.ts).
Deleted unused backend: whisper_stream.py, empty models/ and routers/ dirs.
Removed dead code: Message interface, messages/micError/sendText/addMessage,
conversation_text handler, 3 unused memory.py methods.
Fixed task persistence bug: added 'tasks' to DEFAULT_PREFERENCES.
Stale comment in Live2DStage updated.
This commit is contained in:
2026-06-06 12:05:28 -04:00
parent 1eaef8b6ab
commit b06f20f9d8
17 changed files with 4 additions and 537 deletions
-23
View File
@@ -383,29 +383,6 @@ async def gemini_voice_ws(websocket: WebSocket):
await gemini_ws.send(json.dumps(gemini_msg)) await gemini_ws.send(json.dumps(gemini_msg))
continue continue
if msg_type == "conversation_text":
text = msg.get("text", "").strip()
if not text:
continue
logger.info(f"[{session_id}] User (text): {text}")
if gemini_ws and gemini_ws.state.name == "OPEN":
user_part = {"text": text}
if memory_suffix:
user_part = {"text": f"[Context: {memory_suffix}]\n{text}"}
gemini_msg = {
"clientContent": {
"turns": [{"role": "user", "parts": [user_part]}],
"turnComplete": True,
}
}
await gemini_ws.send(json.dumps(gemini_msg))
await websocket.send_json({
"type": "transcript",
"role": "user",
"text": text,
})
continue
if msg_type == "ping": if msg_type == "ping":
await websocket.send_json({"type": "pong"}) await websocket.send_json({"type": "pong"})
View File
View File
+1 -46
View File
@@ -134,57 +134,12 @@ class KiraMemory:
return "\n\n---\n### What Kira remembers:" + "".join(parts) return "\n\n---\n### What Kira remembers:" + "".join(parts)
def store_messages(
self,
user_message: str,
kira_message: str,
) -> None:
"""Store a conversation exchange in Honcho."""
if not self.enabled or not self._session:
return
try:
messages = []
if self._user_peer:
messages.append(
self._user_peer.message(user_message)
)
if self._kira_peer:
messages.append(
self._kira_peer.message(kira_message)
)
if messages:
self._session.add_messages(messages)
logger.debug("Stored conversation exchange in Honcho")
except Exception as e:
logger.warning(f"Failed to store messages: {e}")
def store_user_message(self, text: str) -> None:
"""Store a single user message."""
if not self.enabled or not self._session or not self._user_peer:
return
try:
self._session.add_messages([self._user_peer.message(text)])
except Exception as e:
logger.warning(f"Failed to store user message: {e}")
def store_kira_message(self, text: str) -> None:
"""Store a single Kira message."""
if not self.enabled or not self._session or not self._kira_peer:
return
try:
self._session.add_messages([self._kira_peer.message(text)])
except Exception as e:
logger.warning(f"Failed to store Kira message: {e}")
# ─── User preferences (stored in Honcho peer metadata) ───
DEFAULT_PREFERENCES: dict[str, str] = { DEFAULT_PREFERENCES: dict[str, str] = {
"name": "", "name": "",
"scene": "cozy-room", "scene": "cozy-room",
"outfit": "cozy-hoodie", "outfit": "cozy-hoodie",
"accessory": "", "accessory": "",
"tasks": "[]",
} }
def get_user_preferences(self, user_id: str) -> dict[str, str]: def get_user_preferences(self, user_id: str) -> dict[str, str]:
-158
View File
@@ -1,158 +0,0 @@
"""Realtime streaming transcription service via gpt-realtime-whisper.
Connects to OpenAI Realtime API via WebSocket, configures the session
for pure transcription (no model responses), and streams word-level
transcript deltas back. Full utterances are then processed by the
cheap LLM + TTS pipeline.
"""
import json
import base64
import logging
import asyncio
from typing import Callable, Awaitable
from config import settings
logger = logging.getLogger("kira.whisper")
class WhisperStream:
"""Streaming transcription via gpt-realtime-whisper over WebSocket."""
def __init__(
self,
on_transcript_delta: Callable[[str], Awaitable[None]],
on_transcript_done: Callable[[str], Awaitable[None]],
on_ready: Callable[[], Awaitable[None]],
on_error: Callable[[str], Awaitable[None]],
):
self._on_delta = on_transcript_delta
self._on_done = on_transcript_done
self._on_ready = on_ready
self._on_error = on_error
self._conn = None
self._connected = False
self._transcript = ""
async def connect(self):
if self._connected:
return
try:
import websockets
url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
ws = await websockets.connect(
url,
additional_headers={
"Authorization": f"Bearer {settings.openai_api_key}",
},
)
async with ws as conn:
self._conn = conn
self._connected = True
logger.info("Connected to Realtime transcription session")
# Configure: transcribe only with gpt-realtime-whisper, no model responses
await self._send({
"type": "session.update",
"session": {
"input_audio_format": "pcm16",
"input_audio_transcription": {
"model": "gpt-realtime-whisper",
"enabled": True,
},
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"prefix_padding_ms": 300,
"silence_duration_ms": 600,
},
},
})
await self._on_ready()
while self._connected:
try:
raw = await conn.recv()
if isinstance(raw, (str, bytes)):
data = json.loads(raw if isinstance(raw, str) else raw.decode())
await self._handle(data)
except Exception as e:
if self._connected:
logger.warning(f"recv: {e}")
break
except Exception as e:
logger.error(f"Whisper stream error: {e}")
await self._on_error(str(e))
finally:
self._connected = False
self._conn = None
async def _handle(self, data: dict):
et = data.get("type", "")
if et == "input_audio_buffer.speech_started":
self._transcript = ""
logger.debug("speech_started")
elif et in ("conversation.item.input_audio_transcription.delta", "input_audio_buffer.transcription.delta"):
# Partial streaming transcript
delta = data.get("delta", "") or data.get("transcript", "")
if delta:
self._transcript = delta # or append if cumulative
await self._on_delta(delta)
elif et in ("conversation.item.input_audio_transcription.completed", "input_audio_buffer.transcription.completed", "conversation.item.created"):
# Final or item created with transcript
item = data.get("item", {})
content = item.get("content", []) if item else []
transcript = data.get("transcript", "")
if not transcript:
for part in (content or []):
if part.get("type") in ("transcript", "text"):
transcript = part.get("transcript", "") or part.get("text", "")
break
if transcript:
self._transcript = transcript
await self._on_delta(transcript)
await self._on_done(transcript.strip())
self._transcript = ""
elif et == "input_audio_buffer.speech_stopped":
if self._transcript.strip():
await self._on_done(self._transcript.strip())
self._transcript = ""
elif et == "error":
err = data.get("error", {})
msg = err.get("message", str(data))
logger.warning(f"Whisper error: {msg}")
await self._on_error(msg)
async def send_audio(self, pcm16_bytes: bytes):
if not self._connected:
return
try:
b64 = base64.b64encode(pcm16_bytes).decode("utf-8")
await self._send({"type": "input_audio_buffer.append", "audio": b64})
except Exception as e:
logger.warning(f"send audio: {e}")
async def _send(self, data: dict):
try:
await self._conn.send(json.dumps(data))
except Exception as e:
logger.warning(f"send: {e}")
async def disconnect(self):
self._connected = False
if self._conn:
try:
await self._conn.close()
except Exception:
pass
self._conn = None
-1
View File
@@ -16,7 +16,6 @@ import { useConversation } from './hooks/useConversation';
export default function App() { export default function App() {
const [isLoggedIn, setIsLoggedIn] = useState(() => sessionStorage.getItem('kira-auth') === '1'); const [isLoggedIn, setIsLoggedIn] = useState(() => sessionStorage.getItem('kira-auth') === '1');
const { const {
messages,
isConnected, isConnected,
isKiraSpeaking, isKiraSpeaking,
isRecording, isRecording,
@@ -1,35 +0,0 @@
import { SCENES } from './scenes';
interface Props {
currentScene: string;
onSelect: (id: string) => void;
}
export default function BackgroundScene({ currentScene, onSelect }: Props) {
return (
<div className="p-4">
<h3 className="text-sm font-bold text-kira-plum mb-3 flex items-center gap-2">
<span>🎨</span> Scene
</h3>
<div className="flex flex-wrap gap-2">
{SCENES.map((s) => (
<button
key={s.id}
onClick={() => onSelect(s.id)}
className={`
flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-sm font-medium transition-all
${currentScene === s.id
? 'bg-kira-pink text-white shadow-md scale-105'
: 'bg-white/50 text-kira-plum/70 hover:bg-kira-glow hover:scale-102'
}
`}
title={s.name}
>
<span>{s.icon}</span>
<span className="hidden sm:inline">{s.name}</span>
</button>
))}
</div>
</div>
);
}
-85
View File
@@ -1,85 +0,0 @@
import { useRef, useEffect } from 'react';
interface Message {
id: string;
role: 'user' | 'kira';
text: string;
timestamp: number;
}
interface Props {
messages: Message[];
isKiraSpeaking: boolean;
userName?: string;
}
export default function ChatBubble({ messages, isKiraSpeaking }: Props) {
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div className="p-3 flex flex-col" style={{ maxHeight: 160 }}>
<h3 className="text-sm font-bold text-kira-plum mb-3 flex items-center gap-2">
<span>💬</span> Conversation
<span className={`w-2 h-2 rounded-full ${isKiraSpeaking ? 'bg-kira-pink animate-pulse' : 'bg-kira-mint'}`} />
</h3>
<div className="flex-1 overflow-y-auto space-y-2 scrollbar-thin pr-1">
{messages.length === 0 && (
<div className="text-xs text-kira-plum/30 text-center py-6">
click the mic or type to talk to Kira
</div>
)}
{messages.map((msg, i) => {
const isLastKira = msg.role === 'kira' && i === messages.length - 1 && isKiraSpeaking;
return (
<div
key={msg.id}
className={`flex gap-2 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{msg.role === 'kira' && (
<span className="text-sm mt-1">🌸</span>
)}
<div
className={`
max-w-[80%] px-3 py-2 rounded-2xl text-sm leading-relaxed
${msg.role === 'user'
? 'bg-kira-lav/30 text-kira-plum rounded-br-md'
: 'bg-white/60 text-kira-plum rounded-bl-md'
}
${isLastKira ? 'animate-fade-in' : ''}
`}
>
{msg.text}
{isLastKira && (
<span className="inline-block w-1.5 h-4 bg-kira-pink/60 ml-0.5 animate-blink" />
)}
</div>
{msg.role === 'user' && (
<span className="text-sm mt-1">👤</span>
)}
</div>
);
})}
<div ref={bottomRef} />
</div>
<style>{`
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.animate-fade-in { animation: fade-in 0.3s ease-out; }
.animate-blink { animation: blink 0.8s infinite; }
`}</style>
</div>
);
}
-21
View File
@@ -1,21 +0,0 @@
import { useEffect, useState } from 'react';
export default function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id);
}, []);
return (
<div className="px-5 py-3 text-center">
<div className="text-3xl font-bold tracking-tight text-kira-plum">
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
<div className="text-xs text-kira-violet/60 font-medium">
{time.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })}
</div>
</div>
);
}
-76
View File
@@ -1,76 +0,0 @@
import { useEffect, useRef } from 'react';
interface Props {
className?: string;
}
export default function Live2DCat({ className }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
let mounted = true;
const canvas = canvasRef.current;
if (!canvas) return;
let app: any = null;
const init = async () => {
try {
const pixi = await import('pixi.js');
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
(Live2DModel as any).registerTicker(pixi.Ticker as any);
const w = canvas.clientWidth || 120;
const h = canvas.clientHeight || 120;
// Use explicit CanvasRenderer to avoid WebGL context conflict with KiraAvatar
app = new pixi.Application({
view: canvas,
width: w,
height: h,
antialias: true,
resolution: Math.min(window.devicePixelRatio || 1, 2),
backgroundAlpha: 0,
autoDensity: true,
// Use WebGL1 context (more compatible with separate contexts)
preferWebGLVersion: 1,
} as any);
if (!mounted) { app.destroy(true); return; }
const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', {
autoInteract: false,
});
if (!mounted) { app.destroy(true); return; }
const sw = app.screen.width;
const sh = app.screen.height;
const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height);
model.scale.set(s);
model.anchor.set(0.5, 0.5);
model.position.set(sw / 2, sh / 2);
app.stage.addChild(model as any);
(model as any).isInteractive = () => false;
try { model.motion('Idle'); } catch { /* */ }
} catch (e) {
console.warn('[Live2DCat]', e);
}
};
init();
return () => {
mounted = false;
if (app) { app.destroy(true, { children: true }); }
};
}, []);
return (
<canvas
ref={canvasRef}
className={className}
style={{ display: 'block' }}
/>
);
}
+1 -1
View File
@@ -11,7 +11,7 @@ interface Props {
/** /**
* Single full-viewport Live2D stage. One WebGL context, two models: * Single full-viewport Live2D stage. One WebGL context, two models:
* - Kira (center panel) * - Kira (center panel)
* - Mochi the cat (PetZone, bottom of right sidebar) // Mochi the cat (next to Kira in center panel)
* *
* Canvas sits above UI panels (z-50, pointer-events: none) so Live2D * Canvas sits above UI panels (z-50, pointer-events: none) so Live2D
* models render on top of the frosted-glass sidebars. * models render on top of the frosted-glass sidebars.
-12
View File
@@ -1,12 +0,0 @@
export default function PetZone() {
return (
<div data-petzone className="p-4 relative overflow-hidden" style={{ minHeight: 140 }}>
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
<span>🐱</span> Pet Zone
</h3>
<div className="flex flex-col items-center pt-2">
<span className="text-[10px] text-kira-plum/60 font-medium">Mochi</span>
</div>
</div>
);
}
-34
View File
@@ -1,34 +0,0 @@
import { SCENES } from './scenes';
interface Props {
currentScene: string;
onSceneChange: (id: string) => void;
}
export default function Toolbar({ currentScene, onSceneChange }: Props) {
return (
<div className="px-4 py-3 flex items-center justify-between gap-2">
<div className="flex items-center gap-3">
<span className="text-lg font-bold text-kira-plum">Kira</span>
<span className="text-kira-plum/20">|</span>
<div className="flex gap-1.5">
{SCENES.filter((_, i) => i < 4).map((s) => (
<button
key={s.id}
onClick={() => onSceneChange(s.id)}
className={`text-sm ${currentScene === s.id ? 'opacity-100' : 'opacity-40 hover:opacity-70'} transition-opacity`}
title={s.name}
>
{s.icon}
</button>
))}
</div>
</div>
<div className="flex items-center gap-3 text-sm text-kira-plum/50">
<span>🐱 2</span>
<span className="hidden sm:inline">focus bestie</span>
</div>
</div>
);
}
+1 -22
View File
@@ -13,12 +13,6 @@ export interface Task {
completed: boolean; completed: boolean;
} }
interface Message {
id: string;
role: 'user' | 'kira';
text: string;
timestamp: number;
}
const WS_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/api/ws`; const WS_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/api/ws`;
const USER_ID_KEY = 'kira-user-id'; const USER_ID_KEY = 'kira-user-id';
@@ -56,7 +50,6 @@ function float32ToPcm16Base64(float32: Float32Array, srcRate: number): string {
} }
export function useConversation() { export function useConversation() {
const [messages, setMessages] = useState<Message[]>([]);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isKiraSpeaking, setIsKiraSpeaking] = useState(false); const [isKiraSpeaking, setIsKiraSpeaking] = useState(false);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
@@ -68,7 +61,6 @@ export function useConversation() {
accessory: '', accessory: '',
}); });
const [loadingPrefs, setLoadingPrefs] = useState(true); const [loadingPrefs, setLoadingPrefs] = useState(true);
const [micError, setMicError] = useState<string | null>(null);
const [tasks, setTasks] = useState<Task[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [celebrateTrigger, setCelebrateTrigger] = useState(0); const [celebrateTrigger, setCelebrateTrigger] = useState(0);
@@ -130,7 +122,6 @@ export function useConversation() {
break; break;
case 'transcript': case 'transcript':
addMessage(msg.role === 'user' ? 'user' : 'kira', msg.text);
break; break;
case 'audio': { case 'audio': {
@@ -220,12 +211,6 @@ export function useConversation() {
return playbackCtxRef.current; return playbackCtxRef.current;
} }
const addMessage = useCallback((role: 'user' | 'kira', text: string) => {
setMessages((prev) => [
...prev,
{ id: crypto.randomUUID(), role, text, timestamp: Date.now() },
]);
}, []);
// ── Identity ── // ── Identity ──
const identify = useCallback((name: string) => { const identify = useCallback((name: string) => {
@@ -248,12 +233,10 @@ export function useConversation() {
// ── Audio capture via ScriptProcessorNode (PCM16 16kHz stream) ── // ── Audio capture via ScriptProcessorNode (PCM16 16kHz stream) ──
const startRecording = useCallback(async () => { const startRecording = useCallback(async () => {
if (!navigator.mediaDevices?.getUserMedia) { if (!navigator.mediaDevices?.getUserMedia) {
addMessage('kira', 'Mic requires HTTPS. Try accessing via HTTPS!');
return; return;
} }
try { try {
setMicError(null);
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 }, audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 },
}); });
@@ -261,7 +244,6 @@ export function useConversation() {
const ws = wsRef.current; const ws = wsRef.current;
if (!ws || ws.readyState !== WebSocket.OPEN) { if (!ws || ws.readyState !== WebSocket.OPEN) {
addMessage('kira', 'Not connected to server yet...');
stream.getTracks().forEach((t) => t.stop()); stream.getTracks().forEach((t) => t.stop());
return; return;
} }
@@ -295,10 +277,9 @@ export function useConversation() {
setIsRecording(true); setIsRecording(true);
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
setMicError(msg);
console.error('[Kira Mic]', msg); console.error('[Kira Mic]', msg);
} }
}, [addMessage]); }, []);
const stopRecording = useCallback(() => { const stopRecording = useCallback(() => {
processorRef.current?.disconnect(); processorRef.current?.disconnect();
@@ -328,14 +309,12 @@ export function useConversation() {
}, [connect, stopRecording]); }, [connect, stopRecording]);
return { return {
messages,
isConnected, isConnected,
isKiraSpeaking, isKiraSpeaking,
isRecording, isRecording,
identified, identified,
preferences, preferences,
loadingPrefs, loadingPrefs,
micError,
identify, identify,
setPreference, setPreference,
sendText, sendText,
-9
View File
@@ -7,7 +7,6 @@
--color-kira-bg: #FFF5F5; --color-kira-bg: #FFF5F5;
--color-kira-plum: #4A1942; --color-kira-plum: #4A1942;
--color-kira-violet: #7C3AED; --color-kira-violet: #7C3AED;
--color-kira-card: #FFFFFF;
--color-kira-glow: #FDF2F8; --color-kira-glow: #FDF2F8;
--font-nunito: 'Nunito', sans-serif; --font-nunito: 'Nunito', sans-serif;
} }
@@ -27,14 +26,6 @@ body {
width: 100vw; width: 100vw;
} }
.glass-card {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 182, 193, 0.3);
border-radius: 16px;
box-shadow: 0 4px 24px rgba(74, 25, 66, 0.08);
}
.btn-kira { .btn-kira {
background: linear-gradient(135deg, #FFB6C1, #D8B4FE); background: linear-gradient(135deg, #FFB6C1, #D8B4FE);
color: white; color: white;
-13
View File
@@ -1,13 +0,0 @@
import { SCENES, type Scene } from '../components/scenes';
export interface KiraState {
currentScene: Scene;
isListening: boolean;
isSpeaking: boolean;
currentOutfit: string;
currentAccessory: string | null;
sessionNotes: string[];
}
export type { Scene };
export { SCENES };
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/Live2DCat.tsx","./src/components/Live2DStage.tsx","./src/components/LoginScreen.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/TaskList.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/KiraAvatar.tsx","./src/components/Live2DStage.tsx","./src/components/LoginScreen.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/TaskList.tsx","./src/components/Timer.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts"],"version":"6.0.3"}