#!/usr/bin/env python
"""Build the openjev v2 training mixture: hard NLI + long documents + image premises + agentic traces.
Every adapter emits rows in ONE schema, with labels already in our space (0=contradiction, 1=entailment, 2=neutral):
{"premise": str, "hypothesis": str, "label": int, "source": str, "image": str}
`image` is "" for text rows, else a path relative to the mixture dir. When an image is present the premise carries the
literal marker `<
>` at the position where the `<|vision_start|><|image_pad|>*n<|vision_end|>` block must go
(train.py substitutes it, because only the tokenizer knows the real token count).
Subcommands (each writes parts/.jsonl incrementally, so a crash never loses finished work):
python data_mix.py images --out /mnt/nli_mix --n-rows 120000 # VQAv2 stream -> jpegs + claims
python data_mix.py text --out /mnt/nli_mix # SNLI/MNLI/ANLI/WANLI/FEVER/... + haystack
python data_mix.py agentic --out /mnt/nli_mix # xlam / AgentTraj-L / AgentInstruct / When2Call / synth
python data_mix.py build --out /mnt/nli_mix # merge, balance, leakage-check, save_to_disk
"""
import argparse
import json
import os
import random
import re
import sys
from collections import Counter, defaultdict
OURS = {"contradiction": 0, "entailment": 1, "neutral": 2}
SYN = {
"entailment": "entailment", "entails": "entailment", "entailed": "entailment", "supports": "entailment",
"contradiction": "contradiction", "contradicts": "contradiction", "refutes": "contradiction",
"neutral": "neutral", "not_entailment": "neutral", "not-entailed": "neutral", "not entailment": "neutral",
"not enough info": "neutral", "not_enough_info": "neutral", "nei": "neutral",
}
IMG = "<
>"
# Datasets/splits that eval.py reports on. Never train on them, in any split (see plan: strict zero-shot).
BANNED = {
"nyu-mll/multi_nli": {"validation_matched", "validation_mismatched"},
"cais/mmlu": "*", "allenai/ai2_arc": "*", "allenai/winogrande": "*", "Rowan/hellaswag": "*",
"openai/gsm8k": "*", "Idavidrein/gpqa": "*",
}
def norm_label(value, feature=None):
"""Resolve any source's label to our id, by NAME (never by position)."""
name = value
if isinstance(value, int) and feature is not None and hasattr(feature, "names"):
name = feature.names[value]
if not isinstance(name, str):
raise ValueError(f"cannot resolve label {value!r} (feature={feature})")
key = SYN.get(name.strip().lower())
if key is None:
raise ValueError(f"unknown label name {name!r}")
return OURS[key]
def check_not_banned(repo, split):
banned = BANNED.get(repo)
if banned == "*" or (banned and split in banned):
raise RuntimeError(f"LEAKAGE: {repo}:{split} is an eval set and must never be used for training")
class Part:
"""Incremental jsonl writer; a teardown crash keeps everything already written."""
def __init__(self, out, name):
os.makedirs(os.path.join(out, "parts"), exist_ok=True)
self.path = os.path.join(out, "parts", f"{name}.jsonl")
self.f = open(self.path, "w")
self.n = 0
self.by_label = Counter()
def add(self, premise, hypothesis, label, source, image=""):
premise, hypothesis = premise.strip(), hypothesis.strip()
if not premise or not hypothesis:
return
self.f.write(json.dumps({"premise": premise, "hypothesis": hypothesis, "label": int(label),
"source": source, "image": image}, ensure_ascii=False) + "\n")
self.n += 1
self.by_label[int(label)] += 1
if self.n % 20000 == 0:
self.f.flush()
print(f" {os.path.basename(self.path)}: {self.n} rows", flush=True)
def close(self):
self.f.close()
print(f"[part] {os.path.basename(self.path)}: {self.n} rows, labels {dict(self.by_label)}", flush=True)
# --------------------------------------------------------------------------------------- text NLI
def take(ds, n, seed):
if n and len(ds) > n:
ds = ds.shuffle(seed=seed).select(range(n))
return ds
def build_text(args):
from datasets import concatenate_datasets, get_dataset_config_names, load_dataset
seed = args.seed
p = Part(args.out, "text")
pool = [] # filler sentences for the haystack block
pairs_for_haystack = []
def emit(ds, src, prem_col="premise", hyp_col="hypothesis", lab_col="label", keep_for_haystack=False):
feat = ds.features.get(lab_col)
kept = 0
for ex in ds:
v = ex[lab_col]
if isinstance(v, int) and v < 0:
continue
try:
y = norm_label(v, feat)
except ValueError:
continue
pr, hy = ex[prem_col], ex[hyp_col]
if not pr or not hy:
continue
p.add(pr, hy, y, src)
kept += 1
if len(pool) < 200_000:
pool.append(pr.strip())
if keep_for_haystack and len(pairs_for_haystack) < 250_000:
pairs_for_haystack.append((pr.strip(), hy.strip(), y))
print(f"[text] {src}: {kept}", flush=True)
# core NLI
for repo, n in [("stanfordnlp/snli", args.n_snli), ("nyu-mll/multi_nli", args.n_mnli)]:
check_not_banned(repo, "train")
emit(take(load_dataset(repo, split="train"), n, seed), repo.split("/")[-1], keep_for_haystack=True)
# adversarial
anli = load_dataset("facebook/anli")
emit(concatenate_datasets([anli[f"train_r{i}"] for i in (1, 2, 3)]), "anli", keep_for_haystack=True)
wanli = load_dataset("alisawuffles/WANLI", split="train")
emit(wanli, "wanli", lab_col="gold")
# evidence-grounded
fever = take(load_dataset("pietrolesci/nli_fever", split="train"), args.n_fever, seed)
emit(fever, "nli_fever", keep_for_haystack=True)
emit(load_dataset("tasksource/lingnli", split="train"), "lingnli")
emit(load_dataset("tasksource/ConTRoL-nli", split="train"), "control")
sci = load_dataset("allenai/scitail", "snli_format", split="train")
emit(sci, "scitail", prem_col="sentence1", hyp_col="sentence2", lab_col="gold_label")
qnli = take(load_dataset("nyu-mll/glue", "qnli", split="train"), args.n_qnli, seed)
emit(qnli, "qnli", prem_col="sentence", hyp_col="question")
for cfg in get_dataset_config_names("tasksource/babi_nli"):
try:
emit(load_dataset("tasksource/babi_nli", cfg, split="train"), f"babi:{cfg}")
except Exception as e: # noqa: BLE001
print(f"[text] babi:{cfg} skipped: {type(e).__name__}", flush=True)
p.close()
# ------------------------------------------------------------------ haystack (long-document entailment)
rng = random.Random(seed)
h = Part(args.out, "haystack")
rng.shuffle(pairs_for_haystack)
for prem, hyp, y in pairs_for_haystack[: args.n_haystack]:
filler = rng.sample(pool, rng.randint(15, 40))
drop = rng.random() < 0.30 # 30%: the evidence is NOT in the document -> "not stated" == neutral
if drop:
doc, label = filler, OURS["neutral"]
else:
pos = rng.randrange(len(filler) + 1)
doc, label = filler[:pos] + [prem] + filler[pos:], y
h.add("\n\n".join(doc), hyp, label, "haystack_drop" if drop else "haystack")
h.close()
# --------------------------------------------------------------------------------------- images (VQAv2)
NUM_WORDS = {"0": "no", "1": "one", "2": "two", "3": "three", "4": "four", "5": "five"}
LEAD_INS = ["A photograph:", "A photo:", "A picture:", "An image:", "A photograph of a scene:"]
LEAD_PIX = "A photograph, 320 pixels wide, the centre at x = 160:"
def q_to_statement(q, a, atype):
"""VQA question + answer -> a declarative claim. Returns (statement, flip_ok) where flip_ok means
a yes/no question whose 'no' answer turns the same statement into a contradiction."""
q = q.strip().rstrip("?").strip()
a = a.strip().lower()
ql = q.lower()
m = re.match(r"^what colou?r (?:is|are) (?:the )?(.+)$", ql)
if m:
return f"The {m.group(1)} is {a}.", False
m = re.match(r"^how many (.+)$", ql)
if m:
noun = m.group(1)
noun = re.sub(r"\b(are|is|can|do|does|there|in|on|the|this|that|photo|picture|image)\b.*$", "", noun).strip()
if noun:
return f"There are {NUM_WORDS.get(a, a)} {noun} in the image.", False
if atype == "yes/no":
m = re.match(r"^(is|are|was|were|does|do|did|can|has|have|will)\s+(.+)$", ql)
if m:
aux, rest = m.group(1), m.group(2)
if aux in ("is", "are", "was", "were"):
parts = rest.split(" ", 1)
if len(parts) == 2:
subj, tail = parts
return f"{subj.capitalize()} {aux} {tail}.", True
else:
return f"{rest.capitalize()} ({aux}).".replace(" ().", "."), True
if atype == "yes/no":
return f'The answer to "{q}?" is {a}.', False
return f'In this image, the answer to "{q}?" is {a}.', False
def spatial_claims(dets, W, H, rng):
"""Templated claims from DETA detections, in the coordinates of the RESIZED 320x240 frame.
Returns a list of (hypothesis, label)."""
out = []
if not dets:
return out
sx, sy = 320.0 / max(W, 1), 240.0 / max(H, 1)
boxes = []
for d in dets:
b = d.get("box")
lab = (d.get("label") or "").strip().lower()
if not b or len(b) != 4 or not lab:
continue
boxes.append((lab, [b[0] * sx, b[1] * sy, b[2] * sx, b[3] * sy]))
if not boxes:
return out
present = Counter(l for l, _ in boxes)
labels = sorted(present)
def third(cx):
return "left" if cx < 320 / 3 else ("right" if cx > 2 * 320 / 3 else "middle")
lab, box = rng.choice(boxes)
cx = (box[0] + box[2]) / 2
t = third(cx)
out.append((f"There is a {lab} in the {t} third of the image.", OURS["entailment"]))
wrong = rng.choice([x for x in ("left", "middle", "right") if x != t])
out.append((f"There is a {lab} in the {wrong} third of the image.", OURS["contradiction"]))
# pixel-coordinate form: mirrors the `pixels` variant of doom_vision.py, the best one on Doom
if cx < 110:
out.append((f"The {lab} is at x < 110, on the left.", OURS["entailment"]))
out.append((f"The {lab} is at x > 210, on the right.", OURS["contradiction"]))
elif cx > 210:
out.append((f"The {lab} is at x > 210, on the right.", OURS["entailment"]))
out.append((f"The {lab} is at x < 110, on the left.", OURS["contradiction"]))
else:
out.append((f"The {lab} is near the centre, around x = 160.", OURS["entailment"]))
out.append((f"The {lab} is at x < 110, on the left.", OURS["contradiction"]))
# counting
n = present[lab]
out.append((f"There are {NUM_WORDS.get(str(n), n)} {lab}s in the image.", OURS["entailment"]))
out.append((f"There are {NUM_WORDS.get(str(n + 2), n + 2)} {lab}s in the image.", OURS["contradiction"]))
# relative position
if len(labels) >= 2:
a, b = rng.sample(labels, 2)
ca = sum((bx[0] + bx[2]) / 2 for l, bx in boxes if l == a) / present[a]
cb = sum((bx[0] + bx[2]) / 2 for l, bx in boxes if l == b) / present[b]
rel, anti = ("left", "right") if ca < cb else ("right", "left")
out.append((f"The {a} is to the {rel} of the {b}.", OURS["entailment"]))
out.append((f"The {a} is to the {anti} of the {b}.", OURS["contradiction"]))
# absent object -> contradiction; unverifiable attribute -> neutral
for cand in ("giraffe", "helicopter", "piano", "traffic light", "zebra"):
if cand not in present:
out.append((f"There is a {cand} in the image.", OURS["contradiction"]))
break
out.append((f"The {lab} was bought last week.", OURS["neutral"]))
return out
def build_images(args):
from datasets import load_dataset
from PIL import Image
rng = random.Random(args.seed)
img_dir = os.path.join(args.out, "images")
os.makedirs(img_dir, exist_ok=True)
p = Part(args.out, args.part_name)
ds = load_dataset(args.vqa_repo, split=args.vqa_split, streaming=True)
ds = ds.shuffle(seed=args.seed, buffer_size=10_000)
seen_images, seen_answers, n_rows = {}, defaultdict(list), 0
for ex in ds:
if n_rows >= args.n_rows or p.n >= args.n_claims:
break
n_rows += 1
try:
iid = str(ex["id_image"])
rel = f"images/{iid}.jpg"
if iid not in seen_images:
if len(seen_images) >= args.max_images:
continue
im = ex["image"].convert("RGB")
seen_images[iid] = im.size
im.resize((320, 240)).save(os.path.join(img_dir, f"{iid}.jpg"), quality=88)
W, H = seen_images[iid]
q, a = ex["question"], (ex["multiple_choice_answer"] or "").strip()
atype = ex.get("answer_type") or ""
if not a:
continue
answers = [x["answer"] if isinstance(x, dict) else x for x in (ex.get("answers") or [])]
agree = sum(1 for x in answers if str(x).strip().lower() == a.lower())
stmt, flip_ok = q_to_statement(q, a, atype)
lead = rng.choice(LEAD_INS)
if agree and agree < 6: # annotators disagree -> not determinable from the image
p.add(f"{lead} {IMG}", stmt, OURS["neutral"], "vqa_disagree", rel)
elif atype == "yes/no" and flip_ok:
p.add(f"{lead} {IMG}", stmt, OURS["entailment"] if a == "yes" else OURS["contradiction"], "vqa_yesno", rel)
else:
p.add(f"{lead} {IMG}", stmt, OURS["entailment"], "vqa_answer", rel)
pool = seen_answers[atype]
if pool and rng.random() < args.answer_neg_prob:
wrong = rng.choice(pool)
if wrong.lower() != a.lower():
bad, _ = q_to_statement(q, wrong, atype)
p.add(f"{lead} {IMG}", bad, OURS["contradiction"], "vqa_answer_neg", rel)
if len(pool) < 5000:
pool.append(a)
dets = ex.get("DETA_detections_deta_swin_large_o365_coco_classes") or []
claims = spatial_claims(dets, W, H, rng)
rng.shuffle(claims)
for hyp, y in claims[: args.spatial_per_image]:
p.add(f"{LEAD_PIX} {IMG}", hyp, y, "vqa_spatial", rel)
except Exception as e: # noqa: BLE001
print(f"[vqa] row skipped: {type(e).__name__}: {str(e)[:80]}", flush=True)
p.close()
print(f"[vqa] read {n_rows} rows, {len(seen_images)} images saved", flush=True)
sys.stdout.flush()
os._exit(0) # datasets 5.0 segfaults tearing down the parquet stream generator
# --------------------------------------------------------------------------------------- agentic
def _tools_str(tools, limit=1200):
s = tools if isinstance(tools, str) else json.dumps(tools, ensure_ascii=False)
return s[:limit]
def build_agentic(args):
from datasets import load_dataset
rng = random.Random(args.seed)
p = Part(args.out, "agentic")
# ---- xlam function calling (gated upstream; download it yourself to data/xlam.jsonl)
xlam_path = os.path.join(args.data_dir, "xlam.jsonl")
if os.path.exists(xlam_path):
rows = [json.loads(l) for l in open(xlam_path)]
alt_names = []
for r in rows:
try:
calls = json.loads(r["answers"]) if isinstance(r["answers"], str) else r["answers"]
except Exception: # noqa: BLE001
continue
if not calls:
continue
for c in calls[:2]:
alt_names.append(c.get("name", ""))
for r in rows:
try:
calls = json.loads(r["answers"]) if isinstance(r["answers"], str) else r["answers"]
except Exception: # noqa: BLE001
continue
if not calls:
continue
call = calls[0]
name, argd = call.get("name", ""), call.get("arguments", {}) or {}
if not name:
continue
prem = f"User request: {r['query'].strip()}\nAvailable tools: {_tools_str(r.get('tools'))}"
fmt = lambda n, d: f"The correct call is {n}(" + ", ".join(f"{k}={json.dumps(v, ensure_ascii=False)}" for k, v in d.items()) + ")."
p.add(prem, fmt(name, argd), OURS["entailment"], "xlam", "")
bad = rng.choice(alt_names)
if bad and bad != name:
p.add(prem, fmt(bad, argd), OURS["contradiction"], "xlam_neg_name", "")
if argd:
k = rng.choice(list(argd))
mut = dict(argd)
v = mut[k]
mut[k] = (v + 7) if isinstance(v, (int, float)) and not isinstance(v, bool) else (str(v) + "_x")
p.add(prem, fmt(name, mut), OURS["contradiction"], "xlam_neg_arg", "")
drop = {kk: vv for kk, vv in argd.items() if kk != k}
p.add(prem, f"The value of `{k}` cannot be determined from the user request.",
OURS["neutral"] if len(argd) > 1 else OURS["contradiction"], "xlam_underspec", "")
if drop:
p.add(prem, fmt(name, drop), OURS["contradiction"], "xlam_neg_missing", "")
else:
print(f"[agentic] {xlam_path} missing -> xlam skipped", flush=True)
# ---- trajectory datasets: premise = task + last observation, hypothesis = next action
def from_conversations(repo, cfgs, src, cap):
n = 0
for cfg in cfgs:
try:
ds = load_dataset(repo, split=cfg) if cfg else load_dataset(repo, split="train")
except Exception as e: # noqa: BLE001
print(f"[agentic] {repo}:{cfg} skipped: {type(e).__name__}", flush=True)
continue
all_actions = []
convs = []
for ex in ds:
turns = ex.get("conversations") or []
msgs = [(t.get("from") or t.get("role") or "", (t.get("value") or t.get("content") or "").strip()) for t in turns]
acts = [m for who, m in msgs if who in ("gpt", "assistant") and m]
if len(msgs) >= 3 and acts:
convs.append(msgs)
all_actions.extend(acts)
for msgs in convs:
if n >= cap:
break
idxs = [i for i, (who, _) in enumerate(msgs) if who in ("gpt", "assistant") and i > 0]
for i in rng.sample(idxs, min(2, len(idxs))):
if n >= cap:
break
ctx = "\n".join(m for _, m in msgs[max(0, i - 2):i])[-1500:]
act = msgs[i][1][:400]
p.add(f"Agent task and last observations:\n{ctx}", f"The next action is: {act}", OURS["entailment"], src, "")
other = rng.choice(all_actions)[:400]
if other != act:
p.add(f"Agent task and last observations:\n{ctx}", f"The next action is: {other}", OURS["contradiction"], f"{src}_neg", "")
n += 2
print(f"[agentic] {src}: {n}", flush=True)
from_conversations("AgentGym/AgentTraj-L", [None], "agenttraj", args.n_traj)
from_conversations("THUDM/AgentInstruct", ["os", "db", "alfworld", "webshop", "kg", "mind2web"], "agentinstruct", 8000)
# ---- When2Call: call a tool / ask a clarifying question / answer directly
try:
w2c = load_dataset("nvidia/When2Call", split="mcq")
for ex in w2c:
q = (ex.get("question") or "").strip()
if not q:
continue
prem = f"{q[:2500]}\nAvailable tools: {_tools_str(ex.get('tools'))}"
correct = str(ex.get("correct_answer", "")).strip()
answers = ex.get("answers")
opts = answers if isinstance(answers, list) else [answers]
for o in opts:
o = str(o).strip()
if not o:
continue
y = OURS["entailment"] if o == correct else OURS["contradiction"]
p.add(prem, f"The assistant should: {o[:300]}", y, "when2call", "")
if ex.get("held_out_param"):
p.add(prem, f"The value of `{ex['held_out_param']}` is stated in the user request.",
OURS["contradiction"], "when2call_param", "")
except Exception as e: # noqa: BLE001
print(f"[agentic] When2Call skipped: {type(e).__name__}: {str(e)[:80]}", flush=True)
# ---- synthetic state predicates (the Minecraft-scaffold format), fully generated here
if not args.no_synth_state:
items = ["oak log", "oak planks", "stick", "crafting table", "cobblestone", "raw iron", "iron ingot",
"furnace", "coal", "wooden pickaxe", "stone pickaxe", "iron pickaxe", "bucket", "torch", "apple"]
places = ["a crafting table", "a furnace", "a chest", "water", "lava"]
for _ in range(args.n_synth):
inv = {it: rng.randint(1, 40) for it in rng.sample(items, rng.randint(2, 7))}
near = rng.sample(places, rng.randint(0, 2))
health = rng.randint(1, 20)
lines = [f"{k}: {v}" for k, v in inv.items()] or ["(empty)"]
prem = ("Agent state.\nInventory:\n" + "\n".join(lines)
+ f"\nNearby: {', '.join(near) if near else 'nothing'}\nHealth: {health}/20")
kind = rng.randrange(4)
if kind == 0:
it = rng.choice(items)
have, thr = inv.get(it, 0), rng.randint(1, 20)
p.add(prem, f"The number of {it} in the inventory is {thr} or more.",
OURS["entailment"] if have >= thr else OURS["contradiction"], "synth_count", "")
elif kind == 1:
it = rng.choice(items)
p.add(prem, f"There are no {it} in the inventory.",
OURS["entailment"] if inv.get(it, 0) == 0 else OURS["contradiction"], "synth_absent", "")
elif kind == 2:
pl = rng.choice(places)
p.add(prem, f"The agent is near {pl}.",
OURS["entailment"] if pl in near else OURS["contradiction"], "synth_near", "")
else:
p.add(prem, rng.choice([f"The agent has been playing for {rng.randint(2, 90)} minutes.",
"The agent intends to build a shelter next.",
f"The {rng.choice(items)} was crafted by another player."]),
OURS["neutral"], "synth_unknown", "")
p.close()
# --------------------------------------------------------------------------------------- merge
def build_final(args):
from datasets import Dataset
rng = random.Random(args.seed)
rows, by_source, by_label = [], Counter(), Counter()
drop = set(x for x in args.drop.split(",") if x)
cap_prefixes = tuple(x for x in args.cap_prefixes.split(",") if x) or ("\0",)
part_dir = os.path.join(args.out, "parts")
for fn in sorted(os.listdir(part_dir)):
if not fn.endswith(".jsonl"):
continue
with open(os.path.join(part_dir, fn)) as f:
for line in f:
r = json.loads(line)
if r["source"] in drop:
continue
if (args.cap_per_source and r["source"].startswith(cap_prefixes)
and by_source[r["source"]] >= args.cap_per_source):
continue
rows.append(r)
by_source[r["source"]] += 1
by_label[r["label"]] += 1
print(f"[build] {len(rows)} rows from {len(by_source)} sources; labels {dict(by_label)}", flush=True)
# class balance: downsample the dominant class to at most 1.15x the smallest
target = int(min(by_label.values()) * 1.15)
keep, per = [], Counter()
rng.shuffle(rows)
for r in rows:
if per[r["label"]] < target:
keep.append(r)
per[r["label"]] += 1
rows = keep
print(f"[build] after balancing: {len(rows)}; labels {dict(per)}", flush=True)
# leakage check against the MNLI validation splits that eval.py reports
from datasets import load_dataset
banned_pairs = set()
for split in ("validation_matched", "validation_mismatched"):
for ex in load_dataset("nyu-mll/multi_nli", split=split):
banned_pairs.add((ex["premise"].strip().lower()[:200], ex["hypothesis"].strip().lower()[:200]))
before = len(rows)
rows = [r for r in rows if (r["premise"].strip().lower()[:200], r["hypothesis"].strip().lower()[:200]) not in banned_pairs]
print(f"[build] leakage filter dropped {before - len(rows)} rows overlapping MNLI val", flush=True)
rng.shuffle(rows)
n_val = args.n_val
val, train = rows[:n_val], rows[n_val:]
ds = {"train": Dataset.from_list(train), "val": Dataset.from_list(val)}
from datasets import DatasetDict
DatasetDict(ds).save_to_disk(os.path.join(args.out, "mix"))
comp = {"n_train": len(train), "n_val": len(val), "by_source": dict(by_source),
"by_label_final": dict(per), "images": sum(1 for r in rows if r["image"])}
json.dump(comp, open(os.path.join(args.out, "composition.json"), "w"), indent=2)
print(json.dumps(comp, indent=2)[:2000])
print("\n| source | rows |\n|---|---|")
for s, n in by_source.most_common():
print(f"| {s} | {n} |")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["text", "images", "agentic", "build"])
ap.add_argument("--out", default="/mnt/nli_mix")
ap.add_argument("--data-dir", default="data")
ap.add_argument("--seed", type=int, default=0)
# text
ap.add_argument("--n-snli", type=int, default=120_000)
ap.add_argument("--n-mnli", type=int, default=120_000)
ap.add_argument("--n-fever", type=int, default=150_000)
ap.add_argument("--n-qnli", type=int, default=60_000)
ap.add_argument("--n-haystack", type=int, default=80_000)
# images
ap.add_argument("--n-rows", type=int, default=200_000)
ap.add_argument("--n-claims", type=int, default=220_000)
ap.add_argument("--max-images", type=int, default=60_000)
ap.add_argument("--spatial-per-image", type=int, default=1)
ap.add_argument("--answer-neg-prob", type=float, default=0.5)
ap.add_argument("--vqa-repo", default="Multimodal-Fatima/VQAv2_train")
ap.add_argument("--vqa-split", default="train")
ap.add_argument("--part-name", default="vqa")
# agentic
ap.add_argument("--n-traj", type=int, default=40_000)
ap.add_argument("--n-synth", type=int, default=50_000)
ap.add_argument("--no-synth-state", action="store_true")
# build
ap.add_argument("--n-val", type=int, default=4000)
ap.add_argument("--drop", default="xlam_underspec", help="comma-separated sources to exclude (xlam_underspec is mislabeled)")
ap.add_argument("--cap-per-source", type=int, default=40000)
ap.add_argument("--cap-prefixes", default="xlam", help="the per-source cap applies only to sources with these prefixes")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
{"text": build_text, "images": build_images, "agentic": build_agentic, "build": build_final}[args.cmd](args)
if __name__ == "__main__":
main()