diff --git a/client/platform/desktop/backend/native/interactive.ts b/client/platform/desktop/backend/native/interactive.ts index 3df410405..ab1a8ac74 100644 --- a/client/platform/desktop/backend/native/interactive.ts +++ b/client/platform/desktop/backend/native/interactive.ts @@ -201,12 +201,15 @@ export class InteractiveServiceManager extends EventEmitter { const segConfig = npath.join(pipelines, 'interactive_segmenter_default.conf'); const stereoConfig = npath.join(pipelines, 'interactive_stereo_default.conf'); - // -I: isolated mode — do not prepend the process cwd to sys.path. Without - // this, a sibling folder named "coverage" (e.g. Vitest HTML output under - // client/coverage/) shadows Python's coverage package, breaks numba import, - // and prevents ocv_watershed from registering during plugin load. + // -s: ignore the per-user site-packages dir so a stray package in + // ~/.local (e.g. a conflicting "coverage" that breaks numba import and + // prevents ocv_watershed from registering) can't shadow VIAME's. We can't + // use -I/-E here: those also discard PYTHONPATH, which setup_viame.sh uses + // to expose the viame module in from-source/system-python builds. The + // process cwd is forced to viamePath (below), which has no shadowing + // modules, so the cwd entry on sys.path is harmless. const pyCommand = [ - `"${pythonExe}" -I -m viame.core.interactive_service`, + `"${pythonExe}" -s -m viame.core.interactive_service`, `--segmentation-config "${segConfig}"`, `--stereo-config "${stereoConfig}"`, ].join(' '); @@ -456,11 +459,19 @@ export class InteractiveServiceManager extends EventEmitter { settings: Settings, calibration?: StereoCalibration, calibrationFile?: string, - ): Promise<{ success: boolean; error?: string }> { + ): Promise<{ success: boolean; error?: string; launchFailed?: boolean }> { try { await this.ensureStarted(settings); } catch (err) { - return { success: false, error: err instanceof Error ? err.message : String(err) }; + // The service process couldn't even start (missing interpreter, import + // failure, etc.). This is an infrastructure error, distinct from a benign + // per-dataset enable failure (e.g. missing calibration), so flag it so the + // caller always surfaces it rather than degrading silently. + return { + success: false, + launchFailed: true, + error: err instanceof Error ? err.message : String(err), + }; } try { const response = await this.sendRequest({ diff --git a/client/platform/desktop/backend/native/linux.ts b/client/platform/desktop/backend/native/linux.ts index 77041b879..1bc3a077e 100644 --- a/client/platform/desktop/backend/native/linux.ts +++ b/client/platform/desktop/backend/native/linux.ts @@ -65,7 +65,14 @@ function getViameConstants(settings: Settings): viame.ViameConstants { } function getViamePythonExe(settings: Settings): string { - return npath.join(settings.viamePath, 'bin', 'python'); + // Packaged binary installs bundle an interpreter at bin/python. From-source + // builds against system python have no such interpreter and instead rely on + // setup_viame.sh putting the correct python on PATH, so fall back to that. + const bundled = npath.join(settings.viamePath, 'bin', 'python'); + if (fs.existsSync(bundled)) { + return bundled; + } + return 'python'; } async function validateViamePath(settings: Settings): Promise { diff --git a/client/platform/desktop/frontend/api.ts b/client/platform/desktop/frontend/api.ts index 7d6acd865..4f1fb1c71 100644 --- a/client/platform/desktop/frontend/api.ts +++ b/client/platform/desktop/frontend/api.ts @@ -440,7 +440,7 @@ interface StereoTransferPointsResponse { async function stereoEnable( calibration?: StereoCalibration, calibrationFile?: string, -): Promise<{ success: boolean; error?: string }> { +): Promise<{ success: boolean; error?: string; launchFailed?: boolean }> { return window.diveDesktop.invoke('stereo-enable', { calibration, calibrationFile }); } diff --git a/client/platform/desktop/frontend/components/ViewerLoader.vue b/client/platform/desktop/frontend/components/ViewerLoader.vue index a56d6f900..a24099282 100644 --- a/client/platform/desktop/frontend/components/ViewerLoader.vue +++ b/client/platform/desktop/frontend/components/ViewerLoader.vue @@ -442,7 +442,14 @@ export default defineComponent({ } const result = await stereoEnable(undefined, stereoCalibrationFile); if (!result.success) { - throw new Error(result.error || 'Failed to enable stereo service'); + // launchFailed means the backend service couldn't even start (e.g. + // missing python interpreter or a broken import). That is a real + // error and must always be surfaced, even on the load-time + // auto-enable, instead of degrading silently like a missing + // calibration would. + const err = new Error(result.error || 'Failed to enable stereo service'); + (err as Error & { launchFailed?: boolean }).launchFailed = result.launchFailed; + throw err; } stereoEnabled.value = true; stereoLoadingDialog.value = false; @@ -454,15 +461,18 @@ export default defineComponent({ } catch (err) { stereoEnabled.value = false; console.error('[Stereo] Failed to enable interactive stereo:', err); - if (userInitiated) { - // The user explicitly enabled a feature: revert the toggles and - // surface the failure in a dialog. + const launchFailed = (err as Error & { launchFailed?: boolean }).launchFailed === true; + if (userInitiated || launchFailed) { + // The user explicitly enabled a feature, or the backend service + // failed to launch (an infrastructure error). Either way, revert the + // toggles and surface the failure in a dialog. disableStereoFeatureToggles(); stereoLoadingError.value = err instanceof Error ? err.message : String(err); stereoLoadingDialog.value = true; } else { - // Load-time auto-enable failed: degrade silently and keep the - // toggle states so a later calibrated dataset still works. + // Load-time auto-enable failed for a benign reason (e.g. an + // uncalibrated stereo dataset): degrade silently and keep the toggle + // states so a later calibrated dataset still works. stereoLoadingDialog.value = false; } }