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)
  1. 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).
  2. Phase 2 — Correction. Violations get rewritten out of the prompt before generation ever happens.
  3. 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.
  4. 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-lite by default) and audio TTS. Its generation manifest, including a canonical_hash for the produced asset, is bound directly into the sealed ComplianceBundle as 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 returns ABSTAIN instead 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 .sha256 sidecars. 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 reporting prohibited fails the gate; disagreement or a missing detector produces ABSTAIN rather than a false PASS, 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.py runs 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.py is 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, so 1, "1", True, and Fraction(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.md and 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/

Built With

Share this project:

Updates