Skip to content

test(qa): MLX image-gen QA lane — t2i matrix + adversarial VLM scoring (stacked on #311)#315

Open
ziyu4huang wants to merge 5 commits into
calesthio:mainfrom
ziyu4huang:feat/qa-mlx-image
Open

test(qa): MLX image-gen QA lane — t2i matrix + adversarial VLM scoring (stacked on #311)#315
ziyu4huang wants to merge 5 commits into
calesthio:mainfrom
ziyu4huang:feat/qa-mlx-image

Conversation

@ziyu4huang

Copy link
Copy Markdown
Contributor

What

Fills a QA lane for the mlx_image provider (shipped in #311): a small deterministic t2i matrix + adversarial VLM scoring. Dual purpose — (1) the local-generation slot in QA_PLAN.md, 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.

tests/qa/test_02_mlx_image_gen.py + a QA_PLAN.md table/run-order row.

⚠️ Stacked on #311 — merge #311 first

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:

tests/qa/QA_PLAN.md                |  2 ++
tests/qa/test_02_mlx_image_gen.py  | 264 ++++++++++++++++++++++++++++++++

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):

  1. MLXImage().execute({prompt, width, height, seed, output_path}) — generates via the local MLX provider ($0, Apple-Silicon-native), shells out to mlx-movie-director run.py.
  2. run.py caption <img> --style score --samples 3 — scores it with Qwen3-VL via LM Studio. --samples 3 writes 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.
  3. Writes 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 on localhost:1234. All execution is under if __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.py

Baseline receipt (mlx-zimage, --samples 3)

tag overall detail sharpness composition prompt artifacts flag
portrait 6 6 8 8 9 5 ⚠artifacts
still_life 9 8 9 9 10 9
hands 6 5 8 8 9 4 ⚠artifacts

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 artifacts rising on portrait/hands; a regression that breaks generation shows up as a FAILURE exit.

🤖 Generated with Claude Code

ziyu4huang and others added 5 commits July 6, 2026 22:58
…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>
@ziyu4huang ziyu4huang requested a review from calesthio as a code owner July 6, 2026 21:59

@calesthio calesthio left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

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