Modes
Table with columns: tag, output| tag | output |
|---|
<frame> | frame-shaped JSON (q/target/zoom/move/ev) |
<plan> / <coach> | coach-shaped JSON (+wb/flash/pose/say) |
<verify> plan:{...} | {ok, q, fix, say} — build the prompt with contract.sft_verify_prompt(plan) byte-exact |
Output fields
Key order is fixed and the model is trained to emit it exactly; decode with
contract.decode_frame / decode_coach / decode_verify rather than a bare
json.loads, which is what enforces the ranges and truncations below.
Table with columns: field, type, notes| field | type | notes |
|---|
q | int 0–9 | shot quality; 2 is the no-person default |
target | {cx, cy, h} | where the subject should sit, in percent units of the frame |
zoom | float | multiplier implied by target.h; the app caps auto-zoom at 3× |
|
Image input is 420×560 max (3:4), mean/std 0.5, patch 16, merge 2.
Usage
import torch
from transformers import AutoProcessor, AutoModelForImageTextToText
from PIL import Image
REPO = "CasperAtB612/autoportrait-qwen35-08b-v6"
processor = AutoProcessor.from_pretrained(REPO)
model = AutoModelForImageTextToText.from_pretrained(REPO, dtype=torch.bfloat16, device_map="auto")
image = Image.open("frame.jpg")
messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "<plan>"}]}]
prompt = processor.apply_chat_template(
messages, add_generation_prompt=True, enable_thinking=False,
)
inputs = processor(text=[prompt], images=[image], return_tensors="pt").to(model.device)
out = model.generate(**inputs, do_sample=False, max_new_tokens=120)
text = processor.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
import contract
print(contract.decode_coach(text))
Swap "<plan>" for "<frame>", or for a compliance check pass
contract.sft_verify_prompt(plan) as the text and decode with contract.decode_verify.
The prompt must match byte-for-byte — GOLDEN_SFT_VERIFY_PROMPT in contract.py pins it,
and python contract.py runs the whole contract self-test including the golden vectors.
Numbers (full val, 2,954 frames)
validity 0.999 · implied-crop IoU 0.6764 · move acc 0.582 · q-Spearman 0.604 · ev MAE 0.154.
Test: IoU 0.672 / validity 0.999. VERIFY: not-ok recall 0.925, pose-hard-negative recall 1.00
(the bad-pose-coaching scenario takes the not-ok path), ok-accuracy 0.976.
Operational rules
- batch=1 for variable-length prompts (VERIFY); tag-only prompts may batch.
- Non-thinking mode (
enable_thinking=False); empty <think> scaffold expected.
- Preprocessing: mean/std 0.5, patch 16, merge 2, max 420×560 (flat
preprocessor_config.json).
- Greedy decode, max 120 new tokens.
Parity tripwire (Mac/MLX)
verify_parity.py --mlx <converted> --emit mlx.jsonl --data-root parity then
--compare parity/hf.jsonl mlx.jsonl — must pass before on-device soak.
Intended use
Real-time framing and pose guidance inside a portrait-camera app, running on-device. It is
built to answer "where should this person sit in the frame, and what should they change" for
a single human subject in a 3:4 preview. It is not a general VQA, captioning, or aesthetic
scoring model, and it is not a face recognition or person identification model.
Limitations
wb is inert in this build (emits 0, false-nudge rate 0.0) and flash misses its
gates (recall 0.11). Both are safe to decode — the defaults are no-ops — but do not build
UI that expects a nonzero white-balance nudge or trusts "flash":"fill" from this build.
- Editorial-portrait bias. On full-body subjects that are small in frame, plans skew to a
tight crop. Mixing wider environmental-portrait ideals into training is an open item.
- Phone-camera domain shift is the known open eval. Training frames are stock-photo
renders; real preview frames from a phone sensor are not yet measured at scale.
move accuracy plateaus around 0.59–0.61 and is label-design-limited, not capacity-limited.
- Batched padded decode is corrupting, not merely slower, for variable-length prompts —
Qwen3.5 GatedDeltaNet. Keep batch=1 for VERIFY.
- The training recipe shows run-to-run instability that eval_loss does not reveal; any retrain
must re-pass artifact-level acceptance (geometry, validity, termination, verify) before ship.
- Guidance is aesthetic advice from a 0.8B model. It will sometimes be confidently wrong, and
it encodes conventional portrait composition norms rather than any universal standard.
Training data
Roughly 81k SFT records derived from stock photography — Pexels (primary), Unsplash
(secondary), and COCO non-person images for negatives — under licenses that permit ML
training. Photo-level sidecars retain source, photographer, and license per image. The image
collections themselves are not redistributed, here or elsewhere; this repo ships weights and
the 20-image parity set only.
Geometry, q, ev, and say labels are derived deterministically by contract.py; pose
tips are model-annotated with structured output enforced. Any photograph of the developer or
their family was annotated locally on-box only and never sent to a cloud API.
Full recipe and per-run analysis: HANDOFF.md plus the TRAINING_REPORT / DATASET_REPORT v6
sections (chain-I config A; the run-to-run instability note applies to retrains).
License
MIT for the fine-tuned weights, contract.py, verify_parity.py,
golden_vectors.json, and the docs. Two carve-outs, both spelled out in LICENSE:
- The base model,
Qwen/Qwen3.5-0.8B, is Apache-2.0. That license still governs the
base model's contribution to these derived weights, and its attribution requirements are
not waived by the MIT grant.
- The 20 images under
parity/parity_images/ are third-party photographs (19 Pexels,
1 COCO) included only as a fixed input set for the conversion parity check. They keep
their original licenses, are not sublicensed here, and should not be redistributed as a
collection.
No training data is distributed in this repository.