Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/compositor-view-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions electron/ipc/nativeBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
31 changes: 31 additions & 0 deletions electron/native-bridge/services/compositorViewService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
GifParamsInput,
NativeFramePacket,
RemuxStats,
SegmentationSupport,
} from "../../native/compositor-view/addon";

/**
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions electron/native/compositor-view/addon.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 47 additions & 2 deletions scripts/before-pack.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.`,
Expand Down
155 changes: 84 additions & 71 deletions src/components/ai-edition/RightPanes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Sliders,
Trash2,
} from "lucide-react";

import {
type ChangeEvent,
type CSSProperties,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -2148,77 +2151,87 @@ export function LayoutPane() {
</div>
</div>
) : null}
<div className={styles.sectionLabel}>{ts("layout.webcamBackground")}</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: 8,
padding: "0 var(--sp-4) 12px",
}}
>
{CAMERA_BACKGROUND_MODES.map((mode) => {
const isActive = settings.webcamBackgroundMode === mode.value;
return (
<button
type="button"
key={mode.value}
className={`${styles.cursorCell} ${isActive ? styles.isActive : ""}`}
style={{
flexDirection: "column",
gap: 4,
padding: 8,
display: "flex",
alignItems: "center",
minWidth: 0,
}}
disabled={layoutControlsDisabled}
onClick={() => {
void set({ webcamBackgroundMode: mode.value });
}}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width={22}
height={22}
>
{mode.icon}
</svg>
<span style={{ font: "500 11px/1 var(--font-body)" }}>{ts(mode.labelKey)}</span>
</button>
);
})}
</div>
{settings.webcamBackgroundMode === "blur" ? (
<div className={styles.sliderGrid}>
<SliderCell
label={ts("layout.webcamBlurIntensity")}
value={Math.round(settings.webcamBlurIntensity * 100)}
min={0}
max={100}
suffix="%"
disabled={layoutControlsDisabled}
onChange={(next) => setLive({ webcamBlurIntensity: next / 100 })}
onCommit={() => void commit()}
/>
</div>
) : null}
{settings.webcamBackgroundMode === "custom" ? (
<div style={{ padding: "0 var(--sp-4) 12px" }}>
<WallpaperPicker
value={settings.webcamWallpaper}
hasDocument={hasDocument && !layoutControlsDisabled}
onChange={(url) => void set({ webcamWallpaper: url })}
onLiveChange={(url) => setLive({ webcamWallpaper: url })}
onCommit={commit}
updateNativeBackground={false}
onPickFile={handlePickWebcamWallpaper}
/>
{webcamWallpaperInput}
</div>
{/* 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 ? (
<>
<div className={styles.sectionLabel}>{ts("layout.webcamBackground")}</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(4, minmax(0, 1fr))",
gap: 8,
padding: "0 var(--sp-4) 12px",
}}
>
{CAMERA_BACKGROUND_MODES.map((mode) => {
const isActive = settings.webcamBackgroundMode === mode.value;
return (
<button
type="button"
key={mode.value}
className={`${styles.cursorCell} ${isActive ? styles.isActive : ""}`}
style={{
flexDirection: "column",
gap: 4,
padding: 8,
display: "flex",
alignItems: "center",
minWidth: 0,
}}
disabled={layoutControlsDisabled}
onClick={() => {
void set({ webcamBackgroundMode: mode.value });
}}
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
width={22}
height={22}
>
{mode.icon}
</svg>
<span style={{ font: "500 11px/1 var(--font-body)" }}>{ts(mode.labelKey)}</span>
</button>
);
})}
</div>
{settings.webcamBackgroundMode === "blur" ? (
<div className={styles.sliderGrid}>
<SliderCell
label={ts("layout.webcamBlurIntensity")}
value={Math.round(settings.webcamBlurIntensity * 100)}
min={0}
max={100}
suffix="%"
disabled={layoutControlsDisabled}
onChange={(next) => setLive({ webcamBlurIntensity: next / 100 })}
onCommit={() => void commit()}
/>
</div>
) : null}
{settings.webcamBackgroundMode === "custom" ? (
<div style={{ padding: "0 var(--sp-4) 12px" }}>
<WallpaperPicker
value={settings.webcamWallpaper}
hasDocument={hasDocument && !layoutControlsDisabled}
onChange={(url) => void set({ webcamWallpaper: url })}
onLiveChange={(url) => setLive({ webcamWallpaper: url })}
onCommit={commit}
updateNativeBackground={false}
onPickFile={handlePickWebcamWallpaper}
/>
{webcamWallpaperInput}
</div>
) : null}
</>
) : null}
<div className={styles.sectionLabel}>{ts("layout.webcamFraming")}</div>
<div className={styles.sliderGrid}>
Expand Down
19 changes: 19 additions & 0 deletions src/native/compositorViewClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type {
CompositorParamValue,
CompositorViewRect,
CompositorViewResult,
SegmentationSupport,
SegmentationSupportResult,
} from "./contracts";

/** Which backend the native compositor will use here.
Expand All @@ -39,6 +41,23 @@ export async function probeCompositorBackend(): Promise<CompositorBackend> {
}
}

/** 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<SegmentationSupport> {
try {
const result = await requireNativeBridgeData<SegmentationSupportResult>({
domain: "compositor",
action: "probeSegmentation",
});
return result.support;
} catch {
return "none";
}
}

export function createCompositorView(
rect: CompositorViewRect,
sources?: { screenPath?: string; webcamPath?: string; cursorPath?: string },
Expand Down
Loading
Loading