Reescalá, restaurá, separá voces, transcribí, doblá, generá imágenes y video, y armá piezas 3D — todo en tu propia GPU. Sin nube, sin cuentas, sin subir un solo archivo. Y sin CUDA: anda en AMD, Intel y NVIDIA por igual.
Todo corre local. Cada función se baja aparte, cuando la usás por primera vez, así que instalás la app y no 40 GB de modelos.
| Reescalar | Subí la resolución de un video viejo o comprimido. Perfiles listos para anime y para imagen real. |
| Más fluido | Duplicá, triplicá o cuadruplicá los cuadros por segundo. Detecta los cortes de escena para no inventar cuadros fantasma entre dos planos distintos. |
| Subtítulos | Sacá la transcripción con tiempos, o devolvé el video con los subtítulos adentro o quemados en la imagen. En otro idioma si querés. |
| Doblaje | El video hablado en otro idioma, con el audio original conservado como segunda pista. |
| Cámaras de seguridad | Aclará lo que exportó tu DVR (Hikvision/HiLook, Dahua, MP4) con filtros clásicos, sin IA y reproducibles, y llevate un paquete de entrega con el original intacto, un informe y los hashes. No es una herramienta forense certificada. |
| Reescalar | Fotos y anime, de a una o por lote, 2× a 4×. |
| Restaurar fotos | Rayas, manchas, desvaído, ruido, trama de diario y bloques de JPEG en una foto vieja; caras y color si los pedís. Lo que la IA inventa, lo dice. Apagado en esta versión: los modelos todavía no se pueden bajar (detalle). |
| Borrar cosas | Pintá lo que sobra —o tocá el objeto para seleccionarlo solo— y desaparece. |
| Generar | Texto a imagen e imagen a imagen con Stable Diffusion, SDXL o Flux. |
| Detalle inventado | Más textura de la que había, dibujada por IA. No es una copia más fiel: para eso está el reescalado. |
| Karaoke y pistas | Separá la voz de la instrumental para cantar encima, o quedate solo con la voz. |
| Karaoke completo | El video listo: sin voz, con la letra encendiéndose palabra por palabra. |
| Cuatro pistas | Voz, batería, bajo y resto, cada una en su archivo. |
| Probar antes | Corré dos o tres separadores sobre 30 s de tu tema y elegí con el oído. |
| Máxima calidad | Combiná varios separadores en uno: se refuerza lo que coinciden. |
| Limpiar | Ruido, eco y reverberación, encadenables en una sola pasada. |
| Restaurar | Recuperá una grabación vieja o de baja calidad; hay un modo que directamente reinventa el agudo que se perdió. |
| Nivelar | Volumen al estándar de entrega de Spotify, YouTube o emisión (EBU R128). |
| Convertir | FLAC a MP3 y demás, sin tocar nada más del archivo. |
| Transcribir | Audio o video a texto, con tiempos. |
| Hablar | Escribís y la app lo dice, en varias voces. |
| Cambiar de voz | Le das una grabación y una muestra, y devuelve lo mismo con esa otra voz. |
| ¿Se imprime? | Soltá un STL y te dice si sale bien en tu impresora y qué arreglarle. Sin instalar nada. |
| Piezas con medidas | Tubos, tacos, placas con agujeros, escuadras. Escribís las cotas y sale la pieza exacta, ya verificada. |
| Generar formas | Describís algo —o le das una foto— y sale una malla. Para formas, no para piezas que tienen que encajar. |
| Reparar | Cierra los agujeros de una malla rota, y vuelve a medir en vez de decirte que quedó bien. |
- Generar video desde un texto o una imagen, local.
- Descargar de internet lo que vas a procesar, sin salir de la app — y encadenar: pegás un link y te llegan las pistas separadas. YouTube incluido: el token que ahora exige lo acuña ceca, sin cuentas ni cookies de nadie.
- Varios archivos de una: elegís un montón y quedan encolados, sin repetir el proceso uno por uno.
- Tiempo real: reescalá en vivo lo que estés mirando o jugando, en una ventana superpuesta.
- API REST y servidor MCP: encolá trabajos desde otro programa, o dejá que un agente de IA use la app como herramienta.
- Varios usuarios con permisos y cuotas, si la compartís en tu red.
¿Querés los números, los límites medidos y el porqué de cada decisión? Está todo en docs/FEATURES.md.
No hace falta tocar Python ni la consola. Dos opciones, misma app:
- Descargá
upflow-setup-v<version>.exedel último release. - Ejecutalo — no pide admin, se instala en tu carpeta de usuario (
%LOCALAPPDATA%\Upflow) e incluye su propio Python embebido, así que no necesitás tener Python instalado. - Al terminar, tildá "iniciar Upflow" (o abrilo después desde el acceso directo del escritorio/menú inicio).
- Esperá la primera descarga (~3-4 GB: motor de upscaling + FFmpeg + RIFE + dependencias de Python; puede tardar varios minutos según tu conexión) — cuando el servidor está listo, el navegador se abre solo en
http://127.0.0.1:8090. Las siguientes veces arranca al instante.
Desinstalar preserva por defecto tus archivos y modelos (runtime\); hay un checkbox opcional durante la instalación para que el desinstalador también los borre. Ver installer/README.md para el detalle del instalador.
- Descargá el
.zipdel último release (por ejemploupflow-v0.1.0.zip). - Extraelo en cualquier carpeta.
- Doble click en
Upflow.bat. - Esperá la primera descarga de binarios (~1 GB: motor de upscaling + FFmpeg + RIFE; algunos minutos según tu conexión) — cuando el servidor está listo, el navegador se abre solo en
http://127.0.0.1:8090.
Requiere Python 3.11+ instalado y en el PATH. Si tenés winget, con esto alcanza: winget install Python.Python.3.12
Requisitos (ambas opciones):
- Windows 10 u 11 de 64 bits.
- Cualquier GPU con Vulkan o DirectX 12 — AMD, Intel o NVIDIA. Varias funciones corren también en el procesador, más lento.
¿Tenés una NVIDIA RTX? Anda exactamente igual: Upflow usa Vulkan y DirectML, así que no necesita CUDA ni drivers especiales. Si querés, después se le puede sumar TensorRT-RTX.
¿Preferís correrlo desde el código fuente, o contribuir al proyecto? Seguí con la sección de abajo.
Empezó siendo un reescalador para GPUs AMD, porque casi todo lo bueno era CUDA-only, de código cerrado, o una pila de flags de consola. Creció hasta ser lo que usás cuando tenés un archivo y querés que quede mejor: video, foto, audio, voz, texto o una pieza para imprimir.
Tres cosas no cambiaron desde el primer día:
- Nada sale de tu máquina. No hay cuenta, no hay nube, no hay subida. Tus archivos son tuyos.
- Tu placa alcanza. Corre por Vulkan y DirectML, así que anda igual en AMD, Intel y NVIDIA — y en el procesador cuando hace falta. CUDA es opcional, nunca un requisito.
- Nada se decide en silencio. Si algo salió por un camino más lento, si hubo que cambiar el formato o si una mejora falló, el trabajo te lo dice al terminar, con el detalle a la vista.
Por dentro es una SPA en React sobre una API REST en FastAPI, con los motores detrás de interfaces: cambiar el modelo que hace el trabajo es una entrada en una tabla, no una cirugía.
- Windows con una GPU compatible con Vulkan o DirectX 12 (AMD, Intel o NVIDIA).
- Python 3.11+ en el
PATH. - PowerShell (para correr los scripts de
scripts/). - Node.js 20+ en el
PATH— solo si corrés desde el código fuente (para compilar la SPA defrontend/). El.zipde release ya traefrontend/dist/compilado, así que los usuarios finales no lo necesitan.
git clone https://github.com/santiquiroz/upflow.git
cd upflow
# 1. Entorno Python (crea .venv e instala el paquete en modo editable)
powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1
# 2. Motor de upscaling: Real-ESRGAN NCNN Vulkan (obligatorio)
powershell -ExecutionPolicy Bypass -File .\scripts\download-realesrgan.ps1
# 3. FFmpeg (obligatorio solo si vas a usar upscaling de video)
powershell -ExecutionPolicy Bypass -File .\scripts\download-ffmpeg.ps1
# 4. RIFE NCNN Vulkan (opcional, solo si querés el FPS boost — ver más abajo)
powershell -ExecutionPolicy Bypass -File .\scripts\download-rife.ps1
# 5. Frontend: compilar la SPA de React (necesario para correr desde codigo fuente,
# requiere Node.js 20+; ver seccion "Desarrollo del frontend" mas abajo)
cd frontend
npm install
npm run build
cd ..
# 6. (opcional) copiar .env.example a .env y ajustar valores
copy .env.example .env
# 7. Arrancar el servidor
.\.venv\Scripts\uvicorn app.main:app --host 127.0.0.1 --port 8090 --reloadAbrí http://127.0.0.1:8090.
Todos los binarios de vendor/ y todo lo de runtime/ (uploads, outputs, temp, video-work) están en .gitignore — se generan localmente con los scripts de arriba y en tiempo de ejecución, nunca se commitean. Lo mismo para frontend/dist/ y frontend/node_modules/: se generan con el paso 5, nunca se commitean.
Instalación más pesada de lo habitual: el paso 1 (
pip install -e .) instala tambiénonnxruntime-directml,torch(CPU-only),spandrelyonnx— dependencias del módulo de modelos HF (ver sección "Modelos" abajo). Sumalas y son ~2-3 GB extra, la mayoría portorch. No hace falta ningún paso manual adicional, solo tener espacio en disco y paciencia la primera vez.
FastAPI sirve el build de producción de frontend/dist/ en / (paso 5 arriba). Para desarrollar la UI con hot-reload, corré el backend y el dev server de Vite en paralelo:
# Terminal 1: backend (API en :8090)
.\.venv\Scripts\uvicorn app.main:app --host 127.0.0.1 --port 8090 --reload
# Terminal 2: frontend con hot-reload (:5173, hace proxy de /api hacia :8090)
cd frontend
npm install
npm run devAbrí http://localhost:5173 durante el desarrollo. npm run build genera el bundle de producción en frontend/dist/ que consume FastAPI; npm test corre la suite de vitest.
La SPA de React se navega por tarea: la raíz pregunta qué querés hacer y te lleva a la pantalla que corresponde, con el estado preseleccionado.
Qué se instala: el asistente del instalador tiene una pantalla de componentes donde elegís las funciones extra —generar fotogramas, quitar ruido con IA, restaurar agudos— cada una con su tamaño. Vienen todas tildadas (perfil "Completa"), y hay un perfil "Mínima" que deja solo imagen y video. Dos paquetes no se ofrecen porque sin ellos la app no hace nada: el motor de upscaling y ffmpeg.
Lo que no instales no se pierde: la pantalla de Tasks lo muestra con un botón que corre el mismo script de descarga, en contexto y con la explicación de para qué sirve. Ese es el mejor momento para decidir, no el de la instalación.
El zip portable no tiene asistente, así que baja todo en el primer arranque. Para automatizar hay
-InstallAlly-SkipOptionalen el launcher.
- Tasks (
/) — el árbol de capacidades, resuelto contra tu máquina: cinco dominios (video, imágenes, audio, generar, impresión y modelado 3D) y sus capacidades. Una capacidad lista te lleva a su pantalla; una que le falta un paquete ofrece bajarlo con un click (corre el mismoscripts/download-*.ps1que antes se corría a mano); y las que todavía no existen se muestran inertes, con el motivo escrito, bajo un encabezado de mapa de ruta. El status sale de mirar el disco y el registro, así que borrar una carpeta devendor/a mano se refleja al instante. - Enhance (
/enhance,/enhance/image,/enhance/video) — imagen y video, con tabs:- Imagen: subís el archivo, elegís modelo, dispositivo de cómputo (
cpu/dml:N) y escala (la lista se filtra automáticamente según lo que soporta cada modelo), formato de salida. Job en vivo con progreso y descarga directa al terminar. - Video: subís el archivo (se analiza automáticamente con
/video/analyze) y elegís un perfil, que rellena una pila de pasos con lo que el job va a hacer de verdad (reescalar, interpolar, audio, subtítulos). Podés quitar y agregar pasos; el orden se muestra pero no se reordena, porque el backend lo tiene fijo y ofrecer reordenar sería un control que miente. Hay opciones avanzadas para sobreescribir modelo, escala, contenedor, códec, preset, CRF, audio, el dropdown FPS boost (Off, o 2×/3×/4×; solo produce resultado si tenésENABLE_INTERPOLATION=truey RIFE instalado — ver más abajo), mejora de audio (Off/RNNoise/DeepFilterNet) y formato de audio de salida (Auto/FLAC/AAC). Si el video trae más de una pista de audio o subtítulos embebidos, aparece un selector de pistas: tildá cuáles pistas de audio conservar (la primera tildada es la primaria, la única que pasa por enhance/restore) y si querés preservar los subtítulos (sube el contenedor a.mkvautomáticamente si hacía falta). - Video → modo CCTV: el interruptor "Security camera footage (CCTV)" (o
/enhance/video?cctv=1) cambia perfil y pasos por el flujo de cámaras de seguridad: diagnóstico, preset, cajas del texto en pantalla, recorte por cuadro, datos del caso y paquete de entrega. Ver Video de cámaras de seguridad.
- Imagen: subís el archivo, elegís modelo, dispositivo de cómputo (
- Models (
/models) — buscador de modelos de super-resolución en Hugging Face con compatibilidad detectada en cada resultado (si trae.onnxse instala directo; si trae pesos de PyTorch se convierten con Spandrel; si está restringido o no tiene pesos, lo dice antes de que aprietes instalar), instalación con un click con polling de progreso, lista de modelos instalados con borrado, y selección de dispositivo por default. - Settings (
/settings) — estado del motor (disponibilidad, ffmpeg), concurrencia de GPU y profundidad de las colas de jobs, en vivo, y selector de idioma (español / inglés, se recuerda en el equipo). - Realtime (
/realtime) — página de roadmap: explica el plan de interpolación en tiempo real (Fase 7) y por qué el frame generation en vivo no es viable todavía en Windows sin driver hooks propietarios.
Un panel de cola de jobs global (imagen + video) con progreso en vivo está disponible desde cualquier módulo.
Todos los endpoints viven bajo /api/v1. Los campos de formulario (subida) van en snake_case; las respuestas JSON usan camelCase (p. ej. subís video_codec como campo del form y la respuesta lo devuelve como videoCodec).
| Método | Endpoint | Descripción |
|---|---|---|
GET |
/api/v1/health |
Healthcheck: version, motor activo, ncnnAvailable/onnxAvailable, devices con freeVramMb, defaultDevice, modelsInstalled, tile (defaults de tiling por motor), gpuConcurrency y profundidad de ambas colas |
GET |
/api/v1/engine |
Estado del motor, si FFmpeg está disponible, catálogo de modelos y de perfiles de video |
GET |
/api/v1/devices |
Dispositivos de cómputo disponibles (cpu, dml:0, dml:1...) y defaultDeviceId efectivo |
GET |
/api/v1/models |
Catálogo completo de modelos instalados (builtin + los instalados desde Hugging Face) |
GET |
/api/v1/models/search?q= |
Busca modelos de super-resolución en Hugging Face Hub, con compatibilidad detectada por resultado (sin requests extra: sale de la metadata que ya viene) |
GET |
/api/v1/models/preflight?repoId= |
Antes de instalar un upscaler: veredicto de compatibilidad, tamaño real de la descarga, disco libre, VRAM libre por dispositivo y RAM. No estima pico de VRAM a propósito — un upscaler hace tiling, así que el factor de estimación de difusión no aplica |
GET |
/api/v1/capabilities/tree |
El árbol de lo que la app puede hacer, resuelto contra esta máquina: available, needs_setup (con el paquete que falta) o not_implemented (con el motivo) |
POST |
/api/v1/capabilities/{id}/provision |
Baja el paquete que le falta a una capacidad corriendo su scripts/download-*.ps1 → 202 |
GET |
/api/v1/capabilities/provision/{jobId} |
Estado de esa descarga |
POST |
/api/v1/video/jobs |
Acepta target_height (opcional): se pide una RESOLUCION de salida en vez de un multiplicador. La app elige el escalado entero mas chico que la alcance y redimensiona a la medida exacta; si la fuente ya llega, no corre el modelo |
POST |
/api/v1/generation/init-image |
Sube la imagen de partida para imagen a imagen y devuelve su token (201). Va aparte del job para que POST /generation/jobs siga siendo JSON |
GET |
/api/v1/asr/models/search?q= |
Busca modelos de reconocimiento de voz en Hugging Face. El filtro por TAG es lo que decide que un repo es de ASR: los nombres de archivo no alcanzan para distinguirlo de otro modelo de audio |
POST |
/api/v1/asr/models/install |
Instala un modelo de ASR: baja el par encoder/decoder no fusionado mas su metadata (~257 MB para whisper-tiny) → 202 |
GET |
/api/v1/asr/models/install/{install_id} |
Estado de esa instalacion |
POST |
/api/v1/transcribe/jobs |
Transcribe un audio a texto (multipart: file, model_id, language?, device?) → 202 |
GET |
/api/v1/transcribe/jobs/{id} |
Estado del job. El TEXTO viaja en la respuesta; .../download da el .txt |
GET |
/api/v1/audio/voice-catalog |
Los pasos de la cadena de mejora de voz en su orden causal y los destinos de entrega con sus números de loudness publicados |
POST |
/api/v1/models/install |
Instala un modelo desde HF por repo_id (202, devuelve install_id) |
GET |
/api/v1/models/install/{install_id} |
Estado de una instalación en curso (pending/downloading/converting/done/error) |
DELETE |
/api/v1/models/{model_id} |
Borra un modelo instalado (204; 403 si es builtin, 404 si no existe) |
POST |
/api/v1/jobs |
Crea un job de imagen (202) |
GET |
/api/v1/jobs/{job_id} |
Estado de un job de imagen (404 si no existe) |
GET |
/api/v1/jobs/{job_id}/download |
Descarga el resultado (404 si no existe, 409 si aún no terminó) |
POST |
/api/v1/video/analyze |
Analiza un video subido (pistas de audio y subtítulos vía ffprobe) sin crear un job; devuelve uploadToken reutilizable en POST /api/v1/video/jobs |
POST |
/api/v1/video/jobs |
Crea un job de video (202) |
GET |
/api/v1/video/jobs/{job_id} |
Estado de un job de video, incluye metadata (stage, fps, dimensiones, outputFps) |
GET |
/api/v1/video/jobs/{job_id}/download |
Descarga el video resultante (404/409 igual que arriba) |
Listar trabajos — las siete familias listan con el MISMO contrato, que es lo que vuelve seguro exponerlo en multiusuario: por defecto devuelve solo los propios, y ?all=true devuelve los de todos pero exige el permiso jobs:read_all (sin él, 403). Los terminados siguen apareciendo hasta que la poda los retira.
| Familia | Endpoint |
|---|---|
| Imagen | GET /api/v1/jobs |
| Video | GET /api/v1/video/jobs |
| Audio | GET /api/v1/audio/jobs |
| Generación | GET /api/v1/generation/jobs |
| Transcripción | GET /api/v1/transcribe/jobs |
| Descargas | GET /api/v1/download/jobs |
| 3D | GET /api/v1/print/generate |
Es lo que usa la interfaz para recuperar la cola al recargar el navegador: sin listado, un trabajo en curso seguía corriendo en el servidor pero se perdía de vista para siempre.
Crear un job de imagen — campos de formulario: file (requerido), model_name (default realesrgan-x4plus, ignorado si se manda model_id), model_id (opcional: id de un modelo ONNX instalado desde HF, ver sección Modelos), device (opcional: cpu/dml:N, ver sección Dispositivos; omitido = DEFAULT_DEVICE), scale (default 4; con un modelo x4 y scale=2/3 el motor corre a 4× y Upflow reduce con Lanczos — nunca con el -s del binario ncnn, que produce un mosaico de tiles), output_format (png/jpg/jpeg/webp, default png), tile_size (opcional: omitido = auto por motor, 0 = sin tiling — ncnn elige el mayor tile que entra en la VRAM libre —, N>=32 = tile fijo), tile_overlap (opcional, solo motor ONNX, default 16). El job terminado devuelve en metadata.effective el comando/config real que corrió (engine, command, nativeScale, requestedScale, tileSize, tileOverlap, resized):
curl -X POST http://127.0.0.1:8090/api/v1/jobs \
-F "file=@input.png" \
-F "model_name=realesrgan-x4plus-anime" \
-F "scale=4" \
-F "output_format=png"
# con un modelo ONNX instalado desde Hugging Face, en la GPU dml:0
curl -X POST http://127.0.0.1:8090/api/v1/jobs \
-F "file=@input.png" \
-F "model_id=sceneworks--real-esrgan-onnx" \
-F "device=dml:0" \
-F "output_format=png"Crear un job de video — campos de formulario: file o upload_token (exactamente uno de los dos: file sube el video directo, upload_token reutiliza el análisis previo de POST /api/v1/video/analyze sin volver a subir el archivo), profile_key (default anime-balanced-2x), y overrides opcionales del perfil: model_name, model_id (modelo ONNX instalado desde HF, ver sección Modelos), device (cpu/dml:N, ver sección Dispositivos), scale, output_container (mp4/mkv), video_codec (libx264/libx265), video_preset (medium/slow/veryslow), crf (10-28), keep_audio, fps_multiplier (1 = sin boost, o uno de ALLOWED_FPS_MULTIPLIERS), audio_enhance (deepfilter/rnnoise, omitido = sin mejora; requiere keep_audio=true y ENABLE_AUDIO_ENHANCE=true — ver "Cómo activar la mejora de audio" abajo), audio_track_indices (índices de pista separados por coma, ej. 0,2; omitido = ffmpeg elige la pista default como hoy — la primera pista de la lista es la primaria, la única que pasa por enhance/restore, el resto se copia sin procesar), keep_subtitles (default false; copia todas las pistas de subtítulos detectadas — sube el contenedor a .mkv automáticamente si hacía falta, con aviso en job.metadata.containerUpgradedReason), audio_output_format (auto/flac/aac, default auto: con audio_restore activo sube a FLAC lossless + .mkv automático, si no mantiene el comportamiento actual):
curl -X POST http://127.0.0.1:8090/api/v1/video/jobs \
-F "file=@input.mp4" \
-F "profile_key=anime-balanced-2x" \
-F "fps_multiplier=2"
# con mejora de audio (requiere ENABLE_AUDIO_ENHANCE=true y haber corrido download-deepfilternet.ps1)
curl -X POST http://127.0.0.1:8090/api/v1/video/jobs \
-F "file=@input.mp4" \
-F "profile_key=anime-balanced-2x" \
-F "keep_audio=true" \
-F "audio_enhance=deepfilter"
# analizar primero (pistas de audio/subtítulos), despues crear el job reusando el upload
curl -X POST http://127.0.0.1:8090/api/v1/video/analyze -F "file=@input.mkv"
# -> {"uploadToken": "...", "audioTracks": [...], "subtitleTracks": [...]}
curl -X POST http://127.0.0.1:8090/api/v1/video/jobs \
-F "upload_token=<uploadToken>" \
-F "profile_key=anime-balanced-2x" \
-F "audio_track_indices=0,2" \
-F "keep_subtitles=true"Consultar y descargar:
curl http://127.0.0.1:8090/api/v1/video/jobs/<job_id>
curl -OJ http://127.0.0.1:8090/api/v1/video/jobs/<job_id>/downloadLa cola de jobs global muestra una barra de progreso en vivo para cada job; hacer click en un job abre un modal de detalle con:
- Stepper de etapas — cada tipo de job tiene sus propias etapas ponderadas (video:
probing→extracting_frames→extracting_audio/enhancing_audio/restoring_audio(si aplica) →upscaling_frames→interpolating_frames(si el FPS boost está activo) →encoding_video; imagen:validating→upscaling), cada una con estadopending/active/done. - Frames X / Y — en video, cuenta de frames procesados sobre el total real (extraídos del contenedor con
ffprobe, o derivados de duración × fps cuando el origen es VFR y no traenb_frames). En imagen, solo aparece para modelos ONNX con tiling (ONNX_TILE_SIZEactivo en un lado más grande que el tile): cuenta tiles procesados sobre el total, actualizado entre cada tile de la grilla de inferencia. Los modelos builtin NCNN (subprocess único, sin conteo intermedio) y las imágenes ONNX que caben en un solo tile se quedan en etapas coarse (validating/upscalingsin frames) — a propósito: no hay conteo honesto que reportar ahí, así que no se inventa uno. - ETA — solo se muestra cuando hay suficiente señal para ser confiable (frames/tiles con denominador real y throughput medido); si no, se omite en vez de mostrar un número inventado.
El progreso combinado (progressPct en la respuesta del job) es un promedio ponderado: cada etapa completada suma su peso completo, la etapa activa suma su peso proporcional a la fracción interna (frames o tiles procesados), y nunca retrocede.
Los jobs largos (videos de muchos frames, modelos ONNX pesados) ya no se matan por un timeout fijo de duración: un stall watchdog cancela el job solo si deja de haber progreso real (sin frames nuevos) durante FRAME_STALL_TIMEOUT_SECONDS (default 900s), no por exceder un techo de reloj arbitrario.
| Modelo | Ideal para | Escalas |
|---|---|---|
realesrgan-x4plus |
Fotos, imágenes generales | 2× / 3× / 4× (2× y 3× = 4× nativo + reducción Lanczos) |
realesrgan-x4plus-anime |
Anime fijo, ilustración, line art | 2× / 3× / 4× (ídem) |
realesr-animevideov3-x2 / -x3 / -x4 |
Fotogramas de video anime | 2× / 3× / 4× |
realesr-animevideov3 |
Preset automático (resuelve a x2/x3/x4 según la escala pedida) | 2×–4× |
Estos modelos vienen empaquetados con el motor (scripts/download-realesrgan.ps1), corren siempre sobre Vulkan y no aceptan device=cpu (ver sección Dispositivos).
Escala nativa y tiling. El binario realesrgan-ncnn-vulkan solo sabe reescalar a la escala del modelo: con -s 2 sobre un modelo x4 devuelve una imagen del tamaño correcto pero armada con el cuarto superior izquierdo de cada tile ampliado (rejilla de bloques de 400 px, PSNR 13 dB contra el 4× real — medido 2026-09-02, ver docs/images/ncnn-scale2-seams-before-after.png). Por eso el motor corre siempre a la escala nativa y Upflow reduce a la pedida con Lanczos; tests/test_seams.py mide la rejilla y falla si vuelve. En video pasa lo mismo desde esta versión: los cuadros van al binario con la escala nativa del modelo, el encode reduce con scale=...:flags=lanczos a la medida pedida y metadata.upscaleEngineScale guarda la escala real; un vkAllocateMemory failed con código 0 hace fallar el job antes del encode en vez de codificar cuadros planos (probado con tests, no en GPU; con el catálogo actual los builtin fuera de su escala nativa ya iban a ONNX, así que cubre un caso latente). Parámetros de tiling del job de imagen (API tile_size/tile_overlap, CLI --tile/--tile-overlap): omitido = auto (ncnn deja elegir al binario por heap de VRAM: 200 px en GPUs con más de 1.9 GB; ONNX usa ONNX_TILE_SIZE), 0 = sin tiling (ncnn: el mayor tile que entra en la VRAM libre, ≈ 600 MB + 0.0215 MB por píxel de tile medido en una RX 7800 XT; ONNX: un solo pase), N>=32 = tile fijo. Tiles más grandes no acortan el tiempo ni cambian la calidad a escala nativa (t200 1.5 GB / 7 s, t600 8.1 GB / 6.9 s, t1000 vkAllocateMemory failed — que ahora se detecta y falla el job en vez de devolver una imagen plana). tile_overlap solo aplica al motor ONNX; ncnn usa un prepadding fijo de 10 px.
Además del catálogo builtin, Upflow puede instalar cualquier modelo de super-resolución publicado en Hugging Face y correrlo con el motor ONNX Runtime + DirectML:
- Buscar en el buscador de la web UI (sección Modelos) o con
GET /api/v1/models/search?q=<texto>— pega directo a la Hub API de Hugging Face. - Instalar con
POST /api/v1/models/install({"repo_id": "org/nombre-repo"}) — devuelve uninstall_idpara hacer polling enGET /api/v1/models/install/{install_id}hastastatus=done. - Una vez instalado, el modelo aparece en
GET /api/v1/modelsconkind=onnxy puede pasarse comomodel_idal crear un job de imagen o video.
Formatos soportados:
.onnxdirecto — se copia tal cual aMODELS_DIR, sin conversión. Camino rápido (ej.SceneWorks/real-esrgan-onnx)..pth/.safetensors(arquitecturas comunitarias tipo ESRGAN/Compact/SRVGG) — se detecta la arquitectura vía Spandrel y se convierte a ONNX automáticamente contorch.onnx.exportantes de dejarlo instalado. Requiere las dependenciastorch/spandrel/onnx(ver "Instalación paso a paso" — se instalan solas conpip install -e .).
Si el repo de HF no expone un archivo compatible, el estado del install job pasa a error con el detalle.
La conversión exporta lo mismo que Spandrel ejecuta (descriptor(x), no model.forward: el canal de ruido de DRUNet/DnCNN, la primera salida de FBCNN, la escala de MixDehazeNet), recortado a [0, 1], y antes de registrar el modelo compara la salida ONNX contra la de PyTorch a dos tamaños de prueba. Se rechazan de entrada los modelos de inpainting, los de caras (FaceSR) y los que no son RGB. El registro guarda el propósito que declara Spandrel (purpose: SR, Restoration...), los canales y los requisitos de tamaño, y GET /api/v1/models los expone. El selector de modelos filtra por propósito: Image y Video ofrecen los de super-resolución y también los 1x de limpieza (DeJPEG, denoise), rotulados como que limpian a 1x y el resto de la escala es un reescalado común; Generate ofrece solo super-resolución, y el selector nombra los instalados que oculta.
Un modelo instalado puede borrarse con DELETE /api/v1/models/{model_id} (los 6 builtins están protegidos: devuelve 403). El límite de tamaño de descarga es MAX_MODEL_DOWNLOAD_MB (default 2048 MB).
Los modelos de generación tienen su propio buscador y su propio camino de instalación, con tres diferencias respecto del de upscalers.
No hace falta saber el repo_id. Con el buscador vacío, la sección Modelos muestra los pipelines text-to-image más descargados de Hugging Face. Cada tarjeta trae un badge de compatibilidad calculado de la metadata real del repo, sin descargar nada:
| Badge | Qué significa |
|---|---|
| Listo | Trae ONNX para todos sus componentes: se instala directo |
| Requiere conversión | Solo pesos PyTorch: se exporta a ONNX localmente (necesita torch, tarda) |
| Gated | Acceso restringido: hay que configurar HF_TOKEN y aceptar la licencia del repo |
| Incompatible | No es un pipeline diffusers (le falta model_index.json) |
Precisión elegible. En los repos que publican ambas variantes se puede elegir entre fp16 y fp32. La elección define las dos cosas: qué archivos se bajan y en qué precisión queda el ONNX exportado. fp16 pesa la mitad, usa menos VRAM y corre más rápido en DirectML; fp32 es más fiel y la única opción sensata en CPU. En un repo que ya viene en ONNX no hay elección: su precisión la fijó quien lo publicó.
No hay techo de descarga, solo avisos. Al expandir una tarjeta, Upflow mide el espacio libre real del disco destino y la VRAM libre de cada dispositivo, y avisa si el modelo probablemente no funcione bien ahí:
Precisión (•) fp16 · baja 2.6 GB ( ) fp32 · baja 5.2 GB
dml:0 RX 7900 XTX libre 22.1 GB ✓ entra
dml:1 RX 6600 libre 7.4 GB ✗ no entra (~8.4 GB estimados a 512×512)
cpu CPU ⚠ varios minutos por imagen
Son avisos, no bloqueos: el botón de instalar queda habilitado siempre. La decisión es de quien usa la app, que sabe de su máquina más que una constante en el código. El estimado de VRAM es eso — un estimado derivado del tamaño de los pesos, etiquetado con la resolución de referencia.
Endpoints: GET /api/v1/generation/models/search?q= (vacío = browse por descargas), GET /api/v1/generation/models/preflight?repoId=<id> y POST /api/v1/generation/models con {"repoId": "...", "precision": "fp16"}.
GET /api/v1/devices enumera los dispositivos de cómputo disponibles para el motor ONNX/DirectML:
cpu— siempre presente. Válido solo para modelos ONNX instalados desde Hugging Face (kind=onnx). Inválido para los 6 modelos builtin (kind=builtin-ncnn): corren siempre sobre Vulkan y pedirdevice=cpucon unmodel_idbuiltin devuelve400("Device 'cpu' is not supported for builtin model ... (requires a Vulkan GPU device)").dml:N— una GPU DirectML-capable,N= índice de adaptador DXGI (0, 1, 2...). El nombre real de cada GPU viene deIDXGIFactory1::EnumAdapters1(Windows, víactypes, sin dependencia extra) yNes exactamente eldevice_idque se le pasa aDmlExecutionProviderde onnxruntime — mapeo verificado empíricamente (ver.superpowers/sdd/sp1-task-8-smoke-report.md).
Es normal que una misma GPU física aparezca más de una vez (ej. dml:0 y dml:2 apuntando ambos a la misma dGPU) en máquinas con configuraciones de gráficos híbridos: Windows/DXGI expone LUIDs de adaptador distintos para el mismo silicio. Cada índice sigue siendo un device_id válido y funcional para DirectML — no es un bug, es comportamiento real de DXGI.
El dispositivo por defecto se controla con DEFAULT_DEVICE en .env (default dml:0); si el dispositivo configurado no está disponible, cae automáticamente a cpu.
Selección de GPU en máquinas multi-adaptador — alcance de la garantía: para modelos ONNX/HF (kind=onnx), dml:N se pasa directo como device_id a DmlExecutionProvider, que lo resuelve contra la misma lista ordenada por DXGI — el mapeo es exacto (verificado empíricamente, ver .superpowers/sdd/sp1-task-8-smoke-report.md). Para los modelos builtin (kind=builtin-ncnn), en cambio, dml:N se traduce a -g N (índice de dispositivo físico Vulkan del binario realesrgan-ncnn-vulkan.exe) — DXGI y Vulkan no garantizan el mismo orden de enumeración en una máquina con más de un adaptador. Solo el default de un único dGPU (dml:0 → -g 0) está verificado end-to-end; en hardware multi-GPU, la selección de un dml:N con N > 0 para un modelo builtin es best-effort, no exacta.
Cada dispositivo tiene su propia cola de concurrencia: un job toma un permiso del semáforo de su dispositivo (app/services/device_semaphores.py), así que jobs en dispositivos distintos corren en paralelo en vez de serializarse detrás de un semáforo global. Un video reescalando en dml:0 y una imagen en la iGPU dml:1 (o en cpu) avanzan a la vez.
PER_DEVICE_GPU_CONCURRENCY(default1) — jobs simultáneos por GPU. Imagen y video comparten el semáforo de esa GPU. No lo subas sin perfilar VRAM: dos jobs pesados en la misma GPU compiten por memoria.CPU_CONCURRENCY(default2) — jobs simultáneos encpu(modelos ONNX). Elcpuno compite con las GPUs.MAX_CONCURRENT_JOBS(default4) — workers por manager (imagen y video por separado). Debe superar la cantidad de dispositivos que quieras correr en paralelo, o no habrá worker libre para desencolar el segundo job.
Además del conteo de jobs, cada permiso también puede exigir recursos libres reales del device antes de otorgarse (app/services/resource_probes.py):
MIN_FREE_VRAM_MB(default768) — VRAM libre mínima para admitir un job GPU nuevo, medida en vivo víaIDXGIAdapter3::QueryVideoMemoryInfo(detecta presión de otras apps, no solo jobs propios).0= sin piso.MIN_FREE_RAM_MB(default1024) — RAM libre mínima para admitir un jobcpunuevo, víapsutil.0= sin piso.RESOURCE_POLL_INTERVAL_SECONDS(default5) — cada cuánto se re-chequea mientras un job espera gateado por recursos, para detectar presión externa que se libera sola.
Un job nunca falla por esto — si no hay recursos suficientes, espera en cola (igual que por conteo de jobs) hasta que se liberen, propios o ajenos.
Este gateo por recursos está activo por defecto (los valores de arriba no son 0): un despliegue existente que actualice a esta versión sin tocar su .env empieza a admitir jobs según VRAM/RAM libre real de inmediato, aunque el umbral por defecto es generoso y no debería notarse en hardware típico. Para restaurar el comportamiento previo (solo conteo de jobs, sin piso de recursos), poné MIN_FREE_VRAM_MB=0 y MIN_FREE_RAM_MB=0.
Auto-router opcional (ENABLE_AUTO_ROUTE, default False): con el toggle activado, los jobs sin dispositivo fijo (o con device="auto") se reparten al primer dispositivo compatible libre en vez de encolarse todos en dml:0. La compatibilidad depende del modelo:
| Tipo de modelo | Dispositivos compatibles |
|---|---|
builtin-ncnn (los 6 builtin) |
solo GPUs Vulkan (dml:N) — nunca cpu |
onnx (instalados desde HF) |
cpu o cualquier dml:N |
Si al desencolar todos los dispositivos compatibles están ocupados, el job espera al que se libere primero (sin bloqueo head-of-line: no se casa con un dispositivo saturado dejando otro libre ocioso). Si no existe ningún dispositivo compatible (ej. modelo builtin en una máquina sin GPU Vulkan), la creación del job responde 400. El toggle vive en Settings de la web UI y se puede elegir "Auto" en el selector de dispositivo por job.
Caso de uso central: reescalar una temporada completa de anime encolando todos los episodios con auto-router on → se reparten entre las GPUs disponibles y terminan antes que en serie.
| Perfil | Categoría | Modelo | Escala | Códec | Preset | CRF |
|---|---|---|---|---|---|---|
general-balanced-4x |
General | realesrgan-x4plus |
4× | libx264 |
medium |
18 |
general-hq-4x |
General | realesrgan-x4plus |
4× | libx265 |
slow |
17 |
anime-balanced-2x (default) |
Anime | realesr-animevideov3-x2 |
2× | libx264 |
medium |
17 |
anime-quality-3x |
Anime | realesr-animevideov3-x3 |
3× | libx265 |
slow |
16 |
anime-max-detail-4x |
Anime | realesr-animevideov3-x4 |
4× | libx265 |
slow |
15 |
Cualquier campo del perfil puede sobreescribirse por request (ver "Crear un job de video" arriba). El catálogo completo vive en app/config.py (MODEL_CATALOG / VIDEO_PROFILE_CATALOG) — agregar un modelo o perfil ahí lo expone automáticamente en la web UI y en GET /api/v1/engine.
Apagado en esta versión. La pestaña, la API (
/api/v1/restore/*), la CLI (upflow restore) y las tools MCP están en el código, pero los tres packs de modelos (restore-core,restore-facesyrestore-colorize) todavía no tienen artefactos publicados. Por eso la versión la trae apagada (RESTORE_PHOTO_ENABLED=false): la pestaña no aparece, la API responde404con el motivo, la CLI y MCP lo rechazan con el mismo texto y el mapa de funciones la muestra entre las que vienen, sin botón de descarga. ConRESTORE_PHOTO_ENABLED=trueen.env(y reiniciando) vuelve todo, pero sin packs solo corren los pasos sin modelo: quitar la trama, color y tono, y reparar lo que pintás a mano con el relleno clásico. Hubo un smoke de punta a punta en DirectML (RX 7800 XT) con seis fotos de dominio público y los modelos todavía sin publicar; los tiempos por etapa se publican junto con los packs.
Soltás la foto (PNG, JPG, WEBP, BMP o TIFF, también de 16 bits) y la app la analiza en la CPU:
dice qué encontró (rayas y manchas, bloques de JPEG, ruido, trama de impresión, desvaído, caras) y
propone un punto de partida. Antes de restaurar podés girar, enderezar y recortar; probar la cadena
en un recorte de hasta 512×512 ("Preview this area"); revisar y corregir la máscara de daños con un
pincel; y elegir cara por cara cuáles restaurar. El resultado se compara con un deslizador antes y
después, y los mismos ajustes se pueden aplicar a más fotos, cada una como su propio trabajo.
El resultado sale en PNG, JPEG, WEBP o TIFF. El TIFF de 8 bits lleva el perfil ICC, el EXIF
limpio y el XMP; el de 16 bits (cuando la foto de origen lo es) no puede llevarlos, y el sidecar lo
declara en privacy.metadataNotEmbedded.
Hojas de escáner y fotos de celular. Si la foto está sobre una hoja de escáner más grande, el
análisis lo nota y ofrece "Auto crop": la endereza (el ángulo sale del borde de la foto) y la recorta
unos píxeles hacia adentro para no dejar franjas de la tapa. Con varias fotos en la misma hoja las
numera en orden de lectura, dibuja su contorno sobre la hoja y cada botón "Photo N" deja lista esa
foto para restaurar; el original no cambia, así que después se puede volver a la hoja y elegir otra.
Si la copia parece fotografiada en ángulo, "Fix perspective" la rectifica con las cuatro esquinas que
encontró, y "Perspective" / "Adjust corners" deja moverlas a mano (arrastrando o con las flechas;
Shift da pasos más grandes). La perspectiva ya fija el giro, así que mientras está puesta "Straighten"
queda bloqueado. Cuando la foto tiene reflejos o perspectiva aparece la ficha "Looks like a phone photo
of a print (glare/perspective)" con el consejo de reescanear a 600 dpi o más, o volver a fotografiar
en ángulo. Nada de esto se aplica solo: son sugerencias con su botón. La detección necesita un fondo
parejo alrededor (la tapa del escáner, una mesa lisa) y sus umbrales son una propuesta que se
recalibra con escaneos reales; en el sidecar la geometría registra las esquinas (corners) solo si hay
perspectiva.
Varias fotos con caras. "Apply these settings to more photos" repite los pasos y ajustes en
cada foto nueva, con su propia máscara automática. Las caras quedan afuera salvo que marques "Also
restore faces in these photos": entonces cada foto elige sus caras con la misma política de siempre
(solo las de 32 px o más entre ojos que no se ven nítidas; nada por debajo de 8 px) y la mezcla que
elegiste. Lo que se decidió mirando la primera foto —qué cara, su mezcla propia, la geometría, el
recorte de prueba, el punto gris o la máscara pintada— no viaja: el backend rechaza un lote que lo
mande. Debajo aparece la lista de fotos del lote, con el progreso de cada una y, cuando terminan,
cuántas caras se restauraron y el control cara por cara sobre ese resultado ("Review faces"), con
un aviso de cuántas fotos faltan revisar. La lista dura mientras no abras otra foto; los trabajos
siguen en Tareas. El sidecar de cada foto del lote lleva "batch": true.
Los pasos, siempre en este orden (el orden sale del catálogo y no del pedido: cada paso necesita que el anterior ya haya pasado):
| Paso (UI) | Qué arregla | Cómo | Inventa detalle |
|---|---|---|---|
| Remove print pattern | La trama de puntos de un recorte de diario o revista | Filtro clásico (FFT), sin modelo | No |
| Repair damage | Rayas, grietas, manchas, polvo | Detector BOPBTL (pack restore-core, siempre en CPU) o tu máscara pintada; relleno con MI-GAN (pack del borrador) o Telea clásico |
Sí, en los huecos grandes |
| Remove JPEG artifacts | Bloques y halos de compresión | DRUNet (restore-core) |
No |
| Reduce noise | Grano y ruido del escaneo, conservando parte del grano | DRUNet (restore-core) |
No |
| Fix colors and tone | Desvaído y dominante de color; conserva sepia, virado e iluminado a mano salvo que pidas "Neutral gray" | Clásico, sin modelo | No |
| (agrandar) | Opcional: ninguno, clásico o un modelo de super-resolución | Motor de reescalado existente | Solo con modelo IA generativo, y el selector lo dice |
| Restore faces | Caras chicas o borrosas | RetinaFace-R34 + GFPGAN v1.4 (restore-faces), mezcla 60% por defecto |
Sí: cada cara restaurada lleva su aviso |
| Colorize | Fotos en blanco y negro o viradas; con colorize.from_luminance (solo API por ahora) también una copia de color muy desvanecida, que se recolorea desde su luminancia y pierde el color que le quedaba |
DDColor-tiny (restore-colorize) |
Sí: los colores son una estimación |
Puntos de partida: Gentle (solo lo que el análisis encontró, conserva el tono), Heavy damage, Newspaper / magazine clipping, Faded color print y Portrait (Gentle más las caras que dan para restaurar). Ninguno colorea ni neutraliza: eso lo pedís vos.
Lo que no recupera, dicho en la UI y acá. Detalle que el papel nunca registró (una cara de 15 px no vuelve), zonas quemadas, ni los colores reales de una foto en blanco y negro: la colorización es una estimación con sesgos conocidos (piel más clara, colores "seguros"). La identidad exacta de una cara chica tampoco: por eso las caras de menos de 8 px entre ojos no se restauran, de 8 a 32 px quedan apagadas (opt-in cara por cara, con confirmación por debajo de 16 px) y cada cara restaurada se puede apagar o re-mezclar sobre el resultado sin volver a correr nada.
Qué te llevás. La foto restaurada; si colorizaste, también la versión sin color; y
<nombre>.restore.json con los pasos, sus ajustes, qué modelo corrió en qué placa y precisión, y
qué cayó a la CPU. La foto lleva XMP con DigitalSourceType: compositeWithTrainedAlgorithmicMedia
cuando hay contenido inventado a la vista (caras restauradas, color, un relleno de más del 1% de la
foto o sobre una cara, un agrandado generativo) y algorithmicallyEnhanced en los demás casos. El
GPS del EXIF se quita salvo que pidas conservarlo. Todo corre local: un test
(tests/test_restore_is_local.py) falla si algún módulo de restauración importa algo que hable por
red.
Nada viene en el instalador: cada pack se baja aparte con su botón o con
scripts\download-restore.ps1 -Bundle core|faces|colorize, e instala junto a los modelos su
LICENSE y un NOTICE.txt que dice qué se modificó (export a ONNX opset 17, normalización
incluida en el grafo y, donde aplica, conversión fp16). Los pesos se re-exportan desde los
oficiales en un repo propio (port-restore-onnx, código MIT); cada peso conserva la licencia de
su origen, la insignia MIT del repo de export no la reemplaza. En la app, Ajustes → Licenses
muestra lo mismo por pack instalado, y THIRD_PARTY_NOTICES.md lista el
código portado.
| Pack | Modelo | Licencia (código / pesos) | Atribución | Datos de entrenamiento | Linaje |
|---|---|---|---|---|---|
restore-core |
Detector de rayas BOPBTL | MIT / MIT | Microsoft, Bringing Old Photos Back to Life | Pascal VOC (fotos de Flickr con sus propios términos) + 783 fotos antiguas de origen no declarado | D1a + D1c |
restore-core |
DRUNet (de-JPEG y ruido) | MIT / MIT | Kai Zhang, KAIR / DPIR | BSD400, WED, DIV2K y Flickr2K | D1a |
restore-faces |
RetinaFace-R34 (detector) | MIT / MIT | yakhyo, a partir de biubug6 | WIDER FACE (CC BY-NC-ND 4.0) + backbone de ImageNet | D1b + D1a |
restore-faces |
GFPGAN v1.4 | Apache-2.0 con excepciones de terceros¹ | Tencent ARC | FFHQ (compilación CC BY-NC-SA 4.0); pérdida de identidad con un ArcFace de datos no declarados | D1b + D1c |
restore-colorize |
DDColor-tiny | Apache-2.0 / Apache-2.0 | piddnad (DDColor) | ImageNet (investigación, no comercial) | D1a |
| pack del borrador | MI-GAN | MIT / MIT | Picsart AI Research | Places2 (investigación, no comercial) | D1a |
¹ El LICENSE de GFPGAN excluye StyleGAN2 (NVIDIA Source Code License-NC) y DFDNet (CC BY-NC-SA 4.0)
sin decir qué archivos; la inferencia usa solo la arquitectura clean (gfpganv1_clean_arch +
stylegan2_clean_arch), así que el riesgo residual es bajo, no nulo.
Ningún peso está libre de deuda de datos. El tipo de cláusula es lo que distingue:
- D1a — las imágenes de entrenamiento son "solo investigación" o no comerciales, o son compilaciones de Flickr sin licencia propia (DIV2K, Flickr2K, BSD/WED, Places2, ImageNet, Pascal VOC). Es el mismo criterio con el que Upflow ya distribuye Real-ESRGAN.
- D1b — la compilación tiene cláusulas NoDerivatives o ShareAlike (WIDER FACE, FFHQ): el peso podría leerse como obra derivada de la compilación.
- D1c — datos no declarados (las 783 fotos de BOPBTL, el ArcFace de la pérdida de GFPGAN).
Si alguna de esas clases se descarta, el pack correspondiente no se publica y la función queda sin ese modelo (por ejemplo, sin D1b no hay restauración de caras). Los modelos no comerciales (CodeFormer y afines) no están en v1.
Compuerta de licencias. Un pack de LICENSE_GATED_PACKS (los de modelos con
commercial_use == "no") no se baja sin aceptar su licencia: POST /api/v1/packs/{pack}/provision
responde 403 con la clave pack.license.required hasta que llega ?acceptLicense=true, y el botón
de descarga muestra entonces el texto completo (GET /api/v1/packs/{pack}/license, leído de
app/licenses/gated/<pack>.txt) con una casilla. Si ese texto falta, la descarga se rechaza aunque
se acepte (pack.license.unavailable). Hoy la lista está vacía.
Todas las variables leen de .env (ver .env.example con los defaults y comentarios). get_settings() cachea el resultado — reiniciá el servidor después de cambiar .env.
| Variable | Default | Descripción |
|---|---|---|
APP_NAME |
Upflow |
Nombre interno del proceso FastAPI |
APP_HOST |
127.0.0.1 |
Host de bind de uvicorn |
APP_PORT |
8090 |
Puerto de bind de uvicorn |
MAX_UPLOAD_MB |
50 |
Tamaño máximo de subida para imágenes (MB) |
MAX_VIDEO_UPLOAD_MB |
2048 |
Tamaño máximo de subida para videos (MB) |
MAX_IMAGE_PIXELS |
120000000 |
Límite de píxeles (ancho × alto) para evitar decompression bombs |
PER_DEVICE_GPU_CONCURRENCY |
1 |
Jobs simultáneos por GPU (semáforo por dispositivo; imagen y video comparten el de esa GPU) — no subirlo sin perfilar VRAM. Ver Multi-GPU |
CPU_CONCURRENCY |
2 |
Jobs simultáneos en cpu (modelos ONNX); no compite con las GPUs |
MIN_FREE_VRAM_MB |
768 |
VRAM libre mínima (MB) para admitir un job GPU nuevo; 0 = sin piso. Ver Multi-GPU |
MIN_FREE_RAM_MB |
1024 |
RAM libre mínima (MB) para admitir un job cpu nuevo; 0 = sin piso |
RESOURCE_POLL_INTERVAL_SECONDS |
5 |
Cada cuánto se re-chequea VRAM/RAM mientras un job espera por presión externa |
MAX_CONCURRENT_JOBS |
4 |
Workers por manager (imagen y video por separado); debe superar la cantidad de dispositivos a correr en paralelo |
ENABLE_AUTO_ROUTE |
False |
Auto-router: reparte jobs sin dispositivo fijo (o device="auto") al primer dispositivo compatible libre. Ver Multi-GPU |
SUBPROCESS_TIMEOUT |
86400 |
Backstop absoluto (24h) para matar cualquier subproceso; NO es el mecanismo real (ver FRAME_STALL_TIMEOUT_SECONDS) |
FRAME_STALL_TIMEOUT_SECONDS |
900 |
Watchdog real: mata la etapa solo si no produce frames/bytes nuevos por este tiempo (15 min); se reinicia con cada frame nuevo |
FFMPEG_BINARY |
vendor/ffmpeg/bin/ffmpeg.exe |
Ruta al binario de FFmpeg |
FFPROBE_BINARY |
vendor/ffmpeg/bin/ffprobe.exe |
Ruta al binario de ffprobe |
FFMPEG_DECODE_THREADS |
12 |
Hilos para extraer frames del video de entrada |
FFMPEG_ENCODE_THREADS |
24 |
Hilos para re-encodear con libx264 |
FFMPEG_X265_THREADS |
8 |
Hilos para re-encodear con libx265 (limitado: falla en Windows con exceso de threads) |
RUNTIME_DIR |
runtime |
Carpeta para uploads/outputs/temp/video-work (relativa a la raíz del proyecto, funciona sin importar el CWD) |
ENGINE |
realesrgan-ncnn |
Identificador del motor de upscaling activo |
ENGINE_BINARY |
vendor/realesrgan/realesrgan-ncnn-vulkan.exe |
Ruta al binario del motor |
ENGINE_MODELS_DIR |
vendor/realesrgan/models |
Carpeta de modelos del motor |
DEFAULT_MODEL |
realesrgan-x4plus |
Modelo preseleccionado en la UI y en POST /api/v1/jobs |
DEFAULT_SCALE |
4 |
Escala preseleccionada |
ALLOWED_SCALES |
2,3,4 |
Escalas permitidas por la API (lista separada por comas) |
DEFAULT_VIDEO_PROFILE |
anime-balanced-2x |
Perfil de video preseleccionado en la UI |
OUTPUT_TTL_HOURS |
24 |
Horas antes de borrar outputs y jobs terminados (el sweep corre cada hora) |
ALLOWED_ORIGINS |
(derivado de APP_HOST/APP_PORT; ej. http://127.0.0.1:8090,http://localhost:8090) |
Orígenes permitidos para requests que cambian estado (POST/PUT/PATCH/DELETE). Si no se define, se deriva automáticamente del APP_HOST/APP_PORT configurados; fijarlo explícitamente para sobreescribir |
MAX_QUEUE_SIZE |
20 |
Tamaño máximo de cada cola de jobs (imagen y video por separado); responde 429 si se llena |
RIFE_BINARY |
vendor/rife/rife-ncnn-vulkan.exe |
Ruta al binario de RIFE NCNN Vulkan |
RIFE_MODELS_DIR |
vendor/rife/models |
Carpeta de modelos de RIFE |
RIFE_MODEL |
rife-v4.25 |
Modelo RIFE usado para interpolar (recomendado, general-purpose) |
ENABLE_INTERPOLATION |
false |
Habilita el FPS boost; requiere haber corrido download-rife.ps1 |
ALLOWED_FPS_MULTIPLIERS |
2,3,4 |
Multiplicadores de FPS permitidos por la API (lista separada por comas) |
ENABLE_GMFSS |
false |
Habilita el motor GMFSS (máxima calidad, muy lento); requiere ENABLE_INTERPOLATION=true además y haber corrido download-gmfss-onnx.ps1 |
GMFSS_MODEL_DIR |
vendor/gmfss |
Carpeta con los 4 .onnx + manifest.json del port GMFSS (+ fusionnet_fp16.onnx opcional) |
DEEPFILTER_BINARY |
vendor/deepfilternet/deep-filter.exe |
Ruta al binario CLI de DeepFilterNet |
RNNOISE_MODEL |
vendor/deepfilternet/models/sh.rnnn |
Ruta al modelo .rnnn usado por el filtro arnndn de FFmpeg |
ENABLE_AUDIO_ENHANCE |
false |
Habilita la mejora de audio (audio_enhance=deepfilter|rnnoise); requiere haber corrido download-deepfilternet.ps1 |
DEFAULT_DEVICE |
dml:0 |
Dispositivo preferido (cpu/dml:N) para el motor ONNX/DirectML; cae a cpu si no está disponible (ver sección Dispositivos) |
MODELS_DIR |
models |
Carpeta donde se guardan los modelos ONNX instalados desde Hugging Face (relativa a la raíz del proyecto) |
HF_TOKEN |
(vacío) | Token de Hugging Face opcional, para buscar/descargar modelos privados o evitar rate limiting anónimo |
MAX_MODEL_DOWNLOAD_MB |
2048 |
Tamaño máximo permitido para un archivo de modelo descargado desde HF (MB) |
ONNX_TILE_SIZE |
256 |
Tamaño de tile (px) para inferencia ONNX por partes, con blend de 16px de solape; 0 desactiva el tiling (imagen completa de una pasada) |
ENABLE_STREAM_PIPELINE |
True |
Pipeline de frames en streaming (decode→interp→upscale→encode por colas en memoria, sin PNGs intermedios). Cae al camino clásico ante cualquier fallo. False fuerza siempre el camino clásico. Ver Pipeline de frames en streaming |
ONNX_VIDEO_MAX_PIPELINE_MB |
1024 |
Presupuesto de RAM para las colas de frames en vuelo. Es global: se reparte entre todas las colas del pipeline, no es por cola |
ENABLE_FILE_LOGGING |
False |
Escribe los logs a runtime/logs/upflow.log (rotado). Apagado por defecto: en uso normal es ruido y disco. Se enciende desde Settings sin reiniciar, para que quien reporta un problema pueda reproducirlo y mandar el archivo |
CCTV_X264_THREADS |
4 |
Hilos fijos de x264 en el modo CCTV (copia de visualización y comparativo): es lo único del carril clásico cuya salida cambia con la cantidad de hilos, y el informe promete bytes reproducibles. Cambiarlo cambia los hashes de esas copias. Ver Video de cámaras de seguridad |
CCTV_FFV1_SLICES |
4 |
Slices fijos de FFV1 de la copia sin pérdida (analysis.mkv); mismo motivo que el anterior |
CCTV_MAX_STILL_FRAMES |
20 |
Tope de cuadros exactos exportados por job CCTV |
CCTV_ROI_MAX_FRAMES |
60 |
Tope de cuadros para la fusión multi-cuadro de una región (placa o cara); lo validan la pantalla, la API, la CLI y MCP |
CCTV_ROI_ECC_MIN |
0.8 |
Correlación ECC mínima (entre 0 y 1, sin incluirlos) para que un cuadro entre en la fusión de una región; los que quedan debajo se descartan y el informe los lista |
LOG_FILE_MAX_MB / LOG_FILE_BACKUPS |
10 / 3 |
Techo por archivo antes de rotar y cuántos rotados se conservan |
RESTORE_PHOTO_ENABLED |
false |
Muestra y habilita Restaurar fotos (pestaña, API, CLI y MCP). Apagado en esta versión porque sus packs de modelos todavía no están publicados |
CCTV_AI_ENABLED |
false |
Habilita el carril "AI enhancement (visual only)" del modo CCTV. Apagado en esta versión por el mismo motivo (necesita restore-core); el carril clásico y la foto multi-cuadro no dependen de él |
RESTORE_MODEL_DIR |
vendor/restore |
Carpeta de los packs restore-* de Restaurar fotos. Con los flags de arriba encendidos, bajar el pack es poder usarlo |
RESTORE_CALL_BUDGET_MS |
1200 |
Presupuesto por llamada a la GPU (límite TDR de Windows); la calibración de tiles apunta a la mitad |
RESTORE_GPU_THROTTLE_SECONDS |
0.0 |
Respiro entre tiles para que el escritorio siga fluido |
RESTORE_SESSION_CACHE_MB / RESTORE_MAX_LIVE_SESSIONS |
3000 / 3 |
Tope de VRAM estimada y de cantidad de sesiones ONNX de restauración vivas a la vez |
RESTORE_NCNN_HEADROOM_MB |
2048 |
VRAM libre mínima antes de un agrandado ncnn; si no alcanza, se liberan las sesiones de restauración |
RESTORE_MAX_INPUT_PIXELS / RESTORE_MAX_OUTPUT_PIXELS |
40000000 / 100000000 |
Topes de memoria de la foto de entrada y de la salida (con agrandado) |
RESTORE_ANALYSIS_CONCURRENCY |
1 |
Análisis de fotos simultáneos (corren en CPU) |
El Optimization Center (en el módulo Settings) detecta cinco configuraciones del sistema que afectan el rendimiento de upscaling en GPUs DirectML de AMD; tres son corregibles con un click (requieren confirmación UAC), dos son diagnóstico de solo lectura:
| Detección | Qué es | Fija automáticamente | Requiere reboot |
|---|---|---|---|
| HAGS (Hardware Accelerated GPU Scheduling) | Necesario para DirectML moderno en AMD; activa el scheduler de GPU del OS | Sí, un click | Sí |
| Disk write-cache | Caché de escritura en disco — cuando está apagado, cada PNG intermedio se sincroniza a disco antes de continuar. Su peso bajó bastante desde el pipeline de streaming (los caminos que no escriben PNGs no lo sufren), pero sigue importando en el camino clásico: NCNN, modelos HF-ONNX y cualquier job que caiga al fallback | Sí, un click | Sí |
| Defender en runtime/ | Exclusión del antivirus para runtime/ — si está activado, el scanner de Windows ralentiza I/O de frames |
Sí, un click | No |
| PCIe link | Velocidad y ancho del enlace PCIe entre la GPU y la CPU | Diagnóstico: solo lectura (no hay fix de software) | — |
| ONNX CPU fallback | Detecta qué operaciones de los modelos reales corren en CPU en vez de GPU — herramienta de diagnóstico para identificar cuellos de botella | Manual: ejecutar diagnóstico desde el panel | — |
Todos los fixes ejecutan vía elevated PowerShell (pide UAC una sola vez). El timeout de espera es CAPABILITY_FIX_TIMEOUT_SECONDS (default 120s) — si el usuario no responde al prompt UAC en ese tiempo, el fix falla sin romper nada.
La interfaz también incluye un checklist informativo para Resizable BAR y Above 4G Decoding (configuraciones BIOS):
- Resizable BAR — permite que la CPU acceda a toda la VRAM de la GPU en una sola pasada (vs. el default de 256 MB por ventana). Típicamente aparece en BIOS como "Resizable BAR" o "Smart Access Memory (SAM)" según el fabricante. Hay un checkbox para confirmar manualmente que ya lo activaste (se persiste server-side en
REBAR_CONFIRMED, con migración automática del valor que versiones viejas guardaban en el localStorage del navegador) — nunca bloquea lógica, es orientativo. - Above 4G Decoding — permite direccionar framebuffers >4GB (relevante solo si tienes >8GB de VRAM en la GPU + resoluciones extremas). Misma mecánica: checklist informativa, sin enforce automático.
Ambas están fuera del alcance de fix automatizado (requieren cambios en BIOS/firmware, no en software).
Poco, y conviene ser honestos al respecto: ReBAR acelera las escrituras CPU→GPU, y en un upscaler esa es justamente la dirección barata. La transferencia grande va al revés.
Medido en una RX 7800 XT con realesr-animevideov3-x4 sobre DirectML, desglosando un frame completo (subida del input, cómputo, readback del resultado):
| Entrada | Input subido | Output bajado | Subida | Total/frame | % que ReBAR podría tocar |
|---|---|---|---|---|---|
| 640×480 | 0.9 MB | 14.1 MB | 0.06 ms | 22.9 ms | 0.26% |
| 960×720 | 2.0 MB | 31.6 MB | 0.08 ms | 55.2 ms | 0.15% |
| 1280×720 | 2.6 MB | 42.2 MB | 0.12 ms | 73.1 ms | 0.16% |
El desglose a 960×720: subida 0.2%, cómputo 90%, readback 10%. El output pesa 16x más que el input (es un 4x: scale² en píxeles) y viaja GPU→CPU, que es un camino distinto que ReBAR no acelera.
El argumento cierra por los dos lados: si ReBAR ya está activo, la subida ya mide 0.2% del tiempo; si está inactivo, activarlo puede ganar como mucho ese 0.2%. En ambos casos es ruido frente al 90% de cómputo.
Donde sí podría notarse es en la carga inicial de modelos grandes a VRAM (los pipelines de generación pesan GB, no los 2.5 MB del upscaler de video), pero es un costo de una sola vez por sesión, no de throughput.
Conclusión: dejarlo activado no hace daño y es buena higiene general del sistema, pero no esperes ganancia medible en upscaling de video por activarlo. El checklist se mantiene como orientación, no como promesa de rendimiento.
- Retrofit IOBinding (Fase 0.2, Task 10):
ApolloRestorerganó un fast-path IOBinding para DirectML, peroAudioSrRestorer,GmfssEngineyOnnxUpscalerlo difirieron explícitamente — sus arquitecturas (AudioSR: loop DDIM con shapes dinámicas por step; GMFSS: 4 grafos con constraints frágilesORT_DISABLE_ALL; OnnxUpscaler: auditoría pendiente) necesitan análisis y retrofit dedicados en passes futuras. El código de Apollo es la referencia. - Manual BIOS Checklist (Fase 2): la comprobación de Resizable BAR/Above 4G no tiene backend de detección automática (requeriría acceso a BIOS/firmware propietario) — es un checklist guiado + confirmación manual en localStorage, nunca bloquea la lógica.
El FPS boost está deshabilitado por defecto. Para activarlo:
# 1. Descargar el motor RIFE NCNN Vulkan (fork TNTwise, incluye varios modelos v4.x)
powershell -ExecutionPolicy Bypass -File .\scripts\download-rife.ps1
# 2. En .env, habilitar la interpolación
ENABLE_INTERPOLATION=trueEl dropdown "FPS boost" siempre está visible en la UI de video (con las opciones de ALLOWED_FPS_MULTIPLIERS), pero solo funciona una vez activado: pedir un fps_multiplier > 1 (por UI o directo en POST /api/v1/video/jobs) sin ENABLE_INTERPOLATION=true o sin el binario de RIFE instalado devuelve 400.
GMFSS es un segundo motor de FPS boost — mucha más calidad que RIFE en anime, pero ~10x o más lento (medido 0.72-0.73 fps @1080p 2x en una RX 7800 XT con splat OpenCL GPU activo — ver nota de pyopencl abajo; sin él, GMFSS cae a splat por CPU y ronda 0.38fps. Pensalo como "máxima calidad, muy lento", no como reemplazo de RIFE para uso diario). Deshabilitado por defecto:
# 1. Descargar los modelos ONNX de GMFSS (~55MB, port propio: santiquiroz/port-gmfss-onnx)
powershell -ExecutionPolicy Bypass -File .\scripts\download-gmfss-onnx.ps1
# 2. En .env, habilitar GMFSS (además de ENABLE_INTERPOLATION=true, arriba)
ENABLE_GMFSS=true
# 3. Opcional pero recomendado: splat GPU vía OpenCL (~2x más rápido que CPU-only,
# ver benchmarks abajo). Sin este paso, GMFSS sigue funcionando, solo cae a CPU
# para ese sub-paso (fallback automático, con warning único).
.\.venv\Scripts\python -m pip install -e ".[gpu-splat]"Con ambos motores disponibles, el selector RIFE/GMFSS aparece en el dropdown de FPS boost de la UI; por API se elige con interp_engine=rife|gmfss en POST /api/v1/video/jobs (default siempre rife, GMFSS es opt-in por job).
La fusión GMFSS+upscale en una pasada (ENABLE_INTERP_UPSCALE_FUSION, Fase 2) midió ~1.7x MÁS LENTA que las dos pasadas a 4x/8K en una RX 7800 XT: era un generador secuencial de un solo hilo sin overlap load/compute/save. Fue eliminada y reemplazada por el pipeline de frames en streaming (ver docs/superpowers/specs/2026-07-25-stream-frame-pipeline-design.md), que conecta las etapas por colas con un thread por etapa.
Activo por defecto. Conecta decode→(interpolación)→upscale→encode por colas acotadas en memoria (frames rgb24 crudos), con cada etapa en su propio thread — sin materializar PNGs intermedios. El presupuesto de RAM es el mismo ONNX_VIDEO_MAX_PIPELINE_MB del pipeline ONNX, repartido globalmente entre todas las colas.
| Camino | Con el pipeline | PNGs eliminados |
|---|---|---|
| Sin interpolación + upscale ONNX builtin | decode→stream→upscale→stream→encode | todos |
| GMFSS + upscale ONNX builtin | decode→stream→GMFSS→stream→upscale→stream→encode | todos |
| RIFE + upscale ONNX builtin | decode→PNG→RIFE→(lee sus PNGs)→upscale→stream→encode | los de salida (los más pesados) |
| Upscale NCNN (binario) / modelos HF-ONNX | camino clásico completo | ninguno |
Anillo de readback. El reescalado ONNX builtin escribe la salida de cada cuadro directo en un anillo de buffers preasignados (tantos como cuadros en vuelo aguas abajo, más uno), en lugar de recibir de ONNX Runtime un array nuevo por cuadro (~44 MB a 720p ×4) y copiarlo. Medido en una RX 7800 XT con realesr-animevideov3-x4 fp16, 720p → 2880p, 400 cuadros y 3 corridas alternadas por lado contra la versión anterior: 50,2 → 39,7 ms por cuadro en la inferencia (−21 %) y 50,9 → 40,0 ms en el pipeline de frames, con σ de 0,6 ms contra 1,6–3,9 ms y salida bit a bit idéntica. Si ONNX Runtime rechaza el buffer, esa sesión vuelve a la copia de antes (una sola vez, con aviso en el log). La subida, el Run y el readback de cada cuadro van dentro del lock por adaptador de DirectML, igual que GMFSS. No medido: otros modelos y resoluciones, fp32, y un job completo con el encoder (que puede pasar a ser el cuello).
Ante CUALQUIER excepción del pipeline, el job cae automáticamente al camino clásico desde cero (se registra en el log y en job.metadata.streamPipelineFallback) — el job nunca falla por culpa del camino nuevo. ENABLE_STREAM_PIPELINE=false restaura el comportamiento anterior completo (incluido el raw-pipe clásico). Diseño: docs/superpowers/specs/2026-07-25-stream-frame-pipeline-design.md.
La mejora de audio está deshabilitada por defecto. Para activarla:
# 1. Descargar el binario de DeepFilterNet (~26 MB) + el modelo .rnnn para el filtro arnndn de FFmpeg (~300 KB)
powershell -ExecutionPolicy Bypass -File .\scripts\download-deepfilternet.ps1
# 2. En .env, habilitar la mejora de audio
ENABLE_AUDIO_ENHANCE=trueCon eso activado, un job de video con keep_audio=true puede pedir audio_enhance=deepfilter (red neuronal DeepFilterNet3, mejor calidad, más lento) o audio_enhance=rnnoise (filtro arnndn de FFmpeg, más liviano). Pedir audio_enhance sin keep_audio=true, sin ENABLE_AUDIO_ENHANCE=true o sin los binarios instalados devuelve 400. Omitir audio_enhance deja el audio original intacto (remux con -c:a copy).
Para lo que exporta un DVR o NVR: la cámara de la casa, del local o del edificio. La idea es ver mejor lo que ya está grabado, sin tocar el original y dejando constancia de todo lo que se hizo. Está en Mejorar → Video, con el interruptor "Security camera footage (CCTV)" arriba del panel (o directo en /enhance/video?cctv=1). Si soltás un archivo que parece de una cámara de seguridad —cabecera IMKH de Hikvision/HiLook, DHAV de Dahua, H.264/H.265 crudo, o la resolución "lite" 960×1080 o 640×720— la pantalla ofrece pasar a este modo.
Upflow no es una herramienta forense certificada y ningún laboratorio forense la validó. El informe documenta lo que hizo Upflow; no certifica autenticidad ni admisibilidad. Lo que se entrega a una autoridad es siempre el original. Nada de esto es asesoría legal.
Requisitos: solo el FFmpeg vendorizado (scripts/download-ffmpeg.ps1), con ffv1 y libx264. No baja modelos ni usa la GPU: el carril clásico corre en CPU. Si la build de ffmpeg no trae esos dos codecs, el modo queda en "needs setup" con el motivo escrito; si le falta algún filtro, ese paso aparece deshabilitado en vez de romper el job.
Qué pasa con el archivo, en orden:
- Se calcula el SHA-256 del archivo tal como llegó, antes de cualquier otra operación, y se anota la hora de recepción (UTC y local). Esa copia verificada va a
01_originalcon su nombre y no se modifica nunca. - Se re-empaqueta sin re-encodear a una copia de trabajo MKV, probando el demuxer automático, el Program Stream de Hikvision,
dhavy el stream crudo; los intentos quedan en el informe. Se decodifican 100 cuadros de prueba: si el video no se puede decodificar (por ejemplo, un export cifrado) se dice y se explica cómo reexportarlo. Upflow no descifra nada. - Índice de cuadros hasheado (número, tiempo, keyframe y tipo de cuadro) con los fps medidos, CFR o VFR, huecos, duplicados probables y GOP.
- Diagnóstico con los filtros de análisis de ffmpeg (
idet,blockdetect,blurdetect,freezedetect) más estadísticas de luma, croma y clipping: entrelazado, bloqueo, desenfoque, noche/IR, y un preset sugerido.
Presets del carril clásico — la pantalla muestra cada paso con su descripción en lenguaje llano y un enlace a la documentación del filtro; los parámetros se tocan en "Advanced":
| Preset | Para qué | Pasos |
|---|---|---|
| Day | Color con bloqueo de compresión moderado | aspecto (si el cuadro es anamórfico), desentrelazado bwdif (si está entrelazado), deblock suave, denoise hqdn3d |
| Night / IR | Oscuro o infrarrojo, sin color real | igual, con deblock fuerte, denoise temporal atadenoise, escala de grises y gamma 1,2 |
| Analog (interlaced) | Cámaras analógicas entrelazadas | desentrelazado siempre, deblock suave, hqdn3d |
| Low-res sub-stream | Sub-stream de 704×576 o menos | Day más ampliación ×2 por vecino más cercano, que no inventa píxeles |
Ningún preset aplica nitidez ni Lanczos: generan halos y píxeles que no estaban. Stabilize (vidstabdetect + vidstabtransform, solo en el carril clásico y en ningún preset) corre en dos pasadas: la primera mide el movimiento y lo guarda en un .trf de texto, la segunda mueve cada cuadro con esa medición; los bordes que quedan al descubierto salen en negro, sin zoom ni píxeles de otros cuadros, y el informe lo anota como limitación. Las dos pasadas usan un solo hilo de OpenMP, porque con varios el .trf cambiaba de una corrida a otra; la vista previa de un cuadro no la muestra. Lens correction (solo en el carril clásico y en ningún preset) endereza las líneas que curva el lente con lenscorrection (k1, k2) o aplana un ojo de pez con v360 (FOV del lente, FOV de la vista, yaw y pitch) al mismo tamaño de cuadro; las dos toman el píxel más cercano, así que mueven píxeles pero no calculan valores nuevos, y el informe lo anota como limitación. El selector "Starting point" trae valores por lente (6 mm, 4 mm, 2,8 mm, 2,0-2,2 mm, ojo de pez de 180° y 190°) sin calibrar contra exports reales: se ajustan en la vista previa hasta que las rectas se vean rectas. El texto en pantalla se restituye sin doblar desde el original. El carril clásico rechaza en el backend cualquier paso de IA, la interpolación y todo filtro fuera de su lista blanca, aunque llegue por la API.
Texto en pantalla (fecha, hora, cámara): el job no arranca hasta que confirmás las cajas del texto sobre un cuadro donde se lea la hora, o elegís "No on-screen text". Para Hikvision se proponen las dos cajas típicas (hora arriba a la izquierda, cámara abajo a la derecha), sin confirmar. Los píxeles de esas cajas salen del original, así que un filtro temporal no puede emborronar la hora ni mezclar dos segundos distintos. Un chequeo de contraste y de píxeles quietos avisa si una caja no parece texto, sin bloquear.
Recorte y cuadros: el recorte es por número de cuadro (con su timecode). Los cuadros que elijas salen como PNG del original y del procesado, extraídos por número exacto (select=eq(n,N), o saltando al keyframe anterior y verificando el PTS contra el índice) y con el SHA-256 del cuadro decodificado. Antes de lanzar el job podés ver los filtros aplicados alrededor del cuadro actual, encima del original.
Qué te llevás (outputs/<jobId>.cctv/, y lo mismo dentro del .zip de entrega, sin compresión, bajo <caso>_<fecha>_upflow/):
01_original/ copia verificada, idéntica byte a byte a la recibida, con su nombre
02_processed/ <nombre>__upflow-clarify__<jobId>.mkv sin pérdida (FFV1), para examinar
<nombre>__upflow-clarify__<jobId>.mp4 copia de visualización (H.264)
<nombre>__upflow-clarify__<jobId>.trf movimiento medido por Stabilize (solo si se usó)
03_comparisons/ comparison.mp4: original | procesado, con contador de cuadros
04_stills/ cuadros exactos, original y procesado
frame_index.csv índice de cuadros del original
report.html informe legible (se imprime a PDF desde el navegador), con "AI used: NO/YES"
report.json el mismo informe, validable contra docs/schemas/cctv-report-v1.schema.json
SHA256SUMS.txt hashes de todo, en el formato de sha256sum -c
reproduce.cmd rehace 02_processed desde 01_original y compara contra SHA256SUMS.txt
README.txt español e inglés: qué es cada carpeta, cómo verificar, cómo entregar
Los nombres procesados nunca repiten el del original. El informe lleva cada paso en orden con su descripción, sus parámetros y el argv exacto de ffmpeg, la versión y el SHA-256 del binario de ffmpeg, las extensiones de la CPU, el recorte, las cajas del texto en pantalla, el clipping antes y después, las limitaciones que corresponden a los pasos usados y los datos del caso si los cargaste (etiqueta, operador, marca, modelo y número de serie del grabador, cámara, desfase del reloj, cómo y cuándo se exportó el video). "Check files are unchanged" vuelve a hashear la carpeta y dice qué cambió o falta; detecta cambios accidentales desde que Upflow recibió el archivo, no prueba que la grabación sea auténtica, y quien altere los archivos también puede regenerar SHA256SUMS.txt.
Reproducible bit a bit con la misma build de ffmpeg y las mismas extensiones de CPU: los hilos de x264 y los slices de FFV1 son fijos (CCTV_X264_THREADS, CCTV_FFV1_SLICES), los codificadores y el muxer van con +bitexact y el reescalado con accurate_rnd+full_chroma_int+bitexact. Otra build u otra CPU pueden dar bytes distintos, y reproduce.cmd lo dice. Salvedad abierta: con la CPU muy cargada, la copia H.264 (.mp4) salió distinta en 13 de 280 codificaciones del mismo analysis.mkv (los últimos cuadros, también con x264 de un hilo); la copia FFV1 (.mkv) salió idéntica siempre. Si reproduce.cmd marca DIFFERENT solo en el .mp4, volvé a correrlo con la PC tranquila; la causa todavía no está identificada. Pista medida sin confirmar: sobre un analysis.mkv estabilizado, x264 dio otra salida en 6 de 150 codificaciones con sus rutas AVX-512 y en 0 de 150 limitado a AVX2 (asm=avx2).
Reproducir desde un informe, dentro de la app (solo API por ahora): POST /api/v1/video/cctv/reproduce con {token, report, operatorName?} recibe el report.json de un job del carril clásico y el token de la sesión donde se cargó de nuevo el original. El informe se trata como un input no confiable: se valida contra el esquema, el archivo cargado tiene que tener el SHA-256 del original que nombra el informe (si no, 400 cctv.error.reproduceSourceMismatch), y cada paso vuelve a pasar por la lista blanca del catálogo; si al resolverlo los parámetros no dan exactamente los del informe, responde cctv.error.reproduceStepsMismatch. Del informe se toman solo ids, filtros y parámetros, nunca los argv. Arma un job clarify común (con el recorte, las cajas del texto en pantalla, los cuadros exportados y el caso del informe; el operador no se hereda: es el operatorName del pedido o queda vacío, y el informe nuevo lleva case.reproductionOf con la fecha, la versión y el SHA-256 del report.json de origen tal como Upflow lo escribe, que coincide con su SHA256SUMS.txt si nadie lo editó, y lo dice arriba en report.html: una reproducción nunca pasa por un procesamiento original) y avisa antes de correr si la versión de Upflow, la build de ffmpeg o las extensiones de CPU no son las del informe. Al terminar, GET /api/v1/video/jobs/{id}/reproduce compara el original, el framehash de cada cuadro exportado, los archivos que compara reproduce.cmd (.mkv, .mp4 y .trf), la build y la versión. La versión de Upflow va escrita dentro de los archivos procesados, así que con otra versión sus hashes cambian aunque los cuadros sean idénticos: por eso el resultado separa framesIdentical de identical. Todavía no reproduce la foto multi-cuadro ni el carril IA, que no es repetible.
Carril "AI enhancement (visual only)" (apagado en esta versión, CCTV_AI_ENABLED=false: la tarjeta aparece deshabilitada con el motivo y POST /cctv/jobs con task=enhance responde 400 cctv.error.aiLaneDisabled): procesa siempre por stream, sin cuadros intermedios en disco. Los pasos clásicos de geometría y limpieza van en el decode; después una sola etapa corre el desbloqueo DRUNet (opcional), el reescalado ONNX (opcional, con su etiqueta "Generative (invents texture)" o "Non-generative") y vuelve a poner las cajas del texto en pantalla, decodificadas en una rama con solo geometría y desentrelazado; niveles, redimensión y nitidez van en el encode sin tocar esas cajas. Necesita una GPU DirectML sana y el pack restore-core; en CPU se bloquea y muestra cuánto tardaría. Todo lo que produce lleva una banda bilingüe visible ("AI-enhanced visualization") y una marca dentro de la imagen, el informe en modo ai-visual con el modelo y el SHA-256 de cada archivo usado, y XMP en los cuadros exportados: sirve para mirar, no como prueba. Mientras restore-core no tenga artefactos publicados, el carril aparece como no disponible aunque se encienda el flag. Medido en DirectML (RX 7800 XT, DRUNet fp16 de cuadro entero, clip sintético de 2 min con ruido y bloques): ~5,2 cuadros/s a 1080p y ~9,6 a 960×1080 (unos 4,8 y 2,6 min por minuto de video a 25 fps), ~3,5 cuadros/s a 1080p con el ×2 de animevideov3; el parpadeo en zonas quietas bajó respecto de la entrada y la cancelación deja la GPU usable. Todavía no hay una medición de cuánto mejora con clips reales, así que no se promete ninguna mejora. Tampoco trae comparativo ni paquete ZIP todavía.
"Plate or face still (multi-frame)": alinea la misma región (placa o cara) en hasta CCTV_ROI_MAX_FRAMES cuadros y los combina por mediana o media recortada, en CPU, sin IA y de forma determinista. En la pantalla se dibuja la caja sobre un cuadro y se eligen el primero y el último del rango; la referencia es el cuadro donde se dibujó la caja o el que propone "Suggest reference frame" (el más nítido dentro de la caja sin zonas quemadas). El resultado muestra cuántos cuadros aportaron información nueva, la imagen combinada junto a la referencia ampliada sin suavizado, el mapa de acuerdo entre cuadros y los avisos (densidad, saturación, copias del GOP). Reduce ruido y a veces recupera algo de detalle, pero no crea detalle más fino que el grabado, y cuánto gana frente al mejor cuadro solo todavía no está medido con clips reales. Deja informe y SHA256SUMS.txt, pero todavía no un paquete ZIP.
"Redacted copy (for sharing)": una copia para compartir con caras, placas o pantallas tapadas (research-forense §3, guía de la SIC y Ley 1581). Las cajas se dibujan a mano sobre los cuadros: moverlas en otro cuadro agrega una keyframe, entre keyframes la caja avanza en línea recta y cada caja tiene su tramo de cuadros. Se tapa con una caja sólida negra (por defecto: es lo único irreversible, y lo indicado para placas, textos y pantallas), pixelado (pocas celdas sobre el lado largo, con la grilla anclada al cuadro para que la caja en movimiento no muestree con otra fase en cada cuadro) o desenfoque, en CPU y sin IA; en video el pixelado y el desenfoque pueden dejar reconocible a un sujeto que se mueve, y así lo dicen la pantalla y redaction.json. Las cajas dibujadas sobre todo el recorte lo siguen si después se amplía o se mueve, la pantalla nombra los cuadros del recorte que ninguna caja tapa y no deja empezar con una caja fuera del recorte (el backend la rechaza con cctv.error.redactionFrames); no hay detector automático de caras, así que hay que recorrer el clip y revisar cada caja. La copia es H.264 sin audio, con la banda bilingüe "REDACTED COPY / COPIA ANONIMIZADA" fuera de la imagen, una marca dentro y el comentario del contenedor; va en 05_redacted/ con redaction.json (cajas, estilo, uncoveredFrames con los tramos de la copia sin ninguna caja, SHA-256 del original y de la copia) y SHA256SUMS.txt, y nunca entra en el paquete de entrega: el original se entrega aparte. Solo acepta un recorte (sin filtros ni cajas de OSD); los cuadros conservan su orden y su conteo, pero se reproducen a ritmo constante.
Lo que ningún filtro recupera: detalle que el sensor nunca registró (una cara de 15 px, letras de 3 px de alto, el fondo que H.265+ descartó a propósito), píxeles saturados (la placa quemada por el IR, faros), cuadros que no se grabaron (12–15 fps) y la información que el encoder copió de un cuadro al siguiente. Upflow no identifica a nadie: no hay OCR de placas ni reconocimiento de caras.
Retención: como todo job, los resultados se borran a las OUTPUT_TTL_HOURS (24 por defecto). Bajá el paquete de entrega para conservarlos.
Lista armada a partir de SWGDE 17-V-002 y 20-V-002, la sentencia SP248-2025 de la Corte Suprema, la Ley 906 de 2004 y la Ley 1581 de 2012. No es asesoría legal. El README.txt del paquete trae la versión corta.
- Actuá rápido. El DVR sobrescribe lo viejo según su disco. Mirá en su menú cuál es la grabación más antigua y, si lo permite, bloqueá o protegé el tramo (los equipos Hikvision tienen "lock" de archivos; no está verificado para todos los modelos).
- Medí el desfase del reloj. Sacale una foto con el celular a la pantalla del DVR mostrando su hora junto a la hora legal (horalegal.inm.gov.co) y anotá la diferencia en segundos. Va en los datos del caso.
- Exportá en el formato nativo desde el propio DVR a una USB nueva: el MP4 o el PS/
.davdel fabricante, con margen antes y después del hecho, de todas las cámaras que sirvan. Si se puede, exportá también el reproductor del fabricante. - Hasheá enseguida. Cargar el archivo en Upflow calcula su SHA-256 antes que nada (equivale a
Get-FileHash -Algorithm SHA256) y anota la hora. - Guardá una copia maestra de solo lectura y entregá una copia verificada (mismo hash) en un medio nuevo, sin recortarla, recomprimirla ni mejorarla: es
01_original. - Acta de entrega: quién extrajo, cuándo, de qué equipo (marca, modelo, número de serie, canales), el desfase del reloj, la lista de archivos con tamaño y SHA-256, y firma. La autoridad que recibe hace su propio registro de cadena de custodia.
- Lo procesado va aparte. Si querés aportar la versión aclarada, entregala como anexo separado, rotulado "Versión procesada — no es la grabación original", con
report.htmlySHA256SUMS.txt, que la vinculan con el original. Nunca en lugar del original. - No lo difundas antes. Publicarlo puede afectar la investigación y los datos de terceros (Ley 1581). Para compartir con vecinos o en redes hace falta anonimizar a terceros, y eso nunca va en la copia para la autoridad.
- Denuncia: la Policía tiene el portal ¡A Denunciar!. No está verificado que acepte video adjunto; lo prudente es ofrecerle la entrega física al investigador asignado.
Todo bajo /api/v1/video, con errores {"detail": {"key": "cctv.error.*", "reason": "..."}}:
POST /cctv/analyze—fileoupload_token; hashea, ingesta y diagnostica.200con el análisis (token,sourceSha256,receivedAt, contenedor, índice, GOP, calidad,suggestedPreset,proposedStepspor carril, pasos no disponibles, avisos) o202conanalysisJobIdsi no termina en la ventana sincrónica →GET /cctv/analysis/{analysisJobId}.GET /cctv/presets— presets y pasos, con la disponibilidad de cada filtro en esta build.GET /cctv/{token}/preview?frame=N— el cuadro N decodificado (PNG); constepsaplica solo pasos clásicos alrededor de ese cuadro.POST /cctv/{token}/osd-check—{"boxes": [[x, y, w, h]], "frame": N}→ si cada caja parece texto.POST /cctv/{token}/roi/reference—{"firstFrame", "lastFrame", "box": [x, y, w, h], "steps"}(solodeinterlaceydeblock) →{"referenceFrame"}, el cuadro que propone "Suggest reference frame".POST /cctv/jobs— cuerpo JSON camelCase estricto (token,task: "clarify" | "enhance" | "roi_fusion" | "redact",preset,steps,osdBoxes+osdBoxesConfirmedonoOsd,trim: [primero, último]inclusivo,stillFrames,caseLabel,operatorName,acquisition;roiconfirstFrame,lastFrame,referenceFrame,box,kind,scaleymethodsolo enroi_fusion;modelId,scaleydevicesolo enenhance;redactionconstyle(fillpor defecto |pixelate|blur) ytracks[{firstFrame,lastFrame,keyframes[{frame,box}]}] solo enredact) →202con un job de la familiavideo; se sigue conGET /jobs/{id}como cualquier video, ycctv.artifactslista los archivos.GET /jobs/{id}/artifacts/{name}— un archivo del resultado (el informe se abre en el navegador, el resto se descarga);POST /jobs/{id}/verify→{ok, checked, mismatches, missing}.
Para agentes: upflow cctv probe|clarify|roi|verify y las tools MCP upflow_cctv_*, en docs/agent-usage.md.
Además de imagen y video, Upflow tiene un apartado de Audio propio (ruta /audio): subís un archivo de audio (wav/mp3/flac/m4a/ogg/opus), elegís la mejora y descargás el resultado. La cadena es entrada → [limpieza] → [denoise] → [restore] → [voz] → [acabado] → salida, cada paso opcional y todos combinables en el mismo trabajo (la única excepción es la separación de stems, que corre sola porque entrega dos archivos).
-
Limpieza (cadena) — saca defectos de una grabación encadenando modelos de máscara del catálogo de UVR, y devuelve un solo archivo. Sirve para cualquier audio, música incluida: es la sección para limpiar música. Campo
cleanup_steps(CSV de ids); combinable con denoise/restore/voz/acabado en el mismo trabajo. Ver "Cadena de limpieza" abajo. -
Denoise (para VOZ) — quita ruido de fondo:
deepfilter(DeepFilterNet3) ornnoise. Los dos están entrenados con habla: separan una voz de su ruido muy bien, y en música tratan a los instrumentos como ruido y pueden apagarlos — para música, usar la cadena de limpieza. Es el mismo motor que ya se usa en video (ver sección anterior); requiereENABLE_AUDIO_ENHANCE=true+download-deepfilternet.ps1. -
Restore (EXPERIMENTAL) — dos motores, elegibles por job:
apollo: reconstruye la banda de agudos perdida por compresión de códec (audio de WhatsApp/Telegram/redes). Rápido y liviano (~74 MB). RequiereENABLE_AUDIO_RESTORE=true+scripts/download-apollo.ps1.audiosr: super-resolución de audio general por difusión latente (cualquier banda → 48 kHz, UNet de 258M params). Techo de calidad muy superior a Apollo pero ~2 min de proceso por minuto de audio en GPU (50 pasos DDIM con CFG). Port ONNX propio — el primero conocido de AudioSR: santiquiroz/port-audiosr-onnx. RequiereENABLE_AUDIOSR=true+scripts/download-audiosr-onnx.ps1. El script acepta-Precision fp32|fp16, y el botón de la tarjeta elige solo: fp16 si el dispositivo por defecto es GPU (1.26 GB en vez de 2.51 GB, 9% más rápido, salida a 59.4 dB SI-SDR de la fp32 — inaudible), fp32 si es CPU, porque el EP de CPU tiene muchos menos kernels fp16. Correr un pack fp16 en CPU falla a propósito, antes de crear la sesión, diciendo que hay que reinstalarlo en fp32. Medición completa:docs/superpowers/specs/2026-08-17-audiosr-fp16-findings.md.
Ambos motores son ONNX multi-provider (corren en cualquier GPU DirectX12 —AMD/NVIDIA/Intel— o CPU, igual que los modelos de imagen HF). Si un modelo no está instalado, ese modo simplemente no aparece — la app nunca se rompe por esto. Preservan estéreo/surround: en vez de downmixear a mono antes de restaurar, decodifican Mid/Side (restauran solo el Mid, el Side queda intacto) en estéreo, y en 5.1/7.1 restauran frente/rears por par M/S + centro directo + LFE intacto, con RMS-match por canal contra el original al final; un layout de canales no reconocido cae a mono con warning explícito (nunca en silencio).
-
Formato de salida —
output_format: wav|flac|mp3|m4a, defaultflac(sin pérdida, ~50% más liviano que WAV).wavpara compatibilidad con editores viejos;m4a(AAC) es el más compatible con teléfonos y con el ecosistema Apple;mp3lo lee todo, incluso equipos viejos. Paramp3/m4a,lossy_quality: maximum|balanced|compact(defaultmaximum= MP3 320 kbps / AAC 256 kbps) elige el bitrate; enwav/flacse ignora. Ogg/Opus se aceptan de entrada pero no se ofrecen de salida: Vorbis pierde contra Opus en calidad y contra MP3/AAC en compatibilidad, y Opus gana en eficiencia pero pierde justo en el eje por el que existe esta selección. -
Convertir de formato sin procesar nada — un job sin ningún paso (sin limpieza, denoise, restauración, voz, mastering ni separación) es válido si el formato de salida es distinto al del archivo: convierte en una sola pasada de ffmpeg desde el original, conservando la frecuencia de muestreo y la profundidad de bits hasta donde el destino lo permita. Un FLAC de 44.1 kHz / 24 bits sale como WAV de 44.1 kHz / 24 bits, no como los 48 kHz / 16 bits a los que decodifica el camino de procesamiento (esa tasa la exigen DeepFilterNet, RNNoise y los separadores; una conversión pura no pasa por ahí). Lo que el destino no admite queda registrado en
job.metadatay se muestra en el detalle del trabajo: un resample forzado (96 kHz → MP3 sale a 48 kHz) enconversionResampled, un downmix forzado (5.1 → MP3 sale estéreo) enconversionDownmixed, una profundidad recortada enconversionBitDepthReduced, y un remux sin recodificar enconversionCopied— nunca en silencio. Pedir el mismo formato que ya tiene el archivo sin ningún paso devuelve400: no hay nada que hacer. -
Separación de stems (karaoke y limpieza) — parte la mezcla en dos archivos con un modelo del catálogo de Ultimate Vocal Remover. Corre solo (los demás pasos se aplicarían a un stem ambiguo): pedilos en un segundo trabajo sobre el stem que quieras. Cada modelo se baja por separado con
scripts/download-karaoke.ps1 -Model <id>(o con el botón de descarga que la propia UI muestra al lado del modelo que falta); tener uno instalado alcanza para habilitar el modo.Grupo Id Qué hace Stems (el 1º es el que baja downloadUrl)Karaoke inst_hq_3Saca la instrumental; la voz es el resto instrumental+vocalsKaraoke voc_ftSaca la voz; la instrumental es el resto instrumental+vocalsKaraoke mel_band_roformer_kimMáxima calidad, ~20x más lento; saca la voz, la instrumental es el resto instrumental+vocalsLimpieza reverb_hqSaca la cola de reverb; la pista limpia es la resta dry+wetLimpieza deecho_normalEco moderado, sin tocar el resto de la señal no_echo+echoLimpieza deecho_aggressiveEco fuerte; pega más duro y puede apagar la señal no_echo+echoLimpieza deecho_dereverbEco y reverb de sala en una pasada no_reverb+reverbLos tres
deecho_*son de FoxJoy, distribuidos por el canal oficial de descargas de UVR y exportados a ONNX por un port propio y público: santiquiroz/port-uvr-deecho-onnx (MIT, paridad medida contrapython-audio-separator: 61-65 dB SI-SDR). Son otra arquitectura (VR 5.1 CascadedNet) que los MDX-Net, pero eso no se elige: el catálogo es una sola lista y el backend resuelve el motor por el modelo que pediste. Medido en una RX 7800 XT (dml:0): ~21x tiempo real — 5 minutos de audio en 14 segundos.mel_band_roformer_kimes el carril de máxima calidad, y es una elección explícita, no el default: es Mel-Band RoFormer de KimberleyJSN (MIT — el roformer vocal con mejor SDR que declara licencia permisiva: 10.98 en el multisong de MSST, por encima del BS-RoFormer de viperx, que además no declara ninguna), exportado a ONNX por otro port propio y público: santiquiroz/port-bs-roformer-onnx (MIT). Tercera arquitectura del catálogo, misma lista única. Lo que hay que saber antes de elegirlo, y por eso la UI lo advierte al lado del modelo: pesa 931 MB, necesita ~2,3 GB libres en el dispositivo (el grafo fp32 más un intermedio de atención de ~1,3 GB — si no los hay, el trabajo falla al cargar con un mensaje que lo dice, no a mitad de camino) y cuesta ~20x lo queinst_hq_3. Medido de punta a punta en una RX 7800 XT (dml:0, sesión caliente): 50 s de GPU por minuto de audio, contra 2,5 s deinst_hq_3. En CPU no es un carril viable (0,69x tiempo real, más lento que reproducir el archivo). Mismo lugar de producto que GMFSS en video: elegilo cuando la calidad de la separación importa más que la espera.La descarga se pide por stem:
GET /api/v1/audio/jobs/{id}/download?stem=<id>(sinstemsirve el primero). La respuesta del job traestems[]con las dos URLs ya etiquetadas. El contrato de dos salidas es exclusivo de este modo: la cadena de limpieza entrega un archivo y no traestems[].
Los modelos del grupo "Limpieza" no son separadores aunque compartan motor: un separador parte una mezcla en dos cosas que querés, y estos quitan un defecto — entra audio, sale el mismo audio sin ruido, sin eco o sin reverb. Por eso además de correrse sueltos (modo separación, para escuchar qué sacan) se pueden encadenar, que es el flujo que la gente hace a mano en UVR.
POST /api/v1/audio/jobs con cleanup_steps=<csv de ids> corre una pasada por id y devuelve un archivo: el audio limpio. Los stems removidos de las pasadas intermedias no se guardan.
-
El orden lo fija el catálogo, no el request.
denoise→deecho_*→reverb_hq, siempre, mandes los ids en el orden que los mandes. Tiene causalidad: el ruido de banda ancha está en todo el espectro y en todo el tiempo, así que confunde a los modelos que vienen después; el eco son reflejos discretos y se modelan mientras sigan siendo copias reconocibles; la reverb es la cola difusa que queda cuando los reflejos ya no se distinguen, y sacarla primero le borraría al de-echo el material del que deduce los reflejos. -
Exclusión por familia.
deecho_normalydeecho_aggressiveson el mismo modelo en dos intensidades: elegí uno.deecho_dereverbhace eco y reverb en una pasada, así que excluye a los dos de-echo y areverb_hq. Una combinación redundante devuelve400nombrando el par — no se normaliza en silencio, porque elegir un ganador entre dos intensidades sería inventar una decisión de calidad que es del usuario. -
Se combina con
denoise/restore/voice_steps/masteren el mismo trabajo. Corre después del decode y antes de todos ellos: limpiar antes de reconstruir y de nivelar. -
Cada pasada es con pérdida (son máscaras: descartan señal y no la devuelven). Desde la tercera, el job marca
metadata.cleanupOverprocessedy la UI avisa que el resultado puede sonar sobreprocesado. No bloquea. -
separate=true+cleanup_stepsdevuelve400: la separación entrega dos archivos y la cadena uno, así que la combinación no define qué se entrega. Encadenalo en dos trabajos. -
El catálogo, en orden de ejecución, sale de
GET /api/v1/audio/capabilities→cleanupSteps[](id,name,family,covers,installed,descriptionKey) máscleanupOverprocessingThreshold.coverses lo que le permite a un cliente aplicar la exclusión sin hard-codear ids.Medido en una RX 7800 XT (
dml:0), cadena de 2 pasadas sobre 12 s de audio estéreo 44,1 kHz: 4,44 s totales — 0,10 s de decode, 2,17 s la pasada dedenoise, 2,21 s la dedeecho_normal. Cada pasada es una inferencia completa sobre el archivo entero, así que el costo es lineal en la cantidad de pasos.
# Restore experimental: descargar el modelo Apollo (~74 MB) y habilitarlo
powershell -ExecutionPolicy Bypass -File .\scripts\download-apollo.ps1
# en .env: ENABLE_AUDIO_RESTORE=trueAPI: POST /api/v1/audio/jobs (multipart: file, cleanup_steps? (CSV), denoise?, restore?, voice_steps? (CSV), master?, output_format? default flac, lossy_quality? default maximum, device?; todos los pasos son opcionales: solo file + un output_format distinto al del archivo ya es una conversión válida) → 202; GET /api/v1/audio/jobs/{id} (estado + progreso), .../download (resultado), GET /api/v1/audio/capabilities (qué motores están instalados; restoreModes lista los modos listos). El mismo restore=apollo|audiosr se puede pedir en un job de video vía el campo audio_restore (con keep_audio=true), aplicado después del denoise; el formato de salida de esa pista restaurada se controla con audio_output_format (ver "Crear un job de video" arriba).
Nota experimental: el restore es un port ONNX del modelo Apollo (ver
docs/y la guía del port). Funciona y es multi-provider, pero la calidad de reconstrucción y el rendimiento GPU aún se están evaluando — por eso va detrás de un flag y con badge "Experimental" en la UI.
Guía completa: docs/agent-usage.md. Lo mínimo:
.venv\Scripts\pip install -e . # deja `upflow` y `upflow-mcp` en .venv\Scripts
upflow health --json # ¿GPU, pack ncnn, modelos?
upflow upscale --in foto.png --out foto-2x.webp --model realesrgan-x4plus --scale 2 --json
upflow models --json # ids válidos para --model
upflow restore --in escaneo.tif --out foto.png --steps repair,denoise,tone --jsonupflow upscalecorre en proceso, sin servidor;--jsonimprime una sola línea JSON al final (ok,output,width,height,model,device,scale,nativeScale,tile,seconds). Códigos de salida:0ok,2argumentos,3modelo no instalado,4dispositivo,5fallo de inferencia. Sin prompts; las descargas exigen--yes. Formatos:png/jpg/webp(motor) yjxl/avif(ffmpeg vendorizado).upflow cctv probe|clarify|roi|verifyhace lo mismo con video de cámaras de seguridad (carril clásico, CPU); ver docs/agent-usage.md.- Mismo input + mismos parámetros ⇒ mismos bytes (id de job = hash del contenido y los parámetros, sin nombres temporales aleatorios).
- MCP:
claude mcp add upflow -- upflow-mcp --autostart(Claude Code) o[mcp_servers.upflow] command = "upflow-mcp" args = ["--autostart"]en~/.codex/config.toml(Codex). Con--autostartlevanta el servidor si no está; sin servidor las tools de imagen corren in-process.
Upflow expone toda su funcionalidad como tools MCP (Model Context Protocol) para que agentes de IA (Claude Code, Claude Desktop, o cualquier cliente MCP) puedan reescalar, transcribir, generar y procesar medios directamente.
- 70 tools que cubren la API entera: upscale de imagen/video, restauración de fotos, video de cámaras de seguridad (CCTV, carril clásico), audio (denoise/restore/master), transcripción/doblaje, descargas (yt-dlp), generación de imágenes/video, TTS, 3D imprimible, modelado 3D con Blender, edición de imagen (seleccionar por clic, insertar objeto), reparación de mallas, prompts guardados, conversión y optimización de modelos, y ajustes/diagnóstico del sistema.
upflow_init_imagesube una imagen y devuelve su token: es la puerta de entrada de todo lo que parte de una imagen —img2img, inpaint, selección por clic, insertar objeto, foto a malla—, que antes solo existía para quien usaba la pantalla.upflow_segment_objectno reenvía lo que devuelve la ruta:/editor/segmentcontesta un PNG crudo, inservible para encadenar, así que la tool vuelve a subir la máscara y entrega el token que consumeupflow_insert_object.- Restauración de fotos:
upflow_restore_analyze(diagnóstico y punto de partida propuesto),upflow_restore_photo(con los pasos deproposedSteps) yupflow_restore_recompose(re-mezcla las caras de un job terminado sin volver a correr modelos). Detalle en docs/agent-usage.md. - Modelo de jobs unificado:
upflow_job_status/upflow_wait_job/upflow_cancel_job/upflow_download_resultfuncionan igual para cualquier familia (image | video | audio | generation | transcribe | download | shape3d). - Las tools de creación aceptan rutas de archivo locales y resuelven la subida (multipart o staging por token) por sí solas.
- Es un cliente HTTP fino: con el servidor corriendo, MCP y la web UI ven exactamente los mismos jobs. Sin servidor,
upflow_upscale_image,upflow_list_modelsyupflow_healthcaen al modo in-process (UPFLOW_MCP_MODE=auto|server|inprocess, o--mode), yupflow-mcp --autostartlo levanta solo. - Tools con el mismo contrato que la CLI
upflow:upflow_health,upflow_upscale_image(ahora contile_size/tile_overlap),upflow_upscale_image_headless,upflow_list_models,upflow_preflight_upscaler,upflow_install_upscaler(repo_id, confirm=true)(las in-process;upflow_model_preflight/upflow_install_modelsiguen siendo las variantes por servidor, multi-kind).
Config para un cliente MCP (ej. .mcp.json de Claude Code):
{
"mcpServers": {
"upflow": {
"command": "C:/ruta/a/upflow/.venv/Scripts/python.exe",
"args": ["-m", "app.mcp"],
"cwd": "C:/ruta/a/upflow",
"env": { "UPFLOW_URL": "http://127.0.0.1:8090" }
}
}
}Variables: UPFLOW_URL (default http://127.0.0.1:8090); con AUTH_MODE=multi, UPFLOW_USERNAME/UPFLOW_PASSWORD hacen login automático. En modo single-user (default) no hace falta nada. También queda el script upflow-mcp instalado por pip install -e .: claude mcp add upflow -- upflow-mcp --autostart lo registra en Claude Code de una.
Flujo típico de un agente: upflow_status → upflow_upscale_image(file_path=..., destination_path=...) (espera y descarga en un solo paso) o, para videos largos, upflow_upscale_video(...) → upflow_wait_job → upflow_download_result.
Upflow chequea en silencio si hay una release más nueva en GitHub y, si la hay, muestra un banner discreto arriba de la UI ("New version X available") con link a la release. El chequeo es opcional y a prueba de fallos: si no hay red, hay timeout o GitHub responde con rate-limit (403), el endpoint igual devuelve 200 con updateAvailable=false y un campo error — el banner simplemente no aparece y la app nunca se rompe por el chequeo. El resultado se cachea en memoria (UPDATE_CHECK_TTL_SECONDS, default 3600s) para no pegarle a la API de GitHub en cada request. El banner se puede descartar por versión: una vez descartado, esa versión no vuelve a aparecer, pero una versión más nueva sí.
GET /api/v1/update-check→{ currentVersion, latestVersion, updateAvailable, releaseUrl, publishedAt, checkedAt, error }(camelCase). Acepta?force=truepara saltar el cache.
Si un chequeo falla justo cuando el cache expira, el banner no desaparece: mientras hubo un resultado bueno en la sesión, el servicio sigue sirviéndolo (un parpadeo de red no oculta una actualización real). Un error sin ningún resultado bueno previo se cachea solo UPDATE_ERROR_RETRY_SECONDS (default 300s) para reintentar pronto, no por el TTL completo.
GET /api/v1/update-check→{ currentVersion, latestVersion, updateAvailable, releaseUrl, publishedAt, checkedAt, error }(camelCase). Acepta?force=truepara saltar el cache.
Reusar el patrón en otro proyecto: el chequeo no tiene nada hardcodeado a Upflow, así que se reusa cambiando dos variables de .env:
UPDATE_REPO→ el repo destino, con formatoowner/nombre(defaultsantiquiroz/upflow).UPDATE_PACKAGE_NAME→ el nombre del paquete instalado cuya versión se compara contra eltag_namede la release (importlib.metadata.version(...), con fallback al[project] versiondelpyproject.toml). También define el User-Agent del request.
Toggles: UPDATE_CHECK_ENABLED (default true) apaga el chequeo por completo, y UPDATE_API_TIMEOUT_SECONDS (default 5.0) acota cuánto espera a GitHub.
Backend (pytest):
# instalar dependencias de desarrollo (pytest, pytest-asyncio) una sola vez
.\.venv\Scripts\python -m pip install -e ".[dev]"
# correr toda la suite
.\.venv\Scripts\python -m pytest
# un archivo o test puntual
.\.venv\Scripts\python -m pytest tests/test_health.py::test_health_endpoint
# regresion de costuras de tiling (usa el binario ncnn real si esta en vendor/, si no lo salta)
.\.venv\Scripts\python -m pytest tests/test_seams.py
# con cobertura (requiere pytest-cov: pip install pytest-cov)
.\.venv\Scripts\python -m pytest --cov=app --cov-report=term-missing
# tests que ocupan la GPU: viven en tests/gpu (fuera de la colección por defecto), de a uno
$env:UPFLOW_GPU_TESTS = "1"; .\.venv\Scripts\python -m pytest tests/gpu/<archivo>.pyFrontend (vitest):
cd frontend
npm install
npm test # correr toda la suite una vez
npm run test:watch # modo watchBrowser
│
SPA de React (frontend/, build de Vite servido desde frontend/dist/)
│
FastAPI (app/) ── sirve la SPA en "/" (fallback de rutas cliente) + routers REST en /api/v1
│
Cola de jobs por tipo (imagen/video) ── workers async + semáforo de GPU compartido
│
┌────┴──────────────────┐
│ │
Motor de imagen Pipeline de video (FFmpeg)
(Real-ESRGAN extraer frames → upscale por lote →
NCNN Vulkan) interpolar con RIFE/GMFSS (opcional) → re-encode + audio
El motor de upscaling vive detrás de una interfaz UpscaleEngine (app/services/engines/base.py), así que el backend Vulkan es un componente reemplazable. Un RetentionSweeper en background borra outputs y jobs vencidos según OUTPUT_TTL_HOURS. La SPA (frontend/) se compila una sola vez a frontend/dist/ — en release, scripts/package-release.ps1 corre npm ci && npm run build antes de empaquetar el .zip; en desarrollo, se compila a mano o se corre con hot-reload (npm run dev, ver "Desarrollo del frontend" arriba).
- 🌊 FPS boost con RIFE NCNN Vulkan (2×/3×/4×, activable por config)
- 🎨 GMFSS — segundo motor de interpolación de máxima calidad (ONNX, port propio santiquiroz/port-gmfss-onnx), opt-in, ~10x o más lento que RIFE
- 🧹 Limpieza automática de disco + retención de jobs (TTL)
- 🔊 Mejora de audio con IA (DeepFilterNet / RNNoise) — denoise como etapa opcional del pipeline, activable por config
- 🧠 Modelos HF + selección de dispositivo — instalar cualquier modelo de super-resolución de Hugging Face (
.onnxdirecto o.pth/.safetensorsvía conversión Spandrel) y elegircpu/dml:Npor job - 📝 Subtítulos con IA (whisper.cpp) — generación + traducción, muxeados como pista blanda
- 🎚️ Slider calidad ↔ velocidad (presets Fast/Balanced/Best mapeados a los knobs reales de cada motor)
- 📦 Modo batch por temporada (subida múltiple, progreso agregado)
Fuera de alcance (por ahora): interpolación en tiempo real estilo Lossless Scaling. Requiere captura del swapchain DirectX en vivo, arquitectónicamente incompatible con una app de archivos FastAPI/Python en el proceso principal. Diseño de Fase 7 (fork/vendor de Magpie como proceso helper separado, sin implementar todavía) documentado en docs/REALTIME_MODULE.md. Hasta que eso exista, usá Lossless Scaling o Magpie (open source) — Upflow se mantiene como pipeline offline de máxima calidad.
Ver el plan de ingeniería completo en docs/IMPLEMENTATION_PLAN.md y la investigación detrás de estos ítems en docs/RESEARCH_ANIME_SUITE.md.
Los PRs son bienvenidos. Ver CONTRIBUTING.md y el plan de implementación para saber dónde ayuda más.
MIT © 2026 Santiago Quiroz. Hacé lo que quieras con esto.
El carril de modelado puede generar una malla desde una imagen con un motor
local. Ninguno viaja con la app ni se baja al apretar un botón: son varios GB
de pesos con licencias distintas entre sí, y elegir cuál instalar es tuyo.
/api/v1/model3d/capabilities dice cuáles hay y qué le falta a cada uno.
Revisado el 2026-08-28, el panorama incómodo es que el motor libre no corre en AMD y el que corre en AMD no es libre:
| motor | licencia | corre en AMD/Windows | fuerte en |
|---|---|---|---|
| TripoSG | MIT (código y pesos) | sí, en CPU (lento) | es el único limpio y ejecutable acá |
| TRELLIS.2 | MIT | no: CUDA-only (issue #74 sin respuesta) | geometría |
| Hunyuan3D 2.1 | tencent-hunyuan-community: comercial bajo 1M MAU pero excluye UE, Reino Unido y Corea del Sur |
sí, con plantilla oficial de AMD para ComfyUI sobre ROCm | textura y PBR |
| SAM 3D Objects | SAM License | no: pide Linux, CUDA y 32 GB de VRAM | clic sobre un objeto dentro de una foto |
Por eso el primero soportado es TripoSG, y no por ser el mejor de la comparativa.
Actualización 2026-08-29 — la GPU de AMD ya no es el problema en Windows. Medido en una Radeon RX 7800 XT (gfx1101): PyTorch con ROCm nativo instala y corre, con wheels oficiales de AMD y sin ZLUDA, sin WSL y sin portar nada.
python -m pip install --index-url https://repo.amd.com/rocm/whl-multi-arch/ \
"torch[device-gfx1101]==2.12.0+rocm7.14.0"
Pide Python 3.12. Verificado: torch.cuda.is_available() en True y 43,9 TFLOPS
fp16 en un matmul de 4096³.
LA TRAMPA, que cuesta una hora si no se sabe: en un Ryzen con gráfica integrada,
ROCm enumera PRIMERO la iGPU. Acá el dispositivo 0 es una gfx1036 integrada y la
7800 XT es el 1. Torch toma el 0 por defecto y, como el paquete instalado es el de
gfx1101, el proceso no falla al arrancar: revienta en el primer cálculo con un
volcado de pila que no menciona la palabra GPU. Se arregla fijando
HIP_VISIBLE_DEVICES=1, y el carril de motores ya lo hace por motor.
Hunyuan3D 2.1 ya corre en AMD/Windows y está soportado, para generación de FORMA.
La textura no: necesita custom_rasterizer compilado para HIP y además viene
reportando salidas corruptas en AMD. No es una pérdida para este carril, porque el
banco mide SILUETA y una textura preciosa no mueve el número.
Dos trampas más que costaron su rato, ambas ya resueltas por el servicio:
- Sin
TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1, de los tres backends descaled_dot_product_attentionsólo sobrevive el matemático: flash y memory-efficient revientan conNo available kernel. Aborting execution.— un mensaje que no nombra ni la atención ni la GPU, y que manda a buscar el problema en los pesos. Con la variable puesta, los tres andan. - Bajar pesos falla con "El cliente no dispone de un privilegio requerido": Windows
pide permiso de administrador para crear enlaces simbólicos y
huggingface_hublos usa por defecto. Se arregla conHF_HUB_DISABLE_SYMLINKS=1.
Lo que dijo el banco sobre la misma gorra, con las siluetas igualadas por alto porque las mallas generadas no están en metros:
| candidata | calce | caras | islas | sana |
|---|---|---|---|---|
| blockout ajustado por el banco | 0.657 | 36.368 | 1 | sí |
| blockout original | 0.599 | 96.920 | 1 | sí |
| TripoSG (perfil) | 0.441 | 421.330 | 23 | no |
| Hunyuan3D en GPU | 0.427 | 854.582 | 1 | sí |
| TripoSG (frente) | 0.368 | 5.876 | 4 | sí |
El número más alto entre los generativos es de TripoSG, pero está sobre una malla con 23 islas sueltas y aristas no-manifold, así que el banco no la corona: Hunyuan3D es el único generativo que devolvió una malla usable. Es exactamente para lo que existe separar "cuánto calza" de "si sirve".
Para instalarlo: un venv aparte —sus dependencias fijan numpy==1.22.3 y
romperían el resto de la app— más el código y los pesos, bajo ~/3d-engines
(configurable con MESH_ENGINES_ROOT).
Lo que sale de un generador no está aprobado por haber salido: audited
viaja en false y el paso siguiente es auditar la malla o medir su calce contra
el dibujo.
Con una excepción: app/services/blender_scripts/ es GPL-2.0-or-later y
lleva su propio LICENSE. Esos archivos corren DENTRO de Blender y usan bpy a
fondo, y la Fundación Blender considera derivada a una extensión distribuida
así. El resto del árbol —incluido el código que invoca a Blender como proceso
aparte— sigue siendo MIT.