test(qa): MLX image-gen QA lane — t2i matrix + adversarial VLM scoring (stacked on #311)#315
test(qa): MLX image-gen QA lane — t2i matrix + adversarial VLM scoring (stacked on #311)#315ziyu4huang wants to merge 5 commits into
Conversation
…ontage Add the first Apple-Silicon-native image provider. `mlx_image` shells to the sibling mlx-movie-director repo's `run.py image` (Z-Image / Flux2 Klein / Lens) and is auto-discovered by `image_selector` with zero selector edits — the selector-plus-provider seam from the story→image review. Closes OpenMontage's biggest verified image gap: a local, $0 path that advertises the full control surface (controlnet / img2img / reference_image / faceswap / lora / multi_lora) that today only grok_image's edit mode partially covers. The scorer rewards it accordingly — it ranks calesthio#1 by weighted score (control=1.0, cost_efficiency=1.0) and survives the edit-mode candidate filter. - tools/graphics/mlx_image.py: provider; action routing (t2i/i2i/controlnet/ faceswap), CLI arg mapping, availability gate (arm64 + MLX_MOVIE_DIRECTOR_DIR + venv + models), JSON_SUMMARY output parsing with dir-scan fallback. - .agents/skills/mlx-movie-director/SKILL.md: Layer-3 bridge (env contract, pipeline choice, controlnet/i2i/faceswap mapping, LoRA stacks). - tests/tools/test_mlx_image.py: 24 tests — registration, capability surface, zero-edit discovery, edit-filter survival, gate (env/venv/full), action routing, arg building, mocked-subprocess execute success + error paths. - docs/REVIEW-story-to-image.md, docs/REVIEW-image-to-video-voice.md: the grounded reviews that motivated this provider (gap analysis + MLX side-by-side). Verified end-to-end: a real Z-Image generation (828 KB PNG, 17.6s, $0) flows through image_selector -> mlx_image -> run.py and lands at output_path with asset_manifest-ready provenance (source_tool=mlx_image, model=mlx-zimage, seed). Two upstream MLX-repo deps (opencv-python, mflux) are missing from its requirements.txt — noted in install_instructions + the skill (tracked upstream). Co-Authored-By: Claude <noreply@anthropic.com>
The motion stitch of the OM↔MLX seam. `mlx_video` wraps LTX-2.3 22B (`run.py video generate` / `video t2i2v`) and is auto-discovered by `video_selector` with zero selector edits. Refined value (REVIEW-image-to-video-voice §7): OM's four $0 local i2v providers (wan/hunyuan/ltx/cogvideo) need CUDA diffusers and don't run on Apple Silicon. MLX LTX-2.3 is the MPS-native i2v/t2v path — a free offline local default, NOT a premium-cinema replacement (deliberately does NOT advertise cinematic_quality/lip_sync/multi_shot/native_audio). - tools/video/mlx_video.py: provider; action routing (prompt-only → t2i2v 3-stage; image present → generate I2V; explicit action override), LTX-2.3 arg mapping (frames 8k+1, /64 dims), availability gate reusing the mlx_image pattern, dir-scan output fallback (video subparser doesn't register --json-summary — an upstream MLX-repo asymmetry, documented). fallback_tools excludes image_selector so a motion-required brief can never silently degrade to a still. - .agents/skills/mlx-movie-director/SKILL.md: video section (action routing table, honest surface, motion-required governance note). - tests/tools/test_mlx_video.py: 25 tests — registration, honest-surface assertion (premium flags MUST be absent), zero-edit discovery, i2v filter survival, gate (env/venv/full), action routing, arg building, output parsing, mocked-subprocess execute success + error paths. End-to-end smoke status: DEFERRED to G1. The MLX video path needs a heavier upstream dep stack than requirements.txt provides — cv2, mflux, and the three ltx-2-mlx workspace members (ltx-core-mlx / ltx-pipelines-mlx / ltx-trainer, installable per-member since the ltx-2-mlx root fails uv install under PEP 639), plus a transformers/mlx_lm version conflict at the NewlineTokenizer register call. The provider logic is fully covered by mocked-subprocess tests; the image analog (mlx_image) is smoked end-to-end. install_instructions documents the full chain. Co-Authored-By: Claude <noreply@anthropic.com>
…LX env The third leg of the MLX-runtime trio (image gen + video gen + VLM analysis), all bridging the sibling mlx-movie-director run.py on Apple Silicon. mlx_caption (tools/analysis/mlx_caption.py): - Bridges run.py `caption` (Qwen3-VL via LM Studio localhost:1234/v1) into the `analysis` capability — the $0, offline, private image/video-understanding path. Fills a verified gap: no tool used local LM Studio before (video_understand is a direct-transformers VLM that downloads+loads a model each run; mlx_caption talks to an already-running server the box already serves mlx_image/video from). - Auto-discovered by the registry (13th analysis tool, zero selector edits). - Availability gate: MLX_MOVIE_DIRECTOR_DIR + venv + LM Studio socket-probe (need_models=False — caption talks to LM Studio, not the mlx-models stack). - 21 spawn-free tests (capability surface, env gates incl. LM Studio down, arg-mapping for all --style/--lang/--model/--api-url combos, output parsing). Shared env module (tools/_mlx/env.py): - Factors the duplicated _resolve_env() out of mlx_image + mlx_video (the TODO both files carried: 'if a third MLX provider appears, factor into tools/_mlx/env.py'). resolve_mlx_env(need_models, need_lm_studio) serves all three. All 49 existing mlx_image/mlx_video tests still green.
The two test_status_available_with_full_env tests asserted env["ok"] is True unconditionally, but resolve_mlx_env() correctly rejects non-Apple-Silicon hosts (MLX runs on Apple Silicon only). So both tests failed on the Windows/ AMD64 review machine and on ubuntu x86_64 CI — the production code was right, the tests were not portable. Mock platform.machine() -> "arm64" inside these two tests so the full available-path (including the arch guard) executes on ANY host, and drop the now-stale `if env["arm64"]` skip-guard in favor of direct assertions. The unavailable-path tests stay honest (they don't force arch). Verified: - real arm64: 70 passed (3 MLX suites) - simulated AMD64: 66 passed, 4 skipped (unrelated caption/LM Studio), 0 failed — incl. both previously-failing tests green under the simulated host Addresses calesthio review on calesthio#311 (CHANGES_REQUESTED). Co-Authored-By: Claude <noreply@anthropic.com>
Fills a QA slot for the mlx_image provider (shipped in feat/mlx-image-provider): generates a small deterministic t2i matrix (portrait / still_life / hands — each stresses a known weak spot) via the local MLX provider, then scores every output with `run.py caption --style score --samples 3` (Qwen3-VL via LM Studio; median of 3 samples to survive the local Gemma-4 VLM's image-dependent empty-response flakiness). Writes per-image scores + flags to tests/qa/output/qa_image_gen_receipt.json and leaves the PNGs for human inspection per the QA_PLAN protocol. Dual purpose: (1) the QA_PLAN test_02 slot for local generation, and (2) a regression gate for mlx_image — a score collapse or generation failure here catches a provider/env regression before it reaches a user. Exit code is non-zero only on generation/scoring FAILURE; low scores are flagged ⚠ (the scorer is deliberately adversarial) but do not fail the run, since QA is human-inspected. NOT CI pytest — needs MLX_MOVIE_DIRECTOR_DIR + mlx venv + staged models + Apple Silicon + LM Studio on localhost:1234. All execution is under `if __name__`, so pytest collection imports it cleanly and runs 0 tests. Baseline receipt (mlx-zimage, this run): portrait overall=6 artifacts=5 ⚠ (plasticky skin — known zimage base trait) still_life overall=9 artifacts=9 (excellent) hands overall=6 artifacts=4 ⚠ (finger fusion — known AI weak spot) Stacked on feat/mlx-image-provider (imports mlx_image); retarget to main once that lands. Co-Authored-By: Claude <noreply@anthropic.com>
calesthio
left a comment
There was a problem hiding this comment.
Thank you for adding this QA lane. The idea is useful, but I?m requesting changes because this PR is explicitly stacked on #311, and #311 currently has requested changes / failing portability tests. As-is, this PR?s diff includes the entire MLX provider family, so it is not safe to approve independently.
Verification I ran on this branch:
python -m py_compile tests/qa/test_02_mlx_image_gen.py
# passed
python -m pytest tests/qa/test_02_mlx_image_gen.py -q
# no tests ran
git diff --check origin/main...HEAD
# clean
The ?no tests ran? result matches the PR description that this is a manual QA script, not a CI pytest suite, so that part is not by itself a blocker. The blocker is dependency hygiene: please rebase this after #311 is merge-ready/merged so the review diff collapses to the intended QA files only. Once the branch contains just tests/qa/QA_PLAN.md and tests/qa/test_02_mlx_image_gen.py, this can be reviewed on its own merits.
I did not find an actionable security issue in the reviewed QA diff, but I?m not approving a stacked PR while the base provider PR is still blocked.
What
Fills a QA lane for the
mlx_imageprovider (shipped in #311): a small deterministic t2i matrix + adversarial VLM scoring. Dual purpose — (1) the local-generation slot inQA_PLAN.md, and (2) a regression gate formlx_image: a score collapse or generation failure here catches a provider/env regression before it reaches a user.tests/qa/test_02_mlx_image_gen.py+ aQA_PLAN.mdtable/run-order row.This imports
from tools.graphics.mlx_image import MLXImage, which lands in #311. Until #311 merges, this PR's diff includes #311's commits (that's why it looks large). Once #311 merges to main, GitHub recomputes the diff against main and it collapses to just the two files this PR adds:Please review/merge #311 first; this one becomes a clean 2-file PR after.
What it does
For each row in a fixed-seed matrix (portrait / still_life / hands — each stresses a known weak spot):
MLXImage().execute({prompt, width, height, seed, output_path})— generates via the local MLX provider ($0, Apple-Silicon-native), shells out to mlx-movie-directorrun.py.run.py caption <img> --style score --samples 3— scores it with Qwen3-VL via LM Studio.--samples 3writes the per-dimension median of 3 VLM passes, which is necessary because the local Gemma-4 VLM is image-dependent and can return an empty response on a single pass.tests/qa/output/qa_image_gen_receipt.json(per-image scores + flags + gen metadata) and leaves the PNGs for human inspection per the QA_PLAN protocol.Exit code is non-zero only on a generation or scoring failure (provider broke / caption broke). Low scores are flagged ⚠ in the table — the scorer is deliberately adversarial ("FIND flaws, not praise"), so soft thresholds flag rather than fail; QA is human-inspected.
Not CI pytest
Needs
MLX_MOVIE_DIRECTOR_DIR+ the mlx venv + staged models + Apple Silicon + LM Studio onlocalhost:1234. All execution is underif __name__ == "__main__", so pytest collection imports the module cleanly and runs 0 tests — it won't break CI. Run manually:MLX_MOVIE_DIRECTOR_DIR=/path/to/video_generation \ .venv/bin/python tests/qa/test_02_mlx_image_gen.pyBaseline receipt (mlx-zimage,
--samples 3)The flags are expected known base-model traits, not regressions: plasticky/too-smooth skin (a documented zimage base characteristic) and finger fusion (a known AI weak spot). The still_life scores 9 across the board. This is the honest baseline — a future change that improves skin texture shows up as
artifactsrising on portrait/hands; a regression that breaks generation shows up as a FAILURE exit.🤖 Generated with Claude Code