I measured C2PA coverage on 1,791 AI-generated assets. It was 2.8% — and that's not a C2PA failure.
I have a media pipeline on my own machine that generates images and video with diffusion models. Last week I tried to answer a question about one of its outputs that I assumed was easy: what made this file? Not roughly — exactly. Which model, which seed, which prompt, which earlier file it came out of. I could not answer it for that file, and when I built a tool to check, it turned out I could not answer it for almost any of them. This is what the measurement looked like, and why I think the interesting number is not the one I expected.
The experiment
I wrote a content-addressed provenance registry. Every asset is identified by the SHA-256 of its bytes rather than by its path, generation metadata is recorded against that hash, derivations form a cycle-checked DAG, and the graph exports to an OpenUSD stage. Then I pointed it at the pipeline's actual output directory, read-only, and let it walk everything.
files scanned 1943 assets registered 1791 duplicate bytes 152 derivations recorded 934 bytes hashed 13.9 GB errors 0 wall clock 110.0s
One command, 110 seconds, 13.9 GB, no errors. That corpus is the whole experiment: real files from a pipeline I actually run, not a synthetic fixture built to make a point.
Finding one: the pipeline cannot say what made its own output
Of 1,791 registered assets, 0 had a recoverable model. 60 had a seed. 48 had a prompt. 61 had a measured generation time. That is not a bug in the registry — the registry recorded exactly what was on disk. The model name, step count and guidance scale for a given frame exist only in the config object loaded at render time. Nothing writes them next to the file, so once the process exits they are gone.
The registry stores that as NULL and tags every asset with a metadata_source field saying where its metadata came from, including the honest labels: convention: for lineage I inferred from filename pairing, path: for media with no metadata on disk at all. Refusing to guess a plausible model name is the whole point. An invented provenance record is worse than a blank one.
Finding two: 50 of 1,791 files carry a C2PA manifest
The pipeline embeds C2PA Content Credentials, so I wrote a stdlib reader for the three containers involved — JPEG APP11 segments, the PNG caBX chunk, and the ISO-BMFF top-level uuid box — and re-read all 1,791 files off disk.
files scanned 1791
with a manifest 50
unreadable 0
file missing 0
wall clock 92.2s
claim generators:
50 shorts-factory 1.0.0
50 of 1,791 — 2.8%. 36 stills, 14 clips, one claim generator, zero parse errors. Every manifest that existed was well-formed. And every one of the 50 came from exactly two of the pipeline's output paths: the two that produce posting masters. Nothing else in the pipeline embeds anything, because nothing else in the pipeline publishes.
All 50 carry a claim-signature box. I want to be precise about what I did not do: I did not validate a single one. No COSE check, no certificate chain, no trust list, no hard-binding comparison. The registry stores c2pa_signature_validated as NULL for every row and the USD export writes not-attempted, because a column that could be misread as "verified" is a liability. A manifest is a claim. An unvalidated claim is evidence of intent, not of truth.
Finding three: content addressing sees things paths cannot
152 files were byte-identical to something already registered, and 56 assets turned out to have more than one parent. The memorable one: a single review poster frame was the exact same bytes as the poster for six other videos.
$ apr show 00ed4b0fe4d4 derived from 1e3e0bdc6cd9 [thumbnail:poster-frame] 75c1d850eb9b [thumbnail:poster-frame] ... (7 total)
Seven different videos, one identical thumbnail. Path-based tracking sees seven separate files and notices nothing at all. Content addressing collapses them into one asset with seven parents, and the anomaly becomes visible for free — I did not go looking for it.
Why 2.8% is the right number for C2PA to produce
The easy read of "2.8%" is that C2PA has an adoption problem. I don't think that's what this measures. C2PA is a standard for asserting provenance on content that ships. It is attached at export, by the step that publishes, and on this corpus it did that job with a 100% success rate on the files it was wired into. The 1,741 files without a manifest were never published. Nothing in the standard promised to cover them.
So the two mechanisms cover disjoint ground. C2PA tracks what shipped. An internal content-addressed registry tracks what was made. The 50 files that can vouch for themselves are the ones already published; for the other 1,741, an external registry keyed on content hash is the only record that exists. Neither makes the other redundant, and both together still leave the intermediates dark — the raw clip that got upscaled, the frame that became a thumbnail, the plate that six variants branched from. That gap is not owned by anybody. It is not a standards problem; it is a plumbing problem, and it lives inside each pipeline.
What I'd actually do about it
Capture at generation time, not at export time. The moment a sampler node runs is the only moment the model hash, seed, scheduler and prompt all exist in one place; every later step is archaeology. My companion project, comfy-workflow-pack, emits a provenance record at parameter-bind time for exactly that reason, and the resulting render lands in this registry with a real model and a real seed instead of two nulls.
Limitations, stated plainly
This is n=1. One pipeline, mine, at personal scale, with its own idiosyncratic habits about where it writes metadata — a studio running ComfyUI with PNG metadata intact would measure very differently, and probably much better. I read C2PA manifests, I never validated a signature, so nothing here says anything about whether those 50 claims are trustworthy. Only JPEG, PNG and ISO-BMFF containers are parsed; a manifest in a WebP or a sidecar manifest store would have been missed and counted as absent. And the lineage in some parts of the corpus was inferred from filename conventions rather than recorded, which the registry labels but which is still an inference.
The numbers above are the ones I got. If you run something similar on a real pipeline and get a different shape, I would genuinely like to see it — and if you think the disjointness argument is wrong, the whole scan methodology is written up in the repo so you can attack the specific step you think is bad.