Files
system-docs/v4-archive/penny/docs/PENNY_ESPECIFICACION.md
2025-12-24 17:28:34 +00:00

48 KiB

PENNY — Asistente Personal de Voz

Especificación Técnica v1.0
Componente del Sistema DECK


Índice

  1. Definición
  2. Arquitectura
  3. Planos de Información
  4. Estructura del Log
  5. Integración con GRACE
  6. Pipeline de Voz
  7. Modelos Autoalojados
  8. Conversación Natural por Turnos
  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

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

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

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

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

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

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

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

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

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

{
  "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

{
  "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

# 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

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

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

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

# 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

# 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

# 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