commit 284dfd731e64353c996486432811aebb5ee47ea0
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Fri Nov 6 19:51:31 2026 +0900

    11-06 のゲームパッドで直した成功 5 本を diag_joint_hold.py で点検できるようにし (rpc 行 send_angle / stop を log.jsonl の pad の行と組にして指令を作る・長押しの関節は見ない・送ったらすぐ返る daemon の区間は向かう途中を遅れに数えず指令から離れた分 away を見る. 11-01 の結果は変わらない), 外れのない 183926・184452・184650・184906 を corr_v4b の 7 本に足した corr_v5 で, corr2 の 1499 から続きを学習する lab_train.sh / lab_start.sh の --model corr3 を足す (設定 pi05_mecharm_trim_corr_v5_lora・上げる先 pi05-mecharm-trim-corr-lora-v3). build_correction_episodes がゲームパッドの区間を扱えることを test_pad_build.py で確かめる (依頼 97. codex 3 回)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/lab_lib.sh b/lab_lib.sh
index f890316..e9a0db7 100644
--- a/lab_lib.sh
+++ b/lab_lib.sh
@@ -275,11 +275,14 @@ lab_cmd_addon() {           # 3f. 何度流しても同じ結果 (install_addon.
 # corr の学習の設定 (pi05_mecharm_trim_corr_lora) は lab_train.sh の学習と同じ値で足す (add_trim_corr_config.py は
 # 入っている設定と --steps / --init が違うと止まる. 推論では weight_loader は使わない)
 # corr2 (依頼 57) は corr_v1_1499 から trim2_corr_v4 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v4_lora)
-LAB_MODELS="trim corr corr2"
+# corr3 (依頼 97) は corr_v2_1499 から trim2_corr_v5 で続きを学習したもの (設定 pi05_mecharm_trim_corr_v5_lora)
+LAB_MODELS="trim corr corr2 corr3"
 LAB_CORR_STEPS=1500
 LAB_CORR_INIT=/workspace/ckpt/3999/params
 LAB_CORR2_STEPS=1500
 LAB_CORR2_INIT=/workspace/ckpt/corr_v1_1499/params
+LAB_CORR3_STEPS=1500
+LAB_CORR3_INIT=/workspace/ckpt/corr_v2_1499/params
 lab_model_def() {           # lab_model_def trim|corr → MODEL_REPO MODEL_CKPT MODEL_CONFIG MODEL_DATA MODEL_DESC. 知らない名前なら 1
     case "$1" in
         trim)
@@ -300,24 +303,31 @@ lab_model_def() {           # lab_model_def trim|corr → MODEL_REPO MODEL_CKPT
             MODEL_CONFIG=pi05_mecharm_trim_corr_v4_lora
             MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v4b     # 依頼 58: 182545 を外した 7 本 (8 本の corr_v4 は使わない)
             MODEL_DESC="corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (依頼 57 / 58)" ;;
+        corr3)
+            MODEL_REPO=PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
+            MODEL_CKPT=/workspace/ckpt/corr_v3_1499
+            MODEL_CONFIG=pi05_mecharm_trim_corr_v5_lora
+            MODEL_DATA=mecharm_redblock_box_teach_v1_trim2_corr_v5      # 依頼 97: corr_v4b の 7 本 + 11-06 のゲームパッドの 4 本
+            MODEL_DESC="corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (依頼 97)" ;;
         *) return 1 ;;
     esac
 }
 
-lab_cmd_addon_model() {     # lab_cmd_addon_model trim|corr|corr2 → 3f (trim は lab_cmd_addon と同じ. corr・corr2 は学習の設定と assets も足す)
+lab_cmd_addon_model() {     # lab_cmd_addon_model trim|corr|corr2|corr3 → 3f (trim は lab_cmd_addon と同じ. corr〜corr3 は学習の設定と assets も足す)
     lab_model_def "$1" || return 1
     if [ "$1" = trim ]; then lab_cmd_addon; return; fi
     local corr_args="--steps $LAB_CORR_STEPS --init $LAB_CORR_INIT"
     [ "$1" = corr2 ] && corr_args="--variant v4 --steps $LAB_CORR2_STEPS --init $LAB_CORR2_INIT"
+    [ "$1" = corr3 ] && corr_args="--variant v5 --steps $LAB_CORR3_STEPS --init $LAB_CORR3_INIT"
     # assets は hf download が置いたモデルの assets/local/<データ名> を, openpi の assets/<設定>/local/ に写す
     # (trim の 3f が assets/pi05_mecharm_trim_lora/local/ に写すのと同じ形. 中身は trim2 と同じ正規化の値)
     echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/add_trim_corr_config.py /workspace/openpi $corr_args && mkdir -p assets/$MODEL_CONFIG/local && cp -r $MODEL_CKPT/assets/local/$MODEL_DATA assets/$MODEL_CONFIG/local/"
 }
 
-# ---- lab_train.sh で作るモデル (依頼 57. --model corr | corr2) ----
+# ---- lab_train.sh で作るモデル (依頼 57. --model corr | corr2. 依頼 97 で corr3) ----
 # 作るモデルの名前は lab_start.sh の --model と同じ. 続きの元 (TRAIN_FROM) も lab_model_def のモデル
-LAB_TRAIN_MODELS="corr corr2"
-lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない名前なら 1)
+LAB_TRAIN_MODELS="corr corr2 corr3"
+lab_train_def() {           # lab_train_def corr|corr2|corr3 → TRAIN_* (知らない名前なら 1)
     case "$1" in
         corr)                   # 依頼 49: 3999 から trim2_corr_v3 で続きを学習した 1499
             TRAIN_FROM=trim; TRAIN_VARIANT=v3
@@ -325,6 +335,9 @@ lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない
         corr2)                  # 依頼 57: corr の 1499 から trim2_corr_v4b (依頼 58) で続きを学習する
             TRAIN_FROM=corr; TRAIN_VARIANT=v4
             TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v1/1499 ;;
+        corr3)                  # 依頼 97: corr2 の 1499 から trim2_corr_v5 で続きを学習する
+            TRAIN_FROM=corr2; TRAIN_VARIANT=v5
+            TRAIN_FROM_KIOXIA=pi05_mecharm_trim_corr_lora_v2/1499 ;;
         *) return 1 ;;
     esac
     lab_model_def "$1" || return 1
@@ -336,8 +349,8 @@ lab_train_def() {           # lab_train_def corr|corr2 → TRAIN_* (知らない
     lab_model_def "$1"          # MODEL_* は作るモデルのものに戻す
 }
 
-lab_cmd_addon_train() {     # lab_cmd_addon_train corr|corr2 → lab_train.sh の 3f
-    # corr は lab_cmd_addon と同じ (3999 を取っているので trim の assets も写す). corr2 は 3999 を取らないので
+lab_cmd_addon_train() {     # lab_cmd_addon_train corr|corr2|corr3 → lab_train.sh の 3f
+    # corr は lab_cmd_addon と同じ (3999 を取っているので trim の assets も写す). corr2・corr3 は 3999 を取らないので
     # addon と trim2 の設定だけを入れる (学習の設定と正規化の値は pod_prep_trim_corr.sh が置く)
     if [ "$1" = corr ]; then lab_cmd_addon; return; fi
     echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi"
diff --git a/lab_start.sh b/lab_start.sh
index bc67b77..41ec59b 100755
--- a/lab_start.sh
+++ b/lab_start.sh
@@ -13,8 +13,11 @@
 #              corr = 3999 から trim2_corr_v3 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v1)
 #              corr2 = corr の 1499 から trim2_corr_v4b で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2.
 #                      依頼 57. lab_train.sh --model corr2 で作る. Pod の /workspace/ckpt/corr_v2_1499)
+#              corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習した 1499 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3.
+#                      依頼 97. lab_train.sh --model corr3 で作る. Pod の /workspace/ckpt/corr_v3_1499)
 #              取る場所・設定・assets・サーバーの起動・つなぎ直しの確かめが選んだモデルに合う. 安全範囲 (学習データ ±10°)
-#              と位置合わせのお手本はどれも trim2 の raw_episodes (corr_v3・corr_v4 と合わせても範囲はほぼ同じで, trim2 の方が狭い)
+#              と位置合わせのお手本はどれも trim2 の raw_episodes (corr_v3・corr_v4 と合わせても範囲はほぼ同じで, trim2 の方が狭い.
+#              corr_v5 は 11-06 の 184650 の j2 が trim2 の上限 +31.4° を最大 11° 超える)
 #              つなぎ直しで, 残っている Pod のサーバーが別のモデルで動いていれば, そのことを出して止まる
 #
 # 1. Pod を作る (A40×1・Runpod Pytorch 2.8.0・コンテナのディスク 200GB・22/tcp を外へ出す)
@@ -62,7 +65,7 @@ while [ $# -gt 0 ]; do
         --dry-run) DRY_RUN=1; shift ;;
         --model) [ $# -ge 2 ] || { echo "[ERROR] --model には ${LAB_MODELS// / か } を付ける" >&2; exit 2; }     # 値がないと shift 2 が失敗して回り続ける (codex R1)
                  MODEL="$2"; shift 2 ;;
-        -h|--help) sed -n '2,39p' "$0"; exit 0 ;;
+        -h|--help) sed -n '2,42p' "$0"; exit 0 ;;
         *) echo "[ERROR] 知らない引数: $1  (./lab_start.sh --help)" >&2; exit 2 ;;
     esac
 done
diff --git a/lab_train.sh b/lab_train.sh
index a42bc4b..5e5efe3 100755
--- a/lab_train.sh
+++ b/lab_train.sh
@@ -13,8 +13,11 @@
 #                    corr  = 3999 から trim2_corr_v3 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v1
 #                    corr2 = corr の 1499 から trim2_corr_v4b で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v2
 #                            (設定 pi05_mecharm_trim_corr_v4_lora. 3c で取るのは 3999 ではなく corr の 1499)
-#   --steps / --init / --exp-name   学習 (既定 1500 / 続きの元の params / trim_corr_v3 か trim_corr_v4)
-#                    続きの元: corr は /workspace/ckpt/3999/params, corr2 は /workspace/ckpt/corr_v1_1499/params
+#                    corr3 = corr2 の 1499 から trim2_corr_v5 で続きを学習 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3
+#                            (依頼 97. 設定 pi05_mecharm_trim_corr_v5_lora. 3c で取るのは corr2 の 1499)
+#   --steps / --init / --exp-name   学習 (既定 1500 / 続きの元の params / trim_corr_v3・trim_corr_v4・trim_corr_v5)
+#                    続きの元: corr は /workspace/ckpt/3999/params, corr2 は /workspace/ckpt/corr_v1_1499/params,
+#                              corr3 は /workspace/ckpt/corr_v2_1499/params
 #   --poll-min     進みを見る間 (既定 5 分)
 #   --stall-min    学習のログが何分書かれなければ「止まった」とするか (既定 30 分)
 #
@@ -62,7 +65,7 @@ while [ $# -gt 0 ]; do
         --stall-min) lab_need_val 30 "$@"; STALL_MIN="$2"; shift 2 ;;
         --stop) MODE=stop; shift ;;
         --dry-run) DRY_RUN=1; shift ;;
-        -h|--help) sed -n '2,36p' "$0"; exit 0 ;;
+        -h|--help) sed -n '2,39p' "$0"; exit 0 ;;
         *) echo "[ERROR] 知らない引数: $1  (./lab_train.sh --help)" >&2; exit 2 ;;
     esac
 done
@@ -92,17 +95,17 @@ if [ $DRY_RUN = 1 ]; then
 fi
 CKPT_ROOT="$DATA_ROOT/checkpoints"
 ADD_TRIM="$DATA_ROOT/lab/Log11/pod/add_trim_config.py"
-use_model() {               # use_model corr|corr2 → 作るモデルで決まる値 (lab_lib.sh の lab_train_def. 依頼 57)
+use_model() {               # use_model corr|corr2|corr3 → 作るモデルで決まる値 (lab_lib.sh の lab_train_def. 依頼 57 / 97)
     lab_train_def "$1" || return 1
-    DATA_NAME=$TRAIN_DATA                                   # corr: ..._trim2_corr_v3 / corr2: ..._trim2_corr_v4b (依頼 58)
+    DATA_NAME=$TRAIN_DATA                                   # corr: ..._trim2_corr_v3 / corr2: ..._trim2_corr_v4b (依頼 58) / corr3: ..._trim2_corr_v5
     TAR="$DATA_ROOT/datasets/$DATA_NAME/${DATA_NAME}_raw_episodes.tar"
-    CONFIG=$TRAIN_CONFIG                                    # corr: pi05_mecharm_trim_corr_lora / corr2: pi05_mecharm_trim_corr_v4_lora
-    HF_REPO_IN=$TRAIN_REPO_IN                               # 続きの元 (corr: 3999 / corr2: corr の 1499. lab_start.sh と同じ)
+    CONFIG=$TRAIN_CONFIG                                    # corr: pi05_mecharm_trim_corr_lora / corr2: ..._v4_lora / corr3: ..._v5_lora
+    HF_REPO_IN=$TRAIN_REPO_IN                               # 続きの元 (corr: 3999 / corr2: corr の 1499 / corr3: corr2 の 1499. lab_start.sh と同じ)
     HF_REPO_OUT=$TRAIN_REPO_OUT                             # 上げる先 (非公開). 名前は checkpoints/ のフォルダ名に合わせる
     CKPT_NAME=$TRAIN_CKPT_NAME                              # KIOXIA の checkpoints/<これ>/<ステップ>/
     CKPT_IN=$TRAIN_CKPT_IN                                  # 3c で続きの元を置く場所
     CKPT_IN_KIOXIA="$CKPT_ROOT/$TRAIN_FROM_KIOXIA"          # HF_REPO_IN と同じもの (由来の指紋を取る. 依頼 54)
-    PREP_VARIANT=$TRAIN_VARIANT                             # pod_prep_trim_corr.sh の 3 つ目 (v3 / v4)
+    PREP_VARIANT=$TRAIN_VARIANT                             # pod_prep_trim_corr.sh の 3 つ目 (v3 / v4 / v5)
 }
 use_model "$MODEL"
 POD_EXP_DIR=/workspace/openpi/checkpoints/$CONFIG/$EXP
@@ -154,7 +157,7 @@ hm() { local x=$1; [ "$x" -lt 0 ] && x=0; printf '%d:%02d' $((x / 3600)) $((x %
 PROV_PROCESS=""; PROV_ERROR=""; PROV_CKPT_DIR=""; PROV_HF=0; PROV_DONE=0
 OPENPI_JSON='[]'            # 3f のあとに Pod の openpi のコミットと変更の有無を入れる
 iso_of() { [ -n "${1:-}" ] && [ "$1" != 0 ] && date -r "$1" '+%Y-%m-%dT%H:%M:%S%z' | sed -E 's/([0-9]{2})$/:\1/'; }
-prov_inputs() {             # 入力 (学習データ・続きの元 (corr は 3999・corr2 は corr の 1499)・設定を足すスクリプト) を spec の inputs の形で出す
+prov_inputs() {             # 入力 (学習データ・続きの元 (corr は 3999・corr2 は corr の 1499・corr3 は corr2 の 1499)・設定を足すスクリプト) を spec の inputs の形で出す
     local init_ck=""
     case "$INIT" in "$CKPT_IN"/*) init_ck="$CKPT_IN_KIOXIA" ;; esac
     jq -n --arg data "$DATA_NAME" --arg data_dir "$DATA_ROOT/datasets/$DATA_NAME" \
diff --git a/lab_train_fakes.sh b/lab_train_fakes.sh
index 2f787e7..87a5d9f 100644
--- a/lab_train_fakes.sh
+++ b/lab_train_fakes.sh
@@ -36,6 +36,7 @@ echo "[DRY   ] ドライラン: Pod は借りません. 書くのは $LAB_DRY_RO
 if [ -d /Volumes/KIOXIA/r2r2r-data ]; then
     for f in "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3/mecharm_redblock_box_teach_v1_trim2_corr_v3_raw_episodes.tar" \
              "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b/mecharm_redblock_box_teach_v1_trim2_corr_v4b_raw_episodes.tar" \
+             "/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5/mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar" \
              /Volumes/KIOXIA/r2r2r-data/lab/Log11/pod/add_trim_config.py "$RUNPOD_KEY_FILE" "$HF_TOKEN_FILE"; do
         echo "[DRY   ] 本物の $(basename "$f"): $([ -s "$f" ] && echo ある || echo ない)"
     done
@@ -46,13 +47,14 @@ LAB_ROOT="$DATA_ROOT/lab"; LAB_CURRENT="$LAB_ROOT/.lab_current"
 RUNPOD_KEY_FILE="$LAB_DRY_ROOT/keys/runpod_api_key.txt"; HF_TOKEN_FILE="$LAB_DRY_ROOT/keys/hf_token.txt"
 POD_SSH_KEY="$LAB_DRY_ROOT/keys/id_ed25519"
 mkdir -p "$LAB_DRY_ROOT/keys" "$LAB_DRY_ROOT/fake" "$DATA_ROOT/lab/Log11/pod" \
-         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3" "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b"
+         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v3" "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v4b" \
+         "$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_v5"
 [ -s "$RUNPOD_KEY_FILE" ] || echo "DRY-RUNPOD-SECRET-KEY" > "$RUNPOD_KEY_FILE"
 [ -s "$HF_TOKEN_FILE" ] || echo "hf_DRYSECRETTOKEN" > "$HF_TOKEN_FILE"
 [ -s "$POD_SSH_KEY" ] || echo "dry-ssh-key" > "$POD_SSH_KEY"
 [ -s "$POD_SSH_KEY.pub" ] || echo "ssh-ed25519 AAAA dry" > "$POD_SSH_KEY.pub"
 [ -f "$DATA_ROOT/lab/Log11/pod/add_trim_config.py" ] || echo "# dry" > "$DATA_ROOT/lab/Log11/pod/add_trim_config.py"
-for v in v3 v4b; do         # --model corr は corr_v3, corr2 は corr_v4b (依頼 57 / 58)
+for v in v3 v4b v5; do      # --model corr は corr_v3, corr2 は corr_v4b (依頼 57 / 58), corr3 は corr_v5 (依頼 97)
     TARF="$DATA_ROOT/datasets/mecharm_redblock_box_teach_v1_trim2_corr_$v/mecharm_redblock_box_teach_v1_trim2_corr_${v}_raw_episodes.tar"
     [ -f "$TARF" ] || echo "dry tar $v" > "$TARF"
 done
diff --git a/pi05/openpi_addon/add_trim_corr_config.py b/pi05/openpi_addon/add_trim_corr_config.py
index d928407..2dd76a0 100644
--- a/pi05/openpi_addon/add_trim_corr_config.py
+++ b/pi05/openpi_addon/add_trim_corr_config.py
@@ -29,6 +29,10 @@ pi05_mecharm_trim_corr_v4_lora を足す (名前が違うので v3 の設定と
   python3 add_trim_corr_config.py /workspace/openpi --variant v4 --steps 1500 --init /workspace/ckpt/corr_v1_1499/params
 依頼 58: v4 のデータを ..._trim2_corr_v4b (corr_v4 から 10-31 の 182545 を外した 7 本) に替えた. 設定の名前は
 pi05_mecharm_trim_corr_v4_lora のまま. 8 本の ..._trim2_corr_v4 の設定が入っている config.py には足さずに止まる
+依頼 97: --variant v5 で, corr_v5 (corr_v4b の 7 本 + 11-06 のゲームパッドで直した 4 本) と合わせた ..._trim2_corr_v5 の設定
+pi05_mecharm_trim_corr_v5_lora を足す (v3・v4 と並べて入る). 学習率の予定・ほかの値は v3 と同じ.
+続きの元 (--init) は corr_v2_1499 (/workspace/ckpt/corr_v2_1499/params)
+  python3 add_trim_corr_config.py /workspace/openpi --variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params
 """
 import argparse
 import re
@@ -41,6 +45,7 @@ VARIANTS = {
            "old_tags": ("teach_v1_trim2_corr_v1", "teach_v1_trim2_corr_v2")},   # 依頼 45 / 46 で入れた版
     "v4": {"name": "pi05_mecharm_trim_corr_v4_lora", "data_tag": "teach_v1_trim2_corr_v4b",
            "old_tags": ("teach_v1_trim2_corr_v4",)},   # 依頼 57 / 58 (8 本の corr_v4 → 182545 を外した 7 本の corr_v4b)
+    "v5": {"name": "pi05_mecharm_trim_corr_v5_lora", "data_tag": "teach_v1_trim2_corr_v5", "old_tags": ()},  # 依頼 97
 }
 
 
diff --git a/pi05/openpi_addon/pod_prep_trim_corr.sh b/pi05/openpi_addon/pod_prep_trim_corr.sh
index 7296957..575f190 100644
--- a/pi05/openpi_addon/pod_prep_trim_corr.sh
+++ b/pi05/openpi_addon/pod_prep_trim_corr.sh
@@ -6,13 +6,17 @@
 #         /workspace/data/mecharm_redblock_box_teach_v1_trim2_corr_v3_raw_episodes.tar
 #         /workspace/r2r2r/pi05/  (この依頼の版. add_trim_corr_config.py と convert_to_lerobot.py を使う)
 #
-#   bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh <steps> <init> [v3|v4]
+#   bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh <steps> <init> [v3|v4|v5]
 #   例: bash ... 1500 /workspace/ckpt/3999/params
 #   依頼 57: 3 つ目に v4 を付けると trim2 + corr_v4 (..._trim2_corr_v4・設定 pi05_mecharm_trim_corr_v4_lora) の準備.
 #     例: bash ... 1500 /workspace/ckpt/corr_v1_1499/params v4
 #     続きの元は corr_v1_1499 (lab_train.sh --model corr2 の 3c が取る). 3999 は取らないので, 正規化の値は
 #     1499 の assets にある値 (3999 の trim2 の値と sha256 が同じ. 依頼 52 で確かめた) を, 同じ sha256 で確かめて置く
 #   依頼 58: v4 のデータは ..._trim2_corr_v4b (8 本の corr_v4 から 182545 を外した 7 本. 合わせて 26 本). 設定の名前は同じ
+#   依頼 97: v5 は trim2 + corr_v5 (..._trim2_corr_v5・設定 pi05_mecharm_trim_corr_v5_lora. 合わせて 30 本).
+#     続きの元は corr_v2_1499 (lab_train.sh --model corr3 の 3c が取る). 正規化の値は 1499 の assets にある値
+#     (trim2 の値と sha256 が同じ. 依頼 97 で KIOXIA の写しを確かめた) を, 同じ sha256 で確かめて置く
+#     例: bash ... 1500 /workspace/ckpt/corr_v2_1499/params v5
 #   v3 (既定) は今までと同じ
 #
 # すること (何度流しても同じ結果):
@@ -41,13 +45,17 @@ case "$VARIANT" in
         CONFIG=pi05_mecharm_trim_corr_v4_lora
         NORM_SRC=/workspace/ckpt/corr_v1_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v3/norm_stats.json
         EXP=trim_corr_v4; N_MAC=26 ;;
-    *) echo "[ERROR] 3 つ目は v3 か v4: $VARIANT" >&2; exit 2 ;;
+    v5) NAME=mecharm_redblock_box_teach_v1_trim2_corr_v5
+        CONFIG=pi05_mecharm_trim_corr_v5_lora
+        NORM_SRC=/workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json
+        EXP=trim_corr_v5; N_MAC=30 ;;
+    *) echo "[ERROR] 3 つ目は v3 か v4 か v5: $VARIANT" >&2; exit 2 ;;
 esac
 TAR=/workspace/data/${NAME}_raw_episodes.tar
 DATA=/workspace/data/$NAME
 OPENPI=/workspace/openpi
 ADDON=/workspace/r2r2r/pi05/openpi_addon
-NORM_SHA256=80346837eb12ffa6e683416f0fa41a196519fa8459d1f0b666b06cd621c7f61b   # Mac の写しの sha256 (v3 も v4 も同じ trim2 の値)
+NORM_SHA256=80346837eb12ffa6e683416f0fa41a196519fa8459d1f0b666b06cd621c7f61b   # Mac の写しの sha256 (v3・v4・v5 とも同じ trim2 の値)
 NORM_DST=assets/$CONFIG/local/$NAME/norm_stats.json                           # $OPENPI からの相対
 
 [ -f "$TAR" ] || { echo "[ERROR] tar がない: $TAR" >&2; exit 1; }
diff --git a/pi05/test_merge_and_config.py b/pi05/test_merge_and_config.py
index 4239e75..eb4d17d 100644
--- a/pi05/test_merge_and_config.py
+++ b/pi05/test_merge_and_config.py
@@ -15,6 +15,8 @@
     pod_prep_trim_corr.sh の v4 / KIOXIA の corr_v4 が決めた 8 本で, 前の 5 本は corr_v3 と同じ
   - 依頼 58: v4 のデータは ..._trim2_corr_v4b (corr_v4 から 182545 を外した 7 本) / 8 本の corr_v4 の設定が入っていたら止まる /
     KIOXIA の corr_v4b は決めた 7 本で, 中身は corr_v4 と同じ. 8 本の corr_v4 は残っている
+  - 依頼 97: --variant v5 (pi05_mecharm_trim_corr_v5_lora・..._trim2_corr_v5・続きの元 corr_v2_1499) が v3・v4 と並べて入る /
+    pod_prep_trim_corr.sh の v5 / KIOXIA の corr_v5 は corr_v4b の 7 本 (中身も同じ) と 11-06 の 4 本
 """
 import ast
 import json
@@ -297,6 +299,40 @@ class TestConfigV4(unittest.TestCase):
             self.assertEqual(cfg.read_text(), before)
 
 
+class TestConfigV5(unittest.TestCase):
+    """依頼 97: --variant v5 は別の名前の設定として v3・v4 と並べて入り, データと続きの元だけが違う."""
+    INIT5 = "/workspace/ckpt/corr_v2_1499/params"
+    INIT4 = "/workspace/ckpt/corr_v1_1499/params"
+
+    def test_v5_with_v3_v4(self):
+        for order in (("v5",), ("v3", "v4", "v5"), ("v5", "v4")):
+            with tempfile.TemporaryDirectory() as td, mock.patch("sys.stdout"):
+                cfg = fake_config(Path(td))
+                inits = {"v3": "/workspace/ckpt/3999/params", "v4": self.INIT4, "v5": self.INIT5}
+                for v in order:
+                    self.assertEqual(atc.main([td, "--variant", v, "--steps", "1500", "--init", inits[v]]), 0)
+                c = configs(cfg)
+                self.assertIn("pi05_mecharm_trim_corr_v5_lora", c)
+                v5 = c["pi05_mecharm_trim_corr_v5_lora"]
+                self.assertIn("'local/mecharm_redblock_box_teach_v1_trim2_corr_v5'", v5["data"])
+                self.assertIn(self.INIT5, v5["weight_loader"])
+                self.assertEqual(v5["num_train_steps"], "1500")
+                lr = {k.arg: ast.literal_eval(k.value) for k in ast.parse(v5["lr_schedule"]).body[0].value.keywords}
+                self.assertEqual(lr, {"warmup_steps": 100, "peak_lr": 2.5e-5, "decay_steps": 1500, "decay_lr": 5e-6})
+                if "v4" in order:      # v4 は今までのまま (corr_v4b・corr_v1_1499). データ・続きの元のほかは同じ値
+                    v4 = c["pi05_mecharm_trim_corr_v4_lora"]
+                    self.assertIn("'local/mecharm_redblock_box_teach_v1_trim2_corr_v4b'", v4["data"])
+                    self.assertIn(self.INIT4, v4["weight_loader"])
+                    for k in set(v4) - {"name", "data", "weight_loader"}:
+                        self.assertEqual(v5[k], v4[k], k)
+                text = cfg.read_text()
+                self.assertEqual(atc.main([td, "--variant", "v5", "--steps", "1500", "--init", self.INIT5]), 0)
+                self.assertEqual(cfg.read_text(), text)                    # 二重に入れない
+                with self.assertRaises(SystemExit):                        # 違う続きの元では止まる
+                    atc.main([td, "--variant", "v5", "--steps", "1500", "--init", self.INIT4])
+                self.assertEqual(cfg.read_text(), text)
+
+
 class TestPodPrep(unittest.TestCase):
     """依頼 48: pod_prep_trim_corr.sh は正規化の値を計算し直さず, 3999 の学習の trim2 の値を置く."""
     SH = _HERE / "openpi_addon" / "pod_prep_trim_corr.sh"
@@ -344,6 +380,21 @@ class TestPodPrep(unittest.TestCase):
             m = re.search(r"^NORM_SHA256=([0-9a-f]{64})", self.SH.read_text(), re.M)
             self.assertEqual(hashlib.sha256(mac.read_bytes()).hexdigest(), m.group(1))
 
+    def test_v5_uses_corr2_1499_norm(self):
+        """依頼 97: v5 は corr_v5 の設定で, 正規化の値は続きの元 (corr2 の 1499) の assets から同じ sha256 で確かめて置く."""
+        code = "\n".join(self.lines())
+        for x in ("NAME=mecharm_redblock_box_teach_v1_trim2_corr_v5\n", "N_MAC=30 ;;", "CONFIG=pi05_mecharm_trim_corr_v5_lora",
+                  "NORM_SRC=/workspace/ckpt/corr_v2_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json",
+                  "EXP=trim_corr_v5;"):
+            self.assertIn(x, code)
+        mac = Path("/Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499/assets/local/"
+                   "mecharm_redblock_box_teach_v1_trim2_corr_v4b/norm_stats.json")
+        if mac.exists():
+            import hashlib
+            import re
+            m = re.search(r"^NORM_SHA256=([0-9a-f]{64})", self.SH.read_text(), re.M)
+            self.assertEqual(hashlib.sha256(mac.read_bytes()).hexdigest(), m.group(1))
+
     def test_unknown_variant_stops_before_anything(self):
         r = subprocess.run(["bash", str(self.SH), "1500", "base", "v9"], capture_output=True, text=True)
         self.assertEqual(r.returncode, 2)
@@ -413,5 +464,36 @@ class TestCorrV4bData(unittest.TestCase):
         self.assertEqual(sorted(Path(n).parent.name for n in names if n.endswith("/episode.npz")), eps)
 
 
+@unittest.skipUnless((DS / "mecharm_redblock_box_corr_v5").is_dir(), "KIOXIA の corr_v5 がない")
+class TestCorrV5Data(unittest.TestCase):
+    """依頼 97: corr_v5 は corr_v4b の 7 本 (中身も同じ) と, 11-06 のゲームパッドで直した成功のうち外れのない 4 本.
+    corr_v4b と合わせたもの (v4b) は残す."""
+    OLD = TestCorrV4bData.RUNS
+    NEW = ["pi05_20261106_183926", "pi05_20261106_184452", "pi05_20261106_184650", "pi05_20261106_184906"]
+
+    def test_runs_and_old_same(self):
+        raw = DS / "mecharm_redblock_box_corr_v5" / "raw_episodes"
+        self.assertEqual(sorted(p.parent.name for p in raw.glob("[!.]*/episode.npz")), sorted(self.OLD + self.NEW))
+        for r in self.OLD:
+            a = (DS / "mecharm_redblock_box_corr_v4b" / "raw_episodes" / r / "episode.npz").read_bytes()
+            self.assertEqual(a, (raw / r / "episode.npz").read_bytes(), r)
+        for r in self.NEW:
+            m = json.loads((raw / r / "meta.json").read_text())
+            self.assertEqual(m["gap_fill"]["mode"], "speed20")
+            self.assertEqual(m["trim_hold"], {"keep_model": 0, "keep_correct": 1, "keep_grip": 2})
+            self.assertGreater(m["correct_steps"], 0)
+        self.assertNotIn("pi05_20261106_185554", [p.parent.name for p in raw.glob("*/episode.npz")])
+        self.assertEqual(len(list((DS / "mecharm_redblock_box_corr_v4b" / "raw_episodes").glob("[!.]*/episode.npz"))), 7)
+
+    def test_merged_and_tar(self):
+        ds = DS / "mecharm_redblock_box_teach_v1_trim2_corr_v5"
+        eps = sorted(p.parent.name for p in (ds / "raw_episodes").glob("[!.]*/episode.npz"))
+        self.assertEqual(len(eps), 30)
+        tar = ds / "mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar"
+        names = subprocess.run(["tar", "-tf", str(tar)], capture_output=True, text=True, check=True).stdout.split()
+        self.assertFalse([n for n in names if Path(n).name.startswith("._")])
+        self.assertEqual(sorted(Path(n).parent.name for n in names if n.endswith("/episode.npz")), eps)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/test_lab_start.sh b/test_lab_start.sh
index be6f152..c481953 100755
--- a/test_lab_start.sh
+++ b/test_lab_start.sh
@@ -398,13 +398,27 @@ check "corr2 で動いている Pod に --model corr2 → 3b〜4 を飛ばして
 rm -rf "$T"
 check "フォルダからモデルの名前 (corr2)" '[ "$(. "$HERE/lab_lib.sh"; lab_model_of_dir /workspace/ckpt/corr_v2_1499)" = corr2 ]'
 
+echo "== 34c. --model corr3 (依頼 97) で新しく作る → corr_v5 で学習したモデルを取り, v5 の設定・assets を足して起動"
+setup; run --model corr3
+check "終了コード 0" '[ $RC = 0 ]'
+check "corr3 のモデルを /workspace/ckpt/corr_v3_1499 に取る (ほかのモデルは取らない)" 'has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 --local-dir /workspace/ckpt/corr_v3_1499" && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 " && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 "'
+check "3f で学習と同じ v5 の設定 (続きの元 corr_v2_1499) を足し, モデルの assets を写す" 'has ssh.log "add_trim_corr_config.py /workspace/openpi --variant v5 --steps 1500 --init /workspace/ckpt/corr_v2_1499/params" && has ssh.log "mkdir -p assets/pi05_mecharm_trim_corr_v5_lora/local && cp -r /workspace/ckpt/corr_v3_1499/assets/local/mecharm_redblock_box_teach_v1_trim2_corr_v5 assets/pi05_mecharm_trim_corr_v5_lora/local/"'
+check "corr3 の設定とフォルダでサーバーを起動" 'has ssh.log "--policy.config=pi05_mecharm_trim_corr_v5_lora --policy.dir=/workspace/ckpt/corr_v3_1499 2>&1"'
+check "クライアントに --model corr3 を渡す・安全範囲と位置合わせは trim2 のまま" 'has py.log "--model corr3 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v3)" && has py.log "datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes "'
+check "記録に model=corr3" '[ "$(jq -r .model "$T/data/lab/LogT/pod/lab_pod.json")" = corr3 ]'
+rm -rf "$T"
+setup; state $ID_OLD; pod 200 "$RUNNING"; probe 1 1 1 1 1; serve_dir /workspace/ckpt/corr_v2_1499; run --model corr3
+check "corr2 のサーバーが動いている Pod に --model corr3 → 止まり, --model corr2 を案内" '[ $RC = 1 ] && has out.txt "別のモデル (--model corr2)で動いています" && ! has ssh.log "tmux new" && ! has ssh.log "pkill"'
+rm -rf "$T"
+check "フォルダからモデルの名前 (corr3)" '[ "$(. "$HERE/lab_lib.sh"; lab_model_of_dir /workspace/ckpt/corr_v3_1499)" = corr3 ]'
+
 echo "== 35. --model に知らない名前 → 何もせずに止まる"
 setup; run --model foo
-check "終了コード 2・使える名前を出す・API を呼ばない" '[ $RC = 2 ] && has out.txt "--model は trim か corr か corr2" && [ ! -s "$T/calls.log" ]'
+check "終了コード 2・使える名前を出す・API を呼ばない" '[ $RC = 2 ] && has out.txt "--model は trim か corr か corr2 か corr3" && [ ! -s "$T/calls.log" ]'
 rm -rf "$T"
 setup
 (cd "$T" && PATH="$T/bin:$PATH" perl -e 'alarm 10; exec @ARGV' ./lab_start.sh --log LogT --dry-run --model < /dev/null > "$T/out.txt" 2>&1); RC=$?
-check "--model だけで値がない → 回り続けずに終了コード 2 (codex R1)" '[ $RC = 2 ] && has out.txt "--model には trim か corr か corr2 を付ける"'
+check "--model だけで値がない → 回り続けずに終了コード 2 (codex R1)" '[ $RC = 2 ] && has out.txt "--model には trim か corr か corr2 か corr3 を付ける"'
 rm -rf "$T"
 for a in "--gpu" "--episode" "--log" "--gpu --dry-run" "--episode --log LogT" "--log --dry-run"; do
     setup
diff --git a/test_lab_train.sh b/test_lab_train.sh
index fafe2cf..4bf9c55 100755
--- a/test_lab_train.sh
+++ b/test_lab_train.sh
@@ -213,6 +213,28 @@ TD="$T/data/lab/LogT/train"
 check "--stop (--model なし): 記録の model=corr2 の設定・exp でログと一覧を取る" '[ $(n_call DELETE) = 1 ] && [ -f "$TD/train_trim_corr_v4.log" ] && has ssh.log "cd /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v4_lora/trim_corr_v4" && [ "$(jq -r .config.data_name "$TD/train_provenance.json")" = mecharm_redblock_box_teach_v1_trim2_corr_v4b ]'
 rm -rf "$T"
 
+echo "== 12c. --model corr3 (依頼 97): corr2 の 1499 から trim2_corr_v5 で続きを学習する"
+run success --model corr3
+CK3="$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v3"
+check "終了コード 0・画面に --model corr3 と続きの元 → 上げる先" '[ $RC = 0 ] && has out.txt "--model corr3 (PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 → PixelstoMotors/pi05-mecharm-trim-corr-lora-v3)"'
+check "3c は corr2 の 1499 を /workspace/ckpt/corr_v2_1499 に取る (3999・corr の 1499 は取らない)" 'has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 --local-dir /workspace/ckpt/corr_v2_1499" && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 " && ! has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-corr-lora-v1 "'
+check "3f は 3999 の assets を写さない" 'has ssh.log "add_trim_config.py /workspace/openpi" && ! has ssh.log "cp -r /workspace/ckpt/3999/"'
+check "corr_v5 の tar を送り, pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5" 'has scp.log "mecharm_redblock_box_teach_v1_trim2_corr_v5_raw_episodes.tar root@" && ! has scp.log "corr_v4b_raw_episodes.tar" && ! has scp.log "corr_v3_raw_episodes.tar" && has ssh.log "pod_prep_trim_corr.sh 1500 /workspace/ckpt/corr_v2_1499/params v5"'
+check "学習は pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5" 'has ssh.log "uv run scripts/train.py pi05_mecharm_trim_corr_v5_lora --exp-name=trim_corr_v5 --overwrite > /workspace/logs/train_trim_corr_v5.log"'
+check "Hugging Face の pi05-mecharm-trim-corr-lora-v3 へ上げ, KIOXIA の checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 に置く" 'has ssh.log "hf_upload_ckpt.py --dir /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5/1499 --repo PixelstoMotors/pi05-mecharm-trim-corr-lora-v3" && [ -f "$CK3/1499/_CHECKPOINT_METADATA" ] && [ ! -e "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499/assets" ]'
+check "記録 lab_pod.json に model・設定・exp・init" '[ "$(jq -r "[.model,.config,.exp,.init] | join(\" \")" "$TD/lab_pod.json")" = "corr3 pi05_mecharm_trim_corr_v5_lora trim_corr_v5 /workspace/ckpt/corr_v2_1499/params" ]'
+P3="$TD/train_provenance.json"
+check "由来: 入力は corr_v5 と corr2 の 1499 (KIOXIA の原本・指紋・Hugging Face の元)" '[ "$(jq -r ".inputs[0].id" "$P3")" = mecharm_redblock_box_teach_v1_trim2_corr_v5 ] && [ "$(jq -r ".inputs[1] | [.kind,.id,.path,.hf_repo,.pod_path] | join(\" \")" "$P3")" = "checkpoint pi05_mecharm_trim_corr_lora_v2/1499 checkpoints/pi05_mecharm_trim_corr_lora_v2/1499 PixelstoMotors/pi05-mecharm-trim-corr-lora-v2 /workspace/ckpt/corr_v2_1499/params" ] && [ "$(jq -r ".inputs[1].fingerprint" "$P3")" = "$(python3 "$HERE/provenance.py" fingerprint "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v2/1499")" ]'
+check "由来: 設定・出力・チェックポイント側と Hugging Face にも同じもの" '[ "$(jq -r ".config | [.name,.exp_name,.init_params,.data_name] | join(\" \")" "$P3")" = "pi05_mecharm_trim_corr_v5_lora trim_corr_v5 /workspace/ckpt/corr_v2_1499/params mecharm_redblock_box_teach_v1_trim2_corr_v5" ] && [ "$(jq -r ".outputs[0] | [.path,.hf_repo] | join(\" \")" "$P3")" = "checkpoints/pi05_mecharm_trim_corr_lora_v3/1499 PixelstoMotors/pi05-mecharm-trim-corr-lora-v3" ] && cmp -s "$P3" "$CK3/1499/train_provenance.json" && has calls.log "hf upload PixelstoMotors/pi05-mecharm-trim-corr-lora-v3 train_provenance.json"'
+rm -rf "$T"
+T=$(mktemp -d); mkdir -p "$T/data/lab/LogT/train" "$T/fake"
+jq -n '{id:"dryrunpod0001", kind:"train", model:"corr3", config:"pi05_mecharm_trim_corr_v5_lora", gpu:"NVIDIA A40", costPerHr:0.44, created_epoch:1793449178, created_at:"2026-10-31 21:19:38", exp:"trim_corr_v5", train_started_epoch:1793449300}' > "$T/data/lab/LogT/train/lab_pod.json"
+echo "NVIDIA A40" > "$T/fake/created"
+(cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run --stop < /dev/null > "$T/out.txt" 2>&1); RC=$?
+TD="$T/data/lab/LogT/train"
+check "--stop (--model なし): 記録の model=corr3 の設定・exp でログと一覧を取る" '[ $(n_call DELETE) = 1 ] && [ -f "$TD/train_trim_corr_v5.log" ] && has ssh.log "cd /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_v5_lora/trim_corr_v5" && [ "$(jq -r .config.data_name "$TD/train_provenance.json")" = mecharm_redblock_box_teach_v1_trim2_corr_v5 ]'
+rm -rf "$T"
+
 echo "== 13. 見守る途中で止められても Pod を残さない (TERM. Ctrl-C・HUP も同じ扱い)"
 T=$(mktemp -d)
 (cd "$HERE" && LAB_DRY_ROOT="$T" LAB_FAKE_SCENARIO=timeout LAB_DRY_POLL_SEC=1 ./lab_train.sh --log LogT --dry-run --limit-hours 1 < /dev/null > "$T/out.txt" 2>&1) &
