visioncore
by Denisijcu
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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues