This repository has no description
1#!/usr/bin/env bash
2#
3# Drives one closed-loop experiment run for the model comparison harness:
4#
5# 1. Snapshots existing experiment logs on the phone (so we know which file
6# is the "new" one this run produces).
7# 2. Prompts you to confirm Experiment Mode is armed and Start has been
8# tapped on the phone (skipped with --no-confirm).
9# 3. Records t0_wall_ms in milliseconds, then plays the test video from the
10# start in QuickTime Player via osascript.
11# 4. Sleeps for --duration seconds.
12# 5. Pauses QuickTime.
13# 6. Prompts you to tap Stop on the phone (skipped with --no-confirm).
14# 7. Diffs the post-run file list against the snapshot, pulls only the
15# newly-written JSON file(s) into experiments/<run-id>/.
16# 8. Writes experiments/<run-id>/manifest.json with t0_wall_ms, the video
17# path, model tag, device info, and duration.
18#
19# Usage:
20# tools/run_experiment.sh \
21# --video /abs/or/rel/path/test.mp4 \
22# --duration 10 \
23# --model-tag yolo11n_su_416 \
24# [--device RFCX10EM9LR] \
25# [--no-confirm]
26#
27# After two runs (model A then model B against the same video) point
28# tools/compare_logs.py at experiments/ to produce the report.
29
30set -euo pipefail
31
32ADB="${ADB:-/Users/virtualintern/Library/Android/sdk/platform-tools/adb}"
33# DEVICE may be passed via --device or EXP_DEVICE. Empty means "auto-detect:
34# if exactly one device is connected, use it; otherwise error and require
35# explicit --device".
36DEVICE="${EXP_DEVICE:-}"
37PKG="com.nate.posedetection.androidApp"
38REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
39EXPERIMENTS_DIR="$REPO_ROOT/experiments"
40REMOTE_LOGS_DIR="/sdcard/Android/data/$PKG/files/experiment_logs"
41
42usage() {
43 sed -n '3,28p' "$0"
44 exit "${1:-1}"
45}
46
47VIDEO=""
48DURATION=""
49MODEL_TAG=""
50NO_CONFIRM=0
51
52while [ $# -gt 0 ]; do
53 case "$1" in
54 --video) VIDEO="$2"; shift 2 ;;
55 --duration) DURATION="$2"; shift 2 ;;
56 --model-tag) MODEL_TAG="$2"; shift 2 ;;
57 --device) DEVICE="$2"; shift 2 ;;
58 --no-confirm) NO_CONFIRM=1; shift ;;
59 -h|--help) usage 0 ;;
60 *) echo "unknown arg: $1" >&2; usage ;;
61 esac
62done
63
64[ -n "$VIDEO" ] || { echo "error: --video is required" >&2; exit 2; }
65[ -n "$DURATION" ] || { echo "error: --duration is required" >&2; exit 2; }
66[ -n "$MODEL_TAG" ] || { echo "error: --model-tag is required" >&2; exit 2; }
67
68if [ ! -f "$VIDEO" ]; then
69 echo "error: video file not found: $VIDEO" >&2
70 exit 2
71fi
72VIDEO_ABS="$(cd "$(dirname "$VIDEO")" && pwd)/$(basename "$VIDEO")"
73
74# Sanitize model tag for filesystem use (run-id slug).
75SAFE_TAG="$(printf '%s' "$MODEL_TAG" | tr -c 'A-Za-z0-9._-' '_' | sed 's/^_*//; s/_*$//')"
76[ -n "$SAFE_TAG" ] || SAFE_TAG="unnamed"
77
78# Resolve / verify device.
79if [ -z "$DEVICE" ]; then
80 # Auto-detect: only succeed if exactly one device is attached.
81 CONNECTED="$("$ADB" devices | awk 'NR>1 && $2=="device" {print $1}')"
82 NUM_CONNECTED="$(printf '%s\n' "$CONNECTED" | grep -c . || true)"
83 if [ "$NUM_CONNECTED" -eq 0 ]; then
84 echo "error: no devices connected. Plug in the phone and run 'adb devices'." >&2
85 exit 3
86 elif [ "$NUM_CONNECTED" -gt 1 ]; then
87 echo "error: multiple devices connected — pass --device <serial> to disambiguate." >&2
88 echo " connected:" >&2
89 printf ' %s\n' $CONNECTED >&2
90 exit 3
91 fi
92 DEVICE="$CONNECTED"
93 echo "==> auto-selected device: $DEVICE"
94fi
95if ! "$ADB" -s "$DEVICE" get-state >/dev/null 2>&1; then
96 echo "error: device $DEVICE is not connected." >&2
97 echo " adb devices output:" >&2
98 "$ADB" devices >&2
99 exit 3
100fi
101
102now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
103
104confirm() {
105 if [ "$NO_CONFIRM" -eq 1 ]; then return 0; fi
106 printf '%s [y/N] ' "$1"
107 local response
108 read -r response
109 case "$response" in
110 [yY]|[yY][eE][sS]) return 0 ;;
111 *) return 1 ;;
112 esac
113}
114
115list_remote_logs() {
116 "$ADB" -s "$DEVICE" shell "ls -1 $REMOTE_LOGS_DIR 2>/dev/null" \
117 | tr -d '\r' \
118 | grep '\.json$' \
119 || true
120}
121
122# 1. Snapshot existing logs so we can diff after the run.
123echo "==> Snapshotting existing logs on $DEVICE..."
124LOGS_BEFORE="$(list_remote_logs)"
125EXISTING_COUNT=$(printf '%s\n' "$LOGS_BEFORE" | grep -c '\.json$' || true)
126echo " ($EXISTING_COUNT existing log file(s))"
127
128# 2. Confirm the phone side is armed.
129if ! confirm "Phone ready? Model picked, Experiment Mode on, Start tapped?"; then
130 echo "Aborted by user."
131 exit 0
132fi
133
134# 3. Record t0 and play the video.
135T0_WALL_MS="$(now_ms)"
136echo "==> t0_wall_ms = $T0_WALL_MS"
137echo "==> Playing $VIDEO_ABS in QuickTime..."
138osascript \
139 -e 'tell application "QuickTime Player" to activate' \
140 -e "tell application \"QuickTime Player\" to open POSIX file \"$VIDEO_ABS\"" \
141 -e 'tell application "QuickTime Player" to set current time of front document to 0' \
142 -e 'tell application "QuickTime Player" to play front document' >/dev/null
143
144# 4. Wait the requested duration.
145echo "==> Sleeping ${DURATION}s..."
146sleep "$DURATION"
147
148# 5. Pause playback.
149echo "==> Pausing QuickTime..."
150osascript -e 'tell application "QuickTime Player" to pause front document' >/dev/null
151
152STOPPED_WALL_MS="$(now_ms)"
153
154# 6. Prompt the user to tap Stop on the phone.
155if [ "$NO_CONFIRM" -eq 0 ]; then
156 echo
157 echo "==> Tap 'Stop Experiment' on the phone now."
158 printf " Press Enter when done... "
159 read -r _
160fi
161
162# 7. Diff the file list, pull anything new.
163LOGS_AFTER="$(list_remote_logs)"
164NEW_LOGS="$(comm -13 <(printf '%s\n' "$LOGS_BEFORE" | sort -u) <(printf '%s\n' "$LOGS_AFTER" | sort -u) | grep '\.json$' || true)"
165
166RUN_ID="${T0_WALL_MS}_${SAFE_TAG}"
167RUN_DIR="$EXPERIMENTS_DIR/$RUN_ID"
168mkdir -p "$RUN_DIR"
169
170if [ -z "$NEW_LOGS" ]; then
171 echo "warning: no new log files appeared on the phone. Was Experiment Mode" >&2
172 echo " actually started/stopped? Was a model selected? The manifest" >&2
173 echo " will still be written so you can debug, but the run is empty." >&2
174else
175 echo "==> Pulling new log(s) into $RUN_DIR/"
176 while IFS= read -r f; do
177 [ -z "$f" ] && continue
178 "$ADB" -s "$DEVICE" pull "$REMOTE_LOGS_DIR/$f" "$RUN_DIR/" >/dev/null
179 echo " pulled $f"
180 done <<< "$NEW_LOGS"
181fi
182
183# 8. Write the manifest. Use python for JSON to avoid quoting headaches.
184DEVICE_MANUFACTURER="$("$ADB" -s "$DEVICE" shell getprop ro.product.manufacturer 2>/dev/null | tr -d '\r' || true)"
185DEVICE_MODEL_NAME="$("$ADB" -s "$DEVICE" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || true)"
186DEVICE_LABEL="$DEVICE_MANUFACTURER $DEVICE_MODEL_NAME"
187
188NEW_LOGS_JOINED="$(printf '%s\n' "$NEW_LOGS" | tr '\n' ',' | sed 's/,$//')"
189
190RUN_ID="$RUN_ID" \
191MODEL_TAG="$MODEL_TAG" \
192VIDEO_ABS="$VIDEO_ABS" \
193DURATION="$DURATION" \
194T0_WALL_MS="$T0_WALL_MS" \
195STOPPED_WALL_MS="$STOPPED_WALL_MS" \
196DEVICE="$DEVICE" \
197DEVICE_LABEL="$DEVICE_LABEL" \
198NEW_LOGS_JOINED="$NEW_LOGS_JOINED" \
199python3 -c '
200import json, os
201files = [f for f in os.environ["NEW_LOGS_JOINED"].split(",") if f]
202manifest = {
203 "run_id": os.environ["RUN_ID"],
204 "model_tag": os.environ["MODEL_TAG"],
205 "video_path": os.environ["VIDEO_ABS"],
206 "duration_seconds": float(os.environ["DURATION"]),
207 "t0_wall_ms": int(os.environ["T0_WALL_MS"]),
208 "stopped_wall_ms": int(os.environ["STOPPED_WALL_MS"]),
209 "device_serial": os.environ["DEVICE"],
210 "device_label": os.environ["DEVICE_LABEL"].strip(),
211 "log_files": files,
212}
213print(json.dumps(manifest, indent=2))
214' > "$RUN_DIR/manifest.json"
215
216echo "==> Wrote $RUN_DIR/manifest.json"
217echo
218echo "Run dir: $RUN_DIR"
219echo "Next: tools/compare_logs.py experiments/ (after at least 2 runs against the same video)"