Downloads · 30 days
8
29% of all-time downloads
anchiehc/capra-v3-4b-n8
capra-v3-4b-n8 is a image-text-to-text model from anchiehc. Use it for the image-text-to-text task on the model card, and read the license before you ship it in a product. It is set up for transformers.
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-spa…
Downloads · 30 days
8
29% of all-time downloads
All-time downloads
28
Public
Parameters
4.4B
8.9 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors8.9 GB · 100%
From the Hugging Face model README
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.
R2R val_unseen (VLN-CE, 1,839 episodes, discrete Habitat action space, 110° HFOV, greedy decoding):
| Metric | Value |
|---|---|
| SR ↑ | 0.581 |
| SPL ↑ | 0.535 |
| OS ↑ | 0.695 |
| NE ↓ | 4.25 |
Per query the model outputs one of:
| output | meaning |
|---|---|
"u v yaw" | 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.
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).These are your historical observations: line is omitted —
just {conjunction}<image>. after the ask.you can see ) is one of several
training paraphrases; fix one canonical phrase for deterministic inference.| 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 |
Two details of the reference evaluator matter if you aim to reproduce the benchmark numbers exactly (rather than just deploy the model):
(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.import re
import torch
from PIL import Image
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
REPO = "anchiehc/capra-v3-4b-n8"
model = Qwen3VLForConditionalGeneration.from_pretrained(
REPO, torch_dtype=torch.bfloat16, attn_implementation="sdpa").cuda().eval()
processor = AutoProcessor.from_pretrained(REPO)
coord_norm = bool(getattr(model.config, "coord_norm", True))
SENSOR_W, SENSOR_H = 720, 640
# --- observations: list of (frame_index, PIL.Image), oldest first
history = [(0, Image.open("frame000.jpg")), (4, Image.open("frame004.jpg"))]
current = Image.open("frame008.jpg")
instruction = ('{"goal":{"type":"object","value":"the grand piano in the middle '
'of the room"},"route":[],"constraints":[],"end_pose":[],"quality":5}')
ask = ("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.")
if history:
hist_imgs = "".join("<image>\n" for _ in history)
question = (f"You are an autonomous navigation assistant.\n{instruction}\n{ask}\n"
f"These are your historical observations: {hist_imgs}. you can see <image>.")
images = [im.resize((256, 256)) for _, im in history] + [current.resize((512, 512))]
else:
question = (f"You are an autonomous navigation assistant.\n{instruction}\n{ask}\n"
f"you can see <image>.")
images = [current.resize((512, 512))]
parts = re.split(r"(<image>)", question)
content, k = [], 0
for p in parts:
if p == "<image>":
content.append({"type": "image", "image": images[k]}); k += 1
elif p:
content.append({"type": "text", "text": p})
text = processor.apply_chat_template([{"role": "user", "content": content}],
tokenize=False, add_generation_prompt=True)
inputs = processor(text=[text], images=images, return_tensors="pt").to("cuda")
with torch.inference_mode():
out = model.generate(**inputs, max_new_tokens=32, do_sample=False, use_cache=True)
decoded = processor.tokenizer.decode(
out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
# --- parse (arrows -> STOP -> first 3 ints); yaw positive = LEFT
def parse(t):
arrows = [c for c in t if c in "↑←→"]
if arrows:
return ("turn", arrows)
if "STOP" in t.upper():
return ("stop",)
nums = list(map(int, re.findall(r"-?\d+", t)))
if len(nums) < 3:
return None
u, v, yaw = nums[:3]
if coord_norm:
u = round(u / 1000 * SENSOR_W); v = round(v / 1000 * SENSOR_H)
yaw = ((yaw + 180) % 360) - 180 # positive = LEFT
return ("waypoint", u, v, yaw)
print(decoded, "->", parse(decoded))
Notes:
(u, v) against depth at the native resolution.Qwen/Qwen3-VL-4B-Instruct, finetuned (stock architecture, no
vocab extension), bf16, 1 epoch."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.| file | purpose |
|---|---|
model-*.safetensors, model.safetensors.index.json | weights (bf16) |
config.json | includes coord_norm: true |
tokenizer*, vocab.json, merges.txt, added_tokens.json, special_tokens_map.json, chat_template.jinja | text tokenizer |
preprocessor_config.json | image processor |
generation_config.json | generation defaults (overridden to greedy at inference) |