Safe Frame

ClickHouse track · version-lineage safety

The master passed. The version people watched was never checked.

One film becomes dozens of cuts, grades, frame rates and ad-break versions. Safe Frame searches a whole distribution catalogue for a photosensitivity risk that a conversion introduced, even when the approved master was clean.

Transition measurements are aligned on presentation time in ClickHouse. Both published flash rules are evaluated across every rendition in a single pass through the official mcp-clickhouse server, and master/rendition isolation happens inside the same query. Gemini explains the rows; SQL decides.

Check your own video

Four panels, queried from ClickHouse when you press it. Nothing here is pre-rendered.

Who this is for: the studio crew that ships the versions, distribution QC and encoding teams who sign off a master and then send dozens of derived assets out the door.

n/a transition measurements are loaded and waiting to be searched.

Why this matters

A rendition is a new chance to hurt somebody.

Photosensitive epilepsy is rare, documented, and regulated. The standards are public and the analysers are mature, but they were built to judge a file, not to judge a file against the one that was already approved.

~1 in 4,000

people in the general population have photosensitive epilepsy. It is roughly twice as common in women, and most often begins before age 20.

Epilepsy Foundation review, Fisher et al., Epilepsia 2022
685 children

were taken to hospital across Japan after a single television broadcast on 16 December 1997. Two stayed in hospital more than two weeks.

The 1997 broadcast incident
Rule 2.12

of the Ofcom Broadcasting Code requires broadcasters to keep risk to photosensitive viewers low. WCAG 2.3.1 sets equivalent thresholds for anything delivered on the web.

Ofcom §2 · W3C WCAG 2.3.1
Standard practice puts the quality gate at ingest, before transcode, because a bad master multiplies across the whole ladder. That gate is doing its job. It simply is not looking at what the transcode adds after the master was signed off.

What already exists

Detection, repair, viewer-side dimming and networked batch analysis are all mature. Harding-style analysers test luminance flashes, red flashes and spatial patterns, and fail a file that breaches them. We claim none of this as ours.

What we could not find

A documented check that compares a rendition against its own approved parent and attributes a newly-introduced violation to the transform that caused it. Our search covered public product documentation; it cannot prove no private integration exists.

Live catalogue sweep

Every title. Every rendition. One question.

Which renditions about to ship introduced a flash their approved master never had?

Both rules are computed live over the corpus below in one pass. Nothing on this page is a pre-computed answer.

n/atransition rows
n/arenditions
n/atitles
n/atransform types
Inspect MCP evidence API

Ready, press “Sweep the catalogue”.

Renditions that introduced a photosensitivity violation their approved master did not have. Select a row to see its evidence.
TitleRenditionRule Introduced byWindow TransitionsPeak area

No sweep has been run in this browser session.

Does it actually work

Scored against a ground truth it cannot see.

The fair objection to a synthetic demonstration is that it is circular: data was generated with flashes in it, and then the flashes were found. So the planted set is recovered from the generator’s own random decisions, without reading a single measurement, and compared against what the criteria returned.

sql/008_ground_truth.sql recomputes the decisions that chose where to plant. It never touches luma_delta, red_delta, luma_min, changed_area_fraction or direction. Two queries, produced by independent means; agreement between them is a result rather than a restatement.

The catalogue holds two kinds of row, and they are scored separately on purpose. Most are authored, SQL decided what each measurement should be. The measured_* titles are not: they are constructed RGB frames pushed through the same measurement code a decoder would feed, so every value is the output of relative_luminance and red_flash_mask over real pixel arrays. If the detector only scored well on numbers we wrote, the corpus would be doing the work.

Press “Score the detector” to compute this live.

Why the decoys are the point

Recall alone can be bought by flagging everything. The corpus carries renditions that carry a real burst and are still not regressions, because their approved master has the same burst at the same presentation time, and a cohort that clears every floor but sits above the published darker-image ceiling. Precision is measured against those.

What this does not show

Agreement with a synthetic ground truth we authored. It demonstrates that the implementation does what the criteria say at catalogue scale, and it is not evidence about real footage, not a comparison against a certified analyser, and not a clinical result.

Systemic cause

Dozens of findings, or a handful of broken profiles?

The count is not the useful number. The useful number is how many encoder profiles are producing them, because a profile is one upstream fix, not N renditions to patch one at a time.

Press “Profile every transform” to compute this live.

The agent’s mission

A brief a QC lead can act on before the shift starts.

The agent does not decide anything. It reads the database four different ways, works out which conversion is at fault, and hands a human the order to review in.

A Google ADK LlmAgent on Gemini with four tools, each a live query through the official mcp-clickhouse server. It sequences them itself. Every figure in the brief came back from SQL; the tool trace is returned alongside so you can check that.

QC triage brief

No brief has been generated in this browser session.

Check your own

Everything above ran on our catalogue. Run it on yours.

Choose a video and it is decoded in this browser. It is never uploaded, and never displayed on this page: only downscaled samples are sent, and they are measured by the same function the catalogue used. The verdict comes back from ClickHouse.

Add the approved master as well and the question changes from does this flash to the one this product exists for: did this conversion introduce a flash the master did not have. A clip that flashes is not a regression if its master flashed the same way.

No video to hand? Run one of ours. Each loads a constructed pair and checks it for real.

Constructed from arithmetic, not filmed, by scripts/make_samples.py. Checking one does not play it. If you want to inspect the material itself, it is below, behind a warning rather than behind a refusal.

Play or download the sample files. Two of the three contain a deliberate flash sequence.

These are test patterns. They are the kind of material a QC engineer needs to be able to look at, so they are here rather than withheld, but nothing plays until you press play. If you are photosensitive, do not open the two marked as flashing.

  • master.webm no flash sequence

    The approved master. Gentle drift, nothing that meets any threshold.

    Download
  • rendition-flash.webm flashes

    A full-screen luminance alternation, about twelve per second for two seconds, from one second in.

    Download
  • rendition-red.webm flashes

    A saturated-red alternation at matched luminance, the case a luminance-only checker passes. Same rate, same two seconds.

    Download
  • Never uploaded. Decoded in this browser; the file does not leave your machine.
  • Your file is never displayed. Checking a clip never plays it back at you. Our own sample files can be played, but only if you ask for them above.
  • Nothing stored. Downscaled samples are measured in memory and written nowhere.
Nothing is uploaded until you press this.

Choose a video to check it against the published criteria.

Build against it

No account. No approval. A key only raises your limits.

Every endpoint on this service works without a credential, and the whole judge path is meant to be exercised that way. If you are building something against it, mint a key: it multiplies the per-minute cap on the endpoints that spend model tokens or write, and names your calls in the logs. Reads are never capped either way.

  • No account. No email, no name, no approval step, and no user record.
  • Not stored. The key is signed rather than saved, so there is no credential table.
  • Shown once. We cannot recover it for you, because we never had it.
Full OpenAPI reference Nothing is stored: the key carries its own signature.

Shown once, here, and never stored by us. Press the button above.

Run it from here

Calls every read endpoint in your browser, in order, and shows what came back. Uses your key if you minted one, anonymously if not.

    Request and response examples: mint a key, check a measured pair, post frames directly.

    Mint a key from the command line

    Send an explicit empty body. A POST with no Content-Length is rejected with 411 by the load balancer before it reaches the service, which is a quirk of the hosting rather than of this API.

    curl -s -X POST https://safe-frame-regression-109051079423.us-central1.run.app/v1/keys   -H 'content-type: application/json' -d '{}'

    Check a clip you have already measured

    curl -s https://safe-frame-regression-109051079423.us-central1.run.app/v1/samples > pair.json
    
    curl -s -X POST https://safe-frame-regression-109051079423.us-central1.run.app/v1/scan \
      -H 'content-type: application/json' \
      -H 'Authorization: Bearer YOUR_KEY' \
      -d @pair.json

    Check frames directly

    Post decoded RGB samples and the service measures them for you, the same way this page does. Send master as well to ask the regression question instead of the absolute one.

    POST /v1/analyze
    {
      "rendition": {
        "frames_b64": "<base64 of raw RGB bytes, frame_count * height * width * 3>",
        "width": 48, "height": 27, "frame_count": 600, "frame_rate": 30
      },
      "master": { "...": "optional; omit for an absolute check" }
    }

    One endpoint does store what you send it. /v1/scan persists the transition rows you submit so the per-pair anti-join can read them back, under identifiers you choose, with no expiry. That is what the endpoint is for. If you would rather nothing were retained, use /v1/analyze, which answers the same question and writes nothing. Full detail in docs/PRIVACY.md.

    Or measure frames yourself with safe_frame.ingest.frames_to_transitions and post the transition rows to /v1/scan under your own asset identifiers. Identifiers belonging to the published catalogue are refused as write targets, so no caller can alter what another sees.

    Criteria provenance

    Every threshold traces to a published definition.

    Nothing here is a number we chose. Each one is the published criterion, and the one rule we have not implemented is named rather than implied.

    Every threshold, side by side with the document it comes from, including the rule we did not implement.
    What is testedThreshold in Safe FramePublished definition
    General flash, amplitude luma_delta ≥ 0.10 “a pair of opposing changes in relative luminance of 10% or more of the maximum relative luminance…”, WCAG 2.3.1
    General flash, darker image luma_min < 0.80 “…where the relative luminance of the darker image is below 0.80”. A swing between two bright states is not a general flash, however large. We were missing this until we audited against the published text: it was producing false positives. WCAG 2.3.1
    Red flash per pixel: R/(R+G+B) ≥ 0.8
    and |Δ(R−G−B)×320| > 20
    then red_delta ≥ 0.20
    no luminance condition
    “any pair of opposing transitions involving a saturated red”, where “for either or both states… R/(R+G+B) >= 0.8, and the change in the value of (R-G-B)x320 is > 20”. People are more sensitive to red flashing, so it gets its own test, deliberately with no luminance condition. The pixel test runs in safe_frame/metrics.py and yields red_delta, the fraction of the frame that satisfied it; the SQL thresholds that column at 0.20. That floor is lower than the 0.25 area condition, so the red rule is deliberately the more sensitive of the two. WCAG 2.3.1
    Relative luminance 0.2126R + 0.7152G + 0.0722B
    over linearised sRGB
    “if RsRGB <= 0.04045 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4”. We were weighting the gamma-encoded values without linearising them. W3C
    Affected area changed_area_fraction ≥ 0.25
    approximated
    WCAG asks for “25% of any 10 degree visual field”, an angular measure needing screen size and viewing distance a file does not carry. We use the fraction of the frame, which matches the Ofcom/ITU wording (“a quarter of the screen area”) and, per a 2024 gap analysis, every guideline except WCAG. Against WCAG specifically it can under-report. WCAG 2.3.1 · Ofcom §2
    Rate > 6 opposing transitions
    in any 1000 ms
    “more than three flashes in any one second period”. One flash is a pair of opposing transitions, so three flashes is six transitions, WCAG 2.3.1
    Spatial pattern not implemented Harding-style analysers also test regular spatial patterns. Safe Frame does not, and will not flag a striped or checkerboard pattern that breaches the spatial criteria.

    What auditing against the source found

    We could not obtain review by a qualified photosensitive-epilepsy professional, so we audited the implementation against the published texts instead and wrote down what was wrong. Three defects, all of which changed results: relative luminance was never linearised; the darker-image condition was missing entirely; and saturated red was a plausible-looking proxy rather than the published test. The guidelines are themselves not harmonised, so passing here is not compliance with every regime.

    What that was worth

    On this catalogue the missing darker-image condition alone produced 13 false positives, renditions reported as general flashes where both states were above the 0.80 ceiling the published test sets. The sweep returns 44 with the condition and 57 without it. The corpus now carries a bright-on-bright control cohort so the condition cannot quietly go missing again.

    This audit is not equivalent to expert review, and it does not establish efficacy. It is written so it can be checked: every threshold above quotes the source. Safe Frame is an open pre-check implementing published criteria. It is not certified, not broadcaster-approved, and not a medical device. It does not replace certified delivery testing.

    What this is built from

    Every component, and which ones are answering right now.

    Read live when this section loads. A dot is only green when something actually answered: ClickHouse returning its own version string, the official MCP server listing its tools, Vertex completing a round trip. Parts that build or verify the project but are not in the request path say so instead of borrowing a green light.

    Nothing here is hardcoded.
    The long version

    Press the button to query each component.

    Runtime evidence

    The partner integration, checked from this page.

    Every catalogue read and every verdict on this page went through the official ClickHouse/mcp-clickhouse server in read-only stdio mode, against a self-hosted ClickHouse 26.3 LTS cluster. You do not have to take that from the README.

    Raw JSON Health

    Not checked in this browser session.

    Cloud RunFastAPI on a dedicated runtime identitysafe-frame-runtime
    Google ADK · GeminiTwo agents on Vertex AI. Orchestrate and explain; never decide.gemini-2.5-flash
    Official MCP · read-onlystdio child process. Receives ClickHouse credentials only: it cannot reach Google credentials.CLICKHOUSE_ALLOW_WRITE_ACCESS=false
    ClickHouse 26.3 LTSSelf-hosted behind HTTPS. A SELECT-only user for MCP, a separate user for ingest.safe_frame_mcp · safe_frame_ingest

    The dashed box is the credential boundary. Secrets come from Secret Manager, and the two database identities are distinct so a read path cannot write.

    Decision boundary

    Arithmetic owns the verdict.

    The AI cannot declare a video safe. It explains rows the database returned, and asks a human QC operator to act.

    If the MCP query fails, or its rows cannot be parsed, the endpoint returns 502. It never substitutes Gemini or a local guess in the live path.

    tests/test_sql_parity.py runs the reference Python detector and this SQL over identical randomised inputs, for both rules, and requires exact agreement, so the catalogue sweep cannot silently drift from the per-file pre-check.

    Each rule is windowed over its own qualifying transitions, so one rule can never pad the other’s count, and the isolation step is keyed on rule: a master that already flashed in luminance does not excuse a rendition that introduced a red flash.

    -- each rule gets its own qualifying set
    WHERE luma_delta >= 0.10
      AND luma_min < 0.80 -- the darker image
      AND changed_area_fraction >= 0.25
      AND direction != 'flat'

    WHERE red_delta >= 0.20 -- no luma floor
      AND changed_area_fraction >= 0.25
      AND direction != 'flat'

    WINDOW w AS (PARTITION BY asset_id
      ORDER BY pts_ms
      RANGE BETWEEN CURRENT ROW AND 999 FOLLOWING)

    WHERE win_transitions > 6
      AND win_dir_min != win_dir_max

    decision_source = clickhouse_sql_via_official_mcp