Skip to content

Latest commit

 

History

History
290 lines (229 loc) · 12.8 KB

VISION_PLAN.md

File metadata and controls

290 lines (229 loc) · 12.8 KB

Plan: Nodos especializados de visión + Leader orquestador

Estado: PLAN APROBADO, pendiente de implementación. Este documento describe el diseño acordado. No hay código implementado aún.

Visión

Cardinal-System es una arquitectura de nodos distribuidos donde todos comparten un log de eventos replicado (estilo blockchain-lite vía eventBroadcast). Hay dos tipos de nodos de cómputo:

  • Nodos réplica (workers genéricos): aumentan capacidad de cómputo horizontal.
  • Nodos especializados: realizan UNA tarea concreta en loop continuo y reportan resultados al enjambre.

Este plan añade dos nodos especializados de visión por computador:

  1. vision-yolo — detección de objetos (YOLO) sobre el stream de una cámara.
  2. vision-face — reconocimiento facial (face_recognition) contra una base de datos de caras conocidas almacenada en un volumen compartido.

Ambos son procesos Python autónomos que hablan el protocolo Envelope (un JSON por frame de texto WebSocket) directamente con el leader. El leader, además de recibir eventos, actúa como orquestador: puede levantar/detener contenedores de visión pasándoles la URL de la cámara.

Arquitectura

                         ┌─────────────────────────────────────────────────┐
                         │                  leader (Swift)                 │
                         │                                                 │
   Envelope (ws/json)    │  - recibe .detection / .faceRecognition        │
 ┌───────────────────────┤  - informa (log / fase 2: CLI + LLM acciones)   │
 │                       │  - ORQUESTA: lanza/para contenedores vision     │
 │                       │    según CAMERA_URL (Docker socket)             │
 │                       └───────┬──────────────────────┬──────────────────┘
 │                               │ docker socket        │ docker socket
 │                               ▼                      ▼
 │                    ┌──────────────────┐   ┌──────────────────────┐
 └────────────────────┤  vision-yolo     │   │   vision-face        │
        .detection    │  (Python)        │   │   (Python)           │
                      │  ultralytics+    │   │   face_recognition   │
                      │  opencv          │   │   + opencv           │
                      │  CAM: /dev/video0│   │   CAM: /dev/video1   │
                      └──────────────────┘   │   DB: known_faces/   │
                                             │   (volumen compart.) │
                                             └──────────────────────┘
                      .faceRecognition ──────▲

Decisiones tomadas

# Decisión Elección
1 Orquestación El leader instancia/para contenedores de visión pasándoles la CAMERA_URL (Docker socket).
2 Payloads Separados: DetectionPayload y FacePayload (type-safe, más claro para el LLM).
3 DB de caras Carpeta en un volumen (./FaceRecognition/known_faces), con .gitignore para no commitear fotos reales.
4 Pruebas En la Mac (cámara integrada + USB), workers nativos o con DEMO_MODE.
5 Acciones del LLM Fase 2 (después de validar que los eventos llegan).

Decisiones de diseño adicionales:

  • Contenedores separados e independientes: la inferencia corre en Python en su propio contenedor por servicio. El core Swift NO carga modelos ML ni lanza subprocesos. Los workers Python hablan Envelope como un cliente WebSocket más.
  • Cada contenedor de visión captura su propia fuente (su propia cámara). No se comparten frames entre contenedores (la pipeline "YOLO → crop → face" queda como optimización futura fuera de este plan).
  • Multi-cámara / RTSP: CAMERA_URL es un parámetro. Para IP remota solo cambia la URL (rtsp://usuario:pass@ip:554/stream). Varios nodos = varios contenedores.
  • Reconexión: los workers Python llevan loop de reconexión con backoff exponencial + restart: unless-stopped en compose. (El core Swift sigue sin reconexión nativa; es deuda aparte.)

PARTE 1 — Protocolo (core Swift, aditivo)

Sources/Cardinal-System/Network/Envelope.swift — 2 casos nuevos:

case detection(DetectionPayload)
case faceRecognition(FacePayload)

Nuevo Sources/Cardinal-System/Network/VisionPayloads.swift:

public struct DetectionPayload: Codable, Sendable, Equatable {
    public var cameraID: String
    public var timestamp: Date
    public var frame: Int
    public var objects: [DetectedObject]
}
public struct DetectedObject: Codable, Sendable, Equatable {
    public var label: String
    public var confidence: Float
    public var bbox: [Float]      // [x, y, w, h]
}

public struct FacePayload: Codable, Sendable, Equatable {
    public var cameraID: String
    public var timestamp: Date
    public var frame: Int
    public var faces: [RecognizedFace]
}
public struct RecognizedFace: Codable, Sendable, Equatable {
    public var name: String        // "alice" | "unknown"
    public var confidence: Float   // 1 - distancia
    public var bbox: [Float]
}

Compatibilidad: nodos viejos que reciban los casos nuevos simplemente no los decodifican y los ignoran (comportamiento ya existente en handleIncomingText). Cero breaking changes.

PARTE 2 — Leader maneja los eventos

Sources/Cardinal-System/System/Node.swift — 2 casos nuevos en receive(_:from:):

case .detection(let p) where role == .leader:
    onDetection?(p)
    try? await network.broadcast(.detection(p))   // réplicas / CLI
case .faceRecognition(let p) where role == .leader:
    onFaceRecognition?(p)
    try? await network.broadcast(.faceRecognition(p))
  • hooks public var onDetection / onFaceRecognition (mismo patrón que onDelegateReply).

Sources/Run/ALO.swift — el leader loguea ambos eventos (fase "informar").

Tests — round-trip Codable de ambos envelopes.

PARTE 3 — Leader orquestador (decisión 1)

Nuevo Sources/Cardinal-System/System/ContainerOrchestrator.swift (actor):

  • Se comunica con el Docker daemon para crear/arrancar/parar/eliminar contenedores de visión.
  • Implementación: invoca la Docker HTTP API sobre el socket (unix:///var/run/docker.sock) usando async-http-client (ya es dependencia). Endpoints: POST /containers/create, /start, /stop, DELETE /containers/{id}. Sin shell-out a docker, sin dependencia nueva pesada.
  • API:
actor ContainerOrchestrator {
    func launchVisionNode(kind: VisionKind, cameraURL: String, cameraID: String) async throws -> String // containerID
    func stopVisionNode(containerID: String) async throws
    func listVisionNodes() async throws -> [VisionNodeInfo]
}
enum VisionKind { case yolo, face }
  • Los contenedores lanzados reciben env (LEADER_HOST=leader, CAMERA_URL, CAMERA_ID, etc.) y etiquetas (cardinal.managed=true, cardinal.kind=yolo) para que el leader liste solo los suyos.

⚠️ Requisito de seguridad

El leader necesita el socket de Docker montado:

  leader:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Esto le da al leader control total del Docker del host (equivale a root en la máquina). Aceptable en una red/lab privada. Alternativa futura: docker-socket-proxy limitando a endpoints de contenedores.

Disparo del orquestador (fase 1)

El leader lee env vars estáticas (VISION_YOLO_CAMERA_URL, VISION_FACE_CAMERA_URL) y al arrancar lanza los contenedores correspondientes. La orquestación dinámica por demanda (que el LLM decida levantar una cámara nueva) queda para la fase 2.

PARTE 4 — Contenedor vision-yolo (Python)

Vision/ (carpeta nueva, aislada):

  • yolo_worker.py: OpenCV captura CAMERA_URL → YOLOv8n cada DETECTION_INTERVAL_MS → envía .detection por WS con reconexión/backoff. DEMO_MODE=true genera detecciones sintéticas (pruebas sin cámara).
  • requirements.txt: ultralytics, opencv-python-headless, websockets.
  • Dockerfile.vision: python:3.12-slim + libgl1/libglib2.0 + descarga yolov8n.pt en el build.

PARTE 5 — Contenedor vision-face (Python)

FaceRecognition/ (carpeta nueva, aislada):

  • face_worker.py: carga la DB de known_faces/*.jpg (encoding → nombre), captura CAMERA_URL, cada DETECTION_INTERVAL_MS detecta caras y compara (RECOGNITION_TOLERANCE) → envía .faceRecognition por WS con reconexión. Hot-reload de la DB al detectar cambios en el directorio (añadir personas sin reiniciar). DEMO_MODE para pruebas.
  • requirements.txt: face_recognition, opencv-python-headless, websockets.
  • Dockerfile.face: python:3.12-slim + cmake/build-essential/libboost para compilar dlib (build lento, imagen ~1.5-2GB).
  • known_faces/ con un README de ejemplo (las fotos reales van al .gitignore).

PARTE 6 — Compose, gitignore, pruebas

docker-compose.yml — 2 servicios nuevos con profile vision + volumen de caras

  • docker.sock en el leader:
  vision-yolo:
    build: { context: ./Vision, dockerfile: Dockerfile.vision }
    profiles: ["vision"]
    environment: { LEADER_HOST: leader, CAMERA_URL: /dev/video0, CAMERA_ID: usb-cam-0,
                   DETECTION_INTERVAL_MS: 500, CONFIDENCE_THRESHOLD: 0.5, DEMO_MODE: "false" }
    devices: ["/dev/video0:/dev/video0"]
    restart: unless-stopped
    depends_on: [leader]

  vision-face:
    build: { context: ./FaceRecognition, dockerfile: Dockerfile.face }
    profiles: ["vision"]
    environment: { LEADER_HOST: leader, CAMERA_URL: /dev/video1, CAMERA_ID: usb-cam-1,
                   FACES_DB_PATH: /app/known_faces, RECOGNITION_TOLERANCE: 0.6,
                   DETECTION_INTERVAL_MS: 1000, DEMO_MODE: "false" }
    devices: ["/dev/video1:/dev/video1"]
    volumes: ["./FaceRecognition/known_faces:/app/known_faces:ro"]
    restart: unless-stopped
    depends_on: [leader]

Uso:

docker compose --profile vision up -d                 # leader + worker + vision-yolo + vision-face
DEMO_MODE=true docker compose --profile vision up -d  # prueba sin cámara

.gitignore: ignorar FaceRecognition/known_faces/*.jpg|*.png (privacidad).

Pruebas en la Mac (decisión 4): la cámara integrada y la USB aparecen como fuentes 0 y 1. Nota: en macOS con Docker Desktop el paso de /dev/video* al contenedor no es directo (la VM de Docker no expone webcams fácilmente). Caminos:

  • (a) Correr los workers Python nativos en la Mac (python3 yolo_worker.py con LEADER_HOST=<ip-del-host>), apuntando al leader en Docker en Linux.
  • (b) Mover todo a la Mac y correr los workers nativos con cv2.VideoCapture(0) (integrada) y cv2.VideoCapture(1) (USB).

CAMERA_URL abstrae la fuente (0/1 en Mac, /dev/videoN en Linux, rtsp:// en el futuro).


Resumen de cambios

Archivo / Carpeta Cambio Riesgo
Sources/Cardinal-System/Network/Envelope.swift +2 casos Mínimo (aditivo)
Sources/Cardinal-System/Network/VisionPayloads.swift nuevo Nulo
Sources/Cardinal-System/System/Node.swift +2 casos + 2 hooks Bajo
Sources/Run/ALO.swift loguea eventos Bajo
Sources/Cardinal-System/System/ContainerOrchestrator.swift nuevo (Docker HTTP API) Medio
Vision/ nuevo servicio Python Nulo (aislado)
FaceRecognition/ nuevo servicio Python + DB Nulo (aislado)
docker-compose.yml +2 servicios + sock en leader Bajo
.gitignore +ignorar fotos DB Nulo
Tests/ round-trip Codable Aditivo

Orden de implementación

  1. Envelope + payloads + tests Codable.
  2. Node.receive casos + hooks + log en ALO.swift.
  3. Vision/yolo_worker.py (DEMO primero) + Dockerfile.vision.
  4. FaceRecognition/face_worker.py + DB + Dockerfile.face.
  5. Compose + gitignore.
  6. ContainerOrchestrator + arranque por env en leader.
  7. Prueba end-to-end con DEMO_MODE=true (sin cámara): python → leader → log.
  8. Prueba real en la Mac (workers nativos, 2 cámaras).

Fuera de alcance (Fase 2)

  • LLM del leader decide acciones basadas en detecciones/caras (contexto en prompt
    • filtro de labels, ej. ALERT_LABELS=person,car).
  • Cerrar el puente .callTool (ejecutar acciones reales; hoy no ejecuta nada).
  • Orquestación dinámica (el LLM levanta/para cámaras por demanda).
  • CLI mostrando eventos en vivo.