PreFlight: Observe (Qwen-VL)
Observe — a local Qwen-VL that describes your image and refuses to judge it
- image
- observations_json
- raw_response
What it's for
You generate something borderline - a swimwear shot, a gym clip - and then wonder whether Instagram will demote it or TikTok will eat it. PreFlight is a three-node pack that answers that locally, and PreFlight: Observe is the sensor half: a Qwen3-VL model in your graph that writes down what is visually present - garment, exposure, framing, pose, any text burned into the frame - as strict JSON.
It deliberately does not judge. Judgement lives in PreFlight: Report, a plain-Python rules engine, and that split is the whole design: when a verdict looks wrong you can ask whether the model misread the image or the rule is just stupid. It also means observations stay reusable against updated rules without touching the GPU.
How it works
A local Qwen3-VL (via transformers) gets a system prompt holding a JSON schema with eleven fields - garment, setting, framing, pose, exposure, see_through_or_wet, nudity_or_sexual_act, subject_appears_under_18, motion_flags, visible_text, confidence - plus explicit instructions that it is a sensor, not a judge, and must return only that object.
Decoding is greedy, fixed seed, 300-token cap - the same image and settings give byte-identical output run to run, which is what makes a recorded prediction comparable to last week's. The node pulls the first balanced {...} out of whatever the model wrote (fences and preamble get skipped happily), retries once with a corrective instruction if that isn't a real observation, and after that gives you {"error": ...}. The graph keeps running; Report fails closed to UNKNOWN rather than pretending.
Video is handled by sampling frames evenly and sending them in one call, so the model can infer motion; appearance fields come from the most revealing frame. Flip side: a batch of unrelated stills gets treated as video frames, so ten different images turn into a description of the "motion" between them.
Each observation is stamped with its schema and prompt version, which is how old records stay interpretable.
The inputs that matter
image- the only thing you actually wire. Single image, or a frame batch for video.model_name-Qwen3-VL-8B-Instructby default, with 4B and 2B for smaller cards. Only Instruct variants are listed on purpose; the Thinking variants emit reasoning text that breaks strict JSON output.quantization-8-bitdefault,4-bit, orNone (FP16). Both quantized paths use bitsandbytes and are CUDA-only.max_frames- 6 by default, 1–16, sampled evenly.keep_model_loaded- leave it on while working through a folder; off if you need the card back for the actual generation.
attention_mode (auto tries flash-attention-2, falls back to sdpa) and device are fine at their defaults; quantized and CPU paths force sdpa regardless.
Outputs are two strings: observations_json, which goes into PreFlight: Report's observations_json input, and raw_response, for debugging only - if it contains a --- retry --- banner, the first attempt wasn't valid JSON.
Rough VRAM: the default 8B is ~7 GB at 8-bit (~12 GB at FP16), 4B is ~3.5 GB, 2B is ~2.5 GB.
Install
ComfyUI Manager, search PreFlight, or:
cd ComfyUI/custom_nodes
git clone https://github.com/0xBeycan/ComfyUI-PreFlight
cd ComfyUI-PreFlight
pip install -r requirements.txt
That buys you transformers, accelerate, bitsandbytes, huggingface_hub, pillow and numpy. torch is deliberately not listed - it comes from your ComfyUI runtime, which is the correct call. Restart, and the three nodes appear under the PreFlight category.
The model downloads itself on first run into ComfyUI/models/LLM/Qwen-VL/<name> - shared with other Qwen-VL packs, so an 8B you already have gets reused rather than re-fetched. First run looks frozen while that happens; watch the console.
Where it goes wrong
You get {"error": ...} and raw_response is prose, not JSON. Assuming nothing crashed, that's usually a refusal. Qwen3-VL's Instruct models carry assistant-style refusals - the reason the community runs abliterated builds for explicit captioning - and a sensor pointed at the content you're nervous about is exactly where that bites. models.json exists for this: copy an entry, point repo_id at an abliterated repo, no code change. A de-censored sensor is a different sensor though, so re-calibrate if you switch.
VRAM pressure. You're holding a vision LLM alongside your diffusion model now. Drop to 4B or 2B, or 4-bit, or set keep_model_loaded off. Same reason captioning nodes make you budget for two models instead of one.
8-bit on CPU. bitsandbytes is a CUDA thing; if you set device to cpu, use None (FP16). And leave attention_mode on auto - flash-attention-2 has to be compiled and usually isn't installed, so the sdpa fallback is doing real work.
One last thing: the node loads model repos with trust_remote_code=True, which means the repository's own Python executes. Fine for the official Qwen repos in models.json; think twice before pointing it at a random upload.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | A single image or a video frame batch. | |
| model_name | COMBO | Qwen3-VL-8B-Instruct | Qwen-VL model (from models.json). Downloaded once into models/LLM/Qwen-VL and reused across packs. |
| quantization | COMBO | 8-bit | 4-bit / 8-bit (bitsandbytes) shrink VRAM; None keeps FP16. Quantized paths force sdpa attention. |
| attention_mode | COMBO | auto | auto tries flash_attention_2 then falls back to sdpa. Quantized / CPU always use sdpa. |
| max_frames | INT | 61–16 | For video: how many frames to sample evenly from the batch. All are sent in one model call. |
| keep_model_loaded | BOOLEAN | true | Keep the model resident in VRAM between runs. Turn off to free VRAM after each run. |
| device | COMBO | auto | auto picks cuda when available, else cpu. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| observations_json | STRING | — |
| raw_response | STRING | — |