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 2022ClickHouse 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.
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.
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 incidentof 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.1Standard 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.
Ready, press “Sweep the catalogue”.
| Title | Rendition | Rule | Introduced by | Window | Transitions | Peak area |
|---|---|---|---|---|---|---|
No sweep has been run in this browser session. | ||||||
Qualifying transitions per second
Approved master above, shipped rendition below, one shared scale.
Select a rendition above.
View this chart as a table
| Second | Master, general | Master, red | Rendition, general | Rendition, red |
|---|
Selected regression
introduced violation
- Approved master
- n/a
- Rendition
- n/a
- Rule
- n/a
- Introduced by
- n/a
- Window
- n/a
- Transitions
- n/a
- Peak area
- n/a
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.
- Precision
- n/a
- Recall
- n/a
- Ground truth
- n/a
- Detector
- n/a
| Cohort | Where the numbers came from | Planted | Found | Precision | Recall |
|---|
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.
-
rendition-flash.webm flashes
A full-screen luminance alternation, about twelve per second for two seconds, from one second in.
-
rendition-red.webm flashes
A saturated-red alternation at matched luminance, the case a luminance-only checker passes. Same rate, same two seconds.
- 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.
Choose a video to check it against the published criteria.
- Findings
- n/a
- Transitions measured
- n/a
- Decision
- n/a
- Measured by
- n/a
This is an open pre-check against published criteria, not a certified or medical device, and it always requires human review.
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.
n/a
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 tested | Threshold in Safe Frame | Published 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.
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.
Not checked in this browser session.
- Transport
- n/a
- Write access
- n/a
- Tools advertised
Result of a live query run through that server just now:
safe-frame-runtimegemini-2.5-flashCLICKHOUSE_ALLOW_WRITE_ACCESS=falsesafe_frame_mcp · safe_frame_ingestThe 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.
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