# Documento de Definición Técnica — AgroVisión (Plataforma Completa) > **Propósito del documento:** Servir como única fuente de verdad técnica y funcional para desarrolladores, arquitectos y sistemas de inteligencia artificial. Define la arquitectura modular, flujos de datos, APIs, modelos de base de datos, lógica de negocio y configuraciones del sistema de la plataforma AgroVisión. > > **Stack confirmado:** UI en **Astro + Tailwind** (SPA estática), backend/gateway en **FastAPI** (sirve la UI en `/` y la API en `/api`), base de datos en **Supabase (PostgreSQL + PostGIS + Storage + Queues/PGMQ)**, agente conversacional con **Groq (Llama 3)** y teledetección con **Copernicus Sentinel-2 / NASA POWER / Open-Meteo**. Despliegue gratuito: **Hugging Face Spaces (Docker)**; alternativa Render. > > ⚠️ **Actualización (Fases 8 y 10):** este documento se redactó con la UI original en **Shiny for Python**. Desde la **Fase 8** la UI es **Astro + Tailwind** servida por el gateway, y en la **Fase 10 Shiny se eliminó por completo** (sin `/shiny`) y el despliegue pasó a **Hugging Face Spaces** (shinyapps.io quedó descartado: solo hospeda Shiny, no FastAPI). Donde el texto diga "Shiny / reactive.value / WebSocket / ShinyApps.io", léase: **SPA Astro en el navegador / estado en memoria del navegador / HF Spaces**. La arquitectura vigente está en [`architecture_agrovision.md`](../architect/architecture_agrovision.md) (incl. §8 Seguridad). > > **Licencia y modelo:** AgroVisión es **open-source bajo AGPL-3.0** (lo que permite usar **YOLO26**). El modelo de conteo es **agnóstico**: la app consume el artefacto `agrovision-plantcount` (multi-candidato YOLO26/RF-DETR) por **contrato**, vía un adaptador de inferencia. El **módulo de conteo arranca en _standby_** (flag `COUNTING_ENABLED=false`) hasta que el **repo del modelo** (proyecto separado) publique el artefacto. > > **Documentos relacionados:** > - El modelo de conteo se desarrolla en un **repo separado** (produce los pesos predeterminados). > - Investigación base: [`Plan Detallado Data Science Agrícola.md`](../investigation/Plan%20Detallado%20Data%20Science%20Agr%C3%ADcola.md) > - Mockup interactivo: [`agrovisi_n_spa_prototype.html`](../investigation/agrovisi_n_spa_prototype.html) --- ## 0. Resumen Ejecutivo AgroVisión es una plataforma de **monitoreo agronómico de precisión** que unifica tres pilares tecnológicos en una sola interfaz: (1) **teledetección satelital** con índices vegetativos sobre Sentinel-2, (2) **visión computacional** para el conteo automatizado de plantas a partir de ortomosaicos RGB capturados por drones, y (3) un **agente conversacional (RAG)** que contextualiza el historial de las parcelas con datos climáticos y de campo. El sistema se diseña sobre una arquitectura **de código abierto, sin servidores de costo fijo y desplegable íntegramente en capa gratuita**, bajo un paradigma estricto de **"Trae tu Propia Llave" (Bring Your Own Keys, BYOK)** con **cero persistencia de credenciales**. * **Propósito:** Centralizar fuentes geoespaciales (satélite, dron, clima) para optimizar decisiones agronómicas en campo, sin costos de infraestructura y sin exponer claves propietarias. * **Usuarios Objetivos:** Agrónomos, productores y consultores agrícolas. * **Casos de Uso Core:** 1. El usuario **delimita una parcela** sobre un mapa interactivo (polígono EPSG:4326) y consulta la **serie temporal de NDVI** de Sentinel-2 cruzada con clima de NASA POWER / Open-Meteo. 2. El usuario **sube un ortomosaico RGB del dron**; el sistema ejecuta el **modelo de conteo predeterminado** (agnóstico: YOLO26/RF-DETR, entrenado en un repo aparte) y devuelve **conteo de plantas, densidad por hectárea, malezas y fallas de siembra**. 3. El usuario **conversa con el agente AI**, que invoca funciones tipadas (function calling) sobre la base de datos y las APIs externas para producir un diagnóstico agronómico argumentado. 4. El usuario **introduce sus credenciales** en la última pestaña; estas viven **solo en memoria de sesión**, no se guardan ni almacenan, y **se borran al refrescar la página**. > **Decisión de diseño clave (efimeralidad):** A diferencia del mockup original (que usaba `localStorage`), esta plataforma mantiene credenciales y datos del usuario exclusivamente en el **estado reactivo en memoria de la sesión de Shiny**. Cada conexión WebSocket es una sesión aislada; al refrescar/cerrar la pestaña, la sesión se destruye y **todo dato se pierde**. Esto elimina cualquier vector de persistencia accidental de secretos. --- ## 1. Arquitectura de Componentes (Modular y Desacoplada) La plataforma es un **gateway único**: el backend **FastAPI** sirve la **UI Astro estática** en `/` y la **API** en `/api` (mismo origen). *(El diseño original separaba UI Shiny y backend; desde la Fase 10 es un solo servicio — ver banner arriba.)* El cómputo pesado (inferencia de visión, descargas satelitales) queda aislado en `services/`. ```mermaid flowchart TD classDef frontend fill:#BFDBFE,stroke:#2563EB,color:#1E3A8A classDef gateway fill:#FDE047,stroke:#EAB308,color:#713F12 classDef service fill:#BBF7D0,stroke:#16A34A,color:#14532D classDef broker fill:#FED7AA,stroke:#EA580C,color:#7C2D12 classDef db fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95 classDef ext fill:#FECACA,stroke:#DC2626,color:#7F1D1D UI["💻 Capa Cliente (UI)
Astro + Tailwind (SPA estática)
6 vistas · Leaflet-draw · Chart.js
Estado efímero en memoria del navegador
Servida por el gateway en /"]:::frontend GW["🔀 Gateway / BFF
FastAPI + Starlette (ASGI)
sirve UI en / + API en /api · rate limiting
proxy efímero de llaves · Host: HF Spaces"]:::gateway WK["⚙️ Worker Asíncrono
Consumidor PGMQ (vt=120)
Inferencia RF-DETR-Nano (CPU)
Mismo servicio/proceso de fondo en Render"]:::service Q["✉️ Cola de Mensajes
Supabase Queues (PGMQ)
count_tasks"]:::broker DB[("🗄️ Base de Datos
Supabase PostgreSQL + PostGIS
fields · ndvi_timeseries · plant_counts · chat_messages")]:::db ST[("🗄️ Almacenamiento
Supabase Storage (bucket privado)
Signed URLs efímeras")]:::db MODEL["🧠 Modelo Predeterminado
RF-DETR-Nano (.pt / ONNX)
Empaquetado en la imagen del backend
Producido por el repo del modelo"]:::service EXT["🌐 APIs Externas (BYOK)
Copernicus CDSE (Sentinel-2)
NASA POWER · Open-Meteo
Groq (Llama 3)"]:::ext UI <-->|"HTTPS + headers X-User-*-Key
(WebSocket interno de Shiny)"| GW GW <-->|REST| DB GW -->|"Encola ortomosaico"| Q Q <--> WK WK -->|"Carga pesos"| MODEL WK <--> ST WK <--> DB GW <-->|"Proxy efímero de credenciales"| EXT ``` ### 1.1 Glosario de Módulos e Infraestructura | Componente | Descripción de Responsabilidad | Tecnologías | Estrategia de Resiliencia / Despliegue | | :--- | :--- | :--- | :--- | | **Capa Cliente (UI)** | Interfaz interactiva de 6 módulos. Gestiona estado local **efímero** (credenciales, parcela activa, resultados) y orquesta llamadas al gateway. | **Astro + Tailwind** (SPA estática), Leaflet-draw, Chart.js (CDN) | Compilada a estático y **servida por el gateway en `/`**. Estado en memoria del navegador; refrescar = reset total. | | **Gateway / BFF** | Punto de entrada único. Sirve la UI Astro, valida CORS, aplica rate limiting, recibe llaves por cabecera, orquesta teledetección, encola inferencia y proxy del LLM. | FastAPI, Starlette, `httpx` | 1 contenedor en **Hugging Face Spaces** (Docker). Sin secretos persistidos. | | **Worker Asíncrono** | Consume la cola PGMQ, ejecuta inferencia RF-DETR-Nano sobre ortomosaicos, persiste conteos y genera Signed URLs. | FastAPI background task / proceso, onnxruntime, OpenCV, `pgmq-sqlalchemy` (asyncpg) | Reintento automático por *visibility timeout* (vt=120). Tolerante a reinicios del contenedor gratuito. | | **Base de Datos + GIS** | Almacén relacional/espacial del dominio (parcelas, series NDVI, conteos, chat). | Supabase PostgreSQL + **PostGIS** | **Supabase Free** (500 MB DB). Índices GIST/GIN. Modelo BYOK (proyecto del usuario). Keep-alive contra pausa a 7 días. | | **Almacenamiento de Objetos** | Custodia privada de ortomosaicos e imágenes de resultado. | Supabase Storage | Bucket **privado**; visualización solo vía **Signed URLs** (`expires_in=600`). | | **Cola de Mensajes** | Desacopla la subida de imágenes pesadas de la inferencia para evitar *timeouts*. | Supabase Queues (**PGMQ** / pgmq) | Transaccional ACID embebido en Postgres; sin broker externo (no Redis). | | **Modelo de Visión** | Pesos preentrenados de conteo; **solo inferencia**, nunca entrenamiento. Se carga vía **adaptador de inferencia** (onnxruntime o `ultralytics` según la arquitectura ganadora) — **agnóstico** al modelo. Arranca en **standby**. | `agrovision-plantcount` (ONNX; multi-candidato YOLO26/RF-DETR) | Artefacto **predeterminado** de Hugging Face Hub (build); **AGPL-3.0 aceptada** (app open-source). | --- ## 2. Flujo de Datos e Integración ### 2.1 Orígenes y Destinos de Datos * **Entradas del Sistema:** * *Inputs de Usuario:* Polígonos de parcela (mapa interactivo → GeoJSON EPSG:4326), **carga de ortomosaico RGB del dron** (`ui.input_file`), consultas al chat, **credenciales BYOK** (Groq, Copernicus, Supabase URL + anon key). * *Sincronizaciones Externas:* Sentinel-2 L2A (Copernicus CDSE vía STAC), NASA POWER (REST por coordenadas), Open-Meteo (REST público sin llave), Groq (inferencia LLM). * **Salidas del Sistema:** * *Visualizaciones:* KPIs, series NDVI vs. clima (Plotly), ortomosaico con *bounding boxes* (Signed URL efímera), respuestas del agente. * *Persistencia opcional (Supabase del usuario):* `fields`, `ndvi_timeseries`, `plant_counts`, `chat_messages`. ### 2.2 Grafo de Dependencias Reactivas y Propagación Shiny es un framework **reactivo**: los `input.*` disparan `@reactive.calc` y `@render.*` automáticamente. El flujo de una operación de conteo es: ```mermaid flowchart LR A["input.file_upload
(ortomosaico RGB)"] --> B["@reactive.calc
validación + lectura datapath"] B --> C["POST /api/count
(httpx, headers BYOK)"] C --> D["FastAPI: subir a Storage
+ pgmq.send(count_tasks)"] D --> E["Worker: pgmq.read(vt=120)
inferencia RF-DETR-Nano"] E --> F["plant_counts + Signed URL"] F --> G["Polling GET /api/count/{id}"] G --> H["@render.ui / @render.plot
conteo, densidad, overlay"] ``` > **Nota de efimeralidad en el flujo:** las credenciales viajan en cada petición como cabeceras `X-User-Groq-Key`, `X-User-Copernicus-Secret`, etc. El gateway las usa **en la ejecución** y las descarta; nunca se escriben a disco, log ni base de datos. En la UI residen en un `reactive.value` que muere con la sesión. --- ## 3. Modelo de Datos (Bases de Datos) > El esquema es **BYOK**: el usuario provee su propio proyecto Supabase (URL + anon key, inyectados por sesión). El repo entrega las **migraciones** para crearlo en una cuenta gratuita. La persistencia es **opcional**: la plataforma funciona en modo efímero (sin BD) y solo escribe cuando el usuario configura Supabase. ### 3.1 Diagrama Entidad-Relación (ERD) ```mermaid erDiagram USUARIO ||--o{ FIELDS : "posee" FIELDS ||--o{ NDVI_TIMESERIES : "registra" FIELDS ||--o{ PLANT_COUNTS : "asocia" USUARIO ||--o{ PLANT_COUNTS : "ejecuta" USUARIO ||--o{ CHAT_MESSAGES : "conversa" USUARIO { uuid id PK string email "auth.users (Supabase Auth)" timestamp created_at } FIELDS { uuid id PK uuid user_id FK string name geometry geom "Polygon, 4326 (GIST)" timestamp created_at } NDVI_TIMESERIES { int id PK uuid field_id FK date date float mean_ndvi float min_ndvi float max_ndvi float cloud_cover string source "sentinel2" } PLANT_COUNTS { int id PK uuid user_id FK uuid field_id FK string image_url "Storage privado" int count jsonb result_json "boxes + clases (GIN)" timestamp processed_at } CHAT_MESSAGES { int id PK uuid user_id FK string session_id string role "user|assistant" string content timestamp created_at } ``` ### 3.2 DDL de Referencia (PostGIS) ```sql -- Extensión espacial create extension if not exists postgis; -- 1. Parcelas / Lotes agrícolas create table fields ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users on delete cascade, name text not null, geom geometry(Polygon, 4326) not null, -- WGS84 created_at timestamp default now() ); create index fields_geom_gist_idx on fields using gist (geom); -- 2. Serie temporal de índices vegetativos (Sentinel-2) create table ndvi_timeseries ( id serial primary key, field_id uuid references fields on delete cascade, date date not null, mean_ndvi float not null, min_ndvi float, max_ndvi float, cloud_cover float, source text default 'sentinel2', constraint unique_field_date unique (field_id, date) ); create index ndvi_field_date_idx on ndvi_timeseries (field_id, date); -- 3. Conteos por dron create table plant_counts ( id serial primary key, user_id uuid references auth.users on delete cascade, field_id uuid references fields on delete set null, image_url text not null, -- ruta en Storage privado count integer not null, result_json jsonb, -- {"boxes": [[x1,y1,x2,y2,conf,cls]], "classes": {...}} processed_at timestamp default now() ); create index plant_counts_json_gin_idx on plant_counts using gin (result_json); -- 4. Memoria conversacional create table chat_messages ( id serial primary key, user_id uuid references auth.users on delete cascade, session_id text not null, role text not null check (role in ('user', 'assistant')), content text not null, created_at timestamp default now() ); create index chat_session_history_idx on chat_messages (session_id, created_at); ``` ### 3.3 Políticas de Integridad y Seguridad * **Row Level Security (RLS):** cada tabla se filtra por `auth.uid() = user_id` para aislar productores en el mismo proyecto Supabase. * **Storage privado + Signed URLs:** el bucket `drone-images` es privado; toda visualización usa enlaces firmados con expiración de 10 minutos. * **Canonicalización:** `fields.name` se normaliza a *Title Case*; `source` en minúsculas. * **Geometría:** `geom` siempre en SRID 4326; validación `ST_IsValid` antes de insertar. --- ## 4. Contratos de API (Endpoints Críticos) El gateway FastAPI es el único servicio backend expuesto. Todas las llaves del usuario viajan en cabeceras `X-User-*` y se descartan tras la ejecución. ### 4.1 Endpoints de la API | Método | Path | Descripción | Entrada | Respuesta Exitosa | | :--- | :--- | :--- | :--- | :--- | | `GET` | `/api/status` | Healthcheck y versión. | — | `{"status":"ok","version":"1.0.0"}` | | `POST` | `/api/ndvi` | Calcula estadística zonal NDVI de Sentinel-2 para un polígono y rango de fechas. | `{"geojson":{...},"start":"YYYY-MM-DD","end":"YYYY-MM-DD"}` + `X-User-Copernicus-Secret` | `{"series":[{"date":"...","mean_ndvi":0.72,"cloud_cover":4.1}]}` | | `POST` | `/api/count` | Encola un ortomosaico para conteo. Retorna id de tarea. | `multipart/form-data` (imagen) + `field_id?` | `{"task_id":"uuid","status":"queued"}` | | `GET` | `/api/count/{task_id}` | Sondea el estado/resultado de la inferencia. | — | `{"status":"done","count":124,"density":72400,"weeds":12,"failures":1.2,"overlay_url":""}` | | `POST` | `/api/chat` | Conversa con el agente RAG (streaming SSE). | `{"session_id":"...","message":"..."}` + `X-User-Groq-Key` | `text/event-stream` (tokens + logs de herramientas) | | `POST` | `/api/weather` | Historial agroclimático por coordenadas (NASA POWER / Open-Meteo). | `{"lat":-34.6,"lon":-58.4,"start":"...","end":"..."}` | `{"temp":[...],"precip":[...],"radiation":[...]}` | > **CORS:** `CORSMiddleware` con `allow_origins=[, "http://localhost:8000"]`, `allow_credentials=True`, `allow_methods=["*"]`, `allow_headers=["*"]`. El comodín en headers es necesario para inyectar `X-User-Groq-Key`, `X-User-Copernicus-Secret`, etc. ### 4.2 Esquema de Mensajes Asíncronos (PGMQ) * **Cola:** `count_tasks` * **Productor:** `POST /api/count` → sube a Storage + `pgmq.send`. * **Consumidor:** worker con `pgmq.read("count_tasks", vt=120)`; al éxito `pgmq.archive`. ```json { "task_id": "uuid-v4", "event_type": "drone.count.requested", "timestamp": "2026-06-03T12:00:00Z", "actor_id": "user_uuid", "data": { "image_path": "raw/user_99/field_a.png", "field_id": "field_uuid", "model": "agrovision-plantcount" } } ``` Si el contenedor gratuito colapsa o excede el límite, el mensaje vuelve a ser visible al expirar `vt=120` y se reintenta automáticamente (tolerancia a fallos). --- ## 5. Lógica de Negocio, Motores y Fórmulas ### 5.1 Índice de Vegetación de Diferencia Normalizada (NDVI) A partir de las bandas de reflectancia Sentinel-2 L2A (B08 NIR ~842 nm, B04 Rojo ~665 nm): $$NDVI = \frac{\rho_{NIR} - \rho_{Red}}{\rho_{NIR} + \rho_{Red}}$$ El gateway descarga los rásters intersecados por el GeoJSON con `pystac-client` (Copernicus CDSE) y calcula estadística zonal (media, min, max, % nubes) por fecha. ### 5.2 Distancia de Muestreo Terrestre (GSD) Relaciona la resolución espacial con la geometría de vuelo del dron: $$GSD = \frac{S_w \times H}{f \times I_w}$$ Donde $S_w$ = ancho del sensor (mm), $H$ = altura de vuelo (m), $f$ = distancia focal (mm), $I_w$ = ancho de la imagen (px). Un vuelo a baja altitud (~15–30 m) produce un GSD nominal ≈ 1.2 cm/px, suficiente para segmentar plantas y brotes tempranos. El GSD se usa para convertir conteos a **densidad real**. ### 5.3 Densidad de Siembra $$Densidad_{pl/Ha} = \frac{Conteo_{detectado}}{Área_{Ha}}, \quad Área_{Ha} = \frac{ST\_Area(geom::geography)}{10000}$$ PostGIS calcula el área de la parcela en m²; el worker divide el conteo más reciente entre el área para obtener plantas por hectárea. ### 5.4 Herramientas del Agente (Function Calling) El agente (Llama 3 70B vía Groq) no especula: traduce la intención del usuario en llamadas a funciones tipadas. | Herramienta | Firma | Acción | | :--- | :--- | :--- | | `get_vegetation_index_trend` | `(field_name, start_date, end_date) -> str` | Une `fields` ⋈ `ndvi_timeseries`; calcula ΔNDVI y diagnóstico. | | `get_weather_context` | `(lat, lon, start_date, end_date) -> dict` | Llama NASA POWER: radiación, humedad, temperaturas extremas. | | `get_field_planting_density` | `(field_name) -> str` | `ST_Area` de la parcela + último conteo → densidad pl/Ha. | **Ejemplo de plan de 3 pasos** ante *"¿Por qué bajó el NDVI de mi Lote A en agosto?"*: (1) `get_vegetation_index_trend` confirma la caída; (2) `get_weather_context` detecta déficit hídrico/exceso térmico; (3) sintetiza una respuesta agronómica que asocia ambos. ### 5.5 Reglas de Validación 1. **Rango razonable NDVI:** valores fuera de $[-1, 1]$ disparan *warning*, no error duro. 2. **Tamaño de imagen:** `ui.input_file` limita el ortomosaico (p. ej. ≤ 50 MB) para no saturar Render/Storage. 3. **Cobertura de nubes:** escenas con `cloud_cover > 60%` se marcan como baja confianza. 4. **Credenciales ausentes:** si falta la llave requerida por un módulo, la UI muestra un *toast* que redirige a la pestaña de Credenciales. --- ## 6. Interfaz de Usuario (UI/UX) y Estados Reactivos La UI es una **SPA de 5 módulos** construida con `ui.page_navbar` (un `nav_panel` por módulo), replicando el [mockup](../investigation/agrovisi_n_spa_prototype.html). Paleta: *Warm Neutrals & Deep Canopy* (verde #15803D sobre stone #FAF9F6). ### 6.1 Módulos (pestañas) 1. **Resumen de Campo** — KPIs (NDVI promedio, densidad, temperatura de suelo, alertas del agente) + gráfica de evolución + diagnóstico del asistente. Selector de parcela activa en el topbar. 2. **Teledetección (GIS)** — Mapa interactivo (`ipyleaflet`) para dibujar polígonos EPSG:4326 + serie temporal NDVI vs. precipitación (Plotly de doble eje). Explica la fórmula NDVI. 3. **Detección Dron (Conteo)** — **arranca en _standby_**: mientras `COUNTING_ENABLED=false`, muestra un aviso "Módulo en preparación — disponible cuando el modelo de conteo esté publicado". Al activarse: **carga de ortomosaico** (`ui.input_file`), botón "Iniciar Conteo", visualizador con *bounding boxes* y panel de métricas (total plantas/arbustos, densidad, malezas, fallas). El modelo es **predeterminado** y **agnóstico** (definido en el repo del modelo; multi-candidato YOLO26/RF-DETR; cultivo inicial: arándano). 4. **Asistente Agentivo (RAG)** — Consola de chat con sugerencias precargadas, historial y *logs* de ejecución del agente ("Pensando…", "Llamando herramienta…"). 5. **Credenciales & APIs** — Formularios para Groq, Copernicus/Sentinel Hub, Supabase URL + anon key, con barras de cuota informativas. ### 6.2 Layout (Wireframe ASCII) ```text ┌───────────────────────────────────────────────────────────────────────────┐ │ 🌱 AgroVisión [Parcela Activa: Lote Norte ▾] [👤 Agrónomo] │ ├──────────────┬────────────────────────────────────────────────────────────┤ │ NAV (navset)│ PANEL ACTIVO (nav_panel) │ │ ─────────── │ ───────────────────────────────────────────────────────── │ │ ▸ Resumen │ ┌─ KPI ─┐ ┌─ KPI ─┐ ┌─ KPI ─┐ ┌─ KPI ─┐ │ │ ▸ Teledet. │ │ NDVI │ │ Dens. │ │ Temp. │ │Alertas│ │ │ ▸ Dron YOLO │ └───────┘ └───────┘ └───────┘ └───────┘ │ │ ▸ Asistente │ [ Mapa GIS / Visualizador / Chat según módulo ] │ │ ▸ Credenciales│ [ Gráficas Plotly · overlay de detección ] │ │ │ │ │ ● Supabase │ │ │ (sesión) │ │ └──────────────┴────────────────────────────────────────────────────────────┘ ``` ### 6.3 Comportamientos Reactivos y Efimeralidad * **Estado por sesión:** `reactive.value` guarda credenciales, parcela activa y resultados **en memoria**. No hay `localStorage` ni cookies persistentes. * **Reset al refrescar:** recargar la pestaña abre una nueva sesión WebSocket → todo el estado se reinicia y las llaves desaparecen. La pestaña de Credenciales lo advierte de forma destacada: > ⚠️ *Tus credenciales se usan solo durante esta sesión. **No se guardan ni almacenan.** Si actualizas o cierras la página, todos los datos y llaves se borrarán y deberás reingresarlos.* * **Polling no bloqueante:** tras `POST /api/count`, la UI sondea `GET /api/count/{id}` con `reactive.invalidate_later` hasta `status="done"`, mostrando un overlay de progreso. * **Debounce:** inputs de fechas/coordenadas aplican retardo antes de llamar al gateway. --- ## 7. Configuración de Entornos y Políticas de Despliegue ### 7.1 Variables de Entorno (`.env.example`) ```bash # --- General --- APP_ENV=development # development | production LOG_LEVEL=info # --- Gateway (FastAPI sirve UI Astro en / + API en /api) --- API_PORT=8000 ALLOWED_ORIGINS=http://localhost:4321 # solo para el dev server de Astro (cross-origin); # en HF Spaces UI y API comparten origen RATE_LIMIT_PER_MIN=120 # rate limiting de /api (anti-abuso); 0 desactiva # --- Despliegue (Hugging Face Spaces) --- HF_TOKEN= # token write HF_SPACE_ID= # / # --- Modelo predeterminado (descargado de Hugging Face Hub en el build) --- MODEL_PATH=/app/models/agrovision-plantcount-v2.0.0.onnx MODEL_VERSION=2.0.0 HF_MODEL_REPO=/agrovision-plantcount # repo de modelos en Hugging Face Hub COUNTING_ENABLED=false # standby: el módulo de conteo se activa cuando el modelo esté publicado # --- NOTA DE SEGURIDAD --- # Las credenciales de usuario (Groq, Copernicus, Supabase) NO van aquí en producción. # Se inyectan por sesión desde la UI vía cabeceras X-User-*-Key y se descartan. # Estas variables solo sirven para desarrollo local con datos de prueba. DEV_GROQ_API_KEY= DEV_COPERNICUS_CLIENT_ID= DEV_COPERNICUS_CLIENT_SECRET= DEV_SUPABASE_URL= DEV_SUPABASE_ANON_KEY= ``` ### 7.2 Dockerización y `docker-compose` (Local) Hay **un solo `Dockerfile`** (multi-stage: Node compila Astro → Python/uv corre el gateway que sirve UI + API). El `docker-compose.yml` actual levanta solo ese servicio: ```yaml # docker-compose.yml (real) services: api: build: { context: ., dockerfile: Dockerfile } ports: ["8000:8000"] # sirve UI en / y API en /api environment: [COUNTING_ENABLED=false] volumes: ["./models:/app/models:ro", "./sample_data:/app/sample_data:ro"] ``` > Ya **no hay** servicio `ui` aparte (la UI Astro la sirve el gateway). El `Dockerfile` de la **raíz** es el equivalente endurecido para Hugging Face Spaces (usuario 1000, `app_port` 8000). Las imágenes de dron de ejemplo viven en `sample_data/` para probar el conteo (mock). ### 7.3 Despliegue en Producción (Capa Gratuita) | Componente | Plataforma Gratuita | Límites Relevantes | Caveat | | :--- | :--- | :--- | :--- | | **Gateway (UI Astro + API)** | [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-sdks-docker) (SDK Docker) | CPU básica **2 vCPU · 16 GB RAM** · **duerme a 48 h** | 1 contenedor; HF construye el `Dockerfile` al hacer `git push`. Cold start 30–60 s. | | **Alternativa: gateway** | [Render Free](https://render.com/docs/free) | 512 MB RAM · 0.1 CPU · **duerme a 15 min** · 750 h/mes | mismo `Dockerfile` (raíz) + `render.yaml`. | | **BD / Storage / Colas** | [Supabase Free](https://supabase.com/pricing) | 500 MB DB · 1 GB Storage · **pausa a 7 días** sin actividad | Keep-alive (cron ligero) para evitar la pausa. | | **LLM** | Groq Free | Rate limit por minuto/día | BYOK: la llave la pone el usuario. | | **Satélite / Clima** | Copernicus CDSE · NASA POWER · Open-Meteo | Cuotas públicas | Open-Meteo no requiere llave. | **Pipeline de despliegue (resumen):** ```powershell # 1. Gateway (UI Astro + API) -> Hugging Face Spaces (SDK Docker) # HF construye el Dockerfile de la raíz; un git push basta. .\scripts\deploy_hf.ps1 -Force # primer deploy # 2. BD -> aplicar migraciones PostGIS al proyecto Supabase del usuario uv run python -m backend.db.migrate ``` > **Un solo origen:** UI y API se sirven desde el mismo gateway, así que **no hay CORS entre front y back** ni `API_BASE_URL` apuntando a otro host. La "Regla de Oro" (rutas relativas, hash-routing, CSS inline) se conserva como salvaguarda para sub-paths. Detalle del despliegue en [`ejecucion.md`](../ejecucion.md) §5. ### 7.4 Gestión del Modelo Predeterminado * AgroVisión **no entrena**: consume pesos producidos por el **repo del modelo de conteo** (proyecto separado). * El artefacto `agrovision-plantcount-v2.0.0.onnx` (RF-DETR-Nano exportado a ONNX) se **publica en Hugging Face Hub** y se descarga con `hf_hub_download` durante el build de la imagen del backend, copiándose a `/app/models/` (`MODEL_VERSION` fija la trazabilidad). El nombre es de **marca, desacoplado de la arquitectura** interna. * **Contrato de inferencia** que el worker espera: entrada = tile RGB; salida = `{"boxes":[[x1,y1,x2,y2,conf,cls]], "count":int}` compatible con `plant_counts.result_json`. --- ## Apéndice A — Seguridad de Credenciales (Resumen) 1. **Cero persistencia:** ninguna llave se guarda en disco, base de datos, log ni `localStorage`. 2. **Solo memoria de sesión:** `reactive.value` en Shiny; muere al refrescar/cerrar. 3. **Transmisión por cabecera:** `X-User-*-Key` sobre HTTPS; el gateway es un proxy efímero. 4. **Aviso explícito al usuario:** la pestaña de Credenciales declara que nada se guarda y que refrescar borra todo. 5. **Aislamiento por sesión:** cada WebSocket de Shiny es independiente; no hay fuga cruzada entre usuarios concurrentes. ## Apéndice B — Fuentes Basado en [`Plan Detallado Data Science Agrícola.md`](../investigation/Plan%20Detallado%20Data%20Science%20Agr%C3%ADcola.md) (41 fuentes citadas, incl. Copernicus CDSE, NASA POWER, Supabase Queues/PGMQ, RF-DETR (Apache 2.0), SAM) y verificación de límites de capa gratuita ([ShinyApps.io](https://support.posit.co/hc/en-us/articles/217592947-What-are-the-limits-of-the-shinyapps-io-Free-plan), [Render](https://render.com/docs/free), [Supabase](https://supabase.com/pricing)).