Replaces narrative claims with verified numbers across all report pages: - Cross-model held-out validation (LVFace/mbf/r18, all 5 held-out films): LVFace wins every film outright, not just "consistent with" the training-set pick. r50 dropped from the detailed comparison (gallery has ~30% fewer reference images per actor than the other three models on identical source photos). - Per-film training breakdown: LVFace does not win every training film (mbf beats it on Lord of War); the 75.3% macro figure hides a 10.7pp spread. - Gallery coverage computed per film (20.3%-78.6%) instead of one flat 67%-missing average. - Found and fixed a real scoring bug in optimize.py: a candidate whose hardest film's replay timed out was averaged over survivors instead of penalized, silently rewarding partial coverage. Affected 3 of 16 training combos; corrected throughout, and optimize.py now scores an incomplete evaluation f1=0.0 instead of averaging over whichever films happened to finish. - Every FPI frame in the deep dive now comes from the proper montage renderer (Onscreen/Offscreen panel, ghosts never drawn as boxes), never the bare-box debug overlay used earlier. - Every distinct out-of-cast name across all 9 films gets its own frame at its first appearance (9 names, 4 films), not a single-example spot check: 2 ground-truth gaps, 1 photograph misread as a person, 6 genuine lookalike confusions. - New methodology.md: the scene-level-vs-per-second scoring mismatch that the rest of the report assumes, written out once. - Cut the deadlock/gdb debugging narrative from the experiment log; kept the one fact that matters (KPN's node/network split lets the expensive GPU stage run once and the cheap stage replay against cached embeddings). - Plain declarative style throughout, no em dashes, no blog voice.
149 lines
7.0 KiB
Bash
Executable File
149 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
|
# build_site.sh — pull the images the docs reference from the artifact registry
|
|
# (if not already present locally), stage them under docs/assets/, then build
|
|
# the MkDocs site. The built site/ output is what gets pushed to gitea-pages —
|
|
# never the source images themselves (see scripts/artifacts/push_artifacts.sh).
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
ASSETS_DIR="docs/assets/images"
|
|
mkdir -p "$ASSETS_DIR"
|
|
|
|
# Frames referenced by the docs. The scene best/worst montages (proper
|
|
# renderer: Onscreen/Offscreen panel + TPI/FPI/FN legend) live in the
|
|
# montage-frames registry packages, one per film; pull any film that's missing.
|
|
MONTAGE_ROOT="experiments/results/holdout/montage_bestworst"
|
|
for film in Downton_Abbey__A_New_Era Lovelace Café_Society \
|
|
Valerian_and_the_City_of_a_Thousand_Plan The_Many_Saints_of_Newark; do
|
|
if [ ! -d "${MONTAGE_ROOT}/${film}" ]; then
|
|
echo "==> pulling montage-frames/${film} (not found locally)..."
|
|
scripts/artifacts/pull_artifacts.sh montage-frames "$film" || true
|
|
fi
|
|
done
|
|
|
|
# stage_frame <src> <dest-basename>: downscale to <=1920px wide for the site
|
|
stage_frame() {
|
|
if [ ! -f "$1" ]; then
|
|
echo "WARN: $1 not present; keeping existing ${ASSETS_DIR}/$2 (if any)"
|
|
return
|
|
fi
|
|
python3 - "$1" "${ASSETS_DIR}/$2" <<'PY'
|
|
import sys
|
|
from PIL import Image
|
|
src, dst = sys.argv[1], sys.argv[2]
|
|
im = Image.open(src)
|
|
if im.width > 1920:
|
|
im = im.resize((1920, round(im.height * 1920 / im.width)), Image.LANCZOS)
|
|
im.save(dst, quality=88)
|
|
print(f"staged {dst} ({im.width}x{im.height})")
|
|
PY
|
|
}
|
|
|
|
echo "==> staging referenced frames into ${ASSETS_DIR}"
|
|
stage_frame "${MONTAGE_ROOT}/Downton_Abbey__A_New_Era/scene_4/4_best_t000128.jpg" \
|
|
downton_wedding_couple.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Downton_Abbey__A_New_Era/scene_16/16_best_t007100.jpg" \
|
|
downton_funeral_19of20.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Downton_Abbey__A_New_Era/scene_7/7_worst_t001754.jpg" \
|
|
downton_crew_fn.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lovelace/scene_24/24_best_t003607.jpg" \
|
|
lovelace_polygraph_bridged.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lovelace/scene_6/6_worst_t000421.jpg" \
|
|
lovelace_robert_patrick_fpi.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lovelace/scene_15/15_best_t002005.jpg" \
|
|
lovelace_perfect_second.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Café_Society/scene_20/20_best_t002430.jpg" \
|
|
cafe_society_rapid_cut.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Valerian_and_the_City_of_a_Thousand_Plan/scene_4/4_best_t001027.jpg" \
|
|
valerian_screen_call.jpg
|
|
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/out_of_cast_fpi/4_worst_t000871.jpg" \
|
|
many_saints_outofcast_fpi.jpg
|
|
|
|
# One frame per DISTINCT out-of-cast name across all 9 films, uniform rule
|
|
# (see scripts/docs/first_fpi_frames.py): the first second in the raw replay
|
|
# stream where the pipeline names someone not in the film's credited cast at
|
|
# all. Rendered with the proper montage renderer (Onscreen/Offscreen panel),
|
|
# never dump_error_frames.py's bare-box overlay. Regenerate with:
|
|
# python3 scripts/docs/first_fpi_frames.py
|
|
# 5 of 9 films have zero out-of-cast names in their whole runtime (Benny &
|
|
# Joon, Cafe Society, Downton Abbey, Sound of Metal, Valerian) and produce
|
|
# no frames here.
|
|
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_david_shumbris/first_fpi_t000418.jpg" \
|
|
lord_of_war_fpi_shumbris.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_ronald_reagan/first_fpi_t001003.jpg" \
|
|
lord_of_war_fpi_reagan_photo.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lord_of_War/first_fpi_lance_reddick/first_fpi_t006424.jpg" \
|
|
lord_of_war_fpi_reddick.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Lovelace/first_fpi_chloë_sevigny/first_fpi_t002451.jpg" \
|
|
lovelace_fpi_sevigny.jpg
|
|
stage_frame "${MONTAGE_ROOT}/Scarface/first_fpi_kirstie_alley/first_fpi_t002451.jpg" \
|
|
scarface_fpi_alley.jpg
|
|
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_germar_terrell_gardner/first_fpi_t000848.jpg" \
|
|
many_saints_fpi_gardner.jpg
|
|
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_archie_yates/first_fpi_t002521.jpg" \
|
|
many_saints_fpi_yates.jpg
|
|
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_zooey_deschanel/first_fpi_t002819.jpg" \
|
|
many_saints_fpi_deschanel.jpg
|
|
stage_frame "${MONTAGE_ROOT}/The_Many_Saints_of_Newark/first_fpi_talia_balsam/first_fpi_t004551.jpg" \
|
|
many_saints_fpi_balsam.jpg
|
|
|
|
if [ ! -f "${ASSETS_DIR}/germar_beats_xray.jpg" ]; then
|
|
echo "==> pulling report-highlights/germar_beats_xray.jpg..."
|
|
scripts/artifacts/pull_artifacts.sh report-highlights germar_beats_xray.jpg
|
|
fi
|
|
|
|
# pipeline_topology.svg is small and hand-authored (not pulled from anywhere) —
|
|
# committed directly at docs/assets/images/, not staged from the registry.
|
|
|
|
if [ ! -d experiments/galleries ] || [ -z "$(ls -A experiments/galleries 2>/dev/null)" ]; then
|
|
echo "==> pulling galleries (not found locally)..."
|
|
scripts/artifacts/pull_artifacts.sh galleries
|
|
fi
|
|
|
|
echo "==> generating calibration curve chart"
|
|
python3 scripts/docs/calibration_chart.py --out "${ASSETS_DIR}/calibration_curves.png"
|
|
|
|
echo "==> generating experiment charts (16-combo ranking, DE landscape, held-out F1, ghost timeline)"
|
|
python3 scripts/docs/experiment_charts.py --out-dir "${ASSETS_DIR}"
|
|
|
|
echo "==> building site"
|
|
mkdocs build
|
|
|
|
# -- commit-pinned repo links -------------------------------------------------
|
|
# Docs reference repo files via the placeholder hosts https://REPOLINK/<path>
|
|
# (this repo) and https://KPNLINK/<path> (the KPN++ submodule). Substitute them
|
|
# with raw URLs pinned to the exact commit being published, and fail the build
|
|
# if any linked path doesn't actually exist at that commit — no dead links.
|
|
HEAD_SHA="$(git rev-parse HEAD)"
|
|
KPN_SHA="$(git rev-parse HEAD:external/KPN)"
|
|
REPO_RAW="https://gitea.tourolle.paris/dtourolle/scene-actor-extraction/raw/commit/${HEAD_SHA}"
|
|
KPN_RAW="https://gitea.tourolle.paris/dtourolle/KPN/raw/commit/${KPN_SHA}"
|
|
|
|
if [ -n "$(git status --porcelain -- docs scripts src experiments)" ]; then
|
|
echo "WARN: working tree is dirty — commit-pinned links will point at ${HEAD_SHA}," >&2
|
|
echo " which may not contain your latest changes. Commit before deploying." >&2
|
|
fi
|
|
|
|
echo "==> verifying repo-linked paths exist at ${HEAD_SHA}"
|
|
missing=0
|
|
for p in $(grep -rhoE 'https://REPOLINK/[A-Za-z0-9_./-]+' docs/*.md | sed 's|https://REPOLINK/||' | sort -u); do
|
|
if ! git cat-file -e "HEAD:${p}" 2>/dev/null; then
|
|
echo "error: docs link to '${p}', which does not exist at HEAD" >&2
|
|
missing=1
|
|
fi
|
|
done
|
|
[ "$missing" -eq 0 ] || exit 1
|
|
|
|
echo "==> pinning repo links to ${HEAD_SHA} (KPN: ${KPN_SHA})"
|
|
find site -name '*.html' -exec \
|
|
sed -i "s|https://REPOLINK|${REPO_RAW}|g; s|https://KPNLINK|${KPN_RAW}|g" {} +
|
|
|
|
if grep -rq 'REPOLINK\|KPNLINK' site; then
|
|
echo "error: unsubstituted REPOLINK/KPNLINK placeholder left in site/" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> done. site/ is ready to deploy to the gitea-pages branch."
|