diff --git a/crates/compositor-view-napi/src/lib.rs b/crates/compositor-view-napi/src/lib.rs index 62c164e0c..69476ffc2 100644 --- a/crates/compositor-view-napi/src/lib.rs +++ b/crates/compositor-view-napi/src/lib.rs @@ -70,6 +70,22 @@ pub fn probe_backend() -> String { .to_string() } +/// Si cette machine peut produire un masque de segmentation, c'est-à-dire si la bibliothèque +/// ONNX Runtime est là où l'app l'a posée. +/// +/// Sert à NE PAS MENTIR : le contrôle « fond de caméra » est le seul de l'éditeur dont l'effet +/// dépend d'un binaire optionnel. Sans lui, `Segmenter::load` refuse, le compositeur dessine la +/// webcam telle quelle, et l'utilisateur clique sur un réglage qui ne fait rien — exactement ce +/// qu'un contrôle ne doit jamais faire. +/// +/// Une question posée au système plutôt que devinée depuis la plateforme : `darwin` ne suffit +/// pas à répondre, puisque l'amont ne publie aucun binaire ONNX pour les Macs Intel, et une +/// build de dev ou un `--dir` n'en ont pas davantage. Seul l'état réel de la machine le sait. +#[napi] +pub fn segmentation_runtime_available() -> bool { + openscreen_compositor::segmentation::runtime_available() +} + #[napi] pub fn create_view( rect: CompositorViewRect, diff --git a/electron/ipc/nativeBridge.ts b/electron/ipc/nativeBridge.ts index 47d66e270..5d4992c10 100644 --- a/electron/ipc/nativeBridge.ts +++ b/electron/ipc/nativeBridge.ts @@ -353,6 +353,12 @@ export function registerNativeBridgeHandlers(context: NativeBridgeContext) { return createSuccessResponse(requestId, { backend: compositorViewService.probeBackend(), }); + case "probeSegmentation": + // No view needed either: the layout panel decides whether to offer the + // camera-background control before any preview exists. + return createSuccessResponse(requestId, { + support: compositorViewService.probeSegmentation(), + }); case "setRect": compositorViewService.setRect(request.payload.id, request.payload.rect); return createSuccessResponse(requestId, { ok: true }); diff --git a/electron/native-bridge/services/compositorViewService.ts b/electron/native-bridge/services/compositorViewService.ts index 1a61bd696..59d0ee547 100644 --- a/electron/native-bridge/services/compositorViewService.ts +++ b/electron/native-bridge/services/compositorViewService.ts @@ -16,6 +16,7 @@ import type { GifParamsInput, NativeFramePacket, RemuxStats, + SegmentationSupport, } from "../../native/compositor-view/addon"; /** @@ -485,6 +486,36 @@ export class CompositorViewService { } } + /** Whether this machine can actually segment the camera, and if not, what is missing. + * + * Three things have to line up, and each of them has been silently absent at some point: + * the addon, the ONNX Runtime library, and the model. The renderer used to guess from + * `process.platform`, which was wrong in both directions — it hid the control on Linux + * builds that could segment, and shows it on Intel Macs, for which upstream publishes no + * ONNX binary at all. A dev checkout and a `--dir` build have none staged either. + * + * Same shape as `probeBackend`: asked without allocating a view, because the panel needs + * the answer before any preview exists. */ + probeSegmentation(): SegmentationSupport { + const addon = this.ensureAddon(); + if (!addon) { + return "none"; + } + try { + if (!addon.segmentationRuntimeAvailable()) { + return "no-runtime"; + } + } catch (err) { + // An older `.node` predates this probe. Treat as unsupported rather than crashing + // the bridge — same contract as `probeBackend`. + console.warn("[compositor-view] segmentationRuntimeAvailable unavailable:", err); + return "none"; + } + // The model is resolved by this process, not the addon, so it is checked here — and it + // is the same lookup `resolveSceneAssetPaths` performs, so the two cannot disagree. + return resolveSceneAssetPath(SEGMENTATION_MODEL_ASSET) ? "ready" : "no-model"; + } + /** Allocates an offscreen compositor view sized to `rect.width`x`rect.height`. * `rect.x` / `rect.y` are vestigial (ignored native-side) — the renderer * keeps them on the wire so the existing `CompositorViewRect` shape stays diff --git a/electron/native/compositor-view/addon.d.ts b/electron/native/compositor-view/addon.d.ts index dc5fe599a..4c63065d8 100644 --- a/electron/native/compositor-view/addon.d.ts +++ b/electron/native/compositor-view/addon.d.ts @@ -104,11 +104,22 @@ export interface ClipInput { * all, so the view will fail with its own, more specific message. */ export type CompositorBackend = "hardware" | "cpu" | "none"; +/** Whether this machine can segment the camera, and if not, what is missing. Mirrored in + * `src/native/contracts.ts` for the renderer, the same way `CompositorBackend` is — the addon + * boundary and the IPC boundary each own their shape. */ +export type SegmentationSupport = "ready" | "no-runtime" | "no-model" | "none"; + export interface CompositorViewAddon { /** What this machine offers, asked without allocating a view — the export dialog * needs the answer before any preview exists. Cached native-side. */ probeBackend(): CompositorBackend; + /** Whether the ONNX Runtime library is where the app staged it, and therefore whether a + * segmentation mask can be produced at all. Asked of the machine rather than guessed from + * the platform: upstream publishes no ONNX build for Intel Macs, and a dev checkout or a + * `--dir` build has none either. */ + segmentationRuntimeAvailable(): boolean; + /** Allocates an offscreen compositor view sized to `rect.width`x`rect.height` (the * target preview resolution; `rect.x` / `rect.y` are vestigial and ignored native-side). * No HWND/native-window-handle is passed: there's no OS window to parent to. The diff --git a/scripts/before-pack.cjs b/scripts/before-pack.cjs index d9701c654..4b97a35a3 100644 --- a/scripts/before-pack.cjs +++ b/scripts/before-pack.cjs @@ -68,6 +68,26 @@ const HELPER_SOURCE_PATHS = [ * `mac.extraResources` ships this directory wholesale (`filter: ["darwin-*/*"]`), so * "present here" is the same thing as "present in the installed app". */ +/** + * L'exigence ONNX Runtime de macOS, séparée parce qu'elle ne vaut QUE sur arm64. + * + * L'amont ne publie aucun binaire ONNX pour les Macs Intel : `fetch-onnxruntime.mjs` le constate + * et sort en 0 sans rien poser. Un paquet x64 sans la bibliothèque est donc CORRECT, et l'exiger + * là ferait échouer à l'empaquetage une build parfaitement saine — la garde se retournerait + * contre ce qu'elle protège. + * + * Sur arm64 en revanche son absence ne casse rien de visible : `Segmenter::load` refuse, le + * compositeur dessine la webcam telle quelle, et le contrôle disparaît de l'éditeur. Le paquet + * est silencieusement amputé, ce qui est exactement la panne que cette garde existe pour + * attraper et qu'aucun test ne peut voir puisque tout se dégrade proprement. + */ +const MAC_ONNX_REQUIRED = { + match: (name) => name === "libonnxruntime.dylib", + what: "the ONNX Runtime library the camera-background segmentation loads", + breaks: "the camera-background control vanishes from the editor and every effect is a no-op", + fix: "Stage it with:\n\n npm run fetch:onnxruntime", +}; + const MAC_REQUIRED = [ { match: (name) => name === "compositor_view.node", @@ -119,6 +139,17 @@ const MAC_REQUIRED = [ * `helper-ffmpeg/` subdirectory holds. */ const LINUX_REQUIRED = [ + // L'effet de fond de caméra est le seul dont la présence dépend d'un binaire optionnel, et + // son absence ne casse RIEN de visible : `Segmenter::load` refuse, le compositeur dessine la + // webcam telle quelle, et le contrôle disparaît de l'éditeur. Un paquet livré sans elle est + // donc silencieusement amputé — la panne exacte que cette garde existe pour attraper, et + // celle qu'aucun test ne peut voir puisque tout se dégrade proprement. + { + match: (name) => name === "libonnxruntime.so", + what: "the ONNX Runtime library the camera-background segmentation loads", + breaks: "the camera-background control vanishes from the editor and every effect is a no-op", + fix: "Stage it with:\n\n npm run fetch:onnxruntime", + }, { match: (name) => name === "compositor_view.node", what: "the wgpu/Vulkan compositor addon", @@ -223,6 +254,17 @@ function checkNativePayload({ dir, required, osLabel, bundleNoun, emptyDirFix }) * "together here" is the same thing as "together in the installed app". */ const WIN_REQUIRED = [ + // L'effet de fond de caméra est le seul dont la présence dépend d'un binaire optionnel, et + // son absence ne casse RIEN de visible : `Segmenter::load` refuse, le compositeur dessine la + // webcam telle quelle, et le contrôle disparaît de l'éditeur. Un paquet livré sans elle est + // donc silencieusement amputé — la panne exacte que cette garde existe pour attraper, et + // celle qu'aucun test ne peut voir puisque tout se dégrade proprement. + { + match: (name) => name === "onnxruntime.dll", + what: "the ONNX Runtime library the camera-background segmentation loads", + breaks: "the camera-background control vanishes from the editor and every effect is a no-op", + fix: "Stage it with:\n\n npm run fetch:onnxruntime", + }, { match: (name) => name === "compositor_view.node", what: "the D3D11 compositor addon", @@ -419,10 +461,13 @@ function checkWinNativePayload() { } function checkMacNativePayload(context) { - const dir = path.join(ROOT, "electron", "native", "bin", `darwin-${archTagFor(context)}`); + const arch = archTagFor(context); + const dir = path.join(ROOT, "electron", "native", "bin", `darwin-${arch}`); checkNativePayload({ dir, - required: MAC_REQUIRED, + // Voir `MAC_ONNX_REQUIRED` : exiger la bibliothèque sur Intel ferait échouer une build + // que l'amont rend impossible à satisfaire. + required: arch === "arm64" ? [...MAC_REQUIRED, MAC_ONNX_REQUIRED] : MAC_REQUIRED, osLabel: "macOS", bundleNoun: "the .app", emptyDirFix: `${FIX_MAC}\n\nThe STT helper and the capture helper are separate builds — see\ntechnical-documentation/engineering/build-and-packaging.md.`, diff --git a/src/components/ai-edition/RightPanes.tsx b/src/components/ai-edition/RightPanes.tsx index 880d94b8c..2e15f7b12 100644 --- a/src/components/ai-edition/RightPanes.tsx +++ b/src/components/ai-edition/RightPanes.tsx @@ -16,6 +16,7 @@ import { Sliders, Trash2, } from "lucide-react"; + import { type ChangeEvent, type CSSProperties, @@ -80,6 +81,7 @@ import { type AspectRatio, getAspectRatioLabel, } from "@/utils/aspectRatioUtils"; +import { useCanSegmentCamera } from "../../native/hooks/useSegmentationSupport"; import styles from "./NewEditorShell.module.css"; interface PaneProps { @@ -1943,6 +1945,7 @@ const CAMERA_BACKGROUND_MODES: Array<{ ]; export function LayoutPane() { + const canSegmentCamera = useCanSegmentCamera(); const ts = useScopedT("settings"); const { settings, set, setLive, commit, hasDocument } = useEditorSettings(); const { pick: handlePickWebcamWallpaper, input: webcamWallpaperInput } = useWallpaperFileInput( @@ -2148,77 +2151,87 @@ export function LayoutPane() { ) : null} -