✅ 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/
208 lines
5.8 KiB
Python
208 lines
5.8 KiB
Python
"""
|
|
Conversation retention and deletion policies.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
from datetime import datetime, timedelta
|
|
import sqlite3
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RetentionPolicy:
|
|
"""Defines retention policies for conversations."""
|
|
|
|
def __init__(self,
|
|
max_age_days: int = 90,
|
|
max_sessions: int = 1000,
|
|
auto_delete: bool = False):
|
|
"""
|
|
Initialize retention policy.
|
|
|
|
Args:
|
|
max_age_days: Maximum age in days before deletion
|
|
max_sessions: Maximum number of sessions to keep
|
|
auto_delete: Whether to auto-delete old sessions
|
|
"""
|
|
self.max_age_days = max_age_days
|
|
self.max_sessions = max_sessions
|
|
self.auto_delete = auto_delete
|
|
|
|
def should_delete(self, session_timestamp: datetime) -> bool:
|
|
"""
|
|
Check if session should be deleted based on age.
|
|
|
|
Args:
|
|
session_timestamp: When session was created
|
|
|
|
Returns:
|
|
True if should be deleted
|
|
"""
|
|
age = datetime.now() - session_timestamp
|
|
return age.days > self.max_age_days
|
|
|
|
|
|
class ConversationRetention:
|
|
"""Manages conversation retention and deletion."""
|
|
|
|
def __init__(self, db_path: Optional[Path] = None, policy: Optional[RetentionPolicy] = None):
|
|
"""
|
|
Initialize retention manager.
|
|
|
|
Args:
|
|
db_path: Path to conversations database
|
|
policy: Retention policy
|
|
"""
|
|
if db_path is None:
|
|
db_path = Path(__file__).parent.parent.parent / "data" / "conversations.db"
|
|
|
|
self.db_path = db_path
|
|
self.policy = policy or RetentionPolicy()
|
|
|
|
def list_old_sessions(self) -> List[tuple]:
|
|
"""
|
|
List sessions that should be deleted.
|
|
|
|
Returns:
|
|
List of (session_id, created_at) tuples
|
|
"""
|
|
if not self.db_path.exists():
|
|
return []
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
cutoff_date = datetime.now() - timedelta(days=self.policy.max_age_days)
|
|
|
|
cursor.execute("""
|
|
SELECT session_id, created_at
|
|
FROM sessions
|
|
WHERE created_at < ?
|
|
ORDER BY created_at ASC
|
|
""", (cutoff_date.isoformat(),))
|
|
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
return [(row["session_id"], row["created_at"]) for row in rows]
|
|
|
|
def delete_session(self, session_id: str) -> bool:
|
|
"""
|
|
Delete a session.
|
|
|
|
Args:
|
|
session_id: Session ID to delete
|
|
|
|
Returns:
|
|
True if deleted successfully
|
|
"""
|
|
if not self.db_path.exists():
|
|
return False
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
cursor = conn.cursor()
|
|
|
|
try:
|
|
# Delete session
|
|
cursor.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
|
|
|
# Delete messages
|
|
cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,))
|
|
|
|
conn.commit()
|
|
logger.info(f"Deleted session: {session_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error deleting session {session_id}: {e}")
|
|
conn.rollback()
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
def cleanup_old_sessions(self) -> int:
|
|
"""
|
|
Clean up old sessions based on policy.
|
|
|
|
Returns:
|
|
Number of sessions deleted
|
|
"""
|
|
if not self.policy.auto_delete:
|
|
return 0
|
|
|
|
old_sessions = self.list_old_sessions()
|
|
deleted_count = 0
|
|
|
|
for session_id, _ in old_sessions:
|
|
if self.delete_session(session_id):
|
|
deleted_count += 1
|
|
|
|
logger.info(f"Cleaned up {deleted_count} old sessions")
|
|
return deleted_count
|
|
|
|
def get_session_count(self) -> int:
|
|
"""
|
|
Get total number of sessions.
|
|
|
|
Returns:
|
|
Number of sessions
|
|
"""
|
|
if not self.db_path.exists():
|
|
return 0
|
|
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM sessions")
|
|
count = cursor.fetchone()[0]
|
|
conn.close()
|
|
|
|
return count
|
|
|
|
def enforce_max_sessions(self) -> int:
|
|
"""
|
|
Enforce maximum session limit by deleting oldest sessions.
|
|
|
|
Returns:
|
|
Number of sessions deleted
|
|
"""
|
|
current_count = self.get_session_count()
|
|
|
|
if current_count <= self.policy.max_sessions:
|
|
return 0
|
|
|
|
# Get oldest sessions to delete
|
|
conn = sqlite3.connect(str(self.db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT session_id
|
|
FROM sessions
|
|
ORDER BY created_at ASC
|
|
LIMIT ?
|
|
""", (current_count - self.policy.max_sessions,))
|
|
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
deleted_count = 0
|
|
for row in rows:
|
|
if self.delete_session(row["session_id"]):
|
|
deleted_count += 1
|
|
|
|
logger.info(f"Enforced max sessions: deleted {deleted_count} sessions")
|
|
return deleted_count
|
|
|
|
|
|
# Global retention manager
|
|
_retention = ConversationRetention()
|
|
|
|
|
|
def get_retention_manager() -> ConversationRetention:
|
|
"""Get the global retention manager instance."""
|
|
return _retention
|