From 7770c7fa63203315e504ed93380db5c6b2c97fa1 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sun, 30 Aug 2026 18:08:29 +0200 Subject: [PATCH 1/2] fix(webcam): ask the machine whether it can segment, instead of guessing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three separate ways the camera-background feature could be silently absent, and they turn out to be one bug: nothing ever asked whether a mask could be produced. - **Intel Macs.** Upstream publishes no ONNX Runtime binary for osx-x64, so `runtime_available()` is false there forever. The control was shown anyway. - **Nothing reached the renderer.** `runtime_available()` existed in Rust and stopped there. The compositor logged one line and left `fx.z` at 0; the user clicked Blur, the setting persisted, the button highlighted, the slider appeared — and the camera was untouched in the preview and in the export. - **`before-pack` did not require the library.** A build that skipped staging, or hit a network blip in a job that continued, passed the payload guard and shipped an installer whose control does nothing. `segmentation_runtime_available()` on the addon, composed with the model lookup in `compositorViewService.probeSegmentation()`, gives a four-state answer: `ready` / `no-runtime` / `no-model` / `none`. It rides the `probeBackend` chain exactly — N-API, addon.d.ts, service, IPC, contracts, client — and `useSegmentationSupport` mirrors `useCompositorBackend`, memoised once per session, failing closed on every non-`ready` answer including "still probing". Asked rather than guessed, because the guess was wrong in both directions: the old `process.platform` gate hid the control on Linux builds that could segment and showed it on Intel Macs that never can. Platform is not capability — a dev checkout and a `--dir` build have no runtime staged either, and neither is distinguishable from `darwin` or `win32`. `before-pack` now names the library in all three payload lists, with no entry on Intel macOS since a package without it is correct there. This is the guard whose own header says a mac package built without the compositor addon "shipped silently"; the same hole was open for this one. Verified: 2208 JS tests (7 new on the hook, three of which fail when the fail-closed rule is mutated away), 146 Rust, tsc clean, before-pack loads. --- crates/compositor-view-napi/src/lib.rs | 16 ++ electron/ipc/nativeBridge.ts | 6 + .../services/compositorViewService.ts | 31 ++++ electron/native/compositor-view/addon.d.ts | 11 ++ scripts/before-pack.cjs | 35 ++++ src/components/ai-edition/RightPanes.tsx | 155 ++++++++++-------- src/native/compositorViewClient.ts | 19 +++ src/native/contracts.ts | 21 +++ .../hooks/useSegmentationSupport.test.ts | 79 +++++++++ src/native/hooks/useSegmentationSupport.ts | 60 +++++++ 10 files changed, 362 insertions(+), 71 deletions(-) create mode 100644 src/native/hooks/useSegmentationSupport.test.ts create mode 100644 src/native/hooks/useSegmentationSupport.ts 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..02a1d9fd7 100644 --- a/scripts/before-pack.cjs +++ b/scripts/before-pack.cjs @@ -69,6 +69,19 @@ const HELPER_SOURCE_PATHS = [ * "present here" is the same thing as "present in the installed app". */ const MAC_REQUIRED = [ + // Pas d'entrée sur x64 : l'amont ne publie aucun binaire ONNX pour les Macs Intel, donc un + // paquet Intel SANS la bibliothèque est correct. `fetch-onnxruntime.mjs` le dit et sort en 0. + // 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.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", + }, { match: (name) => name === "compositor_view.node", what: "the Metal compositor addon", @@ -119,6 +132,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 +247,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", 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} -
{ts("layout.webcamBackground")}
-
- {CAMERA_BACKGROUND_MODES.map((mode) => { - const isActive = settings.webcamBackgroundMode === mode.value; - return ( - - ); - })} -
- {settings.webcamBackgroundMode === "blur" ? ( -
- setLive({ webcamBlurIntensity: next / 100 })} - onCommit={() => void commit()} - /> -
- ) : null} - {settings.webcamBackgroundMode === "custom" ? ( -
- void set({ webcamWallpaper: url })} - onLiveChange={(url) => setLive({ webcamWallpaper: url })} - onCommit={commit} - updateNativeBackground={false} - onPickFile={handlePickWebcamWallpaper} - /> - {webcamWallpaperInput} -
+ {/* Le seul contrôle de l'éditeur dont l'effet dépend d'un binaire optionnel : sans la + bibliothèque ONNX Runtime, le compositeur dessine la webcam telle quelle et le réglage + ne fait rien. On demande donc à la machine plutôt que de deviner depuis la plateforme — + `process.platform` se trompait dans les deux sens : il cachait le contrôle sur des + builds Linux capables de segmenter, et le montrait sur les Macs Intel, pour lesquels + l'amont ne publie aucun binaire ONNX. */} + {canSegmentCamera ? ( + <> +
{ts("layout.webcamBackground")}
+
+ {CAMERA_BACKGROUND_MODES.map((mode) => { + const isActive = settings.webcamBackgroundMode === mode.value; + return ( + + ); + })} +
+ {settings.webcamBackgroundMode === "blur" ? ( +
+ setLive({ webcamBlurIntensity: next / 100 })} + onCommit={() => void commit()} + /> +
+ ) : null} + {settings.webcamBackgroundMode === "custom" ? ( +
+ void set({ webcamWallpaper: url })} + onLiveChange={(url) => setLive({ webcamWallpaper: url })} + onCommit={commit} + updateNativeBackground={false} + onPickFile={handlePickWebcamWallpaper} + /> + {webcamWallpaperInput} +
+ ) : null} + ) : null}
{ts("layout.webcamFraming")}
diff --git a/src/native/compositorViewClient.ts b/src/native/compositorViewClient.ts index 68f062e62..987d506d2 100644 --- a/src/native/compositorViewClient.ts +++ b/src/native/compositorViewClient.ts @@ -20,6 +20,8 @@ import type { CompositorParamValue, CompositorViewRect, CompositorViewResult, + SegmentationSupport, + SegmentationSupportResult, } from "./contracts"; /** Which backend the native compositor will use here. @@ -39,6 +41,23 @@ export async function probeCompositorBackend(): Promise { } } +/** Whether this machine can segment the camera. `"none"` outside Electron or without the addon. + * + * Fails closed: any error means the control should not be offered. Showing a setting that + * cannot do anything is the failure this exists to prevent, so a broken probe must not be + * read as capability. */ +export async function probeSegmentationSupport(): Promise { + try { + const result = await requireNativeBridgeData({ + domain: "compositor", + action: "probeSegmentation", + }); + return result.support; + } catch { + return "none"; + } +} + export function createCompositorView( rect: CompositorViewRect, sources?: { screenPath?: string; webcamPath?: string; cursorPath?: string }, diff --git a/src/native/contracts.ts b/src/native/contracts.ts index ede052f32..14108ddb7 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -128,6 +128,22 @@ export interface CompositorBackendResult { backend: CompositorBackend; } +/** Whether this machine can segment the camera, and if not, what is missing. + * + * `"ready"` — runtime and model both present. + * `"no-runtime"` — no ONNX Runtime library. Upstream publishes none for Intel Macs, and a dev + * checkout or a `--dir` build has none staged either. + * `"no-model"` — the runtime is there but the `.onnx` does not resolve. + * `"none"` — no native addon at all; the pure-web/dev case, not a degraded machine. + * + * Asked rather than guessed. Gating on `process.platform` was wrong in both directions: it hid + * the control on a Linux box that could segment, and showed it on an Intel Mac that never can. */ +export type SegmentationSupport = "ready" | "no-runtime" | "no-model" | "none"; + +export interface SegmentationSupportResult { + support: SegmentationSupport; +} + /** A self-describing preview frame returned by `readFrame` (native → renderer): pixels * (`data`, RGBA8, `width * height * 4` bytes) plus their dimensions and a monotonic * generation. The hook keeps `gen` and passes it back as `sinceGen`; an unchanged frame @@ -675,6 +691,11 @@ export type NativeBridgeRequest = action: "probeBackend"; requestId?: string; } + | { + domain: "compositor"; + action: "probeSegmentation"; + requestId?: string; + } | { domain: "compositor"; action: "setRect"; diff --git a/src/native/hooks/useSegmentationSupport.test.ts b/src/native/hooks/useSegmentationSupport.test.ts new file mode 100644 index 000000000..bc71cc658 --- /dev/null +++ b/src/native/hooks/useSegmentationSupport.test.ts @@ -0,0 +1,79 @@ +// @vitest-environment jsdom +/** + * The camera-background control must appear only where a mask can actually reach the shader. + * + * The failure worth guarding is the false POSITIVE, and it is the mirror of the CPU-notice + * one next door: here, offering the control is the damage. Every non-`"ready"` answer means + * the user would click a setting, watch it persist and highlight, and see nothing change — + * in the preview and in the exported file alike. + * + * This replaced a `process.platform` guess that was wrong in both directions: it hid the + * control on Linux builds that could segment, and showed it on Intel Macs, for which + * upstream publishes no ONNX Runtime binary at all. + */ + +import { renderHook, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ probeSegmentationSupport: vi.fn() })); + +vi.mock("../compositorViewClient", () => ({ + probeSegmentationSupport: mocks.probeSegmentationSupport, +})); + +import { + resetSegmentationSupportProbeForTests, + useCanSegmentCamera, + useSegmentationSupport, +} from "./useSegmentationSupport"; + +describe("useSegmentationSupport", () => { + beforeEach(() => { + vi.clearAllMocks(); + resetSegmentationSupportProbeForTests(); + }); + + it("offers the control when the machine can segment", async () => { + mocks.probeSegmentationSupport.mockResolvedValue("ready"); + const { result } = renderHook(() => useCanSegmentCamera()); + await waitFor(() => expect(result.current).toBe(true)); + }); + + // The three ways it silently cannot, each of which has actually happened: no ONNX Runtime + // (Intel Mac, dev checkout, `--dir` build), no model (not lifted out of app.asar), no addon + // (pure-web dev). None of them fails loudly, which is exactly why the probe exists. + it.each([ + "no-runtime", + "no-model", + "none", + ] as const)("hides the control when the answer is %s", async (answer) => { + mocks.probeSegmentationSupport.mockResolvedValue(answer); + const { result } = renderHook(() => useCanSegmentCamera()); + await waitFor(() => expect(mocks.probeSegmentationSupport).toHaveBeenCalled()); + expect(result.current).toBe(false); + }); + + it("reports the reason, not just the verdict", async () => { + mocks.probeSegmentationSupport.mockResolvedValue("no-runtime"); + const { result } = renderHook(() => useSegmentationSupport()); + // Which one it is decides whether staging is missing or the model is — the difference + // between a build bug and a packaging bug. + await waitFor(() => expect(result.current).toBe("no-runtime")); + }); + + it("hides the control until the probe answers", () => { + mocks.probeSegmentationSupport.mockReturnValue(new Promise(() => {})); + const { result } = renderHook(() => useCanSegmentCamera()); + // Fails closed: no flash of a control that is about to disappear. + expect(result.current).toBe(false); + }); + + it("probes once for the whole session, however many consumers ask", async () => { + mocks.probeSegmentationSupport.mockResolvedValue("ready"); + const a = renderHook(() => useCanSegmentCamera()); + const b = renderHook(() => useCanSegmentCamera()); + await waitFor(() => expect(a.result.current).toBe(true)); + await waitFor(() => expect(b.result.current).toBe(true)); + expect(mocks.probeSegmentationSupport).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/native/hooks/useSegmentationSupport.ts b/src/native/hooks/useSegmentationSupport.ts new file mode 100644 index 000000000..3a6873892 --- /dev/null +++ b/src/native/hooks/useSegmentationSupport.ts @@ -0,0 +1,60 @@ +/** + * Whether this machine can segment the camera, probed once per session. + * + * A property of the installation, not of a view: the same three things have to line up + * whatever is on screen — the native addon, the ONNX Runtime library, and the model. So it + * is memoised in a module-level promise, exactly as `useCompositorBackend` is. + * + * Returns `null` until the probe resolves, so callers render nothing rather than flashing a + * control that may be about to disappear. + */ + +import { useEffect, useState } from "react"; +import { probeSegmentationSupport } from "../compositorViewClient"; +import type { SegmentationSupport } from "../contracts"; + +let cached: Promise | null = null; + +function probeOnce(): Promise { + if (!cached) { + cached = probeSegmentationSupport(); + } + return cached; +} + +/** Test seam: drops the memoised probe so each test observes its own mock. */ +export function resetSegmentationSupportProbeForTests(): void { + cached = null; +} + +export function useSegmentationSupport(): SegmentationSupport | null { + const [support, setSupport] = useState(null); + + useEffect(() => { + let disposed = false; + probeOnce().then((value) => { + if (!disposed) { + setSupport(value); + } + }); + return () => { + disposed = true; + }; + }, []); + + return support; +} + +/** + * True only once the machine has answered that it CAN segment. + * + * Fails closed on purpose. `null` (still probing) is not capability, and neither is + * `"no-runtime"` / `"no-model"` / `"none"` — the whole point is that a control which cannot + * do anything must not be offered. Showing it and having it do nothing is the failure this + * replaces: the previous gate read `process.platform`, which hid the control on Linux builds + * that could segment and showed it on Intel Macs, for which upstream publishes no ONNX + * binary at all. + */ +export function useCanSegmentCamera(): boolean { + return useSegmentationSupport() === "ready"; +} From 1e0198024ff01f0184a509b40c6738cbf956a461 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sun, 30 Aug 2026 18:20:49 +0200 Subject: [PATCH 2/2] fix(build): the ONNX requirement must not apply to Intel Macs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment said "no entry on x64" and the code added one anyway, for every macOS arch. Upstream publishes no ONNX Runtime binary for Intel Macs — `fetch-onnxruntime.mjs` says so and exits 0 without staging anything — so a clean x64 payload would have failed at pack time. The guard would have turned against exactly what it protects. Split into `MAC_ONNX_REQUIRED` and applied only on arm64, so the comment and the code now say the same thing. Also aligns the `SegmentationSupport` doc with its producer: `"none"` is also what the service returns when an addon is present but too old to answer the probe, not only when none loaded at all. Consumers were told it meant one thing and could get the other. Both caught in review. --- scripts/before-pack.cjs | 40 +++++++++++++++++++++++++--------------- src/native/contracts.ts | 3 ++- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/scripts/before-pack.cjs b/scripts/before-pack.cjs index 02a1d9fd7..4b97a35a3 100644 --- a/scripts/before-pack.cjs +++ b/scripts/before-pack.cjs @@ -68,20 +68,27 @@ 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 = [ - // Pas d'entrée sur x64 : l'amont ne publie aucun binaire ONNX pour les Macs Intel, donc un - // paquet Intel SANS la bibliothèque est correct. `fetch-onnxruntime.mjs` le dit et sort en 0. - // 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.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", - }, { match: (name) => name === "compositor_view.node", what: "the Metal compositor addon", @@ -454,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/native/contracts.ts b/src/native/contracts.ts index 14108ddb7..7e7fc11c9 100644 --- a/src/native/contracts.ts +++ b/src/native/contracts.ts @@ -134,7 +134,8 @@ export interface CompositorBackendResult { * `"no-runtime"` — no ONNX Runtime library. Upstream publishes none for Intel Macs, and a dev * checkout or a `--dir` build has none staged either. * `"no-model"` — the runtime is there but the `.onnx` does not resolve. - * `"none"` — no native addon at all; the pure-web/dev case, not a degraded machine. + * `"none"` — no usable native addon: none loaded at all (the pure-web/dev case), or one + * too old to answer the probe. Not a degraded machine, and not a verdict on the runtime. * * Asked rather than guessed. Gating on `process.platform` was wrong in both directions: it hid * the control on a Linux box that could segment, and showed it on an Intel Mac that never can. */