1194 lines
48 KiB
Markdown
1194 lines
48 KiB
Markdown
|
|
# 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*
|