This repository has no description
1#!/usr/bin/env bash
2#
3# Drives one fully unattended closed-loop experiment run:
4#
5# 1. Force-stops the sample app and clears Logcat so the cold start is
6# clean and the only ExperimentAuto: log lines are from this run.
7# 2. Picks a wall-clock target start_at_wall_ms = now + APP_BOOT_PAD seconds.
8# 3. Launches AppActivity via `am start` with the auto-mode intent extras
9# (model_name, start_at_wall_ms, duration_ms). The app's Compose
10# auto-driver selects the model, waits for the same wall-clock target,
11# runs the experiment buffer for duration_ms, writes the JSON, and
12# finishes the activity.
13# 4. Sleeps until start_at_wall_ms locally on the Mac, then plays the
14# video in QuickTime at the requested --start-offset (seconds). Both
15# sides hit the same wall-clock instant.
16# 5. Sleeps for the buffer duration, pauses QuickTime.
17# 6. Diff-polls /sdcard/.../experiment_logs for the new JSON file (with a
18# 10s timeout) and pulls it into experiments/<run-id>/.
19# 7. Captures the ExperimentAuto Logcat lines for the manifest, writes
20# experiments/<run-id>/manifest.json with t0_wall_ms, video offset,
21# device label, model tag, and the captured log lines.
22#
23# Usage:
24# tools/run_auto_experiment.sh \
25# --video ~/Downloads/clip.mp4 \
26# --duration 10 \
27# --model-tag yolo11n_su_416 \
28# [--start-offset 10] \
29# [--device <serial>]
30#
31# After at least two runs (model A then model B against the same video,
32# same --start-offset) point tools/compare_logs.py at experiments/ to
33# build the comparison report.
34
35set -euo pipefail
36
37ADB="${ADB:-/Users/virtualintern/Library/Android/sdk/platform-tools/adb}"
38PKG="com.nate.posedetection.androidApp"
39ACTIVITY="$PKG/com.nate.posedetection.AppActivity"
40REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
41EXPERIMENTS_DIR="$REPO_ROOT/experiments"
42REMOTE_LOGS_DIR="/sdcard/Android/data/$PKG/files/experiment_logs"
43APP_BOOT_PAD_SECONDS="${APP_BOOT_PAD_SECONDS:-9}"
44PULL_TIMEOUT_SECONDS="${PULL_TIMEOUT_SECONDS:-15}"
45
46usage() {
47 sed -n '3,33p' "$0"
48 exit "${1:-1}"
49}
50
51VIDEO=""
52DURATION=""
53MODEL_TAG=""
54START_OFFSET=0
55DEVICE=""
56
57while [ $# -gt 0 ]; do
58 case "$1" in
59 --video) VIDEO="$2"; shift 2 ;;
60 --duration) DURATION="$2"; shift 2 ;;
61 --model-tag) MODEL_TAG="$2"; shift 2 ;;
62 --start-offset) START_OFFSET="$2"; shift 2 ;;
63 --device) DEVICE="$2"; shift 2 ;;
64 -h|--help) usage 0 ;;
65 *) echo "unknown arg: $1" >&2; usage ;;
66 esac
67done
68
69[ -n "$VIDEO" ] || { echo "error: --video is required" >&2; exit 2; }
70[ -n "$DURATION" ] || { echo "error: --duration is required" >&2; exit 2; }
71[ -n "$MODEL_TAG" ] || { echo "error: --model-tag is required" >&2; exit 2; }
72
73if [ ! -f "$VIDEO" ]; then
74 echo "error: video file not found: $VIDEO" >&2
75 exit 2
76fi
77VIDEO_ABS="$(cd "$(dirname "$VIDEO")" && pwd)/$(basename "$VIDEO")"
78
79# Resolve device.
80if [ -z "$DEVICE" ]; then
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." >&2
85 exit 3
86 elif [ "$NUM_CONNECTED" -gt 1 ]; then
87 echo "error: multiple devices connected — pass --device <serial>." >&2
88 printf ' %s\n' $CONNECTED >&2
89 exit 3
90 fi
91 DEVICE="$CONNECTED"
92 echo "==> auto-selected device: $DEVICE"
93fi
94
95now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
96
97DURATION_MS=$(python3 -c "print(int(float('$DURATION') * 1000))")
98
99echo "==> auto experiment"
100echo " model_tag = $MODEL_TAG"
101echo " duration = ${DURATION}s ($DURATION_MS ms)"
102echo " video = $VIDEO_ABS"
103echo " start_offset = ${START_OFFSET}s"
104
105# 1. Snapshot the existing logs so we can diff-pull only the new file.
106LOGS_BEFORE="$("$ADB" -s "$DEVICE" shell "ls -1 $REMOTE_LOGS_DIR 2>/dev/null" \
107 | tr -d '\r' | grep '\.json$' || true)"
108EXISTING_COUNT=$(printf '%s\n' "$LOGS_BEFORE" | grep -c '\.json$' || true)
109echo "==> $EXISTING_COUNT existing log(s) on device (will be ignored)"
110
111# 2. Pre-open the video in QuickTime *first* and position to start_offset
112# while paused. Large 4K files can take 10-60s to index, so we do this
113# before any wall-clock timing matters. The 600s timeout covers extreme
114# cases (default AppleEvent timeout is 60s and can fire on big files).
115echo "==> pre-opening video in QuickTime (large files may take a moment)..."
116osascript <<APPLESCRIPT
117with timeout of 600 seconds
118 tell application "QuickTime Player"
119 activate
120 if (count of documents) > 0 then
121 try
122 close every document saving no
123 end try
124 end if
125 open POSIX file "$VIDEO_ABS"
126 repeat while (count of documents) is 0
127 delay 0.1
128 end repeat
129 tell front document
130 pause
131 set current time to $START_OFFSET
132 present
133 end tell
134 end tell
135end timeout
136APPLESCRIPT
137
138# 3. Cold-start the app: kill any existing instance and clear Logcat.
139echo "==> force-stopping app and clearing Logcat..."
140"$ADB" -s "$DEVICE" shell am force-stop "$PKG" >/dev/null
141"$ADB" -s "$DEVICE" logcat -c >/dev/null
142
143# 4. Compute the wall-clock target and launch via intent with auto-mode extras.
144NOW_MS=$(now_ms)
145START_AT_WALL_MS=$((NOW_MS + APP_BOOT_PAD_SECONDS * 1000))
146RUN_ID="${START_AT_WALL_MS}_$(printf '%s' "$MODEL_TAG" | tr -c 'A-Za-z0-9._-' '_' | sed 's/^_*//; s/_*$//')"
147RUN_DIR="$EXPERIMENTS_DIR/$RUN_ID"
148echo " start_at_wall = $START_AT_WALL_MS (in ${APP_BOOT_PAD_SECONDS}s)"
149echo " run_dir = $RUN_DIR"
150
151echo "==> launching $ACTIVITY with auto-mode extras..."
152"$ADB" -s "$DEVICE" shell am start -n "$ACTIVITY" \
153 --ez experiment_auto true \
154 --es model_name "$MODEL_TAG" \
155 --el start_at_wall_ms "$START_AT_WALL_MS" \
156 --el duration_ms "$DURATION_MS" >/dev/null
157
158# 5. Wait until the wall-clock target, then play (already pre-loaded so this
159# is sub-second). t0_wall_ms is captured immediately before the play call.
160WAIT_MS=$((START_AT_WALL_MS - $(now_ms)))
161if [ "$WAIT_MS" -gt 0 ]; then
162 python3 -c "import time; time.sleep($WAIT_MS/1000.0)"
163fi
164
165T0_WALL_MS=$(now_ms)
166echo "==> playing video at offset ${START_OFFSET}s (t0_wall_ms=$T0_WALL_MS)"
167osascript -e 'tell application "QuickTime Player" to play front document' >/dev/null
168
169# 5. Sleep the buffer duration plus a small safety margin then pause.
170echo "==> sleeping ${DURATION}s for buffer to fill..."
171sleep "$DURATION"
172osascript -e 'tell application "QuickTime Player" to pause front document' >/dev/null
173
174# 6. Poll for the new log file with a timeout.
175echo "==> polling for new log file (timeout ${PULL_TIMEOUT_SECONDS}s)..."
176NEW_LOGS=""
177DEADLINE=$(( $(now_ms) + PULL_TIMEOUT_SECONDS * 1000 ))
178while [ "$(now_ms)" -lt "$DEADLINE" ]; do
179 LOGS_AFTER="$("$ADB" -s "$DEVICE" shell "ls -1 $REMOTE_LOGS_DIR 2>/dev/null" \
180 | tr -d '\r' | grep '\.json$' || true)"
181 NEW_LOGS="$(comm -13 \
182 <(printf '%s\n' "$LOGS_BEFORE" | sort -u) \
183 <(printf '%s\n' "$LOGS_AFTER" | sort -u) \
184 | grep '\.json$' || true)"
185 if [ -n "$NEW_LOGS" ]; then
186 break
187 fi
188 sleep 0.25
189done
190
191mkdir -p "$RUN_DIR"
192
193if [ -z "$NEW_LOGS" ]; then
194 echo "error: no new log file appeared on device after ${PULL_TIMEOUT_SECONDS}s" >&2
195 echo " recent ExperimentAuto Logcat:" >&2
196 "$ADB" -s "$DEVICE" logcat -d -s ExperimentAuto:I 2>/dev/null | tail -30 >&2 || true
197 exit 4
198fi
199
200echo "==> pulling new log(s) into $RUN_DIR/"
201while IFS= read -r f; do
202 [ -z "$f" ] && continue
203 "$ADB" -s "$DEVICE" pull "$REMOTE_LOGS_DIR/$f" "$RUN_DIR/" >/dev/null
204 echo " pulled $f"
205done <<< "$NEW_LOGS"
206
207# 7. Capture the ExperimentAuto log lines (for the manifest).
208LOGCAT_EXCERPT="$("$ADB" -s "$DEVICE" logcat -d -s ExperimentAuto:I 2>/dev/null \
209 | grep ExperimentAuto || true)"
210
211DEVICE_MANUFACTURER="$("$ADB" -s "$DEVICE" shell getprop ro.product.manufacturer 2>/dev/null | tr -d '\r' || true)"
212DEVICE_MODEL_NAME="$("$ADB" -s "$DEVICE" shell getprop ro.product.model 2>/dev/null | tr -d '\r' || true)"
213DEVICE_LABEL="$DEVICE_MANUFACTURER $DEVICE_MODEL_NAME"
214
215NEW_LOGS_JOINED="$(printf '%s\n' "$NEW_LOGS" | tr '\n' ',' | sed 's/,$//')"
216
217RUN_ID="$RUN_ID" \
218MODEL_TAG="$MODEL_TAG" \
219VIDEO_ABS="$VIDEO_ABS" \
220START_OFFSET="$START_OFFSET" \
221DURATION="$DURATION" \
222T0_WALL_MS="$T0_WALL_MS" \
223START_AT_WALL_MS="$START_AT_WALL_MS" \
224DEVICE="$DEVICE" \
225DEVICE_LABEL="$DEVICE_LABEL" \
226NEW_LOGS_JOINED="$NEW_LOGS_JOINED" \
227LOGCAT_EXCERPT="$LOGCAT_EXCERPT" \
228python3 -c '
229import json, os
230files = [f for f in os.environ["NEW_LOGS_JOINED"].split(",") if f]
231logcat = [l for l in os.environ["LOGCAT_EXCERPT"].splitlines() if l.strip()]
232manifest = {
233 "run_id": os.environ["RUN_ID"],
234 "mode": "auto",
235 "model_tag": os.environ["MODEL_TAG"],
236 "video_path": os.environ["VIDEO_ABS"],
237 "video_start_offset_seconds": float(os.environ["START_OFFSET"]),
238 "duration_seconds": float(os.environ["DURATION"]),
239 "t0_wall_ms": int(os.environ["T0_WALL_MS"]),
240 "start_at_wall_ms": int(os.environ["START_AT_WALL_MS"]),
241 "device_serial": os.environ["DEVICE"],
242 "device_label": os.environ["DEVICE_LABEL"].strip(),
243 "log_files": files,
244 "logcat_excerpt": logcat,
245}
246print(json.dumps(manifest, indent=2))
247' > "$RUN_DIR/manifest.json"
248
249echo "==> wrote $RUN_DIR/manifest.json"
250echo
251echo "Run dir: $RUN_DIR"
252ls -la "$RUN_DIR"