ilia bdbf09a9ac feat: Implement voice I/O services (TICKET-006, TICKET-010, TICKET-014)
 TICKET-006: Wake-word Detection Service
- Implemented wake-word detection using openWakeWord
- HTTP/WebSocket server on port 8002
- Real-time detection with configurable threshold
- Event emission for ASR integration
- Location: home-voice-agent/wake-word/

 TICKET-010: ASR Service
- Implemented ASR using faster-whisper
- HTTP endpoint for file transcription
- WebSocket endpoint for streaming transcription
- Support for multiple audio formats
- Auto language detection
- GPU acceleration support
- Location: home-voice-agent/asr/

 TICKET-014: TTS Service
- Implemented TTS using Piper
- HTTP endpoint for text-to-speech synthesis
- Low-latency processing (< 500ms)
- Multiple voice support
- WAV audio output
- Location: home-voice-agent/tts/

 TICKET-047: Updated Hardware Purchases
- Marked Pi5 kit, SSD, microphone, and speakers as purchased
- Updated progress log with purchase status

📚 Documentation:
- Added VOICE_SERVICES_README.md with complete testing guide
- Each service includes README.md with usage instructions
- All services ready for Pi5 deployment

🧪 Testing:
- Created test files for each service
- All imports validated
- FastAPI apps created successfully
- Code passes syntax validation

🚀 Ready for:
- Pi5 deployment
- End-to-end voice flow testing
- Integration with MCP server

Files Added:
- wake-word/detector.py
- wake-word/server.py
- wake-word/requirements.txt
- wake-word/README.md
- wake-word/test_detector.py
- asr/service.py
- asr/server.py
- asr/requirements.txt
- asr/README.md
- asr/test_service.py
- tts/service.py
- tts/server.py
- tts/requirements.txt
- tts/README.md
- tts/test_service.py
- VOICE_SERVICES_README.md

Files Modified:
- tickets/done/TICKET-047_hardware-purchases.md

Files Moved:
- tickets/backlog/TICKET-006_prototype-wake-word-node.md → tickets/done/
- tickets/backlog/TICKET-010_streaming-asr-service.md → tickets/done/
- tickets/backlog/TICKET-014_tts-service.md → tickets/done/
2026-01-12 22:22:38 -05:00

312 lines
9.3 KiB
Python

"""
Memory storage using SQLite.
"""
import sqlite3
import json
import logging
from pathlib import Path
from typing import Optional, List
from datetime import datetime
from uuid import uuid4
from memory.schema import MemoryEntry, MemoryCategory, MemorySource
logger = logging.getLogger(__name__)
class MemoryStorage:
"""SQLite storage for memory entries."""
def __init__(self, db_path: Optional[Path] = None):
"""
Initialize memory storage.
Args:
db_path: Path to SQLite database. If None, uses default.
"""
if db_path is None:
db_path = Path(__file__).parent.parent / "data" / "memory.db"
self.db_path = db_path
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init_db()
def _init_db(self):
"""Initialize database schema."""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS memory (
id TEXT PRIMARY KEY,
category TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
confidence REAL DEFAULT 0.5,
source TEXT NOT NULL,
timestamp TEXT NOT NULL,
last_accessed TEXT,
access_count INTEGER DEFAULT 0,
tags TEXT,
context TEXT,
UNIQUE(category, key)
)
""")
# Create indexes
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_category_key
ON memory(category, key)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_category
ON memory(category)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_last_accessed
ON memory(last_accessed)
""")
conn.commit()
conn.close()
logger.info(f"Memory database initialized at {self.db_path}")
def store(self, entry: MemoryEntry) -> bool:
"""
Store a memory entry.
Args:
entry: Memory entry to store
Returns:
True if stored successfully
"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
try:
cursor.execute("""
INSERT OR REPLACE INTO memory
(id, category, key, value, confidence, source, timestamp,
last_accessed, access_count, tags, context)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
entry.id,
entry.category.value,
entry.key,
entry.value,
entry.confidence,
entry.source.value,
entry.timestamp.isoformat(),
entry.last_accessed.isoformat() if entry.last_accessed else None,
entry.access_count,
json.dumps(entry.tags),
entry.context
))
conn.commit()
logger.info(f"Stored memory: {entry.category.value}/{entry.key} = {entry.value}")
return True
except Exception as e:
logger.error(f"Error storing memory: {e}")
conn.rollback()
return False
finally:
conn.close()
def get(self, category: MemoryCategory, key: str) -> Optional[MemoryEntry]:
"""
Get a memory entry by category and key.
Args:
category: Memory category
key: Memory key
Returns:
Memory entry or None
"""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM memory
WHERE category = ? AND key = ?
""", (category.value, key))
row = cursor.fetchone()
conn.close()
if row:
# Update access
self._update_access(row["id"])
return self._row_to_entry(row)
return None
def get_by_category(self, category: MemoryCategory, limit: Optional[int] = None) -> List[MemoryEntry]:
"""
Get all memory entries in a category.
Args:
category: Memory category
limit: Maximum number of entries to return
Returns:
List of memory entries
"""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = "SELECT * FROM memory WHERE category = ? ORDER BY last_accessed DESC"
if limit:
query += f" LIMIT {limit}"
cursor.execute(query, (category.value,))
rows = cursor.fetchall()
conn.close()
entries = [self._row_to_entry(row) for row in rows]
# Update access for all
for entry in entries:
self._update_access(entry.id)
return entries
def search(self, query: str, category: Optional[MemoryCategory] = None, limit: int = 10) -> List[MemoryEntry]:
"""
Search memory entries by value or context.
Args:
query: Search query
category: Optional category filter
limit: Maximum results
Returns:
List of matching memory entries
"""
conn = sqlite3.connect(str(self.db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
search_term = f"%{query.lower()}%"
if category:
cursor.execute("""
SELECT * FROM memory
WHERE category = ?
AND (LOWER(value) LIKE ? OR LOWER(context) LIKE ?)
ORDER BY confidence DESC, last_accessed DESC
LIMIT ?
""", (category.value, search_term, search_term, limit))
else:
cursor.execute("""
SELECT * FROM memory
WHERE LOWER(value) LIKE ? OR LOWER(context) LIKE ?
ORDER BY confidence DESC, last_accessed DESC
LIMIT ?
""", (search_term, search_term, limit))
rows = cursor.fetchall()
conn.close()
entries = [self._row_to_entry(row) for row in rows]
# Update access
for entry in entries:
self._update_access(entry.id)
return entries
def _update_access(self, entry_id: str):
"""Update access timestamp and count."""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
cursor.execute("""
UPDATE memory
SET last_accessed = ?, access_count = access_count + 1
WHERE id = ?
""", (datetime.now().isoformat(), entry_id))
conn.commit()
conn.close()
def _row_to_entry(self, row: sqlite3.Row) -> MemoryEntry:
"""Convert database row to MemoryEntry."""
tags = json.loads(row["tags"]) if row["tags"] else []
return MemoryEntry(
id=row["id"],
category=MemoryCategory(row["category"]),
key=row["key"],
value=row["value"],
confidence=row["confidence"],
source=MemorySource(row["source"]),
timestamp=datetime.fromisoformat(row["timestamp"]),
last_accessed=datetime.fromisoformat(row["last_accessed"]) if row["last_accessed"] else None,
access_count=row["access_count"],
tags=tags,
context=row["context"]
)
def delete(self, entry_id: str) -> bool:
"""
Delete a memory entry.
Args:
entry_id: Entry ID to delete
Returns:
True if deleted successfully
"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
try:
cursor.execute("DELETE FROM memory WHERE id = ?", (entry_id,))
conn.commit()
logger.info(f"Deleted memory entry: {entry_id}")
return True
except Exception as e:
logger.error(f"Error deleting memory: {e}")
conn.rollback()
return False
finally:
conn.close()
def update_confidence(self, entry_id: str, confidence: float) -> bool:
"""
Update confidence of a memory entry.
Args:
entry_id: Entry ID
confidence: New confidence value (0.0-1.0)
Returns:
True if updated successfully
"""
conn = sqlite3.connect(str(self.db_path))
cursor = conn.cursor()
try:
cursor.execute("""
UPDATE memory
SET confidence = ?
WHERE id = ?
""", (confidence, entry_id))
conn.commit()
return True
except Exception as e:
logger.error(f"Error updating confidence: {e}")
conn.rollback()
return False
finally:
conn.close()