Bands = end profiles of per-control-span shell capsules (~9.35 m pitch = span spacing). Shell now smooth-unions adjacent spans over 6 m and reuses the interior chord distance; a 0.60 m clearance fades in 5-20 m from each mouth. Close-ups: bands reduced but still visible on part of the exposed shell. Cost vs L109: near-ready +8..+12% (FI 0.88 -> 1.00, TN 0.92 -> 1.01 s @W12). Audits, Clang/GCC determinism, fast tier and Linux UE build pass. Kept on its own branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
56 lines
2.2 KiB
Python
56 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Build review sheets for every L110 before/after passage capture."""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
|
SHEETS = CAPTURES / "contact-sheets"
|
|
GROUPS = {
|
|
"openair-view-1": ("*-openair-*-view-1.png", "3x3"),
|
|
"openair-view-2": ("*-openair-*-view-2.png", "3x3"),
|
|
"openair-view-3": ("*-openair-*-view-3.png", "3x3"),
|
|
"openair-closeups": ("*-openair-*-closeup-*.png", "3x4"),
|
|
"mouths": ("*-*-*-*-mouth.png", "3x2"),
|
|
"stations": ("*-station-*.png", "3x3"),
|
|
}
|
|
|
|
|
|
def build(variant: str, group: str, pattern: str, layout: str) -> int:
|
|
files = sorted((CAPTURES / variant).glob(pattern))
|
|
if not files:
|
|
raise RuntimeError(f"no images matched {variant}/{pattern}")
|
|
columns, rows = (int(value) for value in layout.split("x"))
|
|
scale = "480:270"
|
|
output = SHEETS / f"{variant}-{group}.png"
|
|
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
|
"-f", "image2", "-pattern_type", "glob", "-framerate", "1",
|
|
"-i", str(CAPTURES / variant / pattern), "-vf",
|
|
f"scale={scale},tile={columns}x{rows}:nb_frames={columns*rows}:padding=6:margin=6",
|
|
"-frames:v", "1", str(output)]
|
|
result = subprocess.run(command, cwd=ROOT, check=False,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
if result.returncode:
|
|
raise RuntimeError(f"ffmpeg failed for {variant}/{group}: {result.stderr.strip()}")
|
|
return len(files)
|
|
|
|
|
|
def main() -> int:
|
|
SHEETS.mkdir(parents=True, exist_ok=True)
|
|
rows = []
|
|
for variant in ("before", "after"):
|
|
for group, (pattern, layout) in GROUPS.items():
|
|
count = build(variant, group, pattern, layout)
|
|
rows.append(f"- [{variant} {group}]({variant}-{group}.png) — {count} views")
|
|
(SHEETS / "INDEX.md").write_text("# L110 capture contact sheets\n\n"
|
|
+ "\n".join(rows) + "\n", encoding="utf-8")
|
|
print(f"contact sheets={len(rows)} captures={sum(
|
|
int(row.rsplit('— ', 1)[1].split()[0]) for row in rows)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|