winbeau's picture
code: include exact task interfaces and inference source snapshot
fa82a9d verified
Raw History Blame Contribute Delete
9.78 kB
#!/usr/bin/env python3
"""Annotate prompts with DeepSeek (two passes + arbitration) into LLM-labelled gold sets.
Scope of this entry point: text-only tasks (spatial, action, objects). Scene items
need frames and real captions and are produced by the caption/vision pipeline.
Every provider call is charged to ``output/annotation/BUDGET-annotate-<name>.json``
before it is sent; ``--budget`` is a hard cap for the run.
Usage::
# MovieGen long-prompt pilot (decision B1: existing long prompts first)
uv run --no-sync python scripts/annotate_gold.py --name longprompt-pilot \\
--source moviegen --buckets 50-99,100-200 --budget 300
# full run over a bucket mix
uv run --no-sync python scripts/annotate_gold.py --name gold-v1 \\
--source moviegen+seed --buckets 1-14,15-49,50-99,100-200 --limit-per-task 120 --budget 4000
"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
import json
from pathlib import Path
import sys
from typing import Any, Iterable, Sequence
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from vbench_prompts_compile import annotate as A # noqa: E402
from vbench_prompts_compile import records as R # noqa: E402
from vbench_prompts_compile import sources as S # noqa: E402
from vbench_prompts_compile.llm import ChatClient, RequestBudget, load_tokens # noqa: E402
def build_moviegen_items(tasks: Sequence[str], buckets: Sequence[str], limit_per_task: int | None, seed: int) -> list[A.Item]:
import random
refs = [ref for ref in S.load_moviegen_prompts() if R.length_bucket(ref.word_count) in set(buckets)]
rng = random.Random(seed)
rng.shuffle(refs)
items: list[A.Item] = []
for task in tasks:
for index, ref in enumerate(refs):
if limit_per_task is not None and index >= limit_per_task:
break
items.append(
A.Item(
item_id=f"{task}-moviegen-{ref.sha256[:12]}",
task=task,
prompt=ref.text,
group_id=f"moviegen:{ref.sha256[:12]}",
bucket=R.length_bucket(ref.word_count),
source="moviegen",
source_id=f"{ref.source_file}:{ref.line}",
meta={"word_count": ref.word_count, "licence": "CC-BY-NC-4.0"},
)
)
return items
def build_seed_items(tasks: Sequence[str], buckets: Sequence[str], limit_per_task: int | None, seed: int) -> list[A.Item]:
"""Seeds from the existing weak corpora (VG templates, K400 templates, fixtures)."""
import random
rng = random.Random(seed + 7)
pools: dict[str, list[A.Item]] = defaultdict(list)
vg = [row for row in R.read_jsonl(ROOT / "data" / "processed" / "formal-0001" / "candidates" / "spatial.jsonl")]
rng.shuffle(vg)
for row in vg[: (limit_per_task or 200)]:
for task in ("spatial", "objects"):
if task not in tasks or task not in ("spatial", "objects"):
continue
prompt = row["input"]["prompt"]
if R.length_bucket(R.word_count(prompt)) not in set(buckets):
continue
pools[task].append(
A.Item(
item_id=f"{task}-vg-{row['source_id']}",
task=task,
prompt=prompt,
group_id=row["group_id"],
bucket=R.length_bucket(R.word_count(prompt)),
source="visual_genome_prompt",
source_id=row["source_id"],
meta={"licence": "CC-BY-4.0"},
)
)
k400 = R.read_jsonl(ROOT / "data" / "processed" / "formal-0001" / "candidates" / "action.jsonl")
rng.shuffle(k400)
for row in k400[: (limit_per_task or 200)]:
if "action" not in tasks:
continue
prompt = row["input"]["prompt"]
if R.length_bucket(R.word_count(prompt)) not in set(buckets):
continue
pools["action"].append(
A.Item(
item_id=f"action-k400-{row['source_id']}",
task="action",
prompt=prompt,
group_id=row["group_id"],
bucket=R.length_bucket(R.word_count(prompt)),
source="k400_prompt",
source_id=row["source_id"],
)
)
items: list[A.Item] = []
for task in tasks:
items.extend(pools.get(task, [])[: (limit_per_task or len(pools.get(task, [])))])
return items
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--name", required=True)
parser.add_argument("--tasks", default="spatial,action,objects")
parser.add_argument("--source", default="moviegen", help="moviegen | seed | moviegen+seed")
parser.add_argument("--buckets", default="1-14,15-49,50-99,100-200")
parser.add_argument("--limit-per-task", type=int, default=None)
parser.add_argument("--budget", type=int, default=200, help="hard cap of provider requests for this run")
parser.add_argument("--provider", default="deepseek")
parser.add_argument("--model", default=None)
parser.add_argument("--token-file", default=str(ROOT / "token.txt"))
parser.add_argument("--seed", type=int, default=20260919)
parser.add_argument("--max-tokens", type=int, default=384)
parser.add_argument("--dry-run", action="store_true")
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
tasks = [t.strip() for t in args.tasks.split(",") if t.strip()]
buckets = [b.strip() for b in args.buckets.split(",") if b.strip()]
items: list[A.Item] = []
if "moviegen" in args.source:
items += build_moviegen_items(tasks, buckets, args.limit_per_task, args.seed)
if "seed" in args.source:
items += build_seed_items(tasks, buckets, args.limit_per_task, args.seed)
if not items:
print("no items selected", file=sys.stderr)
return 2
out_dir = ROOT / "data" / "gold" / args.name
out_dir.mkdir(parents=True, exist_ok=True)
R.write_jsonl(out_dir / "items.jsonl", [item.as_dict() for item in items])
print(json.dumps({"items": len(items), "by_task": dict(Counter(i.task for i in items)), "by_bucket": dict(Counter(i.bucket for i in items))}, ensure_ascii=False))
if args.dry_run:
return 0
tokens = load_tokens(args.token_file)
client = ChatClient(provider=args.provider, api_key=tokens.get(args.provider), model=args.model)
budget = RequestBudget(ROOT / "output" / "annotation", f"annotate-{args.name}", authorized_total=args.budget)
vocab = S.load_k400()
resolver = vocab.resolve
gold: list[dict[str, Any]] = []
quarantine: list[dict[str, Any]] = []
reports: list[dict[str, Any]] = []
for item in items:
if budget.remaining() <= 0:
print("budget exhausted; stopping", file=sys.stderr)
break
result = A.annotate_item(
client,
item,
action_resolver=resolver,
max_tokens=args.max_tokens,
charge=lambda entry: budget.charge(entry, provider=args.provider),
)
entry = {
"item_id": item.item_id,
"task": item.task,
"bucket": item.bucket,
"status": result.status,
"agreement": result.agreement,
"errors": result.errors,
"target": result.target,
"usage": result.usage,
}
reports.append(entry)
if result.target is not None:
gold.append(R.add_length_meta(A.result_to_record(result, item, action_resolver=resolver)))
else:
quarantine.append({**entry, "prompt": item.prompt, "passes": result.passes})
budget.append({"kind": "result", "item_id": item.item_id, "status": result.status, "agreement": result.agreement, "errors": result.errors})
R.write_jsonl(out_dir / "gold.jsonl", gold)
R.write_jsonl(out_dir / "quarantine.jsonl", quarantine)
status_counts = Counter(entry["status"] for entry in reports)
report = {
"name": args.name,
"source": args.source,
"buckets": buckets,
"tasks": tasks,
"items": len(items),
"processed": len(reports),
"status_counts": dict(status_counts),
"agreement_rate": (sum(1 for entry in reports if entry["agreement"]) / len(reports)) if reports else 0.0,
"by_task": {task: dict(Counter(entry["status"] for entry in reports if entry["task"] == task)) for task in tasks},
"by_bucket": {bucket: dict(Counter(entry["status"] for entry in reports if entry["bucket"] == bucket)) for bucket in buckets},
"error_counts": dict(Counter(error for entry in reports for error in entry["errors"])),
"target_kinds": {
"action_other": sum(1 for record in gold if record["task"] == "action" and R.ACTION_OTHER in record["target"]["actions"]),
"empty_results": sum(1 for record in gold if isinstance(record["target"], dict) and any(isinstance(v, list) and not v for v in record["target"].values())),
},
"budget": budget.summary(),
"quality": A.ANNOTATION_QUALITY,
"note": "LLM labels (two passes + arbitration), not human gold",
}
(out_dir / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())