Skip to main content
Glama
README.md
# VisionCore AI — Centro de Vigilancia Autónomo 🛡️👁️

Sistema de videovigilancia con comprensión semántica de escena. N nodos de
captura transmiten fotogramas a un núcleo FastAPI; un modelo de visión local
interpreta lo que ve; un motor de reglas decide si eso merece una alarma; y un
servidor MCP le da a un agente de IA las herramientas de un operador de sala.

**Todo corre en local.** Ninguna imagen sale de la máquina.

---

## 🎯 Qué lo diferencia

Un VMS clásico (Milestone, Genetec, Avigilon) detecta **objetos**: "persona en
polígono 3", "movimiento en zona 2". Configuras zonas y ajustas umbrales de
píxeles.

VisionCore entiende **situaciones**, y las reglas se escriben en lenguaje
natural:

| Capacidad | VMS clásico | VisionCore |
| --- | --- | --- |
| "Alguien deja un bulto y se aleja" | No es una clase de objeto | Una pregunta en la regla |
| Persona a las 10:00 vs a las 03:00 | Misma detección, misma alarma | Ventana horaria por regla |
| "¿Por qué sonó esa alarma?" | Umbral superado | Motivo en lenguaje natural |
| Consultar el estado | Interfaz propietaria | El agente pregunta y responde |

Las reglas ejemplo que vienen de fábrica: fuego o humo, presencia en horario
restringido, objeto abandonado, persona en el suelo, aglomeración inusual.

---

## 🏗️ Arquitectura

Dos procesos que comparten estado por SQLite (WAL, lectura concurrente):

```
  [ capture_node ] --HTTP--> [ server/main.py ]  <--\
  [ capture_node ] --HTTP-->    FastAPI             |
   hilo por camara              + monitor           |  storage/visioncore.db
   recarga en caliente          + alarmas           |
                                + voz               |
                                                    |
                              [ mcp_server.py ] <---/
                                stdio -> agente IA
```

* **`client/capture_node.py`** — Un proceso, N cámaras, un hilo cada una.
  Webcam por índice o cámara IP por RTSP. Recarga en caliente: editas
  `cameras.json` y añade, quita o reconfigura sin reiniciar.
* **`server/main.py`** — Ingesta HTTP con escritura atómica, API de operador y
  arranque del monitor.
* **`server/monitor.py`** — Barrido autónomo con cadencia adaptativa y puerta
  de movimiento.
* **`server/rules.py`** — Motor de reglas semánticas con ventana horaria.
* **`server/alarms.py`** — Ciclo de vida de alarmas, antirruido y escalado.
* **`server/voice.py`** — Anuncios TTS locales con cola de prioridad.
* **`mcp_server.py`** — Herramientas MCP para el agente.

### Ciclo de vida de una alarma

```
NUEVA ──(nadie la atiende en escalate_after_s)──> ESCALADA
  │                                                  │
  └──────────(operador la reconoce)─────────────────┘
                      │
                      ▼
                 RECONOCIDA ──(operador cierra)──> RESUELTA
```

Todo queda en `audit_log`: quién reconoció qué y cuándo.

---

## ⚡ Las tres optimizaciones que lo hacen viable

Un modelo de visión local tarda segundos por imagen. Sin esto, seis cámaras
serían imposibles.

**1. Puerta de movimiento.** Compara una miniatura 32×32 en gris con la última
escena analizada. Si la diferencia media queda bajo el umbral, no se llama al
modelo. Un pasillo vacío de madrugada cuesta cero.

**2. Cadencia adaptativa.** Tres ritmos: alarma → 10s, movimiento sin alarma →
30s, escena quieta → hasta 300s. El cómputo se gasta donde pasa algo.

**3. Cola de análisis asíncrona.** Las herramientas MCP nunca bloquean:
devuelven el último análisis si es reciente, o encolan y responden al
instante. El trabajo lento lo hace el monitor.

---

## ⚙️ Puesta en marcha

```powershell
python -m venv venv
venv\Scripts\Activate.ps1
pip install -r requirements.txt

copy .env.example .env
copy client\cameras.example.json client\cameras.json
```

Edita `.env` con tu `LM_STUDIO_TOKEN` y `VISION_MODEL`. Ambos archivos están
en `.gitignore`: el `.env` por el token, `cameras.json` porque las URLs RTSP
llevan usuario y clave.

### El modelo de visión

**Esto decide si el sistema es usable.** Medido en una GTX 1660 Ti (6 GB):

| Modelo | Latencia por análisis |
| --- | --- |
| Gemma 4 12B QAT (7.15 GB, mitad en CPU) | ~137 s |
| Qwen3-VL-4B (entra completo en GPU) | **~6.5 s** |

Dos ajustes obligatorios en LM Studio antes de arrancar:

* **Custom Fields → Enable Thinking: OFF.** Los modelos de razonamiento gastan
  todo el presupuesto de tokens pensando y devuelven respuesta vacía. LM
  Studio **ignora** `reasoning_effort` por API; hay que apagarlo en la interfaz.
* **Parallel: 2.** Cada slot reserva su propia KV cache. Con 4 no caben las
  capas en GPU.

### Arranque

```powershell
python -m server.main                    # nucleo + monitor + voz
python -m client.capture_node            # todas las camaras habilitadas
python -m client.capture_node --list     # ver que hay definido
python -m client.capture_node --only patio
```

Documentación interactiva en `http://127.0.0.1:8000/docs`.

### Servidor MCP

```json
{
  "mcpServers": {
    "visioncore": {
      "command": "F:\\vision-core-mcp\\venv\\Scripts\\python.exe",
      "args": ["F:\\vision-core-mcp\\mcp_server.py"],
      "cwd": "F:\\vision-core-mcp"
    }
  }
}
```

Herramientas: `situation_report`, `active_alarms`, `analyze_scene`,
`check_analysis`, `sweep_all_cameras`, `acknowledge_alarm`, `resolve_alarm`,
`announce`, `audit_trail`, `list_cameras`, `system_status`.

---

## 📷 Cámaras

`client/cameras.json`:

```json
{
  "server_url": "http://localhost:8000",
  "defaults": { "fps": 1, "max_width": 640, "jpeg_quality": 80 },
  "cameras": [
    { "camera_id": "entrada", "location": "entrada_principal",
      "source": 0, "enabled": true },
    { "camera_id": "calle", "location": "acera_norte",
      "source": "rtsp://usuario:clave@192.168.1.50:554/stream1",
      "enabled": false, "fps": 0.5 }
  ]
}
```

Guardas el archivo y el supervisor aplica los cambios: arranca las nuevas, para
las que quitaste o deshabilitaste, y reinicia **solo** las que cambiaron. Si
guardas JSON inválido a medio editar, mantiene la configuración anterior en vez
de tumbar las cámaras.

---

## 🔔 Reglas

`rules.json` en la raíz (si no existe se usa el catálogo por defecto):

```json
{
  "rules": [{
    "rule_id": "intrusion_nocturna",
    "name": "Presencia en horario restringido",
    "prompt": "¿Hay alguna persona en la escena? ¿Su comportamiento parece normal o furtivo?",
    "cameras": ["*"],
    "min_level": "VERDE",
    "require_keywords": ["persona", "hombre", "mujer"],
    "exclude_keywords": ["nadie", "vacio"],
    "active_window": "22:00-06:00",
    "priority": 2,
    "cooldown_s": 60,
    "escalate_after_s": 120,
    "actions": ["voice", "log", "webhook"]
  }]
}
```

El modelo responde con `{"respuesta": true|false, "nivel": ..., "descripcion":
..., "motivo": ...}`. La regla dispara solo si `respuesta` es `true`. Las
palabras clave son un filtro adicional, con detección de negación y sin
sensibilidad a tildes.

---

## 🔌 API

| Método | Ruta | Qué hace |
| --- | --- | --- |
| `GET` | `/health` | Estado y avisos de configuración |
| `GET` | `/api/situation` | Parte de situación completo |
| `POST` | `/api/register` | Alta de cámara |
| `POST` | `/api/upload/{id}` | Ingesta de fotograma |
| `GET` | `/api/cameras` | Cámaras, online/offline, frame disponible |
| `DELETE` | `/api/cameras/{id}` | Baja de cámara |
| `POST` | `/api/analyze/{id}` | Análisis inmediato (bloquea) |
| `GET` | `/api/alerts` | Histórico de análisis |
| `GET` | `/api/alarms` | Alarmas del ciclo de vida |
| `POST` | `/api/alarms/{id}/ack` | Reconocer |
| `POST` | `/api/alarms/{id}/resolve` | Cerrar |
| `GET` | `/api/rules` | Reglas cargadas y cuáles están activas ahora |
| `POST` | `/api/monitor/tick` | Forzar un ciclo |
| `POST` | `/api/voice/announce` | Megafonía manual |
| `GET` | `/api/audit` | Registro de auditoría |

`/api/alerts` son **análisis**; `/api/alarms` son **alarmas**. Se confunden
fácil y responden preguntas distintas.

---

## 🧪 Tests

```powershell
pytest
```

167 tests. Corren sin LM Studio, sin cámara, sin tarjeta de sonido y sin red.

---

## 🩹 Bugs corregidos

Los que costaron más tiempo, por si vuelven:

| Problema | Efecto |
| --- | --- |
| Token de LM Studio escrito en el código | Credencial expuesta |
| `BASE_DIR/server/storage` calculado desde dentro de `server/` | FastAPI escribía en una carpeta y el MCP leía en otra: **el análisis nunca veía los fotogramas** |
| Se leía `result["output"]` de `/v1/chat/completions` | Campo inexistente ahí; devolvía el JSON crudo como si fuera el informe |
| `from mcp.server.mcpserver import MCPServer` | No existe en el SDK; el servidor MCP nunca importó |
| Herramienta MCP bloqueando 137 s | El cliente cortaba antes; `curl` funcionaba porque no tiene timeout |
| Modelo de razonamiento con presupuesto corto | 597 de 600 tokens pensando, `content` vacío, 200 s tirados |
| `require_keywords` por subcadena | "**No** se observa ninguna persona **caída**" generaba alarma |
| Palabras clave con tilde | `"caid"` nunca coincidió con `"caída"`; la regla llevaba meses sin funcionar |
| Puerta de movimiento por hash MD5 | El ruido del sensor cambia el JPEG siempre: no ahorró nada |
| Movimiento sin alarma tratado como calma | La cadencia se relajaba justo tras detectar actividad |
| `camera_id` sin sanear en la ruta | Un id `../../` escribía fuera de `storage/` |
| Escritura directa del JPEG | El MCP podía leer un fotograma a medio escribir |
| Bucle de registro sin pausa al fallar | 100% de CPU hasta que el servidor respondiera |
| `pydantic==2.6.4` fijado | Chocaba con el SDK de MCP; pip abortaba la instalación entera |

---

## 🚧 Qué falta para producción crítica

Honestamente, esto es una arquitectura sólida y un producto demostrable. **No
es todavía un sistema certificable** para infraestructura crítica:

* **Seguridad.** Sin TLS, sin autenticación de nodos, sin roles de operador.
  Cualquiera en la red puede subir fotogramas y cerrar alarmas.
* **Redundancia.** Un proceso, un SQLite, una GPU. Sin failover.
* **Cadena de custodia.** La evidencia no está firmada ni sellada en tiempo.
* **Latencia.** ~6.5 s por análisis sirve para vigilancia por eventos, no para
  detección de intrusión en tiempo real.

El siguiente salto arquitectónico es la **detección en dos etapas**: un
detector rápido (YOLO, ~20 ms) mira todos los fotogramas y el VLM solo
interpreta lo que el detector marca. El detector dice "hay una persona"; el
VLM dice "está forzando la puerta".

---

## 📄 Licencia

Internal Proprietary — **Vertex Coders LLC**. All Rights Reserved.