# PENNY — Asistente Personal de Voz **Especificación Técnica v1.0** **Componente del Sistema DECK** --- ## Índice 1. [Definición](#1-definición) 2. [Arquitectura](#2-arquitectura) 3. [Planos de Información](#3-planos-de-información) 4. [Estructura del Log](#4-estructura-del-log) 5. [Integración con GRACE](#5-integración-con-grace) 6. [Pipeline de Voz](#6-pipeline-de-voz) 7. [Modelos Autoalojados](#7-modelos-autoalojados) 8. [Conversación Natural por Turnos](#8-conversación-natural-por-turnos) 9. [Implementación de Referencia](#9-implementación-de-referencia) --- ## 1. Definición ### 1.1 ¿Qué es PENNY? PENNY es el **asistente personal de voz** del sistema DECK. Proporciona una interfaz conversacional hablada 100% natural para interactuar con el ecosistema. ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PENNY │ │ │ │ • ES la voz del DECK │ │ • ES la interfaz hablada con el usuario │ │ • ES quien llama a GRACE cuando necesita datos │ │ • HABLA con el usuario (GRACE no puede) │ │ • REGISTRA todo en el log (planos de información) │ │ • MANTIENE contexto durante la sesión │ │ │ │ "PENNY habla, GRACE procesa, el Log recuerda." │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 1.2 Principios Fundamentales | Principio | Descripción | |-----------|-------------| | **Privacidad radical** | Audio procesado 100% local en servidor autoalojado | | **Conversación natural** | Turnos fluidos, interrupciones, latencia <2s | | **Log como fuente de verdad** | Todo está en el log, incluyendo personalidad | | **Planos separados** | Contextos cargables independientemente | | **GRACE como backend** | PENNY pregunta, GRACE extrae, PENNY responde | ### 1.3 Rol en el Sistema ``` ┌─────────────────────────────────────────────────────────────────────┐ │ DECK (Servidor Personal) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Usuario ◄────────────────────────────────────────────► PENNY │ │ (voz natural) │ (habla) │ │ │ │ │ ┌──────────────┼──────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ GRACE │ │ LOG │ │ VAULT │ │ │ │ (datos) │ │(planos) │ │(recados)│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 2. Arquitectura ### 2.1 Diagrama de Componentes ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PENNY VOICE ENGINE (Servidor Local) │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Silero │ │ Faster │ │ PENNY │ │ XTTS-v2 │ │ │ │ VAD │──▶ Whisper │──▶ CORE │──▶ /Kokoro │ │ │ │ │ │ │ │ │ │ │ │ │ │ Detecta │ │ Transcribe │ │ Orquesta │ │ Sintetiza │ │ │ │ voz │ │ audio │ │ todo │ │ respuesta │ │ │ └────────────┘ └────────────┘ └─────┬──────┘ └────────────┘ │ │ │ │ │ ┌───────────────────┼───────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ PLANOS │ │ GRACE │ │ THE VAULT │ │ │ │ (Log) │ │ (Contrato │ │ (Recados) │ │ │ │ │ │ Común) │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ SOLO texto (nunca audio) ▼ ┌─────────────────────────────┐ │ Claude API │ │ (Conversación inteligente) │ └─────────────────────────────┘ ``` ### 2.2 Flujo de Procesamiento ``` ┌─────────────────────────────────────────────────────────────────────┐ │ FLUJO VOZ → VOZ │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. VAD DETECTA (Silero) │ │ └── Usuario empieza a hablar │ │ └── Usuario termina (silencio 700ms) │ │ │ │ 2. ASR TRANSCRIBE (Faster-Whisper) │ │ └── Audio → Texto │ │ └── ~200-400ms con GPU │ │ │ │ 3. PENNY CORE PROCESA │ │ ├── Cargar planos relevantes │ │ ├── ¿Necesita GRACE? → Llamar con Contrato Común │ │ ├── Construir prompt con contexto │ │ └── Enviar a Claude API (solo texto) │ │ │ │ 4. LLM RESPONDE (Claude) │ │ └── Streaming de tokens │ │ └── ~1-2s primera palabra │ │ │ │ 5. TTS SINTETIZA (XTTS-v2/Kokoro) │ │ └── Texto → Audio │ │ └── Streaming mientras llegan tokens │ │ │ │ 6. LOG REGISTRA │ │ └── Todo el intercambio → plano CONVERSACION │ │ │ │ LATENCIA TOTAL OBJETIVO: <2 segundos voice-to-voice │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Planos de Información ### 3.1 Concepto El Log de PENNY está organizado en **planos** (layers) que se cargan independientemente. Cada plano tiene un propósito específico y puede actualizarse sin afectar a los demás. ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PLANOS DE INFORMACIÓN │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 0: SISTEMA │ │ │ │ Instrucciones base, comportamiento fundamental │ │ │ │ INMUTABLE durante sesión │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 1: PERSONALIDAD │ │ │ │ Tono, estilo, nombre, voz │ │ │ │ CONFIGURABLE por usuario │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 2: CONTEXTO PERSONAL │ │ │ │ Información del usuario, preferencias, historial │ │ │ │ PERSISTENTE entre sesiones │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 3: CONTEXTO AMBIENTAL │ │ │ │ Fecha, hora, ubicación, estado del sistema │ │ │ │ DINÁMICO (actualiza cada sesión) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 4: DATASET │ │ │ │ Datos específicos de la tarea actual │ │ │ │ OPCIONAL (carga bajo demanda) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ PLANO 5: CONVERSACIÓN │ │ │ │ Historial de la sesión actual │ │ │ │ VOLÁTIL (se archiva al cerrar sesión) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 3.2 Definición de Planos #### PLANO 0: SISTEMA ```yaml plano: SISTEMA id: plano_0 persistencia: permanente modificable: false contenido: version: "1.0" nombre_asistente: "Penny" capacidades: - conversacion_natural - gestion_recados - consulta_datos_via_grace - registro_habitos limitaciones: - no_modifica_datos_directamente - no_ejecuta_acciones_externas - no_accede_internet reglas: - respuestas_concisas_max_3_oraciones - confirmar_antes_de_crear_recados - nunca_inventar_datos integraciones: grace: true vault: true vision_builder: true ``` #### PLANO 1: PERSONALIDAD ```yaml plano: PERSONALIDAD id: plano_1 persistencia: permanente modificable: true contenido: nombre: "Penny" tono: "Cercano pero profesional" estilo: "Directo, sin rodeos, eficiente" tratamiento: "Tuteo natural" humor: "Ligero cuando apropiado" muletillas_evitar: - "¡Claro que sí!" - "¡Por supuesto!" - "¡Genial!" expresiones_permitidas: - "Vale" - "Entendido" - "Listo" voz_tts: modelo: "xtts-v2" speaker_wav: "/voces/penny_base.wav" idioma: "es" velocidad: 1.0 ``` #### PLANO 2: CONTEXTO PERSONAL ```yaml plano: CONTEXTO_PERSONAL id: plano_2 persistencia: permanente modificable: true player_id: "uuid-usuario" contenido: nombre_usuario: "Carlos" preferencias: idioma: "es" formato_hora: "24h" formato_fecha: "dd/mm/yyyy" recordar: - "Prefiere respuestas cortas" - "Trabaja en tecnología" - "Tiene reuniones los lunes por la mañana" no_recordar: - datos_financieros_especificos - conversaciones_marcadas_privadas historial_reciente: ultima_sesion: "2025-12-15T18:30:00Z" temas_frecuentes: - "proyectos" - "recados" - "calendario" ``` #### PLANO 3: CONTEXTO AMBIENTAL ```yaml plano: CONTEXTO_AMBIENTAL id: plano_3 persistencia: sesion modificable: auto contenido: fecha: "2025-12-16" hora: "10:30" dia_semana: "martes" timezone: "Europe/Madrid" estado_sistema: grace_disponible: true vault_disponible: true ultimo_backup: "2025-12-16T03:00:00Z" recados_pendientes: 3 eventos_hoy: - "14:00 - Llamada con cliente" - "17:00 - Revisión semanal" ``` #### PLANO 4: DATASET ```yaml plano: DATASET id: plano_4 persistencia: sesion modificable: false carga: bajo_demanda contenido: tipo: "productos" fuente: "/bloques/datasets/catalogo_2025.json" registros: 1547 campos: - nombre - precio - stock - categoria fecha_actualizacion: "2025-12-10" ``` #### PLANO 5: CONVERSACIÓN ```yaml plano: CONVERSACION id: plano_5 persistencia: sesion modificable: append_only contenido: sesion_id: "uuid-sesion" inicio: "2025-12-16T10:25:00Z" turnos: - turno: 1 rol: "usuario" timestamp: "2025-12-16T10:25:15Z" texto: "¿Qué tengo pendiente para hoy?" audio_hash: "sha256..." duracion_ms: 2100 - turno: 2 rol: "penny" timestamp: "2025-12-16T10:25:17Z" texto: "Tienes 3 recados pendientes y 2 eventos: llamada con cliente a las 14:00 y revisión semanal a las 17:00." tokens_usados: 45 latencia_ms: 1850 grace_llamadas: 0 ``` ### 3.3 Planos Adicionales Sugeridos | Plano | Propósito | Persistencia | |-------|-----------|--------------| | **MEMORIA_CORTA** | Últimas 5 sesiones resumidas | 7 días | | **EXPERTISE** | Conocimiento específico de dominio | Permanente | | **RESTRICCIONES** | Límites y reglas temporales | Configurable | | **MODO** | Estado actual (trabajo/personal/descanso) | Sesión | --- ## 4. Estructura del Log ### 4.1 Tabla Principal: PENNY_LOG ```sql CREATE TABLE penny_log ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), sesion_id UUID NOT NULL, turno_index INTEGER NOT NULL, -- Timing timestamp_inicio TIMESTAMPTZ NOT NULL, timestamp_fin TIMESTAMPTZ, -- Contenido rol VARCHAR(10) NOT NULL CHECK (rol IN ('usuario', 'penny', 'sistema')), texto TEXT NOT NULL, texto_hash VARCHAR(64) NOT NULL, -- Audio (solo para entrada de usuario) audio_ref VARCHAR(255), -- hostinger://audio/sesion/turno.wav audio_hash VARCHAR(64), audio_duracion_ms INTEGER, -- Métricas latencia_total_ms INTEGER, latencia_asr_ms INTEGER, latencia_llm_ms INTEGER, latencia_tts_ms INTEGER, tokens_prompt INTEGER, tokens_respuesta INTEGER, -- Trazabilidad trace_id UUID, grace_llamadas JSONB DEFAULT '[]', -- Planos cargados planos_activos JSONB NOT NULL, -- Calidad interrumpido BOOLEAN DEFAULT false, confidence_asr DECIMAL(4,3), -- Índices CONSTRAINT unique_sesion_turno UNIQUE (sesion_id, turno_index) ); -- Índices CREATE INDEX idx_penny_log_sesion ON penny_log(sesion_id); CREATE INDEX idx_penny_log_timestamp ON penny_log(timestamp_inicio); CREATE INDEX idx_penny_log_trace ON penny_log(trace_id); ``` ### 4.2 Tabla: PENNY_PLANOS ```sql CREATE TABLE penny_planos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación plano_tipo VARCHAR(50) NOT NULL, plano_version VARCHAR(20) NOT NULL, player_id UUID, -- NULL para planos globales -- Contenido contenido JSONB NOT NULL, contenido_hash VARCHAR(64) NOT NULL, -- Metadata fecha_creacion TIMESTAMPTZ DEFAULT NOW(), fecha_modificacion TIMESTAMPTZ DEFAULT NOW(), modificado_por VARCHAR(100), -- Estado activo BOOLEAN DEFAULT true, CONSTRAINT unique_plano_version UNIQUE (plano_tipo, plano_version, player_id) ); ``` ### 4.3 Tabla: PENNY_SESIONES ```sql CREATE TABLE penny_sesiones ( id UUID PRIMARY KEY, -- Usuario player_id UUID NOT NULL, -- Timing inicio TIMESTAMPTZ NOT NULL, fin TIMESTAMPTZ, duracion_total_ms INTEGER, -- Estadísticas total_turnos INTEGER DEFAULT 0, total_tokens_prompt INTEGER DEFAULT 0, total_tokens_respuesta INTEGER DEFAULT 0, -- Planos usados planos_cargados JSONB NOT NULL, -- Resumen resumen TEXT, -- Generado al cerrar sesión temas_detectados JSONB DEFAULT '[]', -- Estado estado VARCHAR(20) DEFAULT 'activa' CHECK (estado IN ('activa', 'cerrada', 'archivada')) ); ``` ### 4.4 Ejemplo de Registro Completo ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "sesion_id": "660e8400-e29b-41d4-a716-446655440001", "turno_index": 3, "timestamp_inicio": "2025-12-16T10:26:00.000Z", "timestamp_fin": "2025-12-16T10:26:01.850Z", "rol": "penny", "texto": "La factura es de Telefónica por 45,90 euros con fecha del 10 de diciembre.", "texto_hash": "sha256:a1b2c3...", "latencia_total_ms": 1850, "latencia_asr_ms": 0, "latencia_llm_ms": 1200, "latencia_tts_ms": 650, "tokens_prompt": 1250, "tokens_respuesta": 32, "trace_id": "770e8400-e29b-41d4-a716-446655440002", "grace_llamadas": [ { "modulo": "FIELD_EXTRACTOR", "trace_id": "880e8400-e29b-41d4-a716-446655440003", "latencia_ms": 450, "status": "SUCCESS" } ], "planos_activos": { "plano_0": "v1.0", "plano_1": "v1.2", "plano_2": "v3.1", "plano_3": "auto", "plano_4": null, "plano_5": "current" }, "interrumpido": false, "confidence_asr": null } ``` --- ## 5. Integración con GRACE ### 5.1 Principio Fundamental ``` ┌─────────────────────────────────────────────────────────────────────┐ │ PENNY ↔ GRACE │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ PENNY puede HABLAR GRACE puede PROCESAR │ │ PENNY no puede PROCESAR DATOS GRACE no puede HABLAR │ │ │ │ Cuando PENNY necesita datos: │ │ 1. PENNY llama a GRACE usando Contrato Común │ │ 2. GRACE procesa y devuelve datos estructurados │ │ 3. PENNY formula respuesta hablada con los datos │ │ │ │ "PENNY es la voz, GRACE es el cerebro analítico" │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 5.2 Módulos de GRACE que PENNY Puede Invocar | Módulo | Cuándo PENNY lo llama | |--------|----------------------| | `ASR` | Audio del usuario → Texto (interno, no directo) | | `TTS` | Texto de respuesta → Audio (interno, no directo) | | `FIELD_EXTRACTOR` | "¿Qué dice esta factura?" | | `SUMMARIZER` | "Resume este documento" | | `CLASSIFIER` | "¿De qué tipo es este email?" | | `TASK_DETECTOR` | "¿Qué tareas hay en esta reunión?" | | `LANG_NORMALIZER` | Detectar idioma de entrada | ### 5.3 Formato de Llamada PENNY → GRACE ```json { "contract_version": "1.2", "profile": "FULL", "envelope": { "trace_id": "penny-trace-uuid", "parent_trace_id": "sesion-uuid", "step_id": "grace-call-uuid", "step_index": 1, "total_steps": 1, "idempotency_key": "sha256-del-contenido", "timestamp_init": "2025-12-16T10:26:00.000Z", "ttl_ms": 10000 }, "routing": { "module": "FIELD_EXTRACTOR", "version": "1.0", "provider_preference": ["local"], "fallback_chain": ["FIELD_EXTRACTOR_LOCAL"], "max_fallback_level": 1 }, "context": { "lang": "es", "mode": "strict", "pii_filter": true, "player_id": "uuid-usuario", "caller": "PENNY", "caller_sesion": "uuid-sesion-penny" }, "payload": { "type": "document", "encoding": "url", "content": "hostinger://uploads/factura_2025.pdf", "content_hash": "sha256...", "schema_expected": "extracted_invoice" }, "storage": { "input_location": "internal", "output_location": "internal", "persist_intermediate": false }, "security": { "encryption_profile": "E2E_BASIC", "data_sensitivity": "MEDIUM", "pii_detected": true } } ``` ### 5.4 Flujo Completo de Ejemplo ``` Usuario: "¿Qué dice la factura que escaneé ayer?" ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 1: ASR (interno) │ │ Audio → "¿Qué dice la factura que escaneé ayer?" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 2: PENNY detecta necesidad de GRACE │ │ Análisis: usuario pide contenido de documento → FIELD_EXTRACTOR │ │ Buscar: última factura escaneada del usuario │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 3: PENNY → GRACE (Contrato Común) │ │ Request: FIELD_EXTRACTOR + documento │ │ Response: { proveedor: "Telefónica", total: 45.90, fecha: "..." } │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 4: PENNY construye respuesta │ │ Prompt a Claude con datos de GRACE + contexto de planos │ │ Respuesta: "La factura es de Telefónica por 45,90€..." │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 5: TTS (interno) │ │ Texto → Audio │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ PASO 6: LOG │ │ Registrar turno completo + llamada a GRACE + métricas │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 6. Pipeline de Voz ### 6.1 Stack Autoalojado Recomendado ```yaml # Configuración 100% local para máxima privacidad pipeline: vad: modelo: silero-vad-v5 ubicacion: local config: threshold: 0.5 min_speech_duration_ms: 250 min_silence_duration_ms: 700 # Fin de turno speech_pad_ms: 30 asr: modelo: faster-whisper variante: large-v3 # Máxima calidad ubicacion: local config: compute_type: float16 device: cuda language: es beam_size: 5 vad_filter: true fallback: - variante: distil-large-v2 # Más rápido - variante: medium # Mínimo aceptable llm: modelo: claude-sonnet-4 ubicacion: api # Único componente externo config: max_tokens: 150 temperature: 0.7 streaming: true nota: "Solo texto, nunca audio. Zero Data Retention si enterprise." tts: modelo: xtts-v2 ubicacion: local config: speaker_wav: "/voces/penny_base.wav" language: es gpt_cond_len: 6 streaming: true fallback: - modelo: kokoro-82m # Más rápido, menor calidad - modelo: piper # Emergencia ``` ### 6.2 Requisitos de Hardware ```yaml hardware_minimo: gpu: RTX 3060 (12GB VRAM) cpu: 8 cores ram: 32GB almacenamiento: 100GB SSD NVMe distribucion_vram: faster_whisper_large: ~3GB xtts_v2: ~3GB buffer: ~6GB hardware_recomendado: gpu: RTX 3080/3090 (10-24GB VRAM) cpu: 12+ cores ram: 64GB almacenamiento: 500GB SSD NVMe beneficios: - Modelos más grandes - Batch processing - Menor latencia ``` ### 6.3 Latencias Objetivo | Componente | Objetivo | Máximo Aceptable | |------------|----------|------------------| | VAD | <50ms | 100ms | | ASR | <400ms | 800ms | | LLM (primer token) | <500ms | 1500ms | | LLM (completo) | <1500ms | 3000ms | | TTS (primer audio) | <200ms | 500ms | | **Total voice-to-voice** | **<2000ms** | **<4000ms** | --- ## 7. Modelos Autoalojados ### 7.1 Comparativa ASR | Modelo | Latencia | Calidad | VRAM | Recomendación | |--------|----------|---------|------|---------------| | Whisper Large V3 | 400ms | ★★★★★ | 3GB | **Producción** | | Whisper Distil Large V2 | 200ms | ★★★★☆ | 2GB | Baja latencia | | Whisper Medium | 300ms | ★★★☆☆ | 2GB | Fallback | | Whisper Small | 150ms | ★★☆☆☆ | 1GB | No recomendado | ### 7.2 Comparativa TTS | Modelo | Latencia | Naturalidad | VRAM | Clonación | Recomendación | |--------|----------|-------------|------|-----------|---------------| | XTTS-v2 | 300ms | ★★★★★ | 3GB | ✅ 6s audio | **Producción** | | Kokoro-82M | 150ms | ★★★★☆ | 1GB | ❌ | Baja latencia | | Piper | 50ms | ★★★☆☆ | <1GB | ❌ | Emergencia | | OpenVoice | 400ms | ★★★★☆ | 2GB | ✅ | Alternativa | ### 7.3 Experiencias Reales de Usuarios ``` ┌─────────────────────────────────────────────────────────────────────┐ │ FASTER-WHISPER + XTTS-v2 en RTX 3080 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ "Conseguí ~500ms latencia voice-to-voice con un modelo 24B │ │ de Mistral via Ollama. La clave es el streaming de TTS." │ │ — KoljaB, RealtimeVoiceChat │ │ │ │ "En una 4090 con faster-distil-whisper-large-v2 bajamos │ │ a 300ms de latencia total. Es casi como hablar con alguien." │ │ — voicechat2 │ │ │ │ "XTTS-v2 clone de la voz de mi abuelo me hizo llorar. │ │ Vale cada dolor de cabeza de instalación." │ │ — GitHub contributor │ │ │ │ "El turn detection es lo más difícil. Silero VAD funciona │ │ bien pero hay que ajustar los thresholds para cada persona." │ │ — Reddit r/LocalLLaMA │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 8. Conversación Natural por Turnos ### 8.1 Principios de Turn-Taking ``` ┌─────────────────────────────────────────────────────────────────────┐ │ CONVERSACIÓN NATURAL │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. DETECCIÓN DE FIN DE TURNO │ │ ├── Silencio: 700ms mínimo (configurable) │ │ ├── Prosodia: bajada de tono indica fin │ │ └── Semántica: frase completa detectada │ │ │ │ 2. BARGE-IN (Interrupción) │ │ ├── Usuario puede interrumpir a PENNY │ │ ├── VAD detecta voz mientras TTS reproduce │ │ └── Se cancela TTS y se procesa nueva entrada │ │ │ │ 3. BACKCHANNELING │ │ ├── "Mm-hmm", "Vale", "Sí" no son turnos completos │ │ └── PENNY no debe responder a acknowledgments │ │ │ │ 4. OVERLAPPING │ │ ├── Permitir ligero solapamiento natural │ │ └── No cortar bruscamente al usuario │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 8.2 Configuración de Turn Detection ```yaml turn_detection: silero_vad: speech_threshold: 0.5 min_speech_ms: 250 min_silence_ms: 700 # Fin de turno padding_ms: 30 end_of_turn: # Tiempo de silencio para considerar fin de turno confident_silence_ms: 500 # Si hay alta confianza semántica uncertain_silence_ms: 1000 # Si la frase parece incompleta max_silence_ms: 2000 # Forzar fin de turno barge_in: enabled: true min_user_speech_ms: 200 # Evitar falsos positivos cancel_tts: true cancel_llm_streaming: true backchanneling: ignore_patterns: - "^(mhm|mm|sí|ajá|vale|ok|ya)$" max_duration_ms: 500 ``` ### 8.3 Manejo de Estados ``` ┌─────────────────────────────────────────────────────────────────────┐ │ ESTADOS DE PENNY │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ IDLE ──────────────────────────────────────────────────────────▶ │ │ │ │ │ │ VAD detecta voz │ │ ▼ │ │ LISTENING ─────────────────────────────────────────────────────▶ │ │ │ │ │ │ Silencio detectado (end of turn) │ │ ▼ │ │ PROCESSING ────────────────────────────────────────────────────▶ │ │ │ │ │ │ ASR completo + LLM responde │ │ ▼ │ │ SPEAKING ──────────────────────────────────────────────────────▶ │ │ │ │ │ ├── TTS termina → IDLE │ │ │ │ │ └── Barge-in → LISTENING (cancela TTS) │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 8.4 Feedback Visual/Sonoro ```yaml feedback: estados: idle: led: "azul tenue" sonido: null listening: led: "azul pulsante" sonido: "bip_inicio" # Opcional processing: led: "amarillo" sonido: null speaking: led: "verde" sonido: null error: led: "rojo" sonido: "error_tone" transiciones: inicio_escucha: true # Feedback al empezar a escuchar fin_escucha: false # Sin feedback (fluido) inicio_respuesta: false # Directo a hablar ``` --- ## 9. Implementación de Referencia ### 9.1 Estructura de Directorios ``` /penny/ ├── config/ │ ├── pipeline.yaml # Configuración de modelos │ ├── planos/ │ │ ├── sistema.yaml # Plano 0 │ │ ├── personalidad.yaml # Plano 1 │ │ └── default_user.yaml # Plano 2 template │ └── turn_detection.yaml # Configuración de turnos │ ├── core/ │ ├── penny_core.py # Orquestador principal │ ├── planos_manager.py # Gestión de planos │ ├── grace_client.py # Cliente para GRACE │ └── log_writer.py # Escritura a PENNY_LOG │ ├── voice/ │ ├── vad_processor.py # Silero VAD │ ├── asr_processor.py # Faster-Whisper │ ├── tts_processor.py # XTTS-v2 / Kokoro │ └── turn_manager.py # Gestión de turnos │ ├── models/ │ ├── whisper/ │ │ └── large-v3/ │ ├── xtts/ │ │ └── v2/ │ └── silero/ │ └── vad_v5.onnx │ ├── voces/ │ └── penny_base.wav # Voz base para clonación │ └── server/ ├── main.py # Servidor FastAPI ├── websocket.py # WebSocket handler └── routes.py # Endpoints REST ``` ### 9.2 Dependencias ```bash # requirements.txt # Framework pipecat-ai>=0.0.50 pipecat-ai[silero,whisper,coqui] # LLM anthropic>=0.35.0 # Audio pyaudio>=0.2.14 numpy>=1.24.0 scipy>=1.11.0 soundfile>=0.12.0 # Servidor fastapi>=0.109.0 uvicorn>=0.27.0 websockets>=12.0 # Base de datos asyncpg>=0.29.0 sqlalchemy>=2.0.0 # Utilidades pyyaml>=6.0 python-dotenv>=1.0.0 ``` ### 9.3 Configuración de Inicio ```yaml # config/pipeline.yaml penny: version: "1.0" server: host: "0.0.0.0" port: 8765 websocket_path: "/ws/voice" models: vad: type: "silero" path: "models/silero/vad_v5.onnx" asr: type: "faster-whisper" model: "large-v3" path: "models/whisper/large-v3" device: "cuda" compute_type: "float16" tts: type: "xtts-v2" path: "models/xtts/v2" speaker_wav: "voces/penny_base.wav" language: "es" llm: provider: "anthropic" model: "claude-sonnet-4-20250514" max_tokens: 150 temperature: 0.7 streaming: true grace: endpoint: "http://localhost:8080/api/v1" timeout_ms: 10000 log: database_url: "postgresql://penny:xxx@localhost:5432/penny_db" retain_audio_days: 7 retain_text_days: 365 ``` ### 9.4 Ejemplo de Código Core ```python # core/penny_core.py import asyncio from typing import Optional from dataclasses import dataclass from .planos_manager import PlanosManager from .grace_client import GraceClient from .log_writer import LogWriter @dataclass class PennyConfig: planos_path: str grace_endpoint: str llm_config: dict log_config: dict class PennyCore: """Orquestador principal de PENNY.""" def __init__(self, config: PennyConfig): self.config = config self.planos = PlanosManager(config.planos_path) self.grace = GraceClient(config.grace_endpoint) self.log = LogWriter(config.log_config) self.sesion_id: Optional[str] = None self.turno_index: int = 0 async def iniciar_sesion(self, player_id: str) -> str: """Inicia una nueva sesión de conversación.""" self.sesion_id = await self.log.crear_sesion(player_id) self.turno_index = 0 # Cargar planos await self.planos.cargar_plano(0) # Sistema await self.planos.cargar_plano(1) # Personalidad await self.planos.cargar_plano(2, player_id) # Contexto personal await self.planos.cargar_plano(3) # Contexto ambiental (auto) return self.sesion_id async def procesar_turno(self, texto_usuario: str, audio_ref: Optional[str] = None) -> str: """Procesa un turno del usuario y genera respuesta.""" self.turno_index += 1 timestamp_inicio = datetime.utcnow() # Detectar si necesita GRACE grace_llamadas = [] if self._necesita_grace(texto_usuario): resultado_grace = await self._llamar_grace(texto_usuario) grace_llamadas.append(resultado_grace) # Construir prompt con planos prompt = self._construir_prompt(texto_usuario, grace_llamadas) # Llamar a Claude respuesta = await self._llamar_llm(prompt) # Registrar en log await self.log.registrar_turno( sesion_id=self.sesion_id, turno_index=self.turno_index, rol="usuario", texto=texto_usuario, audio_ref=audio_ref, timestamp_inicio=timestamp_inicio ) await self.log.registrar_turno( sesion_id=self.sesion_id, turno_index=self.turno_index + 1, rol="penny", texto=respuesta, grace_llamadas=grace_llamadas, timestamp_inicio=datetime.utcnow() ) self.turno_index += 1 return respuesta def _necesita_grace(self, texto: str) -> bool: """Determina si la consulta requiere llamar a GRACE.""" keywords = ["factura", "documento", "escaneé", "resume", "extraer"] return any(kw in texto.lower() for kw in keywords) async def _llamar_grace(self, texto: str) -> dict: """Llama a GRACE con el Contrato Común.""" modulo = self._detectar_modulo_grace(texto) return await self.grace.llamar( modulo=modulo, payload={"query": texto}, caller_sesion=self.sesion_id ) def _construir_prompt(self, texto: str, grace_datos: list) -> str: """Construye el prompt completo con todos los planos.""" planos_texto = self.planos.obtener_contexto_completo() prompt = f""" {planos_texto} --- DATOS DE GRACE --- {json.dumps(grace_datos, indent=2) if grace_datos else "No se consultaron datos."} --- USUARIO --- {texto} --- INSTRUCCIONES --- Responde de forma concisa (máximo 3 oraciones) siguiendo tu personalidad definida. """ return prompt ``` --- ## Resumen PENNY es el asistente de voz del DECK con las siguientes características: 1. **100% autoalojado** excepto Claude API (solo texto) 2. **Planos de información** separados y cargables independientemente 3. **Log completo** de todas las interacciones 4. **Integración con GRACE** via Contrato Común 5. **Conversación natural por turnos** con barge-in y turn detection 6. **Latencia objetivo** <2 segundos voice-to-voice ### Próximos Pasos 1. [ ] Configurar servidor con GPU 2. [ ] Instalar modelos (Whisper, XTTS-v2, Silero) 3. [ ] Crear base de datos y tablas de log 4. [ ] Definir planos específicos del usuario 5. [ ] Grabar voz base para clonación TTS 6. [ ] Implementar pipeline con Pipecat 7. [ ] Ajustar turn detection para conversación natural --- *Documento generado: Diciembre 2025* *Versión: 1.0*