Sergei b2639dd540 Add Sherpa TTS voice synthesis system
Core TTS infrastructure:
- sherpaTTS.ts: Sherpa ONNX integration for offline TTS
- TTSErrorBoundary.tsx: Error boundary for TTS failures
- ErrorBoundary.tsx: Generic error boundary component
- VoiceIndicator.tsx: Visual indicator for voice activity
- useSpeechRecognition.ts: Speech-to-text hook
- DebugLogger.ts: Debug logging utility

Features:
- Offline voice synthesis (no internet needed)
- Multiple voices support
- Real-time voice activity indication
- Error recovery and fallback
- Debug logging for troubleshooting

Tech stack:
- Sherpa ONNX runtime
- React Native Audio
- Expo modules
2026-01-14 19:09:27 -08:00

176 lines
4.1 KiB
TypeScript

/**
* Centralized Debug Logger
* Captures console logs, errors, warnings for display in Debug tab
*/
export interface LogEntry {
id: string;
timestamp: Date;
level: 'log' | 'warn' | 'error' | 'info';
category: string; // e.g., "TTS", "STT", "Chat", "System"
message: string;
data?: any;
}
class DebugLogger {
private logs: LogEntry[] = [];
private maxLogs = 500; // Keep last 500 logs
private listeners: Set<(logs: LogEntry[]) => void> = new Set();
// Original console methods
private originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
};
constructor() {
this.interceptConsole();
}
/**
* Intercept console methods to capture logs
*/
private interceptConsole() {
console.log = (...args: any[]) => {
this.originalConsole.log(...args);
this.addLog('log', 'System', this.formatArgs(args));
};
console.warn = (...args: any[]) => {
this.originalConsole.warn(...args);
this.addLog('warn', 'System', this.formatArgs(args));
};
console.error = (...args: any[]) => {
this.originalConsole.error(...args);
this.addLog('error', 'System', this.formatArgs(args));
};
console.info = (...args: any[]) => {
this.originalConsole.info(...args);
this.addLog('info', 'System', this.formatArgs(args));
};
}
/**
* Format console arguments into string
*/
private formatArgs(args: any[]): string {
return args
.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch {
return String(arg);
}
}
return String(arg);
})
.join(' ');
}
/**
* Add a log entry
*/
private addLog(level: LogEntry['level'], category: string, message: string, data?: any) {
const entry: LogEntry = {
id: `${Date.now()}-${Math.random()}`,
timestamp: new Date(),
level,
category,
message,
data,
};
this.logs.push(entry);
// Trim old logs
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
// Notify listeners
this.notifyListeners();
}
/**
* Public logging methods with category support
*/
log(category: string, message: string, data?: any) {
this.originalConsole.log(`[${category}]`, message, data || '');
this.addLog('log', category, message, data);
}
warn(category: string, message: string, data?: any) {
this.originalConsole.warn(`[${category}]`, message, data || '');
this.addLog('warn', category, message, data);
}
error(category: string, message: string, data?: any) {
this.originalConsole.error(`[${category}]`, message, data || '');
this.addLog('error', category, message, data);
}
info(category: string, message: string, data?: any) {
this.originalConsole.info(`[${category}]`, message, data || '');
this.addLog('info', category, message, data);
}
/**
* Get all logs
*/
getLogs(): LogEntry[] {
return [...this.logs];
}
/**
* Get logs by category
*/
getLogsByCategory(category: string): LogEntry[] {
return this.logs.filter(log => log.category === category);
}
/**
* Clear all logs
*/
clear() {
this.logs = [];
this.notifyListeners();
}
/**
* Subscribe to log updates
*/
subscribe(listener: (logs: LogEntry[]) => void) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/**
* Notify all listeners
*/
private notifyListeners() {
this.listeners.forEach(listener => listener(this.getLogs()));
}
/**
* Export logs as text
*/
exportAsText(): string {
return this.logs
.map(log => {
const time = log.timestamp.toLocaleTimeString();
const level = log.level.toUpperCase().padEnd(5);
const category = `[${log.category}]`.padEnd(12);
return `${time} ${level} ${category} ${log.message}`;
})
.join('\n');
}
}
// Singleton instance
export const debugLogger = new DebugLogger();