Sonya — A Notary for Generative Media
From prompt to pixels: audited, sealed, and independently verifiable.
Inspiration
Every "safe image" pipeline we looked at stops at the prompt. A user asks for an image, the request gets rewritten to strip out anything prohibited, and the corrected prompt is handed to the model. From that point on, the system just trusts the model to obey.
It doesn't. A diffusion model can take a perfectly clean, corrected prompt — "family having dinner, no alcohol" — and still render a wine bottle into the background. The prompt-only pipeline never notices, because it never looks at what actually came out. It checked the words, not the picture.
That gap is the whole idea behind Sonya. If a brand, a regulated advertiser, or a stock-media marketplace is going to ship a generated asset, "the prompt told it not to" is not a defensible answer when the pixels say otherwise. Someone needs to check the output, not just the request — and then be able to prove, to someone who doesn't trust them, that the check actually happened.
What it does
Sonya is a compliance notary that sits around a generative-media pipeline. It doesn't generate images or audio itself — it audits the request, watches the generation, audits the result, and seals everything into a tamper-evident record that a completely independent tool can re-verify.
For every job, Sonya runs a four-phase pipeline:
prompt ─▶ Phase 1 audit ─▶ Phase 2 correction ─▶ Phase 3 generation ─▶ Phase 4 visual audit
(policy_engine.py) (media_pipeline.py) (visual_audit.py)
│
prompt_gate ◀──────────── engine verdict │ visual_gate
└──────▶ ComplianceBundle ◀──────┘ (deterministic fusion)
(sealed) ─▶ ProvenanceChain ─▶ Backblaze B2
│
verify_sonya.py (independent, stdlib-only)
- Phase 1 — Prompt audit. The incoming prompt is checked against a cultural/regional compliance profile (alcohol, religious symbols, minors, currency, and more, depending on the market).
- Phase 2 — Correction. Violations get rewritten out of the prompt before generation ever happens.
- Phase 3 — Generation. The corrected prompt goes to Genblaze (GMI Cloud), which generates the image or audio asset and returns a manifest with a declared content hash.
- Phase 4 — Visual audit. Sonya re-checks the generated pixels — independently of the prompt — against the same rule set, using a vision model as a detector.
The prompt-side verdict and the pixel-side verdict are two separate gates.
Neither one can override the other — the final verdict (COMPLIANT /
CONDITIONAL / NON_COMPLIANT / ABSTAIN) is a pure function of both, computed
by plain categorical branching with no LLM, no floating point, and no model
call in the decision path itself. If the output contains a prohibited
element, the job is NON_COMPLIANT even if the prompt was clean — that's the
exact case a prompt-only pipeline cannot catch, and it's covered end-to-end by
tests/test_notary.py::test_prohibited_output_forces_non_compliant and by
demo/notarize_demo.py.
Every job produces one ComplianceBundle: five independent sub-hashes
(evidence, audit, generation, visual, verdict) plus a master seal, so a
retroactive edit to any single part is localizable, not just detectable.
Bundles are chained together with a ProvenanceChain, so tampering with one
sealed job breaks every entry after it — a chain of custody, not a single
stamp on a single file.
And critically: you don't have to trust Sonya to trust the result.
verify_sonya.py re-implements the canonical encoder, the verdict logic, and
the hashing protocol from scratch, using only the Python standard library. It
imports nothing from the rest of the codebase. Point it at a sealed bundle
and it tells you, independently, whether the seal is intact.
How we built it, and how B2 and Genblaze fit in
Genblaze generates. Backblaze B2 stores the evidence.
- Genblaze / GMI Cloud does the actual generative work in
core/media_pipeline.py— image generation (seedream-5.0-liteby default) and audio TTS. Its generation manifest, including acanonical_hashfor the produced asset, is bound directly into the sealedComplianceBundleas generation provenance. - Before that generated asset is handed to the Phase 4 visual audit,
image_bytes_for_audit()recomputes its hash and compares it to the hash Genblaze declared. If they don't match — or the declared hash is missing — Sonya refuses to audit it and returnsABSTAINinstead of silently certifying an asset that may have been swapped out from under it after generation. - Backblaze B2 (via its S3-compatible API,
core/b2_client.py) is the durable store for cultural compliance profiles, sealed compliance reports,ComplianceBundles, provenance-chain exports, and.sha256sidecars. Every object carries its canonical seal in object metadata and is re-verified on read, not just on write — so a bit-flip anywhere in the store, not just at ingestion, gets caught. - The visual detector for Phase 4 is a vision model via GMI Cloud
(default
Qwen2.5-VL-7B-Instruct,temperature=0), optionally cross-checked with a local CLIP model. Detectors only emit categorical observations (clean/prohibited/uncertain) — they never decide the verdict themselves. Fusion across detectors is danger-dominant and honest-degrading: any detector reportingprohibitedfails the gate; disagreement or a missing detector producesABSTAINrather than a falsePASS, because one detector agreeing with itself isn't verification. - Prompt audit and correction (Phases 1–2) use an injected LLM backend (GMI
Cloud, Ollama, or Anthropic), also at
temperature=0, and also never in the decision path — it proposes corrections; it doesn't decide compliance.
The whole thing is exposed as a small FastAPI service (api/app.py) with a
real generation endpoint, POST /localize, that runs all four phases against
live Genblaze/B2 and returns the notary's verdict — not a prompt-only stand-in.
There's also POST /demo/notarize, an offline walkthrough endpoint for
reviewers without cloud credentials.
Challenges we ran into
The hardest part wasn't calling a generation API — it was making the
absence of information honest. A visual detector can fail to load, two
detectors can disagree, or a hash can not match what Genblaze declared. In
every one of those cases it would have been easy to default to COMPLIANT
("nothing flagged it") and quietly ship a false pass. We deliberately built
the fusion logic to be danger-dominant: any real signal of a violation wins,
and any missing signal degrades to ABSTAIN, never to PASS. That
discipline shows up everywhere — from the detector fusion, to refusing to
audit an asset whose hash doesn't match what generation declared, to the
verifier script sharing zero code with the producer so it can't inherit the
producer's bugs.
The other real challenge was being honest about what a hash chain actually
proves. A tamper-evident seal proves integrity — that nothing changed after
sealing — not authenticity by itself, since whoever can write to the store
and knows the hash formula could in principle re-seal a forged chain forward.
We added optional HMAC signing (SONYA_HMAC_KEY) to close that gap when a
held secret is available, and we say plainly in the README where the
boundary is when it isn't.
Accomplishments we're proud of
- The prompt/pixel disagreement is real and tested, not just described in
a README:
demo/notarize_demo.pyruns it live, offline, in front of a reviewer. - 74 passing tests, all reproducible from a clean checkout with no cloud
credentials (
python3 -m pytest tests/ -q). - A verifier that owes the producer nothing.
verify_sonya.pyis stdlib-only and imports nothing from Sonya — a reviewer doesn't have to believe our seal, they can run their own check against it. - Determinism, proven, not asserted. The canonical hashing protocol
(
core/canonical.py) is type-tagged and rejects floats in the sealed path, so1,"1",True, andFraction(1,1)seal to provably distinct bytes, and re-parsed JSON always hashes identically regardless of whitespace or key order. - We wrote down what we can't prove, in
KNOWN_LIMITATIONS.mdand the README's own "Scope and limitations" section — detector accuracy isn't established here, B2 Object Lock (WORM) isn't wired up yet, and live GMI/B2 paths are structured but only exercised offline in this repo's automated checks. We'd rather a judge find that in our own words than discover it themselves.
What we learned
Trust in a generative pipeline isn't a single checkbox — it's the disagreement between two independent checks that catches what either one alone would miss. A prompt-only system and a pixel-only system each have blind spots; running both, refusing to let either override the other, and sealing the disagreement itself turned out to be more valuable than trying to make either check "smarter." We also learned that the most persuasive thing you can hand a skeptical reviewer isn't a claim — it's a script they can run themselves that shares no code with yours.
What's next for Sonya
- B2 Object Lock (WORM) for true immutable retention, once tested against a live Object-Lock-enabled bucket.
- Live end-to-end runs against production GMI/B2, beyond the current mocked/offline test and demo coverage.
- Measured detector accuracy — false-negative/false-positive rates for the vision detectors, published alongside the determinism guarantees that are already test-backed, so "COMPLIANT" can eventually mean more than "no configured detector flagged it."
- More cultural/regional profiles, expanding beyond the current rule set so more markets can get a per-asset compliance record out of the box.
Try it yourself
No cloud credentials required:
python3 -m pytest tests/ -q # 74 tests
python3 demo/notarize_demo.py # end-to-end walkthrough, offline
python3 verify_sonya.py demo_out/clean_bundle.json # independent verification
Built With
python · fastapi · backblaze-b2 (S3-compatible API) · genblaze ·
gmi-cloud · qwen2.5-vl · clip · anthropic · ollama · pytest
License
Apache 2.0.
https://github.com/annatchijova/sonya/ https://sonya-replay-deploy.vercel.app/

Log in or sign up for Devpost to join the conversation.