An earlier version of this product read the EXIF `Software` tag out of a JPEG and substring-matched it against a list of tokens. Two of the tokens were `"gpt"` and `"dream"`. Any hit forced the AI likelihood to at least 90, reported at "Very High" confidence.
The failure modes are legible in the tokens themselves. `"gpt"` is a substring of ordinary strings. "Dream" ships in consumer camera firmware. And the tag being matched is unsigned plain text: any editor writes it, any uploader strips it. `inference/app/engines/image_ladder.py`, the file that replaced that engine, describes what we had built as a catastrophic false-positive generator wearing a high-confidence label.
It is gone, and its absence is pinned by a regression test named `test_exif_software_can_never_decide_anything`. The test runs two files through the image lane — one JPEG whose `Software` tag reads "Midjourney 6", one with no tag at all — and requires the same rungs to report the same statuses, none of them fired, and the string "Midjourney" to appear in no rung's summary.
The provenance rung states the principle in one line of comment: inferring "AI" from a software name would be a guess dressed as provenance. We shipped that guess and ran it. So when the rest of this piece says metadata is weaker evidence than people assume, the people include us.
What are EXIF, XMP and container metadata?
They are three separate places a file keeps statements about itself, and none of them is signed. Each was designed to carry useful bookkeeping between programs, not to survive an adversary.
**EXIF** lives in a JPEG's `APP1` segment, marked by the six bytes `Exif\0\0` and structured as a TIFF file: a byte-order marker (`II` for little-endian, `MM` for big), then a chain of image file directories. IFD0 holds the file's top-level tags — make, model, `Software` — and pointers out to more. Its next-directory pointer leads to IFD1, which holds an embedded thumbnail. Our forensics rung walks exactly that path and reads exactly one tag from it, `0x0202`, the thumbnail's byte length.
**XMP** is an XML packet, also carried in `APP1`, introduced by the namespace string `http://ns.adobe.com/xap/1.0/`. It is a general-purpose container for RDF properties, so anything can go in it — including a URL: C2PA uses the `dcterms:provenance` property to point at a manifest stored outside the file. That matters beyond provenance: a parser following that pointer fetches a URL chosen by whoever supplied the bytes, which we confirmed by experiment turns a detection endpoint into a server-side request forgery primitive. Our reader runs with `remote_manifest_fetch: False`, and a file referencing a remote manifest is reported as a rung that did not run, never as a file with nothing in it.
**Container metadata** in MP4 and MOV is not tags but boxes: a nested tree in which `moov/udta/meta/ilst` holds the writer's own notes. `(c)too` and `(c)swr` name the tool and the software; `mvhd` and the track headers carry creation and modification times; `hdlr` boxes carry track handler names; `ftyp` declares the container brand. PNG has its own version: `tEXt`, `iTXt` and `zTXt` chunks, keyword-and-value pairs with no more structure than that.
What does a metadata field actually prove?
It proves what the last program to write the file chose to say about itself. That is two separate limits, and our video lane prints whichever one applies beside every observation it makes, so a reader can discount a finding without having to trust a caveat buried elsewhere in the report. Verbatim, they read:
> Attributes the last program to write this file, not the origin of the footage. Any transcode - including every upload to a social platform - replaces it.
> Self-reported by the writer and trivially editable. Corroborating, never deciding.
The first limit is the one people underestimate. On a still image the last encoder is often the only encoder, so the writer's signature carries real weight — that is why header ballistics work at all. Kee, Johnson and Farid, in "Digital Image Authentication from JPEG Headers" (IEEE TIFS 6(3), 2011), measured a header signature across 1.3 million images spanning 773 different camera and cellphone models: 62% of images had a signature unique to a single camera, 80% one shared by three or fewer cameras, 99% one unique to a single manufacturer. Those are their corpus statistics for the technique, not a claim about our implementation, which has measured nothing of the kind. And note what the technique attributes at its best: the device that encoded the file. Origin is a different question.
On video the last writer almost never is the only writer. The file has passed through an editor, an exporter and a platform transcoder, each rewriting the container wholesale. So `_forensics_finding` in the video ladder is capped at `clear` — it publishes its observations in full and decides nothing — and its docstring says why: a `(c)too` string naming a generative tool is evidence that a generative tool wrote the container, which is a claim the container makes about itself, in a field anyone can edit with a hex editor.
The second limit closes the remaining escape route. Suppose a file's metadata matched a camera perfectly: a JPEG quantisation table is 64 bytes, and copying a camera's table onto an AI image is a mechanical operation. The rule at the top of this system is that a `human` verdict requires positive provenance of these exact bytes, and a field that can be typed cannot supply it.
Do AI generators label their output in their metadata?
Sometimes, in the image tools, and mostly not in the video ones. Either way it is not something to build on, because the signal is worthless in the direction that would matter.
We surveyed this directly, testing a proposal to map known writer strings to known generators. **0 of 6 generators self-identified in container metadata**, across the six generated clips in a nine-video corpus (three camera captures, six generators: Google's generative pipeline, Kling, Pika, Mochi 1, CogVideoX-2B and CogVideoX-5B). Six is a small number and this is one measurement, not a coverage rate. The reason it went that way is structural, though, and does generalise: what a detector reads is the last stage of the pipeline, and for anything that reached you through a dataset, an asset host or a social platform, the last stage is that pipeline rather than the generator.
Two details from that survey say more than the headline. A single `ffmpeg -c copy` — a stream copy that does not touch a pixel — replaced the Truepic test file's writer string and destroyed its C2PA box outright. And the one string in the whole corpus that named a vendor outright, `(c)too` reading `"Google"`, sat on the file whose signed manifest had already settled the question cryptographically. The metadata was redundant precisely where it was correct.
The image side genuinely does write labels. Generator front-ends leave PNG text chunks under keywords like `parameters`, `prompt`, `workflow`, `invokeai_metadata` and `comfy`. The forensics rung matches those as exact keywords rather than substrings — the distinction the retired `Software` matcher failed — and records a hit as an observation that fires nothing, alongside its precondition: PNG text chunks are unsigned plain text that any tool can write, copy or strip, so their presence is not proof of generation and their absence is not evidence of anything.
That asymmetry is the whole practical lesson. A generator tag found is weak evidence, because it is trivially forgeable in both directions. A generator tag absent is not evidence at all, and absence is the common case.
The two ways MP4 stores metadata, and what reading one of them cost us
MP4 and MOV have two different encodings for the same metadata, and a reader that handles only one will silently publish half of what a file says. Ours did, for a while.
In the first form the field name *is* the box type. QuickTime's convention prefixes such types with byte `0xA9`, which is why they are written `(c)too` and `(c)swr`. Name and value arrive together.
In the second form they are split across two boxes. `moov/meta/keys` holds an ordered list of names, and the matching `ilst` children are named by their 1-based index into that list — so a metadata item's four-character box type is literally the bytes `\x00\x00\x00\x01`. Read one box without the other and you have values with no names, or names with no values.
Missing the second form was not cosmetic. Measured across the video corpus, two strings reachable only through it were invisible to us: a Kling clip's `encoder`, and, on the camera side, an Android capture's `com.android.version`.
The same parser carried three further bugs worth naming, because each one published something false under the word "verbatim".
- The `keys` list was taken globally rather than per container, so the first `keys` box in a file renamed every field in it. On a real capture, the `encoder` value was published under the name `com.apple.quicktime.make`, and the true encoder attribution was dropped. Names and values must come from the same container or they are not a pair.
- An empty `data` box is exactly 16 bytes, so a `> 16` length test failed, the header was never stripped, and the box's own structure bytes were decoded as the value: the rung reported `encoder: 'data'` for a file whose encoder tag was blank.
- The type indicator was ignored, so a numeric payload was run through a UTF-8 decode with errors suppressed. A real file whose encoder field reads `30.000000` was published as `'A'` — byte `0x41`, the one character left standing after the invalid sequences were dropped.
The rule the parser now follows is that a field this build cannot decode is a field it does not report. One more of the same family: `meta` is a FullBox in ISO-BMFF and a bare container in QuickTime, four bytes of difference, and guessing wrong silently loses every `udta` string in the file. The walker peeks rather than assuming.
What is metadata actually good for?
Corroboration and timeline. It is good for checking whether a story about a file hangs together, and never good for authorship.
Timeline first. The movie header and each track header carry creation and modification times; a modification time later than the creation time says the file was rewritten after it was made. They are counted from the ISO-BMFF epoch of 1 January 1904, which is 2,082,844,800 seconds before the Unix epoch — worth knowing because a reader that gets it wrong produces dates wrong by 66 years rather than obviously absent. The self-reported precondition applies to every one of them.
Then handling. Whether `moov` sits before or after `mdat` distinguishes a progressive-download layout, written or rewritten by a faststart pass, from the layout a recorder writes as it captures. Cameras almost always embed an EXIF IFD1 thumbnail and most re-encoders drop it, so its absence is a fact about the last writer. Neither says anything about the content, but a file presented as an untouched camera original with a faststart layout and no thumbnail has a gap in its story.
That is the shape of every legitimate use. Metadata gives you several independent fields describing the same pipeline, and the value is in whether they agree. A set that contradicts the account you were given is a lead worth pulling. A set that supports it proves nothing: a coherent set is exactly what a forger produces, because making the fields agree takes no work.
What separates a checkable record from a field that merely says something is a hard binding: a cryptographic hash over the file's actual bytes, stored inside the record and signed. Change one byte and the hash stops matching. A C2PA manifest carries one; EXIF, XMP and `ilst` do not, which is why a signed manifest is the only thing in this product permitted to say a file came off a camera. Everything else here is a program's note to itself — worth reading, worth publishing, and never a fact about where a picture came from.