Spaces:
Running
A newer version of the Gradio SDK is available: 6.20.0
Vision Prompt Roles: Split System and User Prompt
Question being answered
Is it better to split the analysis portion and the generation portion into different system prompts and user prompts? Would this help the model interpret the input and produce better output?
Short answer: Yes, split β but split by chat role inside the single analysis call, not into two model calls. The "generation" step is not a model step in this codebase, so the only useful split is system-vs-user within the one vision call. Wiring that up is a small, safe change that aligns the prompt with how the instruct/reasoning model was trained.
Findings
Finding 1 β There is only one model step; "generation" is deterministic
The pipeline is:
image -> [MODEL] vision analysis -> validated JSON -> [NO MODEL] Three.js scene
- Scene rendering is deterministic browser-side Three.js built from the
validated analysis JSON (
AGENTS.md:34-35,AGENTS.md:196-198). InferenceClient.generate_scene(snap2sim/backend.py:47-51) does no model inference β it validates the analysis and picks arender_mode. Model- authored scene HTML/JS was intentionally removed (SECURITY.md:96-97).
Implication: "split analysis and generation into different prompts" cannot
mean two model calls. There is no generation model call to separate, and
adding a second round-trip would double an already slow path (~35s observed,
300s timeout at modal_app.py:392) for zero quality gain, since the scene is
deterministic. Do not introduce a second inference step.
Finding 2 β The system prompt is currently dead code
VISION_SYSTEM_PROMPT(snap2sim/prompts.py:5-10) is defined but never imported or used.modal_app.pyimports onlybuild_vision_prompt(modal_app.py:20).- The real call,
run_llamacpp_prompt(modal_app.py:291-334), passes the prompt through a single-pflag (modal_app.py:308-309). There is no system message. - Net effect: the model receives one large user turn that mixes four different
concerns β role framing, the analytical task, the full output schema, and
field-by-field formatting rules (
build_vision_prompt,snap2sim/prompts.py:12-70).
So today there is effectively no system/user separation, and the one "system" string we wrote is doing nothing.
Finding 3 β The runtime supports a real role split
llama-mtmd-cliaccepts a separate system message via-sysalongside the user prompt-p, and applies the model's chat template. (Confirmed against llama.cppmtmd-cliusage.)- The model is
unsloth/NVIDIA-Nemotron-3-Nano-...-Reasoning-GGUF(AGENTS.md:20), an instruct/reasoning model. Such models are trained to treat the system turn as persistent role + constraints and the user turn as the immediate request. Collapsing everything into the user turn is off-distribution and dilutes the per-request ask.
Why a role split should help interpretation and output
- Aligns with training. Stable behavior/output-contract belongs in system; the moment-to-moment ask belongs in user. The model already expects this shape.
- Sharpens the ask. The user turn becomes short and image-focused ("Analyze the component in this photo β¦") instead of being buried under ~40 lines of schema rules.
- Separates invariants from the request. The schema, shape/motion vocabulary, and hard rules are constant across every image; they read as policy in system, not as part of this request.
- Stops wasting the role channel. We already wrote a system prompt; right now it is ignored.
This is a low-risk change: it does not touch the schema, validator, coercion
(snap2sim/schema.py, snap2sim/model_io.py), or the deterministic renderer.
Recommendation
Adopt a system/user role split within the single vision call. Concretely:
- System message = the invariant output contract:
- role + reasoning frame (it is a reasoning model; brief reasoning then JSON),
- the "emit exactly one JSON object, no markdown" rule,
- the JSON skeleton,
- the shape -> use-for and motion -> use-for vocabulary guide,
- the hard field rules (
size: [x,y,z], numeric axis vectors, 2β6 parts, noradius/height/length/width/depth).
- User message = only the per-image ask: "Analyze the hardware component in
this photo as a cutaway mechanism and return the analysis JSON." (The image is
attached via
--image.)
Keep it one inference call. Keep temperature, token, context, and timeout
budgets as-is (modal_app.py:388-394) β this is a prompt-structure change, not
a budget change.
Implementation plan (completed)
snap2sim/prompts.py- Repurpose
VISION_SYSTEM_PROMPTto hold the full invariant contract (role + JSON-only rule + skeleton + shape/motion vocabulary guide + hard field rules) currently living insidebuild_vision_prompt. - Reduce
build_vision_prompt()to the short per-image ask only. Consider renaming itbuild_vision_user_prompt()(keep a back-compat alias if any diagnostic entrypoint imports the old name). - Optionally add
build_vision_messages()returning(system, user)so call sites have one source of truth.
- Repurpose
snap2sim/prompts.py(smoke test parity, optional)- The smoke-test prompt is a separate inline string
(
modal_app.py:211-234). Leave functionally as-is, but it can reuse the same system message for realism. Not required for the demo.
- The smoke-test prompt is a separate inline string
(
modal_app.py- In
run_llamacpp_prompt(:291-334), add an optionalsystem_prompt: str | None = Noneparameter and append-sys <system_prompt>tocmdwhen provided. Verify the exact flag name against the deployedllama-mtmd-clibuild before relying on it (-sys); if the installed build does not accept it, fall back to prepending the system text to-pso behavior never regresses. - In
analyze_image_llamacpp_payload(:385-400), pass the new system message + the short user prompt. - Update the diagnostic entrypoints that build the prompt
(
run_analysis_raw_checknear:489,run_analysis_endpoint_check, remote check near:512) to use the same two-part prompt so diagnostics match production.
- In
Verify before/after (no regressions, measured improvement)
- Local: schema/parser checks + FastAPI
TestClientfor/,/analyze_image,/generate_scenestill pass. - Modal: run
run_analysis_raw_checkandrun_analysis_endpoint_checkagainst the synthetic target image; confirm strict JSON still parses and latency is comparable. Compare parse success / field quality vs. the current single--pprompt on a few representative real photos before committing the prompt change. - Confirm the flag actually took effect (e.g. inspect that
-sysis honored, not silently ignored) before trusting the split.
- Local: schema/parser checks + FastAPI
Then deploy following the normal path (Modal deploy of
analyze_image_llamacpp, GitHubmainpush, GitHub->HF sync, and Space verification), per the existing workflow inAGENTS.md.
Final submission note, 2026-06-15: this prompt-role split shipped before the
public build-small-hackathon/Snap2Sim submission.
Explicitly NOT recommended
- Two separate model calls (an "analysis" call feeding a "generation" call). There is no model generation step; the scene is deterministic. This would only add latency and a failure mode.
- Re-introducing model-authored scene HTML/JS. Prohibited by
SECURITY.md:96-97andAGENTS.md:35.
Open question for the user
The recommendation assumes you meant "should the prompt be structured into system vs user roles" (yes), not "should the app make two separate model requests" (no β there is no model generation step). If you specifically wanted the model to also generate the scene/animation (re-adding a model generation call), that conflicts with the current deterministic-renderer security decision and should be discussed separately before implementing.