Build Shotomatic

KINDAreplaces $7.99/mosaves $95.88/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A screenshot automation tool whose entire value is determinism: the same page captured twice must produce the same bytes, or the output cannot be diffed, reviewed or trusted, and almost everything about a modern page conspires against that.

estimated effort multi-daythe files for this build are in the project pack

CaptureA headless browser with a pinned version and fixed viewportDeterminismFrozen clock, disabled animation, blocked external resourcesFramingDevice frames and annotations applied after captureLocalisationThe same script run per locale with the locale set properlyRuntimePython 3.12 with a job queue

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Orchestrates capture, comparison and decoration.

    Get it python.org or your package manager, then a virtual environment.

    Verify python3 --version prints 3.12 or higher

  2. installfree

    Why A browser update changes font rendering and layout subtly, which invalidates every baseline at once and looks like your site changed.

    Get it A browser automation library with a pinned browser build, recorded in your configuration.

    Verify The browser version is pinned and reported in every run

  3. have readyfree

    Why Determinism only fails on real pages, and the failures are specific: animation, a live clock, lazy images, web fonts and third-party content.

    Get it Collect pages exhibiting each, from a site you own or have permission to capture.

    Verify All five exhibit their problem on a naive capture

  4. have readyfree

    Why Localised capture is the main reason to automate screenshots, and half-translated output is the failure that looks correct.

    Get it A real application with genuine translations, including one language substantially longer than English.

    Verify Four locales render with translated dates and numbers, not only text

  5. decidefree

    Why A baseline updated automatically makes the entire comparison meaningless, and deciding the process after the first noisy run means deciding it while annoyed.

    Get it Decide who approves a baseline change and what tolerance is acceptable, and write it down.

    Verify A written process exists and tolerance is configured

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- scripts(id, name, steps_json, viewport_json, wait_strategy, created_at)
- targets(id, script_id, url, locale, theme, device_profile)
- captures(id, target_id, run_id, image_path, content_hash, captured_at, attempt, stable)
- runs(id, started_at, finished_at, browser_version, succeeded, failed, non_deterministic_count)
- baselines(target_id, content_hash, approved_at, approved_by) · what the output is supposed to look like
- diffs(capture_id, baseline_hash, changed_pixels, changed_regions_json, verdict)
- A capture records whether it was stable, meaning two consecutive captures produced identical bytes, because an unstable capture cannot be compared to anything.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
BROWSER_VERSIONrequiredpinned build identifierRecorded per run. A browser update invalidates every baseline at once.
OUTPUT_DIRrequired./capturesRaw captures and decorated outputs, kept separately.
FREEZE_CLOCK_ATrequired2026-01-01T12:00:00ZA fixed time, so any visible clock renders identically.
BLOCK_EXTERNAL_RESOURCESrequiredalwaysPresent as a constant. Third-party content is never the same twice.
STABILITY_CAPTURESrequired2Each target captured this many times and compared before being used.
DIFF_TOLERANCE_PIXELSrequired0Zero by default. Tolerance hides the changes you are looking for.

The build, in order

  1. The same page twice, identically

    Capturing a page twice produces byte-identical images.

    1. Two of the four things that make a page render differently each run.

      Files src/determinism.py

    2. Advertisements and third-party widgets are never the same twice.

      Files src/determinism.py

    3. The stability check is what makes every later diff meaningful.

      Files src/capture.pytests/test_stability.py

    done when · tick each as it passes
    watch out
    • Everything on a modern page fights determinism: a timestamp, a rotating advertisement, a fade-in that has not finished, a font arriving late, a randomised placeholder. Capture twice and compare on every run, so an unstable target is detected rather than producing a diff you spend an hour investigating.
  2. Knowing when the page is ready

    Captures happen after the page has finished, not after a guessed delay.

    1. Fonts and network, not a number of seconds.

      Files src/wait.py

    2. The page knows when it is ready better than any generic rule.

      Files src/scripts.py

    3. A screenshot of a loading spinner is worse than a failure.

      Files src/capture.py

    done when · tick each as it passes
    watch out
    • Waiting a fixed number of seconds is either too short, producing captures of half-loaded pages, or too long, making a batch take hours, and it is both on different pages in the same run. Wait on signals, and treat a page that never settles as a failure rather than capturing whatever was there at the timeout.
  3. The same shot in every language

    A script runs across locales and the text is genuinely translated.

    1. Browser locale, header and application preference together.

      Files src/locale.py

    2. German is longer and the button is not.

      Files src/overflow.py

    3. Comparing a German screenshot to an English baseline finds nothing useful.

      Files src/baselines.py

    done when · tick each as it passes
    watch out
    • Setting a language through a query parameter while the browser still reports a different locale produces pages that are half translated, with dates and numbers in the original language, and it looks correct enough to ship. Set the browser locale, the accept header and any application preference together.
  4. Diffs a person can review quickly

    A change is shown as a region rather than a pixel count.

    1. A percentage cannot distinguish a missing button from antialiasing.

      Files src/diff.py

    2. Automatic baseline updates make the whole comparison pointless.

      Files src/baselines.py

    3. An unstable target's diff is not a change and should not be reviewed as one.

      Files src/diff.py

    done when · tick each as it passes
    watch out
    • Reporting a diff as a percentage of changed pixels tells you nothing about whether the change matters, since a one percent change might be an entire button disappearing or an antialiasing difference along one edge. Report bounding regions and show them, so a reviewer looks at the change rather than at a number.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more screenshots.