#!/usr/bin/env python3 """decision_eval.py — Gemini Decision 1 が mechArm のお手本をどこまで正しく判定できるかを測る (読むだけ). 正解: lab/Log25・Log26 の runs_table.csv (人の判定 pick/place・外した理由) と grasp_depth.py の結果 (浅い=閉じて1秒後 24px 超・手の疑い). 入力: manifest.csv (prep.py が作る. 1本につき上カメラのコマ3枚: 閉じて1秒後 after / 閉じて3秒後 lift / 最後のコマ end) 鍵: /Volumes/KIOXIA/gemini_api_key.txt python3 decision_eval.py run [--limit N] [--resolution low|medium|high] # API を呼んで results.csv に書く python3 decision_eval.py score # results.csv と正解を比べて score.md に書く """ import argparse, base64, csv, json, os, sys, time, urllib.request, statistics as st URL = "https://generativelanguage.googleapis.com/v1beta/decisions" KEY = "/Volumes/KIOXIA/gemini_api_key.txt" CTX = ("Top-down camera image of a small desktop robot arm (mechArm 270) with a two-finger gripper " "picking a red cube from a mat and placing it into a white box.\n") Q = { "after": { "depth": {"type": "score", "instructions": CTX + "The gripper has just closed on the red cube. How deep is the cube inside the gripper fingers?", "criteria": ["Deep: the cube sits well inside the fingers, not sticking out past the finger tips.", "Partly: the cube sticks out slightly past the finger tips.", "Shallow: the cube is held only at the very tips of the fingers or sticks out a lot."]}, "claw_on_block": {"type": "noul", "instructions": CTX + "Is a gripper finger resting on top of the red cube instead of closing beside it?"}, "hand": {"type": "noul", "instructions": CTX + "Is a human hand or finger visible in the image?"}, }, "lift": {"picked": {"type": "noul", "instructions": CTX + "Is the red cube held between the gripper fingers and lifted off the mat?"}}, "end": {"placed": {"type": "noul", "instructions": CTX + "Is the red cube inside the white box?"}}, } def call(key, img, questions, res): body = {"model": "gemini-decision-1", "input": [{"type": "image", "data": base64.b64encode(open(img, "rb").read()).decode(), "mime_type": "image/jpeg", "resolution": res}], "questions": questions} req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json", "x-goog-api-key": key}) t = time.time() with urllib.request.urlopen(req, timeout=60) as r: out = json.load(r) return out, time.time() - t def run(a): key = open(KEY).read().strip() rows = list(csv.DictReader(open("manifest.csv")))[: a.limit or None] w = csv.writer(open("results.csv", "w")); w.writerow(["run", "image", "question", "value", "probabilities", "confidence", "latency_s", "input_tokens", "resolution"]) for r in rows: for kind, qs in Q.items(): img = r[f"img_{kind}"] if not img: continue try: out, lat = call(key, img, qs, a.resolution) except Exception as e: print("ERR", r["run"], kind, e, file=sys.stderr); continue tok = out.get("usage", {}).get("total_input_tokens", "") for q, ans in out["answers"].items(): v = ans.get("noul", ans.get("score", ans.get("choice"))) w.writerow([r["run"], kind, q, v, json.dumps(ans.get("probabilities", "")), ans.get("confidence"), round(lat, 3), tok, a.resolution]) print(r["run"], kind, "ok", round(lat, 2)) time.sleep(0.4) def score(a): man = {r["run"]: r for r in csv.DictReader(open("manifest.csv"))} res = {} for r in csv.DictReader(open("results.csv")): res[(r["run"], r["question"])] = r truth = { # 正解 (True = 問いに「はい」) "depth": lambda m: m["shallow"] == "True", # 浅い (24px 超) "claw_on_block": lambda m: m["claw_on_block"] == "True", "hand": lambda m: m["hand_suspect"] == "True", "picked": lambda m: m["human_pick"] == "yes", "placed": lambda m: m["human_place"] == "yes"} L = ["# Gemini Decision 1 の判定と正解の比べ\n", "| 問い | 本数 | 正解が「はい」 | 一致 | 見逃し | 誤り | 自信0.8以上の一致 | 自信0.8未満の本数 |", "|---|---|---|---|---|---|---|---|"] for q, f in truth.items(): n = tp = fn = fp = hi_n = hi_ok = lo = 0; pos = 0 for run, m in man.items(): r = res.get((run, q)) if not r: continue t = f(m); v = float(r["value"]); c = float(r["confidence"] or 0) p = (v >= 1.0) if q == "depth" else (v >= 0.5) # depth は 0〜2 の段階の平均. 1 以上を「浅い」とみなす n += 1; pos += t; ok = (p == t); tp += ok and t; fn += (t and not p); fp += (p and not t) if c >= 0.8: hi_n += 1; hi_ok += ok else: lo += 1 agree = sum(1 for _ in [0]) if n: L.append(f"| {q} | {n} | {pos} | {sum(1 for run,m in man.items() if (run,q) in res and ((float(res[(run,q)]['value'])>=(1.0 if q=='depth' else 0.5))==f(m)))} | {fn} | {fp} | {hi_ok}/{hi_n} | {lo} |") lats = [float(r["latency_s"]) for r in res.values()]; toks = [int(r["input_tokens"]) for r in res.values() if r["input_tokens"]] if lats: L.append(f"\n1回の依頼の時間 (通信込み): 中央値 {st.median(lats):.2f} 秒 / 最大 {max(lats):.2f} 秒") if toks: L.append(f"入力トークン: 1回あたり中央値 {int(st.median(toks))} / 合計 {sum(toks)} (費用の目安 ${sum(toks)/1e6*0.10:.4f})") open("score.md", "w").write("\n".join(L) + "\n"); print("\n".join(L)) if __name__ == "__main__": ap = argparse.ArgumentParser(); ap.add_argument("cmd", choices=["run", "score"]); ap.add_argument("--limit", type=int); ap.add_argument("--resolution", default="medium") a = ap.parse_args(); run(a) if a.cmd == "run" else score(a)