aperture-forensics / scripts /capture_screenshots.py
Nandan Acharya
deploy: HF Spaces snapshot — README_HF overlay + fresh screenshot pipeline
65f640c
Raw
History Blame Contribute Delete
6.1 kB
"""Headless screenshot capture for README demo images.
Boots Streamlit, drives the UI via Playwright, picks an example image,
clicks through each tab, saves PNGs to docs/screenshots/.
Run:
.venv/bin/python .capture_screenshots.py
"""
from __future__ import annotations
import socket
import subprocess
import sys
import time
from pathlib import Path
from playwright.sync_api import sync_playwright
OUT = Path("docs/screenshots")
OUT.mkdir(parents=True, exist_ok=True)
# Example image to drive the captures. "Copy-move" is the most visually-
# busy example (gives ELA + noise + 28 SIFT matches), so it makes the
# tampering tab actually interesting.
EXAMPLE_LABEL = "Copy-move"
TAB_TARGETS = [
("Verdict", "verdict.png"),
("AI Detection", "ai_detection.png"),
("Tampering", "tampering.png"),
("Scene", "scene.png"),
("Metadata", "metadata.png"),
("Model Performance", "performance.png"),
]
def _pick_port() -> int:
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
def _wait_for_server(port: int, timeout: float = 30.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", port), timeout=1.0):
return
except OSError:
time.sleep(0.4)
raise TimeoutError(f"streamlit did not start on :{port} within {timeout}s")
def main() -> None:
port = _pick_port()
print(f"[boot] streamlit on :{port}")
proc = subprocess.Popen(
[
".venv/bin/streamlit", "run", "app.py",
"--server.headless", "true",
"--server.port", str(port),
"--browser.gatherUsageStats", "false",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
try:
_wait_for_server(port)
print("[boot] server up — waiting 2s for first render")
time.sleep(2)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1440, "height": 1600},
device_scale_factor=2,
color_scheme="dark",
)
page = context.new_page()
page.goto(f"http://127.0.0.1:{port}/", wait_until="networkidle", timeout=60000)
# Streamlit's initial render finishes after the websocket hydrates.
page.wait_for_selector("[data-testid='stSidebar']", timeout=30000)
time.sleep(2)
# --- Welcome screen capture (no image yet) ---
page.screenshot(path=str(OUT / "welcome.png"), full_page=True)
print(f"[shot] {OUT / 'welcome.png'}")
# --- Pick an example via the sidebar selectbox ---
print(f"[select] choosing example: {EXAMPLE_LABEL}")
# Streamlit's selectbox renders the trigger as data-baseweb=select
# and options as role=option in a popover that appears on click.
selectboxes = page.locator("[data-testid='stSidebar'] [data-baseweb='select']")
example_box = selectboxes.nth(0)
example_box.click()
page.wait_for_timeout(400)
# Options live in a popover; match by exact text via the option role.
option = page.locator("[role='option']").filter(has_text=EXAMPLE_LABEL).first
try:
option.wait_for(state="visible", timeout=5000)
option.click()
except Exception:
# Fallback: type into the underlying input + Enter.
page.keyboard.type(EXAMPLE_LABEL, delay=20)
page.keyboard.press("Enter")
page.wait_for_timeout(800)
# Wait for analyses: image header, then the tab strip.
print("[wait] analyses running... up to 180s")
try:
page.wait_for_selector("button[role='tab']", timeout=180000)
except Exception:
page.screenshot(path=str(OUT / "_debug_no_tabs.png"), full_page=True)
print(f"[debug] saved {OUT / '_debug_no_tabs.png'}")
raise
# Once tabs exist, wait for any in-flight spinners to clear.
try:
page.wait_for_selector("[data-testid='stSpinner']", state="detached", timeout=60000)
except Exception:
pass
time.sleep(3)
# Hide the cold-start info banner and the deploy chrome — they
# add noise to every screenshot. Keep the rest of the UI intact.
page.add_style_tag(content="""
[data-testid='stAlertContainer'] { display: none !important; }
[data-testid='stStatusWidget'] { display: none !important; }
[data-testid='stToolbar'] { display: none !important; }
header { display: none !important; }
""")
# --- Cycle through each tab and screenshot ---
for tab_label, out_name in TAB_TARGETS:
print(f"[tab] {tab_label}")
tab = page.get_by_role("tab", name=tab_label)
tab.click()
page.wait_for_timeout(2500) # allow tab content + plots to paint
# Scroll the tablist to the top so the active tab content
# dominates the screenshot, not the file-header strip.
page.evaluate(
"document.querySelector('[role=\"tablist\"]')"
".scrollIntoView({block: 'start', behavior: 'instant'})"
)
page.wait_for_timeout(400)
page.screenshot(path=str(OUT / out_name), full_page=False)
print(f"[shot] {OUT / out_name}")
context.close()
browser.close()
print("[done] screenshots saved to", OUT)
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
if __name__ == "__main__":
main()