Grading
Read this if you hold a
detectresult and need to know what it shows, or you are choosing decode settings and want the measured error rates. You will read one real result line by line, then find the rule behind any grade, status or caveat. Skip if you only want to rundetect(Quickstart). Time: 10 minutes for the walkthrough; the rest is reference. Needs: nothing to read;pip install "laidline[image]"and the models to run the walkthrough's commands.
In short
- A result is a verdict, an overall grade (the best signal's), one entry per signal, caveats, and an evidence entry when the file resolves to your log.
- A means a trusted manifest with a timestamp. B means confirmed against your evidence log. C means a mark naming your issuer that nothing confirms (never an unkeyed audio mark: it shows presence only, D). D means present but unverified, foreign or shared.
nonemeans nothing found. - A decoded payload alone is never grade B: decoders emit phantom payloads, so B needs your key tag, a registry entry and the file's exact bytes or a matching fingerprint.
noneand an absent signal say nothing about origin; a result never says human-made, authentic or not AI.- Text is a statistical test and never grades A; audio alone never grades A.
detect returns laidline.detect/1: an overall verdict and grade, one signal per check, the caveats, and the
matching evidence entry when the asset resolves locally. A grade of none means no signal was found. It is not a
statement about origin. The response never says "human-made", "authentic" or "not AI".
Reading a result
You read a result in five passes: the verdict, the overall grade, each signal, the caveats and the evidence entry. This section makes the five passes once, on a real result, line by line. The sections after it hold the rules behind every grade, status and caveat you meet here.
Make the result
The result below comes from a deployment started with laidline init --system-name my-image-model --org "Example Org",
with watermark.issuer_id set to 65300, a number from the reserved range that documentation uses
(Licensing). A deployment you have just initialised draws a trial number instead,
and the range field below then reads trial. Mark an image:
laidline mark photo.png --out marked.png
Expected output:
{
"out": "marked.png",
"asset_id": "ast_01M5VGW1RBJDK2F3N17Q1WKY4K",
"manifest_id": "urn:c2pa:24a2a7f8-72bd-4c98-bb0a-b7df010bd3a9",
"entry_hash": "39add78633ae38ae8ea21b30343375dc7c1d66efbb435a094b78ef95eb8d3b20",
"warnings": []
}
What just happened: Laidline wrote Content Credentials signed with your key, hid a watermark in the pixels, and
appended one entry to your evidence log. entry_hash names that entry. The first mark loads the models and takes a
minute or more on a CPU.
Now read the file back:
laidline detect marked.png
Expected output:
verdict: marked_by_this_system grade: A
c2pa present=True grade=A status=valid_trusted
watermark present=True grade=D status=shared mark
issuer: reserved issuer number (documentation or test value)
note: Absence of a signal is not evidence that content is human-made.
note: Revocation status was not checked (OCSP disabled in this deployment).
What just happened: the command printed the headline of the result. The full result is the same call with --json,
and POST /detect returns it too (HTTP API). The rest of this section reads that JSON in order. Two
values in it are replaced here: the machine name in log_stream and stream, and the Laidline version number.
laidline detect marked.png --json
Expected output: the JSON that the passes below show in pieces, in order.
Pass 1: the verdict and the grade
{ "schema": "laidline.detect/1", "asset": { "sha256": "5e2328eebc62a3200a7f9e23992a8ee5f16b1ba66ad0a47e181a0b5011d07c69", "mime": "image/png", "bytes": 2215042 }, "verdict": "marked_by_this_system", "grade": "A", "signals": [ ...
schemanames the format,laidline.detect/1. A consumer should check it before it reads anything else.assetdescribes the file you sent: its SHA-256, its type and its size in bytes. The result is about exactly these bytes.verdictis the one-word answer (Verdicts lists all five).marked_by_this_systemhere means your own trusted Content Credentials were found and verify for exactly this file.gradeis the overall grade, the best grade among the signals (Per-signal grades). Here it is A because the Content Credentials are A. The watermark signal below is D, and that does not lower the overall grade.
Pass 2: the Content Credentials signal
{ "type": "c2pa", "present": true, "status": "valid_trusted", "grade": "A", "validator": { "name": "c2pa-rs", "version": "0.90.19", "trust_anchors": "own" }, "manifest_id": "urn:c2pa:24a2a7f8-72bd-4c98-bb0a-b7df010bd3a9", "claim_generator": { "name": "my-image-model", "version": "1.0", "com.laidline.version": "1.0.0", "org.contentauth.c2pa_rs": "0.90.19" }, "signed_at": "2026-10-26T07:13:53+00:00", "cert": { "common_name": "my-image-model dev", "issuer": "Example Org", "serial": "192149098549047973573199500531996850574032082158", "trusted": true, "trusted_by": "own_anchor", "revocation": "not_checked" }, "digital_source_type": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", "soft_binding": { "alg": "com.laidline.pixelseal.i77-r1", "value": "0bfc508f734f2291ac06" }, "failure_codes": [], "laidline": { "asset_id": "ast_01M5VGW1RBJDK2F3N17Q1WKY4K", "log_stream": "host-1c28f5" } }
status: valid_trustedmeans the signature verifies and the certificate chains to an anchor you trust. The other states arevalid_untrusted(the signature verifies, the certificate is unknown),invalidandabsent.validatornames the checker.trust_anchors: ownmeans the anchor was your own; it readsc2pa-trust-list+ownwhendetect.trust_anchors_pathloads the public list (Certificates).claim_generatoris the software that signed.nameandversioncome fromsystem.nameandsystem.versionin your configuration;com.laidline.versionis the Laidline version that signed.signed_atis the time from the manifest's RFC 3161 timestamp. Grade A needs it. Without it a trusted manifest grades B.certdescribes the signing certificate.trusted_by: own_anchormeans it chains to your anchor, the development CA thatlaidline initmade.revocation: not_checkedis the OCSP caveat that every result repeats.digital_source_typeis the manifest's declaration that a trained model made the content.soft_bindingnames the watermark the manifest says is in the pixels. Itsvalueequalspayload.hexin the next signal (Soft-binding names).failure_codesis empty because the validator found nothing wrong.laidline.asset_idandlog_streamcome from the Laidline assertion inside the manifest. They are howevidence_logat the end finds your entry, and only a manifest that chains to your own anchor is trusted to name one.
Pass 3: the watermark signal
The watermark signal is the long one. Its first fields say what was read:
{ "type": "watermark", "scheme": "pixelseal", "present": true, "grade": "D", "status": "confirmed_shared_key", "key_family": "shared", "presence": true, "presence_basis": "pixelseal_score_gate", "payload": { "payload_layout": "pixelseal-i77-r1", "version": 2, "issuer_id": 65300, "asset_seq": 150418674, "hex": "0bfc508f734f2291ac06" }, "matched_by": null, "confirmation_scope": null, "region": null, "query_size": [ 1024, 1024 ], "asset_ids": [], "unconfirmed_asset_ids": [], "registry_match": null, "other_payload_unchecked": [], ...
schemeis the watermark that read the file, andpresentsays it found one.grade: Dwithstatus: confirmed_shared_keyis the line to understand. The key tag in the payload verified under the shared pool key, so Laidline knows this is a Laidline mark, but a shared mark names no provider: anyone can make one. It grades D whatever your evidence log holds (Keyed image payloads, the shared family). The keylaidline initwrote is inert until you register.presenceandpresence_basissay why the signal counts as present: the decoder's score cleared the PixelSeal gate.payloadis what was decoded.issuer_idis the issuer number the mark claims, andasset_seqis the asset's 36-bit sequence number within that issuer.hexis the same bits assoft_binding.value. A decoded payload is a claim, not a finding: anyone with the public weights can write any issuer number (Why a payload alone is not evidence).matched_by,confirmation_scope,region,asset_idsandregistry_matchare filled in only at grade B, so they are empty here. At B they say how the file was tied to a registered asset and which one.unconfirmed_asset_idslists registry candidates that failed confirmation, andother_payload_uncheckedlists other payloads the read found.query_sizeis the size of the image that was read.
The next fields bound the chance that the read was an accident:
... "false_accept_bound_per_key": { "event": "own_accept", "keys": 1, "unit": "read", "per_read_log10": -14.64, "reads": 4, "per_image_log10": -14.04 }, "false_accept_bound": { "event": "own_accept", "keys": 2, "unit": "read", "per_read_log10": -14.34, "reads": 4, "per_image_log10": -13.74 }, "caveats": [ "shared mark: a Laidline mark is confirmed; the provider is not identified (shared key)", "Own-accept bound (per live key): under a random-word model an unmarked image yields a payload accepted as this deployment's (version, issuer and key tag) with probability at most 10^-14.6 per read and 10^-14.0 over 4 reads. This bounds a chance acceptance only: a copy of a genuine mark carries a valid tag, and grade B needs the registered asset itself (its exact bytes, or its perceptual hash within the confirmation distance)." ], "tier_caveats": [ "claimed issuer 65300: the payload names this issuer number; anyone with the public weights can write any issuer, and only grade B ties this file to a registered asset of that issuer", "Own-accept bound (with 2 live keys): under a random-word model an unmarked image yields a payload accepted as this deployment's (version, issuer and key tag) with probability at most 10^-14.3 per read and 10^-13.7 over 4 reads. This bounds a chance acceptance only: a copy of a genuine mark carries a valid tag, and grade B needs the registered asset itself (its exact bytes, or its perceptual hash within the confirmation distance)." ], ...
false_accept_bound_per_keyis the largest chance that an unmarked image yields a payload accepted as yours, per read and over thereadsthe call made, as a base-10 logarithm: -14.04 is about 10^-14 over 4 reads.false_accept_boundis the same figure at the number of live keys (2 here: your key and the shared pool key). It bounds a chance acceptance only. It says nothing about a forger who copies a genuine mark.caveatsholds the sentences that follow from the payload itself: the mark is shared and the bound above in words.tier_caveatsholds the full-tier extras, among them "claimed issuer N" for a payload whose tag verified and whose grade is not B. A keyed signal names no issuer unless its tag verified (or a registry-only issuer's records confirmed it): a payload that failed its tag, however well it reads, is not shown with an issuer on any tier, and the public tier shows a payload only at grade B (every other present signal ends itscaveatswith "a mark is present; its issuer could not be read").
The last fields are the checks that produced the grade:
... "tag": "verified", "key_id": "pool-0b19a1bc728c92e5", "key_fp": "0cdd37786df4a62a", "key_state": "active", "registry": "hit", "registry_entries": 1, "live_keys": 2, "fingerprint": { "alg": "phash64/v1", "view": "direct", "hex": "daa58d0da41fa666", "distance": 0, "matched_by": "bytes" }, "decode_path": "direct", "attempts": 1, "score": 22.836, "det": 0.0525, "deep_scan": false, "ambiguous": false, ...
tagis the result of the key-tag check:verifiedunder keykey_id(the shared pool key, whose fingerprint iskey_fp), which isactive.registry: hitandregistry_entries: 1mean your evidence log holds one entry for this payload.fingerprintis the confirmation against that entry.matched_by: bytesanddistance: 0mean the file is byte for byte the output you recorded. So your log does confirm this file, and the grade stays D only because the key is the shared one. A registered deployment marks with its own key, and the same file would grade B withstatus: confirmed_bytes(Licensing).decode_path: directandattempts: 1say the watermark decoded on the first, direct read.score,det,deep_scanandambiguousare informational.
... "issuer": { "id": 65300, "range": "reserved", "claim": "unverified", "directory": null, "manifest_signer_matches_directory": null, "manifest_signer_issuer": null, "key_family": "shared" } }
issueris the block that the CLI prints as theissuer:line: the range the number falls in (reserved), andclaim: unverified, because no directory entry backs it (Issuer lines).
Pass 4 and 5: the evidence entry and the caveats
... "evidence_log": { "stream": "host-1c28f5", "seq": 1, "entry_hash": "39add78633ae38ae8ea21b30343375dc7c1d66efbb435a094b78ef95eb8d3b20", "status": "ok" }, "caveats": [ "Absence of a signal is not evidence that content is human-made.", "Revocation status was not checked (OCSP disabled in this deployment)." ], "validators_disagree": false }
evidence_logpoints at the entry in your log that this file resolves to.entry_hashequals the onelaidline markprinted. It is set here because the Content Credentials chain to your own anchor and name the asset. When nothing resolves, it isnull(Verdicts).caveatsalways includes the absence sentence: "Absence of a signal is not evidence that content is human-made."noneand a missing signal say nothing about who made a file. The revocation sentence appears because Laidline does not fetch OCSP.validators_disagreeisfalsebecause no second validator is configured (Secondary validator).
The same image in two other states
Save a copy of marked.png as stripped.png with any image editor's export-without-metadata option. That removes the
Content Credentials but not the watermark:
laidline detect stripped.png
Expected output:
verdict: signals_present_unverified grade: D
c2pa present=False grade=none status=absent
watermark present=True grade=D status=shared mark
issuer: reserved issuer number (documentation or test value)
note: Absence of a signal is not evidence that content is human-made.
note: Revocation status was not checked (OCSP disabled in this deployment).
note: A watermark shown as present but not verified is not evidence of origin: no payload it carries was tied to a registered asset of this deployment by its key tag and the file's bytes or fingerprint. Grade B needs the registry hit confirmed by fingerprint or exact bytes.
The verdict falls to signals_present_unverified and the grade to D: a Laidline mark is there, and nothing names who made
it. evidence_log is null, because a shared mark resolves no asset and the manifest that would have named one is gone.
In the JSON, registry still reads hit, now with matched_by: fingerprint and distance: 0, because the pixels are
unchanged. With your own registered key this copy would grade B. The third note is the keyed-mode caveat for a watermark
that is present but not verified (Caveats).
A photo that was never marked gives the empty result:
laidline detect photo.png
Expected output:
verdict: no_signal_found grade: none c2pa present=False grade=none status=absent watermark present=False grade=none status=None note: Absence of a signal is not evidence that content is human-made. note: Revocation status was not checked (OCSP disabled in this deployment).
no_signal_found with grade none is not a statement about origin. It means neither check found anything.
| File | Verdict | Grade | Content Credentials | Watermark |
|---|---|---|---|---|
marked.png |
marked_by_this_system |
A | valid_trusted |
D, shared mark |
stripped.png |
signals_present_unverified |
D | absent |
D, shared mark |
photo.png |
no_signal_found |
none | absent |
none |
Rules and measurements behind the grades
The sections below keep the full rules and the measurements they rest on.
The 2026-10 null campaign
Between 2026-10-25 and 2026-10-30 Laidline measured its false positives on unmarked media, and its sensitivity on blind marked positives, for five media with the shipped defaults. The pass lines were written and sealed in the repository history before any unit was read. Intervals are Wilson 95 percent; "upper" is the upper bound. Every result below its sealed line is stated in the section of its medium, the ones that pass and the ones that do not, and the full tables are in Benchmarks (images), video, audio and text.
| medium | unmarked reads | what was counted | result | upper | against the sealed line |
|---|---|---|---|---|---|
| image | 38,500 photos | PixelSeal default reads, presence | 0 | 0.0100 % | met |
| image | 38,500 photos | keyed PixelSeal payload, tag accepted | 0 | 0.0100 % | met |
| image | 2,200 screenshots of unmarked photos | PixelSeal default reads, presence | 0 | 0.174 % | reported, no line |
| video | 4,854 clips of 5 s | VideoSeal, presence | 0 | 0.079 % | met |
| video | 4,854 clips of 5 s | keyed VideoSeal, tag accepted | 0 | 0.079 % | met |
| video | 4,854 clips of 5 s | keyframe TrustMark, tag accepted | 0 | 0.079 % | met |
| audio, keyed | 5,000 each of single utterances, 30 s joins, 120 s joins; 4,979 music clips | tag accepted | 0 | 0.077 % | met |
| audio, keyed | the same, with the streaming-card rule | public presence: speech 0, music 2 of 4,979, sounds 15 of 5,000 | 0.15 % (music) | speech and music met; sounds reported, no sealed line | |
| audio, unkeyed | the same strata | public presence: music 361 of 4,979 (7.25 %) | 8.00 % | not met (line 0.5 %) | |
| text | 20,000 documents per scheme | grade B, both schemes | 0 of 40,000 reads | 0.02 % per scheme | met |
The sensitivity lines that were not met are stated where they belong: keyframe TrustMark on video, and keyed and unkeyed audio ("Audio in the 2026-10 null campaign" below). The strict reading of "wrong payloads" for voice calls is in the voice-call paragraph further down.
What the numbers do not show:
- The rules and lines were sealed before any unit was read, but no digest of the records was committed before the result files were opened, in any of the campaigns.
- The registry bundles used were controls only. Grade C was not exercised, and the image and video positives reached grade B by fingerprint, never by exact bytes.
- The text gate (grade B at p < 1e-5 with the exact KGW tail) and the audio streaming-card rule were chosen on the same data they are reported on, so those two results are in-sample. The audio figures under the rule are a replay of the stored records, not a new run.
- The voice-call null rows share sources, so the rows are not independent.
- Video leaves out 40 clips of 4096 x 2160 that
watermark.video.max_pixelsrefuses (0.71 % of the set). - The audio strata for sounds and synthetic speech were added after the lines were sealed and have no line.
- The corpora are licensed CC0, CC BY, public domain, ODC-BY or Apache-2.0, with stated exceptions: Wikipedia text and Wikimedia Commons clips admitted under CC BY-SA (read for measurement only, not republished), and the Xiph derf test sequences and the Sintel film (97 of the 4,854 video clips), whose licences were not verified one by one (Benchmarks).
- On a second GPU type and on a workstation GPU the decisions were identical for image, video and audio (0
decision mismatches over 800, 556 and 400 pairs per device pair); for audio the workstation GPU differs only in one diagnostic
field on 13 units (
extra.fine_read_removed), which no decision reads.
Per-signal grades
| Signal | Condition | Grade |
|---|---|---|
| c2pa | valid_trusted (chains to your own anchor or the public trust list) with a timestamp |
A |
| c2pa | valid_trusted without a timestamp, or valid_untrusted (signature verifies, certificate unknown) |
B |
| c2pa | invalid (hash mismatch, bad signature, expired) |
D |
| c2pa | absent | none |
| watermark | payload decodes (version 1), issuer is yours, and a (issuer_id, asset_seq) entry in the local evidence log is confirmed: the query is the recorded bytes (sha256_out) or its phash64/v1 fingerprint is within detect.fingerprint_max_distance bits (default 10) of the recorded one (registry_match: local) |
B |
| watermark | payload decodes, issuer is yours, entries with that (issuer_id, asset_seq) exist but none is confirmed (registry_match: payload_only; their ids are in unconfirmed_asset_ids) |
C |
| watermark | payload decodes and issuer is yours, but no local entry | C |
| watermark | payload decodes for another issuer, carries a payload version other than 1 (payload_version_ok: false), or bits do not parse |
D |
| watermark | keyed mode: the key tag verifies under the shared pool key (confirmed_shared_key), whatever the registry holds: a Laidline mark that names no provider |
D |
| watermark | nothing detected | none |
watermark (audioseal), unkeyed |
detector score at or above watermark.audio.detect_threshold: presence only, whatever the 16 bits say; no payload, no issuer_match, no issuer block; never resolves an asset (registry_match: null) |
D |
watermark (audioseal), keyed mode with payload.layout: none |
detected, whatever issuer the 16 bits name (the full tier shows the word, present_unverified) |
D |
watermark (audioseal), public tier only |
detected only by the hardened decode's fallback paths (decode_path other than fast or normalised: today phase, windowed or, in keyed P70 deployments, located) and not confirmed: the public body is an undetected clip's ("Audio presence on the public tier") |
none |
watermark (videoseal) |
the BCH code and the CRC-16 pass, the issuer is yours, and a videoseal entry with that (issuer_id, asset_seq) is confirmed by exact bytes or by the fingerprint of the frame at 1 s |
B |
watermark (videoseal) |
as above, but no entry, or none confirmed (payload_only) |
C |
watermark (trustmark_keyframes) |
as the image watermark rows, over the voted keyframe payload | B, C or D |
watermark (pixelseal, the default image scheme) |
as the image watermark rows, with two differences: only entries recorded under pixelseal are looked up, and confirmation compares the recorded fingerprint with the view that decoded (the image, its border cut, its half turn, its photo box box1 or a deep-scan turn) and with the query, its cut and its photo box in all 8 dihedral turns (see "PixelSeal image scheme"). On the cut and box paths, B means the registered image appears within the query, which may hold other content (box and query_size say where) |
B, C or D |
| text_watermark | p_value < detect_p_value over at least min_tokens tokens (KGW: tokens covered by distinct n-grams, scored_tokens + context_width), and the reported test's p below grade_b_p_value (default 1e-5) |
B |
| text_watermark | p_value < detect_p_value over fewer tokens (KGW: fewer tokens covered by distinct n-grams), or over min_tokens with p not below grade_b_p_value |
D |
| text_watermark | p_value >= detect_p_value |
none |
The overall grade is the best signal grade. With the optional secondary validator, a C2PA verdict it disputes takes the lower of the two grades ("Secondary validator" below).
With signing.tsa.mode: checkpoint_only the manifests carry no RFC 3161 timestamp, so the C2PA signal grades B
at best, even when it is trusted. The evidence checkpoints are still timestamped, but a checkpoint token proves when
the log reached a head hash (and so that every entry up to it existed by then), not when a given manifest was signed,
and a C2PA validator never sees it. Choose per_asset when grade A for the manifest matters more than the TSA round
trip per asset. Under checkpoint_only the evidence export's summary.md says so in a "Timestamps" section.
There is no A for text. Only this deployment holds the key, so a present text signal points at this system, but the test
is statistical (a p-value), not a signature. A text is present (grade D) at p < detect_p_value (1e-3) and grade B only
when the reported test's p is also below grade_b_p_value (1e-5) over at least min_tokens tokens; the nominal false-positive
rate of B is 1e-5 per text, not 1e-3. KGW's p-value is the exact binomial upper tail of the green count, as the fallback's
already was. Before 2026-10-28 B was the presence gate itself and the KGW fast test used a normal approximation, which
understates the right-skewed tail at a green share of 0.25. The measured false-positive figures below were taken with
that normal tail and the 1e-3 gate (before 2026-10-28): on human C4 text under random KGW keys the first 1,024 tokens
flagged 7 of 5,000 (0.14 %, exact 95 percent interval 0.056 to 0.29 %) against a nominal 0.1 %; at 3,000 tokens 5 of
5,000 (0.10 %, 0.032 to 0.23 %); at 8,192 tokens 7 of 5,000 (0.14 %, 0.056 to 0.29 %) (KGW's fast test scores the first
1,024 tokens at any length). In the 2026-10 text null campaign (20,000 unmarked texts per scheme: 15,000 human, 5,000 machine-written) grade B at that gate
fired on 19 SynthID and 32 KGW texts.
Campaign numbers (re-scored 2026-10-29). The 2026-10 text null campaign was re-scored with the shipped gate (B at p < 1e-5, KGW on the exact tail; 41,600 records, 0 unreadable). Grade B fired on 0 of 20,000 unmarked documents per scheme (40,000 reads; Wilson 95 percent upper bound 0.02 percent), against 19 SynthID and 32 KGW at the earlier 1e-3 gate. Presence (grade D) on unmarked documents, with the Wilson upper bound against the 0.5 percent line: KGW 32 of 20,000 (0.23 percent), by cell 13 of 5,000 at 1,024 tokens (upper 0.44 percent), 8 at 512 (0.32), 6 at 8,192 (0.26) and 5 on LLM text (0.23); SynthID 19 of 20,000 (0.15 percent), by cell 3, 4, 7 and 5 of 5,000. Marked positives: none of the 800 matching-scheme texts (identity and 10 % deletion) lost presence or grade B against the earlier gate (KGW 400 of 400 present; SynthID 399 of 400), and the cross-scheme reads of marked texts gave 2 present of 800, both grade D and none B. These are the same documents the gate was chosen on, not a held-out result. The cost of the gate is length: B at z of 4.27 instead of 3.09 needs about 1.9 times the tokens for the same per-token signal, so short or heavily edited marked text loses B (it becomes D) at about half the length or edit strength it had. That is not yet measured on paraphrase, translation or dilution rows.
In flight and positive sensitivity (the same re-score). The 41,600 reads (20,800 documents under two schemes) had 0 unreadable records and ran on a CPU only: the median read time is 8.7 ms at 200 tokens to 266 ms at 8,192 for the SynthID-style scheme, and 125 ms to 1,086 ms for KGW. The positives were 800 blind texts of issuer 1, 400 per scheme (280 unedited, 120 after deleting 10 percent of the words):
| scheme and edit | present | grade B | grade D |
|---|---|---|---|
| SynthID-style, unedited | 280 of 280 | 280 | 0 |
| SynthID-style, 10 % deleted | 119 of 120 | 112 | 7 |
| KGW, unedited | 280 of 280 | 260 | 20 |
| KGW, 10 % deleted | 120 of 120 | 100 | 20 |
None of the grade D positives is D on the p-value. The 7 SynthID-style ones were cut to 166 to 199 tokens, under
min_tokens; the 40 KGW ones all sit in the 512-token stratum, where the distinct n-grams cover fewer than 200 tokens
(a repetitive generation, or the deletion took it under 200). The one absent positive is a text of 19 tokens after the
edit, which cannot be scored. KGW's null still shifts with the kind of text (arXiv abstracts at about 2.6 times the
rate of C4 or Wikipedia), so its margin under the presence line is thin. Text grade B is a statistical claim with a
per-text probability, not the equivalent of the registry-confirmed grade B of images and video
(Benchmarks).
KGW scores each distinct
(context, token) n-gram once, at its first occurrence: a repeated n-gram adds no evidence, so a looping text (as greedy
decoding produces) neither inflates z nor reaches B on the strength of its length. For B, KGW counts the tokens its
distinct n-grams cover (scored_tokens + context_width). That is every token of a text without repeats, so
repeat-free KGW text is B from min_tokens tokens, as SynthID text is. The signal gives tokens (scored input),
scored_tokens (distinct n-grams for KGW) and repeated_ngrams (occurrences not scored again; null for SynthID,
whose mask already skips repeated contexts). Text has no manifest, so a text result has one signal, the text watermark.
detect needs model (the tokenizer id) for text; see vLLM.
With watermark.text.decode: hardened (the default), detection stops at the first of three steps that detects:
(a) the fast test, exactly as above (for KGW over its first 1,024 tokens); if it detects, the result is the fast test's, with
decode_path: fast and hypotheses 1, so no text gets a lower grade or loses a detection against decode: fast. For KGW only,
the fallback: (b) the whole-text test over up to max_detect_tokens tokens (made only when the text runs past the fast test's
cap) and (c) windows of 128, 256 and 512 tokens, each stepped by half its length. Each fallback test's p-value is the
exact binomial upper tail of the green count (the normal tail understates it: a 128-token window at z 4.15 is 1.7e-5
normal, 5.9e-5 exact). The fallback has its own budget: it detects when its smallest p-value times F, the number of
fallback tests, is below fallback_p_value (default 0.001). Two thresholds decide the grade of a fallback detection:
it is grade B only when that corrected p-value is also below the smaller of the strict fallback_grade_b_p_value
(default 0.0001) and grade_b_p_value (default 1e-5, so 1e-5) and the detecting test covers min_tokens; otherwise it is D,
the grade of a detection that does not qualify for B. The fast test is grade B at p < grade_b_p_value over min_tokens,
else D. SynthID text stops at (a): it answers as fast, with hypotheses 1 and the whole text as
window.
The signal of a hardened result adds decode_path (fast, whole_text or window), window (the reported test's
{start, end} in scored tokens), hypotheses (M = F + 1), fallback_p_value, p_fallback_min (the smallest
fallback p-value) and p_fallback_corrected (F times it; both null when the fallback did not run),
fallback_grade_b_p_value (the effective budget: the smaller of the configured one and grade_b_p_value),
fallback_grade_b_met (whether p_fallback_corrected is below it; null when the fallback did not run), grade_b_p_value,
grade_b_met (whether the reported test's p, or the corrected fallback p, is below the grade-B gate, min_tokens aside)
and bound_total. A detection in (b) or (c) is described by its test: tokens,
scored_tokens, z_score and p_value are that test's, and caveats name the test and, for a D that would otherwise
be B, the strict budget it missed. When nothing detects, the reported test is the fast test's, whose p-value is not below
detect_p_value; the fallback's evidence is only in p_fallback_min and p_fallback_corrected.
bound_total is detect_p_value + fallback_p_value (0.2 percent at the defaults) when the fallback ran. It is nominal,
not a guarantee: it assumes every test's p-value is exact, which holds for every KGW test since the fast test moved to the
exact tail (2026-10-28). Measured, in an earlier bench than the 2026-10 campaign (whose re-score is under "Campaign numbers" above), on human C4 text under random KGW keys (5,000 trials per length), flagged by the fast
test alone (on the normal tail) and by the whole hardened decode at the defaults of that date: 1,024 tokens 7 and 12 (0.14 and 0.24 percent; exact 95
percent intervals 0.056 to 0.29 and 0.12 to 0.42 percent), the 5 flags the fallback added all grade D; 3,000 tokens 5
and 8 (0.10 and 0.16 percent; 0.032 to 0.23 and 0.069 to 0.32 percent), the 3 added all grade D; 8,192 tokens 7 and 12
(0.14 and 0.24 percent; 0.056 to 0.29 and 0.12 to 0.42 percent), the 5 added all grade D (the closest to B, a 256-token
window, had a corrected p-value of 1.2e-4 against the strict 1e-4). On the bench's 500 human C4 texts the default
flagged KGW 1 (the fast test 0, grade D). Applied to SynthID a window scan flagged 3 human texts (the fast test 1), which fails the bench's
binomial guard for a few points on harsh edits only (SynthID's embedded rows are already at 100 percent), so SynthID is
not scanned.
The fallback finds KGW text diluted inside longer human text (measured offline on the text bench at the defaults:
at a 10 percent share 32 to 100 percent sampled and 6 to 72 percent greedy). Greedy KGW at a 25 percent share went
from 17.0 to 79.0 percent against 95.0 percent for the unedited text, a missed target (it was to come within 5
points). An earlier figure of 88.5 percent was computed with the normal tail, which overstated detections.
The strict grade-B budget cost no marked text its grade B on that bench: every fallback detection that covered
min_tokens also cleared 0.0001 (0 grade-B losses on every KGW row against one threshold of 0.001).
Why a payload alone is not evidence
Neural watermark decoders emit payloads on images that were never marked. On 5,000 unmarked COCO val2017 photos, Laidline's TrustMark decoder (BCH_5, Q then B then C) returned a well-formed 61-bit
payload for 8 of them: 0.16 percent (95 percent interval 0.081 to 0.32 percent). The decoder's own "detected"
flag fires far more often: 417 of 5,000 images (8.3 percent) under the other TrustMark schemas alone, which Laidline
rejects (8.5 percent with the BCH_5 hits). Those
phantom payloads parse to arbitrary (issuer_id, asset_seq) values, so a decoded payload says nothing on its own
about where an image came from. The measured numbers ship with the package (laidline.compliance.phantom) and are
quoted in the detect caveat; the method and the full table are in Benchmarks.
These are TrustMark's numbers, from an earlier run. The default scheme, PixelSeal, accepted a payload on 0 of 38,500
unmarked photos in the 2026-10 null campaign (Wilson 95 percent upper bound 0.0100 percent;
Benchmarks). A payload alone is still not grade B.
Confirmation rule
A registry hit therefore needs a second check before it counts. Every marked image entry records
fingerprint: {"alg": "phash64/v1", "hex": ...}, a 64-bit DCT perceptual hash of the watermarked pixels
(laidline.media.fingerprint). detect recomputes it on the query and confirms a hit when either
- the query's SHA-256 equals the entry's
sha256_out(matched_by: bytes) or, for a streamed audio entry, its signed twin'stwin.sha256(matched_by: twin_bytes), or - the Hamming distance between the two fingerprints is at most
detect.fingerprint_max_distance(default 10;matched_by: fingerprint).
Only confirmed hits appear in asset_ids, resolve evidence_log, and grade B. Hits that fail both checks give
registry_match: payload_only, grade C, verdict signals_present_unverified, their ids in unconfirmed_asset_ids
for an operator to look at, and the phantom caveat. The signal carries
fingerprint: {alg, hex, distance, matched_by} with the query's hash and the smallest distance over the hits.
On the bench, JPEG q75 and a 60 percent resize move at most 2 bits and unrelated images differ by at least 20;
crops move the hash more than recompression does, so a cropped copy of your own output may fall to
payload_only. Entries written before the field existed carry fingerprint: null and confirm by exact bytes only.
Computing the fingerprint costs a few milliseconds at typical sizes, about 70 ms at 4k: once per marked image
(timings_ms.fingerprint in the entry) and once per detect query.
Laidline writes payload version 1 only, and 5 of the 8 measured phantoms carried another version, so a payload
whose version is not 1 is never matched against the issuer or the registry (payload_version_ok: false, grade D).
bit_accuracy (informational)
A present watermark signal carries bit_accuracy. bit_accuracy is informational. It is the share of the 100 raw
decoder bits that agree with the codeword of the decoded payload; a phantom also lies within the BCH radius (>= 0.95),
so grading and confidence do not use it. The codeword is the packet encode writes for that payload (61 data bits,
BCH_5 parity and the schema bits), so the value is 1 minus the flips the BCH layer corrected, over 100, rounded to 3
places. confidence stays 1.0 for a parsed payload (BCH decoding is all-or-nothing) and 0.5 when the bits do not
parse. bit_accuracy is null when nothing is detected, and also when the raw decoder bits or the codeword are
unavailable; computing it never fails a detect call. It is not used for grading. The distribution on marked images
after common edits (mean and 5th percentile per transform) is in the image robustness table of Benchmarks.
Hardened image decode (watermark.image.decode: hardened)
The hardened decoder replaces the fast decoder for images; it does not run the fast decoder first, because the fast decoder itself returns payloads for unmarked photos. It reads the TrustMark decoder's 100 outputs before thresholding and decodes bits 0-95 with strict BCH_5 (no correction in the pad bits, at most 5 corrected bits) after Chase-II flips of the 8 least reliable bits. The four version bits are ignored, so a flipped schema bit no longer loses a marked image. The highest-scoring candidate is accepted only when both gates hold:
- signed score (mean over bits 0-95 of the output, multiplied by +1 where the candidate's codeword bit is 1 and -1 where it is 0) at least 2.95, and
- normalised score (signed score over the mean absolute output) at least 0.945,
and its payload version is 1. A best candidate with another version is rejected; the next candidate is not tried. Variants run in the configured order (Q, B, C) and the first accepted one wins. Without the gates the same decoding returns a payload for 1,526 of 2,500 unmarked photos (61 percent, any payload version; 249 carry version 1), measured on the fit half of the calibration data.
The gates were fitted on one half of the bench arrays (Q-, B- and C-marked COCO photos under every bench row, and 5,000 unmarked COCO photos, 500 of which are also the bench's unmarked originals) and checked on the other half, split by photo. Three gate pairs were evaluated on that validation half, two in an interim run and then the shipped pair, so its wrong-payload counts describe the shipped gates rather than confirm them. The unmarked result does not depend on that choice: the best candidate on any unmarked photo scores at most 1.35, against a gate of 2.95.
| validation half | fast decoder | hardened |
|---|---|---|
| unmarked photos accepted (of 2,500) | 6 | 0 (95 percent interval 0 to 0.15 percent) |
| exact decodes, detect's chain (of 46,475 marked measurements) | 33,879 | 34,625 |
| wrong payloads, chain: Q-, B-, C-marked | 7, 17, 17 | 2, 1, 4 |
| wrong payloads, the marked variant's own decoder: Q, B, C | 5, 2, 0 | 2, 1, 4 |
Over both halves no unmarked photo is accepted (0 of 5,000; fast decoder 8). The normalised gate sits next to a wrong payload: on the fit half the best wrong candidate that clears the score gate has a normalised score of 0.9449, against a gate of 0.945. The fit half's zero wrong payloads is therefore an in-sample result, and it did not carry over (7 wrong on the validation half).
Wrong payloads on misaligned images. The gates separate unmarked photos from marked ones by the size of the
decoder outputs; they do not tell a right payload from a wrong one on a marked image that is out of alignment. Every
wrong payload the hardened decoder returned is on a geometric row (crop_75_corner 2, rotate_+5 2, rotate_-5 2,
the rotation sweep at 5 degrees 1) and clears the score gate with room (3.1 to 4.8) and the normalised gate by only
0.0004 to 0.024 (0.9454 to 0.969). On C-marked images the variant's own decoder returns 4 wrong payloads where the fast decoder returns none. On the
validation half crop_75_corner gives 19 exact and 2 wrong of 705 (fast decoder 4 and 1), and rotate_+5 with rotate_-5
450 exact and 4 wrong of 1,410 (fast decoder 350 and 2). Each wrong payload is a codeword 11 to 21 of its 96 bits away from the
embedded one (6 to 12 of its 61 payload bits), a neighbour of the truth rather than an unrelated word: two random
codewords differ in 48 bits on average, and two bench assets of the same issuer in 35 (median). None is a shift or
another simple transform of the truth. All 7 carry another issuer id, so detect never looked them up in the registry
and reported them at grade D. That is how these errors fell, not a guarantee: a neighbour that keeps the issuer bits
would be looked up, and a hit on another asset still needs fingerprint or exact-bytes confirmation for grade B.
Compression rows gain (on both halves, JPEG q25 on Q-marked photos: 77.6 to 91.8 percent). Of the 33,879 exact fast-decoder
decodes on the validation half, 4 are lost (rotation and crop rows, best candidates just under a gate). Rotation by 5
degrees decodes about one measurement in three (validation rotate_+5: 225 of 705, fast decoder 176). Larger rotations,
quarter turns, screenshots and 50 percent crops stay near 0 percent under either decoder: they need a geometric
search, not a better decoder.
The watermark signal then also carries decode_path (fast when the fast decoder decodes the same raw bits to the same payload,
soft when only the hardened decoder finds it), score, normalised_score and attempts (BCH decodes run, 256 per
variant tried), all null when nothing is accepted. They are informational. Grading is the same for either decoder:
a payload needs a confirmed registry hit for grade B. The calibration was made on decoder outputs computed on a GPU,
while the image watermarker runs on the CPU by default (watermark.image.device). The video keyframe scheme
(trustmark_keyframes) keeps the fast keyframe vote under watermark.image.decode: hardened: its per-frame vote calls the fast
decoder, and the gates were not calibrated on keyframes. watermark.video.decode: hardened is what changes the keyframe
vote (the video section below).
PixelSeal image scheme (watermark.image.scheme: pixelseal, the default)
New images carry PixelSeal (Meta, MIT). The 61-bit payload and its CRC-16 are coded in
BCH(127, 78) with t=7 and sent twice in PixelSeal's 256-bit message (laidline.watermark.image_code). Each read
extracts the 256 outputs, adds the two copies' outputs and decodes them with Chase-II (the 12 least reliable bits
flipped in every combination, 4,096 bounded-distance decodes). A read accepts a payload only when exactly one
candidate passes the CRC-16 and that payload carries version 1.
Reads. Detect reads the image directly and stops there on a decode. Otherwise it reads up to three more views:
cut: the image cut to its largest non-uniform rectangle, when it has a flat border (a photo on a flat canvas);rotate_180: the image turned half a turn (PixelSeal reads quarter turns directly, but not half turns);box1: the photo found inside a page capture by the localiser (laidline.watermark.image.localise), when it finds one. This is a screenshot with browser bars, a header or a caption around the photo, where the cut stops at the bars. It is not read when the box is nearly the whole image or the cut, which are already read.
A payload is accepted only when exactly one distinct payload appears across these retries; two are ambiguous and
give no payload. watermark.image.deep_scan: true adds, only when the default reads find nothing, the six other
dihedral turns of the image (quarter turns and mirrors) and the seven other turns of the photo box. A default decode
therefore stands under deep scan: a payload found on a retry is not cross-checked against the deep reads (reading them too
would have called a conflict ambiguous; on the saved records that never happened, 0 of 282 retry
decodes). The signal reports decode_path (direct, cut, rotate_180,
box1 or deep_scan:<view>), attempts (reads made), the Chase candidate's score and normalised_score, det,
deep_scan, ambiguous and false_accept_bound.
The localiser (no model and no dependency) works in four steps:
- It marks 8 x 8 blocks as textured when their luma steps both ways (the smaller of the mean horizontal and vertical steps above 1.0). A straight edge between two flat browser bars steps one way only, so it does not count.
- It closes 2-block gaps.
- It takes the largest connected textured region and its bounding box. It drops a box under 10 percent of the image.
- It moves each edge to the strongest straight luma step within one block.
The constants were fitted on the 40 captures of the first real-screenshot run, scored by the boxes within IoU 0.95 of
the photo (33 of 40 there, mean edge error 26 px; the localiser fit of laidline-bench). A first fit at IoU 0.85 with block
standard deviation as the texture measure kept the browser bars in its desktop boxes, and 7 of the 20 desktop captures
failed to decode. That fit was replaced before any validation capture was made. With the final constants the fit set
decodes 40 of 40. Detect reads one box. Every fit capture holds one photo, so the fit could not show a second box
helping. On the two-photo validation pages a second box would land on the other photo (8 of 20 pages at IoU 0.94 or
more), but under the unchanged one-payload rule that would make those pages ambiguous instead of reading one payload,
so one box is the better rule there too. The landscape pairs on a phone (0 of 5 read) are a minimum-area effect:
each photo covers 2.4 to 7.4 percent of the capture, under the localiser's 10 percent MIN_AREA.
Confirmation. The registry lookup keeps only entries recorded under pixelseal. A hit is confirmed when the
query is the recorded bytes, or when the recorded fingerprint is within fingerprint_max_distance of either of these:
- the fingerprint of the view that decoded;
- when that does not confirm, any of the confirmation fingerprints (
pixelseal.confirmation_fingerprints): the decoded view and the photo box in their 8 dihedral turns, and the query and its border cut upright.
That is at most 18 distinct fingerprints per registry candidate, times the registry entries that share the payload's
36-bit asset_seq (normally 1; about 7 colliding pairs among a million assets). These are fingerprint comparisons only,
with no extra decode, so the payload bound below is unchanged. They are computed only when a payload of your issuer
has a pixelseal registry entry that the decoded view alone does not confirm. Two cases need them:
- PixelSeal reads a screenshot whole, or a quarter-turned or mirrored copy, on the direct read. That decoded view is not the recorded image: the whole page in the first case, a turned image in the second.
- A view inside the query, or a turn of the decoded view, is.
The signal's fingerprint block names one view, consistently. When a fingerprint confirmed, it gives that view's
label, hex and distance, and the signal's box gives its region: for example box1, or direct:rotate_90 for a
quarter turn read whole. Otherwise it gives the decoded view's own; the other views are not listed as if they had
matched.
A wider rule comparing 24 fingerprints (the query, the cut and the box, each in 8 turns) was measured too. The set above replaces it because it keeps every grade-B count of the 24-fingerprint rule on every row, capture and fit-set group below.
Grade B is a guard against coincidence, not against a forger. In unkeyed mode the image code is unkeyed and the PixelSeal weights are public, so anyone can write a registered payload into an image (in keyed mode a copied genuine payload still can: "Keyed image payloads" below). A naive low-frequency change also brought 30 of 30 unrelated photos within 10 bits of a target fingerprint on a single upright comparison. The extra views add little to that. A C2PA manifest trusted by your own anchor is the path that resists forgery.
The cost of the widened set. The cost is a higher chance that an unrelated image's fingerprint falls near a recorded
one; a payload match must still accompany it. It is measured per population by the coincidence run of laidline-bench
(seed 20261001). Every pair goes through the product's _confirm, and each query is taken as decoding on its direct
read. The share of pairs within 10 bits:
| population (pairs) | decoded view only | 24 fingerprints | shipped | Wilson 95 percent, shipped |
|---|---|---|---|---|
| COCO photos against COCO photos (1,000,000; 200 per photo) | 0 | 4.0e-6 | 3.0e-6 | 1.0e-6 to 8.8e-6 |
| page captures against COCO photos they do not show (2,299,520; 460 captures) | 1.7e-6 | 1.1e-5 | 7.4e-6 | 4.6e-6 to 1.2e-5 |
| low contrast: bottom decile of thumbnail std, all pairs (249,500; 500 photos) | 8.0e-6 | 8.8e-5 | 8.0e-5 | 5.2e-5 to 1.24e-4 |
| templated: a page capture registered, other captures of the template (39,600) | 2.1e-3 | 4.2e-3 | 3.7e-3 | 3.2e-3 to 4.4e-3 |
- Gated populations. The stop line, 1e-4 per registry candidate, applies to natural photos and page captures, on
the point estimate under the shipped rule. It holds with the measured margins:
- COCO 3.0e-6, a factor of 33 below the line (its upper bound 8.8e-6 is a factor of 11 below);
- page captures 7.4e-6, a factor of 14 below (upper bound 1.2e-5, a factor of 8).
- What the COCO row measures. On COCO most queries have few views: 4,256 of 5,000 had 9 fingerprints (the query's 8 turns and the decoded view), 504 had 10, 184 had 17 and 56 had 18. The page captures nearly all had 18 (449 of 460).
- Low-contrast and templated content are reported, not gated. For them the fingerprint guard is weak under any
number of views:
- The single comparison is already 8.0e-6 for low-contrast photos and 2.1e-3 for registered screenshots of a templated page.
- The widened set raises them to 8.0e-5 (upper bound 1.24e-4, above the line) and 3.7e-3.
- The weakness belongs to the 64-bit pHash.
- A false grade B still needs a payload decode naming the registered asset first. The analytic payload bound per image (10^-4.71, default reads) times a row's upper bound is about 9e-8 for templated captures and 2.4e-9 for low-contrast photos.
A payload without a confirmed hit is grade C at most (your issuer) and carries the PixelSeal caveat. A photo partly out of view (scrolled under the bars) stays at grade C: its visible part is 14 to 34 bits from the recorded fingerprint, and no view of the query can match it (0 of 40 scrolled captures below).
Grade B, before (the decoded view only) and with the shipped rule, on every exact decode. Cells are k/n with the Wilson 95 percent interval:
| set | n | decoded | grade B before | grade B shipped |
|---|---|---|---|---|
| rows: rotate_90 (read directly) | 100 | 100 | 0 (0-4) | 100 (96-100) |
| rows: flip_h (mirrored, read directly) | 100 | 100 | 0 (0-4) | 100 (96-100) |
| rows: rotate_180 | 100 | 100 | 100 (96-100) | 100 (96-100) |
| rows: screenshot (synthetic) | 100 | 100 | 27 (19-36) | 98 (93-99) |
| real captures, base (4 groups) | 40 | 39 | 36 | 37 |
| real captures, dark page | 40 | 39 | 34 | 37 |
| real captures, JPEG q80 | 40 | 39 | 37 | 37 |
| real captures, scrolled | 40 | 37 | 0 | 0 |
| real captures, fit set | 40 | 40 | 32 | 34 |
The 24-fingerprint rule gives the same counts on every line. The validation decodes left at C are all one photo (267351, at both sizes in the base, dark and JPEG variants: its box is off by 522 and 625 px, and the best view is 20 to 26 bits away) plus one two-photo page. In the fit set they are the 6 captures whose box stops short inside the photo (12 to 22 bits). Deep scan gives the same grade-B counts. Deep scan never lowers a grade: its reads run only when the default reads found nothing, so a default decode or ambiguity stands.
False accepts. These are two separate numbers, one analytic and one measured.
- Analytic union bound (a random-word model). In expectation, a uniformly random read yields at most
10^-5.31 CRC-valid Chase candidates (
image_code.LOG10_FALSE_ACCEPT_PER_READ: 2^77 codewords times the Chase-12 decoding volume over 2^(126-12), times 2^-16 for the CRC; the version check is not counted, so this is conservative). Detect makes at most 4 reads per image (direct, the cut, the half turn and the photo box: 10^-4.71), or 17 with deep scan (the six other turns of the image and seven of the box: 10^-4.08). This is per image and per detect call. Over a million unmarked queries it allows about 20 (default) or 83 (deep scan) payloads at most, which is why a payload alone is never grade B. - Measured in the 2026-10 null campaign (rules sealed before any photo was read; 38,500 unmarked photos, 19,175 from COCO train2017 and 19,325 from Open Images): PixelSeal with the default reads accepted a payload on 0 of 38,500 (Wilson 95 percent upper bound 0.0100 percent), deep scan on 0 of 38,500 as well, and the keyed payload read accepted no tag on 0 of 38,500 (upper bound 0.0100 percent). The 2,200 screenshots of unmarked photos in the same set gave 0 (upper bound 0.174 percent). Both pass lines were met (Benchmarks).
- Earlier run on unmarked photos, before the campaign (the
laidline-benchphantom run with the harness's--scheme pixelsealsetting, with the photo box read; the--scheme,--hardenedand--hardened-geometrysettings named in this page belong to the benchmark harness's own runner, not tolaidline bench): 5,000 COCO val2017 photos, every one read under both settings. Default reads accepted 0 of 5,000 (Wilson 95 percent 0 to 0.077 percent), and so did deep scan, with no ambiguous photo. The localiser found a box in 240 of the photos. Default reads per photo: 2 for 4,256 photos, 3 for 688 and 4 for 56; deep scan: 8, 9, 16 and 17. Before the box read it was also 0 of 5,000. The same code at Chase p=12 also accepted 0 of 20,000 null draws. - Measured on screenshots of unmarked photos: 200 new captures of the page harness below, at phone and desktop size. They hold 100 unmarked photos that were not used for the fit or the validation; those photos are among the 5,000 of the paragraph above. Default reads and deep scan both accepted 0 of 200 (Wilson 0 to 1.9 percent), with no ambiguous capture; the localiser read a box in 199 of them.
Marked images (the laidline-bench image run with the harness's --scheme pixelseal setting, n=100, product embed
and default reads):
- Every row is within 1 image of the same sample read with all 9 views; the default reads make at most 4 (3 when this run was made, before the photo box read; the rerun with it gave the same counts).
- rotate_180 decodes 100 of 100, all through the
rotate_180read. - screenshot (the bench's synthetic capture: the photo on a flat canvas) decodes 100 of 100: 74 directly, 26 through the cut. Real browser screenshots go through the photo box read (below).
- crop_50 decodes 87 (all 9 views: 88), webp_50 93, jpeg_50 99 (all 9 views: 100) and resize_0.5 99 (all 9 views: 100). Every other row decodes 99 or 100: blur_2 99, and each of the rest 100 of 100, among them rotate_+5, rotate_-5, rotate_90, crop_75 and crop_75_corner.
- The three images lost against the 9-view read each decode under deep scan through
deep_scan:flip_h; deep scan matches the 9-view read on every row. - Wrong payloads: 0 in either setting on every row.
- The quality floor held on every image: 0 of 100 were refused.
In flight (2026-10 null campaign, 162,800 unmarked reads). Nothing was presented as a mark: 0 presence, 0 tag accepts, no grade D, C or B, 0 errors and no Content Credentials, for PixelSeal, deep scan, keyed PixelSeal and TrustMark alike.
- Unkeyed PixelSeal never produced a CRC-valid payload. It read every photo at least twice, cut a border on 16.5 percent of them and read a photo box on 7.9 percent. Deep scan found nothing either, at 2.7 times the time.
- TrustMark's hardened gates let nothing through. Its own decoder would flag 8 percent of photos under some schema and accept 0.2 percent as BCH_5 words.
- Keyed PixelSeal read a word with a failed tag, carrying a random claimed issuer, on 6.0 percent of unmarked photos,
next to a
no_signal_foundverdict. Since the campaign an issuer is named, and candidate words are listed, only when the tag verifies. - A keyed deployment reads its own issuer's unkeyed PixelSeal mark as grade D, not B.
- Marking can succeed without a mark: TrustMark refused 1.4 percent of photos at the PSNR floor, with a warning the caller must check.
- Detect is CPU-bound. PixelSeal's median time is 97 to 390 ms by resolution up to 4 megapixels (p95 137 to 558 ms); the first TrustMark read on a fresh host pays 8 to 108 s to fetch and load the model.
Positive sensitivity (2026-10 null campaign). 1,500 blind positives of issuer 1 on photos, 500 per read: 350 unedited, 75 after JPEG quality 75 and 75 after a resize to 0.6. Unedited, the sealed line was 340 of 350:
| read | unedited | after JPEG q75 | after resize 0.6 | line |
|---|---|---|---|---|
| PixelSeal, default reads | 350 of 350 | 75 of 75 | 75 of 75 | met |
| keyed PixelSeal | 350 of 350 | 75 of 75 | 75 of 75 | met |
| TrustMark | 346 of 350 (97.1 to 99.6 %) | 74 of 75 | 73 of 75 | met |
All 7 TrustMark misses were photos that were never watermarked, because the PSNR floor refused them (1.4 percent of photos). Wrong payloads and cross-issuer labels: 0 of 1,500. Every hit graded B by fingerprint, none by exact bytes (the registry bundle held controls only). Decisions were identical (0 mismatches over 800 pairs) on a second GPU type and on a workstation GPU.
The detection channel (det) is not a gate. PixelSeal also outputs a detection value. On the saved
arrays it takes these values:
- 5,000 unmarked photos: -0.093 to 0.071;
- the marked decodes of the first run: -0.47 to 0.39;
- this code's decodes on the second set of arrays: -0.42 to 0.45.
The marked values span the unmarked ones on both sides. Any threshold that keeps every marked decode therefore rejects
0 of the 5,000 unmarked photos, on the fit half (by photo name, 2,551 photos) and on the validation half (2,449). No
det gate is added; the signal reports det for information only (the laidline-bench PixelSeal product run).
Real screenshots (the laidline-bench screenshot run, headless Chrome 148.0.7778). The setup:
- Marked photos are shown in a local page drawn with browser-like chrome: status and address bars and a bottom toolbar on a phone, tabs, address and bookmarks bars on a desktop, plus a site header and a caption.
- Captured at phone size (390 x 844, device scale 3: 1170 x 2532 PNG) and desktop size (1920 x 1080, scale 1).
- Decoded with the default reads.
Before the photo box read, the first 20 photos (40 captures) decoded 4 of 40, and 0 of 20 on phones. The border cut needs a flat border, so it stopped at the bars. Cut to the photo's true box, all 40 decoded. Those 40 captures were then used to fit the localiser, so they are not evidence for it.
Declared validation (run once, on fresh captures of the next 10 portrait and 10 landscape photos of the bench order). Each cell is decoded/n with the Wilson 95 percent interval, default reads:
| group | base | dark page | JPEG q80 | photo scrolled 20 % under the bars |
|---|---|---|---|---|
| portrait, phone | 10/10 (72-100) | 10/10 (72-100) | 10/10 (72-100) | 9/10 (60-98) |
| landscape, phone | 10/10 (72-100) | 10/10 (72-100) | 10/10 (72-100) | 8/10 (49-94) |
| portrait, desktop | 10/10 (72-100) | 10/10 (72-100) | 10/10 (72-100) | 10/10 (72-100) |
| landscape, desktop | 9/10 (60-98) | 9/10 (60-98) | 9/10 (60-98) | 10/10 (72-100) |
- Every group of every variant is at or above 80 percent on the point estimate. The intervals are wide at n=10.
- Deep scan gives the same counts, except 10/10 for scrolled portrait phone.
- No wrong payload and no ambiguous read appeared in any variant.
- The one desktop miss in the first three columns is the same photo each time (141671, desktop): the localiser found no box on it. The localiser's box matched the photo's true box to the pixel on most base captures; the median edge error is 0. The others were off by 625 px (267351 desktop, which decoded on the direct read), 522 px (267351 phone, decoded through its box), and 141, 45, 43, 27, 20 and 14 px.
- Two marked photos side by side, each with its own payload, 20 pages: one of the two payloads was read on 9 pages (phone portrait pair 5 of 5, landscape pair 0 of 5; desktop 4 of 10). The second never was, because detect reads one box. No page gave a wrong payload or an ambiguous read.
- The 200 captures of pages with unmarked photos are in the false-accept paragraph above: 0 accepted.
Time (2 CPU cores, p50 of 20 after 2 warm-ups, with the photo box read; at a 1-minute load of 1.7 to 2.4, the 5-minute load still falling from earlier jobs):
| operation | p50 | p95 |
|---|---|---|
| mark, 1024 x 1024 (the embed, the PSNR floor included) | 702 ms | 770 ms |
| detect, marked 1024 x 1024 image | 137 ms | 165 ms |
| detect, unmarked 1024 x 1024 photo (every default read) | 353 ms | 489 ms |
| the localiser alone, phone capture 1170 x 2532 | 282 ms | 331 ms |
| the localiser alone, 1024 x 1024 photo | 57 ms | 70 ms |
| confirmation fingerprints, phone capture read whole (cut, localiser, up to 18 hashes) | 283 ms | 341 ms |
| confirmation fingerprints, 1024 x 1024 photo | 69 ms | 79 ms |
Before the box read (load 1.7 to 1.8) the same rows were 744, 143 and 355 ms. The localiser runs only after
the direct read finds nothing. The detect rows time the reads only. When a payload of your issuer decodes and its
pixelseal registry entry does not confirm on the decoded view, detect then also computes the confirmation
fingerprints: the last two rows (1-minute load 2.6 before and 3.3 after, the 5-minute load still
falling from 9). On a capture they cost about what the localiser does. Two
earlier localiser timings ran at loads of 11 and 19 and are not published.
TrustMark Q under the same conditions: embed 122 ms, detect 55 to 128 ms. The weights are 309 MB, against TrustMark's Q, B and C.
Secondary validator (validators_disagree)
With detect.secondary_validator: true and detect.c2patool_path (Configuration), every manifest is also validated by
a pinned c2patool binary, against the same anchors (your own, plus detect.trust_anchors_path when set), and the
C2PA signal carries validator_secondary: {name, version, status, failure_codes}. The validator matrix
(Benchmarks, "Validators") shows why a second opinion matters: the same file can get a different verdict from a
different validator version.
- Disagreement means the two verdicts' states differ (
valid_trusted,valid_untrusted,invalid,absent) or their failure-code sets differ. This is stricter than comparing states alone: two validators that both sayinvalidfor different reasons disagree too. Success and informational codes are not compared. - On disagreement the C2PA grade becomes the lower of the two verdicts (the secondary graded by the same table as
the primary; A still needs the manifest's timestamp),
validators_disagreeistruein the result, and the caveatvalidators_disagree: c2pa-rs <status>, c2patool <version> <status>is added (when the states agree, followed by the failure codes only one side reported). When the states agree the grade cannot change; only the flag and the caveat say that the reasons differ. - A disputed manifest confirms nothing. Under disagreement a
valid_trustedmanifest no longer givesmarked_by_this_systemormarked_by_known_system, and itsasset_iddoes not resolveevidence_log. The watermark path is unaffected: a confirmed registry hit still givesmarked_by_this_system. - An error never changes a grade. When the secondary validator cannot run, times out (
c2patool_timeout_s) or prints something unreadable, its status iserror, the grade and verdict stay as c2pa-rs set them, and the caveatsecondary validator unavailable: <detail>is added (one line, paths removed, at most 120 characters). It is never counted as a disagreement.
The binary must run at start (it is asked for its version), or the configuration is refused. It runs once per image
detect on a private temporary copy of the query, so it adds a process start per call; measure it before enabling it
on a public endpoint.
Audio and video
Keyed video (keyed mode, Payload keys). VideoSeal and keyframe signals are built from the arbiter's outcome as image
signals are: B confirmed_bytes (scope whole) or confirmed_fingerprint on the frame at 1 s (scope region,
region: {frame_s}, with the caveat "one frame of this video (at T s) looks like the same frame of registered asset X
at thumbnail scale; the other frames were not compared; this rules out coincidence, not imitation"); C issued_for_other_asset on the full tier only; D present_unverified for a tag verified but unregistered, a
failed tag, an ambiguity or nothing chosen, when keyless presence holds (VideoSeal: a
BCH-valid word; keyframes: a gate-passing TrustMark Q read); otherwise absent. Soft bindings:
com.laidline.videoseal.v92-r1, com.laidline.trustmark.kf61-r1. The unkeyed video rows below are unchanged.
Keyed audio (keyed mode). An audioseal-p70-r1 payload is checked once per call, across the default
read and deep scanning (Payload keys, "Audio"). It grades B confirmed_bytes (scope whole) by the file's bytes or the
signed twin's bytes, or by the registered asset's fingerprint record: B confirmed_fingerprint
(scope whole) when every countable window at the matching alignment matched and the searched span is the whole file,
B contains_registered_asset (scope region, region: {start_s, end_s} in the query) when part of it matched. The
searched span is the chosen read's span widened by 4 s each side, at most 20 s, so confirmed_fingerprint is
reachable only for files of at most 20 s: a longer copy of a registered asset, even an exact re-encode, grades
contains_registered_asset with the region that was searched and matched; the matcher is b2-10x14/v1
at tau 0.33 (at least 8 of 10 reliable blocks per 4.736 s window), and only a slot that counts can confirm. A B by
fingerprint carries the caveat "Confirmed by audio fingerprint: query a-b s matches c-d s of the registered asset. This
guards against coincidence, not against deliberate imitation: anyone holding the asset can make matching audio, and
the payload layer is public. Assets that share a music bed at speech level, a stretch of identical audio of about 3.5 s
or more, or a re-performed passage cannot be told apart by the fingerprint; for them the payload alone names the
asset." The full tier of every B adds fingerprint: {alg, rule, status, score, threshold, window_s, searched_query_span_s, query_span_s, asset_span_s, matched_windows, countable_windows, hypotheses, candidates, false_match_bound_log10}; false_match_bound_log10 is log10(candidates) plus log10 of the worst gated null
population's per-search bound (9.5e-5), a bound and not a per-call probability, and it does not cover
templated content. A payload copied onto other audio verifies but does not confirm. C issued_for_other_asset on the
full tier only; D present_unverified for a tag verified but
unregistered, a failed tag, an ambiguity or nothing chosen, when presence holds (the 16-bit detection, an eligible
own-issuer head, or another deployment's head read on three or more grids at score 0.97 or better: this rule was chosen after the data, and its floor was tested once on a fresh 3,000 own marks with 0 labels). Otherwise the
signal is absent. It never grades B on the payload alone. The full tier adds extra.stage, extra.payload_read and
the false-accept bound per detected clip.
The public tier shows presence only: {type, scheme, present: true, grade: "D", status: "present_unverified"}, with the caveat "a watermark of another deployment is present; it is not a mark of this
Laidline deployment" for a foreign head. A 16-bit read in keyed P70 mode never shows its issuer and carries the caveat "a
mark is present; its issuer could not be read". Soft binding: com.laidline.audioseal.p70-r1.
Audio (AudioSeal, unkeyed). The 16-bit payload carries the issuer id and nothing else, and it has no error
detection: one flipped bit renamed the issuer at full confidence on 40 of 100 marked music clips in the 2026-10 null
campaign. An unkeyed deployment therefore reports presence only for audio, grade D at most, whatever the word
reads (your own number, another's or issuer 0): the signal has present: true, payload: null, issuer_match: false and no issuer block, and its one watermark caveat is "an unkeyed audio mark carries a 16-bit word with no error
detection; this deployment reports presence only and names no issuer". Audio never grades C there. (Unkeyed image
and video marks are unchanged: a CRC-checked word names its issuer with the unkeyed-format caveat.) The result also
carries the caveat "an AudioSeal mark names no asset; the file is not resolved against the evidence log.".
Only a C2PA manifest trusted by your own anchor gives marked_by_this_system and evidence_log for audio; opus, aac
and pcm files carry no manifest, so their best verdict is signals_present_unverified. The signal carries
confidence (the detector's score), detection_threshold and detector (the AudioSeal card that fired: the batch
or the streaming detector; detect runs the batch one first). On 530 unmarked clips the rate of false detections was
0.75 percent at the benchmark's operating point 0.39 and 0.38 percent at the default threshold 0.5
(Benchmarks, "Audio robustness"), so an audio watermark alone is weak evidence at either setting. aac (ADTS)
files marked by Laidline are detected but their issuer bits do not read back, so they grade D.
Hardened audio decode (watermark.audio.decode: hardened)
The 16-bit payload has no checksum and no error correction: any 16 bits read back name some issuer, so a detection with misread bits names the wrong one (grade D for another issuer, or C for yours by chance). The bench returned the wrong issuer in 25.1 % of detections at 0.39; at the default threshold 0.5, the n=100 run below gives the fast read 1,801 wrong of 4,387 detections (41.1 %), most of them after trims and offsets, which move AudioSeal's 20 ms bit grid.
The hardened decode (audioseal.decode_hardened) takes these steps, stopping at the first that detects:
- the fast read at
detect_threshold. Its verdict stands. The bits are then read by the detector that fired, at the phase (0 to 319 samples dropped at 16 kHz) whose bits read most confidently: all 320 phases on a 1 s excerpt at the most confident window, then the best 8 and phase 0 on up to 10 s, then the whole clip. Phase 0 is kept unless the other phase is at least 5 % more confident (KEEP_MARGIN0.05). A quiet clip's bits are read on the raised copy of the normalised step. - normalised: a clip quieter than -30 dBFS RMS is raised to it (peak capped at -1 dBFS) and both detectors run
again at
detect_threshold. - phase: per detector, the same phase search on the clip (or its raised copy), then the whole-clip share of flagged frames at that phase against its own threshold: batch 0.13, streaming 0.41.
- windowed: the best 1 s window of that phase's scores: batch 0.70, streaming 0.93.
In steps 3 and 4 the detector that fired reads the bits; when both fire at the same step, the one further over its own
threshold (share / threshold, the batch card on a tie) reads them. That reader rule changes no detection, so
every false-detection count below holds for it; under the earlier rule the batch card always read first, and on
streaming marks it read wrong bits. The phase is chosen by |logit| only; no step looks at a payload. confidence in the signal stays the fast read's whole-clip score;
decode_path, path_score, phase_samples, phase_confidence, attempts and gain_db are added, and extra when
the fine-read floor (below) removed a read that had reached a threshold.
Calibration (the laidline-bench audio calibration, GPU, the product's statistics and decision function). The thresholds were fitted on unmarked audio that no earlier
fit or validation had seen: all 2,939 LibriSpeech test-other utterances, split by a seeded shuffle of their ids into a
fit half (1,469) and a validation half (1,470). Each threshold, in the order phase batch, phase streaming, window batch,
window streaming, is the smallest 0.01 step at which the whole decode flags at most 0.5 % of the fit half (half the
1.0 % budget). The window length (1 s against 2 s) and KEEP_MARGIN were chosen on the fit half (50 clips) of the
marked n=100 run below; the other 50 clips are its validation half.
| unmarked, whole hardened decode at 0.5 | flagged | rate (Wilson 95 %) | by path |
|---|---|---|---|
| test-other fit half (1,469) | 7 | 0.48 % (0.23 to 0.98 %) | fast 1, phase 6 |
| test-other validation half (1,470), evaluated once | 11 | 0.75 % (0.42 to 1.33 %) | fast 3, phase 7, windowed 1 |
| the fast read alone on the validation half | 3 | 0.20 % | |
| earlier-seen: all 2,620 test-clean utterances (reported, not fitted on) | 20 | 0.76 % (0.49 to 1.18 %) | fast 5, phase 9, windowed 6 |
| bench2's own 100 unmarked test-clean clips | 2 | 2 % (0.6 to 7.0 %) | fast 1 |
| the fresh set's 100 unmarked test-clean clips | 0 | 0 % (0 to 3.7 %) |
The budget is at most 1.0 % false detections for the whole decode at 0.5. The test-other validation half (0.75 %) does not establish it beyond those recordings. The halves were split by utterance, so both hold the same 33 speakers and the same recording sessions, and the clips are short (median 5.2 s). The decode also flags almost four times as many unmarked clips as the fast read there (0.75 % against 0.20 %).
Measured on a fresh corpus: hardened audio exceeds its false-detection budget
(watermark.audio.decode is auto: hardened on a usable CUDA device,
the fast read otherwise).
How it was measured: the laidline-bench audio fast search (offline --step a7s). The traces ran
on the GPU (RTX 5080, float32, TF32 off), and the decisions are audioseal.replay at the shipped constants. The
corpus is LibriSpeech train-clean-100 (CC BY 4.0), read audiobook speech. Its speakers are not those of test-other,
test-clean or dev-clean. The joins are consecutive segments of one chapter, cut to exactly 30 s or 120 s.
| unmarked clips, whole hardened decode at 0.5 | clips (speakers, chapters) | flagged | rate | bootstrap 95 % over chapters | by path |
|---|---|---|---|---|---|
| short clips, fit speakers (2.0 to 17.3 s, median 14.1 s) | 3,000 (62, 95) | 65 | 2.17 % | 0.10 to 4.79 % | phase 61, windowed 3, fast 1 |
| short clips, validation speakers (1.9 to 17.6 s, median 13.9 s) | 3,000 (59, 89) | 32 | 1.07 % | 0.12 to 2.86 % | phase 29, windowed 2, fast 1 |
| both, without the 4 chapters that flag on most of their clips | 5,894 (120, 180) | 12 | 0.20 % | 0.08 to 0.37 % | phase 10, windowed 2 |
| 30 s joins (fit speakers / validation speakers) | 100 / 100 (one per chapter) | 2 / 3 | 2.0 % / 3.0 % | 0 to 5.0 % / 0 to 7.0 % | phase 4, windowed 1 |
| 120 s joins (fit speakers / validation speakers) | 30 / 30 (one per chapter) | 5 / 4 | 16.7 % / 13.3 % | 3.3 to 30.0 % / 3.3 to 26.7 % | phase 4, windowed 5 |
| the fast read alone, the same 6,260 clips | 2 | 0.03 % |
False detections concentrate in particular recording sessions. Four of the 184 chapters among the short clips produce 85 of the 97 short-clip flags, and in those sessions most clips are flagged:
- 441-128982: 16 of 24;
- 441-128988: 24 of 29;
- 441-130108: 22 of 30;
- 1963-142776: 23 of 23.
Speaker 1963's other chapter flags 0 of 34. It is the recording, not the voice.
A clip-level rate therefore understates the uncertainty: the Wilson intervals over clips (1.70 to 2.75 % and 0.76 to 1.50 %) assume independent clips, and the bootstrap over chapters above does not. Of the 111 flags in the table, 110 are the batch card's and 109 are on the fallback paths (phase or windowed). The rate is higher at 120 s, where the windowed path supplies 5 of the 9 flags.
What is established and what is not:
- Over budget, as point estimates: the fit speakers' short clips and both 30 s strata.
- Over budget clearly: the 120 s joins.
- Not established as over: the validation speakers' short clips (1.07 %).
watermark.audio.decode is auto: hardened on a usable CUDA device, the fast read otherwise.
The fine-read floor (measured with the laidline-bench audio presence run). Before steps 3 and 4, the batch card's fallback read counts only if the larger of two
message confidences of that read reaches 0.305 (audioseal.FINE_READ_FLOOR): its whole-clip mean |message logit| at
the chosen phase, or its fine-pass confidence, the mean |message logit| over up to 10 s around the most confident 1 s
window. A read removed by the floor is treated as not searched: the streaming card can still detect, and otherwise the
clip is not detected. The streaming card and the phase-0 paths (fast, normalised) are never gated. Both statistics
come from the phase search, so the floor adds no network evaluation. When the floor removes a read that had reached its
phase or window threshold, the full-tier signal records it in extra:
{"fine_read_removed": {"count": 1, "reason": "..."}}, also when the streaming card then detected.
fine_read_removed.count is a count of removed reads (today always 1, the batch card), not of lost detections: a clip
the streaming card still detects carries it too (on the 3,700 marked measurements below, 92 signals carry it and 44
detections were lost). The public tier shows nothing of extra, and hides an unconfirmed fallback-path presence ("Audio presence on the public tier" below).
The floor governs the 16-bit presence verdict only. In keyed mode
with the keyed audio payload and deep scanning enabled (watermark.audio.payload.hardened_read), the payload read is
started by the hardened detection without the floor, so a keyed mark whose fallback read the floor removes can still yield
a verified payload: the signal then shows the payload, the path before the floor in decode_path and extra.fine_read_removed,
and the public tier shows presence (an eligible own-issuer head). With deep scanning off (the
default) or for a 16-bit mark, a fallback read the floor removes is not shown: the one
issuer-exact read lost on the 37 marked rows below and the fresh 3 s mark inside 30 s whose issuer-exact read was lost
(below) leave the public tier too.
The floor was fitted on marked data only: over the 1,572 issuer-exact batch-card fallback reads of the fast-search marked traces, the lowest of the larger statistic is 0.30728 (LibriSpeech 2817-142371-0036, batch mark under 20 dB noise with an 8 ms offset; whole clip 0.30728, fine pass 0.30711), and the floor is the largest 0.005 step not above it. The hold-outs below had been examined under an earlier, rejected gate but never under this one, and the floor was fixed by the fit rule before they were replayed. Unmarked LibriSpeech read speech, the whole hardened decode at 0.5, Wilson 95 % intervals (independent clips) and a bootstrap over chapters (false detections cluster in recording sessions):
| unmarked clips | clips | before the floor | with the floor | bootstrap over chapters | with the floor, by path |
|---|---|---|---|---|---|
| single utterances of test-clean and test-other (1.3 to 35 s, median 5.5 s) | 5,559 | 38, 0.68 % (0.50 to 0.94 %) | 22, 0.40 % (0.26 to 0.60 %) | 0.14 to 0.71 % | fast 9, phase 8, windowed 5; streaming card 21 |
| 30 s joins of one chapter | 300 | 16, 5.33 % (3.31 to 8.49 %) | 2, 0.67 % (0.18 to 2.40 %) | 0 to 1.68 % | fast 2 |
| 120 s joins of one chapter | 240 | 34, 14.17 % (10.32 to 19.14 %) | 3, 1.25 % (0.43 to 3.61 %) | 0 to 2.87 % | fast 1, phase 1, windowed 1 |
| LJ Speech 1,000, VCTK passages 217, Goldberg (solo piano) 30 | 1,247 | 0 | 0 (0 to 0.31 %) |
The table is the bench that fitted the floor, an earlier run. In the 2026-10 null campaign the shipped build, floor included, flagged these unmarked units on the full tier of an unkeyed deployment: single utterances 39 of 5,000 (0.78 percent), 30 s joins 33 of 5,000 (0.66 percent), 120 s joins 74 of 5,000 (1.48 percent) and music 654 of 4,979 (13.1 percent). The full tier shows more than the public tier by 30, 29, 62 and 293 units (full tier minus public tier: 39 less 9, 33 less 4, 74 less 12 and 654 less 361), the presence the hardened fallback paths add, so on music they add 293 of 4,979 clips (Benchmarks).
What is left is mostly phase-0 reads and streaming-card reads, which a batch-card floor cannot touch. On the fast-search unmarked traces (seen while the floor was designed, so a prediction only) the floor leaves 44 of the 116 flags.
What the floor costs on marked audio:
- The 37 marked rows (3,700 measurements): 44 of 3,597 detections lost. One was an issuer-exact read (1 of 3,192: batch mark, music bed at 10 dB with an 8 ms offset); the other 43 named the wrong issuer (music-bed and streaming G.711 rows).
- Fresh 12 s batch marks inside 120 s of unmarked speech (30 clips, LibriSpeech train-clean-100): 30 detected with and without the floor (24 issuer-exact); nothing lost.
- Fresh 3 s batch marks inside 30 s of unmarked speech (30 clips): 30 detected without the floor (27 issuer-exact), 27 with it. Of the 3 lost, 1 was an issuer-exact read (1 of 27, 3.7 %, Wilson 0.66 to 18.28 %). The other 2 were not reads of the mark: their bits were 3 and 4 bits off the payload.
Limit (published as measured). A mark shorter than the 10 s fine-pass excerpt, inside long unmarked audio, is protected only as far as it was measured: 29 of 30 fresh 3 s marks inside 30 s and 30 of 30 fresh 12 s marks inside 120 s kept what the decode read without the floor. On the 3 clips it lost, the 10 s fine-pass excerpt missed the mark and read unmarked speech 0.1 to 6.7 s away from it. The excerpt is placed by the phase-0 detection scores, where a short off-grid mark can lose to the unmarked audio around it. A repair (the fine pass read around the window that detected) is not part of Laidline today.
The product's replay reproduces the floor's verdict on every record it graded: 31,726 saved statistics (the fast-search
6,440 unmarked and 13,860 marked traces, the hold-outs above, the 3,700 marked measurements, the 120 hazard clips and
the 130 CPU-against-GPU pairs), with 0 disagreements. With the floor at 0.0, the decode equals the decode before the floor on all of them.
watermark.audio.decode is auto (hardened on a usable CUDA device, the fast read otherwise).
Marked (the laidline-bench audio run with the harness's --hardened setting, GPU; every decode records its statistics, and the figures are the
product's decision function, audioseal.replay, applied to them at the shipped constants). Two sets of n=100
LibriSpeech test-clean clips, batch and streaming marks: bench2 (seed 0, the offset, splice and volume sweeps; its
first 50 clips by a seeded shuffle fitted the window length and KEEP_MARGIN) and fresh (seed 20261010, the first
seed whose sample shares no clip with bench2, fitted on nothing; on it the product's own decode and the replay agreed on
every measurement). Issuer exact means detected with all 16 bits right; the fast read is at 0.5. Counts of 100, hardened with its
Wilson 95 % interval.
| row | bench2: fast decoder / hardened exact | fresh: fast decoder / hardened exact |
|---|---|---|
| batch offsets 1 to 50 ms | 0 / 98 to 100 each | 0 / 99 to 100 each (lowest 8 ms: 99, 94.6 to 99.8) |
| streaming offsets 1, 2, 8, 12, 18 ms | 0 / 99 to 100 each | 0 / 100 each |
| streaming offset 5 ms | 0 / 99 | 0 / 94 (87.5 to 97.2) |
| streaming offset 10 ms (trim_10ms) | 0 / 99 | 0 / 94 (87.5 to 97.2) |
| streaming offset 15 ms | 0 / 96 | 0 / 94 (87.5 to 97.2) |
| streaming offset 30 / 50 ms | 0 / 98, 99 | 0 / 96, 98 |
| crop_middle_50, batch / streaming | 5, 4 / 100, 100 | 8, 6 / 100, 100 |
| volume -20 dB / -30 dB (batch) | 55, 0 / 100, 100 | not run |
| splice, marked share 25 % / 10 % (batch), detected | 1, 1 / 100, 54 | 0, 0 / 100 (96.3 to 100), 54 |
| splice_half, streaming | 4 / 88 | 2 / 91 |
| opus_12, batch / streaming | 94, 70 / 94, 70 | 92, 55 / 91, 55 |
| opus_24, g711, mp3_64, aac_64 (both modes) | 100 / 100 (streaming g711 0 / 0) | 100 / 100 (streaming g711 0 / 0) |
| noise_20db / music_bed_10db (batch) | 32, 3 / 58, 18 | 40, 3 / 67, 22 |
| reverb, batch / streaming | 3, 30 / 3, 30 | not run |
Wrong issuer among detections, pooled over every measurement: bench2 the fast read 1,801 of 4,387 (41.1 %), hardened 381 of 5,578 (6.8 %, 6.2 to 7.5 %); fresh the fast read 1,618 of 3,012 (53.7 %, more offset rows), hardened 226 of 4,078 (5.5 %, 4.9 to 6.3 %). The fallback paths add wrong-issuer detections, on the fresh set 157: batch music_bed_10db 72 and noise_20db 24, streaming g711 26 (detected, never with the right issuer), streaming splice_half 9, streaming offsets 5, 10, 15, 30 and 50 ms 6, 6, 6, 4 and 2 (the trim_10ms row is the 10 ms point), and 1 each on two more rows. The wrong issuers under a music bed, under noise and after streaming G.711 cannot be screened by the decode: the 16-bit payload has no checksum. Reads that the fast read had right and hardened lost: bench2 3 of 1,292, fresh 2 of the fast-read-exact reads (batch opus_12 1, batch noise_20db 1); no codec row falls below the fast read by more than one clip.
The reader rule on the fresh set (same clips and statistics, only the reader differs): under the earlier rule streaming offsets of 5, 15, 30 and 50 ms read 81, 74, 85 and 85 and trim_10ms 82; under the shipped rule 94, 94, 96, 98 and 94. Pooled, the earlier rule gives 3,779 exact and 299 wrong of 4,078 detections, the shipped rule 3,852 and 226.
Missed target (target: issuer exact at least 95 % for start offsets of 1 to 50 ms in both modes): streaming offsets of 5, 10 and 15 ms reach 94 % on the fresh set (bench2: 99, 99 and 96). Every other listed row meets its target on both sets.
Phase grids and cost. The phase search reads all 320 phases on a short excerpt and the best few on a longer one. Two grids were measured:
| grid | coarse pass | fine pass | CPU, 2 threads, 6 clips of 6.3 s: detected / unmarked (median) |
|---|---|---|---|
| full (shipped) | 320 phases on 1 s | best 8 and phase 0 on up to 10 s | 11.8 s / 23.8 s |
quarter-second grid (code name fast; not the fast decoder) |
320 phases on 0.25 s | best 4 and phase 0 on up to 3 s | 2.9 s / 5.8 s |
The fast read took 0.09 s (detected) and 0.18 s (unmarked) on the same clips; the latency budget is 0.1 x duration + 1.5 s, 2.1 s here, so both grids are over it. Measured at a 1-minute load average of 1.9 to 3.5. On the bench's RTX 5080 the full grid took a median of 0.69 s per measurement the fast read detected and 1.06 s per miss. The hardened audio decode is GPU-recommended: on a CPU it costs about 12 s per detected 6 s clip and 24 s per unmarked one.
The fast grid has its own thresholds, fitted on LibriSpeech dev-clean (2,703 utterances, unseen by every earlier fit or
validation; same rule, fit half 1,351): phase 0.28 / 0.62, window 0.72 / 0.98, 1 s windows and KEEP_MARGIN 0.05
carried over. Its validation half (1,352, evaluated once) flagged 3 (0.22 %, 0.08 to 0.65 %). A floor on the streaming
card's phase-0 share, which would have skipped its search on most unmarked clips, came out at 0 on the bench2 fit half
(streaming marks at bad offsets have phase-0 share 0), so it saves nothing. The decision rule, stated before the run:
the fast grid replaces the full one only if it loses at most one issuer-exact read per row against it on the fresh set
and meets the false-detection budget. It met the budget but lost 12 reads on the 25 % splice (100 to 88), 10 on the
10 % splice, 4 on streaming splice_half, 3 under the music bed and 2 at the 8 ms offset, so the full grid stays and the
fast grid is not offered.
The cell gate for keyed P70 audio and the located path
Applies to keyed deployments with the P70 payload (payload.layout: audioseal-p70-r1) under decode: hardened only.
Unkeyed, 16-bit and payload.layout: none deployments decode exactly as before (the constants are resolved per
configuration; the shipped ones are unchanged).
Why. The fine-read floor's statistic (the clip-mean message logit) was fitted on the 16-bit mark. The keyed P70 mark changes its 16-bit pattern every 1.0 s cell, so the clip mean cancels on short marks inside longer audio. The phase, window and located paths were found to reach the mark while the floor removed the read (for example a 1 s mark in 10 s: 73 of 100 found).
The cell gate. For the batch card's fallback read, the decode computes the largest per-segment mean |message logit| over isolated 1.0 s reads at the chosen phase. The reads cover 12 s centred on the best 0.5 s window of the detection shares there, in one batched detector call. A read the fine-read floor would remove stands when this statistic reaches 0.305. The OR can only admit: a detection of today's decode always stands. After the windowed path, a located path fires when the best 0.5 s window share at the chosen phase reaches 0.80 and the read stands. A located presence is a fallback path, hidden on the public tier unless it is confirmed ("Audio presence on the public tier" below).
The fit. Fitted on unmarked audio only: 300 30 s joins, 240 120 s joins and 1,000 single utterances of LibriSpeech test corpora, the fine-read floor's former hold-outs, now spent for audio presence statistics.
- The rule: the smallest 0.005 step strictly above the largest statistic of an unmarked read that reached a phase, window or located threshold, never under the floor's 0.305.
- The largest such read was 0.140, so the floor is 0.305.
- The largest statistic of any unmarked fit unit, not reached, is 0.343. The conservative floor 0.345 would cost 0 to 1 found mark on the measured rows.
Hold-outs (fresh speakers, evaluated once, at 0.305):
- 0 of 200 30 s joins (Wilson 0 to 1.9 %);
- 0 of 100 120 s joins (0 to 3.7 %);
- 0 of 547 clips of LJ Speech, VCTK and solo piano (0 to 0.7 %).
These equal the fine-read floor alone on every set: the cell gate added no flag. At these sizes the increment is resolvable only to about 1 % of clips.
Keyed P70 marks at alpha 1.41 inside unmarked speech, found (full tier), before (the fine-read floor alone) and with the cell gate and located path:
| mark inside host | before | with the cell gate |
|---|---|---|
| 2.5 s in 10 s | 63 of 100 | 100 |
| 7.5 s in 30 s | 94 | 100 |
| 24 s in 120 s | 96 | 99 |
| 1 s in 10 s | 73 | 100 |
| 3 s in 30 s | 93 | 98 |
| 12 s in 120 s | 86 | 98 |
| 1.5 s in 30 s | 86 | 98 |
| 6 s in 120 s | 89 | 100 |
| Opus 24 kbit/s, 3 s in 30 s | 73 | 92 |
| Opus 24 kbit/s, 12 s in 120 s | 88 | 96 |
| Opus 24 kbit/s, 1 s in 10 s (reported) | 30 | 78 |
| 0.5 s in 10 s (reported, not promised) | 1 | 80 |
| 0.5 s in 30 s (reported) | 8 | 69 |
| 0.3 s in 10 s (reported, 60) | 0 | 0 |
| Opus 12 kbit/s, 3 s in 30 s / 12 s in 120 s (reported, 60 each) | 11 / 31 | 22 / 39 |
The payload inside long clips. Under decode: hardened a span read
reads it (watermark.audio.payload.span_trigger, default auto: on exactly when the decode resolves to hardened).
- Why the first read failed. The earlier span rule (a span-triggered read at the searched phase) read 63 to 66 of 100 against a line of 90 and was dropped. The search's fine pass compares phases by a 10 s mean of the message logits. A P70 mark changes its 16-bit pattern every 1.0 s cell, so that mean cancels, and the fine pass lands on a side lobe of the detector's response on about a third of marks. The 70-bit read accepts from about one sample early to four late. Of the earlier span rule's 137 failures, the searched phase was within 4 samples of the mark's phase on 0, within 8 on 2, within 16 on 95 and within 32 on 129; 8 were further off. The coarse pass (every phase on 1 s) still ranked the true phase first on 122 of the 137.
- The span read. On the best 12 s window of the block shares at the searched phase (the trigger: that window's share is at least 0.5 and stage 1 did not detect), isolated reads on three grids at the coarse pass's best phase, then at its best phase more than 8 samples away, then at the searched phase, stopping at the first read after which the arbiter chooses. No detector call is added to the search; a hit costs one read, as the earlier span rule's did.
- Measured on fresh units (new seeded offsets over the donors and hosts of the earlier measurement: the phase under test is fresh, the speakers repeat; disclosed as a hold-out by phase, not by speaker). Lines: 90, 90 and 95 of 100, unchanged from the earlier span rule. A first draw measured the coarse best phase with the searched phase as fallback; it read 97, 86 and 93, UNPROVEN, and stays as measured. A second fresh draw measured the span read, chosen after the first draw's data; its predictions (99, 93, 98, 95) came from the first draw's measured counts and were registered before any of its units was read, with the sequential form adopted before data:
keyed P70 at alpha 1.41, decode: hardened |
span read | line | earlier span rule, same units |
|---|---|---|---|
| 12 s in 120 s | 98 | 90 | 67 |
| Opus 24 kbit/s, 12 s in 120 s | 93 | 90 | 72 |
| 24 s in 120 s | 98 | 95 | 64 |
| 9 s in 30 s (reported) | 98 | 75 |
Every binding row passed; no unit read fewer than the earlier span rule's read; 0 wrong payloads. The read forced on 1,887 unmarked clips gave 0 own-accepts and 0 eligible tops; the trigger fires on 1 of 840 unmarked joins, as before. Presence is unchanged: the presence bodies compare equal before and after the read; the recorded units' statistics round-trip through the product's records and replay to the recorded paths, and on 88 flagged unmarked clips the decode result is the same before and after the read. On 2 CPU threads the read adds 0.02 s on a hit at the first peak (line: at most 0.1 s) and 2.9 s when it reads all three phases, against about 55 s for the hardened decode of a 120 s clip (Benchmarks). Under 8 s of delivered audio, or an edited start in a clip under 8 s, the payload needs deep scanning (the floor of a whole-clip read is 8.0 s on the 20 ms grid; claim 10 s).
The phase search (off by default). A mark that does not start on a 20 ms boundary of the clip (a delayed or trimmed
clip, a codec's lag) is read by a keyless search over the 320 phases (watermark.audio.payload.phase_search, under the
hardened decode only). The v2 voice matrix of 2026-10-30 measured it (19,850 units, 100 keyed
marks per cell, 13,000 unmarked units, 34 lines registered before any read): 31 passed, 3 failed, so the search ships off.
With payload.phase_search: on (ships off), pooled over two runs (n 200 per cell), a 9.5 s excerpt off the grid reads 194
of 200 (6 of 100 with it off), a delayed 10 s clip 191 of 200 (0 of 50 for most delays), G.722 192 of 200 (0 of 100) and AMR-WB 200 of 200 (0 of 100). The failed lines: on the unmarked units the search ran on 35 reads and chose a keyless payload twice
(the Opus 12 kbit/s null row and the packet-loss repeat row; the tag failed on both, 0 tag accepts on 13,000, and the line allows no choice even
then; pooled with the core voice matrix, 2 of 26,000 units), and the detect time ran over its budget (0.1 x duration
- 1.5 s at p95) in 5 of 74 cells pooled, all on an L4 GPU (the A10G passed every cell; on the L4 alone G.722 is over too). Since 2026-10-30 the latency line is bound to the A10G, the campaign's reference GPU for voice, and the L4 numbers stand as a recorded deviation; the "wrong payloads 0" line is read strictly, as the null-row line is: 0 wrong payloads were shown (the issuer suppression hides a word whose tag failed), but 27 of the 6,850 marks produced an internal keyless pick that the tag then rejected (15 at phase 0, 12 from the search, all in degraded cells: Speex, Opus 8k, reverb, white noise), so the strict line fails and both numbers are reported. The false-accept bound in Payload keys is a worst case and counts the search's words whether it is on or off.
Cost. Measured on 2 CPU threads, reserved cores, 7 runs, p50.
- The cell gate adds 0.24 to 0.31 s per fallback decode: 0.004 core-seconds per second of audio on a 120 s clip.
- The hardened decode itself costs 41 s per 30 s clip and 54 s per 120 s clip (GPU recommended).
Which figures apply to which deployment (watermark.audio.decode, default auto):
autoresolves tohardenedwhen the audio detector runs on a usable CUDA device and tofastotherwise. Underwatermark.device: auto, a host with a usable GPU detects audio on it; audio marks are still embedded on the CPU. The figures in this section were measured with GPU detection at full float32 with deterministic cuDNN, the product's default (watermark.cuda_precision: strict); the regression checks run detection on the CPU. On 350 hard saved clips the GPU and the CPU made the same decisions (presence, payload, tag, grade, third party), 0 of 350 differing; the path differed on 1 and soft outputs differ slightly between devices (Robustness baseline, "GPU against CPU, as shipped").- The figures above, and every excerpt figure, are
hardenedfigures. Underfast, a mark inside a longer clip is found only when the whole-clip decode detects it. - A CPU-only deployment uses the fast read unless
hardenedis set explicitly, accepting the cost above.
Audio presence on the public tier
On the public tier (keyed mode; Payload keys, "Response tiers") an AudioSeal signal shows
presence only when it was read on a phase-0 path or is confirmed. The phase-0 paths are decode: fast (the signal has no
decode_path) and the hardened decode's fast and normalised paths (audio_check.PHASE0_DECODE_PATHS). Confirmed
means a public grade B. The 16-bit AudioSeal signal resolves no asset and never grades B, so on it only the phase-0
paths show. Keyed P70 audio is confirmed (by bytes or by the registered asset's fingerprint record;
"Keyed audio" above), but its public body is presence-only whatever the grade: it shows
presence for a 16-bit detection of the batch card on a phase-0 path, a chosen eligible own-issuer head (every B has one)
or another deployment's head, and for no other fallback-path read and no streaming-card detection.
The streaming card in a keyed P70 deployment. Laidline's keyed marks are written by the batch generator, which the
streaming detector card (audioseal_detector_streaming) does not read; in a keyed P70 deployment that card can only
confirm another deployment's mark or an unkeyed stream's. Its detection alone (a phase-0 read with no own or
third-party head behind it) is therefore not public presence: the public body is an undetected clip's. The full tier
still reports it and adds the caveat that says why. The 2026-10 null campaign (Round M) found the cause: on unmarked
audio, 360 of the 361 public false presences on FMA music (7.25 % of 4,979 clips, the Round M unmarked-music rate), all
25 on read speech and 177 of the 192 on FSD50K sounds came from that card (keyed read). Replaying the saved records with the rule
gives music 2 of 4,979, read speech 0, and sounds 15 of 5,000, with 0 of the campaign's positives losing public
presence. These are replayed counts, not a new run of the product. The rule is
for keyed P70 deployments only; an unkeyed or payload.layout: none deployment still shows a streaming-card detection,
because the unkeyed stream mark is the one that card reads (not measured here: the campaign has no unkeyed stream
positives).
A presence found only by the fallback paths (phase, windowed, or any later path name) and not confirmed gives the
public body of an undetected clip, byte for byte: the signal is absent, the verdict is recomputed without it, and the
top-level caveat "an AudioSeal mark names no asset; ..." is dropped. A verified tag alone does not count as
confirmation: a verified and a failed tag give one public body.
The full tier keeps the grade and every field. On a fallback-path presence it adds, last in the signal's caveats:
"This presence was read only by the hardened decode's fallback paths (phase search or windowed read). The audio mark
and its decoder are built for speech and conversations of 8 s and more, and music is a known bound: on unmarked audio
the fallback paths alone flagged 30 of 5,000 single utterances, 29 of 5,000 30 s joins, 62 of 5,000 120 s joins and
293 of 4,979 music tracks (2026-10 null campaign), so this presence alone is weak evidence; the public tier does not
show it unless it is confirmed".
The figures are the 2026-10 null campaign's, read from the packaged result (compliance/data/nulls_audio.json: fallback_only_utt, fallback_only_join30, fallback_only_join120 and unkeyed_fallback_music, each the full tier minus the public tier at the unkeyed point), so a new run changes the sentence with the data. The public tier never shows that sentence. Unkeyed mode serves the full result to every caller (no projection), so there the sentence is the only change.
Music is stated as a known bound of the audio mark, which is built for speech and conversations of 8 s and more (the payload reader returns nothing under 8 s). The earlier wording quoted the earlier bench's rates (0.40 %, 0.67 % and 1.25 % of read speech, and 0 of 1,247 clips of other speech and music); the campaign above showed the last clause did not hold for music, and the sentence no longer makes it.
Measured, earlier run (the hardened decode at 0.5 before the fine-read floor and with it; H-short from
the single-utterance set measured). The saved statistics were replayed through
the product's AudioSealWatermarker._result, audio_watermark_signal and tiers.public_signal
(a replay check over the saved statistics). Its counts equal the measured public-tier
column, keyed and unkeyed. With the floor shipped, the full tier gives the with-floor column, and
the public tier's counts are unchanged. Wilson 95 % intervals assume independent clips; false detections cluster in
recording sessions (above).
| unmarked LibriSpeech read speech | clips | full tier before the floor: flagged | of which fallback paths only | full tier with the floor (shipped) | public tier, either way |
|---|---|---|---|---|---|
| H-short: single utterances of test-clean and test-other (1.3 to 35 s, median 5.5 s) | 5,559 | 38, 0.68 % (0.50 to 0.94 %); fast 9, phase 22, windowed 7 | 29, 0.52 % (0.36 to 0.75 %) | 22, 0.40 % (0.26 to 0.60 %); fast 9, phase 8, windowed 5 | 9, 0.16 % (0.09 to 0.31 %), all fast |
| H-join-30: 30 s joins of one chapter | 300 | 16, 5.33 % (3.31 to 8.49 %) | 14, 4.67 % (2.80 to 7.68 %) | 2, 0.67 % (0.18 to 2.40 %); fast 2 | 2, 0.67 % (0.18 to 2.40 %) |
| H-join-120: 120 s joins of one chapter | 240 | 34, 14.17 % (10.32 to 19.14 %) | 33, 13.75 % (9.96 to 18.68 %) | 3, 1.25 % (0.43 to 3.61 %); fast 1, phase 1, windowed 1 | 1, 0.42 % (0.07 to 2.32 %) |
| other corpora: LJ Speech 1,000, VCTK passages 217, Goldberg (solo piano) 30 | 1,247 | 0 | 0 | 0 | 0 |
What the public rule costs on marked audio: of 3,700 marked measurements (the M-acc rows), the full tier detects 3,597 before
the floor (3,553 with it) and the public tier shows 1,787 either way (fast 1,690, normalised 97). The others are
fallback-path detections, among them every detection of a trimmed, offset or spliced mark that only the search finds;
they stay on the full tier.
Audio in the 2026-10 null campaign
Measured on 2026-10-28 with the shipped hardened decode at threshold 0.5. The unmarked strata were 5,000 single utterances (LibriSpeech and Multilingual LibriSpeech), 5,000 joins of 30 s and 5,000 of 120 s (LibriSpeech), and 4,979 decodable 30 s music clips (Free Music Archive; 21 more were unreadable and are counted as errors, 0.42 percent). Two more strata were added after the lines were sealed and have no line: 5,000 sound clips (FSD50K) and 2,000 clips of synthetic speech. Each unit was read as an unkeyed deployment and as a keyed one.
An unkeyed deployment (16-bit mark, presence only), public presence on unmarked audio:
| stratum | k of n | Wilson upper | line | result |
|---|---|---|---|---|
| single utterances | 9 of 5,000 (0.18 %) | 0.34 % | 0.5 % | met |
| joins of 30 s | 4 of 5,000 (0.08 %) | 0.21 % | 0.5 % | met |
| joins of 120 s | 12 of 5,000 (0.24 %) | 0.42 % | 1 % | met |
| music | 361 of 4,979 (7.25 %) | 8.00 % | 0.5 % | not met |
| sounds (no line) | 191 of 5,000 (3.82 %) | 4.39 % | reported | |
| synthetic speech (no line) | 2 of 2,000 (0.10 %) | 0.36 % | reported |
360 of the 361 music hits are on the streaming detector card, which an unkeyed deployment still shows (the unkeyed stream mark is the one that card reads). The rule below fixes keyed deployments only, so an unkeyed deployment still flags 7.25 percent of unmarked music as present. Where the genre is known the rate runs from 0 (classical, 113 clips) to 12 percent (rock, 452 clips); loud, compressed, bright masters carry it (18.1 percent in the loudest fifth of clips, 1.9 percent in the quietest).
A keyed deployment (the shipped keyed read), tag accepted on unmarked audio: 0 of 5,000 in each speech stratum and 0 of 4,979 on music (Wilson upper bound 0.077 percent against a line of 0.15 percent, met), and 0 of 26,979 unmarked keyed reads in all, the sound and synthetic-speech strata included. Public presence under the streaming-card rule (above): 0 on all three speech strata, music 2 of 4,979 (upper bound 0.15 percent, met against 0.5 percent) and sounds 15 of 5,000 (0.49 percent; reported, no sealed line). These counts replay the stored records through the rule; the rule was chosen on the same records.
In flight.
- A presence is not a payload. A clip can be flagged present with no payload chosen, and the public tier shows only presence; the full tier shows the rest and the caveats.
- The 16-bit word has no error detection: 42 of the 600 unkeyed positives read as another issuer's number at high confidence (40 of them music), through flipped bits. Unkeyed audio therefore shows presence only and names no issuer.
- Keyed, a payload is shown only when its tag verifies. One keyed positive (a music clip) produced a keyless pick naming another issuer; its tag failed under every live key and the public tier showed presence only.
- Detect time is longer for unmarked audio: unkeyed, marked units decode in about half the time of unmarked ones (single utterances 441 against 880 ms at the median) because a fast-path detection stops the search early; keyed, a verified own mark takes about 0.8 s more than a presence with no chosen payload (1,344 against 545 ms on single utterances). Whether a payload head is chosen does not depend on a key (the public weights suffice). See Payload keys for the residual.
- Throughput: the median per unit is 0.88 to 1.78 s unkeyed and 0.94 to 2.28 s keyed on a GPU (GPU utilisation mean 92 percent); the p95 of an unmarked single utterance is 942 ms.
- Deep scanning (
payload.hardened_read: auto) was tried as an add-on for keyed music and rejected: 0 of 33 recovered, 26 payloads chosen on 4,979 unmarked music clips and 3 positives labelled with another issuer's number. It stays off.
Positive sensitivity. 1,200 blind positives of issuer 1 (unkeyed 600; keyed 600, of which 300 marked as whole files and 300 as a stream). The sealed line was 97 percent on unedited clips:
| arm | unedited | Wilson 95 % | result |
|---|---|---|---|
| unkeyed, pooled | 393 of 420 (93.6 %) | 90.8 to 95.5 % | not met |
| keyed, pooled | 375 of 420 (89.3 %) | 86.0 to 91.9 % | not met |
| keyed, whole file | 178 of 210 (84.8 %): speech 95 %, music 45 of 70 (64 %) | 79.3 to 89.0 % | not met |
| keyed, stream | 197 of 210 (93.8 %) | 89.7 to 96.3 % | not met |
The misses have three causes, none a construction defect. (1) The payload needs 8 s: all 13 stream misses are shorter
than 8 s, and across the keyed speech positives 0 of 167 shorter than 8 s verified, against all 34 of 8 to 10 s and all
469 of 10 s or more. On positives of at least 9 s the stream read 196 of 196 and whole-file speech 132 of 132. (2) Keyed
music: 33 of the 100 keyed music positives are present but the default read chooses no payload (deep scanning is off).
(3) Unkeyed music: 16-bit bit errors, above. After edits, a whole-file keyed mark was verified on 77 (86 percent) of the 90 clips after MP3 at 128 kbit/s,
and a keyed stream read 26 of 30 after the MP3 twin and 26 of 30 after Opus at 32 kbit/s (87 percent
each); a 2 s barge-in kept presence and read no payload (0 of 30). Keyed grade B was always by fingerprint, never exact
bytes, and an unedited copy of a registered 30 s or 120 s asset is reported as containing a registered region
(contains_registered_asset), because the fingerprint spans the read span plus 4 s rather than the clip. Of 200
synthetic-speech stream positives 63 verified and all 137 misses were shorter than 8 s. The scope that follows from this
is stated in Audio.
Video (VideoSeal)
The 92-bit payload names the issuer and a 48-bit asset sequence and is protected by a BCH code
and a CRC-16; a signal is present only when both pass. As for images, a decoded payload counts only when a registry hit
is confirmed: the query's exact bytes, or the phash64/v1 fingerprint of its frame at 1 s (at half the video when it is
shorter) within detect.fingerprint_max_distance of the recorded one. Only videoseal entries are searched. The frame
is picked from the frame count and rate, so a re-encode that keeps the frame rate picks the same frame; a black or title
frame at 1 s makes that confirmation weak. The signal carries frames_sampled and an informational bit_accuracy: the
share of the 220 transmitted bits (the 92 payload bits and the 128 BCH bits; the known-zero padding is not counted)
that agree with the codeword of the decoded payload. No unmarked clip decoded to a payload in the 2026-10 null campaign (0 of 4,854; the earlier bench had 0 of 43), and the analytic chance for a
random read is 2^-60.9: within t=16 of a codeword over the 216 bits the BCH decoder reads (124 parity bits), then the
CRC-16. The caveat a VideoSeal result carries (below) says 2^-64.4; that figure assumed 220 code bits and 128 parity
bits and was 3.6 bits optimistic, so use 2^-60.9.
With watermark.video.decode: hardened (the default; Configuration) every fast-decoder decode is kept. When the fast decoder finds
nothing, the detector runs three soft attempts: the mean logits, VideoSeal's l1norm_avg vote, and 4 contiguous
windows of the sampled frames. Each soft input flips every combination of the chase_flips (default 10) least
reliable of the 216 read bits, and a candidate counts only when strict BCH (t=16, no correction in the padding) and the
CRC-16 both pass. A payload is accepted only when every soft input that yields one yields the same one: two distinct
payloads anywhere (within an input, between attempts or between windows) give none. A non-finite logit on a read bit
in any frame means inference is broken and gives none. The rest of the rules are unchanged: the payload still needs a
confirmed registry hit for grade B. The signal adds:
decode_path:fast,soft_mean,soft_l1normorsoft_windows(the first attempt that found the payload), orambiguous,non_finiteornullwithout a payload;scoreandnormalised_score: of the decoded codeword over the 216 read bits of the input it came from. They are informational and no threshold is applied. Onsoft_l1normthe signed score is rescaled by the mean frame weight, so signed scores are not comparable between paths;attempts: the attempts run (1 forfast, 4 once soft decoding ran, 0 fornon_finite).
What is and is not established for hardened:
- Analytic bound. The decoder tries up to 1 + 6 x 2^
chase_flipswords per video: 6,145 at the default 10, which gives 2^-48.3, and 24,577 at 12, which gives 2^-46.3 (2^-60.9 per word, a union bound). This is an analytic bound for uniformly random reads, not a measurement. - Null draws. 200,000 random reads (random signs, magnitudes drawn from the unmarked clips' mean logits) gave no accepted word, and not even a BCH-valid one. They test the same random-read model, so they only bound the per-draw rate at about 1.5e-5 (95 percent).
- Real unmarked video. The evidence is the 2026-10 null campaign: 0 of 4,854 unmarked 5 s clips at each of three reads (VideoSeal presence, keyed VideoSeal tag, keyframe tag), Wilson 95 percent upper bound 0.079 percent at each (Benchmarks). The earlier bench, 0 of 43 clips from one corpus (Xiph derf, upper bound 8.2 percent), is kept as a dated record.
- No score gate. None is set for video because none can be fitted from this data: no wrong or phantom candidate was found to separate from the right ones. That is unproven, not shown to be unnecessary.
- What was measured before
hardenedbecame the default: 200,000 null draws and 200,000 full-search draws accepted nothing (below), and keyed detection on 1,740 unmarked clips graded none B or C (Payload keys, "Video"). Clips marked with foreign payloads were measured for the keyframe scheme (99 clips, "Video (keyframe TrustMark)"), not separately for VideoSeal; that row stays open.
Measured offline on the bench arrays (the saved float16 per-frame logits) by
the laidline-bench video robustness rescoring: 3,141 exact decodes of 3,956 marked clip-rows against 2,941 for the fast decoder
(fast 2,941, then soft_mean 133, soft_l1norm 54, soft_windows 13), none lost, no wrong payload, no ambiguous or
non_finite row, and 0 of 43 unmarked clips decoded. Run alone over the same rows
(hardened_attempts_alone), the attempts find 2,941, 3,074, 3,124 and 2,998 exact decodes with no wrong payload, and
no row has two attempts finding different payloads. A live bench run with the harness's --hardened setting reads float32 logits and may differ
at the rounding edge.
Geometry search (watermark.video.geometry_search, on by default under hardened). When the hardened decode of
the frames as received finds nothing (not ambiguous, not non_finite), the detector reads the same sampled frames
again under up to 19 hypotheses: the picture box inside uniform-colour borders that every sampled frame agrees on
(letterbox and pillarbox bars, a screen capture's canvas), cut out and resized as a frame is; then the frame taken as
90 and as 80 percent of each side of the original, pasted at 9 anchors on mid-grey (what a centre, corner or edge crop
removed). Each hypothesis goes through the full hardened decode above, and a payload is accepted only when every
hypothesis that yields one yields the same one; two different payloads give none (ambiguous). The signal then has
decode_path geometry, hypothesis (the first hypothesis that gave the payload, such as box or
s0.8_top_left), hypothesis_path (its hardened path) and hypotheses (how many were tried); attempts adds every
hypothesis's attempts to the base decode's. hypothesis, hypothesis_path and hypotheses are absent when the
frames as received decoded, so a consumer must treat them as optional. The grading rules do not change.
What the search adds to the false-decode risk: every hypothesis is another 1 + 6 x 2^chase_flips candidate words, so
the analytic union bound for a uniformly random read becomes 2^-60.9 x 20 x 6,145 = 122,900 words, 2^-44.0 at the
defaults. On the bench (43 Xiph derf clips, live GPU run with the harness's --hardened-geometry setting): 0 of 43 unmarked clips decoded through the full search
(Wilson 95 percent upper bound 8.2 percent); no wrong payload on any marked row; no decode lost against hardened.
Exact decodes of 43, hardened then with the search: crop_80 35 then 42, crop_80_corner 29 then 39, letterbox 21
then 43, screen_capture 33 then 43, rescale_0.5 33 then 35; identity 43, h264_crf23 42, rescale_0.75 42, rotate_90 42,
social 33, trim_start_1.3s 40 and middle_2s 40 are unchanged. Null draws for the search (random signs, magnitudes drawn
from the unmarked clips' mean logits, every hypothesis decoded): 10,360 full-search draws, 207,200 hardened decodes,
none with a CRC-valid candidate and none accepted (one-sided 95 percent upper bound 2.9e-4 per draw). The 200,000
reads quoted above are single soft reads from the earlier measurement. The full-search run finished:
200,000 full-search draws (seed 2026, 19 hypotheses at the default chase_flips 10, 4,000,000 hardened decodes),
none with a CRC-valid candidate and none accepted (one-sided 95 percent upper bound 1.5e-5 per draw). Oracle rows first
confirmed that the letterbox and
screen-capture losses are canvas problems: with the true picture box cut back out, 43 of 43 decode on both (against
21 and 34 for hardened on the attack in that run).
The 2026-10 null campaign (rules sealed before any clip was read). The unmarked set was 4,854 decodable 5 s clips:
user-uploaded video 3,041, Wikimedia Commons 1,618, Xiph 93, the project's own 1080p clips 46, open-licence film
footage 56. Each clip went through three reads with the shipped defaults: unkeyed VideoSeal (hardened, with the geometry
search), keyed VideoSeal and the keyed keyframe reader. 40 further clips of 4096 x 2160 were refused by
watermark.video.max_pixels and are not in n (0.71 percent of the set; the same cap stopped 8 positives of that size
from being marked).
In flight. Every read of the 4,854 clips, at all three configurations, returned no_signal_found with grade none: no
CRC-valid payload word, no presence bit, no tag and no score, even after the full hardened chain with its 18 or 19
geometry hypotheses (VideoSeal) or a keyframe scan of up to 281 frames. Presence 0 of 4,854 and tag accepts 0 of 4,854 at
each read (upper bound 0.079 percent; no source had a hit). 872 whole
20 s originals read by unkeyed VideoSeal gave 0 as well (upper bound 0.439 percent). The 120 refused reads (40 clips at
three configurations) came back as an error with the video_too_large caveat, not as a negative verdict. The median
read time is 7 to 12 s for a 5 s clip at 1080p or below and about 32 s at 2160p (16 s for the keyframe reader); the
slowest read took 74 s. A clip carrying the deployment's mark returns in about 0.5 s. Detect is CPU-bound. There was no
timeout, out-of-memory or unreadable file.
Positive sensitivity. 742 blind positives of issuer 1, unedited and after H.264 at crf 23. The sealed line for the unedited clips was 97 percent:
| read | unedited | Wilson 95 % | after H.264 crf 23 | line |
|---|---|---|---|---|
| unkeyed VideoSeal | 169 of 174 (97.1 %) | 93.5 to 98.8 % | 70 of 75 | met on the rate; the stated count of 170 missed by one at n 174 |
| keyed VideoSeal | 172 of 174 (98.9 %) | 95.9 to 99.7 % | 69 of 73 | met |
| keyframe TrustMark | 167 of 173 (96.5 %) | 92.6 to 98.4 % | 70 of 73 | not met |
Two of the keyframe misses were clips the marker never marked (the 38 dB PSNR floor); over the 171 marked unedited clips the keyframe reader read 167 (97.7 percent), which is reported beside the line it did not meet, not instead of it. Wrong payloads and cross-issuer labels: 0 of 742. Every hit graded B by fingerprint, none by exact bytes. The 25 misses sit at about a tenth of the hits' bit rate (median 0.014 against 0.146 bits per pixel and frame), and 8 of the 11 user-uploaded misses are near-black or near-white; 3 of the 16 VideoSeal misses were at the unmarked level, so the mark was effectively erased. The keyframe reads ran on the PyTorch TrustMark reader, not on the ONNX graph the product now mirrors, so the ONNX path was not measured here. Decisions were identical (0 mismatches over 556 pairs) on a second GPU type and on a workstation GPU.
Video (keyframe TrustMark)
The keyframe scheme uses the image watermark and the image rules: the payload voted
over the key frames and the marking positions (a tie gives none) is confirmed against the registry like an image. The
confirmation matters here: on the bench the scheme returned a wrong, well-formed payload for 2.4 percent of clips
after H.264 crf 35 and after a 15 fps re-encode (an earlier run had 4.9 percent at crf 28). The
signal adds frames_sampled, votes and runner_up_votes.
Current false-positive evidence. In the 2026-10 null campaign the keyed keyframe reader (hardened, Q only) accepted
no tag on 0 of 4,854 unmarked clips (upper bound 0.079 percent), and its unedited sensitivity was 167 of 173 against a
line of 97 percent, which it did not meet ("Video (VideoSeal)" above has the table). The unmarked-clip figures below, from
43, 600 and 520 clips, are earlier benches and are kept as dated records. The campaign did not read with decode: fast,
so the fast vote's phantom rate below stands as measured.
The fast keyframe vote (decode: fast) takes each frame's ungated fast-decoder image decode, and one frame can carry it.
Measured on 600 unmarked YouTube UGC clips (CC BY 4.0) with TrustMark on the CPU, the default
device: 3 decode (0.50 %, Wilson 0.17 to 1.46 %; the same 3 clips on CUDA), and on 520 further clips of the same corpus
3 (0.58 %, 0.20 to 1.68 %). Each rests on one phantom frame whose payload version is not 1 (versions 0, 2, 0, 6, 5 and
5), so detect reports present with grade D (signals_present_unverified), no issuer match and no registry lookup, with
the phantom caveat. decode: hardened does not use that vote (below).
With watermark.video.decode: hardened that vote is not run. The same key frames and marking
positions vote through the image hardened decoder and its gates (payload version 1): a frame counts only when that
decoder accepts it, and the vote gives a payload only when exactly one distinct payload was accepted (decode_path
vote; two or more: ambiguous, no payload and no scan). When no vote frame is accepted, it scans every frame within
0.5 s of an expected mark position, rounded outward to whole frames (15 frames, 0.5005 s, at 29.97 fps; at most
max_detect_frames, in stream order) with the same decoder, and accepts a payload only when it is the only one
accepted in the video (two different payloads: ambiguous, none). The signal adds decode_path (vote, scan,
ambiguous or null), attempts (frames the gated decoder read: the vote's, plus the scan's when it ran) and
scan_window_s; votes and runner_up_votes are then counts of accepted frames. Grading reads none of these. The
gates were calibrated on still images, not keyframes, and a frame read at the wrong offset is a misaligned image,
where the image decoder's residual wrong payloads live; on the bench (43 clips, 41 marked) the scan returned no wrong payload on any marked row and no
payload on any of the 43 unmarked clips, and found the payload after a cut off the 1 s grid on 37 of 41 clips
(trim_start_1.3s and middle_2s; the fast keyframe vote 0). It does not help letterbox, screen capture or a quarter turn (0 of 41
each). An unmarked video costs a hardened decode of every vote frame and every frame in the window (about 140 frames
for a 5 s clip at 30 fps: 5 vote frames and 136 scan frames in the records). After a scan, frames_sampled is the
scan's frame count; the vote's frames appear only in attempts.
The gated vote removes the ungated single-vote decode. Measured cost and exposure (
the laidline-bench gated keyframe run):
- Unmarked, TrustMark on the CPU, the product's
decode_keyframes: 0 of 600 clips (Wilson 0 to 0.64 %) and 0 of 520 further clips (0 to 0.73 %) decode underhardened. The 520 are new clips, not new sources: 421 of the 520 are other 5 s slots of sources used in the 600; the 1,120 clips come from 560 sources. By source, 0 of 560 decode underhardened(upper bound 0.68 %) and 6 of 560 gave a fast-vote phantom (1.07 %, Wilson 0.49 to 2.32 %). Over 6,286 vote frames and 152,320 scan frames no read passed both gates; the best score was 1.94 against the 2.95 gate. - Marked with the own payload, every bench row and sweep point, against the shipped
hardened(the fast keyframe vote's payload, else the scan), TrustMark on CUDA for embed and decode:- 41 derf clips: 2 lost of 3,280 marked rows (0.06 %, Wilson 0.02 to 0.22 %); of the 1,940 rows the fast keyframe vote decodes exactly, 2 (0.10 %, Wilson 0.03 to 0.38 %); of the 2,383 rows the shipped path decodes exactly, 2 (0.08 %). 3 gained. 1 wrong payload against 12 for the shipped path; that one is the payload the shipped scan also accepts (a confident near-neighbour on an oracle crop, the image decoder's known residual), so 0 new wrong.
- 100 YouTube UGC clips (the first 100 in run order; the bench rows and the oracle crop sweep): 5 lost of 3,600 marked rows (0.14 %, Wilson 0.06 to 0.32 %); of the 2,455 rows the fast keyframe vote decodes exactly, 5 (0.20 %, Wilson 0.09 to 0.48 %); of the 2,756 rows the shipped path decodes exactly, 5 (0.18 %). The first 12 clips alone gave 3 of 418. 3 gained, 0 wrong payloads; the shipped path had 10 wrong there, of which the gated vote turned 7 into no decode and 3 into the right payload.
- The lost rows sit on 2 of 41 derf clips and 5 of 100 UGC clips, one row each; the row intervals treat the rows of one clip as independent.
- Every lost row rested on one ungated fast-decoder vote at 5 bit errors, the BCH correction limit: the same one-ungated-vote class as the phantoms under the fast keyframe vote. Their gated scores (2.24 to 3.13) lie above every unmarked frame measured (1.94); five of seven fail the 2.95 score gate, one the 0.945 normalised gate, one both. The gates were set on still images before this data and were not moved. One vote at 5 bit errors is not specific to lost rows: of 113 such rows the gated path keeps 106 (derf 46 of 48, UGC 60 of 65).
- The vote takes the only accepted payload (two give
ambiguous). In the records of that measurement no vote held two distinct accepted payloads, so this rule and a majority rule could not differ there. The later fresh rows held one: ugc/LiveMusic_1080P-7948_3 rotated 1.75 degrees, where frame 0 accepted a version-1 neighbour of the embedded codeword (14 codeword bits away, issuer 2887091, score 5.08) and three frames the embedded payload; the vote isambiguousand gives no payload, where a majority rule would have given the right one.
Pooled consistency. One frame's soft decode can land on a BCH neighbour of the embedded codeword that
clears both gates and, on that frame alone, outscores the embedded codeword: on derf/bridge_far_0 at oracle_crop=60
(the one wrong row of the gated-vote measurement) frame 30 read a neighbour 14 of 96 codeword bits away (issuer 1331125 for 4021, at the BCH
limit after two Chase flips; score 3.92, normalised 0.948), while frames 0, 60 and 120 read the embedded codeword
above the score gate and just under the normalised one. The vote, the scan and the keyed reads take the
one accepted payload only when it stands over the deciding step's frames (the vote's, or the scan's when it ran): for
the variant that accepted it, pool every frame whose best Chase candidate clears the score gate (2.95; the normalised
gate aside), except a frame whose best word passes both gates and is another word; the rivals are the other payloads
the pooled frames' reads have as best (all of them fail a gate); the payload stands only when its mean signed score
over the pooled frames is above every rival's (a tie does not stand). Otherwise there is no
payload and decode_path is ambiguous, which also means "the one accepted payload is outscored by the frames'
pooled reads"; as for any ambiguity the step is final and the scan does not run. No constant is added. A read that
passes both gates with another payload is neither a rival nor in the pool: it is a genuine word of another mark,
handled as before (unkeyed, two accepted payloads are ambiguous; keyed, an ineligible word neither accepts nor
blocks, so with a second, foreign mark in the video the eligible word still wins), and it cannot shift the pooled
means either way. (The rule first left such frames in the pool while excluding them from the rivals; a later
measurement on the bridge row showed that one clean foreign frame then turned the keep case into a loss, or the bridge into
an accept, for about 20 % (amplitude 4) to 42 % (amplitude 8) of random foreign words. The pool exclusion was adopted
after the data; the measured figures below are given under both.) The rule is silent when no gate-failing read of another payload clears
the score gate, so a lone accepted frame among noise frames still decodes (the residual, Robustness baseline section 6).
On the bridge row the neighbour's pooled margin is -0.046 (four frames, one rival), so it becomes ambiguous; the
closest true decode it keeps in the earlier saved records, derf/bridge_far_1 at the same crop, sits at +0.131. Measured
(pre-registered in the laidline-bench rules file before any data). The runs measured the rule with foreign gate-passing
frames left in the pool; the pool exclusion was then applied to the same data (H1 replayed again from the saved
logits, H2 re-decided from each row's saved reads, no new rows), and every figure below holds
under both, the one difference named:
- H1, the product reader replayed on 8,661 saved units: 0 of 5,113 exact decodes lost and the one wrong decode made ambiguous on the ONNX Runtime logits (the default path), the same on the 8,177 units with torch logits (0 of 4,845 lost), no decision differing between the two, 0 of 1,781 unmarked units decoded. Every exact decode with a rival kept a margin of +0.131 or more (249 of 249); the smallest is the choosing example's +0.1310.
- H2, fresh geometric rows of 141 marked clips (oracle crops, corner crops, scales and rotations at
parameters no earlier measurement used, read on CUDA by the Q, B, C chain): the evaluated set of 3,400 rows held 2,231
decodes and 300 one-frame decodes. 0 of 2,231 exact decodes lost (the line allowed floor(decodes / 1,000) = 2), 0
wrong decodes; 61 one-frame decodes had a rival, every margin +0.214 or more. One row has both a foreign
gate-passing frame and a rival (derf/akiyo_0 rotated -3 degrees): its margin is +0.519 with that frame pooled and
+0.582 without. The class did occur in H2, twice, and neither became a wrong decode:
ugc/LiveMusic_1080P-7948_3 rotated 1.75 degrees, where frame 0 accepted a version-1 neighbour (issuer 2887091, 14
codeword bits away, score 5.08, normalised 0.952) while three frames accepted the embedded payload, so the unique
rule made the vote
ambiguous(the neighbour's own pooled margin there is -0.396, and -0.811 with every frame pooled); and derf/akiyo_0 rotated -3 degrees, where a gate-passing read of another word (score 3.22) had payload version 3 and the version check rejected it. About 2 in 3,400 rows, in line with an earlier estimate of about 3 per 6,880. The rows held no wrong decode for the pooled rule itself to catch, so H2's power is on the loss side; that the rule removes the class rests on the bridge row, the mechanism and H1. The 618 rows of the gated-vote measurement decided by a B or C accept or by the scan, regenerated: 0 of 618 lost, 0 wrong.
What the scan adds to the false-decode risk is not bounded analytically. Every scanned frame is one image hardened
decode, so the scan's false-accept rate rests on the image gates, which were measured on still images (the image
section above), and a video is read up to max_detect_frames (256) times, each frame through every configured
variant; multiplying the stills figure by that count gives no useful bound. The evidence is the 2026-10 null campaign (0 of 4,854 unmarked clips, keyed reader) and, before it, the bench: 0 of 43 unmarked
clips, 6,508 gated frame reads in all, with the frames of one clip correlated. The uniqueness rule catches a wrong
read only when another payload is also read in the same video. These conditions were met before hardened became the default for video: an
unmarked corpus of at least 500 clips from a source other than Xiph derf went through the geometry search and this
scan, and clips marked with foreign payloads were measured. 1,120 unmarked YouTube UGC
clips went through the scan (0 frames accepted), 99 foreign-marked clips were measured, and the 200,000 full-search null
draws finished with none accepted (above).
detect refuses a video longer than watermark.video.max_duration_s with an error result (caveat
video_too_long: ...), and one whose frames are larger than watermark.video.max_pixels (caveat
video_too_large: ...); it decodes the video stream up to the last frame it samples. Frames are read upright (the
container's display rotation applied), as when marking.
Keyed image payloads (watermark.payload_keys, keyed mode)
In keyed mode (Payload keys) an image payload carries a keyed tag and detect grades as follows. Everything
above stays as it is in unkeyed mode. Per watermark signal, on the full result (Marker.detect, laidline detect,
a credentialed /detect):
| state | grade | status |
full tier adds |
|---|---|---|---|
| tag verified under a live key; a registry entry of the payload's layout under that key; the query's bytes (or a signed twin's) match | B | confirmed_bytes |
key_id, key_fp |
| as above; the fingerprint confirms on the query, the decoded view or one of their turns | B | confirmed_fingerprint |
fingerprint.distance |
| as above; the fingerprint confirms only on a cut or box view | B | contains_registered_asset, with region |
fingerprint.distance |
| tag verified; registered; not confirmed | C | issued_for_other_asset |
unconfirmed ids, distance, registry_entries |
| tag verified; no registry entry | D with presence, else none | present_unverified |
tag: verified, registry: missing |
| own-issuer head, tag failed (or verified only under a key compromised before the entry; a retired key verifies nothing) | D with presence, else none | present_unverified |
tag: failed |
| CRC (version 1) payload, the unkeyed format (not eligible: no lookup, never counted); a foreign issuer; two distinct eligible payloads | D with presence, else none | present_unverified, no issuer named (a payload is shown only when its tag verified); an unkeyed-format payload adds the caveat "The payload is in the unkeyed format, which carries no key tag; this deployment does not check it." |
no tag or registry outcome, or tag: not_checked and other_payload_unchecked |
Grade C needs a registry entry: an unregistered own-issuer payload with a valid tag is not C (it is either an operator
problem, the detect replica not seeing the authoritative sink, or a guessed tag). The public tier (Payload keys,
"Response tiers") shows B or D only: the C row becomes D (present_unverified) when presence holds and an absent
signal otherwise, so a registered and an unregistered value, each with a valid or a failed tag, give the same public
body. The signal states false_accept_bound (the own-accept bound) per live key publicly and at the live key
count on the full tier, each with a caveat saying which event it bounds; for PixelSeal at one key 10^-14.0 over the
4 default reads and 10^-13.4 over the 17 reads of deep scan. For TrustMark the bound is 2^-35 x K per decoded word
(version, issuer and the 16-bit tag) times the variants read, with the chance that a variant decodes a word at all
taken as 1: the per-word phantom rate behind the TrustMark bound is not measured, so this is the unmeasured-rate
form of the bound. A keyed result's top-level caveat is "A watermark shown
as present but not verified is not evidence of origin: ..." when the signal has presence and is not B; it never
depends on the tag or the registry outcome.
Soft-binding names
Every manifest Laidline signs carries a c2pa.soft-binding assertion naming the watermark in it. For images under
the trustmark scheme and video keyframes that is TrustMark's registered algorithm name (com.adobe.trustmark.Q,
.B, .C). The PixelSeal, AudioSeal and VideoSeal ones, com.laidline.pixelseal (the 61-bit image payload in hex,
16 characters; the CRC-16 and the BCH code around it are not part of the value), com.laidline.audioseal (the 16-bit
payload in hex) and com.laidline.videoseal (the 256-bit embedded message in hex), are Laidline-private names: they are not in the C2PA soft-binding algorithm list, so
other validators show them as unknown algorithms and cannot use them to look the asset up. The manifest's hard binding
and signature do not depend on them. In keyed mode an image's soft binding carries the full keyed layout bits, tag
included, under com.laidline.pixelseal.i77-r1 or com.laidline.trustmark.i61-r1 (Laidline-private names too); the
unkeyed names are unchanged.
Issuer lines
Every watermark signal that read an issuer number, and every valid C2PA manifest whose signing key the issuer
directory binds, carries an issuer block; the CLI prints one line for it and the verify page shows the same words.
{who} is {name}, operator of {domain} (the name alone in a snapshot without domains; a registered provider for a
legacy entry without a name).
| State | Line |
|---|---|
| shared key | shared mark, provider not identified (a registered number adds ; it carries number N; anyone can write this number) |
| claimed, number active | claimed by issuer N, unconfirmed (anyone can write this number); number N is registered to {who}, per directory snapshot D |
| valid manifest signed by N's bound key | signed by registered issuer N, watermark agrees; registered to {who}, per directory snapshot D |
| manifest signed by M's key, watermark claims N | claimed by issuer N, unconfirmed (anyone can write this number); the Content Credentials are signed by the certificate registered as issuer M |
| C2PA signal, signer registered | signed by the certificate registered as issuer M ({who}) |
| grade B on the issuer's own checker, or its verified signed reply | confirmed by issuer N ({who}); a verified reply adds at {detector_url} on {issued_at} |
| a reply that does not verify | the reply from issuer N's checker did not verify; it is not a confirmation |
| a verified reply without a grade-B mark of N | issuer N's checker did not confirm its mark in this file |
| an unsigned reply | the reply from issuer N's checker is unsigned; it is not a confirmation |
| no reply | issuer N's checker did not answer; nothing is confirmed |
| trial number | claimed by trial number N, unconfirmed (several deployments may share this number) |
| lapsed, revoked, not listed, no snapshot | claimed by issuer N, unconfirmed (anyone can write this number); and the dated tail |
The "signed" lines need a manifest that validates (valid_trusted or valid_untrusted) and whose leaf certificate's
public key (SHA-256 of its SubjectPublicKeyInfo) is listed in the entry's bound_keys.signing_spki_sha256. Grades do
not change: these lines say who a mark and a credential point to, not how strong the evidence is.
Signed results (attestation)
With detect.attest.enabled, every result /detect serves that names the content's SHA-256 (asset.sha256: file and
text results alike) carries:
"attestation": {"v": 1, "alg": "es256", "spki": "<base64url>", "statement": "<base64url>", "sig": "<base64url>"}
sig is ES256 in P-1363 form (64 bytes) over laidline.detect-attestation/1, a zero byte, then the statement bytes.
The statement is RFC 8785 canonical JSON: type (laidline.detect-attestation), issuer_id (the installed
certificate's number), detector_url (detect.public_url), issued_at, content {sha256, bytes, mime}, tier
(public or full), verdict, grade, findings (type, scheme, present, grade, status, issuer_id per
signal) and result_sha256 (SHA-256 of the canonical JSON of the body without attestation). Encodings are unpadded
base64url; digests are lowercase hex. The key is your active signing key, which the directory already binds for your
number, so a verifier pins it from the signed directory and never from the reply. Error bodies, which name no content,
are not signed. laidline verify-result RESULT.json --file FILE checks a saved result; laidline detect FILE --confirm asks the checker the directory lists for the file's number and checks its reply.
Verdicts
| Verdict | When |
|---|---|
marked_by_this_system |
c2pa trusted by your own anchor (and not disputed by the secondary validator), the image or video watermark resolved to a confirmed entry in your evidence log (registry_match: local), or a text watermark present over at least min_tokens tokens with p below grade_b_p_value (on every tier: the public tier regrades from the projected signals and keeps this). An audio watermark alone never gives it. |
marked_by_known_system |
c2pa trusted via the public trust list (needs detect.trust_anchors_path), and not disputed by the secondary validator |
signals_present_unverified |
something was found but nothing resolved to a known system, including a watermark payload whose registry hits were not confirmed (payload_only); for text, a signal over fewer than min_tokens tokens, or one whose p is not below grade_b_p_value |
no_signal_found |
neither check found anything |
error |
input could not be processed: unsupported type; audio or video that cannot be decoded, a missing audio or video extra, a video longer than max_duration_s; for text, text detection disabled, no model, a model not in detect_models, or a tokenizer that would not load (the caveat names the reason) |
evidence_log ({stream, seq, entry_hash, status: "ok"}) is set only on the two image and video paths that give
marked_by_this_system (for audio, only the manifest path), and only when the entry it names matches the query by the
confirmation rule above (exact bytes or fingerprint). From the manifest, the asset_id in the Laidline evidence
assertion is used only when the manifest is valid_trusted under your own anchor: an invalid manifest (for example
after the pixels were edited) or one signed under someone else's certificate can name any asset and resolves nothing.
From the watermark, only a single confirmed registry hit resolves. In every other case evidence_log is null.
Caveats
Always present: "Absence of a signal is not evidence that content is human-made." Image results add "Revocation
status was not checked (OCSP disabled in this deployment)." and, when relevant, reader errors on odd files and "pixels
could not be decoded, watermark check skipped" when the manifest was checked on a file Pillow cannot open. When a
watermark payload is present but no registry hit is confirmed (grade C or D), the measured phantom-rate caveat from
laidline.compliance.phantom is added (under the pixelseal scheme its own: "A decoded watermark payload alone is not
evidence: PixelSeal accepted a payload on 0 of 38,500 unmarked photos (the laidline false-positive campaign, measured
2026-10-27 ...) ... at most 10^-5.3 per read and 10^-4.7 over the 4 default reads ...", 10^-4.1 over the 17 reads of deep
scan; the count is read from the packaged 2026-10 campaign result, which replaced the earlier 5,000-photo run), unless your own anchor's trusted manifest already made the
verdict marked_by_this_system. With the secondary validator, validators_disagree: ... or secondary validator unavailable: ... as above. Audio and video results carry the revocation caveat too; audio adds "AudioSeal carries
the issuer only; the asset is not resolved against the evidence log." when a watermark is present. Video adds a
phantom caveat under the same condition as images: for VideoSeal its own measured one ("A decoded VideoSeal payload
alone is not evidence: in the laidline false-positive campaign ..., 0 of 4,854 unmarked five-second clips were read as
present, passed a keyed tag check or passed a keyframe tag check ...; the earlier bench: 0 of 43 unmarked clips decoded
to a payload ... and a uniformly random read ... passes the CRC-16 with probability 2^-64.4 ...", from
laidline.compliance.video_robustness, which reads the campaign's three reads from the packaged result), for the keyframe scheme the
TrustMark photo phantom rate above, since its keyframes carry a TrustMark payload. Text results
add "Text detection is statistical: paraphrase, translation and heavy editing remove the signal. A text grade rests on
statistics alone and is not registry-confirmed: in the laidline false-positive campaign (measured ...), SynthID text was
graded B on 0 of 20,000 unmarked texts (15,000 human and 5,000 machine-written) and showed presence on 19 of 20,000."
(KGW: 0 and 32 of 20,000; the shipped
gate's pooled counts, from the packaged result), a note when fewer
than min_tokens tokens were scored (for KGW, "N repeated n-grams were counted once: the M distinct n-grams cover K
tokens, below the configured evidence threshold" when the text is long enough but its repeats keep it below), and
"Only the first N tokens were scored" when the text exceeded the token
cap. Under decode: hardened a KGW detection past the fast test adds "Found by the windowed scan in tokens A to B: ..." or
"Found by the whole-text test over N tokens: ...", followed by "its exact binomial p-value times F, the number of
fallback tests (the whole-text test when the text runs past the fast test's token cap, and each window of 128, 256 or 512
tokens), is P, below the fallback budget B; with the fast test at A the false-positive bound is A+B.", and, for a
detection that covers min_tokens but misses the strict budget, "Grade D, not B: the fallback's corrected p-value P
is not below the strict grade-B budget S (the smaller of watermark.text.fallback_grade_b_p_value and
watermark.text.grade_b_p_value)." A fast-test detection over min_tokens whose p is below detect_p_value but not
below grade_b_p_value carries "Grade D, not B: the p-value P is below the detection threshold but not below the
grade-B threshold G (watermark.text.grade_b_p_value)." The fewer-than-min_tokens note then speaks of the detecting test. A detection by the fast test carries the fast test's caveats unchanged.
A result /detect could not sign adds result not signed: <reason> (timeout, signer_unavailable, sign_failed
or statement_failed). A grade-B mark confirmed on the registry-only path adds confirmed against issuer N's records; key tag not checked by this checker.
Reference
- HTTP API: the
POST /detectrequest, headers and response tiers. - Keys: payload keys, the shared family and the private key, and what each response tier shows.
- Configuration: the
watermark.*anddetect.*settings this page names. - Benchmarks and Robustness: the measurement tables behind the figures.
- Evidence: the log that a confirmed result resolves to.
- Certificates: trust anchors, timestamps and the leaf profile that decide grade A.
- Licensing: shared and private marks, issuer numbers and credits.