open-standards/aac/aac-rs/third_party/mediapipe/vendor.sh
Transaction Science de1243ce71 feat(aac-web): same-origin MediaPipe, a capability probe, a model store
MediaPipe is loaded from the app's own origin in source, not rewritten by the
build: Rust passes the vendored base into the binding, and
third_party/mediapipe/vendor.sh vendors tasks-vision + the face landmarker for
both builds (prerender-static.sh now too), with its Apache-2.0 LICENSE and
NOTICE, and patches out the library's telemetry flush so turning on eye gaze
makes no third-party request. The vendor directory is versioned by content.

capabilities.rs probes the device once after hydration (never in SSR):
WebGPU adapter, limits and subgroups; JSPI; cross-origin isolation; memory,
storage and persistence; Background Fetch; setSinkId on AudioContext and media
elements; speech synthesis, with local voice languages normalised from
Android's es_ES form. WebGPU inference is blocked on Samsung Xclipse and
Imagination PowerVR (Chromium's own blocklist and driver crashes). ai_tier
picks Minimal / WasmOnly / WebGpuSmall / WebGpuLarge; <html data-ai-tier> and
Settings → This device show it.

model_store.rs keeps voice and model packs in the Cache API ('aac-models-v1',
which both service workers now spare on update), verifies SHA-256 before
storing, retries, and asks for persistent storage. The service worker adds
COOP/COEP to the shell it serves, so the app stays cross-origin isolated
offline.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 10:50:19 -04:00

110 lines
5.9 KiB
Shell
Executable file

#!/usr/bin/env bash
# ============================================================================
# vendor.sh — put MediaPipe (eye gaze) on the app's own site.
#
# third_party/mediapipe/vendor.sh <site-dir> [cache-dir]
#
# Writes <site-dir>/vendor/mediapipe-<version>-<revision>/ (and removes any
# other vendor/mediapipe-* there): @mediapipe/tasks-vision from npm, the face
# landmarker model, and this directory's LICENSE.txt and NOTICE.txt. The app
# loads it from there (src/bindings/mediapipe_init.js), so turning on eye gaze
# makes no third-party request, keeps the app cross-origin isolated, and works
# offline once used.
#
# The directory name comes from MEDIAPIPE_VERSION and MEDIAPIPE_VENDOR_REV in
# crates/aac-web/src/eye_tracking.rs. Servers and the service workers cache it
# as immutable, so its bytes are pinned below (VENDOR_DIGEST): any change to
# what is served there fails the build until the revision is bumped, which
# gives the changed files a new URL.
#
# Used by prerender-static.sh (aac.transaction.science) and by the aurasense
# host's scripts/build-app.sh. Idempotent; downloads are cached in cache-dir
# (default ~/.cache/aac-rs).
# ============================================================================
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AAC="$(cd "$HERE/../.." && pwd)"
SITE="${1:?usage: vendor.sh <site-dir> [cache-dir]}"
CACHE="${2:-${XDG_CACHE_HOME:-$HOME/.cache}/aac-rs}"
ET="$AAC/crates/aac-web/src/eye_tracking.rs"
MP_VERSION="$(sed -n 's/^pub const MEDIAPIPE_VERSION: &str = "\([^"]*\)";$/\1/p' "$ET")"
MP_REV="$(sed -n 's/^pub const MEDIAPIPE_VENDOR_REV: &str = "\([^"]*\)";$/\1/p' "$ET")"
[ -n "$MP_VERSION" ] || { echo "✗ MEDIAPIPE_VERSION not found in $ET" >&2; exit 1; }
[ -n "$MP_REV" ] || { echo "✗ MEDIAPIPE_VENDOR_REV not found in $ET" >&2; exit 1; }
DIR="mediapipe-$MP_VERSION-$MP_REV"
# What vendor/$VENDOR_PINNED holds, as a sha256 over its sorted file list and
# each file's sha256. Re-pin only together with a new MEDIAPIPE_VENDOR_REV.
VENDOR_PINNED="mediapipe-1.0.1-aac1"
VENDOR_DIGEST="17805cc3670feb85dc0a555a21a04097f0663bcca5ad2a24454dfccce0b08910"
MP_MODEL_URL="https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
MP_MODEL_SHA256="64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff"
[ "$DIR" = "$VENDOR_PINNED" ] || {
echo "✗ eye_tracking.rs asks for $DIR, but vendor.sh is pinned to $VENDOR_PINNED — vendor it, check it, and re-pin VENDOR_PINNED and VENDOR_DIGEST" >&2
exit 1
}
sha256() { shasum -a 256 "$1" | cut -c1-64; }
echo "▸ vendoring MediaPipe tasks-vision $MP_VERSION ($DIR)"
mkdir -p "$CACHE"
# npm package: unpacked beside the cache entry, then moved in whole, so an
# interrupted run never leaves half a package that looks complete.
MP_PKG="$CACHE/tasks-vision-$MP_VERSION/package"
if [ ! -f "$MP_PKG/package.json" ]; then
TMP="$(mktemp -d "$CACHE/tasks-vision.XXXXXX")"
(cd "$TMP" && npm pack --silent "@mediapipe/tasks-vision@$MP_VERSION" >/dev/null && tar xzf ./*.tgz)
rm -rf "$CACHE/tasks-vision-$MP_VERSION"
mv "$TMP" "$CACHE/tasks-vision-$MP_VERSION"
fi
grep -q "\"version\": \"$MP_VERSION\"" "$MP_PKG/package.json" || { echo "✗ cached tasks-vision is not $MP_VERSION" >&2; exit 1; }
# Model: downloaded to a temporary name and moved in only once its hash
# matches; a cached copy that does not match is fetched again.
MODEL="$CACHE/face_landmarker.task"
if [ ! -f "$MODEL" ] || [ "$(sha256 "$MODEL")" != "$MP_MODEL_SHA256" ]; then
rm -f "$MODEL"
curl -fsSL "$MP_MODEL_URL" -o "$MODEL.tmp"
[ "$(sha256 "$MODEL.tmp")" = "$MP_MODEL_SHA256" ] \
|| { rm -f "$MODEL.tmp"; echo "✗ face_landmarker.task does not match its pinned sha256" >&2; exit 1; }
mv "$MODEL.tmp" "$MODEL"
fi
rm -rf "$SITE"/vendor/mediapipe-*
MP="$SITE/vendor/$DIR"
mkdir -p "$MP"
cp "$MP_PKG/vision_bundle.mjs" "$MP/"
cp -R "$MP_PKG/wasm" "$MP/wasm"
cp "$MODEL" "$MP/"
# The npm package ships no licence text; Apache-2.0 §4 asks for one, and a
# notice of what we changed.
cp "$HERE/LICENSE.txt" "$HERE/NOTICE.txt" "$MP/"
# The library reports usage metrics to Google (odml.pa.googleapis.com) every
# minute, with no option to turn it off (https://goo.gle/mediapipe-privacy,
# read 2026-09-29). Make its one sender drop the queue instead of posting it,
# and say so at the top of the file (Apache-2.0 §4(b)).
MP_SEND='flush(t,e){if(this.error)e?.("net-send-failed")'
MP_DROP='flush(t,e){this.h=[];return void t?.();if(this.error)e?.("net-send-failed")'
[ "$(grep -oF "$MP_SEND" "$MP/vision_bundle.mjs" | wc -l | tr -d ' ')" = 1 ] \
|| { echo "✗ MediaPipe's metrics sender has changed — re-check the telemetry patch for $MP_VERSION" >&2; exit 1; }
{ echo "// Changed by the AAC build: usage-metrics upload disabled (Logger.flush drops its queue). See NOTICE.txt."
sed 's/flush(t,e){if(this.error)e?.("net-send-failed")/flush(t,e){this.h=[];return void t?.();if(this.error)e?.("net-send-failed")/' "$MP/vision_bundle.mjs"
} > "$MP/vision_bundle.mjs.tmp" && mv "$MP/vision_bundle.mjs.tmp" "$MP/vision_bundle.mjs"
grep -qF "$MP_DROP" "$MP/vision_bundle.mjs" || { echo "✗ telemetry patch did not apply" >&2; exit 1; }
if grep -qF "$MP_SEND" "$MP/vision_bundle.mjs"; then echo "✗ unpatched metrics sender still present" >&2; exit 1; fi
for f in vision_bundle.mjs wasm/vision_wasm_internal.js wasm/vision_wasm_internal.wasm face_landmarker.task LICENSE.txt NOTICE.txt; do
[ -s "$MP/$f" ] || { echo "✗ vendored MediaPipe is missing $f" >&2; exit 1; }
done
DIGEST="$(cd "$MP" && find . -type f | LC_ALL=C sort | while IFS= read -r f; do printf '%s %s\n' "$f" "$(sha256 "$f")"; done | shasum -a 256 | cut -c1-64)"
[ "$DIGEST" = "$VENDOR_DIGEST" ] || {
echo "✗ vendor/$DIR differs from what was pinned (got $DIGEST)." >&2
echo " Browsers cache that URL as immutable: bump MEDIAPIPE_VENDOR_REV in eye_tracking.rs," >&2
echo " then set VENDOR_PINNED and VENDOR_DIGEST in $HERE/vendor.sh." >&2
exit 1
}
echo "✓ vendor/$DIR ($(du -sh "$MP" | cut -f1))"