# lab_lib.sh — lab_start.sh / lab_stop.sh / lab_train.sh が読む共通の関数 (単体では動かさない) # # 鍵の扱い: # - Runpod の API キーは curl のヘッダーを <(printf ...) で渡す (printf は bash の組み込みなので # ps にもシェルの履歴にも出ない) # - Hugging Face の鍵は ssh の標準入力で Pod の ~/.cache/huggingface/token に書く # - Pod の IP とポートは画面・ログに出さない. Pod に入る ssh の出力は lab_mask で ・ に置き換える LAB_ROOT=/Volumes/KIOXIA/r2r2r-data/lab LAB_CURRENT="$LAB_ROOT/.lab_current" # 今使っている Log 名 (lab_stop.sh が --log なしで読む) RUNPOD_KEY_FILE=/Volumes/KIOXIA/keys/runpod_api_key.txt HF_TOKEN_FILE=/Volumes/KIOXIA/keys/hf_token.txt POD_SSH_KEY="$HOME/.ssh/id_ed25519" RUNPOD_API=https://rest.runpod.io/v1 TUNNEL_PORT=8000 YEN_PER_USD=158 RUNPOD_HTTP="" # runpod_api が最後に受けた HTTP の番号 ($(...) の中で呼ぶと親には残らない) lab_need_key() { # 鍵のファイルがない・空なら, どこに書けばよいかを出して止まる local f="$1" what="$2" if [ ! -s "$f" ]; then echo "[ERROR] $what がありません (または空です)." >&2 echo " 次のファイルに鍵だけを1行で書いてください: $f" >&2 exit 1 fi } runpod_api() { # runpod_api METHOD PATH [JSON] → 本文を出す. 失敗の HTTP は戻り値 1 local method="$1" path="$2" data="${3:-}" out code out=$(mktemp) if [ -n "$data" ]; then code=$(curl -sS -o "$out" -w '%{http_code}' -X "$method" \ -H @<(printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' < "$RUNPOD_KEY_FILE")") \ -H 'Content-Type: application/json' --data "$data" "$RUNPOD_API$path") || code=000 else code=$(curl -sS -o "$out" -w '%{http_code}' -X "$method" \ -H @<(printf 'Authorization: Bearer %s\n' "$(tr -d '\r\n' < "$RUNPOD_KEY_FILE")") \ "$RUNPOD_API$path") || code=000 fi cat "$out" rm -f "$out" RUNPOD_HTTP=$code case "$code" in 2??) return 0 ;; *) return 1 ;; esac } lab_pod_addr() { # POD_ID から POD_IP / SSH_PORT を API で受け取る (画面には出さない) local j j=$(runpod_api GET "/pods/$POD_ID") || return 1 POD_IP=$(jq -r '.publicIp // ""' <<<"$j") SSH_PORT=$(jq -r '.portMappings["22"] // ""' <<<"$j") POD_STATUS=$(jq -r '.desiredStatus // ""' <<<"$j") [ -n "$POD_IP" ] && [ -n "$SSH_PORT" ] } lab_mask() { # Pod の IP とポートを置き換えて流す if [ -n "${POD_IP:-}" ] && [ -n "${SSH_PORT:-}" ]; then sed -l -e "s/${POD_IP//./\\.}//g" \ -e "s/\([^0-9]\)${SSH_PORT}\([^0-9]\)/\1\2/g" \ -e "s/\([^0-9]\)${SSH_PORT}\$/\1/" else cat fi } # known_hosts は使わない (Pod ごとに IP が変わる). LogLevel=ERROR で「Permanently added 」も出さない lab_ssh_opts() { SSH_OPTS=(-i "$POD_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=10 -o ServerAliveInterval=30) } pod_ssh() { # pod_ssh 'コマンド' (標準入力はそのまま Pod へ) { ssh "${SSH_OPTS[@]}" -p "$SSH_PORT" "root@$POD_IP" "$@" 2>&1; } | lab_mask } pod_scp() { # pod_scp 送る元... 送り先 (送り先の「:」の前に root@IP を付ける) { scp "${SSH_OPTS[@]}" -P "$SSH_PORT" "$@" 2>&1; } | lab_mask } lab_tunnel_pids() { # このスクリプトが張った 8000 番のトンネル (ssh -N -L 8000:localhost:8000) pgrep -f "ssh -N -L ${TUNNEL_PORT}:localhost:${TUNNEL_PORT}" || true } # ---- lab_start.sh のつなぎ直し (依頼 44) ---- lab_valid_pods() { # lab_valid_pods JSON → GET /pods の答えが「id を持つものの配列」なら 0 (読めない答えを「ない」にしない) jq -e 'type == "array" and all(.[]; type == "object" and (.id | type) == "string")' <<<"$1" >/dev/null 2>&1 } lab_pod_kind() { # lab_pod_kind ID → LAB_POD_KIND=running|stopped|gone と LAB_POD_STATUS (API の desiredStatus) # running = RUNNING. stopped = EXITED (止まっていてもディスクの料金がかかる). gone = TERMINATED, # または 404 で一覧にもない. それ以外 (つながらない・404 以外の失敗・答えが読めない・id が違う・知らない状態) は # 戻り値 1 (二重に借りないよう, 呼ぶ側で止まる) local id="$1" out j list rc LAB_POD_KIND=""; LAB_POD_STATUS="" out=$(mktemp) runpod_api GET "/pods/$id" > "$out" # $(...) に入れない (RUNPOD_HTTP をこのシェルに残す) j=$(cat "$out"); rm -f "$out" case "$RUNPOD_HTTP" in 2??) ;; 404) # 一覧にもないことを確かめてから「ない」とする list=$(runpod_api GET /pods) || return 1 lab_valid_pods "$list" || return 1 jq -e --arg id "$id" 'any(.[]; .id == $id and .desiredStatus != "TERMINATED")' <<<"$list" >/dev/null 2>&1 rc=$? [ $rc = 1 ] && { LAB_POD_KIND=gone; return 0; } return 1 ;; *) return 1 ;; esac jq -e --arg id "$id" 'type == "object" and .id == $id' <<<"$j" >/dev/null 2>&1 || return 1 LAB_POD_STATUS=$(jq -r '.desiredStatus // ""' <<<"$j") case "$LAB_POD_STATUS" in RUNNING) LAB_POD_KIND=running ;; EXITED) LAB_POD_KIND=stopped ;; TERMINATED) LAB_POD_KIND=gone ;; *) return 1 ;; esac } lab_probe_cmd() { # lab_probe_cmd OPENPI_COMMIT モデルのフォルダ assets の norm_stats.json (フォルダからの相対) → Pod の中で流すコマンド (結果は PROBE の1行) # serve = serve_policy.py のプロセスがある ([s] は, このコマンドを流す bash -c 自身に当たらないため) # listen = 8000 番 (16進 1F40) が LISTEN (0A). ss がないこともあるので /proc/net/tcp を見る # ckpt = 3c の hf download が置く中身がある (依頼 47): _CHECKPOINT_METADATA・params/ の _METADATA と # manifest.ocdbt・ocdbt.process_0/manifest.ocdbt・3f が写す norm_stats.json. 取りかけ # (.cache/huggingface/download の *.incomplete) が残っている・探せないときは 0 # mark = hf download が最後まで済んだ印 (.lab_download_ok. 依頼 44 からの 3c が付ける. それより前に作った Pod にはない) # openpi = /workspace/openpi の HEAD が OPENPI_COMMIT # venv = .venv の python と uv の env がある cat </dev/null && s=1 l=0; cat /proc/net/tcp /proc/net/tcp6 2>/dev/null | awk '\$4 == "0A" && \$2 ~ /:1F40\$/ {f=1} END {exit !f}' && l=1 c=0; [ -f $2/_CHECKPOINT_METADATA ] && [ -f $2/params/_METADATA ] && [ -f $2/params/manifest.ocdbt ] \\ && [ -f $2/params/ocdbt.process_0/manifest.ocdbt ] && [ -f $2/$3 ] \\ && { [ ! -d $2/.cache/huggingface ] || { r=\$(find $2/.cache/huggingface -name '*.incomplete' -print -quit 2>/dev/null) && [ -z "\$r" ]; }; } && c=1 m=0; [ -f $2/.lab_download_ok ] && m=1 o=0; h=\$(git -C /workspace/openpi rev-parse HEAD 2>/dev/null) && case "\$h" in $1*) o=1 ;; esac v=0; [ -x /workspace/openpi/.venv/bin/python ] && [ -f \$HOME/.local/bin/env ] && v=1 echo "PROBE serve=\$s listen=\$l ckpt=\$c mark=\$m openpi=\$o venv=\$v" EOF } lab_pod_probe() { # lab_pod_probe OPENPI_COMMIT モデルのフォルダ norm_stats.json → PROBE_SERVE / PROBE_LISTEN / PROBE_CKPT / PROBE_MARK / PROBE_OPENPI / PROBE_VENV (1 か 0) local out line k out=$(pod_ssh "$(lab_probe_cmd "$1" "$2" "$3")") || return 1 line=$(grep -E '^PROBE serve=[01] listen=[01] ckpt=[01] mark=[01] openpi=[01] venv=[01]$' <<<"$out" | tail -1) [ -n "$line" ] || return 1 for k in serve listen ckpt mark openpi venv; do printf -v "PROBE_$(tr '[:lower:]' '[:upper:]' <<<"$k")" '%s' "$(sed -E "s/.* $k=([01]).*/\1/" <<<"$line")" done } lab_serve_stop_cmd() { # Pod の中で serve_policy.py を止めるコマンド (30 秒まで終わるのを待ち, 残れば -9) # 起動のコマンド (scripts/serve_policy.py をそのまま含む) とは別の ssh で流す. 同じ bash -c に入れると # pkill がその bash -c 自身に当たる (依頼 47 の codex R1). このコマンドには [s] の形でしか書かない cat <<'EOF' tmux kill-session -t serve 2>/dev/null; pkill -f '[s]cripts/serve_policy\.py' for i in $(seq 1 30); do pgrep -f '[s]cripts/serve_policy\.py' >/dev/null || break; sleep 1; done pkill -9 -f '[s]cripts/serve_policy\.py'; sleep 1; ! pgrep -f '[s]cripts/serve_policy\.py' >/dev/null EOF } lab_archive_state() { # lab_archive_state FILE → 消さずに <名前>_<時刻>.json に変えて残す. 新しい名前は LAB_ARCHIVED local f="$1" dest dest="${f%.json}_$(date +%Y%m%d_%H%M%S).json" [ -e "$dest" ] && dest="${f%.json}_$(date +%Y%m%d_%H%M%S)_$$.json" mv "$f" "$dest" && LAB_ARCHIVED="$dest" } # ---- Pod の準備で流すコマンド (lab_start.sh 3b〜3f と lab_train.sh が同じものを流す. 依頼 49 で lab_start.sh から移した) ---- LAB_OPENPI_COMMIT=215abfb LAB_TEMPLATE_ID=runpod-torch-v280 LAB_IMAGE=runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404 lab_cmd_hf_install() { echo "pip install -q -U --break-system-packages huggingface_hub"; } lab_send_hf_token() { # Hugging Face の鍵を ssh の標準入力で渡す (コマンドの引数・履歴に残らない). hf はこのファイルを読む tr -d '\r\n' < "$HF_TOKEN_FILE" | pod_ssh 'umask 077; mkdir -p ~/.cache/huggingface; cat > ~/.cache/huggingface/token' } lab_cmd_ckpt() { # lab_cmd_ckpt リポジトリ 置く場所 → hf download (最後まで済んだら .lab_download_ok を付ける) echo "rm -f $2/.lab_download_ok && hf download $1 --local-dir $2 && touch $2/.lab_download_ok && du -sh $2" } lab_cmd_openpi() { # lab_cmd_openpi コミット echo "cd /workspace && { [ -d openpi/.git ] || git clone -q --recurse-submodules https://github.com/Physical-Intelligence/openpi.git; } && cd openpi && git checkout -q $1 && git log --oneline -1" } lab_cmd_uv_sync() { echo "curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null && source \$HOME/.local/bin/env && cd /workspace/openpi && GIT_LFS_SKIP_SMUDGE=1 uv sync" } lab_cmd_addon() { # 3f. 何度流しても同じ結果 (install_addon.py・add_trim_config.py とも二重に入れない) echo "cd /workspace/openpi && python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi && python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi && mkdir -p assets/pi05_mecharm_trim_lora/local && cp -r /workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2 assets/pi05_mecharm_trim_lora/local/" } lab_cost_line() { # lab_cost_line 秒 1時間のドル → 「借りていた時間 × 料金 = ドル (円)」の1行 awk -v s="$1" -v c="$2" -v y="$YEN_PER_USD" 'BEGIN { h = s / 3600; usd = h * c printf "借りていた時間 %d 分 %d 秒 (%.2f 時間) × 1時間 %.2f ドル = 約 %.2f ドル (約 %d 円. 1ドル=%d円)\n", int(s / 60), s % 60, h, c, usd, int(usd * y + 0.5), y }' } # ---- lab_train.sh (依頼 49) が Pod の中で流すコマンド. テストでこの Mac の bash でも流すので, GNU と BSD の両方で動く書き方にする ---- # pgrep / pkill -f は, そのコマンドを流している bash -c 自身の引数にも当たる (依頼 47 の codex R1). # 裏で回すものは「bash <パス>」「nvidia-smi --query-gpu=timestamp」で始まる形で起動し, ^ を付けた形で探す # (bash -c の引数は「bash -c」で始まるので当たらない) LAB_RUN_PAT='^bash /workspace/r2r2r/train_run\.sh$' LAB_WD_PAT='^bash /workspace/r2r2r/lab_watchdog\.sh$' LAB_GPU_PAT='^nvidia-smi --query-gpu=timestamp' lab_train_start_cmd() { # lab_train_start_cmd 設定 exp 学習のログ GPU のログ 回したログ 終わりの印 → 学習を setsid nohup で始める (STARTED が出れば始まった) cat < /workspace/r2r2r/train_run.sh <<'RUN_EOF' #!/bin/bash # lab_train.sh が置く. train.py を回し, 終わったら終了コードを $6 に書く echo "\$(date '+%F %T') 始める" >> $5 { cd /workspace/openpi && source \$HOME/.local/bin/env \\ && XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 uv run scripts/train.py $1 --exp-name=$2 --overwrite > $3 2>&1; } rc=\$? echo "\$(date '+%F %T') 終わった (終了コード \$rc)" >> $5 echo \$rc > $6 RUN_EOF setsid nohup bash /workspace/r2r2r/train_run.sh >/dev/null 2>&1 < /dev/null & pkill -f '$LAB_GPU_PAT' setsid nohup nvidia-smi --query-gpu=timestamp,name,memory.used,memory.total,utilization.gpu,temperature.gpu,power.draw --format=csv -l 30 > $4 2>&1 < /dev/null & sleep 3 pgrep -f '$LAB_RUN_PAT' >/dev/null && echo STARTED EOF } lab_train_kill_cmd() { # 学習を止める (片付けの前. 書き終わったログを取るため) echo ": lt=kill_train; pkill -f '$LAB_RUN_PAT'; pkill -f '[s]cripts/train\.py'; sleep 3; pkill -9 -f '[s]cripts/train\.py'; true" } lab_train_status_cmd() { # lab_train_status_cmd 学習のログ 終わりの印 チェックポイントのフォルダ ステップ数 → STATUS の1行 # run = 学習を回すスクリプト (train_run.sh) が動いている. train.py が終わったあと終わりの印を書いてから終わるので, # run=0 で exit=- なら「書く前に消えた」(殺された) # exit = train.py の終了コード (まだなら -) # age = 学習のログが最後に書かれてからの秒 (tqdm が 1 ステップごとに書くので, 長く書かれなければ止まっている) # bar = tqdm の「N/ステップ数」の N. logstep / loss = 最後の「Step N: ... loss=...」の行 # ckpt = 数字だけのフォルダのいちばん大きい番号 (orbax の書きかけ *.orbax-checkpoint-tmp-* は数えない) # cm = そのフォルダに _CHECKPOINT_METADATA がある cat </dev/null && r=1 e=-; [ -f \$E ] && e=\$(tr -d ' \n' < \$E) a=-; s=-; ls=-; lo=- if [ -f \$L ]; then mt=\$(stat -c %Y \$L 2>/dev/null || date -r \$L +%s 2>/dev/null) # GNU (Pod) は stat -c, Mac (テスト) は date -r case "\$mt" in ''|*[!0-9]*) a=- ;; *) a=\$(( \$(date +%s) - mt )) ;; esac t=\$(tail -c 400000 \$L | tr '\r' '\n') s=\$(printf '%s\n' "\$t" | grep -oE '[0-9]+/$4 \[' | tail -1 | cut -d/ -f1) ln=\$(printf '%s\n' "\$t" | grep -E '^Step [0-9]+:' | tail -1) [ -n "\$ln" ] && { ls=\$(printf '%s\n' "\$ln" | sed -E 's/^Step ([0-9]+):.*/\1/'); lo=\$(printf '%s\n' "\$ln" | grep -oE '(^|[ ,])loss=[0-9.eE+-]+' | head -1 | sed 's/.*=//'); } fi c=\$(ls \$C 2>/dev/null | grep -E '^[0-9]+\$' | sort -n | tail -1) m=0; [ -n "\$c" ] && [ -f \$C/\$c/_CHECKPOINT_METADATA ] && m=1 echo "STATUS run=\$r exit=\${e:--} age=\${a:--} bar=\${s:--} logstep=\${ls:--} loss=\${lo:--} ckpt=\${c:--} cm=\$m" EOF } lab_train_parse_status() { # lab_train_parse_status 出力 → S_RUN S_EXIT S_AGE S_BAR S_LOGSTEP S_LOSS S_CKPT S_CM. STATUS の行がなければ 1 local line k v line=$(grep -E '^STATUS run=[01] ' <<<"$1" | tail -1) [ -n "$line" ] || return 1 for k in run exit age bar logstep loss ckpt cm; do v=$(sed -nE "s/.* $k=([^ ]*).*/\1/p" <<<" ${line#STATUS }") printf -v "S_$(tr '[:lower:]' '[:upper:]' <<<"$k")" '%s' "${v:--}" done } lab_train_list_cmd() { # lab_train_list_cmd フォルダ → 「Fパスバイト数」を名前順に (train_state/ は数えない) echo ": lt=list_ckpt; set -o pipefail; cd $1 && find . -type f ! -path './train_state/*' -printf '%P\t%s\n' | LC_ALL=C sort | sed 's/^/F\t/'" } lab_list_files() { # lab_list_files フォルダ → 「パスバイト数」を名前順に (Mac 側. du は KIOXIA でずれるので stat の大きさ) # 数えない: .cache/ (hf download の印)・._* (exFAT が付ける)・.DS_Store・.gitattributes・train_state/ python3 -I -c ' import os, sys root = sys.argv[1] out = [] def err(e): raise e # 読めないフォルダを黙って飛ばさない (一覧が足りないまま「合った」としない) for d, dirs, files in os.walk(root, onerror=err): rel = os.path.relpath(d, root) if rel == ".": dirs[:] = [x for x in dirs if x not in (".cache", "train_state")] for f in files: if f.startswith("._") or f in (".DS_Store", ".gitattributes"): continue p = os.path.normpath(os.path.join(rel, f)) out.append((p, os.path.getsize(os.path.join(d, f)))) for p, s in sorted(out, key=lambda x: x[0].encode()): print(f"{p}\t{s}") ' "$1" } lab_watchdog_script() { # Pod 側の上限の見張り (lab_train.sh が /workspace/r2r2r/lab_watchdog.sh に置き, setsid nohup で裏で回す) cat <<'WATCHDOG_EOF' #!/bin/bash # lab_watchdog.sh — 上限の時刻を過ぎたら, この Pod を自分で消す (lab_train.sh が置く. 依頼 49) # Mac が止まって lab_train.sh が Pod を消せなくても, Pod が残り続けないための保険. # 鍵は Mac から送らない. Runpod が Pod に入れる RUNPOD_POD_ID と Pod 用の鍵 RUNPOD_API_KEY を使う # (ssh のシェルには入っていないことがあるので, 1 番のプロセスの環境と /etc/rp_environment からも探す). 鍵は書き出さない F=${LAB_WD_DEADLINE:-/workspace/logs/.lab_deadline} # 上限の時刻 (epoch). lab_train.sh が書き換えて延ばす L=${LAB_WD_LOG:-/workspace/logs/watchdog.log} RUNPOD_REST=${LAB_WD_API:-https://rest.runpod.io/v1} penv() { { tr '\0' '\n' < /proc/1/environ; sed -E 's/^export //; s/"//g' /etc/rp_environment; } 2>/dev/null \ | sed -n "s/^$1=//p" | head -1 } while :; do d=$(cat "$F" 2>/dev/null) case "$d" in ''|*[!0-9]*) ;; *) [ "$(date +%s)" -ge "$d" ] && break ;; esac sleep "${LAB_WD_POLL:-60}" done echo "$(date '+%F %T') 上限の時刻を過ぎた. この Pod を消します" >> "$L" id=${RUNPOD_POD_ID:-$(penv RUNPOD_POD_ID)} key=${RUNPOD_API_KEY:-$(penv RUNPOD_API_KEY)} [ -n "$id" ] || { echo "RUNPOD_POD_ID が見つからない. 消せない" >> "$L"; exit 1; } # 通信が一時だめでも残らないよう, DELETE と runpodctl remove を間を置いてくり返す (runpodctl の出力は鍵を含まないが, 念のため捨てる) for i in $(seq 1 "${LAB_WD_TRIES:-30}"); do code=$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' -X DELETE \ -H @<(printf 'Authorization: Bearer %s\n' "$key") "$RUNPOD_REST/pods/$id" 2>/dev/null) || code=000 echo "$(date '+%F %T') ($i) DELETE → HTTP $code" >> "$L" case "$code" in 2??) exit 0 ;; esac if command -v runpodctl >/dev/null 2>&1 && RUNPOD_API_KEY="$key" timeout "${LAB_WD_CTL_TIMEOUT:-60}" runpodctl remove pod "$id" >/dev/null 2>&1; then echo "$(date '+%F %T') ($i) runpodctl remove pod で消した" >> "$L"; exit 0 fi sleep "${LAB_WD_RETRY:-60}" done # 消せなければ, せめて止める (GPU の料金は止まる. ディスクの料金は残る) if command -v runpodctl >/dev/null 2>&1 && RUNPOD_API_KEY="$key" timeout "${LAB_WD_CTL_TIMEOUT:-60}" runpodctl stop pod "$id" >/dev/null 2>&1; then echo "$(date '+%F %T') 消せず, runpodctl stop pod で止めた (ディスクの料金は残る)" >> "$L"; exit 0 fi echo "$(date '+%F %T') 消せなかった" >> "$L" exit 1 WATCHDOG_EOF } lab_watchdog_start_cmd() { # lab_watchdog_start_cmd 上限の時刻 → 見張りを (なければ) 起動し, 様子を WATCHDOG の1行で出す (鍵の中身は出さない) cat < /workspace/logs/.lab_deadline.tmp && mv /workspace/logs/.lab_deadline.tmp /workspace/logs/.lab_deadline; chmod +x /workspace/r2r2r/lab_watchdog.sh pgrep -f '$LAB_WD_PAT' >/dev/null || setsid nohup bash /workspace/r2r2r/lab_watchdog.sh >/dev/null 2>&1 < /dev/null & sleep 1 pe() { [ -n "\$(printenv \$1)" ] || { tr '\0' '\n' < /proc/1/environ; cat /etc/rp_environment; } 2>/dev/null | grep -qE "^(export )?\$1=.+"; } w=0; pgrep -f '$LAB_WD_PAT' >/dev/null && w=1 p=0; pe RUNPOD_POD_ID && p=1 k=0; pe RUNPOD_API_KEY && k=1 c=0; command -v runpodctl >/dev/null 2>&1 && c=1 echo "WATCHDOG run=\$w pod=\$p key=\$k ctl=\$c" EOF } # ---- lab_train.sh の Mac 側 (--dry-run とテストでは lab_train_fakes.sh が差し替える) ---- lab_have_hf_cli() { command -v hf >/dev/null 2>&1; } lab_hf_download_mac() { # lab_hf_download_mac リポジトリ 置く場所 → Mac の hf で取る. 鍵は HF_TOKEN_PATH でファイルの場所だけを渡す env -u HF_TOKEN HF_TOKEN_PATH="$HF_TOKEN_FILE" HF_HUB_DISABLE_TELEMETRY=1 hf download "$1" --local-dir "$2" > "$2.download.log" 2>&1 local rc=$? tail -3 "$2.download.log"; rm -f "$2.download.log" return $rc } lab_notify() { # lab_notify 題 文 終了コード → Mac の通知 (0 なら Glass, それ以外は Basso の音) local sound=Basso [ "$3" = 0 ] && sound=Glass osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv) sound name (item 3 of argv)' \ -e 'end run' "$1" "$2" "$sound" >/dev/null 2>&1 \ || echo "[WARN ] Mac の通知を出せなかった" } lab_keep_awake() { # このスクリプトが終わるまで Mac をスリープさせない (CAFF_PID) caffeinate -i -s -w $$ >/dev/null 2>&1 & CAFF_PID=$! echo "[MAC ] 終わるまでスリープしないようにした (caffeinate pid $CAFF_PID). ふたは閉じない" if ! pmset -g batt 2>/dev/null | grep -q "'AC Power'"; then echo "[WARN ] 電源につながっていない. バッテリーだとスリープを止めきれないので, 電源につないでください" fi }