File size: 12,354 Bytes
7399b6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""Registry + runner for the YAML-declared YAM tasks.

A task is a YAML file under ``tasks/`` (see ``tasks/fruit_basket.yaml``). This module turns it
into a command line for the skill script that implements it, so adding a task means writing a
YAML file -- not another 400-line demo script. Modelled on RoboLab's task registration: a task
declares its scene, its instruction variants, and its success conditions; the conditions come
from ``bimanual.yam_tasks.conditions``.

    python scripts/yam_task_runner.py list                    # registry table
    python scripts/yam_task_runner.py show fruit_basket       # resolved command + conditions
    python scripts/yam_task_runner.py run fruit_basket        # run it, print PASS/FAIL + video
    python scripts/yam_task_runner.py run --all --tag robotwin-asset

No Isaac import here: the runner only builds and launches commands, so ``list``/``show`` are
instant and usable without a GPU.
"""
from __future__ import annotations

import argparse
import os
import subprocess
import sys
from pathlib import Path

import yaml

REPO = Path(__file__).resolve().parent.parent
TASK_DIR = REPO/"tasks"
PY = os.environ.get("YAM_PYTHON", sys.executable)
KIT_ARGS = "--kit_args=--/rtx/verifyDriverVersion/enabled=false"

# skill name -> the script that implements it. A skill is "how to do it"; a task is "what,
# with which objects, and what counts as done".
SKILLS = {
    "pick_place": "scripts/yam_grasp_prm.py",
    "insert": "scripts/yam_grasp_prm.py",
    "multi_pick": "scripts/yam_multi_pick.py",
    "stack": "scripts/yam_stack_target.py",
    "dual_arm_lift": "scripts/yam_pot_lift.py",
    "tool_use": "scripts/yam_tool_tasks.py",
    "sort": "scripts/yam_cubes.py",
}


def load_tasks() -> dict[str, dict]:
    tasks = {}
    for f in sorted(TASK_DIR.glob("*.yaml")):
        spec = yaml.safe_load(f.read_text())
        spec["_file"] = str(f.relative_to(REPO))
        tasks[spec["name"]] = spec
    return tasks


def _xy(v):
    return f"{float(v[0])},{float(v[1])}"


def build_command(spec: dict) -> list[str]:
    """Resolve a task spec into an executable command line."""
    skill = spec["skill"]
    if skill not in SKILLS:
        raise SystemExit(f"unknown skill {skill!r}; known: {sorted(SKILLS)}")
    cmd = [PY, str(REPO/SKILLS[skill]), "--headless"]
    scene = spec.get("scene", {})
    params = spec.get("params", {})
    objects = scene.get("objects", [])
    container = scene.get("container")
    video = spec.get("video", f"outputs/tasks/{spec['name']}.mp4")

    if skill == "multi_pick":
        pick = params.get("pick", [o["name"] for o in objects if o.get("role") == "target"])
        leave = params.get("leave", [o["name"] for o in objects if o.get("role") == "distractor"])
        by_name = {o["name"]: o for o in objects}
        cmd += ["--pick", ",".join(pick), "--leave", ",".join(leave),
                "--pick_xy", ";".join(_xy(by_name[n]["xy"]) for n in pick)]
        if leave:
            cmd += ["--leave_xy", ";".join(_xy(by_name[n]["xy"]) for n in leave)]
        if container:
            cmd += ["--basket_xy", _xy(container["xy"]),
                    "--container", container["usd"],
                    "--container_scale", str(container.get("scale", 1.0))]
            if container.get("on_top"):
                cmd += ["--on_top"]
        if params.get("label"):
            cmd += ["--label", params["label"]]

    elif skill in ("pick_place", "insert"):
        obj = objects[0]
        cmd += ["--obj", obj["name"], "--obj_xy", _xy(obj["xy"])]
        if container:
            cmd += ["--box_xy", _xy(container["xy"])]
            if container.get("usd"):
                cmd += ["--container", container["usd"],
                        "--container_scale", str(container.get("scale", 1.0))]
            else:
                cmd += ["--basket"]
        if skill == "insert":
            cmd += ["--insert", "--basket"]
            if params.get("hole"):
                cmd += [f"--hole={params['hole']}"]
            if params.get("grasp_top"):
                cmd += [f"--grasp_top={params['grasp_top']}"]
        if params.get("jaw"):
            cmd += ["--jaw", params["jaw"]]

    elif skill == "stack":
        cmd += ["--blocks", ",".join(params["blocks"]),
                "--block_xy", ";".join(_xy(o["xy"]) for o in objects if o.get("role") != "target_region"),
                "--target_xy", _xy(scene["target_region"]["xy"]),
                "--target_size", str(scene["target_region"].get("size", 0.11))]

    elif skill == "dual_arm_lift":
        cmd += ["--obj", objects[0]["name"], "--pot_xy", _xy(objects[0]["xy"])]
        if params.get("pot_rpy"):
            cmd += ["--pot_rpy", params["pot_rpy"]]
        if params.get("lift"):
            cmd += ["--lift", str(params["lift"])]

    elif skill == "tool_use":
        cmd += ["--task", params["mode"]]
        if objects:
            cmd += ["--obj", objects[0]["name"], "--obj_xy", _xy(objects[0]["xy"])]
        if params.get("tool"):
            cmd += ["--tool", params["tool"]]
        if scene.get("target_region"):
            cmd += ["--target_xy", _xy(scene["target_region"]["xy"]),
                    "--target_size", str(scene["target_region"].get("size", 0.10))]

    elif skill == "sort":
        cmd += ["--task", params.get("mode", "sort")]

    if "episode" in params:
        cmd += ["--episode", str(params["episode"])]
    cmd += ["--video", video, KIT_ARGS]
    return cmd


def env_for(spec: dict) -> dict:
    """Extra env vars a task needs (RoboTwin asset dir, runtime-registered objects)."""
    env = dict(os.environ)
    env.setdefault("OMNI_KIT_ACCEPT_EULA", "YES")
    env.setdefault("OMNI_KIT_ALLOW_ROOT", "1")
    env.setdefault("PYTHONUNBUFFERED", "1")
    env.setdefault("ROBOTWIN_USD", os.environ.get("ROBOTWIN_USD", "/home/yu/internship_yu/robotwin_usd"))
    extra = spec.get("scene", {}).get("register_objects")
    if extra:
        env["YAM_RW_OBJECTS"] = ",".join(
            f"{o['name']}={o['usd']}:{o.get('scale', 0.05)}:{o.get('mass', 0.10)}" for o in extra)
    return env


def describe_conditions(spec: dict) -> list[str]:
    out = []
    for c in spec.get("success", []):
        (kind, kwargs), = c.items()
        out.append(f"{kind}({', '.join(f'{k}={v}' for k, v in kwargs.items())})")
    return out


def cmd_list(tasks, args):
    tag = args.tag
    rows = [(n, s.get("skill", "?"), ",".join(s.get("tags", [])), s.get("video", ""))
            for n, s in sorted(tasks.items()) if not tag or tag in s.get("tags", [])]
    w = max([len(r[0]) for r in rows]+[4])
    print(f"{'name'.ljust(w)}  {'skill'.ljust(14)}  tags")
    print("-"*(w+70))
    for n, sk, tg, _ in rows:
        print(f"{n.ljust(w)}  {sk.ljust(14)}  {tg}")
    print(f"\n{len(rows)} task(s) in {TASK_DIR.relative_to(REPO)}/")


def cmd_show(tasks, args):
    spec = tasks[args.name]
    print(f"# {spec['name']}  ({spec['_file']})")
    print(f"instruction: {spec.get('instruction', {}).get('default','')}")
    print(f"skill:       {spec['skill']} -> {SKILLS.get(spec['skill'])}")
    print(f"tags:        {', '.join(spec.get('tags', []))}")
    print("success conditions:")
    for c in describe_conditions(spec):
        print(f"  - {c}")
    print("\ncommand:")
    print("  " + " ".join(build_command(spec)))
    extra = env_for(spec).get("YAM_RW_OBJECTS")
    if extra:
        print(f"\nYAM_RW_OBJECTS={extra}")


def verify_run(spec: dict, video: Path, verifier: str | None) -> dict:
    """Agent-in-the-loop check: the numeric EPISODE_RESULT only inspects the final object pose,
    so it happily passes an episode where the cup landed upside-down or the basket is sunk into
    the table. Render a contact sheet and have a vision agent judge what actually happened.

    `verifier` is a shell command template receiving {sheet} and {claim}; it must print JSON
    {"verdict": ..., "notes": ...}. With no verifier the request is written out for a harness
    (e.g. a Claude Code subagent) to pick up -- nothing is uploaded anywhere by default.
    """
    sys.path.insert(0, str(REPO/"scripts"))
    from yam_agent_loop import make_sheet, REVIEW_PROMPT  # noqa: E402
    sheet = make_sheet(video)
    if sheet is None:
        return {"verdict": "NO_VIDEO", "notes": "no frames rendered"}
    claim = spec.get("instruction", {}).get("specific") or spec.get("instruction", {}).get("default", spec["name"])
    prompt = REVIEW_PROMPT.format(sheet=sheet.relative_to(REPO), claim=claim)
    if not verifier:
        req = REPO/"outputs"/"verify"/f"{spec['name']}_request.json"
        req.write_text(json.dumps({"sheet": str(sheet.relative_to(REPO)), "claim": claim,
                                   "prompt": prompt}, indent=2))
        return {"verdict": "PENDING_AGENT", "sheet": str(sheet.relative_to(REPO)),
                "request": str(req.relative_to(REPO))}
    out = subprocess.run(verifier.format(sheet=str(sheet), claim=json.dumps(claim)),
                         shell=True, capture_output=True, text=True, cwd=REPO)
    try:
        return json.loads(out.stdout.strip().splitlines()[-1])
    except Exception:
        return {"verdict": "UNPARSED", "notes": (out.stdout or out.stderr)[-400:]}


def cmd_run(tasks, args):
    names = sorted(tasks) if args.all else [args.name]
    if args.tag:
        names = [n for n in names if args.tag in tasks[n].get("tags", [])]
    results = {}
    for n in names:
        spec = tasks[n]
        cmd = build_command(spec)
        log = REPO/"outputs"/"tasks"/f"{n}.log"
        log.parent.mkdir(parents=True, exist_ok=True)
        (REPO/"outputs"/"verify").mkdir(parents=True, exist_ok=True)
        print(f"\n===== RUN {n} =====\n  {' '.join(cmd)}", flush=True)
        with open(log, "w") as fh:
            rc = subprocess.call(cmd, cwd=REPO, env=env_for(spec), stdout=fh, stderr=subprocess.STDOUT)
        text = log.read_text()
        verdict = "UNKNOWN"
        for line in text.splitlines():
            if "EPISODE_RESULT" in line:
                verdict = "SUCCESS" if "SUCCESS" in line else "FAIL"
        entry = {"numeric": verdict, "exit": rc, "video": spec.get("video")}
        if not args.no_verify:
            v = REPO/spec.get("video", f"outputs/tasks/{n}.mp4")
            entry["visual"] = verify_run(spec, v, args.verifier) if v.exists() else {"verdict": "NO_VIDEO"}
        results[n] = entry
        print(f"  exit={rc}  numeric={verdict}  visual={entry.get('visual', {}).get('verdict', 'skipped')}"
              f"  video={spec.get('video')}", flush=True)
    (REPO/"outputs"/"verify"/"run_results.json").write_text(json.dumps(results, indent=2))
    print("\n===== SUMMARY (numeric result + agent visual verdict) =====")
    for n, e in results.items():
        vis = e.get("visual", {}).get("verdict", "skipped")
        print(f"  {e['numeric'].ljust(8)} | {str(vis).ljust(14)} {n}  -> {e['video']}")
    print("\nagreeing SUCCESS+CONFIRMED is the only state that counts as done; "
          "feed the rest to `yam_agent_loop.py update` for the concrete parameter fix.")
    return 0 if all(e["numeric"] == "SUCCESS" for e in results.values()) else 1


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)
    p = sub.add_parser("list"); p.add_argument("--tag", default="")
    p = sub.add_parser("show"); p.add_argument("name")
    p = sub.add_parser("run"); p.add_argument("name", nargs="?"); p.add_argument("--all", action="store_true")
    p.add_argument("--tag", default="")
    p.add_argument("--no-verify", action="store_true", help="skip the agent visual check")
    p.add_argument("--verifier", default=os.environ.get("YAM_VERIFIER"),
                   help="shell command template with {sheet} and {claim}; must print JSON "
                        "{verdict, notes}. Omit to just emit the review request for an agent.")
    args = ap.parse_args()
    tasks = load_tasks()
    if args.cmd == "list":
        return cmd_list(tasks, args)
    if args.cmd == "show":
        return cmd_show(tasks, args)
    if args.cmd == "run":
        if not args.all and not args.name:
            raise SystemExit("give a task name or --all")
        return cmd_run(tasks, args)


if __name__ == "__main__":
    sys.exit(main() or 0)