Skip to content

fix local Python setup for video gen on macOS Apple Silicon#515

Merged
atomantic merged 3 commits into
mainfrom
fix-local-python-setup
May 28, 2026
Merged

fix local Python setup for video gen on macOS Apple Silicon#515
atomantic merged 3 commits into
mainfrom
fix-local-python-setup

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

End-to-end fix for the "configure local Python → install packages → generate a video" flow on Apple Silicon. The previous flow hit five distinct walls in sequence; each one looked like the user's fault but was a real bug.

What was broken

  1. Detect/install was buried in the Settings drawer. The VideoGen "Local Python not configured" warning only linked to Settings. LocalSetupPanel now renders inline below the disconnected status pill so detect / install / Create-venv all happen in place; saving a new pythonPath re-polls status so the pill flips green automatically.

  2. detectPython() preferred Intel Anaconda over Homebrew on Apple Silicon. /opt/anaconda3/bin/python3 (often x86_64) won over /opt/homebrew/bin/python3 (arm64), and the install then failed with No matching distribution found for mlx because mlx ships arm64-only wheels. The detector now probes platform.machine() per candidate on darwin/arm64 and prefers a matching interpreter; /api/image-gen/setup/check returns interpreterArch + suggestedArm64Python so LocalSetupPanel can warn and offer a one-click "Switch to detected arm64 Python" button when the saved path is the wrong arch.

  3. PortOS-owned venvs from PEP 668 base Python were misreported as externally managed. Inside a venv, sysconfig.get_path("stdlib") resolves to the base interpreter's stdlib, so a venv from Homebrew Python inherited Homebrew's EXTERNALLY-MANAGED marker. The check now also verifies sys.prefix != sys.base_prefix and short-circuits to false for venvs.

  4. /api/video-gen/status lied about readiness. It was connected: !!pythonPath, so the green pill appeared the moment any path was saved — even with zero required packages installed. The user only saw the failure when they hit Generate. /status now probes imports via checkPackages and returns connected: false + a missingPackages list when anything's absent. LocalSetupPanel also notifies the parent on local check transition to "all installed" via onPackagesChanged, so the pill flips green without a manual refresh.

  5. The PyPI package literally named mlx_video is unrelated to the mlx_video.generate_av CLI the LTX renderer shells into — that CLI ships in mlx-video-with-audio. Both publish an import mlx_video namespace, so the missing-check passed against the wrong package and the spawn died with ModuleNotFoundError. pipNameFor('mlx_video') now returns mlx-video-with-audio>=0.1.35 on macOS; the import probe checks import mlx_video.generate_av to distinguish them; installPackages gained a pre-uninstall step driven by PIP_PRE_UNINSTALL so the conflicting plain mlx_video is removed before pip refuses to "downgrade" across the namespace collision. Mirrors the same conflict resolution scripts/setup-image-video.sh already did out-of-band.

Separately

  • Media job worker re-resolves pythonPath from live settings at dispatch time for every local-Python job (video + non-codex image), instead of using the snapshot captured at enqueue time. Symptom this fixes: user switches their Python in the UI, clicks Generate, and the worker still shells out to the previous interpreter — because the in-memory queue and data/media-jobs.json carried the old snapshot. Codex image jobs are unaffected.
  • probePythonHealth() consolidates the three subprocesses /setup/check used to fan out (checkPackages + isExternallyManaged + probePythonArch) into one Python invocation that returns everything as JSON. The standalone isExternallyManaged export became unused after the consolidation and was removed.
  • installPackages and installFlux2Venv shared a near-identical spawn-and-stream-stdout helper; extracted to a single streamSpawn().

Test plan

  • Unit: server/routes/videoGen.test.js adds a regression test for the "pythonPath set but packages missing → connected:false + missingPackages list" branch.
  • Unit: server/services/mediaJobQueue/index.test.js adds two tests — video job uses live pythonPath not the snapshot, codex image job's params stay untouched.
  • All tests pass: 8065 server + 602 client.
  • Manual: on an Apple Silicon Mac with Intel Anaconda preinstalled, open /video-gen and confirm the inline panel auto-selects the arm64 Homebrew Python.
  • Manual: from a fresh state, click "Create PortOS venv" → "Install N missing packages" → confirm the pre-uninstall log line appears (pip uninstall -y mlx_video (resolving package-name conflict)) and the pill flips green when install completes.
  • Manual: switch the saved pythonPath while a video job is queued; confirm the worker log shows pythonPath re-resolved from settings: {old} → {new} and uses the new interpreter.

atomantic added 2 commits May 28, 2026 11:56
End-to-end fix for the "configure local Python → install packages →
generate a video" flow on Apple Silicon. The previous flow hit five
distinct walls in sequence; each one looked like the user's fault but
was a real bug:

1. Video Gen's "Local Python not configured" warning only linked to the
   Settings drawer. Now LocalSetupPanel renders inline below the status
   pill so detect / install / Create-venv all happen in place; saving
   a new pythonPath re-polls status so the pill flips green.

2. detectPython() preferred /opt/anaconda3/bin/python3 over
   /opt/homebrew/bin/python3. On Apple Silicon, anaconda is often x86_64,
   and `mlx` ships arm64-only wheels, so the install fails with
   "No matching distribution found for mlx". The detector now probes
   platform.machine() per candidate on darwin/arm64 and prefers a matching
   interpreter; /api/image-gen/setup/check returns interpreterArch +
   suggestedArm64Python so LocalSetupPanel can surface the warning and
   offer a one-click "Switch to detected arm64 Python" affordance.

3. isExternallyManaged() false-positived on PortOS-owned venvs created
   from PEP 668 Homebrew Python — sysconfig.get_path("stdlib") inside a
   venv resolves to the base interpreter's stdlib, so the venv inherited
   Homebrew's EXTERNALLY-MANAGED marker. Now also checks
   sys.prefix != sys.base_prefix and short-circuits to false for venvs.

4. /api/video-gen/status returned `connected: !!pythonPath` — the pill
   went green as soon as any pythonPath was saved, even when none of the
   required packages were installed. Now probes the imports via
   checkPackages and returns connected:false + missingPackages list when
   anything is absent, which keeps the inline panel showing the install
   button. LocalSetupPanel also notifies the parent on local check
   transition to "all installed" so the pill flips without waiting on a
   manual refresh.

5. The PyPI package literally named `mlx_video` is unrelated to the
   `mlx_video.generate_av` CLI the LTX renderer shells into — that ships
   in `mlx-video-with-audio`. Both publish an `import mlx_video`
   namespace, so the missing-check passed against the wrong one and the
   spawn died with ModuleNotFoundError. pipNameFor('mlx_video') now
   returns 'mlx-video-with-audio>=0.1.35' on macOS; the import probe
   checks `import mlx_video.generate_av` to distinguish them;
   installPackages gained a pre-uninstall step driven by
   PIP_PRE_UNINSTALL so the conflicting plain `mlx_video` is removed
   before pip refuses to "downgrade" across the namespace collision.
   Mirrors what scripts/setup-image-video.sh already did out-of-band.

Separately:

* The media job worker now re-resolves pythonPath from live settings at
  dispatch time for every local-Python job, instead of using the
  snapshot captured at enqueue time. Symptom this fixes: user switches
  their Python in the UI, clicks Generate, and the worker still shells
  out to the previous interpreter — because the in-memory queue and
  data/media-jobs.json carried the old snapshot. Codex image jobs are
  unaffected (they don't run a local Python).

* probePythonHealth() consolidates the three subprocesses /setup/check
  used to fan out (checkPackages + isExternallyManaged + probePythonArch)
  into one python invocation that returns everything as JSON.
  isExternallyManaged becomes a thin wrapper; the standalone export was
  unused after the consolidation and is removed.

* installPackages and installFlux2Venv shared a near-identical
  spawn-and-stream-stdout helper; extracted to a single streamSpawn().

Tests: 8061 server + 602 client passing (incl. new coverage for
mediaJobQueue's live-pythonPath re-resolution and videoGen /status's
missing-packages branch). Test count grew by 4 since main.
…warning to darwin/arm64

* LocalSetupPanel: the inline path input fired onPythonPathChange on
  every keystroke. In the VideoGen consumer that meant a settings PATCH
  + a ~1-2s status re-probe per character. Decouple via local draft
  state; commit on 800ms debounce or onBlur. Programmatic updates
  (Detect, Switch-to-arm64, Create-venv) still call onPythonPathChange
  directly so they take effect immediately.
* /api/image-gen/setup/check: generic interpreterArch !== HOST_ARCH
  would false-positive on Windows (Python reports `AMD64` vs Node's
  `x86_64`) and on hypothetical arm64 Linux where mlx isn't even in
  REQUIRED_PACKAGES. Gate the warning to darwin/arm64 with an x86_64
  interpreter — the only configuration where mlx actually breaks.

The third codex finding (hfDownload.js falls back to broken FLUX.2 venv)
is in a file not yet on origin and not part of this PR's surface; left
for whichever branch ships hfDownload.
@atomantic atomantic force-pushed the fix-local-python-setup branch from af3c2a2 to e076590 Compare May 28, 2026 19:45
The gemini /do:review pass reviewed the whole repo instead of the branch
diff (a known issue tracked under [triage-gemini-out-of-scope-findings]).
All 6 findings were in files PR #515 doesn't touch.

* Findings 5+6 (Express body limit, * CORS): explicitly intentional per
  CLAUDE.md Security Model — single-user private-network deployment.
* Findings 1-4 (commands.js naive split, fileUtils non-atomic write,
  two cosAgents non-atomic writes): real but pre-existing, in files
  unrelated to this PR's surface. Appended to PLAN.md as a new triage
  entry so they're not silently dropped.

Verify each finding's reproducibility before acting; gemini's /do:review
has a track record of pattern-matching false-positives.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the end-to-end Apple Silicon local-Python flow for video generation by surfacing setup inline, correcting arch-aware Python detection, fixing PEP 668 venv mis-detection, making /video-gen/status truthful about missing packages, and resolving the mlx_video vs mlx-video-with-audio package-name collision. Also re-resolves pythonPath from live settings at media-job dispatch time so a stale snapshot can't poison the spawn.

Changes:

  • Server: arch-aware detectPython + new probePythonHealth (consolidates 3 subprocesses, fixes venv externally-managed detection), mlx_video import probe + pre-uninstall, /video-gen/status probes imports, media-job worker re-resolves live pythonPath.
  • Client: inline LocalSetupPanel on VideoGen with debounced path commit, arch-mismatch warning + one-click switch, onPackagesChanged notify on transition to all-installed.
  • Tests: regression coverage for /video-gen/status missing-packages branch and live-pythonPath re-resolution at dispatch.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
server/lib/pythonSetup.js Adds HOST_ARCH, arch probes, arm64-preferring detectPython, consolidated probePythonHealth (handles venv stdlib quirk), mlx_video deeper-import probe, PIP_PRE_UNINSTALL, shared streamSpawn.
server/routes/imageGen.js /setup/check now uses probePythonHealth and returns interpreterArch, hostArch, archMismatch, suggestedArm64Python.
server/routes/videoGen.js /status now probes packages via checkPackages and returns connected, reason, missingPackages.
server/routes/videoGen.test.js Adds regression test for missing-packages branch of /status.
server/services/mediaJobQueue/index.js Re-resolves pythonPath from live settings for video and non-codex image jobs at dispatch.
server/services/mediaJobQueue/index.test.js Adds tests for live-pythonPath re-resolution and codex params.pythonPath left untouched.
client/src/pages/VideoGen.jsx Inline LocalSetupPanel below status pill; handleSavePythonPath does the imageGen-slice round-trip and re-polls status.
client/src/components/settings/LocalSetupPanel.jsx Adds debounced draft input, onPackagesChanged transition notify, arch-mismatch banner + Switch-to-arm64 button.
.changelog/NEXT.md Changelog entries for the 6 user-visible fixes.
PLAN.md New deferred items (helper extractions, cache, hook extraction, arch tests) and gemini PR #515 triage entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@atomantic atomantic merged commit c9e68ce into main May 28, 2026
3 checks passed
@atomantic atomantic deleted the fix-local-python-setup branch May 28, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants