Capra-v3 is a vision-language navigation (VLN) policy built on
Qwen3-VL-4B-Instruct. Given a navigation instruction and a short history
of egocentric RGB observations, it predicts navigation as plain text: a
pixel-space goal in the current image plus a facing direction, a turn, or a
stop. It is a stock Qwen3VLForConditionalGeneration (no custom modeling
code, no vocab extension), so it loads with stock transformers and runs
with a plain generate() call.
This repo contains the v3 checkpoint (n8, 1 epoch). Unlike the
trajectory-token line (CapraXL,
which emits delta-bin trajectory tokens), Capra-v3 emits the v3 pixel-goal
text contract — a single "u v yaw" waypoint, turn arrows, or STOP.
waypoint: u/v are coord-normalized to 0–1000 of the sensor (denormalize by sensor W/H); yaw is an integer heading delta in degrees, wrapped to [-180, 180]
↑ / ← / → (repeated)
discrete turn/step run (↑ forward, ← left, → right)
STOP
task complete
⚠️ Yaw sign. The prompt text says "positive = right", but the model
was trained on labels where positive = LEFT (the values came straight
from yaw_goal − yaw_cur, no negation). Execute yaw with positive = left.
Parse precedence (mirror the training/eval contract exactly): arrows first
(turn outputs never contain digits/STOP), then STOP anywhere in the
uppercased text, then the first three signed integers → (u, v, yaw).
Unparseable output was treated as STOP in the reference evaluator.
coord_norm is true in config.json for this checkpoint (u/v are 0–1000);
read it from the config rather than assuming.
Prompt format
The prompt is built through the checkpoint's own chat template (Qwen injects
the default You are a helpful assistant. system turn, which matches
training). The user turn is instruction first, then the ask, then the
history frames, then the current frame:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
You are an autonomous navigation assistant.
{instruction}
Where should you go next to stay on track? Output the next waypoint's pixel coordinates (u v) in the current image and the relative facing direction at that waypoint (yaw, integer degrees, positive = right). Output STOP when finished.
These are your historical observations: <image>
<image>
... you can see <image>.<|im_end|>
<|im_start|>assistant
{instruction} is a compact-JSON prompt_body (whitelist keys in natural
order, no spaces): {"goal":{"type":"<object|area>","value":"<caption>"},"route":[...],"constraints":[],"end_pose":[...],"quality":5}.
Note the goal dict key is value, and always request quality: 5 at
inference. route/end_pose may be empty (the gold-VLN eval drops them);
an end_pose anchor [{"target_type":"object","target":"<name>","relation":"front","distance_m":<m>}]
is understood for fine end-pose control (trained on objgoal data).
At step 0 the These are your historical observations: line is omitted —
just {conjunction}<image>. after the ask.
The connector before the current frame (you can see ) is one of several
training paraphrases; fix one canonical phrase for deterministic inference.
Observation preprocessing (evaluation parity)
setting
value
camera
egocentric RGB, 110° HFOV, single front view, camera height 0.88 m
sensor
720 (W) × 640 (H)
history
up to 8 frames, indices np.unique(np.linspace(0, step_id-1, 8)) (uniform stride over the episode so far, excluding the current frame)
resize
history frames → 256×256, current frame → 512×512 (bilinear)
generation
greedy (do_sample=False), max_new_tokens=32, bf16
control
project the picked (u, v) + depth → world goal, drive there with a discrete-action controller, then rotate to compass + yaw
Reproducibility notes (benchmark-exact details)
Two details of the reference evaluator matter if you aim to reproduce the
benchmark numbers exactly (rather than just deploy the model):
Connector phrase was randomized. The benchmark run drew the connector
before the current frame per query from several training paraphrases
("you can see ", "in front of you is ", "there is ", "you can spot ",
"you are toward the ", "ahead of you is "), not the fixed "you can see "
shown above. Fixing one phrase is the right choice for deployment but is
not bit-identical to the benchmark protocol.
Goal execution was capped. Each predicted waypoint was executed by
unprojecting (u, v) against depth, snapping the world goal to the
navmesh, then driving with a shortest-path follower capped at 10
discrete steps per goal, followed by a yaw-rotation phase capped at
12 turn steps; the model is then re-queried. Unbounded following
(or skipping the navmesh snap) changes SR slightly.
Usage
python
1import re
2import torch
3from PIL import Image
4from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
56REPO ="anchiehc/capra-v3-4b-n8"7model = Qwen3VLForConditionalGeneration.from_pretrained(8 REPO, torch_dtype=torch.bfloat16, attn_implementation="sdpa").cuda().eval()9processor = AutoProcessor.from_pretrained(REPO)10coord_norm =bool(getattr(model.config,"coord_norm",True))11SENSOR_W, SENSOR_H =720,6401213# --- observations: list of (frame_index, PIL.Image), oldest first14history =[(0, Image.open("frame000.jpg")),(4, Image.open("frame004.jpg"))]15current = Image.open("frame008.jpg")1617instruction =('{"goal":{"type":"object","value":"the grand piano in the middle '18'of the room"},"route":[],"constraints":[],"end_pose":[],"quality":5}')19ask =("Where should you go next to stay on track? Output the next waypoint's "20"pixel coordinates (u v) in the current image and the relative facing "21"direction at that waypoint (yaw, integer degrees, positive = right). "22"Output STOP when finished.")2324if history:25 hist_imgs ="".join("<image>\n"for _ in history)26 question =(f"You are an autonomous navigation assistant.\n{instruction}\n{ask}\n"27f"These are your historical observations: {hist_imgs}. you can see <image>.")28 images =[im.resize((256,256))for _, im in history]+[current.resize((512,512))]29else:30 question =(f"You are an autonomous navigation assistant.\n{instruction}\n{ask}\n"31f"you can see <image>.")32 images =[current.resize((512,512))]3334parts = re.split(r"(<image>)", question)35content, k =[],036for p in parts:37if p =="<image>":38 content.append({"type":"image","image": images[k]}); k +=139elif p:40 content.append({"type":"text","text": p})41text = processor.apply_chat_template([{"role":"user","content": content}],42 tokenize=False, add_generation_prompt=True)43inputs = processor(text=[text], images=images, return_tensors="pt").to("cuda")44with torch.inference_mode():45 out = model.generate(**inputs, max_new_tokens=32, do_sample=False, use_cache=True)46decoded = processor.tokenizer.decode(47 out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()4849# --- parse (arrows -> STOP -> first 3 ints); yaw positive = LEFT50defparse(t):51 arrows =[c for c in t if c in"↑←→"]52if arrows:53return("turn", arrows)54if"STOP"in t.upper():55return("stop",)56 nums =list(map(int, re.findall(r"-?\d+", t)))57iflen(nums)<3:58returnNone59 u, v, yaw = nums[:3]60if coord_norm:61 u =round(u /1000* SENSOR_W); v =round(v /1000* SENSOR_H)62 yaw =((yaw +180)%360)-180# positive = LEFT63return("waypoint", u, v, yaw)6465print(decoded,"->", parse(decoded))
Notes:
The predicted pixel is in the native sensor resolution (720×640), not
the resized image handed to the model — resize is only to control the visual
token count. Unproject (u, v) against depth at the native resolution.
In closed-loop use, re-query after executing (part of) the action, appending
the new frame to the history.
Data: rendered VLN-CE-style corpora (R2R, RxR, ScaleVLN, VLNVerse) plus
object-goal navigation, with the v3 pixel-goal text target
("u v yaw" / arrows / STOP); anchor-drop augmentation on the VLN rows
(empty route/end_pose), force_quality=5, single 88 cm / 0° front rig.
Objective: next-token cross-entropy on the assistant text (teacher
forcing).