commit 2da2f61bc875e51d5b4c790147fb558fbde63a41 Author: Eiichi Shimazu Date: Sat Nov 14 04:46:33 2026 +0900 実機推論の始めの確かめ (Enter) を, lab_start の 1 回目の試行だけにする (依頼 177・Log35). run_policy_client.py に --skip-first-confirm を足す (最初に実行する推論の前の確かめだけ待たずに実行し, 画面に [CONFIRM] の 1 行・そのチャンクの行に confirm_skipped. 2 回目からの確かめ・補正 (キー・ゲームパッド)・巻き戻し・止める操作・開始時に爪を開くのは今までどおり). log.jsonl の頭 (type=model) に skip_first_confirm. lab_start.sh は 1 回の起動の中の 2 回目からの試行 (round 2 から) にだけ --skip-first-confirm を渡す (7 の HOME・爪を開く・位置合わせは毎回). テストを足す (test_skip_first_confirm 6・lab_start 214. test_rewind_correct は新しい 1 項目を比べる対象から外す) (依頼 177. codex 1 回) Co-Authored-By: Claude Opus 5.5 --- lab_start.sh | 10 +++- pi05/run_policy_client.py | 19 +++++- pi05/test_rewind_correct.py | 3 +- pi05/test_skip_first_confirm.py | 125 ++++++++++++++++++++++++++++++++++++++++ test_lab_start.sh | 24 ++++++++ 5 files changed, 176 insertions(+), 5 deletions(-) diff --git a/lab_start.sh b/lab_start.sh index a4dd15f..4e9371c 100755 --- a/lab_start.sh +++ b/lab_start.sh @@ -59,6 +59,8 @@ # 6. tools/daemon_ctl.sh status で 5555 の LISTEN を見る. なければ start # 7. HOME → 爪を開く → camera_align.py (q で閉じる) # 8. run_policy_client.py (LOG407 と同じ引数). 終わったら, もう1回やるかを聞く (y なら 7 へ) +# 2 回目からは --skip-first-confirm を渡し, 始めの確かめ (最初の推論の前の Enter) を待たずに進む (依頼 177. +# 7 の HOME・爪を開く・位置合わせは毎回行う. 2 回目からの確かめ・補正・巻き戻し・止める操作は今までどおり) # 9. Pod は消さない. 片付けは ./lab_stop.sh # # Pod の情報 (id・GPU・1時間の料金・作った時刻・サーバーを起動したモデル) は /pod/lab_pod.json に残す (IP とポートは残さない). @@ -513,6 +515,8 @@ trap ':' INT round=0 while :; do round=$((round + 1)) + CONFIRM_ARGS=() # 依頼 177: 1 回目の試行だけ始めの確かめを待つ. 2 回目からは飛ばす + [ $round -ge 2 ] && CONFIRM_ARGS=(--skip-first-confirm) if [ $DRY_RUN = 0 ]; then stage "7. HOME → 爪を開く → 位置合わせ ($round 回目)" python3 dynamic_tracking_limits/place/place_modules/08_home_from_anywhere.py || fail "HOME に戻せない" @@ -525,13 +529,15 @@ while :; do stage "8. 実機推論 ($round 回目)" python3 pi05/run_policy_client.py --raw-dir "$RAW_RANGE" --steps-per-chunk 10 --via-stride 3 \ --settle-retries 0 --state-from-target --execute --ignore-framing --model "$MODEL_TAG" --framing-raw-dir "$RAW" \ - --tilt-raw-dir "$RAW_RANGE" ${TILT_FIX_ARGS[@]+"${TILT_FIX_ARGS[@]}"} ${GRIP_ARGS[@]+"${GRIP_ARGS[@]}"} ${FLOW_ARGS[@]+"${FLOW_ARGS[@]}"} + --tilt-raw-dir "$RAW_RANGE" ${TILT_FIX_ARGS[@]+"${TILT_FIX_ARGS[@]}"} ${GRIP_ARGS[@]+"${GRIP_ARGS[@]}"} ${FLOW_ARGS[@]+"${FLOW_ARGS[@]}"} \ + ${CONFIRM_ARGS[@]+"${CONFIRM_ARGS[@]}"} rc=$? else stage "8. 推論のクライアント (ドライラン・--dry-keys. アームには触らない)" python3 pi05/run_policy_client.py --raw-dir "$RAW_RANGE" --steps-per-chunk 10 --via-stride 3 \ --settle-retries 0 --state-from-target --dry-keys --ignore-framing --model "$MODEL_TAG" --framing-raw-dir "$RAW" \ - --tilt-raw-dir "$RAW_RANGE" ${TILT_FIX_ARGS[@]+"${TILT_FIX_ARGS[@]}"} ${GRIP_ARGS[@]+"${GRIP_ARGS[@]}"} ${FLOW_ARGS[@]+"${FLOW_ARGS[@]}"} + --tilt-raw-dir "$RAW_RANGE" ${TILT_FIX_ARGS[@]+"${TILT_FIX_ARGS[@]}"} ${GRIP_ARGS[@]+"${GRIP_ARGS[@]}"} ${FLOW_ARGS[@]+"${FLOW_ARGS[@]}"} \ + ${CONFIRM_ARGS[@]+"${CONFIRM_ARGS[@]}"} rc=$? fi echo "[CLIENT] run_policy_client.py が終わった (終了コード $rc. モデル $MODEL_TAG)" diff --git a/pi05/run_policy_client.py b/pi05/run_policy_client.py index 0e20e4c..cc6149a 100644 --- a/pi05/run_policy_client.py +++ b/pi05/run_policy_client.py @@ -12,6 +12,9 @@ 安全策: - 既定はドライラン (アームに送らない). 送るには --execute - --execute でも既定は 1 チャンクごとに Enter で確認 (--auto で連続) + --skip-first-confirm を付けると始めの確かめ (最初に実行する推論の前) だけ待たずに実行し, 画面に [CONFIRM] の 1 行・ + log.jsonl の頭 (type=model) に skip_first_confirm・そのチャンクの行に confirm_skipped を残す. 2 回目からの確かめは + 今までどおり (lab_start.sh が 1 回の起動の中の 2 回目からの試行に付ける. 依頼 177) - 学習データで通った関節範囲 ±ENVELOPE_MARGIN_DEG の外へは出さない (クランプ) - 肩 j1 の上限は J1_SAFE_MAX (88°) で更に切る. これを超える指令はアームが黙って捨てる (LOG352) - 送る直前に最終検査. 6 関節のどれかが安全範囲の外か非有限値なら, 送信もグリッパー操作もせず @@ -1404,6 +1407,9 @@ def main(): ap.add_argument("--flow-tol", type=float, default=FLOW_TOL_DEFAULT, help=f"--flow で途中の点にこれだけ近づいたら次を送る (度. 既定 {FLOW_TOL_DEFAULT:.0f}. " f"送る間隔は {FLOW_MIN_INTERVAL_SEC:.1f} 秒より短くしない)") + ap.add_argument("--skip-first-confirm", action="store_true", + help="始めの確かめ (最初に実行する推論の前の Enter) だけを待たずに実行する. 2 回目からの確かめと " + "補正・巻き戻し・止める操作は今までどおり (lab_start.sh が 2 回目からの試行に付ける. 依頼 177)") ap.add_argument("--speed", type=int, default=SPEED, help=f"推論のチャンクを送る速さ (既定 {SPEED} = 今までと同じ. 補正・巻き戻し・HOME は {SPEED} のまま. 依頼 176)") args = ap.parse_args() @@ -1434,7 +1440,8 @@ def main(): "framing_raw_dir": str(framing_raw_dir(args)), "tilt_raw_dir": str(tilt_raw_dir(args)), "tilt_fix": not args.no_tilt_fix, "grip_value": GRIP_VALUE, "flow": bool(args.flow), "flow_tol": args.flow_tol, - "speed": CHUNK_SPEED, "host": args.host, "port": args.port, "t": time.time()}, + "speed": CHUNK_SPEED, "skip_first_confirm": bool(args.skip_first_confirm), + "host": args.host, "port": args.port, "t": time.time()}, ensure_ascii=False) + "\n") log_f.flush() @@ -1450,6 +1457,7 @@ def main(): history = [] # 巻き戻せる区間 (推論 1 回分 / 補正 1 回分). 新しいものが後ろ n_rewinds = n_corrections = 0 end_reason, outcome = None, None + skip_confirm = bool(args.skip_first_confirm) # 依頼 177: 始めの確かめを 1 回だけ飛ばす (使ったら False) try: cams = camera_resolver.resolve_cameras(require_top=True, require_side=False, verbose=False) cap = cv2.VideoCapture(cams["top"]) @@ -1636,7 +1644,14 @@ def main(): # 実測を渡すために None のままにする (--state-from-target 用). last_sent_target = None grasp = None # つかんだ区間を巻き戻すときの答え (GRASP_PLACED / GRASP_DROPPED) - if not args.auto: + skip_now, skip_confirm = skip_confirm and not args.auto, False + if skip_now: + # 依頼 177: lab_start.sh の 2 回目からの試行は, 始めの確かめ (Enter) を待たずに実行する. + # 2 回目からの確かめ・補正・巻き戻し・止める操作は今までどおり + print(" [CONFIRM] 2 回目からの試行なので始めの確かめ (Enter) を待たずに実行します " + "(次の推論からは今までどおり確かめます)") + rec["confirm_skipped"] = True + elif not args.auto: # 入力を待っている間は wait (人が手でブロックを置き直す. 学習データに使わない. 依頼 27) if recorder is not None: recorder.set_mode(MODE_WAIT) diff --git a/pi05/test_rewind_correct.py b/pi05/test_rewind_correct.py index ad59e3e..f20e313 100644 --- a/pi05/test_rewind_correct.py +++ b/pi05/test_rewind_correct.py @@ -605,7 +605,8 @@ class TestClientFlow(unittest.TestCase): new_keys = ("t_exec_start", "t_exec_end", "sent_targets", "framing_raw_dir", # framing_raw_dir は依頼 137 "tilt_raw_dir", "tilt_fix", "measured_on", "rotate_deg", # 依頼 144 "grip_value", # 依頼 152 - "flow", "flow_tol", "speed") # 依頼 176 (type=model の頭) + "flow", "flow_tol", "speed", # 依頼 176 (type=model の頭) + "skip_first_confirm") # 依頼 177 (type=model の頭) # HEAD が依頼 25 より前なら new_keys は新しい方にだけある. 依頼 25 以降なら両方にある (両方から外す) drop = ("t", "latency_s", "reference") + new_keys so, sn = strip(l_old, drop=drop), strip(l_new, drop=drop) diff --git a/pi05/test_skip_first_confirm.py b/pi05/test_skip_first_confirm.py new file mode 100644 index 0000000..ede6f39 --- /dev/null +++ b/pi05/test_skip_first_confirm.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""test_skip_first_confirm.py — 始めの確かめ (Enter) を飛ばす --skip-first-confirm の確かめ (依頼 177. アームなし). + + python3 pi05/test_skip_first_confirm.py # 全部 + python3 pi05/test_skip_first_confirm.py -v + +lab_start.sh は 1 回の起動の中の 2 回目からの試行に --skip-first-confirm を付ける (test_lab_start.sh で確かめる). +ここでは run_policy_client を test_rewind_correct.run_client (偽の daemon / カメラ / モデル) で通し, + - 最初に実行する推論だけ Enter を待たずに実行し, 画面に [CONFIRM] の 1 行, log.jsonl の頭に skip_first_confirm + - 2 回目からの確かめ・b / c・q / h は今までどおり + - 開始時に爪を開くのは今までどおり行う + - 付けなければ今までと同じ (頭の skip_first_confirm は false) +を見る. KIOXIA にもアームにも触らない (一時フォルダだけ). +""" +import sys +import tempfile +import time +import unittest +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(_HERE)) + +import run_policy_client as rpc # noqa: E402 +from test_rewind_correct import run_client # noqa: E402 + +EXEC = ["--execute", "--steps-per-chunk", "10", "--via-stride", "3", "--settle-retries", "0", + "--state-from-target", "--ignore-framing", "--max-chunks", "6"] +SKIP_LINE = "[CONFIRM] 2 回目からの試行なので始めの確かめ (Enter) を待たずに実行します" + + +def chunks(lines): + return [x for x in lines if x.get("type") is None and "chunk" in x] + + +def model_head(lines): + return lines[0] + + +class TestSkipFirstConfirm(unittest.TestCase): + def setUp(self): + self.td = Path(tempfile.mkdtemp()) + time.sleep(1.1) # run のフォルダ名は秒単位なので重ならないように + + def test_execute_skips_only_first(self): + # 推論 0 は待たずに実行 → 推論 1 は Enter で実行 → 推論 2 で q → 結果は記録しない + code, world, run, lines, out = run_client(rpc, EXEC + ["--skip-first-confirm"], ["", "q", ""], self.td) + self.assertEqual(code, 0) + head = model_head(lines) + self.assertEqual(head["type"], "model") + self.assertIs(head["skip_first_confirm"], True) + ch = chunks(lines) + self.assertEqual([x["chunk"] for x in ch], [0, 1, 2]) + self.assertEqual([bool(x.get("executed")) for x in ch], [True, True, False]) + self.assertIs(ch[0].get("confirm_skipped"), True) + self.assertNotIn("confirm_skipped", ch[1]) + self.assertEqual(out.count(SKIP_LINE), 1) + self.assertEqual(out.count("[CONFIRM]"), 1) + # 開始時に爪を開くのは今までどおり (推論 0 の送信より前) + self.assertIs(ch[0].get("initial_gripper_open"), True) + self.assertEqual(world.grips[0], 0) + self.assertEqual(world.events[0], ("grip", 0)) + labels = [s[0] for s in world.sends] + self.assertEqual(labels[:4], ["pi05_chunk000_via02", "pi05_chunk000_via05", + "pi05_chunk000_via08", "pi05_chunk000_via09"]) + self.assertTrue(any(lb.startswith("pi05_chunk001") for lb in labels)) + self.assertFalse(any(lb.startswith("pi05_chunk002") for lb in labels)) + # 飛ばした推論 0 の間は wait にしない. 推論 1 の前からは今までどおり wait + stages = [l.split(",")[2] for l in (run / "recording" / "states.csv").read_text().splitlines()[1:] + if l.split(",")[1] == "stage"] + self.assertEqual(stages[:3], ["model", "wait", "model"]) + + def test_without_flag_waits_first(self): + # 同じ入力で付けなければ, 推論 0 を Enter で実行 → 推論 1 で q (今までどおり) + code, world, run, lines, out = run_client(rpc, EXEC, ["", "q", ""], self.td) + self.assertEqual(code, 0) + self.assertIs(model_head(lines)["skip_first_confirm"], False) + ch = chunks(lines) + self.assertEqual([bool(x.get("executed")) for x in ch], [True, False]) + self.assertFalse(any("confirm_skipped" in x for x in ch)) + self.assertNotIn("[CONFIRM]", out) + self.assertIs(ch[0].get("initial_gripper_open"), True) + + def test_correction_and_rewind_unchanged_after_skip(self): + # 推論 0 は待たずに実行 → 推論 1 の確かめで c (j1 を + 2 刻み) → Enter で戻る → 推論 2 で b (補正を巻き戻す) + # → 推論 3 で h → 結果 y + code, world, run, lines, out = run_client(rpc, EXEC + ["--skip-first-confirm"], + ["c", "b", "h", "y"], self.td, keys="1++\n") + self.assertEqual(code, 0) + corr = [x for x in lines if x.get("type") == "correction"] + self.assertEqual([x["action"] for x in corr], ["move", "move", "accept"]) + rw = [x for x in lines if x.get("type") == "rewind"] + self.assertEqual(len(rw), 1) + self.assertTrue(rw[0]["finished"]) + self.assertEqual([s[0] for s in world.sends][-1], "pi05_home") + self.assertEqual(out.count("[CONFIRM]"), 1) + + def test_q_on_second_confirm(self): + # 2 回目の確かめで止められる (推論 0 だけ実行) + code, world, run, lines, out = run_client(rpc, EXEC + ["--skip-first-confirm"], ["q", ""], self.td) + self.assertEqual(code, 0) + self.assertEqual([bool(x.get("executed")) for x in chunks(lines)], [True, False]) + + def test_dry_keys(self): + code, world, run, lines, out = run_client( + rpc, ["--dry-keys", "--steps-per-chunk", "10", "--via-stride", "3", "--max-chunks", "5", + "--skip-first-confirm"], ["", "q"], self.td) + self.assertEqual(code, 0) + self.assertEqual(world.sends, []) + ch = chunks(lines) + self.assertEqual([bool(x.get("simulated")) for x in ch], [True, True, False]) + self.assertIs(ch[0].get("confirm_skipped"), True) + self.assertEqual(out.count(SKIP_LINE), 1) + + def test_auto_has_nothing_to_skip(self): + code, world, run, lines, out = run_client( + rpc, EXEC[:-1] + ["2", "--auto", "--skip-first-confirm"], [""], self.td) + self.assertEqual(code, 0) + self.assertNotIn("[CONFIRM]", out) + self.assertFalse(any("confirm_skipped" in x for x in chunks(lines))) + self.assertIs(model_head(lines)["skip_first_confirm"], True) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_lab_start.sh b/test_lab_start.sh index d3addf6..626f1eb 100755 --- a/test_lab_start.sh +++ b/test_lab_start.sh @@ -628,6 +628,30 @@ for bad in 0 101 abc 1000 ""; do rm -rf "$T" done +echo "== 始めの確かめは 1 回目の試行だけ (依頼 177): 2 回目からは --skip-first-confirm を渡す" +setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1; run +check "1 回だけの試行には --skip-first-confirm を渡さない" '[ "$(grep -c "pi05/run_policy_client.py" "$T/py.log")" = 1 ] && ! has py.log "--skip-first-confirm"' +rm -rf "$T" +# 「もう1回やりますか?」は端末のときだけ聞くので, script で端末を作って y・y・n を打つ (LAB_LOGGED=1 で記録の script は飛ばす) +setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1 +# 偽物の pod_ssh は標準入力を読み捨てる. 端末では終わりが来ず止まり y を食べるので, この場合だけ端末のときは読まない +sed -i '' 's#^ cat > /dev/null$# [ -t 0 ] || cat > /dev/null#' "$T/fakes.sh" +# 先に打つと端末が待つ前に入力が尽きて script が終わるので, 問いが出るたびに 1 つずつ打つ (問いが出なければ 60 秒であきらめる) +feed() { + local n k + for n in 1 2 3; do + for k in $(seq 1 300); do [ "$(grep -ac "もう1回やりますか" "$T/out.txt" 2>/dev/null)" -ge $n ] && break; sleep 0.2; done + if [ $n -lt 3 ]; then printf 'y\n'; else printf 'n\n'; fi + done + sleep 1 +} +(cd "$T" && feed | PATH="$T/bin:$PATH" LAB_LOGGED=1 script -q /dev/null ./lab_start.sh --log LogT --dry-run --flow > "$T/out.txt" 2>&1); RC=$? +grep "pi05/run_policy_client.py" "$T/py.log" > "$T/client.log" +check "3 回の試行でクライアントを 3 回呼ぶ (終了コード 0)" '[ $RC = 0 ] && [ "$(grep -c . "$T/client.log")" = 3 ] && [ "$(grep -ac "=== 8. " "$T/out.txt")" = 3 ]' +check "1 回目には付けず, 2 回目・3 回目の終わりに --skip-first-confirm を付ける (ほかの引数は同じ)" '! sed -n 1p "$T/client.log" | grep -q -- "--skip-first-confirm" && sed -n 2p "$T/client.log" | grep -q -- "--flow --skip-first-confirm$" && sed -n 3p "$T/client.log" | grep -q -- "--flow --skip-first-confirm$" && [ "$(sed -n 2p "$T/client.log" | sed "s/ --skip-first-confirm$//")" = "$(sed -n 1p "$T/client.log")" ]' +rm -rf "$T" +check "実機の 8 も 2 回目からは --skip-first-confirm を渡し, 7 の HOME・爪を開くは毎回流す" '[ "$(grep -c '"'"'${CONFIRM_ARGS\[@\]+"${CONFIRM_ARGS\[@\]}"}'"'"' "$HERE/lab_start.sh")" = 2 ] && grep -qF "[ \$round -ge 2 ] && CONFIRM_ARGS=(--skip-first-confirm)" "$HERE/lab_start.sh" && [ "$(awk "/^while :; do/,/^done/" "$HERE/lab_start.sh" | grep -c "08_home_from_anywhere.py\|daemon_set_gripper(0)")" = 2 ]' + echo echo "OK $OK / NG $NG" [ $NG = 0 ]