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]>
110 lines
5.9 KiB
Shell
Executable file
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))"
|