#!/usr/bin/env bash # Record the four Chinese feature preview clips off an iOS Simulator. # # Drives the DEBUG in-app demo hosts, which render the **real** keyboard views # (`AIKeyboardView`, `LastInputEditView`, `ClipboardHistoryPanelView`, …) and # the real `PolishStylesView` against a scripted `KeyboardState` — no ASR, no # LLM, no network. `--preview-fullscreen` adds the Notes / Messages host # document above the keyboard band so the frame works as a full-screen App # Store preview instead of a cropped What's New card. # # Why not the keyboard extension (`--whats-new-host`)? Making a third-party # keyboard the *active* one needs a globe-key tap that cannot be scripted # through simctl. The in-app hosts use the same views and need no tap. # # Device must be an iPhone 16 Plus (native 1290x2796 — the App Store Connect # 6.9" preview slot) on a runtime new enough for the app's Liquid Glass APIs # (iOS 26.5 at time of writing; iOS 26.0 is missing `View.glassEffect(_:in:)`). # # Usage: # Scripts/record_feature_previews.sh [slug ...] # PREVIEW_LANG=en Scripts/record_feature_previews.sh [slug ...] # -> English demo copy, footage in `.tmp/feature-previews/raw-en` set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" UDID="${1:?usage: record_feature_previews.sh [slug ...]}" shift || true BUNDLE="com.osgkeyboard.ios" PREVIEW_LANG="${PREVIEW_LANG:-zh}" if [[ "$PREVIEW_LANG" == "zh" ]]; then RAW_DIR="$ROOT/.tmp/feature-previews/raw" else RAW_DIR="$ROOT/.tmp/feature-previews/raw-$PREVIEW_LANG" fi mkdir -p "$RAW_DIR" LANG_ARG="--whats-new-lang=$PREVIEW_LANG" # slug | seconds | launch args CLIPS=( "voice-polish|23|--edit-demo --preview-fullscreen $LANG_ARG" # Static page — the motion is a post-production push-in, so this only needs # to be long enough to fill the App Store 15s minimum after trimming. "personal-style|22|--polish-styles-screenshot" "clipboard-agent|46|--clipboard-demo --preview-fullscreen $LANG_ARG" "ask-ai|36|--ai-demo --preview-fullscreen $LANG_ARG" ) want() { local slug="$1"; shift # No slug filter left on the command line -> record everything. [[ $# -eq 0 ]] && return 0 for requested in "$@"; do [[ "$slug" == "$requested" ]] && return 0 done return 1 } echo "==> Device: $UDID" # Installing over an existing copy does not refresh the App Group container. # When it is missing the app renders `AppGroupErrorView` instead of the demo, # and the recording silently captures an error screen — so fail loudly here. DEVICE_ROOT="$HOME/Library/Developer/CoreSimulator/Devices/$UDID" if ! grep -qls "group.com.osgkeyboard.shared" \ "$DEVICE_ROOT"/data/Containers/Shared/AppGroup/*/.com.apple.mobile_container_manager.metadata.plist then echo "FAIL: App Group container missing on $UDID." >&2 echo " Reinstall cleanly, then retry:" >&2 echo " xcrun simctl uninstall $UDID $BUNDLE && xcrun simctl install $UDID .app" >&2 exit 1 fi # Marketing status bar: fixed time, full bars, full battery. xcrun simctl status_bar "$UDID" override \ --time "9:41" \ --dataNetwork wifi --wifiMode active --wifiBars 3 \ --cellularMode active --cellularBars 4 \ --batteryState charged --batteryLevel 100 for entry in "${CLIPS[@]}"; do IFS='|' read -r slug seconds args <<<"$entry" want "$slug" "$@" || continue out="$RAW_DIR/$slug.mov" echo "==> Recording $slug (${seconds}s) [$args]" xcrun simctl terminate "$UDID" "$BUNDLE" >/dev/null 2>&1 || true sleep 1 rm -f "$out" xcrun simctl io "$UDID" recordVideo --codec h264 --force "$out" & RECORD_PID=$! # Let the recorder attach before the first frame we care about. sleep 1.5 # shellcheck disable=SC2086 -- args are intentionally word-split. xcrun simctl launch "$UDID" "$BUNDLE" $args >/dev/null sleep "$seconds" kill -INT "$RECORD_PID" 2>/dev/null || true wait "$RECORD_PID" 2>/dev/null || true sleep 1 if [[ -s "$out" ]]; then ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height -show_entries format=duration \ -of default=noprint_wrappers=1 "$out" else echo "FAIL: no footage captured for $slug" >&2 exit 1 fi done xcrun simctl terminate "$UDID" "$BUNDLE" >/dev/null 2>&1 || true echo "==> Raw footage in $RAW_DIR"