Spaces:
Sleeping
Sleeping
| """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() | |