v1.22.0: la IA del equipo (máscara de sujeto en la tarjeta NVIDIA)

La máscara de «Sujeto» y «Fondo» del editor de revelado de Labstream OS la busca
la app en la tarjeta del equipo, no el navegador (src-tauri/src/ia/):

- Tres comandos para la página, permitidos solo para el origen de la app (manifiesto
  en build.rs + capabilities): ia_estado (¿hay NVIDIA?, ¿está bajado?, cuánto
  falta), ia_preparar (baja lo que falte con eventos ia-progreso y abre el modelo)
  e ia_sujeto (RGBA en binario → [ancho][alto][un byte por punto]).
- Motor: ONNX Runtime 1.24.4 con DirectML (crate ort 2.0.0-rc.14, load-dynamic,
  api-24): nada se enlaza al compilar; las DLL se bajan de NuGet la primera vez y
  DirectML.dll se carga por su ruta antes que onnxruntime.dll. Sin fundir el grafo
  (fundido, el HR no cabía en 16 GB) y sin seguir en la CPU si la tarjeta falla.
- Modelo: BiRefNet general en media precisión, del servidor de la app con la
  sesión de la pestaña (cookies de WebView2 leídas en el comando asíncrono).
- Todo fijado por huella SHA-256; los .nupkg se sacan por ruta exacta (DirectML trae
  también las DLL de Xbox y de depurar con el mismo nombre).
- ¿Hay NVIDIA?: los adaptadores de DXGI, sin cargar nada de la IA.
- El modelo se suelta de la tarjeta a los 2 min sin uso; si la tarjeta falla, se
  suelta y se abre de nuevo la próxima vez. Todo va a salud.log.

Medido en una RTX 5080: 0,3 s por foto (1,4 s la primera; el navegador tarda
2,5–3 s) y el mismo borde que el modelo original en la CPU. El HR (2048 px,
matting) se probó y se descartó: 14–15 GB de la tarjeta y de 6 a 45 s por foto.

Pruebas nuevas: zip por ruta exacta, entrada igual a la del navegador, salida y
respuesta; y una de punta a punta en la tarjeta (ignorada en CI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Munka
2026-10-14 18:45:04 -05:00
padre 6539f6939c
commit a725900a4a
Se han modificado 17 ficheros con 935 adiciones y 7 borrados

88
src-tauri/src/ia/imagen.rs Archivo normal
Ver fichero

@@ -0,0 +1,88 @@
// ── La foto como la quiere el modelo, y su respuesta como la quiere la página ──
// Lo mismo que hace el navegador (lib/revelado/modelo-sujeto: entradaDelModelo), aquí en Rust:
// la foto RGBA (como se ve en el editor) a `lado` × `lado` entre sus cuatro píxeles más cercanos,
// en tres planos RGB normalizados como ImageNet. Y la salida, a un byte por punto.
const MEDIA: [f32; 3] = [0.485, 0.456, 0.406];
const DESVIACION: [f32; 3] = [0.229, 0.224, 0.225];
/// RGBA (`ancho` × `alto`) → [1, 3, lado, lado] en un vector plano.
pub fn entrada(rgba: &[u8], ancho: usize, alto: usize, lado: usize) -> Vec<f32> {
let plano = lado * lado;
let mut out = vec![0f32; 3 * plano];
for y in 0..lado {
let fy = (((y as f32 + 0.5) / lado as f32) * alto as f32 - 0.5).clamp(0.0, (alto - 1) as f32);
let y0 = fy.floor() as usize;
let y1 = (y0 + 1).min(alto - 1);
let wy = fy - y0 as f32;
for x in 0..lado {
let fx = (((x as f32 + 0.5) / lado as f32) * ancho as f32 - 0.5).clamp(0.0, (ancho - 1) as f32);
let x0 = fx.floor() as usize;
let x1 = (x0 + 1).min(ancho - 1);
let wx = fx - x0 as f32;
for c in 0..3 {
let p = |xx: usize, yy: usize| rgba[(yy * ancho + xx) * 4 + c] as f32;
let arriba = p(x0, y0) + (p(x1, y0) - p(x0, y0)) * wx;
let abajo = p(x0, y1) + (p(x1, y1) - p(x0, y1)) * wx;
let v = arriba + (abajo - arriba) * wy;
out[c * plano + y * lado + x] = (v / 255.0 - MEDIA[c]) / DESVIACION[c];
}
}
}
out
}
/// ¿La salida viene sin sigmoide? (BiRefNet la aplica fuera de la red; si algo cae fuera de 0..1,
/// son logits y hay que aplicarla aquí.)
pub fn son_logits(salida: &[f32]) -> bool {
salida.iter().any(|&v| !(-0.001..=1.001).contains(&v))
}
/// La salida del modelo a un byte por punto. `logits`: la salida va sin sigmoide (BiRefNet la
/// aplica fuera de la red).
pub fn a_bytes(salida: &[f32], logits: bool) -> Vec<u8> {
salida
.iter()
.map(|&v| {
let p = if logits { 1.0 / (1.0 + (-v).exp()) } else { v };
(p.clamp(0.0, 1.0) * 255.0).round() as u8
})
.collect()
}
/// La respuesta de `ia_sujeto` para la página: [ancho u32][alto u32][bytes] (little endian).
pub fn respuesta(ancho: u32, alto: u32, bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + bytes.len());
out.extend_from_slice(&ancho.to_le_bytes());
out.extend_from_slice(&alto.to_le_bytes());
out.extend_from_slice(bytes);
out
}
#[cfg(test)]
mod pruebas {
use super::*;
#[test]
fn la_entrada_es_la_del_navegador() {
// 2 × 1: rojo puro y blanco; a 2 × 2 cada columna toma su píxel
let rgba = [255, 0, 0, 255, 255, 255, 255, 255];
let e = entrada(&rgba, 2, 1, 2);
let norm = |v: f32, c: usize| (v / 255.0 - MEDIA[c]) / DESVIACION[c];
assert!((e[0] - norm(255.0, 0)).abs() < 1e-5); // R de (0,0)
assert!((e[4] - norm(0.0, 1)).abs() < 1e-5); // G de (0,0)
assert!((e[5] - norm(255.0, 1)).abs() < 1e-5); // G de (1,0)
assert!((e[8 + 3] - norm(255.0, 2)).abs() < 1e-5); // B de (1,1)
assert_eq!(e.len(), 12);
}
#[test]
fn la_salida_a_bytes_y_la_respuesta() {
assert_eq!(a_bytes(&[-20.0, 0.0, 20.0], true), vec![0, 128, 255]);
assert_eq!(a_bytes(&[0.0, 0.5, 1.2], false), vec![0, 128, 255]);
assert!(son_logits(&[0.2, -3.0]) && !son_logits(&[0.0, 0.5, 1.0]));
let r = respuesta(3, 2, &[1, 2, 3, 4, 5, 6]);
assert_eq!(&r[..8], &[3, 0, 0, 0, 2, 0, 0, 0]);
assert_eq!(r.len(), 14);
}
}