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>
89 líneas
3.6 KiB
Rust
89 líneas
3.6 KiB
Rust
// ── 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);
|
||
}
|
||
}
|