ONNX Runtime en la CPU solo hace rápida la FFT de LaMa con lados que son potencias de 2: 512 × 512 son 2,7 s y 704 × 448, casi los mismos píxeles, 10 s. El trozo se reduce lo justo para caber en 512 (o en 1024 si pasa de 768 px) y se completa hasta la potencia de 2 con su propio borde en espejo, sin hueco; al volver se descarta. Un trozo de 700 px pasa de 10 s a 4 s; uno de 300 × 200, 2 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Labstream OS — App de escritorio (Windows + macOS)
Cliente de escritorio para Labstream OS, hecho con Tauri 2.
Desde la v1.1.0 no es un envoltorio de una sola vista: es un shell con pestañas
(multiwebview) sobre el servidor del NAS (https://os.labstreamsas.com).
No contiene la app: el backend, la base de datos, OnlyOffice, etc. siguen en el NAS. Esto le da al equipo un cliente nativo de verdad en Windows y Mac:
-
Pestañas como en un navegador: los enlaces internos con
target="_blank"(documentos, entregables, exportaciones, OnlyOffice…) abren pestaña dentro de la app — antes morían en silencio. También ⌘/Ctrl+clic y clic central. Atajos: ⌘/Ctrl+T nueva, ⌘/Ctrl+W cerrar, Ctrl+Tab rotar, ⌘/Ctrl+1…9 ir a la n. La sesión (pestañas abiertas, activa) se restaura al reabrir. -
La barra de pestañas ES la fila del título (v1.6.0), en Windows y en macOS: no se gasta una franja entera en repetir el nombre de la app. En Windows la ventana va sin decoración y la propia barra dibuja los botones minimizar / maximizar / cerrar; en macOS el semáforo lo sigue pintando el sistema y la barra le deja su hueco. El doble clic en el hueco libre maximiza y restaura.
-
Colores de pestaña (v1.6.0): clic derecho sobre una pestaña ▸ Color, con la misma paleta de nueve tonos que usa Chrome para sus grupos. Sirve para agrupar de un vistazo (todo lo de un cliente en verde) y se guarda con la sesión.
-
Pestaña del color de su página (v1.21.0): una página puede pedir el color de su pestaña con
<html data-ls-pestana="#rrggbb">(solo ese formato). El modo edición de un set la pinta del gris del editor: activa se funde con su barra de arriba y detrás se reconoce entre las demás. Esa página no cambia el tema de la barra (su fondo oscuro no es el de la app). -
La IA del equipo (v1.22.0, Windows con tarjeta NVIDIA): la máscara de «Sujeto» y «Fondo» del editor de revelado la busca la app, no el navegador — ONNX Runtime con DirectML sobre la tarjeta y BiRefNet en media precisión (
src-tauri/src/ia/): 0,3 s por foto en vez de 2,5–3 s. La primera vez baja ≈700 MB a los datos de la app (%LOCALAPPDATA%\<identificador>\ia): ONNX Runtime 1.24.4 DirectML y DirectML 1.15.4 de NuGet y el modelo del servidor de la app (/api/modelos/sujeto-equipo, con la sesión de la pestaña), todo fijado por huella SHA-256. La página llama aia_estado/ia_preparar/ia_sujeto, permitidos solo para el origen de la app (build.rs+capabilities/default.json). El modelo se suelta de la tarjeta a los 2 min sin uso. Prueba de punta a punta en la tarjeta del equipo:LS_IA_DIR=<carpeta> cargo test --lib de_punta_a_punta -- --ignored --nocapture. -
Rellenar (v1.23.0, Windows): en Eliminar del editor de revelado, lo pintado lo rellena LaMa (
src-tauri/src/ia/relleno.rs, comandoia_rellenar) con lo que debería haber detrás; la página lo guarda como un parche de la foto. Corre en la CPU (LaMa no corre con DirectML: su FFT falla en la tarjeta): ~3 s a 512 px, ~10 s a 1024 (el tope). La primera vez baja el motor (si no estaba) y el modelo (≈210 MB, del servidor de la app con la sesión de la pestaña, fijado por huella).ia_estado/ia_prepararllevan{ que: "relleno" }; sin él, hablan del sujeto (como la 1.22). -
Pestañas con nombre (v1.21.0):
window.open(url, "ls-…")o un enlace contarget="ls-…"abre la pestaña la primera vez y las siguientes vuelven a ella en vez de abrir otra. La página recibe la URL nueva porwindow.__lsAbrir(url)(el modo edición cambia de foto sin recargar); si no lo tiene, navega. Nombre y color se guardan con la sesión. -
Zoom de interfaz: ⌘/Ctrl
+/−/0y Ctrl+rueda, con indicador en la barra y persistencia entre sesiones (50%–250%). -
Documentos dentro de la app: los visores/editores del propio servidor (OnlyOffice, PDFs, reproductor de revisión) abren en pestañas; solo lo de OTRO origen (Drive…) sale al navegador del sistema. El login por Authentik sigue funcionando dentro.
-
Notas de voz / cámara: pide permiso de micrófono y cámara (entitlements de macOS).
-
Descargas (entregables, exportaciones) con el diálogo nativo de guardado.
-
Bandeja del sistema: cerrar la ventana la oculta; la app sigue notificando. En macOS, el clic en el Dock también reabre la ventana, y hay menú nativo en español (Edición/Vista/Archivo) con los atajos.
-
Arranque automático al iniciar sesión y sesión persistente (no re-loguea).
-
Una sola instancia (release): abrirla de nuevo enfoca la ventana existente.
-
Menú de opciones ⋮ (v1.2.0) en la barra — también con clic derecho sobre ella: pestañas, zoom, recargar, abrir la página en el navegador, Iniciar con el sistema (elección recordada) y Buscar actualización… con aviso del resultado. Es un popup nativo del sistema, igual en Windows y macOS, y vive solo en el shell (la web app no se toca).
-
Pestañas en reposo (v1.19.0, solo Windows): cada pestaña es la web app entera, con su conexión del chat y sus sondeos. Una que lleva 5 minutos oculta se suspende (WebView2
TrySuspend: deja de gastar CPU y el sistema puede recuperar su memoria) y despierta sola, donde estaba, al volver a ella. No se duerme la activa, ni una con una subida, una descarga o audio en curso, ni un editor de documentos (/docs/). -
Salud de la app (v1.20.0,
src-tauri/src/salud.rs): cuánto gasta de verdad, qué falla y cómo se repone sola. Nada sale del equipo; todo queda en la carpeta de logs de la app (⋮ ▸ Abrir diagnóstico, o desde la bandeja).- Recursos reales: el exe es pequeño; el gasto está en la docena de procesos de WebView2
que cuelgan de él (uno por pestaña, GPU, utilidad), que el Administrador de tareas reparte.
Cada minuto se mide el árbol entero —memoria privada, CPU como la columna del Administrador
de tareas, número de procesos— y se ve en la bandeja y en ⋮ («Recursos: 2,1 GB · CPU 3 % ·
13 procesos»). Cada muestra va a
recursos.csv(fecha, procesos, MB, CPU, pestañas, dormidas): la curva de un día de trabajo real. - Diario de errores (
salud.log, rota a 2 MB): pánicos de Rust, procesos de WebView2 caídos o colgados, errores de JavaScript de las páginas (mensaje y archivo:línea, máx. 20 por carga y sin repetidos en un minuto), el hilo principal sin responder (ventana congelada: cuándo y cuánto), fallos del actualizador y cierres de golpe (si al arrancar sigue el testigoen-marcha, la sesión anterior no terminó bien). - Se repone sola: pestaña cuyo proceso muere o se cuelga → se recarga (freno: 3 veces en
10 min; después avisa y la deja quieta). Si muere el proceso principal de WebView2 (caen
todas) → la app se reinicia sola. Windows la reabre si se cuelga o se cierra de golpe
(
RegisterApplicationRestart). Con el árbol por encima de 3 GB se duermen ya las pestañas ocultas (con las mismas reglas del reposo: nunca una con subida, descarga o audio).
- Recursos reales: el exe es pequeño; el gasto está en la docena de procesos de WebView2
que cuelgan de él (uno por pestaña, GPU, utilidad), que el Administrador de tareas reparte.
Cada minuto se mide el árbol entero —memoria privada, CPU como la columna del Administrador
de tareas, número de procesos— y se ve en la bandeja y en ⋮ («Recursos: 2,1 GB · CPU 3 % ·
13 procesos»). Cada muestra va a
-
Ahorro de memoria (v1.20.0,
src-tauri/src/memoria.rs). Medido en el equipo de edición: 2,2 GB con 5 pestañas tras un día abierta, 1,3 GB de ellos en el proceso gráfico de WebView2, que crece con las horas y no devuelve nada (reiniciarlo a mano bajó la app a 1,1 GB sin perder nada). Sin quitar funciones:- Al abrir solo se carga la pestaña activa; las demás esperan en la barra con su título y se cargan al hacer clic (como Chrome/Edge). Antes arrancaban todas a la vez.
- Pestañas descartadas: una que lleva 30 min dormida se cierra de verdad y vuelve a su URL al activarla (1-2 s de carga). Nunca un editor de documentos ni una con subida, descarga o audio.
- Nivel de memoria bajo para lo que no se ve (pestañas ocultas; todo con la ventana en la bandeja o minimizada): WebView2 suelta cachés que puede regenerar.
- El proceso gráfico se recicla al pasar de 600 MB, solo si nadie mira (ventana oculta o minimizada, o 10 min sin tocar el equipo y sin sonido saliendo), como mucho cada 30 min.
- Lo que ya se guardaba en disco sigue igual: la caché HTTP (los archivos de la app vienen con
immutable, 1 año), el service worker de la web y el cajón de miniaturas (cache.rs).
-
Rastreador de trabajo efectivo (v1.10.0,
src-tauri/src/tracker.rs): mide cuánto tiempo se trabaja de verdad y en qué aplicación, y lo publica en el panel /rastreo del servidor. Cada 5 s mira la ventana al frente y si hubo entrada de ratón/teclado; con 3 minutos sin entrada deja de contar (eso cubre también pantalla bloqueada y suspensión). Sube lotes cada 5 min aPOST /api/tracker; sin red, la cola espera en disco y reintenta.- Desde la 1.9 también ANOTA la inactividad: cuando se cruza el umbral de 3 min, el tramo quieto queda registrado aparte (solo desde/hasta — sin app, sin título, sin contenido) y el panel lo enseña junto a cada jornada. La bandeja lo dice («inactivo (se anota aparte)»). Suspender o apagar el equipo NO cuenta como inactividad: el tramo se corta ahí — la noche con el portátil cerrado es una laguna, no un dato (el tramo en curso se guarda a disco cada minuto, así que un apagado o la auto-actualización lo cierran en su último latido en vez de perderlo). Y un tramo se cierra solo a las 4 h: más que eso sin tocar el equipo es ausencia, no descanso frente a la pantalla. En pausa no se anota nada.
- Desde la 1.10, la excepción del VIDEO (solo Windows): si no hay entrada
PERO al frente hay un navegador (Chrome/Brave/Edge/Firefox…) y está saliendo
sonido —un video reproduciéndose—, ese rato NO se anota como inactividad: se
cuenta como tiempo de navegador (con 0 segundos «activos», porque no se
teclea). Se detecta con el medidor de audio del sistema (WASAPI,
IAudioMeterInformation): no abre el navegador, no lee la pestaña, no graba nada — solo pregunta si hay sonido saliendo. Límite honesto: un video mudo no se detecta (no suena). En macOS no aplica (no hay API pública sencilla de nivel de audio); el equipo de edición es Windows. - Qué NO registra, a propósito: ni qué teclas se pulsan (solo compara una huella de posición del ratón y cuántas teclas hay presionadas), ni pantallazos, ni el contenido de nada, ni los procesos de fondo.
- Se ve y se pausa: el menú de la bandeja dice el estado («registrando», «inactivo», «en pausa») y pausa/reanuda de un toque.
- Se vincula desde el servidor: Ajustes ▸ Perfil ▸ Vincular este equipo,
estando dentro de esta app. El servidor genera el token del equipo y se
lo entrega al sensor por el evento
ls-tracker-token; el secreto nunca se enseña ni se copia a mano. Revocar el equipo (misma pantalla) lo corta al instante: el siguiente lote recibe 401, el sensor tira el token y la bandeja vuelve a «sin vincular». - En macOS el sistema pide permiso de Accesibilidad la primera vez (para leer el título de la ventana al frente); sin él, el sensor mide tiempo pero no sabe decir en qué app.
La barra de pestañas vive en dist/index.html (webview local «chrome»); las
pestañas son webviews hijos tab-*. Shell ↔ Rust se hablan por eventos ls-*.
El servidor objetivo se configura en src-tauri/src/lib.rs → constante SERVER_URL.
Portadas autenticadas (1.14.1)
Las imagenes conservan su URL HTTPS y se cargan con la sesion del webview, igual
que en el navegador. Ya no se reescriben a lsthumb://: el descargador Rust no
comparte las cookies y fallaba con miniaturas sin token o con token vencido.
Tampoco se modifica el DOM de las imagenes durante la hidratacion.
Se usa la cache HTTP con los ETag y Cache-Control del servidor. Una respuesta 304 reutiliza los bytes descargados; no implica fabricar otro fotograma. El cajon antiguo no se borra, pero no se usa para las nuevas peticiones de imagenes. No cambia el almacenamiento de originales, la cache HLS de LabTem ni los datos offline.
Google Docs, Sheets, Drive y el inicio de sesion de Google se abren en el navegador del sistema. Google no admite OAuth en webviews controlados por una app; ademas, WKWebView no debe anunciarse como Chrome. Se conserva el agente de usuario nativo y no se borran cookies ni sesiones. Las hojas conectadas siguen disponibles como tabla dentro del area contable. Al reiniciar se restauran solo pestanas de Labstream.
Pruebas: npm test; con Playwright disponible en Node,
node tests/images.browser.cjs verifica sesion, 304, carga dinamica y lienzo.
La prueba de navegador usa un servidor local y una cookie ficticia, sin datos reales.
Salidas:
- Windows → instalador
.exe(NSIS). - macOS →
.dmguniversal (funciona en Intel y Apple Silicon).
Prerrequisitos (solo para construir)
Quien instala no necesita nada (en Windows, WebView2 ya viene en Win 10/11; en Mac, el WebView del sistema). Para construir hacen falta:
- Node.js 20+
- Rust (incluye
cargo) - En Windows: "Visual Studio Build Tools" con el componente de C++.
- En Mac: Xcode Command Line Tools (
xcode-select --install).
⚠️ Cada sistema se construye en su sistema: el
.exese compila en Windows y el.dmgen Mac. Tauri no compila de forma cruzada de manera fiable. Por eso lo normal es dejar que GitHub Actions construya ambos (ver abajo). Como excepción cómoda: si trabajas en Mac, el.dmgsí lo puedes hacer en local.
Construir los instaladores con GitHub Actions (recomendado)
Genera ambos instaladores sin tener las dos máquinas a mano:
- Sube este proyecto a su propio repositorio (ej.
Labstream2026/labstream-desktop). - Cambia la versión en dos sitios (deben coincidir):
src-tauri/tauri.conf.json→"version"src-tauri/Cargo.toml→version- (opcional)
package.json→"version"
- Crea y publica la etiqueta de versión:
git commit -am "Versión 1.1.0" git tag v1.1.0 git push origin main --tags - GitHub Actions construye el
.exe(Windows) y el.dmg(macOS) en paralelo y los publica juntos en Releases:https://github.com/Labstream2026/labstream-desktop/releases.
También puedes lanzarlo a mano sin etiqueta desde Actions → Build instaladores → Run workflow (los instaladores quedan como artifacts).
Construir en local
En Mac (tu máquina) → genera el .dmg
npm install
npm run icons
npm run tauri build -- --bundles dmg
El .dmg queda en:
src-tauri/target/release/bundle/dmg/Labstream OS_<versión>_<arch>.dmg
Para un
.dmguniversal (Intel + Apple Silicon) en local:rustup target add aarch64-apple-darwin x86_64-apple-darwin npm run tauri build -- --target universal-apple-darwin --bundles dmg
En una PC Windows → genera el .exe
npm install
npm run tauri build -- --bundles nsis
Queda en src-tauri/target/release/bundle/nsis/Labstream OS_<versión>_x64-setup.exe.
Para desarrollo con recarga en caliente (en cualquier sistema): npm run dev.
Versiones (SemVer)
Se usa MAYOR.MENOR.PARCHE:
| Cambio | Ejemplo |
|---|---|
| Corrección / arreglo | 1.0.0 → 1.0.1 |
| Función nueva compatible | 1.0.1 → 1.1.0 |
| Cambio grande / incompatible | 1.1.0 → 2.0.0 |
La versión del envoltorio es independiente de la de la app web
(labstream-os). Una sola etiqueta de versión genera el instalador de Windows y
el de Mac a la vez, así ambas plataformas van siempre sincronizadas.
Actualización automática (ACTIVA)
Las apps ya instaladas se actualizan solas: el updater consulta el latest.json
que publica cada Release, y los instaladores se firman en el CI con la clave privada
guardada como secret del repo (TAURI_SIGNING_PRIVATE_KEY). También se puede forzar
la comprobación con clic en la versión de la barra, o desde el menú ⋮ ▸ Buscar
actualización….
⚠️ De ahí se sigue algo que conviene tener presente: un commit en
mainno le llega a nadie. Mientras no exista la etiquetavX.Y.Z, el CI no construye nada y el updater no ve versión nueva. Y al revés: publicar una etiqueta actualiza la app de todo el equipo, así que no es un paso que se dé a la ligera.
Probar un cambio antes de publicarlo
.github/workflows/check.yml corre en cualquier rama que no sea main y no publica
nada:
cargo checken Windows y macOS — comprueba que compila en las dos.- Un job aparte deja el instalador
.exede prueba como artifact de la ejecución (14 días), para instalarlo encima y ver el cambio funcionando de verdad.
Es la forma de mirar un cambio visual —la barra, los colores— sin sacárselo al equipo.
Firma de macOS
Lo que pasaba hasta la 1.15.0
El build no firmaba la app de macOS. No se le pasaba ninguna identidad, así que
el bundler de Tauri se saltaba codesign entero y el .dmg salía con la firma ad hoc
que deja el enlazador. En la app instalada se veía así:
$ codesign -dv "/Applications/Labstream OS.app"
Identifier=labstream_desktop-0a5819ba37119701
CodeDirectory ... flags=0x20002(adhoc,linker-signed)
Info.plist=not bound
Sealed Resources=none
TeamIdentifier=not set
Tres cosas mal, y ninguna avisaba:
Entitlements.plistno se aplicaba. Los permisos de micrófono, cámara y JIT eran decoración: se escriben al firmar, y no se firmaba.Info.plist=not boundySealed Resources=none. Ni el Info.plist ni los recursos iban dentro de la firma: cualquiera podía cambiarlos sin romperla.- Identificador de enlazador (
labstream_desktop-<hash>) en vez del de la app (co.labstream.os), que es por lo que macOS recuerda los permisos concedidos.
Lo que hace ahora (desde la 1.16.0)
build.yml decide según los secrets que haya en el repo:
| Secrets | Resultado |
|---|---|
| ninguno | Firma ad hoc (-s -) con hardened runtime y los entitlements. Gatekeeper sigue avisando, pero el bundle va sellado y los permisos se aplican. |
APPLE_CERTIFICATE + APPLE_CERTIFICATE_PASSWORD |
Firma real con Developer ID. Gatekeeper deja de avisar en cualquier Mac. |
…y además APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID |
Firma real + notarizado y grapado. Ni el primer arranque avisa. |
Y hay un paso que tumba la publicación si los entitlements no llegaron al .app.
Ese paso es la lección: que esto estuviera roto no lo dijo nada durante meses.
Pasar a Developer ID (99 USD/año)
- Alta en el Apple Developer Program.
- En Certificates crear uno de tipo Developer ID Application y exportarlo del
Llavero como
.p12con contraseña. - Convertirlo a texto:
base64 -i certificado.p12 | pbcopy. - En GitHub → Settings ▸ Secrets and variables ▸ Actions, añadir:
APPLE_CERTIFICATE— lo que quedó en el portapapeles.APPLE_CERTIFICATE_PASSWORD— la contraseña del.p12.APPLE_SIGNING_IDENTITY— opcional; por defectoDeveloper ID Application.
- Para notarizar, además:
APPLE_ID— el correo de la cuenta de desarrollador.APPLE_PASSWORD— una contraseña específica de app (appleid.apple.com, no la del Apple ID).APPLE_TEAM_ID— el identificador de equipo de 10 caracteres.
No hay que tocar ni un archivo: en cuanto los secrets existan, la siguiente etiqueta sale firmada.
Mientras siga ad hoc
- macOS (Gatekeeper): "no se puede abrir porque proviene de un desarrollador no
identificado" → clic derecho sobre la app → Abrir → Abrir. (Si insiste:
xattr -cr "/Applications/Labstream OS.app".) - Windows (SmartScreen): "Windows protegió su PC" → Más información → Ejecutar de todos modos. Windows es aparte: su firma cuesta 150–400 USD/año y no la toca nada de lo de arriba.
Estructura
labstream-desktop/
├── app-icon.png fuente del ícono (1024×1024)
├── dist/index.html pantalla de respaldo (requerida por Tauri)
├── package.json scripts de build
├── src-tauri/
│ ├── tauri.conf.json nombre, versión, iconos, instaladores, entitlements
│ ├── Cargo.toml dependencias Rust + versión
│ ├── Info.plist textos de permiso (micrófono/cámara) en macOS
│ ├── Entitlements.plist entitlements de macOS (micrófono, cámara, JIT)
│ ├── build.rs
│ ├── capabilities/default.json
│ ├── icons/ se generan desde app-icon.png
│ └── src/{main.rs,lib.rs} lib.rs crea la ventana + SERVER_URL + tray
└── .github/workflows/build.yml build de .exe (Windows) y .dmg (macOS) + firma de macOS