Benchmarks

Read this if you need the measured figures behind a Laidline claim: latency, what each watermark survives, false positives, validator agreement and image sizes. You will find every number with its corpus, hardware and interval, and the command that reruns a suite on your own media. Skip if you want to know why a mark fails and what could recover it (Robustness) or how a result becomes a grade (Grading). Time: 10 minutes to find a figure; a suite run takes minutes to hours by size. Needs: pip install laidline-bench to rerun a suite; nothing to read the figures.

In short

  • Every figure carries its corpus, sample size and Wilson 95 percent interval; read the interval before the rate.
  • Latency is measured on named hardware and moves 15 to 20 percent between runs; compare rows only within one table.
  • Robustness runs use public corpora (COCO, LibriSpeech, Goldberg Variations, Xiph derf, C4) and one seed; in the pilot a GPU run gave the same decisions as the CPU.
  • False positives are measured on unmarked media: 0.16 percent for the TrustMark chain, 0 of 5,000 for PixelSeal in the earlier run, and 0 of 38,500 photos, 0 of 4,854 clips and 0 grade B on 40,000 reads of 20,000 texts in the 2026-10 null campaign (image, video, audio and text sections below, which also state the results that fall short of their lines).
  • laidline bench reruns the image, audio, video and text suites on your own media.

Reproduce with laidline-bench

The four robustness suites ship as a separate package, so a production install stays small:

pip install laidline-bench

Expected output ends with:

Successfully installed ... laidline-bench-1.0.0

What just happened: pip installed the benchmark harness and, through it, laidline with the image, audio, video and text extras. Without it laidline bench prints laidline bench needs the laidline-bench package: pip install laidline-bench to stderr and exits 2.

laidline bench --help

Expected output:

 Usage: laidline bench [OPTIONS] [suite]

 Run a watermark robustness benchmark.

 Image, audio and video: embed, transform and decode on a directory of photos,
 audio or video clips. Text: generate, transform and detect on a C4 shard.
 Corpora and published results: https://laidline.com/docs/robustness.

╭─ Arguments ──────────────────────────────────────────────────────────────────╮
│   suite      <str>  Benchmark suite: image (TrustMark), audio (AudioSeal),   │
│                     video (VideoSeal, keyframe TrustMark) or text (SynthID   │
│                     and KGW).                                                │
│                     [default: image]                                         │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --corpus                           DIR    Directory of unmarked media (text: │
│                                           a C4 shard or JSON-lines file).    │
│ --n                                <int>  Images, clips or prompts to sample │
│                                           (seeded; 50, or 20 for video and   │
│                                           text).                             │
│ --workers                          <int>  Worker processes, each with its    │
│                                           own watermark models.              │
│                                           [default: 2]                       │
│ --modes                            <str>  Audio only: batch,streaming        │
│                                           (default both).                    │
│ --transforms                       <str>  Comma-separated transform names,   │
│                                           or all.                            │
│ --out                              <str>  JSON result (default:              │
│                                           SUITE-robustness-DATE.json).       │
│ --device                           <str>  Where the suite's models run: cpu, │
│                                           cuda or auto (video also cuda:N;   │
│                                           text: the generating model,        │
│                                           detection stays on CPU). Default:  │
│                                           the harness's own.                 │
│ --schemes                          <str>  Video only:                        │
│                                           videoseal,trustmark_keyframes.     │
│ --model                            <str>  Text only: the generating model, a │
│                                           Hugging Face id or local           │
│                                           directory.                         │
│ --music-bed                        FILE   Audio only: the music file that    │
│                                           music_bed_10db mixes in (the row   │
│                                           is not run without one).           │
│ --sweeps            --no-sweeps           Run the sweeps (image: off by      │
│                                           default; video and text: on;       │
│                                           audio: on the first --sweep-n      │
│                                           clips).                            │
│ --sweep-families                   <str>  Image and video only:              │
│                                           comma-separated sweep families     │
│                                           (video: or points).                │
│ --sweep-n                          <int>  Items of the sample that run the   │
│                                           sweeps (default: the harness's     │
│                                           own).                              │
│ --resume                                  Continue the interrupted run       │
│                                           checkpointed next to --out (same   │
│                                           arguments).                        │
│ --gen-batch                        <int>  Text only: prompts per generate    │
│                                           call (default 1 on cpu, 8 on       │
│                                           cuda).                             │
│ --help                                    Show this message and exit.        │
╰──────────────────────────────────────────────────────────────────────────────╯

The suites, and the section of this page whose figures each one reproduces:

suite command what it runs figures
image laidline bench image --corpus DIR --n 500 --workers 8 --sweeps embed with TrustMark Q, B and C, transform, decode; --sweeps adds the parameter ladders Image robustness
audio laidline bench audio --corpus DIR --n 500 --workers 8 AudioSeal in batch and streaming mode through each transform Audio robustness
video laidline bench video --corpus DIR --n 20 --device auto VideoSeal and the keyframe TrustMark scheme through each transform Video robustness
text laidline bench text --corpus FILE --n 200 --gen-batch 8 --device auto SynthID and KGW: generate, transform, detect Text robustness

Every suite needs a corpus of your own unmarked media (--corpus DIR; for text a C4 shard or a JSON-lines file), samples it with a fixed seed and writes one JSON result next to your working directory (--out renames it). The corpora behind the published figures are named in each section. Each suite also takes --device cpu|cuda|auto; a GPU run gives the same decisions as a CPU run but not bit-identical soft outputs, so a few rows move inside their intervals (see the "Changes from the published" sections). Your rates differ with your media, so compare them with the intervals below, not the point values. The sections for phantom decodes, latency, Vault and S3 sinks, validators and Docker images describe measurements made on Laidline's own test hardware; they are not laidline bench suites, and their figures stand as measured.

What the watermarks survive, why they fail and how much a decode-side fix could recover: Robustness baseline. The robustness sections below hold the full tables of those runs. The tables are the published figures; the per-item records behind them are not distributed with the package.

Image latency

PixelSeal, the default scheme

op (1024 px, real photos, TSA per asset) 2 CPU threads p50 / p95 16 CPU threads p50 / p95 RTX 5080 p50 / p95
PixelSeal mark 980 / 1300 ms (repeat 1165 / 1285) 646 / 682 ms 530 / 589 ms
PixelSeal detect 245 / 278 ms not measured 246 / 310 ms
TrustMark Q mark 600 / 679 ms not measured not measured

Measured on a Ryzen 7 7800X3D, real COCO photos, TSA per asset; the 2-thread rows were pinned to two cores; run-to-run spread 15 to 20 percent.

TrustMark Q, random noise

20 marks of a 1024x1024 random-noise PNG, TrustMark Q on CPU, local PEM signer, evidence file sink. Measured on a test machine with 16 logical CPUs.

Without a timestamp authority:

stage p50 ms p95 ms
watermark 97 97
encode (PNG) 249 249
sign 43 43
total 319 341

With DigiCert as the timestamp authority (tsa.required: true):

stage p50 ms p95 ms
watermark 122 152
encode (PNG) 99 106
sign (incl. TSA round trip) 166 169
total 397 425

Notes:

  • Encode time is PNG compression of incompressible noise at 1024 px; real renders compress faster (the second run shows 99 ms for the same input, the first run overlapped with another process).
  • The TSA adds about 120 ms per asset from this network. tsa.mode: checkpoint_only removes it (tsa.required: false does not: a reachable TSA is still called; it only lets the mark succeed when none answers); checkpoint_only then timestamps one head hash per checkpoint instead, off the marking path, and the C2PA signal grades B instead of A (Grading).
  • Both runs are well inside the 1.2 s p50 budget Laidline designs for, on a machine faster than the target. On 2 CPU threads the sidecar's 1024 px mark (1.54 s p50) is over that budget (see "Sidecar latency", below); 16 threads bring the proxy overhead to about 0.6 s (16 threads, unpinned, host load about 1.5; not a pinned measurement).
  • These runs used the local PEM signer. Vault Transit and the S3 evidence sink add the latencies measured below.

Sidecar latency

op (PixelSeal, 1024 px, TSA per asset; server and client on the same two cores) p50 ms p95 ms
/v1/mark/image (http-mark) 1542 1821
/detect (http-detect) 335 392
proxied image generation (proxy-gen) 1469 1596

Measured on a Ryzen 7 7800X3D, real COCO photos; server and client were pinned to the same two cores; run-to-run spread 15 to 20 percent. A separate run with 16 CPU threads, unpinned, on a host at load about 1.5 (not a pinned measurement, so not comparable with the table) put the proxy at 832 ms p50 against 249 ms direct to the upstream.

Signing through Vault or OpenBao Transit

A live run signs 1000 times through VaultTransitBackend against a Vault and an OpenBao dev server on the same machine (ECDSA P-256 key, prehashed SHA-256, JWS marshaling). Measured on a test machine:

server calls p50 ms p95 ms
Vault 2.1.1 1000 1.45 2.76
OpenBao 2.7.0 1000 1.33 2.12

These are loopback numbers. Over a network, each sign call adds a round trip to the Vault server, like KMS does.

S3 evidence sink writes

A live run appends marked entries to an Object Lock evidence bucket (GOVERNANCE) on real AWS S3 in eu-central-1. Each write is two pointer PUTs in parallel, then the entry PUT. Measured from a test machine outside AWS (about 124 ms round trip to eu-central-1):

write p50 ms p95 ms
marked entry (2 pointers + entry) 533 556

The time is network-bound (533 ms is about four 124 ms round trips across the two sequential phases). Measured with Object Lock and the bucket-default GOVERNANCE retention; no comparison without the lock was run. Writes stay synchronous under the chain lock, so no asset is returned before its entry is stored. This bounds one process to roughly two marked entries per second at this distance, less from further away. Run the worker close to the bucket, or use Postgres as the primary sink for volume and for detect at scale, with the S3 Object Lock bucket as a replica.

What this means. An async writer for secondary S3 sinks is worth having only if the write p95 exceeds 150 ms. This measurement crosses that line, but it is network-bound from a remote client, so it says little about the sink's own cost. Writes stay synchronous, so an S3 sink, primary or replica, adds its full write latency to every mark under the chain lock. Run the marker in the bucket's region, or use Postgres as the primary sink. The in-region measurement follows.

S3 evidence sink writes, in region (Lambda eu-central-1, 3008 MB)

A live run invokes the deployed function with the test-only bench event {"laidline": "bench-s3-sink", "n": 200} (honoured only with LAIDLINE_LAMBDA_BENCH=1): 200 synthetic marked entries appended to a fresh chain on the function's S3 sink (the Object Lock evidence bucket, GOVERNANCE default retention, prefix lambda-it/bench-<id>/), timed per append inside the function. Same write path as above (two pointer PUTs in parallel, then the entry PUT), same bucket, but from inside eu-central-1. Measured (container image, x86_64, 3008 MB):

invoke appends p50 ms p95 ms max ms REPORT Duration Init Duration
cold (fresh execution environment) 200 54.9 77.0 109.0 12186 ms 2583 ms
warm 200 54.7 77.4 108.3 11620 ms none

In region a marked entry costs about 55 ms at the median and 77 ms at p95, against 533 and 556 ms from the remote machine above: the remote numbers were round trips, not the sink. The bench does not load the Marker, so its cold start is the runtime init only.

The same run, for the storage hook itself (REPORT lines from the function's log; 512x384 images):

invocation Duration Init Duration
first mark after a deploy (three uploads, three new environments in parallel) 61.4 to 67.4 s 3.0 to 9.1 s
first mark in an environment whose image is already cached (Marker and TrustMark load, then one mark) 17.1 s none
warm mark (one PNG: KMS sign, evidence entry, checkpoint with a DigiCert token, S3 get and put) 0.93 to 0.97 s none
warm, already-marked key skipped by metadata (and the write-back's own notification) 14 to 44 ms none

The first mark after a deploy takes about a minute, 44 to 50 s more than a first mark on a cached image: Lambda loads container image layers on first access, and this one is 2.7 GB (torch and the TrustMark weights). The 120 s timeout leaves room, but a larger image or a slower first read would not. From upload to marked in place (S3 notification, SQS and the event-source mapping included): 77 s on the first run after a deploy (the live test's three uploads); in a separate manual run right after it (three PNG uploads one after another, each polled with head_object until marked; the 17.1 s and 0.93 to 0.97 s rows above are its REPORT lines), 20.9 s for the first, a cold Marker on a cached image, and 3.4 s for each of the two warm ones.

Recommendation. Async writes for secondary S3 sinks pay off only if the in-region p95 exceeds 150 ms. The in-region p95 is 77 ms (p50 55 ms), under the line. Synchronous S3 writes stay the recommendation in region, primary or replica: an in-region S3 sink adds about 55 to 77 ms to a mark under the chain lock, which bounds one chain to roughly 13 to 18 marked entries per second. The async replica writer (evidence.replicas: async) is for S3 replicas in another region or reached over a long network path, where each write costs the round trips measured from the remote machine above.

Replica recommendation (evidence.replicas). Rule: if the in-region S3 write p95 is at most 150 ms, sync stays the recommendation in region and async is recommended for cross-region or remote replicas; otherwise async is recommended for every S3 replica. Measured: from a remote machine to eu-central-1, p50 533 ms and p95 556 ms (above); in region, from a Lambda in eu-central-1 against the same Object Lock bucket, p50 54.9 ms, p95 77.0 ms, max 109 ms (n=200). The in-region p95 is under 150 ms, so the default and the in-region recommendation stay sync: every replica holds every entry before the mark returns, at about 77 ms added per mark at p95. Set evidence.replicas: async when an S3 replica is in another region or reached over a remote link. Each replica then gets its own FIFO queue (evidence.replica_queue_max, default 10,000) and writer thread, so the mark pays only the primary write. The trade: the replica lags the primary; a full queue drops entries, each counted in laidline_evidence_sink_failures_total and laidline_evidence_replica_dropped_total; entries still queued after evidence.replica_drain_timeout_s (default 30 s) at shutdown are counted in laidline_evidence_sink_failures_total only (a crash or SIGKILL before close() loses whatever is queued without counting it); and verify on that replica reports those gaps later, not at mark time. Watch laidline_evidence_replica_queue_depth (or replica_queue_depth in GET /admin/evidence/status). The primary is always written synchronously, whatever this setting.

TrustMark quality on CPU (variant Q, BCH_5, 61-bit payload)

input PSNR dB
256 px pure noise 28.0 (below the 38 dB floor, watermark skipped)
384 px pure noise 39.2
512 px pure noise 41.6
1024 px pure noise 45.2
256 to 1024 px smooth gradients 46.8 to 47.7

These are single-image probes on synthetic inputs. On 500 COCO photos the embed PSNR of variant Q averages 42.02 dB (5th percentile 38.94 dB) and 7 of 500 fall below the 38 dB floor; what the payload survives (crop, resize, recompression, rotation, blur, screenshots) is measured in "Image robustness" below.

Phantom decode rate (TrustMark, unmarked photos)

Neural watermark decoders emit payloads on images that were never marked (Krawetz, "Meta's Un-Stable Signature", hackerfactor.com, June 30, 2026, reports about 10 to 20 percent for TrustMark on his data). This bench measures it for Laidline's exact decoder configuration: TrustMark 0.9.2, BCH_5, 61-bit payload, binary mode, Q then B then C as detect runs them, a payload accepted only when the decoder reports it detected and it is 61 bits long.

The phantom run is not one of the four laidline bench suites; its figures stand as measured on the corpus below.

Earlier run. This is the earlier false-positive basis for photos (5,000 COCO val2017 photos). The 2026-10 null campaign measured PixelSeal, the default scheme, and keyed PixelSeal on 38,500 further unmarked photos: 0 of 38,500 each (Wilson 95 % upper bound 0.0100 %; Image null campaign). The figures below stand as the history of this run.

COCO val2017, 5,000 unmarked JPEGs, 0 unreadable. Measured on a test machine (16 logical CPUs, 8 workers): machine.decode_seconds 239.6 (3 variants per image), machine.wall_seconds 473.7 including worker start-up (101.2 s). Run again on the RTX 5080 (torch 2.14.0+cu130, 8 workers; the bench turns TF32 off on cuda, but this run's machine block does not record the TF32 or cuDNN flags) with --device cuda --save-logits: the same 8 phantoms (Q 1, B 5, C 2) and every statistic in the table below identical; machine.decode_seconds 108.5, machine.wall_seconds 257.8 including worker start-up (56.6 s). The result ships as laidline/compliance/data/phantom.json (now the GPU run); the detect caveat and the marking-method text of the evidence export quote it.

decoder n phantoms rate 95% interval (Wilson) payload version 1 distinct payloads largest Hamming-6 cluster share of ones monobit chi2 (p) serial chi2 (p) detected under any TrustMark schema
Q 5000 1 0.02% 0.0035% to 0.11% 0 of 1 1 1 49.2% 0.02 (0.90) 3.60 (0.31) 126 (2.5%)
B 5000 5 0.10% 0.043% to 0.23% 1 of 5 5 1 51.8% 0.40 (0.53) 1.41 (0.70) 162 (3.2%)
C 5000 2 0.04% 0.011% to 0.15% 2 of 2 2 1 41.8% 3.28 (0.07) 3.07 (0.38) 145 (2.9%)
chain Q/B/C 5000 8 0.16% 0.081% to 0.32% 3 of 8 8 1 49.0% 0.20 (0.65) 0.87 (0.83) 425 (8.5%)

Reading the table:

  • The TrustMark fast decoder chain (Q/B/C) accepts 8 of 5,000 unmarked photos (0.16 percent); the TrustMark hardened decode 0 of 5,000; PixelSeal, the default scheme, 0 of 5,000. The fast decoder's rate is 0.02 to 0.10 percent per variant. (Earlier run; the PixelSeal figure is 0 of 38,500 in the 2026-10 null campaign.) The chain's first hits came from Q 1, B 5, C 2; no image decoded under two variants.
  • Attractors: none at this sample size. All 8 phantom payloads are distinct, no two are within Hamming distance 6 of each other, and the 8 issuer ids are all different. The top payload in the chain list (166cc2b25f475710, issuer 2542635, seq 10188445456) occurred once.
  • Bias and serial correlation: not detectable here. Monobit (pooled ones against 50/50, 1 dof) and serial (non-overlapping adjacent pairs within each payload against uniform, 3 dof) chi-square over the phantom payload bits give p between 0.07 and 0.90. With 8 phantoms (488 bits) these tests have little power; they would show a Krawetz-style bias only if phantoms were far more common. This run used TrustMark's public decode, which does not return the pre-ECC bits in binary mode, so it did not test the raw decoder output that Krawetz tested. Laidline's decoder now keeps the 100 raw bits in the same pass (TrustMarkImageWatermarker.decode_variant), which the image robustness bench below uses. The GPU run saved the 100 pre-threshold decoder outputs of every image and decoder (--save-logits), so the raw-bit statistics can now be computed offline; they are not computed yet. The phantom bit strings are stored in the JSON (phantom_bits), so every statistic can be recomputed without a re-run.
  • TrustMark's own "detected" flag fires far more often than Laidline accepts a payload. The decoder reads the BCH schema from two of the packet's own version bits and reports "detected" whenever the word falls within the correction radius of that schema's code. Counting every such detection (last column) gives 2.5 to 3.2 percent per variant and 8.5 percent for the chain, mostly schema 3 (75 bits, 3-bit correction: 110, 142 and 113 detections for Q, B, C) and schema 2 (68 bits: 15, 15, 30). Laidline rejects everything that is not a 61-bit BCH_5 payload, which leaves 8. This any-schema rate is a likely contributor to the 10 to 20 percent Krawetz reports for TrustMark (his exact decoder settings are not published), not a full explanation of it.
  • The decoder output is not random. For uniformly random decoder output the acceptance per decode is roughly 0.047 percent as BCH_5 and 2.1 percent under any schema (about 2.3 and 106 expected detections per variant in 5,000 images). These figures are approximate: they model a bounded-distance BCH decoder on 96 bits and ignore TrustMark's padding of data and ECC to whole bytes before the bchlib decode. BCH_5 hits (1, 5, 2) match the random expectation, but the any-schema detections (126, 162, 145) are a significant excess. Schema 3 alone expects about 88 per variant, and B's 142 is about 5.7 standard deviations high (Q 2.4, C 2.7); C's 30 schema-2 detections against about 16 expected are 3.4 high. So on unmarked photos the decoder's output clusters near schema-3 and schema-2 codewords more than chance allows, and the 61-bit BCH_5 check is what keeps Laidline's rate low.
  • Payload version. Laidline only writes payload version 1, yet 5 of the 8 chain phantoms carry another version (0, 2, 5, 5, 6). Detect now refuses a payload whose version is not 1 (payload_version_ok: false, grade D).
  • Parse rate is 100 percent: every 61-bit string maps to some (version, issuer_id, asset_seq).

Registry false positives without confirmation (what marked_by_this_system would have meant before a registry hit had to be confirmed): the expected share of unmarked images whose phantom resolves to a registered asset, under a uniform asset_seq model (P(hit) = N / 2^36 per phantom) and assuming every phantom carries your issuer id (the worst case; for a uniformly random issuer id multiply by a further 2^-22):

registry size N P(hit) per phantom expected false hits per unmarked image one in
10,000 1.46e-07 2.33e-10 4,294,967,296
1,000,000 1.46e-05 2.33e-08 42,949,673
10,000,000 0.000146 2.33e-07 4,294,967
worst case: the top phantom payload itself registered 1 0.0002 (1 image) 5,000

So the uniform model makes chance registry hits rare, and the risk sits in non-uniform decoder output (an attractor payload that some real asset happens to carry). None showed up in 5,000 photos, but the worst-case row is why detect confirms every registry hit by perceptual fingerprint or exact bytes.

Fingerprint separation (phash64/v1, 64-bit DCT perceptual hash, Hamming distance; 500 randomly sampled COCO images; computed with the bench's implementation of the phash64/v1 spec, which is bit-for-bit equal to laidline.media.fingerprint across image modes):

pair p50 p95 max p5 min share at distance <= 10
image vs its JPEG q75 re-encode 0 2 2 100%
image vs its 60 percent resize 0 0 2 100%
image vs a random other image 32 24 20 0%

Related images stay within 2 bits and unrelated images never come closer than 20, so detect.fingerprint_max_distance: 10 sits in a wide gap on this corpus. Heavy crops are not covered by this test (pHash is weak to them; a cropped marked asset can drop to grade C, which is the fail-toward-unverified direction; in keyed mode that C is shown on the full tier only, and the public tier shows D or nothing, Payload keys; the same holds for an unconfirmed own video payload in keyed mode).

Image robustness (TrustMark, marked COCO photos)

What the watermark survives. For 500 photos sampled from COCO val2017 (seed 0), each variant embeds a seeded 61-bit payload on its own (no fallback, production PSNR floor 38 dB), then every marked image goes through each transform and is decoded by that variant's decoder (keeping the 100 raw decoder bits) and by detect's Q/B/C chain. Sweeps rerun the geometric, compression and tone families over a parameter ladder on the first 100 images. What the failures mean and how much a decode-side search could recover is in Robustness baseline.

# with the cu130 torch wheels; drop --device cuda to run on the CPU
laidline bench image --corpus DIR --n 500 --workers 8 --device cuda --sweeps

Point --corpus at a directory of your own unmarked photos; the published run sampled COCO val2017.

Measured on the RTX 5080 (torch 2.14.0+cu130, TF32 off, cuDNN deterministic, 8 workers): machine.run_seconds 933.8, machine.wall_seconds 994.5; 0 unreadable images. The result ships as laidline/compliance/data/robustness.json (with the sweeps and "Where it breaks", without the per-image records and soft outputs) and the marking-method text of the evidence export quotes variant Q from it. The first published run (CPU) had 18 rows; the GPU gives the same decisions as the CPU (pilot: 75 identical, 0 differ) but not bit-identical soft outputs, so a few of those rows moved slightly (below).

Columns: decode rate is the payload recovered exactly by the variant's own decoder, with its Wilson 95 percent interval; mis-decode is a detected but different payload; raw bits is the agreement of the 100 raw decoder bits with the embedded BCH codeword (mean and 5th percentile; about 50 percent is chance); chain TPR is detect's Q/B/C chain recovering the exact payload, at the operating point whose false-positive rate (FPR) is the measured chain phantom rate above. Rows starting oracle_ are not product numbers: they undo the attack with its true parameters before decoding, which estimates what a perfect decode-side resync could recover.

Transforms (transform_definitions in the JSON):

transform definition
identity PNG round trip
resize_0.5 resize to 0.5x each side (Lanczos)
resize_0.75 resize to 0.75x each side (Lanczos)
resize_1.5 resize to 1.5x each side (Lanczos)
crop_90 centre crop keeping 90 percent of each side
crop_75 centre crop keeping 75 percent of each side
crop_50 centre crop keeping 50 percent of each side
jpeg_90 JPEG q90, 4:2:0
jpeg_75 JPEG q75, 4:2:0
jpeg_50 JPEG q50, 4:2:0
webp_80 WebP q80
webp_50 WebP q50
rotate_+5 rotate 5 degrees counter-clockwise (bicubic, same canvas, black fill)
rotate_-5 rotate 5 degrees clockwise (bicubic, same canvas, black fill)
rotate_90 rotate 90 degrees (lossless transpose)
blur_1 Gaussian blur, radius 1
blur_2 Gaussian blur, radius 2
screenshot fit into 90 percent of a 1920x1080 white canvas, centred, rescale x0.97, JPEG q85
flip_h horizontal mirror (lossless)
rotate_+2 rotate 2 degrees counter-clockwise (bicubic, same canvas, black fill)
rotate_-2 rotate 2 degrees clockwise (bicubic, same canvas, black fill)
rotate_+15 rotate 15 degrees counter-clockwise (bicubic, same canvas, black fill)
rotate_180 rotate 180 degrees (lossless transpose)
crop_75_corner top-left crop keeping 75 percent of each side
aspect_0.8 height resized to 0.8x, width kept (Lanczos)
pad_10 black border added on every edge, 10 percent of the width left and right, 10 percent of the height top and bottom
overlay_caption white bar over the bottom 15 percent of the height with the black text 'caption text' (Pillow built-in font, size half the bar)
overlay_logo opaque flat red square, side 12 percent of the shorter side, over the bottom-right corner
brightness_+20 brightness x1.2 (Pillow ImageEnhance.Brightness)
contrast_0.7 contrast x0.7 about the mean grey (Pillow ImageEnhance.Contrast)
saturation_0 saturation 0, greyscale kept as RGB (Pillow ImageEnhance.Color)
gamma_1.5 gamma 1.5 per channel, out = 255 * (in / 255) ** 1.5 (darkens)
sharpen Pillow SHARPEN 3x3 filter
noise_5 additive Gaussian noise, sigma 5 of 255 per pixel and channel, seed 0, rounded and clipped
palette_256 quantise to an adaptive 256-colour palette (Pillow median cut, no dither, GIF-like), back to RGB
jpeg_75x2 JPEG q75, 4:2:0, twice
oracle_rotate_+5 Oracle (not a product number): rotate_+5, then rotated back 5 degrees clockwise (bicubic, same canvas, black fill) with the true angle
oracle_crop_50 Oracle (not a product number): crop_50, then pasted at its true position on a mid-grey (128, 128, 128) canvas of the original size
oracle_screenshot Oracle (not a product number): screenshot, then the photo's true box on the capture cut out and resized to the original size (Lanczos)

TrustMark 0.9.2 robustness (BCH_5, 61-bit payload, each variant forced, PSNR floor 38 dB): corpus COCO val2017, n=500, seed 0, measured.

Operating point: FPR 0.16% (95% interval 0.081% to 0.32%), the phantom rate of detect's Q/B/C chain on n=5000 unmarked photos (COCO val2017); TPR is the chain rate at that FPR.

Variant Q: embed PSNR mean 42.02 dB, p5 38.94 dB; skipped 7 of 500 (below 38 dB or too small).

transform n decode rate 95% interval mis-decode raw bits mean raw bits p5 chain TPR FPR
identity 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
resize_0.5 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
resize_0.75 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
resize_1.5 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
crop_90 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
crop_75 493 31.8% 27.9% to 36.1% 0% 88.4% 70.0% 31.8% 0.16%
crop_50 493 0% 0% to 0.77% 0% 50.6% 43.0% 0% 0.16%
jpeg_90 493 99.8% 98.9% to 100.0% 0% 99.9% 99.0% 99.8% 0.16%
jpeg_75 493 98.8% 97.4% to 99.4% 0% 99.7% 99.0% 98.8% 0.16%
jpeg_50 493 96.8% 94.8% to 98.0% 0% 99.2% 97.0% 96.8% 0.16%
webp_80 493 98.2% 96.6% to 99.0% 0% 99.6% 98.0% 98.2% 0.16%
webp_50 493 93.9% 91.4% to 95.7% 0% 98.4% 94.0% 93.9% 0.16%
rotate_+5 493 1.2% 0.56% to 2.6% 0% 65.2% 47.0% 1.2% 0.16%
rotate_-5 493 2.6% 1.5% to 4.5% 0.2% 65.6% 47.0% 2.6% 0.16%
rotate_90 493 0% 0% to 0.77% 0% 50.4% 43.0% 0% 0.16%
blur_1 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
blur_2 493 99.6% 98.5% to 99.9% 0% 99.9% 99.0% 99.6% 0.16%
screenshot 493 0% 0% to 0.77% 0.2% 51.9% 44.0% 0% 0.16%
flip_h 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
rotate_+2 493 90.1% 87.1% to 92.4% 0% 98.5% 93.0% 90.1% 0.16%
rotate_-2 493 93.1% 90.5% to 95.0% 0% 98.6% 94.0% 93.1% 0.16%
rotate_+15 493 0% 0% to 0.77% 0% 49.9% 42.0% 0% 0.16%
rotate_180 493 0% 0% to 0.77% 0% 51.1% 42.0% 0% 0.16%
crop_75_corner 493 0% 0% to 0.77% 0% 53.2% 43.0% 0% 0.16%
aspect_0.8 493 93.9% 91.4% to 95.7% 0% 97.5% 68.0% 93.9% 0.16%
pad_10 493 0% 0% to 0.77% 0% 51.1% 43.0% 0% 0.16%
overlay_caption 493 99.6% 98.5% to 99.9% 0% 99.9% 99.0% 99.6% 0.16%
overlay_logo 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
brightness_+20 493 95.7% 93.6% to 97.2% 0% 99.2% 95.0% 95.7% 0.16%
contrast_0.7 493 100.0% 99.2% to 100.0% 0% 99.9% 99.0% 100.0% 0.16%
saturation_0 493 94.5% 92.1% to 96.2% 0% 99.0% 95.0% 94.5% 0.16%
gamma_1.5 493 99.8% 98.9% to 100.0% 0% 99.7% 98.0% 99.8% 0.16%
sharpen 493 100.0% 99.2% to 100.0% 0% 99.9% 99.0% 100.0% 0.16%
noise_5 493 100.0% 99.2% to 100.0% 0% 99.9% 99.0% 100.0% 0.16%
palette_256 493 99.4% 98.2% to 99.8% 0% 99.7% 99.0% 99.4% 0.16%
jpeg_75x2 493 98.8% 97.4% to 99.4% 0% 99.7% 98.0% 98.8% 0.16%
oracle_rotate_+5 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
oracle_crop_50 493 0% 0% to 0.77% 0.2% 82.1% 74.0% 0% 0.16%
oracle_screenshot 493 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%

Variant B: embed PSNR mean 41.18 dB, p5 38.32 dB; skipped 16 of 500 (below 38 dB or too small).

transform n decode rate 95% interval mis-decode raw bits mean raw bits p5 chain TPR FPR
identity 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 99.8% 0.16%
resize_0.5 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
resize_0.75 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
resize_1.5 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
crop_90 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
crop_75 484 68.2% 63.9% to 72.2% 0% 94.6% 78.0% 68.2% 0.16%
crop_50 484 0% 0% to 0.79% 0% 50.3% 42.0% 0% 0.16%
jpeg_90 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 99.8% 0.16%
jpeg_75 484 99.4% 98.2% to 99.8% 0% 99.9% 99.0% 99.2% 0.16%
jpeg_50 484 99.2% 97.9% to 99.7% 0% 99.8% 99.0% 99.2% 0.16%
webp_80 484 99.8% 98.8% to 100.0% 0% 99.9% 99.0% 99.8% 0.16%
webp_50 484 99.0% 97.6% to 99.6% 0% 99.6% 98.0% 98.8% 0.16%
rotate_+5 484 45.0% 40.7% to 49.5% 0% 89.3% 66.0% 45.0% 0.16%
rotate_-5 484 40.3% 36.0% to 44.7% 0.41% 88.1% 65.0% 40.3% 0.16%
rotate_90 484 0% 0% to 0.79% 0% 49.8% 41.0% 0% 0.16%
blur_1 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 99.8% 0.16%
blur_2 484 99.8% 98.8% to 100.0% 0% 100.0% 100.0% 99.8% 0.16%
screenshot 484 0.21% 0.036% to 1.2% 0% 52.7% 44.0% 0.21% 0.16%
flip_h 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
rotate_+2 484 99.6% 98.5% to 99.9% 0% 99.9% 99.0% 99.6% 0.16%
rotate_-2 484 100.0% 99.2% to 100.0% 0% 99.8% 99.0% 99.8% 0.16%
rotate_+15 484 0% 0% to 0.79% 0.21% 49.7% 41.0% 0% 0.16%
rotate_180 484 0% 0% to 0.79% 0% 50.0% 41.0% 0% 0.16%
crop_75_corner 484 0.83% 0.32% to 2.1% 0% 62.4% 46.0% 0.83% 0.16%
aspect_0.8 484 94.2% 91.8% to 96.0% 0% 97.5% 64.0% 94.0% 0.16%
pad_10 484 0% 0% to 0.79% 0% 51.5% 43.0% 0% 0.16%
overlay_caption 484 99.8% 98.8% to 100.0% 0% 99.9% 99.0% 99.8% 0.16%
overlay_logo 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
brightness_+20 484 97.7% 96.0% to 98.7% 0% 99.5% 97.0% 97.7% 0.16%
contrast_0.7 484 99.8% 98.8% to 100.0% 0% 99.9% 99.0% 99.8% 0.16%
saturation_0 484 98.8% 97.3% to 99.4% 0% 99.7% 98.0% 98.3% 0.16%
gamma_1.5 484 99.6% 98.5% to 99.9% 0% 99.9% 99.0% 99.6% 0.16%
sharpen 484 100.0% 99.2% to 100.0% 0% 99.9% 99.0% 100.0% 0.16%
noise_5 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
palette_256 484 100.0% 99.2% to 100.0% 0% 99.9% 99.0% 100.0% 0.16%
jpeg_75x2 484 99.4% 98.2% to 99.8% 0% 99.9% 99.0% 99.2% 0.16%
oracle_rotate_+5 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
oracle_crop_50 484 1.4% 0.7% to 3.0% 0% 83.3% 76.0% 1.4% 0.16%
oracle_screenshot 484 100.0% 99.2% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%

Variant C: embed PSNR mean 40.11 dB, p5 37.11 dB; skipped 62 of 500 (below 38 dB or too small).

transform n decode rate 95% interval mis-decode raw bits mean raw bits p5 chain TPR FPR
identity 438 99.5% 98.4% to 99.9% 0% 99.9% 99.0% 99.1% 0.16%
resize_0.5 438 99.8% 98.7% to 100.0% 0% 99.9% 99.0% 99.5% 0.16%
resize_0.75 438 99.5% 98.4% to 99.9% 0% 99.9% 99.0% 99.3% 0.16%
resize_1.5 438 99.5% 98.4% to 99.9% 0% 99.8% 99.0% 99.3% 0.16%
crop_90 438 100.0% 99.1% to 100.0% 0% 100.0% 100.0% 100.0% 0.16%
crop_75 438 75.3% 71.1% to 79.1% 0% 96.5% 89.0% 75.3% 0.16%
crop_50 438 0% 0% to 0.87% 0% 50.0% 42.0% 0% 0.16%
jpeg_90 438 99.3% 98.0% to 99.8% 0% 99.8% 99.0% 99.3% 0.16%
jpeg_75 438 99.8% 98.7% to 100.0% 0% 99.7% 98.0% 99.3% 0.16%
jpeg_50 438 99.5% 98.4% to 99.9% 0% 99.5% 98.0% 99.5% 0.16%
webp_80 438 99.3% 98.0% to 99.8% 0% 99.6% 98.0% 99.3% 0.16%
webp_50 438 97.7% 95.8% to 98.8% 0% 99.0% 97.0% 97.3% 0.16%
rotate_+5 438 27.2% 23.2% to 31.5% 0% 87.7% 73.0% 26.9% 0.16%
rotate_-5 438 28.3% 24.3% to 32.7% 0% 88.0% 73.0% 28.3% 0.16%
rotate_90 438 0% 0% to 0.87% 0% 49.8% 42.0% 0% 0.16%
blur_1 438 99.8% 98.7% to 100.0% 0% 99.9% 99.0% 99.8% 0.16%
blur_2 438 99.3% 98.0% to 99.8% 0% 99.8% 99.0% 99.3% 0.16%
screenshot 438 0% 0% to 0.87% 0% 53.1% 43.0% 0% 0.16%
flip_h 438 99.1% 97.7% to 99.6% 0% 99.8% 99.0% 99.1% 0.16%
rotate_+2 438 97.9% 96.1% to 98.9% 0% 99.2% 97.0% 97.9% 0.16%
rotate_-2 438 99.1% 97.7% to 99.6% 0% 99.1% 97.0% 99.1% 0.16%
rotate_+15 438 0% 0% to 0.87% 0% 50.2% 43.0% 0% 0.16%
rotate_180 438 0% 0% to 0.87% 0.23% 49.3% 41.0% 0% 0.16%
crop_75_corner 438 2.3% 1.2% to 4.2% 0% 79.6% 59.0% 2.3% 0.16%
aspect_0.8 438 94.1% 91.4% to 95.9% 0% 97.7% 67.0% 93.6% 0.16%
pad_10 438 0% 0% to 0.87% 0% 52.1% 43.0% 0% 0.16%
overlay_caption 438 99.3% 98.0% to 99.8% 0% 99.7% 98.0% 99.3% 0.16%
overlay_logo 438 99.5% 98.4% to 99.9% 0% 99.9% 99.0% 99.3% 0.16%
brightness_+20 438 93.8% 91.2% to 95.7% 0% 98.9% 95.0% 93.6% 0.16%
contrast_0.7 438 99.1% 97.7% to 99.6% 0% 99.7% 98.0% 98.9% 0.16%
saturation_0 438 97.7% 95.8% to 98.8% 0% 99.4% 97.0% 97.7% 0.16%
gamma_1.5 438 97.0% 95.0% to 98.3% 0% 99.3% 97.0% 97.0% 0.16%
sharpen 438 99.3% 98.0% to 99.8% 0% 99.7% 98.0% 99.1% 0.16%
noise_5 438 99.5% 98.4% to 99.9% 0% 99.8% 99.0% 99.5% 0.16%
palette_256 438 99.5% 98.4% to 99.9% 0% 99.7% 98.0% 99.5% 0.16%
jpeg_75x2 438 99.8% 98.7% to 100.0% 0% 99.7% 98.0% 99.8% 0.16%
oracle_rotate_+5 438 99.5% 98.4% to 99.9% 0% 99.8% 99.0% 99.5% 0.16%
oracle_crop_50 438 0% 0% to 0.87% 0% 82.0% 75.0% 0% 0.16%
oracle_screenshot 438 99.5% 98.4% to 99.9% 0% 99.8% 99.0% 99.5% 0.16%

Unmarked originals (identity, n=500): detect's Q/B/C chain decoded a payload from 2 of 500 (0.4% (0.11 to 1.4%)); per decoder: Q 0, B 2, C 0.

Reading the table:

  • Compression, resizing, mild crops, tone edits, overlays and blur: survived. Variant Q recovers the exact payload from at least 93.9 percent of photos after JPEG q90 to q50 (and q75 twice), WebP q80 and q50, resizing to 0.5x, 0.75x and 1.5x, a 90 percent crop, blur up to radius 2, noise, sharpening, a 256-colour palette, a caption bar or a logo, brightness, contrast, saturation and gamma changes, and a 0.8 aspect squash (WebP q50 and the squash are the lowest at 93.9 percent). B and C hold up about as well on compression, at a lower embed PSNR.
  • Horizontal flip survives (Q 100.0 percent). Vertical flip is not a row; a development probe found it does not decode.
  • crop_75 is partial and crop_50, the corner crop and a 10 percent border are lost (Q 31.8, 0, 0 and 0 percent). The decoder resizes the whole frame to its fixed input size, so a crop or a border presents the embedded pattern at another scale and position, and production decode searches neither. oracle_crop_50 (the crop pasted back at its true position) is also 0 percent, although its raw bits rise to 82.1 percent: at 50 percent the mark needs a new embedding (tiling), not only a search.
  • Rotation: 2 degrees mostly survives, 5 degrees and quarter turns are lost. Q holds 90.1 and 93.1 percent at +2 and -2 degrees, 1.2 and 2.6 percent at 5 degrees, 0 percent at 15, 90 and 180 degrees. Production decode runs without TrustMark's rotation search (decode_variant mirrors decode without ROTATION). oracle_rotate_+5 (rotated back by the true angle) is 100.0 percent. B (45.0 and 40.3 percent) and C (27.2 and 28.3 percent) tolerate 5 degrees better than Q.
  • screenshot is lost (Q 0 percent, B 0.21 percent, C 0 percent) and oracle_screenshot (the photo's box cut out of the capture) recovers 100.0 percent. The canvas dilutes the mark: a 4:3 COCO photo fitted into 90 percent of a 1920x1080 screen covers only part of the decoder input and the rest is white canvas, which the decoder resizes as a whole. Canvas detection would recover it.
  • Wrong payloads are rare. The variant's own decoder returned a different payload for at most 0.41 percent of images (B, rotate_-5); on the lost transforms the raw bits sit at chance and the decoder mostly reports nothing. Detect's chain occasionally takes an earlier variant's wrong payload on a B- or C-marked image (chain_mis_decoded in the JSON), which is why the chain TPR can sit slightly below the decode rate. Production marks with Q and falls back to B or C only below the PSNR floor.
  • Unmarked originals. Detect's chain decoded a payload from 2 of the 500 unmarked originals (0.4 percent, both from B). The published operating point stays the 5,000-photo phantom rate (0.16 percent).
  • Embed skips. Each forced variant fell below the 38 dB floor on some photos (Q 7, B 16, C 62 of 500), and those are excluded from that variant's rates. Production tries Q first and then the fallbacks, so an image goes unmarked only when every variant falls below the floor.
  • Raw bit accuracy. After JPEG q75 the raw decoder bits of Q agree with the codeword in 99.7 percent of positions on average (5th percentile 99.0 percent). Detect reports the same quantity as the informational bit_accuracy; it is not a confidence, because a phantom decode also lies within the BCH radius (Grading).
  • img2img (--img2img, Stable Diffusion 1.5 at strengths 0.2 and 0.4) has not been run yet.

Where it breaks (variant Q)

Each sweep's harshest parameter that still holds at least 90 and 50 percent, the oracle curve at the same points, how far the failing images missed at the first failing point (raw bit errors: decoded at most 5, near at most 10, mid at most 25, far above), and a heuristic hint. The sweep tables, and the same analysis for B and C, are in the JSON (sweeps, where_it_breaks) and summarised in Robustness baseline.

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint target
rotation_ccw 2 (90.8%) 2 (90.8%) >= 90% up to 15; 100.0% at 3 at 3: decoded 42, near 22, mid 31, far 3 heuristic: resync/search candidate (22 near, 31 mid, 3 far of 56 failing; oracle 100.0%) (4 schema misses: a fixed-schema decode fixes them) 5 >= 90%: 1.0%, missed
rotation_cw -2 (93.9%) -2 (93.9%) >= 90% up to -15; 100.0% at -3 at -3: decoded 47, near 22, mid 24, far 5 heuristic: resync/search candidate (22 near, 24 mid, 5 far of 51 failing; oracle 100.0%) -5 >= 90%: 1.0%, missed
quarter_turn none (first below at 90) none (first below at 90) >= 90% up to 270; 100.0% at 90 at 90: far 98 heuristic: resync/search candidate (0 near, 0 mid, 98 far of 98 failing; oracle 100.0%) n/a
crop_centre 80 (100.0%) 80 (100.0%) >= 90% up to 70; 100.0% at 75 at 75: decoded 35, near 13, mid 35, far 15 heuristic: resync/search candidate (13 near, 35 mid, 15 far of 63 failing; oracle 100.0%) 50 >= 80%: 0%, missed
crop_corner 90 (100.0%) 80 (85.7%) >= 90% up to 80; 99.0% at 80 at 80: decoded 84, near 7, mid 3, far 4 heuristic: soft voting or stronger ECC candidate (7 near, 3 mid, 4 far of 14 failing; oracle 99.0%) (2 schema misses: a fixed-schema decode fixes them) n/a
scale_down 0.25 (98.0%, whole sweep) 0.25 (98.0%, whole sweep) n/a n/a n/a n/a
scale_up 3 (100.0%, whole sweep) 3 (100.0%, whole sweep) n/a n/a n/a n/a
aspect 0.8 (94.9%) 0.7 (62.2%, whole sweep) n/a at 0.7: decoded 61, far 37 heuristic: embedding change candidate (tiling, redundancy, sync) (0 near, 0 mid, 37 far of 37 failing; oracle absent) n/a
screenshot_fill none (first below at 90) none (first below at 90) >= 90% up to 30; 100.0% at 90 at 90: far 98 heuristic: resync/search candidate (0 near, 0 mid, 98 far of 98 failing; oracle 100.0%) 90 >= 80%: 0%, missed
jpeg 35 (92.9%) 25 (77.6%) n/a at 25: decoded 76, near 14, mid 7, far 1 heuristic: soft voting or stronger ECC candidate (14 near, 7 mid, 1 far of 22 failing; oracle absent) (2 schema misses: a fixed-schema decode fixes them) n/a
webp 70 (94.9%) 30 (79.6%, whole sweep) n/a at 50: decoded 88, near 4, mid 5, far 1 heuristic: no single candidate (mixed margins) (4 near, 5 mid, 1 far of 10 failing; oracle absent) (1 schema miss: a fixed-schema decode fixes it) n/a
blur 3 (92.9%) 4 (76.5%, whole sweep) n/a at 4: decoded 75, near 11, mid 6, far 6 heuristic: no single candidate (mixed margins) (11 near, 6 mid, 6 far of 23 failing; oracle absent) (3 schema misses: a fixed-schema decode fixes them) n/a
noise 15 (94.9%, whole sweep) 15 (94.9%, whole sweep) n/a n/a n/a n/a
brightness_down 0.6 (100.0%, whole sweep) 0.6 (100.0%, whole sweep) n/a n/a n/a n/a
brightness_up 1.2 (98.0%) 1.4 (87.8%, whole sweep) n/a at 1.4: decoded 86, near 10, mid 2 heuristic: soft voting or stronger ECC candidate (10 near, 2 mid, 0 far of 12 failing; oracle absent) (1 schema miss: a fixed-schema decode fixes it) n/a
contrast_down 0.6 (100.0%, whole sweep) 0.6 (100.0%, whole sweep) n/a n/a n/a n/a
contrast_up 1.2 (95.9%, whole sweep) 1.2 (95.9%, whole sweep) n/a n/a n/a n/a

Hints are a heuristic from the margin mix and the oracle rate at the first failing point, not a measurement: mostly near (>= 50% of failing items) suggests soft voting or a stronger ECC; mostly mid or far with an oracle >= 80% suggests a resync search; mostly far with a low or no oracle suggests an embedding change.

Schema-bit misses (variant Q; not decoded with at most 5 raw bit errors because bit 98 or 99, which pick TrustMark's schema and are outside the BCH, flipped; counted as near above): rotation_ccw at 2 (3), rotation_ccw at 3 (4), oracle crop_centre at 70 (2), oracle crop_centre at 65 (1), crop_corner at 80 (2), oracle crop_corner at 80 (1), oracle crop_corner at 70 (1), oracle screenshot_fill at 30 (1), jpeg at 95 (1), jpeg at 75 (1), jpeg at 65 (2), jpeg at 55 (1), jpeg at 45 (1), jpeg at 25 (2), jpeg at 15 (1), webp at 70 (1), webp at 50 (1), webp at 30 (2), blur at 4 (3), noise at 15 (1), brightness_up at 1.2 (1), brightness_up at 1.4 (1).

Oracle below the attack (variant Q; not a ceiling there, so the oracle ceiling column uses the attack's rate; the grey canvas blanks what a plain crop keeps at a new scale): none.

Targets on the main rows (variant Q):

row n variant decode rate chain TPR
crop_50 493 crop_50 >= 80%: 0%, missed crop_50 >= 80%: 0%, missed
rotate_+5 493 rotate_+5 >= 90%: 1.2%, missed rotate_+5 >= 90%: 1.2%, missed
rotate_-5 493 rotate_-5 >= 90%: 2.6%, missed rotate_-5 >= 90%: 2.6%, missed
screenshot 493 screenshot >= 80%: 0%, missed screenshot >= 80%: 0%, missed

Chain FPR target: chain FPR <= 0.5%: 0.16%, met (packaged phantom chain rate, n=5000); this bench's unmarked originals: 2 of 500 decoded by the chain, chain FPR <= 0.5%: 0.4%, met.

Changes from the published CPU run

The same seed and images on the GPU. Decisions differ only where the GPU embed's few-LSB differences meet a lossy codec or a borderline image; every change is inside the earlier Wilson interval.

variant row column CPU GPU
Q webp_80 decode rate and chain TPR 98.4% 98.2%
Q webp_50 decode rate and chain TPR 94.3% 93.9%
B crop_75 decode rate and chain TPR 68.0% 68.2%
B webp_80 decode rate and chain TPR 99.6% 99.8%
B jpeg_90 chain TPR 100.0% 99.8%
C webp_50 chain TPR 97.5% 97.3%

Raw-bit columns moved by at most one point on a few other rows (for example Q crop_50 mean 50.7 to 50.6 percent).

Audio robustness (AudioSeal, LibriSpeech and Goldberg)

What the AudioSeal watermark survives. Each clip is marked with the 16-bit issuer payload (issuer 4021) in two modes, then goes through each transform and is decoded by AudioSealWatermarker.decode (the batch detector, then the streaming one), as detect runs it. What the failures mean and how much a decode-side fix could recover is in Robustness baseline.

  • batch: the whole clip at once, as marker.mark_audio, /v1/mark/audio, ingest and the buffered speech proxy mark it.
  • streaming: the streaming embedder (watermark.audio.stream_kind: block, the only kind), each 20 ms block on its own, as the streaming speech proxy, laidline-pipecat and laidline-livekit mark it.
  • streaming_ctx: the experimental context kind (the batch card on each 20 ms block with 1000 ms of left context). It was measured and removed from the product: 22.4 % identity. Only the bench keeps it (laidline_bench.audio_robustness.ContextStream); its rows are kept below as a historical measurement.
# with the cu130 torch wheels; drop --device cuda to run on the CPU
laidline bench audio --corpus DIR --n 500 --workers 8 --device cuda

Point --corpus at a directory of your own unmarked clips; the published run sampled LibriSpeech and the Goldberg Variations (below). --music-bed FILE supplies the music that music_bed_10db mixes in.

Corpora: speech is 500 clips sampled (seed 0) from LibriSpeech test-clean (CC BY 4.0, https://www.openslr.org/resources/12/test-clean.tar.gz); music is 30 seeded 30 s excerpts of the Open Goldberg Variations (Kimiko Ishizaka, CC0, https://archive.org/metadata/OpenGoldbergVariations/files). The music bed of music_bed_10db is a seeded excerpt of a Goldberg track outside the scored 30. Every clip is also decoded unmarked, which gives the false-positive rate. Measured on the RTX 5080 (torch 2.14.0+cu130, TF32 off, cuDNN deterministic, 8 workers): machine.run_seconds 3012.7; 0 unreadable clips. The first published run (September 27, 2026, CPU) had the rows up to lowpass_4k plus the neural codecs; the rows from trim_10ms to compressor and the oracle are new. The result ships as laidline/compliance/data/audio_robustness.json (with the sweeps and "Where it breaks"), and the marking-method text of the evidence export quotes speech in batch mode from it. The neural codecs (encodec_6, mimi) run only in the full published run; their weights are downloaded for the bench and are never used by Laidline itself (Third-party licences). The Twilio row re-decodes saved phone-call recordings (see "Twilio PSTN loopback" below).

Transforms (transform_definitions in the JSON):

transform definition
identity WAV (PCM s16le) round trip
resample_8k resample to 8 kHz and back (libswresample)
resample_44k1 resample to 44.1 kHz and back (libswresample)
resample_48k resample to 48 kHz and back (libswresample)
opus_12 Opus 12 kbps, application voip (libopus, Ogg, 48 kHz)
opus_24 Opus 24 kbps, application voip (libopus, Ogg, 48 kHz)
opus_48 Opus 48 kbps, application voip (libopus, Ogg, 48 kHz)
g711_mulaw G.711 mu-law, 8 kHz mono (a PSTN leg), back to the source rate
g711_alaw G.711 A-law, 8 kHz mono (a PSTN leg), back to the source rate
mp3_64 MP3 64 kbps (libmp3lame)
mp3_128 MP3 128 kbps (libmp3lame)
aac_64 AAC-LC 64 kbps in MP4 (FFmpeg aac)
aac_128 AAC-LC 128 kbps in MP4 (FFmpeg aac)
tempo_0.9 tempo x0.9 without pitch change (libavfilter atempo), 11 percent longer
tempo_1.1 tempo x1.1 without pitch change (libavfilter atempo), 9 percent shorter
noise_20db white Gaussian noise at 20 dB SNR (seed 0)
lowpass_4k low-pass at 4 kHz (libavfilter lowpass, two-pole)
trim_10ms the first 10 ms cut (half of one 20 ms AudioSeal bit block, so the bit grid is out of phase)
trim_1s the first 1 s cut
crop_middle_50 the middle half kept (the first and last quarters cut)
splice_half the marked first half joined to the same clip's unmarked second half (a partially marked edit)
music_bed_10db a music bed mixed in 10 dB below the clip's RMS: the file named in the result's music_bed (the repo runner's is a seeded 30 s Open Goldberg Variations excerpt, CC0, from a track outside the scored music sample), mono, resampled to the clip's rate and looped to its length, added to every channel, clipped to [-1, 1]
pitch_+2st pitch up 2 semitones at the same length (libavfilter asetrate, aresample, atempo)
speed_1.1 speed x1.1 by resampling, so the pitch rises with it (about 1.65 semitones), 9 percent shorter
volume_-12db gain -12 dB (every sample x 0.251)
volume_+6db_clip gain +6 dB (every sample x 1.995), clipped to [-1, 1]
reverb room-like echo: reflections at 23, 37, 53 and 71 ms with decays 0.5, 0.4, 0.3 and 0.25 (libavfilter aecho, gains 0.8 in, 0.9 out), the tail cut to the input's length, clipped to [-1, 1]
bandpass_phone telephone band 300 to 3400 Hz (libavfilter highpass then lowpass, two-pole each)
compressor compressor: threshold -24 dB, ratio 4:1, attack 5 ms, release 100 ms, makeup +6 dB (libavfilter acompressor), clipped to [-1, 1]
oracle_trim_10ms Oracle (not a product number): trim_10ms, then a further 10 ms cut so the total is one whole 20 ms block (320 samples at 16 kHz) and the bit grid is back in phase; bounds a decode-side offset search
encodec_6 EnCodec 24 kHz at 6 kbps (facebook/encodec_24khz, transformers, cuda), mono
mimi Mimi, all 32 quantizers, about 4.4 kbps (kyutai/mimi, transformers, cuda), mono
twilio_loopback Twilio PSTN loopback recorded, re-decoded from the saved recordings: Twilio number-to-number call recorded at the callee (G.711, 8 kHz; carrier routing unconfirmed, possibly on-net)

AudioSeal 0.2.0 robustness (16-bit issuer payload 4021, modes batch/streaming): corpora speech n=500, music n=30, seed 0, measured.

Operating point: detection threshold 0.39, at which 0.75% of n=530 unmarked clips are flagged (95% interval 0.29 to 1.9%; target below 1.0%); at 0.5: 0.38%. This is the earlier false-positive basis for audio; the 2026-10 null campaign measured 5,000 unmarked units in each of four strata, with results above and below their lines (Audio null campaign).

speech, batch

n=500, skipped 0; embed SNR mean 26.6 dB, p5 22.2 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
resample_8k 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
resample_44k1 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
resample_48k 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
opus_12 99.6% (98.6 to 99.9%) 96.8% 96.2% 99.8% / 100.0% 0.78
opus_24 100.0% (99.2 to 100.0%) 100.0% 99.8% 100.0% / 100.0% 0.99
opus_48 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
g711_mulaw 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
g711_alaw 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
mp3_64 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
mp3_128 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
aac_64 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
aac_128 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
tempo_0.9 1.4% (0.68 to 2.9%) 0% 0.2% 97.9% / 93.8% 0.17
tempo_1.1 2.0% (1.1 to 3.6%) 0% 0.6% 98.6% / 93.8% 0.18
noise_20db 76.6% (72.7 to 80.1%) 48.2% 57.6% 97.3% / 87.5% 0.50
lowpass_4k 100.0% (99.2 to 100.0%) 100.0% 98.8% 99.9% / 100.0% 1.00
trim_10ms 100.0% (99.2 to 100.0%) 99.0% 0% 62.6% / 50.0% 0.94
trim_1s 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
crop_middle_50 54.4% (50.0 to 58.7%) 45.2% 5.8% 63.1% / 31.2% 0.44
splice_half 100.0% (99.2 to 100.0%) 88.2% 99.6% 99.8% / 100.0% 0.51
music_bed_10db 41.8% (37.6 to 46.2%) 8.6% 11.6% 90.8% / 75.0% 0.37
pitch_+2st 0.4% (0.11 to 1.4%) 0% 0% 70.9% / 43.8% 0.01
speed_1.1 1.2% (0.55 to 2.6%) 0.2% 0% 59.4% / 43.8% 0.01
volume_-12db 100.0% (99.2 to 100.0%) 100.0% 99.8% 100.0% / 100.0% 0.94
volume_+6db_clip 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
reverb 100.0% (99.2 to 100.0%) 99.8% 5.8% 91.9% / 87.5% 0.80
bandpass_phone 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
compressor 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
oracle_trim_10ms 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
encodec_6 70.8% (66.7 to 74.6%) 48.4% 0% 58.4% / 37.5% 0.50
mimi 1.0% (0.43 to 2.3%) 0.8% 0% 71.6% / 43.8% 0.02
twilio_loopback 99.0% (94.6 to 99.8%) 96.0% 98.0% 99.9% / 100.0% 0.92

speech, streaming

n=500, skipped 0; embed SNR mean 17.4 dB, p5 8.7 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
resample_8k 0% (0 to 0.76%) 0% 0% 58.3% / 31.2% 0.03
resample_44k1 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
resample_48k 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
opus_12 100.0% (99.2 to 100.0%) 98.8% 70.6% 97.9% / 93.8% 0.80
opus_24 100.0% (99.2 to 100.0%) 100.0% 99.6% 100.0% / 100.0% 0.99
opus_48 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
g711_mulaw 0% (0 to 0.76%) 0% 0% 59.6% / 31.2% 0.03
g711_alaw 0% (0 to 0.76%) 0% 0% 60.4% / 31.2% 0.03
mp3_64 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
mp3_128 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
aac_64 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
aac_128 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.98
tempo_0.9 99.8% (98.9 to 100.0%) 98.4% 90.4% 99.3% / 93.8% 0.85
tempo_1.1 100.0% (99.2 to 100.0%) 98.2% 94.0% 99.0% / 93.8% 0.83
noise_20db 100.0% (99.2 to 100.0%) 99.8% 99.2% 100.0% / 100.0% 0.89
lowpass_4k 100.0% (99.2 to 100.0%) 100.0% 0% 69.4% / 50.0% 0.97
trim_10ms 48.4% (44.0 to 52.8%) 29.4% 0% 51.3% / 43.8% 0.38
trim_1s 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
crop_middle_50 71.4% (67.3 to 75.2%) 67.6% 4.4% 51.6% / 31.2% 0.63
splice_half 100.0% (99.2 to 100.0%) 2.6% 99.2% 100.0% / 100.0% 0.49
music_bed_10db 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
pitch_+2st 100.0% (99.2 to 100.0%) 100.0% 0% 51.8% / 43.8% 0.93
speed_1.1 100.0% (99.2 to 100.0%) 100.0% 0% 60.7% / 50.0% 0.97
volume_-12db 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
volume_+6db_clip 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.98
reverb 100.0% (99.2 to 100.0%) 100.0% 31.4% 91.9% / 81.2% 0.99
bandpass_phone 100.0% (99.2 to 100.0%) 100.0% 0% 60.3% / 50.0% 0.92
compressor 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.98
oracle_trim_10ms 100.0% (99.2 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
encodec_6 100.0% (99.2 to 100.0%) 100.0% 0% 55.5% / 50.0% 0.96
mimi 0% (0 to 0.76%) 0% 0% 72.4% / 50.0% 0.00

music, batch

n=30, skipped 0; embed SNR mean 26.0 dB, p5 24.9 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
resample_8k 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
resample_44k1 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
resample_48k 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
opus_12 100.0% (88.6 to 100.0%) 100.0% 33.3% 94.2% / 87.5% 0.83
opus_24 100.0% (88.6 to 100.0%) 100.0% 26.7% 92.5% / 87.5% 0.99
opus_48 100.0% (88.6 to 100.0%) 100.0% 30.0% 94.8% / 87.5% 1.00
g711_mulaw 100.0% (88.6 to 100.0%) 100.0% 80.0% 98.8% / 93.8% 1.00
g711_alaw 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
mp3_64 100.0% (88.6 to 100.0%) 100.0% 83.3% 98.8% / 93.8% 1.00
mp3_128 100.0% (88.6 to 100.0%) 100.0% 83.3% 99.0% / 93.8% 1.00
aac_64 100.0% (88.6 to 100.0%) 100.0% 50.0% 96.2% / 87.5% 1.00
aac_128 100.0% (88.6 to 100.0%) 100.0% 70.0% 97.9% / 93.8% 1.00
tempo_0.9 0% (0 to 11.4%) 0% 0% 81.5% / 56.2% 0.18
tempo_1.1 0% (0 to 11.4%) 0% 0% 82.7% / 56.2% 0.17
noise_20db 100.0% (88.6 to 100.0%) 100.0% 76.7% 98.1% / 93.8% 0.99
lowpass_4k 100.0% (88.6 to 100.0%) 100.0% 73.3% 97.9% / 87.5% 1.00
trim_10ms 100.0% (88.6 to 100.0%) 100.0% 0% 48.1% / 31.2% 0.99
trim_1s 100.0% (88.6 to 100.0%) 100.0% 20.0% 92.3% / 81.2% 1.00
crop_middle_50 100.0% (88.6 to 100.0%) 100.0% 23.3% 92.3% / 81.2% 1.00
splice_half 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.51
music_bed_10db 100.0% (88.6 to 100.0%) 100.0% 6.7% 88.3% / 81.2% 0.83
pitch_+2st 0% (0 to 11.4%) 0% 0% 71.7% / 50.0% 0.01
speed_1.1 0% (0 to 11.4%) 0% 0% 60.0% / 43.8% 0.01
volume_-12db 100.0% (88.6 to 100.0%) 100.0% 66.7% 97.5% / 87.5% 0.98
volume_+6db_clip 100.0% (88.6 to 100.0%) 100.0% 76.7% 98.3% / 93.8% 1.00
reverb 100.0% (88.6 to 100.0%) 100.0% 26.7% 91.5% / 81.2% 0.92
bandpass_phone 100.0% (88.6 to 100.0%) 100.0% 20.0% 95.0% / 93.8% 1.00
compressor 100.0% (88.6 to 100.0%) 100.0% 80.0% 98.5% / 93.8% 1.00
oracle_trim_10ms 100.0% (88.6 to 100.0%) 100.0% 50.0% 96.0% / 87.5% 1.00
encodec_6 76.7% (59.1 to 88.2%) 73.3% 0% 44.2% / 31.2% 0.63
mimi 6.7% (1.8 to 21.3%) 0% 0% 44.4% / 25.0% 0.21

music, streaming

n=30, skipped 0; embed SNR mean 34.3 dB, p5 29.9 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
resample_8k 3.3% (0.59 to 16.7%) 0% 0% 56.5% / 43.8% 0.04
resample_44k1 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
resample_48k 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
opus_12 100.0% (88.6 to 100.0%) 100.0% 80.0% 98.8% / 93.8% 0.89
opus_24 100.0% (88.6 to 100.0%) 100.0% 3.3% 85.2% / 75.0% 0.90
opus_48 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 0.99
g711_mulaw 0% (0 to 11.4%) 0% 0% 68.5% / 50.0% 0.01
g711_alaw 0% (0 to 11.4%) 0% 0% 67.1% / 50.0% 0.00
mp3_64 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
mp3_128 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
aac_64 100.0% (88.6 to 100.0%) 100.0% 80.0% 98.8% / 93.8% 0.95
aac_128 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
tempo_0.9 100.0% (88.6 to 100.0%) 100.0% 0% 82.1% / 75.0% 0.99
tempo_1.1 100.0% (88.6 to 100.0%) 100.0% 0% 87.9% / 75.0% 0.99
noise_20db 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
lowpass_4k 100.0% (88.6 to 100.0%) 100.0% 0% 81.5% / 75.0% 1.00
trim_10ms 70.0% (52.1 to 83.3%) 30.0% 0% 57.9% / 50.0% 0.45
trim_1s 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
crop_middle_50 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
splice_half 100.0% (88.6 to 100.0%) 13.3% 100.0% 100.0% / 100.0% 0.50
music_bed_10db 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
pitch_+2st 100.0% (88.6 to 100.0%) 100.0% 0% 50.0% / 37.5% 0.99
speed_1.1 100.0% (88.6 to 100.0%) 100.0% 0% 63.7% / 62.5% 1.00
volume_-12db 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
volume_+6db_clip 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
reverb 100.0% (88.6 to 100.0%) 100.0% 0% 85.2% / 81.2% 1.00
bandpass_phone 100.0% (88.6 to 100.0%) 100.0% 0% 77.7% / 68.8% 1.00
compressor 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
oracle_trim_10ms 100.0% (88.6 to 100.0%) 100.0% 100.0% 100.0% / 100.0% 1.00
encodec_6 100.0% (88.6 to 100.0%) 100.0% 0% 50.2% / 43.8% 0.93
mimi 0% (0 to 11.4%) 0% 0% 44.6% / 31.2% 0.12

plugin gate: PASS (streaming (block): opus_24 = 100.0%, opus_48 = 100.0%, g711_mulaw = 0%, PASS; required 90% / 95%)

Columns: detection at 0.39 is the share of marked clips detected at the operating point, with its Wilson 95 percent interval; at 0.5 is the same at detect_threshold: 0.5, the configuration default; issuer exact counts clips detected at the operating point with all 16 issuer bits right, which is what detect needs to name the issuer; bits is the agreement of the 16 decoded bits with the payload (mean and 5th percentile; about 50 percent is chance); prob is the detector's score. oracle_trim_10ms is not a product number: it cuts a further 10 ms so the trim is one whole 20 ms block, which estimates what a decode-side offset search could recover.

Reading the tables:

  • Batch mode survives the codecs, the phone band, level changes and a compressor. On speech the batch mark is detected in 100 percent of clips after Opus 24 and 48 kbps, G.711 mu-law and A-law, the 8 kHz round trip, MP3 64 and 128 kbps, AAC 64 and 128 kbps, a 300 to 3400 Hz band-pass, -12 and +6 dB and the compressor, and in 99.6 percent after Opus 12 kbps (issuer exact 96.2 percent).
  • Batch mode does not survive a tempo, pitch or speed change or the neural codecs. Tempo x0.9 and x1.1 leave 1.4 and 2.0 percent, pitch +2 semitones 0.4 percent, speed x1.1 1.2 percent; EnCodec 6 kbps is detected in 70.8 percent with no clip naming the issuer (0 percent issuer exact); Mimi leaves 1.0 percent. White noise at 20 dB SNR leaves 76.6 percent, and a music bed 10 dB below the speech 41.8 percent (issuer exact 11.6 percent).
  • Trimming shifts the bit grid. AudioSeal reads its 16 bits against a 20 ms grid anchored at the first sample it sees. Cutting the first 10 ms keeps detection at 100 percent but no clip names the issuer (trim_10ms, 0 percent issuer exact); cutting a whole second keeps both (trim_1s, 100 percent), and oracle_trim_10ms (a whole 20 ms block cut) is back at 100 percent. Keeping the middle half, cut off the grid, leaves 54.4 percent detected and 5.8 percent exact. The start-offset sweep below shows the issuer lost at every offset that is not a multiple of 20 ms. The same shift explains aac output: raw ADTS AAC carries 1024 frames of encoder priming, so Laidline's own .aac output is detected but its issuer bits do not read back, and it carries the warning watermark_payload_unverified:aac. AAC in MP4 (m4a, the aac_64 and aac_128 rows) is not affected. The hardened decode's phase search reads such an offset; only the fast read does not.
  • Partial marks. A clip whose first half is marked and second half unmarked (splice_half) is detected in 100 percent at 0.39 but 88.2 percent at 0.5, because the score is the share of marked samples over the whole clip.
  • Reverb keeps detection and loses the issuer (100 percent detected, 5.8 percent exact).
  • The default streaming mark (block) does not survive a simulated phone leg. It is detected in at least 99.8 percent after Opus 24 and 48 kbps, MP3 and AAC, and after tempo, pitch and speed changes and a music bed, but in 0 percent after G.711 mu-law, G.711 A-law and the 8 kHz round trip, where the batch mark stays at 100 percent: the streaming card's residual sits mostly above 4 kHz, which an 8 kHz leg removes. EnCodec, a 4 kHz low-pass, the phone band, pitch and speed keep detection at 100 percent but no clip names the issuer. A 10 ms trim leaves 48.4 percent detected.
  • The streaming mark is louder. Embed SNR is mean 17.4 dB, 5th percentile 8.7 dB on speech, against 26.6 and 22.2 dB for the batch mark. Listen to your own voices before streaming them.
  • The context stream kind failed on real audio and was removed. It survived G.711 on synthetic tones in the unit tests, but on LibriSpeech it is detected in 22.4 percent of clips even without any transform (6.7 percent on music). stream_kind accepts only block. Its rows are in the historical tables below.
  • Music. Batch detection is 100 percent on the Goldberg excerpts for every transform except tempo, pitch and speed (0 percent), EnCodec (76.7 percent) and Mimi (6.7 percent), but the issuer is exact on fewer clips than on speech (83.3 percent with no transform, 26.7 to 33.3 percent after Opus). n=30, so the intervals are wide.
  • No loudness normalisation. AudioSeal's normaliser is off in Laidline's checkpoints: -12 dB keeps 100 percent, and the volume sweep below loses the issuer from -20 dB and detection at -30 dB.

Plugin gate. The Pipecat and LiveKit plugins were conditional on the streaming mark reaching at least 90 percent after Opus 24 kbps and 95 percent after Opus 48 kbps on speech, at the operating point. The block kind passed (100 and 100 percent); the context kind failed (10.6 and 15.4 percent). G.711 is reported and does not gate. Both plugins ship with the block kind (Pipecat, LiveKit Agents).

Detection threshold: 0.5 in the configuration, 0.39 in the published numbers. watermark.audio.detect_threshold defaults to 0.5, and detect reports a watermark only when AudioSeal's score reaches it. The bench picks the operating point as the lowest threshold (on a 0.01 grid) whose false-positive rate on the 530 unmarked clips (speech and music pooled) is below 1 percent: 0.39, at which 4 of 500 speech clips and 0 of 30 music clips were flagged (0.75 percent, 95 percent interval 0.29 to 1.9 percent). At 0.5 the rate is 0.38 percent. The marking-method text quotes detection at the deployment's detect_threshold when the bench counted at it (0.39, or 0.5 from the at 0.5 column; another value is quoted at 0.39 and said to be unmeasured). Set detect_threshold: 0.39 for more detection after heavy degradation; keep 0.5 for the lower false-positive rate and read the at 0.5 column (for example Opus 12 kbps drops from 99.6 to 96.8 percent in batch mode, and EnCodec from 70.8 to 48.4 percent). A detected watermark alone never gives marked_by_this_system (Grading).

Where it breaks

Sweeps on the first 100 speech clips and all 30 music clips. A point holds a clip when it is detected at 0.39 with all 16 bits right; margins are the failing clips' bit errors (near 1 to 2, mid 3 to 4, far 5 or more; unknown: not detected with the bits right). The start-offset oracle is each offset rounded up to a whole 20 ms block. The hint is a heuristic, not a measurement.

Where it breaks: speech, batch

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
offset_ms 20 (100.0%; first below at 1, after 0) 20 (100.0%; first below at 1, after 0) >= 90% up to 20; 100.0% at 1 at 1: near 59, mid 41 heuristic: soft voting or stronger ECC candidate (59 near, 41 mid, 0 far of 100 failing; oracle 100.0%)
tempo slower none (first below at 0.99) none (first below at 0.99) n/a at 0.99: decoded 17, near 2, far 1, unknown 80 heuristic: soft voting or stronger ECC candidate (2 near, 0 mid, 1 far of 3 failing; oracle absent)
tempo faster none (first below at 1.01) none (first below at 1.01) n/a at 1.01: decoded 18, near 5, far 1, unknown 76 heuristic: soft voting or stronger ECC candidate (5 near, 0 mid, 1 far of 6 failing; oracle absent)
pitch_st down none (first below at -0.5) none (first below at -0.5) n/a at -0.5: near 9, mid 46, far 45 heuristic: no single candidate (mixed margins) (9 near, 46 mid, 45 far of 100 failing; oracle absent)
pitch_st up none (first below at 0.5) none (first below at 0.5) n/a at 0.5: near 4, mid 29, far 67 heuristic: embedding change candidate (tiling, redundancy, sync) (4 near, 29 mid, 67 far of 100 failing; oracle absent)
speed slower none (first below at 0.98) none (first below at 0.98) n/a at 0.98: near 12, mid 48, far 40 heuristic: no single candidate (mixed margins) (12 near, 48 mid, 40 far of 100 failing; oracle absent)
speed faster none (first below at 1.02) none (first below at 1.02) n/a at 1.02: near 12, mid 50, far 38 heuristic: no single candidate (mixed margins) (12 near, 50 mid, 38 far of 100 failing; oracle absent)
noise_snr_db 30 (99.0%) 25 (89.0%) n/a at 25: decoded 89, near 11 heuristic: soft voting or stronger ECC candidate (11 near, 0 mid, 0 far of 11 failing; oracle absent)
music_bed_db none (first below at 20) 20 (79.0%) n/a at 20: decoded 79, near 18, mid 2, unknown 1 heuristic: soft voting or stronger ECC candidate (18 near, 2 mid, 0 far of 20 failing; oracle absent)
opus_kbps 12 (95.0%) 10 (72.0%) n/a at 10: decoded 72, near 9, mid 1, unknown 18 heuristic: soft voting or stronger ECC candidate (9 near, 1 mid, 0 far of 10 failing; oracle absent)
mp3_kbps 32 (100.0%, whole sweep) 32 (100.0%, whole sweep) n/a n/a n/a
volume_db quieter -12 (100.0%) -20 (56.0%) n/a at -20: decoded 56, near 40, mid 1, unknown 3 heuristic: soft voting or stronger ECC candidate (40 near, 1 mid, 0 far of 41 failing; oracle absent)
volume_db louder 6 (100.0%, whole sweep) 6 (100.0%, whole sweep) n/a n/a n/a
lowpass_khz 4 (99.0%) 3 (63.0%) n/a at 3: decoded 63, near 37 heuristic: soft voting or stronger ECC candidate (37 near, 0 mid, 0 far of 37 failing; oracle absent)
highpass_hz 100 (100.0%) 300 (88.0%) n/a at 300: decoded 88, near 12 heuristic: soft voting or stronger ECC candidate (12 near, 0 mid, 0 far of 12 failing; oracle absent)
kept_s 0.5 (100.0%, whole sweep) 0.5 (100.0%, whole sweep) n/a n/a n/a
spliced_pct 50 (99.0%) 50 (99.0%) n/a at 25: decoded 1, far 1, unknown 98 heuristic: embedding change candidate (tiling, redundancy, sync) (0 near, 0 mid, 1 far of 1 failing; oracle absent)

Where it breaks: speech, streaming

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
offset_ms 20 (100.0%; first below at 1, after 0) 20 (100.0%; first below at 1, after 0) >= 90% up to 20; 100.0% at 1 at 1: far 100 heuristic: resync/search candidate (0 near, 0 mid, 100 far of 100 failing; oracle 100.0%)
tempo slower 0.95 (94.0%, whole sweep) 0.95 (94.0%, whole sweep) n/a n/a n/a
tempo faster 1.05 (96.0%, whole sweep) 1.05 (96.0%, whole sweep) n/a n/a n/a
noise_snr_db 15 (94.0%) 10 (57.0%) n/a at 10: decoded 57, near 7, unknown 36 heuristic: soft voting or stronger ECC candidate (7 near, 0 mid, 0 far of 7 failing; oracle absent)
opus_kbps 16 (94.0%) 12 (70.0%) n/a at 12: decoded 70, near 29, mid 1 heuristic: soft voting or stronger ECC candidate (29 near, 1 mid, 0 far of 30 failing; oracle absent)
lowpass_khz 8 (99.0%) 8 (99.0%) n/a at 6: near 27, mid 73 heuristic: no single candidate (mixed margins) (27 near, 73 mid, 0 far of 100 failing; oracle absent)

Where it breaks: music, batch

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
tempo slower none (first below at 0.99) none (first below at 0.99) n/a at 0.99: near 18, mid 2, unknown 10 heuristic: soft voting or stronger ECC candidate (18 near, 2 mid, 0 far of 20 failing; oracle absent)
tempo faster none (first below at 1.01) none (first below at 1.01) n/a at 1.01: near 16, mid 2, unknown 12 heuristic: soft voting or stronger ECC candidate (16 near, 2 mid, 0 far of 18 failing; oracle absent)
opus_kbps none (first below at 24) none (first below at 24) n/a at 24: decoded 8, near 22 heuristic: soft voting or stronger ECC candidate (22 near, 0 mid, 0 far of 22 failing; oracle absent)

Where it breaks: music, streaming

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
tempo slower none (first below at 0.99) 0.99 (83.3%) n/a at 0.99: decoded 25, near 5 heuristic: soft voting or stronger ECC candidate (5 near, 0 mid, 0 far of 5 failing; oracle absent)
tempo faster none (first below at 1.01) 1.02 (50.0%) n/a at 1.01: decoded 25, near 5 heuristic: soft voting or stronger ECC candidate (5 near, 0 mid, 0 far of 5 failing; oracle absent)
opus_kbps 16 (100.0%; first below at 24) 10 (60.0%; first below at 24) n/a at 24: decoded 1, near 15, mid 14 heuristic: soft voting or stronger ECC candidate (15 near, 14 mid, 0 far of 29 failing; oracle absent)

Hints are a heuristic from the margin mix and the oracle rate at the first failing point, not a measurement: mostly near (>= 50% of failing items) suggests soft voting or a stronger ECC; mostly mid or far with an oracle >= 80% suggests a resync search; mostly far with a low or no oracle suggests an embedding change.

The start-offset sweep (speech, n=100): the issuer reads back only when the cut is a whole number of 20 ms blocks.

offset_ms batch detected batch exact streaming detected streaming exact
0 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%)
1 100.0% (96.3 to 100.0%) 0% (0 to 3.7%) 100.0% (96.3 to 100.0%) 0% (0 to 3.7%)
2 100.0% (96.3 to 100.0%) 0% (0 to 3.7%) 100.0% (96.3 to 100.0%) 0% (0 to 3.7%)
5 53.0% (43.3 to 62.5%) 0% (0 to 3.7%) 41.0% (31.9 to 50.8%) 0% (0 to 3.7%)
8 37.0% (28.2 to 46.8%) 0% (0 to 3.7%) 99.0% (94.6 to 99.8%) 0% (0 to 3.7%)
10 100.0% (96.3 to 100.0%) 0% (0 to 3.7%) 54.0% (44.3 to 63.4%) 0% (0 to 3.7%)
12 61.0% (51.2 to 70.0%) 0% (0 to 3.7%) 100.0% (96.3 to 100.0%) 0% (0 to 3.7%)
15 94.0% (87.5 to 97.2%) 0% (0 to 3.7%) 0% (0 to 3.7%) 0% (0 to 3.7%)
18 95.0% (88.8 to 97.8%) 0% (0 to 3.7%) 100.0% (96.3 to 100.0%) 0% (0 to 3.7%)
20 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%) 100.0% (96.3 to 100.0%)
30 100.0% (96.3 to 100.0%) 0% (0 to 3.7%) 71.0% (61.5 to 79.0%) 0% (0 to 3.7%)
50 100.0% (96.3 to 100.0%) 0% (0 to 3.7%) 67.0% (57.3 to 75.4%) 0% (0 to 3.7%)

The other sweeps (tempo, pitch, speed, noise, music bed, Opus, MP3, volume, low-pass, high-pass, kept duration and spliced share, and tempo and Opus on music) are in the JSON (corpora.<corpus>.sweeps) and summarised in Robustness baseline.

Changes from the published CPU run

The same seed and clips on the GPU. The pilot's decisions were identical on the two devices (75 identical, 0 differ); in the full run these rows moved, the detection rates within the earlier run's Wilson interval (the issuer-exact column has no printed interval; music is n=30):

corpus, mode row column CPU GPU
speech, batch opus_12 detection at 0.39 99.4% 99.6%
speech, batch opus_12 at 0.5 97.2% 96.8%
speech, batch opus_12 issuer exact 95.6% 96.2%
speech, batch opus_24 issuer exact 100.0% 99.8%
speech, batch encodec_6 at 0.5 48.2% 48.4%
speech, streaming opus_12 detection at 0.39 99.8% 100.0%
speech, streaming opus_12 issuer exact 69.4% 70.6%
music, batch opus_12 issuer exact 30.0% 33.3%
music, batch opus_24 issuer exact 33.3% 26.7%
music, batch opus_48 issuer exact 33.3% 30.0%
music, batch mp3_128 issuer exact 80.0% 83.3%
music, streaming opus_12 issuer exact 70.0% 80.0%

Bit-accuracy means moved by at most 0.7 points and prob means by at most 0.01 on a few other rows, and speech streaming opus_12 at 0.5 went from 98.6% to 98.8%. The Twilio row is unchanged.

Historical: streaming_ctx (measured on the CPU)

The removed context kind, from the first published run (torch 2.14.0+cpu, 6 workers). Not re-run.

speech, streaming_ctx

n=500, skipped 0; embed SNR mean 30.0 dB, p5 25.9 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 22.4% (19.0 to 26.3%) 5.8% 13.8% 95.7% / 81.2% 0.25
resample_8k 22.2% (18.8 to 26.0%) 4.8% 13.6% 95.5% / 81.2% 0.24
resample_44k1 22.6% (19.2 to 26.5%) 5.8% 13.8% 95.7% / 81.2% 0.25
resample_48k 22.6% (19.2 to 26.5%) 5.6% 13.8% 95.7% / 81.2% 0.25
opus_12 3.8% (2.4 to 5.9%) 0.6% 0.4% 90.5% / 75.0% 0.08
opus_24 10.6% (8.2 to 13.6%) 1.6% 3.6% 93.9% / 81.2% 0.15
opus_48 15.4% (12.5 to 18.8%) 2.8% 8.4% 94.7% / 81.2% 0.19
g711_mulaw 20.0% (16.7 to 23.7%) 4.4% 12.6% 95.8% / 87.5% 0.24
g711_alaw 23.2% (19.7 to 27.1%) 5.0% 13.6% 96.0% / 87.5% 0.24
mp3_64 21.6% (18.2 to 25.4%) 4.0% 13.0% 95.6% / 81.2% 0.24
mp3_128 23.0% (19.5 to 26.9%) 5.4% 14.0% 95.9% / 81.2% 0.24
aac_64 20.2% (16.9 to 23.9%) 4.6% 11.2% 95.0% / 81.2% 0.23
aac_128 20.6% (17.3 to 24.4%) 5.0% 12.2% 95.1% / 81.2% 0.23
tempo_0.9 0.2% (0.035 to 1.1%) 0.2% 0% 76.7% / 50.0% 0.04
tempo_1.1 0.2% (0.035 to 1.1%) 0% 0% 78.3% / 56.2% 0.05
noise_20db 0% (0 to 0.76%) 0% 0% 85.7% / 75.0% 0.00
lowpass_4k 19.8% (16.5 to 23.5%) 3.8% 7.4% 92.2% / 75.0% 0.23
encodec_6 12.2% (9.6 to 15.4%) 4.6% 0% 48.6% / 31.2% 0.22
mimi 1.2% (0.55 to 2.6%) 0% 0% 70.4% / 43.8% 0.01

music, streaming_ctx

n=30, skipped 0; embed SNR mean 29.5 dB, p5 27.8 dB.

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
identity 6.7% (1.8 to 21.3%) 0% 3.3% 81.9% / 68.8% 0.16
resample_8k 6.7% (1.8 to 21.3%) 0% 3.3% 81.2% / 68.8% 0.16
resample_44k1 6.7% (1.8 to 21.3%) 0% 3.3% 81.5% / 68.8% 0.16
resample_48k 6.7% (1.8 to 21.3%) 0% 3.3% 81.9% / 68.8% 0.16
opus_12 0% (0 to 11.4%) 0% 0% 82.5% / 68.8% 0.03
opus_24 3.3% (0.59 to 16.7%) 0% 3.3% 79.4% / 68.8% 0.10
opus_48 0% (0 to 11.4%) 0% 0% 76.5% / 62.5% 0.13
g711_mulaw 6.7% (1.8 to 21.3%) 0% 3.3% 81.0% / 68.8% 0.17
g711_alaw 6.7% (1.8 to 21.3%) 0% 3.3% 81.2% / 68.8% 0.16
mp3_64 6.7% (1.8 to 21.3%) 0% 3.3% 83.3% / 68.8% 0.15
mp3_128 6.7% (1.8 to 21.3%) 0% 3.3% 81.0% / 68.8% 0.16
aac_64 6.7% (1.8 to 21.3%) 0% 3.3% 81.2% / 68.8% 0.16
aac_128 6.7% (1.8 to 21.3%) 0% 3.3% 81.5% / 68.8% 0.17
tempo_0.9 0% (0 to 11.4%) 0% 0% 69.0% / 43.8% 0.02
tempo_1.1 0% (0 to 11.4%) 0% 0% 70.2% / 50.0% 0.01
noise_20db 3.3% (0.59 to 16.7%) 0% 0% 79.4% / 68.8% 0.15
lowpass_4k 6.7% (1.8 to 21.3%) 0% 3.3% 77.9% / 62.5% 0.19
encodec_6 80.0% (62.7 to 90.5%) 70.0% 0% 39.8% / 25.0% 0.63
mimi 0% (0 to 11.4%) 0% 0% 43.1% / 31.2% 0.18

plugin gate: PASS (streaming (block): opus_24 = 100.0%, opus_48 = 100.0%, g711_mulaw = 0%, PASS; streaming_ctx (context): opus_24 = 10.6%, opus_48 = 15.4%, g711_mulaw = 20.0%, FAIL; required 90% / 95%)

Twilio PSTN loopback (recorded, re-decoded)

A real phone leg, with the batch mark: 100 batch-marked speech clips (16 kHz mono WAV from the LibriSpeech sample above) plus 10 unmarked control clips, each played into a Twilio number-to-number call and recorded at the callee (number B's recording, G.711 at 8 kHz), with the bench's telephony recorder. Twilio does not say how a call between two of its own numbers is routed, so the leg may have stayed on Twilio's network (carrier routing unconfirmed, possibly on-net): the recording is G.711 at 8 kHz, but it is not proof of a carrier PSTN path. Each recording is aligned to its clip by cross-correlation and decoded like the rows above; a call whose clip did not arrive would have counted as an error, not a miss. The GPU run placed no calls: it re-decoded the saved recordings on the GPU, and the row equals the first measurement. The result is the twilio_loopback row of the speech, batch table above:

transform detection at 0.39 at 0.5 issuer exact bits mean/p5 prob mean
twilio_loopback 99.0% (94.6 to 99.8%) 96.0% 98.0% 99.9% / 100.0% 0.92
  • n=100 calls, 0 call errors. Detection 99.0 percent (95 percent interval 94.6 to 99.8 percent) at 0.39 and 96.0 percent at 0.5; issuer exact 98.0 percent; bits mean 99.9 percent; prob mean 0.92 (5th percentile 0.5792).
  • 10 unmarked control calls: 0 flagged (highest score 0.0055).
  • The streaming mark's phone evidence is the G.711 simulation above (0 percent), so the streaming speech proxy and the Pipecat and LiveKit plugins keep "does not survive a simulated phone leg"; a real call carried the batch mark at 99 percent. That is the unkeyed 16-bit streaming mark; the keyed buffered stream of a laidline init deployment reads through simulated G.711 (100 of 100 units, "Voice calls" below).

Voice calls

The voice-call matrix of the 2026-10 null campaign (campaign voice, run on 2026-10-28 UTC, rules and units fixed before the first read) measures the keyed audioseal-p70-r1 mark the way a voice bot sends it: a clip fed through the buffered stream marker in chunks (20 ms unless named), at 16 or 24 kHz, then a simulated phone path (codecs, packet loss, noise, reverb), then read by Marker.detect with the shipped defaults of 2026-10-27 (decode: hardened, hardened_read: off) on a GPU. 10,200 units, each read keyed and unkeyed: 6,200 marked units and 4,000 unmarked ones. The codecs are ffmpeg's encoders (simulated paths, not carrier paths); the real-call runs are above. "Payload read" means the tag verified under the deployment's key and the payload equals the one written (tag_accept). Every cell has n = 100 unless named. The figures are measured on the shipped read; the phase search added since (Payload keys, "Audio") was not in the run; the v2 matrix of 2026-10-30 measured it (off by default, payload.phase_search; numbers under "Voice calls" in Audio).

The four headline lines passed; two pre-registered lines failed. Payload read at 10 s fed: 16 kHz stream 100/100, 24 kHz stream 100/100, G.711 mu-law 100/100, Opus 24 kbit/s 100/100 (Wilson 96.3 to 100 %). The line "0 wrong payloads" failed on 6 of 6,200 marked units (3 under reverb, 2 under white noise at 20 dB, 1 through G.722): each read a payload of another issuer number, its tag failed under all six live keys, the grade was D and the public tier showed presence only. The line "0 cross-issuer labels" failed on the same 6. Since the unverified-issuer rule of 2026-10-28 (Grading, "Keyed image payloads"; Payload keys, "Response tiers") a payload whose tag failed is shown on no tier, so none of the 6 would be shown. The report-only line "tag accepts on unmarked units at most 0.15 % (Wilson upper)" cannot be met at n = 1,000 per row (it needs 2,557); the measured rate was 0 of 1,000 on each of the four rows.

Length and presence

Barge-in units are the fed length minus 0.5 s, because the hold-back is dropped at close.

fed delivered public presence 16 / 24 kHz payload read 16 / 24 kHz
2, 3, 4, 6, 8 s 1.5 to 7.5 s 100 / 100 % 0 / 0 % (too_short)
10, 15, 30 s 9.5, 14.5, 29.5 s 100 / 100 % 100 / 100 %

A keyless re-read (stage 1 and the choice, no tag) of the 100 + 100 units of 30 s, cut from their start: 100 % correct at 8.0, 8.25, 8.5, 8.75 and 9.0 s delivered at both rates, 0 wrong; 7.5 s is too_short. The payload floor is 8.0 s delivered on the mark's 20 ms grid (one P70 cycle); through the stream marker that is 10 s fed (9.5 s delivered), or at least 8.5 s fed for a barge-in. Presence reads from 1.5 s delivered. The docs keep 10 s as the claim. The keyless re-read agreed with the cloud tag accept on 711 of 711 units from 25 cells.

Alignment

The floor holds only on the grid. Stage 1 reads at phase 0 on 320-sample blocks counted from the first sample of the file, and every cloud unit was cut on that grid.

condition payload read
9.5 s excerpt starting on the 20 ms grid, 16 / 24 kHz 100 / 100 of 100
9.5 s excerpt starting 1 to 319 samples off the grid, 16 / 24 kHz 6 / 4 of 100 (presence on 81 / 77 %)
clean 10 s unit delayed by 1 sample 49 of 50
delayed by 2, 3, 8, 11, 22, 80, 160 or 480 samples 0 of 50 each
delayed by 5 samples 9 of 50 (1 wrong)
delayed by 320 or 640 samples (whole blocks) 50 of 50 each

Off-grid audio keeps presence and loses the payload unless the phase search is switched on (payload.phase_search, off by default; v2 matrix). A keyless stage-2 probe (CPU, 26 reads of clean units after an 11-sample delay, G.722, AMR-WB, AMR-NB and white noise at 20 dB) read 26 of 26 with 0 wrong, against 1 of 26 for stage 1; it costs 77 to 200 s per read on a loaded CPU, and its false-accept side on unmarked audio is not measured.

Codecs

Payload read (tag accept) at 10 s fed, out of 100. Every 4 s cell reads no payload.

codec payload read public / full presence median confidence why
G.711 mu-law, A-law 100, 100 100 / 100 0.99 waveform kept, no delay
G.726 at 32 kbit/s 100 100 / 100 0.98 waveform kept, no delay
MP3 (the signed twin) 100 100 / 100 0.99
Opus 32, 24, 16 kbit/s 100, 100, 100 100 / 100 0.99, 0.99, 0.95
Opus 12 kbit/s 98 100 / 100 0.85
Opus 8 kbit/s 0 26 / 66 0.40 destroys the mark: 4 of 50 even with the delay removed
G.722 0 95 / 100 0.70 22-sample delay: 50 of 50 once removed
AMR-WB at 23.85 kbit/s 0 12 / 100 0.34 95-sample delay: 48 of 50 once removed (96 % aligned)
AMR-NB at 12.2 kbit/s 0 14 / 99 0.29 80 to 83-sample delay (v2 measured) plus damage: 28 of 50 once removed (56 % aligned)
Speex 0 99 / 99 0.85 CELP does not keep the waveform: 0 of 50

G.729 and EVS are not measured: ffmpeg 7.0.2 has no encoder. Bandwidth is not the reason: the mark's residual has 99.999 % of its energy under 4 kHz, 23.5 dB under the speech (median, range 17.8 to 30.3 dB); G.711 and a plain 4 kHz low-pass keep it (50 of 50 each), a 4 kHz high-pass removes it (0 of 50). Where a codec fails only through delay, the failure is the alignment above, not the codec.

Packet loss, noise and reverb

condition payload read public presence
packet loss 1, 3, 5 and 10 %, repeat-last or zero fill 100 of 100 in every cell 100 %
white noise at 30 / 25 / 20 dB SNR 99 / 83 / 30 100 / 83 / 35 %
babble noise at 30 / 25 / 20 dB SNR 99 / 100 / 94 100 / 100 / 95 %
reverb, RT60 0.3 s, direct-to-reverb ratio 0 dB 8 94 % (full tier 99 %)

CPU sweeps of 30 units each locate the limits. White noise, payload read by SNR: 35 dB 100 %, 30 dB 100 %, 27.5 dB 97 %, 25 dB 77 %, 22.5 dB 57 %, 20 dB 33 %, 17.5 dB 17 %, 15 dB 7 %; half the payloads are lost at about 22 dB SNR. Babble is milder (94 % at 20 dB). Reverb at 0 dB direct-to-reverb ratio: payload read 13, 7, 13 and 3 % at RT60 0.1, 0.2, 0.3 and 0.5 s, with presence at 83 to 93 %; the ratio matters more than the RT60: at RT60 0.3 s, +6 dB reads 67 % and +12 dB reads 100 %. Reverb followed by G.711 reads 3 %. Audio re-recorded in a room keeps presence and usually loses the payload.

Unmarked voice

Four rows of 1,000 unmarked units each (583 to 593 source clusters), read keyed and unkeyed:

row public presence full-tier presence payload read
G.711 mu-law 0 of 1,000 7 0
Opus 12 kbit/s 1 of 1,000 5 0
AMR-NB 0 of 1,000 4 0
5 % loss, repeat fill 1 of 1,000 7 0

Wilson upper bound 0.56 % on the rows with one hit. 0 tag accepts on 4,000 unmarked reads. Read without a key, the 16-bit word names a random issuer: 0 of the 6,103 marked units present on the unkeyed read named issuer 1.

With the phase search on (v2 matrix, opt-in)

Two further matrices (the v2 matrix of 2026-10-30: a core matrix of 1,250 positives and 13,000 unmarked units, and a broad matrix of 6,850 positives and 13,000 unmarked units; 100 per cell, preregistered lines) re-ran this section's conditions on the build with the phase search added. Every figure in this subsection is with payload.phase_search: on. The search ships off. The shipped defaults, and every figure in the sections above, are the first run's. The cells that keep the 20 ms grid (G.711, G.726, Opus at 16 kbit/s and above, the MP3 twin, 10 s fed) do not use the search and read the same either way.

cell (10 s fed unless named) shipped default (search off) with payload.phase_search: on (ships off) sealed line, where one applies
On-grid 16 and 24 kHz, G.711 mu-law, Opus 24 kbit/s 100 / 100 each 200 / 200 each (two runs pooled) 97 / 97 / 95 / 90 of 100
G.722 0 / 100 192 / 200 = 96.0 % (92.3 to 98.0 %); 94 / 100 in one run, 98 / 100 in the other 95 of 100
AMR-WB 23.85 kbit/s 0 / 100 200 / 200 -
AMR-NB 12.2 kbit/s 0 / 100 91 / 100 -
9.5 s excerpt off the 20 ms grid, 16 kHz 6 / 100 194 / 200 = 97.0 % (93.6 to 98.6 %) 95 of 100
9.5 s excerpt off the 20 ms grid, 24 kHz 4 / 100 96 / 100 -
10 s clip with a delayed start 0 of 50 for most delays (table "Alignment") 191 / 200 = 95.5 % (91.7 to 97.6 %) 95 of 100
  • Unmarked voice. 26,000 unmarked units in two matrices of five rows each: 0 tag accepts, and public presence on at most 6 of 2,600 per row (0.23 %, line 0.5 %). The search chose a keyless payload on 2 of the 26,000; both tags failed and both were shown as presence without a payload. The preregistered line allowed none, so the line is not met, which is why the search ships off.
  • Wrong payloads. None shown on any of the 1,250 and 6,850 marked units. Counted strictly, as the preregistered line counts them, the search produced an internal keyless pick that the tag rejected on 15 of the 1,250 and 27 of the 6,850. The first run's 6 wrong payloads on 6,200 marked units are the figures in the opening paragraph.
  • Detect time. The line is p95 within 0.1 x duration + 1.5 s. On an A10G every cell is within it (worst 0.92 of the budget). On an L4 five cells with the search (both AMR rows, the delayed clip, both off-grid excerpts) run 7 to 10 % over it, and G.722 as well when the L4 machines are taken alone (1.07). The latency line was bound to the A10G; the L4 excess is a recorded deviation. The search costs about +0.5 s per read at p50.
  • Same decisions on a second GPU type and a desktop GPU. The first run: 400 reads compared between the A10G and the L4, 400 between the A10G and the RTX 5080, 0 mismatches. The v2 reads on the RTX 5080: 400 reads compared, 0 mismatches (largest confidence difference 1e-4).

Stream telemetry

Compute numbers on the local RTX 5080 with no real-time pacing, not on the cloud boxes.

rate, chunk units first marked frame (audio) held back at close worst chunk per unit p50 / p99 chunks over budget real-time factor p50
16 kHz, 20 ms 4,900 0.70 s 0.50 s 16.5 / 48 ms 1,473 of 2.05 million 0.024
24 kHz, 20 ms 1,000 0.72 s 0.50 s 16.9 / 44 ms 444 of 490,000 0.026
16 kHz, 10 ms 100 0.70 s 0.50 s 17 / 22 ms 1.6 % of chunks, every unit 0.026
16 kHz, 40 ms 100 0.72 s 0.50 s 18 / 29 ms 0 0.025
16 kHz, 100 ms 100 0.70 s 0.50 s 16 / 26 ms 0 0.024

The 0.7 s is the buffered embedder's lookahead, not compute; flush takes about 12 ms, and a barge-in drops the 0.5 s held back. The chunk that completes a 320-frame model block takes 15 to 18 ms, so 10 ms chunks miss their budget on every block boundary: feed 20 ms or more. One 960 ms first chunk was a cold start. Sessions below 16 kHz are passed through unmarked (not measured, by design).

Null campaign 2026-10: how these were run

The sections below ("Image null campaign", "Video null campaign", "Audio null campaign", "Text null campaign") and the v2 table under "Voice calls" report one campaign, nulls-2026-10, run from 2026-10-25 to 2026-10-30. It measures two things for each medium: how often unmarked media is flagged (false positives), and whether blind marks of a test issuer are read back (sensitivity). About 420,000 reads in all.

  • Pass lines were fixed first. For every medium the pass lines and the preregistration (units, reads, intervals, verdict rules) were sealed in version control before any unit was selected, marked or read. Each result below is stated against its sealed line, including the ones that fall short of it. A line is never reworded after the fact.
  • Blind positives. The marked units were produced by the product's own marker with a test issuer, mixed into the reads under neutral ids, and counted against a manifest that was opened only after the reads were done.
  • Intervals. Wilson 95 % is the headline interval throughout. In no medium did an interval resampled by source exceed 1.5 times the Wilson interval, and where a count is 0 the Wilson upper bound is the figure to read.
  • Hardware. Image, video, audio and voice reads ran on cloud GPU machines (NVIDIA L4 and A10G; L40S for part of the video run). Text ran on a local CPU. "Same decisions on a second GPU type and on a desktop GPU" means that 200 to 400 units per medium were read again on another GPU type and on a desktop RTX 5080, and every decision field was compared; a count of mismatches is given in each section.
  • Corpora and licences. Public corpora only: COCO train2017, Open Images, YouTube UGC, Wikimedia Commons, Netflix Open Content, Xiph derf, LibriSpeech, Multilingual LibriSpeech, the Free Music Archive, FSD50K, C4, mC4, PG-19, arXiv abstracts, MAGE. Sources are licensed CC0, CC BY, public domain, ODC-BY or Apache-2.0, with these stated exceptions: Wikipedia text (CC BY-SA) and Wikimedia Commons clips admitted under CC BY-SA, read for measurement only and not republished; the Xiph derf test sequences and the Sintel film (97 of the 4,854 video clips), whose licences were not verified one by one. Sources used in earlier tuning ("seen": 982 of the video UGC clips, the train-clean-100 LibriSpeech split, a C4 shard) are tagged, and results are reported by source so that they can be checked.

What the campaign does not establish

  • No digest of the stored records was committed before the manifests were opened, in any medium. The lines and preregistrations were sealed in advance; the records were not.
  • The registry bundles in this campaign held controls only, so grade C (a registry-confirmed hit by the deployment's own record) was not exercised. Image and video positives reached grade B by fingerprint match.
  • The text grade-B gate (p < 1e-5 on the exact tail) and the audio streaming-card rule were chosen on the same data they are reported on. They are in-sample choices, not held-out results. The audio rule was applied by replaying the stored records, not by a new run of the product.
  • The voice null rows share source audio (each source appears in several rows), so the rows are not independent.
  • 40 DCI 4K video clips (4096x2160) were refused by watermark.video.max_pixels at every read and are left out of the video counts.
  • The audio strata of other sounds (FSD50K) and synthesised speech (Kokoro) were added after the preregistration. They are reported and bind no line.

Image null campaign

Corpus. 38,500 unmarked photos as distributed (JPEG): 19,175 COCO train2017 photos (Flickr licence CC BY 2.0 or public domain) and 19,325 Open Images validation and test photos (CC BY 2.0, at most 25 per author). 2,200 screenshots (phone 390x844 and desktop 1920x1080 page captures of other Open Images photos, PNG) are a report-only second stratum. 42,200 units, each read four ways (168,800 reads). Positives: 1,500 photos marked by the product with a test issuer, blind, 500 for each of PixelSeal (the default scheme), TrustMark and keyed PixelSeal.

What was measured. The share of unmarked photos that Marker.detect flags (PixelSeal at the shipped defaults; keyed PixelSeal: a tag that verifies under the deployment's key), and the share of blind positives read back with the issuer's exact payload (350 unedited, 75 after JPEG quality 75, 75 after a resize to 0.6).

measure result Wilson 95 % upper sealed line met
PixelSeal default read, presence on unmarked photos 0 / 38,500 0.0100 % at most 19 of 38,500 (0.05 %) yes
Keyed PixelSeal, tag accepted on unmarked photos 0 / 38,500 0.0100 % Wilson upper at most 0.15 % yes
Screenshots of unmarked photos (report only) 0 / 2,200 0.174 % none -
Sensitivity, unedited, PixelSeal 350 / 350 (98.9 to 100 %) at least 340 of 350 yes
Sensitivity, unedited, keyed PixelSeal 350 / 350 (98.9 to 100 %) at least 340 of 350 yes
Sensitivity, unedited, TrustMark (comparison) 346 / 350 (97.1 to 99.6 %) at least 340 of 350 yes
Wrong payloads, cross-issuer labels (1,500 positives) 0, 0 0, 0 yes
  • Sensitivity after edits (report only). PixelSeal and keyed PixelSeal read 75 of 75 after JPEG quality 75 and 75 of 75 after the resize; TrustMark 74 of 75 and 73 of 75. Every positive was read back at grade B by fingerprint match, never by exact bytes (the metadata strip changes the bytes).
  • The 7 TrustMark misses were never watermarked. The marker declined to embed them because the result would have fallen below the 38 dB quality floor (1.4 % of photos); the fast decoder finds nothing on them.
  • Keyed reads of unmarked photos. Before the unverified-issuer rule (Grading, "Keyed image payloads"), a keyed read carried a payload word whose tag had failed on 6.0 % of unmarked photos. Since the rule a word whose tag failed is shown on no tier. The 0 of 38,500 above is tag accepts, which that rule does not change.
  • Detect time. p50 97 to 390 ms by image size up to 4 megapixels for the PixelSeal read (overall p50 139 ms, p95 185 ms); the read is CPU-bound (mean GPU utilisation 6 to 8 % per machine).
  • Same decisions on a second GPU type and a desktop GPU. 800 reads compared between the L4 and the A10G and 800 between the L4 and the RTX 5080: 0 mismatches in any decision field, and a largest score difference of 0.

Video null campaign

Corpus. 4,854 decodable unmarked 5 s clips (H.264, cut to the clip format of the video suite above): 3,041 YouTube UGC (CC BY 4.0; 982 of them were used in earlier tuning, 2,059 were not), 1,618 Wikimedia Commons, 93 Xiph derf, 46 local 1080p, 52 Netflix Open Content (Meridian) and 4 Sintel. 872 whole 20 s UGC videos are a report-only extra stratum. 40 DCI 4K clips (4096x2160) were refused by watermark.video.max_pixels at every read and are left out of the 4,854 (0.71 % of the stratum). Positives: 742 clips marked by the product with a test issuer, blind (VideoSeal 249, keyed VideoSeal 247, keyframe TrustMark 246).

What was measured. The share of unmarked clips flagged by the unkeyed VideoSeal read (presence), by keyed VideoSeal (tag accepted) and by the keyframe TrustMark reader (tag accepted), and the share of blind positives read back with the issuer's exact payload.

measure result Wilson 95 % upper sealed line met
VideoSeal, unkeyed: presence on unmarked clips 0 / 4,854 0.079 % Wilson upper at most 0.15 % yes
VideoSeal, keyed: tag accepted on unmarked clips 0 / 4,854 0.079 % Wilson upper at most 0.15 % yes
Keyframe TrustMark: tag accepted on unmarked clips 0 / 4,854 0.079 % Wilson upper at most 0.15 % yes
Sensitivity, unedited, VideoSeal unkeyed 169 / 174 = 97.1 % (93.5 to 98.8 %) at least 97 % rate met; the stated count of 170 of 174 was missed by one
Sensitivity, unedited, VideoSeal keyed 172 / 174 = 98.9 % (95.9 to 99.7 %) at least 97 % yes
Sensitivity, unedited, keyframe TrustMark 167 / 173 = 96.5 % (92.6 to 98.4 %) at least 97 % no: the stated count was 170 of 173 (168 would meet the 97 % rate)
Wrong payloads, cross-issuer labels (742 positives) 0, 0 0, 0 yes
  • The keyframe sensitivity is below its line. Over the marked positives only (the clips that were actually watermarked) the keyframe reader reads 167 of 171. The misses sit at roughly a tenth of the bit rate of the hits. The keyframe reader ran on PyTorch in this campaign, not on the ONNX reader benchmarked under "Keyframe reader on ONNX Runtime" above.
  • After H.264 crf 23 (report only). VideoSeal unkeyed 70 of 75, keyed 69 of 73, keyframe 70 of 73.
  • Detect time. p50 7 to 12 s for a 5 s clip at 1080p or below and about 32 s at 2160p for VideoSeal (16 s for the keyframe reader); the slowest read took 74 s. The reads are CPU-bound (median GPU utilisation 0 %), so the L4, A10G and L40S machines gave the same throughput.
  • Same decisions on a second GPU type and a desktop GPU. 556 reads compared between the L4 and the A10G and 556 between the L4 and the RTX 5080: 0 mismatches in any decision field; the largest score difference was 4.1e-7.

Audio null campaign

What the audio payload is built for. The keyed audio payload and its decoder are built for speech and conversations of 8 s and more. Music and clips shorter than 8 s are known limits outside that scope. They are stated here, with the numbers, so that customers can plan around them. Two product behaviours follow from the measurements below: a keyed deployment hides the streaming detector's unconfirmed presence, and an unkeyed deployment shows presence only and never names an issuer.

Corpus. Four strata of 5,000 unmarked units each: single utterances (3,177 LibriSpeech, 1,823 Multilingual LibriSpeech in seven languages; CC BY 4.0), 30 s joins and 120 s joins of consecutive LibriSpeech utterances of one chapter, and 30 s music clips (4,979 decodable, Free Music Archive tracks licensed CC0, CC BY or public domain, at most 19 per artist). Add-on strata, added after the preregistration and reported only: 5,000 FSD50K sound clips (CC0 or CC BY) and 2,000 Kokoro synthesised utterances. Positives: 1,200 clips marked with a test issuer, blind (600 unkeyed; 600 keyed, 300 through the buffered marker and 300 through the stream marker).

What was measured. Two readers per unit. The keyed read is the shipped keyed audioseal-p70-r1 read: public presence, and a payload accepted only when its tag verifies. The unkeyed read is the 16-bit presence read at the hardened 0.5 setting. Sensitivity is the share of blind positives read back with the issuer's exact payload.

Keyed deployments

Public presence is shown here after the streaming-card rule (Grading, "The streaming card in a keyed P70 deployment"), replayed over the stored records.

measure result Wilson 95 % upper sealed line met
Tag accepted on unmarked audio, each stratum 0 / 5,000 (music 0 / 4,979) 0.077 % Wilson upper at most 0.15 % yes
Tag accepted, all unmarked keyed reads 0 / 26,979 report only -
Public presence, single utterances / 30 s / 120 s 0 / 5,000 each 0.08 % at most 0.5 % / 0.5 % / 1 % yes
Public presence, music 2 / 4,979 0.15 % at most 0.5 % yes
Public presence, sounds (add-on, report only) 15 / 5,000 0.49 % none (0.5 % shown for reference) -
Positives that lose public presence under the rule 0 of 1,400 -
Sensitivity, stream marker 197 / 210 = 94 % (89.7 to 96.3 %) at least 204 of 210 (97 %) no
Sensitivity, buffered marker 178 / 210 = 85 % (79.3 to 89.0 %) at least 204 of 210 (97 %) no
Wrong payloads shown, cross-issuer labels shown 0, 0 (1 of 600 counted strictly, see below) 0, 0 no (strictly 1 of 600); 0 shown, the 1 internal pick was rejected by the tag
  • Why the rule exists. Before it, keyed music showed public presence on 361 of 4,979 clips (7.25 %, Wilson upper 8.00 %), 360 of them from the streaming detector, which does not read the keyed marks; the same card gave all 25 public false presences on read speech and 177 of the 192 on sounds. In a keyed deployment that card can only confirm someone else's mark, so its detection alone is no longer public presence. Replayed over the saved reads: music 2 of 4,979, read speech 0, sounds 15 of 5,000, and none of the 1,400 positives lost its public presence. This is an in-sample rule applied to stored records (see above), not a new run.
  • Sensitivity is below its line, and where the misses are. All 13 stream misses are clips shorter than 8 s (1.95 to 7.04 s). The keyed payload needs 8 whole one-second segments, 8.0 s of audio; no payload was read from any clip shorter than 8 s (0 of 167 speech positives), while all 34 positives of 8 to 10 s and all 469 of 10 s or more were read (stream positives of 9 s or more: 196 of 196). The buffered misses are the same short clips in speech (speech 133 of 140 = 95 %, 132 of 132 at 9 s or more) and music, where 45 of 70 unedited keyed clips (64 %) were read: 33 of the 100 keyed music positives were present but the default read chose no payload; 2 were under 8 s; 1 chose a keyless wrong payload. Keyed music payloads are a known limit.
  • Wrong payloads. None shown. Counted strictly, as the sealed line counts them, there is one: on a music positive (1 of 600) the keyless step picked a payload naming another issuer. Its tag failed under all six live keys. At the time of the run (Round M, before the verified-tag rule) the full tier showed it as an unverified "claimed issuer"; since that rule it is shown on no tier, and the public tier showed presence only throughout.
  • Deep scanning was measured and rejected. payload.hardened_read: auto (an optional second stage for keyed music) recovered 0 of the 33 music positives it was added for, chose 26 payloads on 4,979 unmarked music clips where the line allowed none (none verified a tag), and chose a wrong issuer on 3 positives. It stays off.
  • Detect time. p50 0.94 s (single utterances) to 2.28 s (120 s joins) per unit on the L4; the read is GPU-bound (mean GPU utilisation 92 %).

Unkeyed deployments

Unkeyed audio carries a 16-bit word with no error detection, so one flipped bit names another issuer. In the shipped product unkeyed audio is shown as presence only, with no issuer named. The rows below are the public presence of the unkeyed read, which the streaming-card rule does not change.

stratum result Wilson 95 % upper sealed line met
Single utterances 9 / 5,000 (0.18 %) 0.34 % at most 0.5 % yes
30 s joins 4 / 5,000 (0.08 %) 0.21 % at most 0.5 % yes
120 s joins 12 / 5,000 (0.24 %) 0.42 % at most 1 % yes
Music 361 / 4,979 (7.25 %) 8.00 % at most 0.5 % no
Sounds (add-on, report only) 191 / 5,000 (3.82 %) none -
Synthesised speech (add-on, report only) 2 / 2,000 (0.10 %) none -
Sensitivity, issuer named correctly 393 / 420 = 93.6 % (90.8 to 95.5 %) at least 408 of 420 no
Wrong issuer named 42 of 600 0 no
  • Unkeyed music is a known limit. 360 of the 361 hits fire on the streaming detector. An unkeyed deployment still shows that detection because the unkeyed stream mark is the one that card reads; the rule above fixes keyed deployments only. Plan for unkeyed music to show presence on about 7 % of unmarked clips. A keyed deployment does not have this limit (2 of 4,979 after the rule).
  • Unkeyed identity. All 600 unkeyed positives were detected as present. The 42 reads that named another issuer are 16-bit words with one flipped bit, 40 of them on the 100 music positives. Presence only is shown for unkeyed audio now, so no issuer is named.
  • Fallback paths. On music the fallback read paths add 293 of 4,979 clips to the full tier's presence beyond the public tier's; the full tier reports them with a caveat that says so.
  • Detect time. p50 0.88 s (single utterances) to 1.78 s (120 s joins) per unit on the L4.

Equality and below-line results

  • Same decisions on a second GPU type and a desktop GPU. 400 reads compared between the L4 and the A10G: 0 mismatches. Between the L4 and the RTX 5080 every decision field is identical; a diagnostic field (a note that a fine read was removed) differs on 13 of the units, which is reported as a deviation and decides nothing.
  • Results below their sealed line, in one place. Keyed sensitivity, stream 94 % and buffered 85 % against 97 % (shortfall: clips under 8 s, and music at 64 %); no payload under 8 s; unkeyed music public presence 7.25 % against 0.5 %; unkeyed sensitivity 93.6 % and 42 of 600 wrong issuers, now not shown; keyed wrong payloads and cross-issuer labels, strictly 1 of 600 against a line of 0; keyed pooled identity 375 of 420. Keyed music public presence, which was also 7.25 % before the rule, is 2 of 4,979 (0.04 %) after it, with no positive lost; unkeyed music is unchanged.

Text null campaign

Corpus. Three strata of 5,000 human documents each, by length in tokens: 200 to 512, exactly 1,024 and exactly 8,192. They are drawn from C4 (ODC-BY), Wikipedia (CC BY-SA, read for measurement only), PG-19 (Apache-2.0 dataset of pre-1919 public-domain books), arXiv abstracts (CC0), and mC4 in German, Spanish, French, Japanese and Chinese (ODC-BY). A fourth stratum of 5,000 machine-written texts (MAGE 4,215, UltraChat 785) is report only. 20,800 units, each read by the SynthID reader and the KGW reader (41,600 reads, none unreadable). Positives: 800 texts marked by the product with a test issuer, blind, 400 per scheme (280 unedited, 120 after deleting 10 % of the words).

What was measured, and which gate. The numbers below are the re-score at the gate that ships: grade B at p < 1e-5, presence (grade D) at p < 1e-3, and KGW scored on the exact binomial tail (Grading, "Per-signal grades"; Configuration, text.grade_b_p_value). The gate was set after an earlier run at the previous gate (B at p < 1e-3, normal tail) had failed several of its lines; only the shipped gate is reported here. The same 20,800 units were re-read.

scheme length presence, k of 5,000 (Wilson upper) grade B, k of 5,000 (Wilson upper) sealed lines met
KGW 200 to 512 tokens 8 (0.32 %) 0 (0.08 %) presence at most 0.5 %, B at most 0.2 % yes
KGW 1,024 tokens 13 (0.44 %) 0 (0.08 %) same yes
KGW 8,192 tokens 6 (0.26 %) 0 (0.08 %) same yes
SynthID 200 to 512 tokens 4 (0.21 %) 0 (0.08 %) same yes
SynthID 1,024 tokens 3 (0.18 %) 0 (0.08 %) same yes
SynthID 8,192 tokens 7 (0.29 %) 0 (0.08 %) same yes
KGW and SynthID machine-written text (report only) 5 and 5 (0.23 %) 0 and 0 none -
  • Pooled. Grade B on 0 of 20,000 unmarked documents per scheme (40,000 reads; Wilson upper 0.02 % per scheme). Presence: KGW 32 of 20,000 (0.16 %), SynthID 19 of 20,000 (0.10 %); the 5,000 machine-written documents are included.
  • Sensitivity. SynthID: unedited 280 of 280 present and grade B; after deleting 10 % of the words 119 of 120 present (the miss is a 19-token text), 112 at grade B. KGW: unedited 280 of 280 present, 260 at grade B (the other 20 are grade D because too few distinct n-grams were scored); 10 % deletion 120 of 120 present, 100 at grade B. All meet the 97 % line (at least 272 of 280). Text carries no payload, so there are no wrong payloads or cross-issuer labels.
  • Detect time. p50 for SynthID 8.7 ms (200 tokens) to 266 ms (8,192 tokens); for KGW 125 ms to 1,086 ms, on a CPU. Text ran on a CPU only, so there is no second-GPU comparison.

Caveats that travel with these numbers.

  • The gate was chosen on these same documents. This is an in-sample result, not a held-out one.
  • KGW's false-positive rate depends on the register of the text: arXiv abstracts score about 2.6 times the rate of the other sources. The margin on the 1,024-token KGW cell (13 of 5,000, upper 0.44 % against a 0.5 % line) is thin.
  • Text grade B is statistical: it is a p-value below 1e-5, not a registry-confirmed match, and it must not be read as equivalent to the grade B of an image or video, which is confirmed against a registered asset by fingerprint.
  • The gate costs length. B at p < 1e-5 needs about 1.9 times the tokens of B at p < 1e-3 for the same per-token signal, so short or heavily edited marked text becomes grade D sooner. That is not measured on paraphrase or translation.

Keyed audio payload

The keyed audioseal-p70-r1 payload (Payload keys, "Audio") was measured on the product path, end to end. Each clip is marked with Marker.mark_audio in keyed mode and written by the product writer. It then goes through the transform and is read by Marker.detect. All runs use alpha 1.41, the c10 cell layout, 10.0 s excerpts unless named, 4 live audio keys and 2 own issuers.

Verified-exact means that detect chose a payload, its tag verified under a live key, and it equals the embedded payload. Intervals are Wilson 95 %. Every line was pre-registered before any data.

F1 is 200 LibriSpeech clips of 13 s or more, not used by any earlier run: 35 test-clean, 15 test-other and 150 dev-clean. Of these, 89 are fully fresh; the other 111 had been used only as unmarked nulls.

The CPU standard: the default read on CPU (A1)

The default read (hardened_read: off, CPU) reads the default envelope: an excerpt that starts on a 20 ms boundary of the marked file, and the whole file. Every row read 200 of 200 (100 %, 98.1 to 100 %) in both envelopes, except Opus at 12 kbit/s.

The rows that read 200 of 200:

  • Opus 24 and 48;
  • MP3 64 and 128;
  • AAC 64 and 128 (MP4);
  • G.711 mu-law and A-law;
  • raw AAC (ADTS) as Laidline writes it, at 16, 44.1 and 48 kHz;
  • Laidline's own mp3, m4a, opus, flac and wav output, decoded by the product;
  • identity.

A1-rate is post-stratified with fixed weights: test-clean 1/2, test-other 1/4, dev-clean 1/4. Its interval combines the per-stratum Wilson bounds, which is conservative. The line is PASS at 95.0 % or better.

row envelope rate interval test-clean test-other dev-clean line
Opus 12 default, 10 s 97.3 % 85.5 to 99.2 % 35/35 14/15 144/150 PASS
Opus 12 whole file 97.0 % 84.7 to 99.2 % 33/35 15/15 149/150 PASS
Opus 12 default, 12 s 99.2 % 88.1 to 99.6 % 35/35 15/15 145/150 (reported)
every other row both 100 % 89.3 to 100 % 35/35 15/15 150/150 PASS

The 89 fresh clips (reported, not the measure). A1-rate is measured on the full 200, a decision taken after the data. On the 89 fresh clips alone:

row rate interval strata result
Opus 12, default 98.6 % 74.7 to 99.5 % 12/12, 6/6, 67/71 PASS
Opus 12, whole file 91.3 % 66.0 to 97.6 % 10/12, 6/6, 70/71 UNPROVEN
every other row 100 % 76.8 to 100 % PASS

At this size (12 test-clean clips carrying half the weight) one clip moves the post-stratified rate by about 4 points. The interval is wide, and the check has little power.

A1-eq (product equals bench; binding). The bench reader read every product file that Marker.detect read.

  • The line: the net loss is at most 4 per row and no payload is wrong.
  • Result: net loss 0 on 37 of the 38 rows, and -1 on Opus 12 default (the product read 193, the bench 192).
  • PASS.

W0. 0 wrong payloads in every product and bench read of the build, including A1's 7,600 product reads.

P5-prod. No call lost to the arbiter (the truth decoded, the call not verified): 0 of 11,200 spans. PASS.

Edited audio under the fast read and the hardened decode (A2-presence, report only)

These are 10.0 s excerpts that start at any sample. They are read with hardened_read: off, under decode: fast (what auto resolves to on a CPU-only host) and under decode: hardened. Each cell gives presence on the full tier and on the public tier, out of 200.

row decode: fast, full = public decode: hardened, full decode: hardened, public
Opus 12 31 (11.1 to 21.2 %) 182 (86.2 to 94.2 %) 34 (12.4 to 22.8 %)
Opus 24 124 199 125
Opus 48 144 200 144
MP3 64 165 200 165
MP3 128 166 200 167
AAC 64 163 200 164
AAC 128 164 200 165
G.711 mu-law 161 200 163
G.711 A-law 164 200 165
identity 164 (76.1 to 86.7 %) 200 165

decode: hardened ran its statistics on the GPU. A 20-clip CPU subsample (200 verdicts) differed in 1: Opus 12 on clip 3570-5694-0003.

The GPU standard: deep scanning on a GPU (B1)

B1 reads with hardened_read: on on a GPU. Its excerpts start at any sample; the trims and offsets apply to the whole file. The line: PASS at 190 of 200 or more, KILLED at 183 or fewer.

rows verified-exact interval line
Opus 24 and 48; MP3 64 and 128; AAC 64 and 128; G.711 mu-law and A-law 200/200 each 98.1 to 100 % PASS
real containers: raw AAC as Laidline writes it, raw AAC at 44.1 and 48 kHz, MP3 64 untagged 200/200 each 98.1 to 100 % PASS
identity 200/200 98.1 to 100 % PASS
trims (10 ms, 1 s) and offsets (0.5 s of silence, 3.0 s of unmarked speech prepended) 200/200 each 98.1 to 100 % PASS
Opus 12 (best effort, no line) 188/200 = 94.0 % 89.8 to 96.5 % reported

Composed rows. A1's default-envelope excerpts were read with deep scanning on. The line: no more than 2 per 100 below A1. The result was no loss:

row deep scanning on A1 (deep scanning off)
identity 200 200
Opus 12 196 193
Opus 24 200 200
MP3 64 200 200
AAC 64 200 200

Nulls, foreign marks and the public body

N1, own-accepts on unmarked audio: 0 everywhere. PASS.

  • Forced reads of the earlier null sets (1,591 clips; stage 1 and stage 2 on both detectors, whatever the trigger): 177,520 words, 307 BCH-valid, 0 eligible, 0 tag checks. The product path as configured showed 3 of those clips as present, and stage 2 ran on 113 of them.
  • Decoder-level nulls on the product's Chase-II decoder and P70 rule:
    • block null: 100,000 draws, 167 BCH-valid;
    • constant messages: 65,536, 9,312 BCH-valid;
    • Gaussian words: 1,000,000, 8,450 BCH-valid. None of them gave an eligible top or a tag check.
  • Foreign marks (40 clips, keyed under 4 foreign keys):
    • with the own issuer: 40 tag failures, 0 accepts;
    • with another issuer: never checked, and labelled third-party on 40 of 40 (91.2 to 100 %).

TP-null: PASS. 0 third-party labels on N1's 1,591 null clips and on G1-null's 88 (102 single-word foreign tops).

TP-own: FAIL under the measured rule at 31 calls, and FAIL under the three-grid rule (chosen after the data) when measured at 3,000 calls. The rule now shipped adds a score floor (chosen after the 3,000-call data) and passed a fresh 3,000 with 0 labels.

  • Under the two-grid rule that ran, one own mark was labelled as another deployment's: B1, Opus 12, clip 5536-43359-0007, under deep scanning. That was 1 of the 31 own-mark calls in which no own payload was chosen (27 on the A1 and B1 rows, 4 on B1's composed rows).
  • Under the three-grid rule, the measured word lists give 0 of 31, and the 40 foreign marks stay labelled.
  • The line allows at most 1 per 1,000 such calls and no row with more than 1 clip, so the line had no power at 31 calls.
  • The powered measurement (rules written before the data): 3,000 of Laidline's own marks (train-clean-100 utterances of 13 to 17 s) keyed under foreign keys, every word of both stages kept, each read scored as if no own payload had been chosen. The three-grid rule labelled 13 of 3,000 (4.3 per 1,000; exact 95 % interval 2.3 to 7.4). The line stated before the data (7 or more labels fail, 0 passes) failed. The two-grid rule labelled 80 and R6' (two grids and two word positions) 21. The own payload was chosen on all 3,000 calls, so the product showed none of the 13 labels; the failure applies to own-mark calls in which no own payload is chosen. No line moved.
  • The hold-out of the score floor (rules and a pre-registration record committed before any unit was read): see "Third-party rule S97 and its hold-out" below. The floor rule labelled 0 of 3,000 fresh own marks (rate shown below 1 per 1,000).

The cause and the candidate rules are below ("Third-party label on an own mark").

P-pub: PASS. All 17,871 public audio watermark bodies (A1, A2, B1 with its composed rows, N1 and the foreign marks) were one of the allowed bodies, with no forbidden field.

D1: the 16-bit claimed issuer.

  • (a) 0 by construction: keyed mode never shows the 16-bit issuer (0 issuers shown in 4,000 calls).
  • (b) The cost of (c): every issuer-exact 16-bit read loses its claimed issuer, 100 per 100 (the saved tables of the earlier 16-bit runs, shipped decode):
    • M-acc: 3,192 of 3,192 issuer-exact reads (3,597 detections of 3,700);
    • HZb-30: 27 of 27;
    • HZb-120: 24 of 24.

Streams (S-1)

The stream path is the keyed stream session (arm L2000-R200-H500, the tail flushed). It was compared with the file path, mark_audio, on the same 100 T1 clips. The rows are the default-envelope excerpts through each codec, plus the streamed MP3 (with the pad) and the streamed Opus. The line: verified-exact within 2 per 100 of the file path.

row stream file line
Opus 12 97 94 FAILED by the letter; accepted after the data
Opus 24 99 100 PASS
every other codec row, identity 100 100 PASS
streamed mp3 / file mp3, excerpt and whole 100 100 PASS
streamed opus / file opus, excerpt and whole 100 100 PASS

The Opus 12 row fails the line's letter: the gap is 3 against the allowed 2, with the stream path reading more, not fewer. The two paths carry independent marks of the same clip (each has its own random cycle offset and asset payload), so the row compares two rates on 100 clips. A gap of 3 is within sampling variation at this n: the intervals overlap (91.5 to 99.0 % against 87.5 to 97.2 %). The row was accepted after the data because the stream path read more.

Timings

T1: stage 1 on CPU (2 threads on 2 reserved cores; the line is 0.1 x duration + 1.5 s). There were two runs:

  • Run 2 (the quiet re-time). The reserved cores were 3 % busy before, at load 1.1 to 2.1. These are the measured figures.
  • Run 1 (kept in the record). It ran under load 2.5 to 5.9, with the cores 6 % busy before.
length 3-grid (default) run 2 5-grid run 2 3-grid run 1 5-grid run 1 line
12 s 2.20 s, MET 3.18 s, MISSED 1.98 s, MET 3.61 s, MISSED 2.7 s
30 s 2.71 s, MET 3.61 s, MET 5.12 s, MISSED 6.25 s, MISSED 4.5 s
60 s 4.08 s, MET 4.98 s, MET 6.86 s, MET 8.74 s, MISSED 7.5 s
120 s 6.94 s, MET 7.84 s, MET 10.82 s, MET 8.35 s, MET 13.5 s

In the quiet run the default 3-grid reader met every line; run 1's 30 s miss was load. The 5-grid reader is not the default; it missed at 12 s and met the other lengths (report only).

T2: deep scanning on the GPU.

  • Stage 2 per call at 12 s: p50 1.04 s, p95 1.14 s.
  • The trigger (the hardened statistics) on unmarked clips: p50 0.44 s at 12 s, 0.63 s at 30 s and 1.70 s at 120 s.
  • Stage 2 started on 113 of 1,591 unmarked calls (N1, the product path).

T3: hit against miss for the audio layout (report only). The measured timing channel stays within the bound stated in Payload keys residual 10 on every backend measured; one file-registry cell is unexplained and indicative. The mitigations are the equal-work construction (Payload keys, "Equal work and its residuals") and the registry recommendation: the file registry is for development; production uses Postgres or S3.

Third-party label on an own mark (TP-own analysis)

The call. B1, Opus 12 kbit/s, clip 5536-43359-0007, under deep scanning. Re-read on CPU, it reproduced the label.

  • The detection. The 16-bit read did not detect (fast-read probability 0.18). Presence came from the hardened detection's phase path, which started stage 2.
  • The own payload. It decoded on no word: there was no own-issuer top, no eligible word, and the truth was not among the tops.
  • The foreign head. The only two BCH-valid words decoded the same version-2 payload, of a foreign issuer (269a0ee4f99e95e8a2, issuer 27040):
    • word positions: detector 0, phase 314, rotation 5, word 9;
    • grids: 20 and 30;
    • scores: 0.918 and 0.912;
    • each word had a single Chase candidate (margin 1.0).
  • Why two grids were not two readings. Grids 20 and 30 are 10 frames (200 ms) apart. Their 1.0 s segments overlap by 80 %, so the two "different grids" read largely the same audio. One wrong decode reappeared on the neighbouring grid and met the two-grid rule.

The same mechanism on two more own marks. N1's foreign-key marks that carry the own issuer (clips 32-21625-0006 and 328-129766-0096) also showed a third-party head in the forced read. The product did not label them, because their own head was chosen. Re-read on CPU with every word kept, both reproduce the head:

  • 328-129766-0096: one word (stage 2, detector 0, phase 15, rotation 6, word 11) read on grids 10 and 20.
  • 32-21625-0006: one word (detector 0, phase 0, rotation 7, word 11) read by stage 1 on grids 0 and 17, and by stage 2's phase-0 read on grid 0. That is the same word of the same audio, counted from two stages.

Candidates. Every candidate narrows R0, so the null sets give 0 under all of them: N1's 1,591 clips, G1-null's 88 and the decoder-level nulls. The own marks and the 40 other-issuer foreign marks were re-read on CPU with every word kept.

candidate B1 call N1f 328-129766-0096 N1f 32-21625-0006 foreign marks still labelled (of 40)
R0, shipped: a foreign v2 head tops words on 2 different grids labelled head head 40
R1 (Chase margin), R2a/R2b (own presence, own top), R3 (no eligible own top) labelled not scored not scored 40
R4: the head on 3 or more grids not labelled no head no head 40
R5: 2 grids at least 15 frames apart not labelled no head head 40
R6 as scored: 2 grids and 2 word positions, the stage counted in the position not labelled no head head 40
R6': 2 words differing in grid and in word position, the stage not counted not labelled no head no head 37
R7: 2 words from different reads (detector or phase) not labelled no head no head 0

R6 as proposed fails on 32-21625-0006: stage 1 and stage 2 read the same phase-0 word. The candidate demanding evidence from different audio reads (R7) removes every genuine foreign label too, because a genuine foreign mark's words come from one read: detector 0 at phase 0, on several grids. R4 is the only candidate here that removes all three own-mark heads and keeps all 40 genuine labels. Three grids still overlap in audio, so its independence is measured, not shown. The line had no power at 31 calls: a rate of 1 per 1,000 cannot be shown at that size.

Decision after the data: R4 ships. A third-party head needs three or more grids. R6 is withdrawn, refuted on 32-21625-0006, and R7 is unusable. Regression tests pin the rule on the three own-mark word lists (no head) and the 40 foreign marks (a head). Under R4 the lines read: TP-own 0 of 31 (no power), TP-null 0, and the foreign other-issuer label 40 of 40. At 3,000 calls R4 failed the TP-own line: 13 labels ("TP-own" above).

Third-party rule S97 and its hold-out

Why a floor. Each of the 13 labels of the 3,000-call run was one wrong word of an own mark (a wrong-rotation decode) read again on overlapping grids, and every such word scored at most 0.9445 (the score is 1 minus the weighted share of bits the Chase decoder flipped). Every genuine head of the fit data had a third grid at 0.9788 or more: the 3,000 own payloads scored as if foreign and the 40 foreign marks. S97 (the third-party rule's score floor) counts a grid only for words scoring 0.97 or more. On the 3,000 of that run it labels 0 and keeps 3,000 of 3,000 own heads and 40 of 40 foreign marks. The floor was chosen after that data (S96 and S95 fit equally; S98 already loses one own head), so it was tested once on a fresh set with the lines, the ladder (S97, then S96, then S95, only on a kept-line FAIL) and the predictions written down first.

The hold-out. 3,000 fresh own marks (train-clean-100, 13 to 17 s, the earlier 3,000 and the P40 units excluded, seed [20261014, 40]) with the earlier run's construction (own issuer, foreign key, every word of both stages kept, each read scored as if no own payload were chosen), and the next 150 clips marked by another issuer under a foreign key (4 of the 9 other bench issuers by the runner's index). run in 29 GPU slices, resumed from the per-unit records after a machine crash; 0 errors, 0 own-accepts, 0 wrong chosen payloads; the shipped third_party agreed with the round's replay from the recorded words (summarise_holdout) on every unit, and a separate replay agrees. Precision: every unit was embedded and read in full float32 on the GPU (TF32 off for matmuls and cuDNN, cuDNN deterministic: the bench's strict setting, applied before the product's models load). Full float32 with deterministic cuDNN is the shipped CUDA precision (watermark.cuda_precision: strict, the default), so these rates are at the shipped precision; under torch's default TF32 near-threshold decisions can differ.

line (stated before the data) statistic result verdict
L-own S97 labels on 3,000 own marks, as if no own payload chosen (PASS 0, FAIL 7 or more) 0 of 3,000 (one-sided 95 % upper bound 1.0 per 1,000) PASS
L-kept-own own heads not kept under S97, scored as if foreign (PASS 0, FAIL 7 or more) 1 of 3,000 (2,999 kept, at least 99.84 %) INCONCLUSIVE
L-kept-foreign clean foreign marks the product leaves unlabelled (PASS 0, FAIL 1 or more) 0 of 150 (at least 98.0 %) PASS

Beside it on the same 3,000 reads: S96 0, S95 0, the three-grid rule without a floor 14 (its own line would fail again), the two-grid rule 76, R6' 18. Own heads kept under S96 and S95: 3,000 of 3,000; the one S97 miss had grid scores 0.9788, 0.9723, 0.9684 and 0.9665. The ladder descends only on a kept-line FAIL, so S97 is adopted; the public caveat stays on an L-own PASS. The reader's phantom-word rate, a property of any rotation-searched block code read on overlapping grids: 1,958 foreign version-2 words in 3,000 own-mark calls (0.65 per call; 1,985 in the 3,000-call run).

Opus 12 kbit/s, reported as measured (no line):

row measured Wilson 95 %
150 foreign marks, labelled by the product (S97) 129 of 150 (86.0 %); presence shown on all 150, the other 21 presence only 79.5 to 90.7 %
same, forced-read words: S97 / S96 / S95 / three grids / two grids 131 / 133 / 133 / 134 / 147 of 150
3,000 own marks: own payload chosen 2,982 of 3,000 (99.4 %) 99.1 to 99.6 %
same: labels among the 18 calls without it, every rule 0 of 18 0 to 17.6 %
same, scored as if no own payload chosen: S97 / three grids / two grids 0 / 1 / 10 of 3,000
same, own heads scored as if foreign and kept: S97 / S96 / S95 / three grids 2,489 / 2,592 / 2,623 / 2,628 of 3,000 S97 81.6 to 84.3 %

Under Opus 12 the floor costs labels on genuine marks (a degraded word scores lower); a missed label is a weaker true statement: with the 16-bit detection fired, the full tier shows "a mark is present; its issuer could not be read", and the public tier shows presence only when that verdict came on a phase-0 path (on a fallback path, the absent body); without it, a head under the floor shows nothing.

Predictions written before the data, against the outcome: S97 about 0.02 labels expected, 0 seen; S96 0.18, 0; S95 0.5, 0; the three-grid rule about 13, 14; own heads missed 0 to 1, 1; clean foreign marks 150 of 150, 150 of 150. Regression tests pin the floor, the absent verdict below it, the 13 phantom heads of the 3,000-call run (no label) and the equal-work property.

Disclosure. Under decode: hardened the span read's words (the payload read inside long clips) join stage 1's read and so reach this rule, S97's floor included. This hold-out ran without the span read (its intact 13 to 17 s clips would not trigger it), so the figures above hold for that construction; the new exposure is a call where the span read triggers. Measured there, not as a pre-registered line: 2,205 three-grid span reads on 800 own- mark units of the two fresh long-clip rounds below gave 408 reads with a BCH-valid wrong top, the highest scoring 0.9638 and none at or over 0.97; the read forced on 1,887 unmarked clips gave 0 third-party heads.

Keyed audio payload inside long clips

The whole-clip payload read needs 8.0 s delivered on the 20 ms grid and the docs claim 10 s ("Voice calls" above); the span read below is what reads a mark inside a longer recording.

The payload of keyed P70 marks (alpha 1.41) spliced into longer unmarked recordings, read under decode: hardened with the span read (Grading, "The cell gate for keyed P70 audio"). Units: the earlier long-clip run's donors, marks, codecs and hosts at fresh seeded offsets (a fresh phase per unit; the speakers repeat). Verified = the issuer's payload chosen and its tag verified. The lines were stated before data and are the earlier long-clip run's, unchanged: 90 / 79 (PASS / KILL) for 12 s in 120 s, also through Opus 24 kbit/s, 95 / 84 for 24 s in 120 s. GPU (strict float32), 100 units a row.

Round 1: PR-A+ (the coarse pass's best phase, then the searched phase). Seed 2026101447. The predictions were restated before any fresh unit was read, from a GPU re-read of the design's 177 units (the CPU replay had predicted 93 for Opus 24).

row PR-A+ verdict prediction PR-A PR-C3 PR-A2 (both peaks) the earlier span rule, same units
12 s in 120 s 97 PASS 94 96 95 99 63
Opus 24, 12 s in 120 s 86 UNPROVEN 87 79 85 93 62
24 s in 120 s 93 UNPROVEN 95 93 96 98 69
9 s in 30 s (reported) 92 report 97 90 94 95 67

PR-A+ stays as measured, UNPROVEN. The read forced on 1,887 unmarked clips (300 30 s and 240 120 s test-corpus joins, 500 short utterances, 200 30 s and 100 120 s train-clean-100 joins, 547 clips of LJ Speech, VCTK and solo piano): 0 own-accepts, 0 eligible tops, 0 tag checks. The trigger fired on 1 of 840 unmarked joins, unit for unit as the earlier long-clip run measured. On 88 clips the hardened decode had flagged (G1-null), the decode result was identical with the read on, and the read gave 0 eligible tops.

Round 2: the span read (the two coarse peaks, then the searched phase, stopping at the first choice). Adopted after round 1. A second fresh draw (seed 2026101448; none of round 1's units), the span read adopted as the candidate before any of its units was read (the both-peaks form PR-A2 reported from the same reads). The predictions are round 1's measured PR-A2 counts.

row the span read verdict prediction PR-A2 PR-A+ the earlier span rule, same units
12 s in 120 s 98 PASS 99 (96 to 100) 98 96 67
Opus 24, 12 s in 120 s 93 PASS 93 (88 to 96) 93 91 72
24 s in 120 s 98 PASS 98 (94 to 99) 98 96 64
9 s in 30 s (reported) 98 report 95 (90 to 98) 98 98 75

P0 (never below the earlier span rule on a unit): holds on every row. 0 wrong payloads, 0 own-accepts. The span read took one read on 94, 86, 94 and 96 units of the four rows (a hit at the first peak), three times on at most 5. On the unmarked join where the trigger fired it read three phases and gave 0 eligible tops.

CPU cost (2 threads on 2 reserved cores, 7 paired runs, p50; the keyed hardened detect with the span read):

clip added over the earlier span rule's read the span read whole detect line
120 s, a hit at the first peak 0.02 s 1.36 s 67.5 s at most 0.1 s (KILL over 1.0 s): PASS
120 s unmarked join, the read forced (three reads) 2.86 s 4.24 s 71.7 s report
30 s unmarked join, the read forced 2.66 s 4.14 s 44.2 s report

On a hit the span read costs what the earlier span rule's one read cost (1.34 s); a miss costs about three such reads. The hardened decode itself is 55 to 56 s of the 120 s calls (GPU recommended); the trigger fires on 1 of 840 unmarked joins, so the miss path is a rare cost. Round 2: PASSED on every line.

Keyed audio fingerprint confirmation

Grade B by the registered asset's fingerprint record (Payload keys, "Audio"; Grading), measured on the product path: Marker.mark_audio in keyed mode, the transform on a 10 s cut, Marker.detect reading every slot's 15,469-byte record. Lines pre-registered, with the N-fp hold-out set. Intervals are Wilson 95 %.

line result
Decoy records never confirm 1,000 synthetic 20 s queries and 10 matcher fixtures against the 5 decoy records: 0 of 5,050 confirmed; lowest statistic 0.458 (tau 0.33)
F-1: B by fingerprint on F1's 10 s cuts (200 clips per row) 200/200 on opus_24, opus_48, G.711 mu-law and A-law, MP3 64 and 128, AAC 64 and 128, resample 8 kHz and identity; opus_12 194/200 (0.936 to 0.986), the 6 misses being payload reads (194/194 confirmed when the payload was chosen); pooled codec rows 1,994/2,000 (0.9935 to 0.9986); scope whole on every confirmed cut
N-fp (a): unmarked hold-outs, forced against every F1 record 79 hold-outs sharing no source audio with F1: 15,800 comparisons, 12,000 scored on 60 hold-outs (18 short ones too short, 1 low content), 0 confirmed; lowest statistic 0.432
N-fp, true-copy control (report only) 9 hold-outs that are joins containing an F1 clip's utterance: confirmed on the 4 whose shared audio lies inside the searched 20 s (4.5 to 13.4 s of it; statistic 0.031 to 0.050), not on the 5 with 0 to 0.1 s inside
N-fp (b): 1,000 forgeries (a registered payload copied onto unrelated audio) payload read and tag verified on all 1,000; grade C on all 1,000, 0 B; forced against the named record 0 confirmed, lowest 0.446
T-fp: CPU of the confirmation per call (query fingerprint, 4 record reads, 4 comparisons) p50 0.467 s at 10 s, 0.471 at 12 s, 0.514 at 30 s, 0.518 at 60 s, 0.505 at 120 s: 19, 17, 11, 7 and 4 % of the 0.1 d + 1.5 s detect budget against a 25 % line (2 threads, reserved cores; indicative: the machine was at load 15 to 29 during the run)
T3: hit against miss, record read included within the bound stated in Payload keys residual 10 on every backend measured; no cell blocks (reported, indicative). Mitigations: the equal-work construction and the registry recommendation (the file registry is for development; production uses Postgres or S3)

Video robustness (VideoSeal and keyframe TrustMark, Xiph derf)

What the video watermarks survive. Each clip is marked by the production pipeline (laidline.core.video, libx264 crf 18 preset medium, 32 frames per model call) with each scheme on its own, then goes through each transform and is decoded the way /detect samples it (at most 256 frames: for VideoSeal at a stride that is a multiple of 4, for the keyframe scheme the key frames plus the marking positions). An exact decode is the full payload recovered; a mis-decode is a different, well-formed payload. What the failures mean and how much a decode-side fix could recover is in Robustness baseline.

  • videoseal: VideoSeal 1.0 (vendored at 870ca7f) on every frame, a 92-bit payload (version, issuer id, 48-bit asset sequence, CRC-16) under a BCH code that corrects 16 bit errors. Ran on the RTX 5080 (cuda:0).
  • trustmark_keyframes: the stopgap scheme, TrustMark Q (61-bit image payload) on one forced IDR frame per second. Ran on the CPU (6 workers).
# with the cu130 torch wheels, or in the laidline-cuda container
laidline bench video --corpus DIR --n 20 --device cuda --workers 6

Point --corpus at a directory of your own clips; the published run used 5 s cuts of the Xiph sequences below.

Corpus: 43 clips of 5 s cut from 24 Xiph.org derf test sequences (https://media.xiph.org/video/derf/y4m/; terms vary by sequence, see Third-party licences; the clips are not redistributed): 35 at 352x288 and 8 at 1280x720, encoded libx264 crf 18. The sequences total about 4.4 GiB of y4m. Measured: VideoSeal on the RTX 5080 (torch 2.14.0+cu130, TF32 off, cuDNN deterministic), the keyframe scheme on the CPU of the same machine; machine.run_seconds 3029.9 including the sweeps on all 43 clips; 0 unreadable clips. The first published run had the rows up to social; the rows from rotate_90 on are new. The result ships as laidline/compliance/data/video_robustness.json (with the sweeps and "Where it breaks"), and the marking-method text of the evidence export quotes the configured scheme from it. What the same model costs on the GPU sidecar (720p job wall time, GPU memory) is in "Video marking on the RTX 5080" and the image sizes in "Docker images", both below.

Transforms (transform_definitions in the JSON):

transform definition
identity decode and re-encode, libx264 crf 18 preset medium (the pipeline's own settings)
h264_crf23 libx264 crf 23, preset ultrafast
h264_crf28 libx264 crf 28, preset ultrafast
h264_crf35 libx264 crf 35, preset ultrafast
h265_crf28 libx265 crf 28, preset ultrafast
vp9_crf33 libvpx-vp9 crf 33 (constant quality, b:v 0, deadline good, cpu-used 4) in WebM
rescale_0.5 rescale to 50 percent of each side (libswscale, even dimensions), libx264 crf 18
rescale_0.75 rescale to 75 percent of each side (libswscale, even dimensions), libx264 crf 18
crop_80 centre crop keeping 80 percent of each side (even dimensions), libx264 crf 18
frame_drop_10 every 10th frame dropped, same frame rate, libx264 crf 18
fps_15 frame rate reduced to 15 fps (libavfilter fps), libx264 crf 18
remux_mkv video packets copied into Matroska (no pixel change)
social short side scaled to 720 or less, 30 fps, libx264 crf 26, faststart (a social-media upload)
rotate_90 rotated a quarter turn clockwise (libavfilter transpose=clock, width and height swap), libx264 crf 18
rotate_+5 rotated 5 degrees clockwise about the centre on the same canvas, corners filled black (libavfilter rotate, bilinear), libx264 crf 18
hflip mirrored left to right (libavfilter hflip), libx264 crf 18
crop_80_corner top-left corner crop keeping 80 percent of each side (even dimensions), libx264 crf 18
letterbox centred black bars, always added: a clip narrower than 16:9 is pillarboxed into the smallest even 16:9 frame that holds it (side bars); a 16:9 or wider clip is letterboxed into the smallest even 4:3 frame (top and bottom bars); libx264 crf 18
overlay_box a caption bar drawn over the lower third: black at 60 percent opacity, x 5 to 95 percent of the width, y 70 to 85 percent of the height (libavfilter drawbox), libx264 crf 18
brightness_eq eq=brightness=0.08:contrast=1.1 on luma, chroma untouched (FFmpeg eq's integer map applied with libavfilter lutyuv, as eq is not in the PyAV build), libx264 crf 18
grayscale saturation set to zero (libavfilter hue=s=0, neutral chroma), libx264 crf 18
trim_start_1.3s the first 1.3 s cut (libavfilter trim; a cut off the 1 s key frame grid), libx264 crf 18
middle_2s a 2 s segment from the middle: round(2 x fps) frames from frame (frames - kept) // 2, libx264 crf 18
splice_unmarked the first half of the frames from the marked clip, the rest from the same clip's unmarked source (a partly marked edit), libx264 crf 18
speed_1.25 played 1.25 times faster: timestamps divided by 1.25 (setpts), then back to the source frame rate (libavfilter fps drops one frame in five), libx264 crf 18
h264_1mbps libx264 crf 18 under a VBV bitrate cap: maxrate 1000 kbit/s, buffer (bufsize) 2000 kbit, preset ultrafast; not a hard cap: with the buffer, the bitrate averaged over a 5 s clip can exceed 1 Mbit/s
screen_capture a screen recording: scaled (aspect kept, even dimensions, libswscale) to fit 90 percent of a 1920x1080 canvas, centred on black, 30 fps, libx264 crf 23
oracle_crop_80 Oracle (not a product number): crop_80's centre crop padded back to the original size at the crop's true offset on mid-grey (0x808080), libx264 crf 18; bounds a detector that could undo the crop

Video watermark robustness (VideoSeal videoseal_1.0@870ca7f, 92-bit payload under BCH t=16; keyframe TrustMark 0.9.2, 61-bit payload, one IDR frame per 1 s): n=43 clips of Xiph derf test sequences, 5 s clips, seed 0; unreadable 0.

videoseal (cuda:0)

n=43, skipped 0; embed PSNR mean 44.7 dB, p5 41.4 dB; embed 2.28 s per clip, detect 0.33 s per unmarked clip. The 43 unmarked clips are the earlier false-positive basis for video; the 2026-10 null campaign read 4,854 unmarked clips and flagged none (Video null campaign).

unmarked clips decoded to a payload: 0 of 43 (95% interval 0 to 8.2%); a uniformly random read lands within the BCH radius (t=16) of a codeword and passes the CRC-16 with probability 2^-64.4, below 2^-50.

transform exact decode (95% CI) mis-decode bits mean/p5 detect s exact, first 25% of frames
identity 100.0% (91.8 to 100.0%) 0% 99.4% / 96.8% 0.27 97.7% (87.9 to 99.6%)
h264_crf23 93.0% (81.4 to 97.6%) 0% 97.5% / 91.5% 0.22 86.0% (72.7 to 93.4%)
h264_crf28 53.5% (38.9 to 67.5%) 0% 91.1% / 79.8% 0.21 48.8% (34.6 to 63.2%)
h264_crf35 14.0% (6.6 to 27.3%) 0% 73.5% / 56.0% 0.21 16.3% (8.1 to 30.0%)
h265_crf28 27.9% (16.7 to 42.7%) 0% 79.4% / 56.0% 0.22 18.6% (9.7 to 32.6%)
vp9_crf33 83.7% (70.0 to 91.9%) 0% 96.6% / 88.7% 0.29 72.1% (57.3 to 83.3%)
rescale_0.5 62.8% (47.9 to 75.6%) 0% 92.1% / 77.4% 0.20 48.8% (34.6 to 63.2%)
rescale_0.75 93.0% (81.4 to 97.6%) 0% 98.6% / 91.5% 0.20 90.7% (78.4 to 96.3%)
crop_80 62.8% (47.9 to 75.6%) 0% 92.4% / 77.0% 0.21 34.9% (22.4 to 49.8%)
frame_drop_10 97.7% (87.9 to 99.6%) 0% 99.1% / 95.6% 0.20 95.3% (84.5 to 98.7%)
fps_15 97.7% (87.9 to 99.6%) 0% 99.3% / 96.0% 0.10 95.3% (84.5 to 98.7%)
remux_mkv 100.0% (91.8 to 100.0%) 0% 99.8% / 98.8% 0.24 100.0% (91.8 to 100.0%)
social 62.8% (47.9 to 75.6%) 0% 93.8% / 82.7% 0.19 58.1% (43.3 to 71.6%)
rotate_90 95.3% (84.5 to 98.7%) 0% 98.3% / 92.3% 0.22 90.7% (78.4 to 96.3%)
rotate_+5 93.0% (81.4 to 97.6%) 0% 98.1% / 90.7% 0.22 86.0% (72.7 to 93.4%)
hflip 93.0% (81.4 to 97.6%) 0% 98.8% / 91.1% 0.22 90.7% (78.4 to 96.3%)
crop_80_corner 41.9% (28.4 to 56.7%) 0% 86.2% / 50.8% 0.21 27.9% (16.7 to 42.7%)
letterbox 34.9% (22.4 to 49.8%) 0% 85.9% / 69.3% 0.23 34.9% (22.4 to 49.8%)
overlay_box 95.3% (84.5 to 98.7%) 0% 98.1% / 93.2% 0.22 90.7% (78.4 to 96.3%)
brightness_eq 97.7% (87.9 to 99.6%) 0% 98.7% / 92.7% 0.22 88.4% (75.5 to 94.9%)
grayscale 100.0% (91.8 to 100.0%) 0% 99.4% / 95.6% 0.21 95.3% (84.5 to 98.7%)
trim_start_1.3s 90.7% (78.4 to 96.3%) 0% 96.5% / 77.0% 0.16 72.1% (57.3 to 83.3%)
middle_2s 90.7% (78.4 to 96.3%) 0% 96.5% / 82.7% 0.09 72.1% (57.3 to 83.3%)
splice_unmarked 97.7% (87.9 to 99.6%) 0% 98.7% / 93.5% 0.23 95.3% (84.5 to 98.7%)
speed_1.25 95.3% (84.5 to 98.7%) 0% 98.8% / 94.3% 0.18 95.3% (84.5 to 98.7%)
h264_1mbps 90.7% (78.4 to 96.3%) 0% 97.4% / 88.7% 0.21 90.7% (78.4 to 96.3%)
screen_capture 69.8% (54.9 to 81.4%) 0% 93.4% / 73.8% 0.30 51.2% (36.8 to 65.4%)
oracle_crop_80 93.0% (81.4 to 97.6%) 0% 96.4% / 91.9% 0.22 74.4% (59.8 to 85.1%)

trustmark_keyframes (cpu)

n=43, skipped 2; embed PSNR mean 42.3 dB, p5 38.8 dB; embed 4.98 s per clip, detect 1.50 s per unmarked clip.

unmarked clips decoded to a payload: 0 of 43 (95% interval 0 to 8.2%).

transform exact decode (95% CI) mis-decode bits mean/p5 detect s exact, first 25% of frames
identity 95.1% (83.9 to 98.7%) 0% 93.3% / 80.6% 0.96 n/a
h264_crf23 87.8% (74.5 to 94.7%) 0% 86.0% / 66.4% 1.26 n/a
h264_crf28 78.0% (63.3 to 88.0%) 0% 68.5% / 58.6% 1.30 n/a
h264_crf35 12.2% (5.3 to 25.5%) 2.4% 59.0% / 52.2% 1.39 n/a
h265_crf28 26.8% (15.7 to 41.9%) 0% 66.0% / 57.0% 1.39 n/a
vp9_crf33 92.7% (80.6 to 97.5%) 0% 93.4% / 80.6% 0.90 n/a
rescale_0.5 90.2% (77.5 to 96.1%) 0% 90.2% / 70.6% 1.03 n/a
rescale_0.75 92.7% (80.6 to 97.5%) 0% 92.9% / 70.2% 0.90 n/a
crop_80 92.7% (80.6 to 97.5%) 0% 92.8% / 70.8% 0.92 n/a
frame_drop_10 92.7% (80.6 to 97.5%) 0% 59.9% / 55.8% 1.28 n/a
fps_15 78.0% (63.3 to 88.0%) 2.4% 88.6% / 49.6% 0.83 n/a
remux_mkv 97.6% (87.4 to 99.6%) 0% 97.9% / 87.8% 0.71 n/a
social 70.7% (55.5 to 82.4%) 0% 76.4% / 62.8% 1.29 n/a
rotate_90 0% (0 to 8.6%) 0% 50.2% / 46.4% 1.42 n/a
rotate_+5 0% (0 to 8.6%) 0% 55.7% / 49.0% 1.43 n/a
hflip 95.1% (83.9 to 98.7%) 0% 95.2% / 76.6% 0.81 n/a
crop_80_corner 39.0% (25.7 to 54.3%) 0% 79.8% / 55.8% 1.32 n/a
letterbox 0% (0 to 8.6%) 0% 54.1% / 49.8% 1.43 n/a
overlay_box 82.9% (68.7 to 91.5%) 0% 89.5% / 72.4% 1.21 n/a
brightness_eq 85.4% (71.6 to 93.1%) 0% 93.4% / 74.4% 0.85 n/a
grayscale 85.4% (71.6 to 93.1%) 0% 93.8% / 80.2% 0.91 n/a
trim_start_1.3s 0% (0 to 8.6%) 0% 49.5% / 44.5% 1.14 n/a
middle_2s 0% (0 to 8.6%) 0% 49.5% / 43.5% 0.59 n/a
splice_unmarked 95.1% (83.9 to 98.7%) 0% 77.0% / 68.6% 1.05 n/a
speed_1.25 92.7% (80.6 to 97.5%) 0% 61.9% / 57.0% 1.00 n/a
h264_1mbps 92.7% (80.6 to 97.5%) 0% 83.2% / 56.0% 0.99 n/a
screen_capture 0% (0 to 8.6%) 0% 51.8% / 47.4% 1.56 n/a
oracle_crop_80 95.1% (83.9 to 98.7%) 0% 93.8% / 71.8% 0.83 n/a

Columns: exact decode is the payload recovered exactly, with its Wilson 95 percent interval; mis-decode a different payload that passed the code's checks; bits the agreement of the decoded bits with the embedded ones (for VideoSeal the 248 hard codeword bits of the averaged logits, for the keyframe scheme the 100 raw TrustMark bits of each voted frame; mean and 5th percentile); detect s the decode time per clip; exact, first 25% of frames (VideoSeal only) the exact decode when only the first quarter of the sampled frames is averaged, a diagnostic for temporal voting. oracle_crop_80 is not a product number: the crop padded back to the original size at its true offset.

Reading the tables:

  • VideoSeal survives light re-encoding, geometry and tone edits, not typical upload compression. Exact decode is 100 percent with no change, after a Matroska remux and in greyscale; 97.7 percent after dropping every 10th frame, at 15 fps, after a brightness and contrast change or a half-unmarked splice; 95.3 percent after a 90-degree turn, a caption box or a x1.25 speed-up; 93.0 percent after a 5-degree rotation, a horizontal flip, rescaling to 75 percent and at H.264 crf 23; 90.7 percent after the start trim, the middle 2 s and a 1 Mbit/s cap. It falls to 83.7 percent at VP9 crf 33, 53.5 percent at H.264 crf 28, 27.9 percent at H.265 crf 28, 14.0 percent at crf 35, 62.8 percent after rescaling to 50 percent, an 80 percent centre crop or the social-media re-encode, 69.8 percent after a screen recording, 41.9 percent after a corner crop and 34.9 percent after letterboxing. The BCH code corrects 16 of 220 effective bits; heavier compression flips more.
  • VideoSeal never returned a wrong payload. Mis-decode is 0 percent on every transform, and 0 of 43 unmarked clips decoded to a payload. The analytic bound for a uniformly random read is 2^-64.4 per decode.
  • The keyframe scheme holds up better under compression and scale, fails every edit that moves its 1 s grid, and can return a wrong payload. It recovers 78.0 percent at H.264 crf 28, 90.2 percent after rescaling to 50 percent and 92.7 percent after the centre crop, 95.1 percent with no change (over the 41 clips it marked; on 2 clips TrustMark skipped every keyframe), but 0 percent after a rotation, letterboxing, the start trim, the middle 2 s and a screen recording. It returned a different, well-formed payload for 2.4 percent of clips at crf 35 and at 15 fps. A decoded payload alone therefore proves nothing: detect counts a registry hit only when the query's bytes or the fingerprint of its frame at 1 s match the evidence entry (Grading). 0 of 43 unmarked clips decoded to a payload.
  • More frames help VideoSeal. Averaging all sampled frames beats the first quarter on most rows (crop 80 percent 62.8 against 34.9 percent, screen recording 69.8 against 51.2 percent), which is why temporal weighting matters.
  • Quality and speed. Embed PSNR is mean 44.7 dB (5th percentile 41.4 dB) for VideoSeal and 42.3 dB (38.8 dB) for the keyframes, measured as the evidence records it. VideoSeal on the RTX 5080 took 2.28 s per 5 s clip to embed and 0.33 s to check an unmarked clip; the keyframe scheme on one CPU worker 4.98 s and 1.50 s. These are wall-clock times with the sweeps running alongside, so they are comparable only between runs on an idle machine.
  • Natural video only. VideoSeal's JND attenuation hides the mark in texture: uniform per-pixel noise frames decode at chance (0.50 bit accuracy in the vendoring test) and very smooth frames carry a faint mark. Test with real footage, not noise or flat colour.

Where it breaks

Sweeps on all 43 clips. Margins are bit errors against the codeword (VideoSeal: decoded at most 16 of 248, near at most 32, mid at most 62, far above; keyframes: the averaged vote against the 100-bit packet, t=5, near at most 10, mid at most 25). The hint is a heuristic, not a measurement. The per-clip margins of every main row are in the JSON (where_it_breaks).

Where it breaks: videoseal

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
h264_crf 23 (93.0%) 28 (53.5%) n/a at 26: decoded 26, near 13, mid 4 heuristic: soft voting or stronger ECC candidate (13 near, 4 mid, 0 far of 17 failing; oracle absent)
h265_crf none (first below at 23) 23 (55.8%) n/a at 23: decoded 21, near 8, mid 13, far 1 heuristic: no single candidate (mixed margins) (8 near, 13 mid, 1 far of 22 failing; oracle absent)
vp9_crf 25 (97.7%) 40 (62.8%, whole sweep) n/a at 33: decoded 34, near 9 heuristic: soft voting or stronger ECC candidate (9 near, 0 mid, 0 far of 9 failing; oracle absent)
bitrate 1 (90.7%) 0.5 (67.4%, whole sweep) n/a at 0.5: decoded 29, near 9, mid 4, far 1 heuristic: soft voting or stronger ECC candidate (9 near, 4 mid, 1 far of 14 failing; oracle absent)
scale 0.6 (90.7%) 0.5 (62.8%) n/a at 0.5: decoded 25, near 10, mid 6, far 2 heuristic: soft voting or stronger ECC candidate (10 near, 6 mid, 2 far of 18 failing; oracle absent)
crop 90 (93.0%) 80 (62.8%) >= 90% up to 80; 88.4% at 85 at 85: decoded 37, near 4, mid 1, far 1 heuristic: soft voting or stronger ECC candidate (4 near, 1 mid, 1 far of 6 failing; oracle 88.4%)
rotate 5 (93.0%) 10 (81.4%, whole sweep) n/a at 10: decoded 34, near 8, mid 1 heuristic: soft voting or stronger ECC candidate (8 near, 1 mid, 0 far of 9 failing; oracle absent)
trim_start 1.5 (90.7%; first below at 1, after 0.6) 2.5 (88.4%, whole sweep) n/a at 1: decoded 38, near 2, mid 1, far 2 heuristic: no single candidate (mixed margins) (2 near, 1 mid, 2 far of 5 failing; oracle absent)
segment 2 (90.7%; first below at 3, after 4) 0.5 (72.1%, whole sweep) n/a at 3: decoded 36, near 4, mid 1, far 2 heuristic: soft voting or stronger ECC candidate (4 near, 1 mid, 2 far of 7 failing; oracle absent)
splice 25 (95.3%, whole sweep) 25 (95.3%, whole sweep) n/a n/a n/a
fps 10 (90.7%, whole sweep) 10 (90.7%, whole sweep) n/a n/a n/a

Where it breaks: trustmark_keyframes

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
h264_crf 20 (95.1%) 28 (78.0%) n/a at 23: decoded 38, near 1, mid 2 heuristic: no single candidate (mixed margins) (1 near, 2 mid, 0 far of 3 failing; oracle absent)
h265_crf none (first below at 23) 23 (68.3%) n/a at 23: decoded 31, near 4, mid 6 heuristic: no single candidate (mixed margins) (4 near, 6 mid, 0 far of 10 failing; oracle absent)
vp9_crf 33 (92.7%) 40 (82.9%, whole sweep) n/a at 40: decoded 36, near 4, mid 1 heuristic: soft voting or stronger ECC candidate (4 near, 1 mid, 0 far of 5 failing; oracle absent)
bitrate 0.5 (90.2%, whole sweep) 0.5 (90.2%, whole sweep) n/a n/a n/a
scale 0.5 (90.2%) 0.33 (68.3%) n/a at 0.33: decoded 32, near 7, mid 1, far 1 heuristic: soft voting or stronger ECC candidate (7 near, 1 mid, 1 far of 9 failing; oracle absent)
crop 80 (92.7%) 80 (92.7%) >= 90% up to 80; 80.5% at 70 at 70: far 41 heuristic: resync/search candidate (0 near, 0 mid, 41 far of 41 failing; oracle 80.5%)
rotate none (first below at 1) 2 (70.7%) n/a at 1: decoded 38, near 1, mid 2 heuristic: no single candidate (mixed margins) (1 near, 2 mid, 0 far of 3 failing; oracle absent)
trim_start 1 (95.1%; first below at 0.1) 1 (95.1%; first below at 0.1) n/a at 0.1: far 41 heuristic: embedding change candidate (tiling, redundancy, sync) (0 near, 0 mid, 41 far of 41 failing; oracle absent)
segment 1 (92.7%; first below at 4) 1 (92.7%; first below at 4) n/a at 4: decoded 1, far 40 heuristic: embedding change candidate (tiling, redundancy, sync) (0 near, 0 mid, 40 far of 40 failing; oracle absent)
splice 25 (92.7%, whole sweep) 25 (92.7%, whole sweep) n/a n/a n/a
fps 30 (95.1%) 15 (78.0%) n/a at 24: decoded 32, near 2, far 7 heuristic: embedding change candidate (tiling, redundancy, sync) (2 near, 0 mid, 7 far of 9 failing; oracle absent)

Hints are a heuristic from the margin mix and the oracle rate at the first failing point, not a measurement: mostly near (>= 50% of failing items) suggests soft voting or a stronger ECC; mostly mid or far with an oracle >= 80% suggests a resync search; mostly far with a low or no oracle suggests an embedding change.

The H.264 quality ladder and the centre crops, with the crop oracle (the other sweeps are in the JSON and in Robustness baseline):

h264_crf: H.264 quality ladder: libx264 at each crf, preset ultrafast

crf videoseal trustmark_keyframes
18 100.0% (91.8 to 100.0%) 95.1% (83.9 to 98.7%)
20 97.7% (87.9 to 99.6%) 95.1% (83.9 to 98.7%)
23 93.0% (81.4 to 97.6%) 87.8% (74.5 to 94.7%)
26 62.8% (47.9 to 75.6%) 70.7% (55.5 to 82.4%)
28 53.5% (38.9 to 67.5%) 78.0% (63.3 to 88.0%)
30 32.6% (20.5 to 47.5%) 46.3% (32.1 to 61.3%)
32 18.6% (9.7 to 32.6%) 36.6% (23.6 to 51.9%)
35 14.0% (6.6 to 27.3%) 12.2% (5.3 to 25.5%)
40 4.7% (1.3 to 15.5%) 7.3% (2.5 to 19.4%)

crop: centre crops: the percentage of each side kept (even dimensions), libx264 crf 18

keep % videoseal oracle videoseal trustmark_keyframes oracle trustmark_keyframes
95 95.3% (84.5 to 98.7%) 95.3% (84.5 to 98.7%) 95.1% (83.9 to 98.7%) 95.1% (83.9 to 98.7%)
90 93.0% (81.4 to 97.6%) 93.0% (81.4 to 97.6%) 95.1% (83.9 to 98.7%) 97.6% (87.4 to 99.6%)
85 86.0% (72.7 to 93.4%) 88.4% (75.5 to 94.9%) 95.1% (83.9 to 98.7%) 95.1% (83.9 to 98.7%)
80 62.8% (47.9 to 75.6%) 93.0% (81.4 to 97.6%) 92.7% (80.6 to 97.5%) 95.1% (83.9 to 98.7%)
70 7.0% (2.4 to 18.6%) 76.7% (62.3 to 86.8%) 0% (0 to 8.6%) 80.5% (66.0 to 89.8%)
60 0% (0 to 8.2%) 23.3% (13.2 to 37.7%) 0% (0 to 8.6%) 7.3% (2.5 to 19.4%)
50 0% (0 to 8.2%) 0% (0 to 8.2%) 0% (0 to 8.6%) 0% (0 to 8.6%)

Changes from the published run

The earlier VideoSeal numbers were embedded with cuDNN TF32 on (the torch default); this run has TF32 off, which changes the VideoSeal embed bytes. libx264 output is also occasionally not byte-reproducible (seen in 2 of 16 encodes under mixed load, 0 of 240 in isolation; cause unidentified), so per-clip bit accuracy reproduces to about +-0.006 between runs and an exact decode can move at the edge; that affects the keyframe scheme as well, which ran on the CPU both times. Rows whose exact decode or mis-decode moved, all inside the earlier run's Wilson interval:

scheme row earlier run this run
videoseal h265_crf28 25.6% 27.9%
videoseal vp9_crf33 79.1% 83.7%
videoseal rescale_0.75 97.7% 93.0%
videoseal crop_80 53.5% 62.8%
videoseal fps_15 100.0% 97.7%
videoseal social 67.4% 62.8%
trustmark_keyframes identity 92.7% 95.1%
trustmark_keyframes h264_crf23 85.4% 87.8%
trustmark_keyframes h264_crf28 75.6% (mis-decode 4.9%) 78.0% (mis-decode 0%)
trustmark_keyframes h264_crf35 12.2% (mis-decode 4.9%) 12.2% (mis-decode 2.4%)
trustmark_keyframes h265_crf28 29.3% 26.8%
trustmark_keyframes rescale_0.5 87.8% 90.2%
trustmark_keyframes rescale_0.75 90.2% (mis-decode 2.4%) 92.7% (mis-decode 0%)
trustmark_keyframes fps_15 78.0% (mis-decode 0%) 78.0% (mis-decode 2.4%)
trustmark_keyframes social 75.6% 70.7%

Keyframe reader on ONNX Runtime (TrustMark Q)

watermark.video.trustmark_runtime: onnx (the default) runs the keyframe reader's TrustMark Q decoder forward through ONNX Runtime on Laidline's FP32 graph of the Q decoder weights (decoder_Q.fp32.onnx, sha256 1e641de3...). Everything around the forward is the torch reader's. Measured on one host: AMD Ryzen 7 7800X3D (AVX-512, VNNI, no AMX), CPU only. The gain on hosts without AVX-512 is unmeasured, and decision-equality belongs to a build (ORT version, CPU kernels).

Decision-equality (onnxruntime 1.29.0, bench reader). 0 differing decisions against the torch FP32 Q-only reader on 8,661 saved units: 1,781 unmarked and 6,880 marked rows. There were 0 decodes on unmarked units. Max |delta normalised score| was 0.069, on reads far from the gates. One marked row (derf/bridge_far_0, crop 60) reads a wrong payload under both readers identically; it is a torch reader row, not an ONNX one.

The shipped onnxruntime 1.24.4. The Pipecat plugin requires onnxruntime~=1.24.3, so Laidline pins 1.24.4 rather than 1.29.0. 1.24.4 gives logits bitwise equal to 1.29.0's on 45 golden inputs (including the 16 reads nearest the gates; the check runs on AVX-512 hosts) and on the bench reader's 512 calibration frames.

The product path against torch (onnxruntime 1.24.4, 2 threads each). The product's unkeyed decode_keyframes(hardened) on the Q-only reader (hardened_reads) ran twice on the same file in one process, once with trustmark_runtime: torch and once with onnx. It covered the bench reader's EQ-screen (364 marked rows and 120 unmarked clips) and EQ-U (every unmarked unit: 41 derf, 1,120 YouTube UGC, 620 fresh UGC; this is the earlier unmarked-clip basis, and the 2026-10 null campaign later read 4,854 unmarked clips, see Video null campaign). The comparison used the bench decision-equality: the same payload and path, and read for read the same passed and accepted bits. A Critical counts only where ONNX is wrong and torch is not. The rules were written before the data.

What this run did not pair: the keyed reader. The keyed reader (keyed_reads, the reader once payload keys are configured) was not run on these sets here; the next paragraph covers it. It reads frames the unkeyed vote does not: these sets' marked rows carry an unkeyed payload, which is not eligible under a key, so the keyed reader would go on to the scan. Two things carry over to it:

  • its per-read gate verdict is the same function of the logits as the unkeyed reader's (best_candidate and softdecode.accept);
  • its outputs on the 45 golden inputs, 16 of them near the gates, are equal under both paths.
set units differing (near-miss / hard) ONNX Critical decodes on unmarked (ONNX / torch) verdict
EQ-screen 484 (marked: 245 vote, 23 scan, 96 none under both) 0 (0 / 0) 0 0 / 0 of 120 PASS
EQ-U 1,781 (each scanned) 0 (0 / 0) 0 0 / 0 of 1,781 PASS

Over 269,819 aligned reads, 71,369 of them with a candidate on both sides: max |delta score| 0.0217, max |delta normalised| 0.069, the same maxima as the bench reader, both on reads rejected under both paths. None of the 2,145 units differs, so no read near a gate changed its verdict. Of the 364 marked rows (the EQ-screen sample of the 6,880; the bench reader ran all of them on 1.29.0), the torch reader makes no wrong row here.

The keyed reader, paired. The product's keyed keyframe branch (keyed_reads, own issuer, 4 bench video keys, a local registry) ran on the same file under torch and under ONNX (onnxruntime 1.24.4, 2 threads), on EQ-screen (484), EQ-U (1,781) and 81 saved keyed-marked keyframe files. The comparison used the whole keyed signal and the bench decision-equality, read for read. Line, written before the data: 0 differing. Result: 0 differing on 484, 1,781 and 81 units, 0 ONNX Critical, 0 payloads chosen on unmarked units on either side; on the keyed-marked set 79 exact by the vote and 2 nothing chosen, the same on both runtimes.

Whole-clip time of the product reader (onnxruntime 1.24.4, quiet window). The product's decode_keyframes(hardened) on the Q-only reader, fingerprint frame included. Each figure is the median of 3 repetitions after a warm-up, with the two readers alternated in one process.

  • 2 threads: two reserved CPUs, idle before start (1-minute load 0.83), torch 2 threads, ORT intra-op 2.
  • Default: the whole machine (8 threads, all CPUs idle before start, load 2.5), torch's default 8 threads, ORT intra-op 0. The 1-minute load reached 4.7 to 7.6 during the repetitions; that is this process's own threads, with no other queue.
  • The line was the ONNX median at most 1.2x the bench reader's R1o figure.
clip threads torch ONNX ONNX gain bench R1o ratio to R1o line
akiyo CIF 2 9.859 s 5.491 s 1.80x 5.436 s 1.01 PASS
old_town_cross 720p50 2 19.054 s 12.308 s 1.55x 11.082 s 1.11 PASS
akiyo CIF default (8) 4.970 s 2.837 s 1.75x 2.497 s 1.14 PASS
old_town_cross 720p50 default (8) 9.236 s 5.614 s 1.65x 5.008 s 1.12 PASS

Neither clip decodes (both unmarked) under either reader.

Whole-clip time, unmarked clip, decode: hardened, Q-only reader (bench reader, onnxruntime 1.29.0, quiet window, median of 3). "8 threads" is the whole machine: torch's default and ORT intra-op 0.

clip torch, 2 threads ONNX, 2 threads torch, 8 threads ONNX, 8 threads
akiyo CIF (141 frames scanned) 9.96 s 5.44 s (1.83x) 4.80 s 2.50 s
old_town_cross 720p50 (231 frames) 17.77 s 11.08 s (1.60x) 8.65 s 5.01 s
marked as written (vote only) 0.28 / 0.69 s 0.22 / 0.66 s

Text robustness (SynthID and KGW, Qwen2.5-0.5B on C4)

What the text watermark survives. For 200 prompts from C4 (the first 50 tokens of a news document), each arm generates a marked completion of up to 300 new tokens with Qwen/Qwen2.5-0.5B-Instruct and the bench key, then every completion goes through each transform and is scored by Marker.detect with the arm's scheme, as /detect scores text. Unmarked generations of the same prompts and 500 human C4 continuations give the false-positive rate. What the failures mean is in Robustness baseline.

  • synthid_t1: SynthID tournament (ngram 5, depth 30), sampling at temperature 1.0 (top-k 0, top-p 1.0, no repetition penalty).
  • kgw_t1: KGW (green-list ratio 0.25, bias 2.0, context width 1), sampling at temperature 1.0.
  • kgw_t0: KGW, greedy decoding (the case KGW is configured for).
# with the cu130 torch wheels; the paraphrase and translation transforms also need sentencepiece
laidline bench text --corpus FILE --n 200 --workers 2 --gen-batch 8 --device cuda

Point --corpus at a C4 shard or a JSON-lines file of your own prompts; --model names the generating model.

Corpus: C4 realnewslike validation, shard 00000-of-00001 (allenai/c4, ODC-BY 1.0); prompts, human controls and the embed_25 filler are disjoint documents drawn with seed 0. Model Qwen/Qwen2.5-0.5B-Instruct (Apache-2.0) at revision 7ae557604adf67be50417f59c2c2f167def9a775; translation by Helsinki-NLP opus-mt-en-de and opus-mt-de-en. Measured on the RTX 5080 (torch 2.14.0+cu130, transformers 5.17.0, TF32 off, cuDNN deterministic, 2 workers, generation batch 8): machine.run_seconds 3581.2. The result ships as laidline/compliance/data/text_robustness.json; the marking-method text does not quote it. The false-positive guard is a binomial bound (below).

Transforms (transform_definitions in the JSON):

transform definition
identity none: the completion as decoded, re-tokenised by detect (a decode/re-tokenise round trip)
truncate_100 keep the first 100 tokens (the generating model's tokenizer), decoded
truncate_50 keep the first 50 tokens (the generating model's tokenizer), decoded
delete_10 delete 10 percent of the words (whitespace-separated, seeded positions); line breaks kept
delete_30 delete 30 percent of the words (whitespace-separated, seeded positions); line breaks kept
insert_10 insert words drawn from the human C4 text (running text, so by frequency; seeded) at random positions, 10 percent of the word count
swap_10 replace 10 percent of the words with words drawn uniformly from the distinct alphanumeric words of the human C4 text (seeded)
swap_30 replace 30 percent of the words with words drawn uniformly from the distinct alphanumeric words of the human C4 text (seeded)
typo_5 each non-space character, with probability 0.05, gets one edit: substitute a random lowercase letter, insert one after it, delete it, or swap it with the next character (no change at the end of a word) (seeded)
normalise Unicode NFKC, curly quotes to straight quotes, every whitespace run collapsed to one space, trimmed
embed_25 the completion placed at a seeded word boundary of human C4 text three times its token length (the mark is 25 percent of the tokens), joined by blank lines; KGW scores only the first 1024 tokens, so under KGW the mark is about 12 to 29 percent of the scored window, and part of it may fall outside
paraphrase rewritten by the generating model (chat prompt, temperature 0.7, no watermark, at most twice the tokens)
translate_rt English to German and back (Helsinki-NLP opus-mt-en-de and opus-mt-de-en, sentence by sentence, greedy)

Text watermark robustness (Qwen/Qwen2.5-0.5B-Instruct, bench key fingerprints synthid 3bc1c45d, kgw 3bc1c45d): n=200 C4 realnewslike validation (allenai/c4, shard 00000-of-00001) prompts (50-token prefixes, up to 300 new tokens), seed 0, measured.

Detected: p < 0.001 from Marker.detect (one z-test over the first max_detect_tokens tokens: SynthID 8192, KGW 1024); grade B: detected over at least 200 tokens (KGW: tokens covered by distinct n-grams, a repeated n-gram counting once).

Decoding: sampled arms do_sample=True, temperature=1.0, top_k=0, top_p=1.0, repetition_penalty=1.0 (temperature: the arm's); greedy arm do_sample=False, temperature=unset, top_k=unset, top_p=unset, repetition_penalty=1.0; the raw C4 prefix as the prompt; the completion ends at its first EOS.

synthid_t1 (SynthID, temperature 1.0)

n=200 generations, tokens mean 296.5, p5 300; stopped before the limit 9.

transform detected (95% CI) grade B z mean / p5 tokens mean scored mean
identity 100.0% (98.1 to 100.0%) 98.5% 24.06 / 17.14 296 289
truncate_100 100.0% (98.1 to 100.0%) 0% 13.51 / 10.02 100 96
truncate_50 99.5% (97.2 to 99.9%) 0% 9.39 / 6.77 50 46
delete_10 100.0% (98.1 to 100.0%) 98.0% 16.28 / 11.50 267 261
delete_30 97.0% (93.6 to 98.6%) 92.5% 6.66 / 4.00 209 204
insert_10 100.0% (98.1 to 100.0%) 98.5% 16.39 / 11.52 327 320
swap_10 100.0% (98.1 to 100.0%) 98.5% 15.28 / 10.37 300 294
swap_30 94.0% (89.8 to 96.5%) 92.5% 5.45 / 2.98 306 301
typo_5 100.0% (98.1 to 100.0%) 98.5% 8.75 / 5.65 355 349
normalise 100.0% (98.1 to 100.0%) 98.5% 20.20 / 13.82 296 288
embed_25 100.0% (98.1 to 100.0%) 100.0% 12.01 / 7.80 1184 1158
paraphrase 49.5% (42.6 to 56.4%) 35.5% 4.38 / -0.57 209 195
translate_rt 89.5% (84.5 to 93.0%) 89.5% 5.94 / 2.17 333 275

kgw_t1 (KGW, temperature 1.0)

n=200 generations, tokens mean 296.4, p5 300; stopped before the limit 6.

transform detected (95% CI) grade B z mean / p5 tokens mean scored mean
identity 100.0% (98.1 to 100.0%) 97.0% 13.52 / 10.74 296 270
truncate_100 100.0% (98.1 to 100.0%) 0% 7.78 / 5.48 100 95
truncate_50 96.5% (93.0 to 98.3%) 0% 5.43 / 3.36 50 48
delete_10 99.5% (97.2 to 99.9%) 95.0% 11.76 / 9.31 267 247
delete_30 99.5% (97.2 to 99.9%) 66.0% 8.57 / 6.20 209 198
insert_10 100.0% (98.1 to 100.0%) 97.5% 11.75 / 9.15 326 303
swap_10 100.0% (98.1 to 100.0%) 97.5% 11.12 / 8.41 300 280
swap_30 99.5% (97.2 to 99.9%) 97.0% 6.96 / 4.71 306 295
typo_5 99.0% (96.4 to 99.7%) 98.5% 8.43 / 6.18 356 339
normalise 99.5% (97.2 to 99.9%) 96.5% 12.28 / 9.73 296 269
embed_25 98.0% (95.0 to 99.2%) 98.0% 6.87 / 4.02 1014 901
paraphrase 57.0% (50.1 to 63.7%) 29.0% 4.19 / 0.00 192 166
translate_rt 97.5% (94.3 to 98.9%) 92.5% 6.54 / 3.88 337 256

kgw_t0 (KGW, greedy)

n=200 generations, tokens mean 300.0, p5 300; stopped before the limit 0.

transform detected (95% CI) grade B z mean / p5 tokens mean scored mean
identity 95.0% (91.0 to 97.3%) 1.0% 6.43 / 3.29 300 63
truncate_100 95.0% (91.0 to 97.3%) 0% 6.28 / 3.17 100 56
truncate_50 93.0% (88.6 to 95.8%) 0% 5.41 / 2.89 50 39
delete_10 89.0% (83.9 to 92.6%) 0.5% 5.24 / 2.43 271 75
delete_30 77.0% (70.7 to 82.3%) 0% 4.19 / 1.54 214 84
insert_10 74.5% (68.0 to 80.0%) 3.0% 4.38 / 1.79 333 116
swap_10 74.0% (67.5 to 79.6%) 2.5% 4.30 / 1.03 306 114
swap_30 39.5% (33.0 to 46.4%) 32.0% 2.77 / 0.56 316 203
typo_5 56.0% (49.1 to 62.7%) 27.5% 3.34 / 0.74 354 181
normalise 90.5% (85.6 to 93.8%) 1.0% 5.60 / 2.45 299 62
embed_25 17.0% (12.4 to 22.8%) 17.0% 1.65 / -0.30 1024 717
paraphrase 30.5% (24.5 to 37.2%) 0.5% 2.29 / -0.40 121 62
translate_rt 63.5% (56.6 to 69.9%) 0.5% 3.51 / 0.44 308 66

False-positive controls

These 500 human continuations and 600 unmarked generations are the earlier false-positive basis for text. The 2026-10 null campaign read 20,000 unmarked documents per scheme at the shipped grade-B gate (Text null campaign).

set scheme n flagged FPR (95% CI) z mean / max
human C4 continuations synthid 500 1 0.2% (0.035 to 1.1%) -0.06 / 3.31
human C4 continuations kgw 500 0 0% (0 to 0.76%) 0.03 / 3.07
unmarked generations, synthid_t1 synthid 200 1 0.5% (0.088 to 2.8%) -0.06 / 3.54
unmarked generations, kgw_t1 kgw 200 0 0% (0 to 1.9%) 0.14 / 2.86
unmarked generations, kgw_t0 kgw 200 0 0% (0 to 1.9%) 0.11 / 2.53

FPR guard (limit 0.1%, binomial tail 0.05): PASS: human/synthid 1 of 500 (0.2%, 95% CI 0.035 to 1.1%; expected 0.5 at the limit, P(X>=1)=0.39); unmarked:synthid_t1/synthid 1 of 200 (0.5%, 95% CI 0.088 to 2.8%; expected 0.2 at the limit, P(X>=1)=0.18); 0 flagged in the other 3 control sets

Columns: detected is p < 0.001 from Marker.detect, with its Wilson 95 percent interval; grade B is detected over at least 200 tokens (for KGW, tokens covered by distinct n-grams, a repeated n-gram counting once); z is the test's z score (mean and 5th percentile); tokens the text's tokens as detect tokenises it; scored the tokens (SynthID) or distinct n-grams (KGW) the test used.

Reading the tables:

  • Sampled text survives light editing. SynthID at temperature 1.0 is detected in 100 percent of completions with no change (98.5 percent at grade B), after deleting, inserting or swapping 10 percent of the words, 5 percent typos, Unicode and whitespace normalisation, and inside human text three times its length (embed_25). It holds 97.0 percent after deleting 30 percent of the words and 94.0 percent after swapping 30 percent. KGW at temperature 1.0 is at least 96.5 percent on every row but paraphrase.
  • Paraphrase removes about half the marks; translation fewer. A paraphrase by the same model leaves 49.5 percent (SynthID) and 57.0 percent (KGW); an English-German-English round trip 89.5 and 97.5 percent.
  • Short text detects but does not grade. The first 100 tokens are detected in 100 percent of completions for both sampled arms but never reach grade B, which needs 200 tokens.
  • Greedy KGW on a small model detects but grades D. Greedy decoding with a 0.5B model repeats itself, and KGW scores each distinct n-gram once, so a 300-token completion has 63 scored bigrams on average: 95.0 percent detected, 1.0 percent at grade B. It also breaks early under edits (74.0 percent after swapping 10 percent of the words) and inside human text (17.0 percent for embed_25).
  • False positives. SynthID flagged 1 of 500 human continuations (z 3.31) and 1 of 200 unmarked generations (z 3.54); KGW flagged none. The guard is a binomial bound on a 0.1 percent limit: at that limit 500 texts expect 0.5 flags, so a single flag is not evidence of excess, and the run passes. With no flag in 500 the Wilson upper bound is still 0.76 percent, so this run cannot show a rate below 0.1 percent either.
  • Known gap. detect scores only the configured watermark.text.scheme with the default key, with one z-test over the first max_detect_tokens tokens (8192; KGW 1024) and no windowed scoring. embed_25 measures the cost of the dilution.

Where it breaks

The marked texts of the first 100 prompts, scored again after each sweep point (scoring only, no extra generation). Margins are z classes (decoded: p < 0.001; near: at least half that z; mid: a quarter; far: below); the hint is a heuristic, not a measurement. The sweep tables are in the JSON (sweeps) and summarised in Robustness baseline.

Where it breaks: synthid_t1 (SynthID, temperature 1.0)

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
length (tokens kept) 25 (98.0%, whole sweep) 25 (98.0%, whole sweep) n/a n/a n/a
delete (% of words) 30 (97.0%) 40 (73.0%) n/a at 40: decoded 73, near 21, mid 4, far 2 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (21 near, 4 mid, 2 far of 27 failing)
insert (% of words) 30 (100.0%) 40 (84.0%) n/a at 40: decoded 84, near 15, mid 1 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (15 near, 1 mid, 0 far of 16 failing)
swap (% of words) 30 (95.0%) 30 (95.0%) n/a at 40: decoded 38, near 49, mid 9, far 4 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (49 near, 9 mid, 4 far of 62 failing)
typo (% of characters) 5 (100.0%) 10 (53.0%, whole sweep) n/a at 10: decoded 53, near 36, mid 9, far 2 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (36 near, 9 mid, 2 far of 47 failing)
embed (mark's % of tokens) 10 (100.0%, whole sweep) 10 (100.0%, whole sweep) n/a n/a n/a

Tokens needed, length sweep (re-tokenised truncations): detected in >= 90% of items from 25 tokens, >= 50% from 25 tokens; grade B in >= 90% from 200 tokens, >= 50% from 200 tokens.

Tokens needed, per item (the first k at which the first k tokens of the marked text, as detect tokenises it, reach p < 0.001 or grade B, from its per-position scores): p < 0.001 reached by 200 of 200, median 9 tokens, p90 15, max 69; grade B reached by 197 of 200, median 200 tokens, p90 200, max 200.

Embed: SynthID scores the first 8192 tokens, which held the whole mark in every embed row (500 of 500), so an embed miss is dilution by the human text. Known product gap, not fixed: detect scores only the configured watermark.text.scheme and the default key_id, with one z-test over the whole text (its first max_detect_tokens tokens: 8192 for SynthID, 1024 for KGW) and no windowed scoring, so marked text inside longer human text is diluted; embed_25 measures that cost. token_ids hold the whole text (up to that cap) and mark_span locates the mark, so windowed scoring can be done offline.

Where it breaks: kgw_t1 (KGW, temperature 1.0)

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
length (tokens kept) 50 (94.0%) 25 (72.0%, whole sweep) n/a at 25: decoded 72, near 24, mid 1, far 3 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (24 near, 1 mid, 3 far of 28 failing)
delete (% of words) 50 (98.0%, whole sweep) 50 (98.0%, whole sweep) n/a n/a n/a
insert (% of words) 50 (100.0%, whole sweep) 50 (100.0%, whole sweep) n/a n/a n/a
swap (% of words) 40 (99.0%) 50 (79.0%, whole sweep) n/a at 50: decoded 79, near 21 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (21 near, 0 mid, 0 far of 21 failing)
typo (% of characters) 10 (97.0%, whole sweep) 10 (97.0%, whole sweep) n/a n/a n/a
embed (mark's % of tokens) 25 (99.0%) 25 (99.0%) n/a at 10: decoded 32, near 6, mid 9, far 53 heuristic: windowed scoring candidate (the mark is intact but diluted by unmarked text) (6 near, 9 mid, 53 far of 68 failing)

Tokens needed, length sweep (re-tokenised truncations): detected in >= 90% of items from 50 tokens, >= 50% from 25 tokens; grade B in >= 90% from 300 tokens, >= 50% from 300 tokens.

Tokens needed, per item (the first k at which the first k tokens of the marked text, as detect tokenises it, reach p < 0.001 or grade B, from its per-position scores): p < 0.001 reached by 200 of 200, median 13 tokens, p90 36, max 92; grade B reached by 194 of 200, median 212 tokens, p90 228, max 292.

Embed: KGW scores only the first 1024 tokens (at 25%: 0 of 4 failing items had none of the mark in that window, 1 only part of it; at 10%: 65 of 68 failing items had none of the mark in that window, 3 only part of it). For a miss with none of the mark in the window the 1024-token cap of the product, not dilution, explains it; with part of it, both; else dilution. Known product gap, not fixed: detect scores only the configured watermark.text.scheme and the default key_id, with one z-test over the whole text (its first max_detect_tokens tokens: 8192 for SynthID, 1024 for KGW) and no windowed scoring, so marked text inside longer human text is diluted; embed_25 measures that cost. Every KGW embed row also keeps the whole text's arrays (full_token_ids, full_kgw_green, full_kgw_pos) and its mark_span, so windowed scoring can be done offline.

Where it breaks: kgw_t0 (KGW, greedy)

sweep holds >= 90% up to holds >= 50% up to oracle ceiling margins at first failing point hint
length (tokens kept) 50 (91.0%) 25 (73.0%, whole sweep) n/a at 25: decoded 73, near 26, far 1 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (26 near, 0 mid, 1 far of 27 failing)
delete (% of words) none (first below at 5) 50 (51.0%, whole sweep) n/a at 5: decoded 89, near 8, mid 3 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (8 near, 3 mid, 0 far of 11 failing)
insert (% of words) none (first below at 5) 20 (50.0%) n/a at 5: decoded 82, near 15, mid 2, far 1 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (15 near, 2 mid, 1 far of 18 failing)
swap (% of words) none (first below at 5) 20 (52.0%) n/a at 5: decoded 81, near 13, mid 4, far 2 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (13 near, 4 mid, 2 far of 19 failing)
typo (% of characters) none (first below at 1) 5 (53.0%) n/a at 1: decoded 83, near 14, mid 2, far 1 heuristic: more evidence candidate (the signal survives but is weak: more tokens, a stronger bias) (14 near, 2 mid, 1 far of 17 failing)
embed (mark's % of tokens) none (first below at 75) 75 (64.0%) n/a at 75: decoded 64, near 24, mid 10, far 2 heuristic: windowed scoring candidate (the mark is intact but diluted by unmarked text) (24 near, 10 mid, 2 far of 36 failing)

Tokens needed, length sweep (re-tokenised truncations): detected in >= 90% of items from 50 tokens, >= 50% from 25 tokens; grade B in >= 90% from never within the sweep, >= 50% from never within the sweep.

Tokens needed, per item (the first k at which the first k tokens of the marked text, as detect tokenises it, reach p < 0.001 or grade B, from its per-position scores): p < 0.001 reached by 196 of 200, median 11 tokens, p90 27, max 53; grade B reached by 2 of 200, median 277.5 tokens, p90 296, max 296.

Identity misses: 10 (distinct bigram ratio mean 0.09, repetition rate mean 0.91; detected items 0.22 and 0.74).

Embed: KGW scores only the first 1024 tokens (at 75%: 0 of 36 failing items had none of the mark in that window, 0 only part of it; at 50%: 0 of 71 failing items had none of the mark in that window, 0 only part of it; at 25%: 0 of 166 failing items had none of the mark in that window, 36 only part of it; at 10%: 67 of 94 failing items had none of the mark in that window, 7 only part of it). For a miss with none of the mark in the window the 1024-token cap of the product, not dilution, explains it; with part of it, both; else dilution. Known product gap, not fixed: detect scores only the configured watermark.text.scheme and the default key_id, with one z-test over the whole text (its first max_detect_tokens tokens: 8192 for SynthID, 1024 for KGW) and no windowed scoring, so marked text inside longer human text is diluted; embed_25 measures that cost. Every KGW embed row also keeps the whole text's arrays (full_token_ids, full_kgw_green, full_kgw_pos) and its mark_span, so windowed scoring can be done offline.

Hints are a heuristic from the z margins at the first failing point, not a measurement: a length failure is too few tokens and an embed failure diluted evidence, by construction; otherwise mostly near (at least 50% of the failing items within half the z needed) suggests more evidence would do, and mostly mid or far that the edits break the n-grams the key hashes (a scheme change). Text has no oracle rows.

Validators

Laidline signs and reads with c2pa-python 0.37.10 (c2pa-rs 0.90.19, recorded in every evidence line as c2pa.c2pa_rs). The validator matrix checks the same signed files against other validator versions, because a verdict can change between versions:

Validator Versions Pinned
c2patool (c2pa-rs CLI, x86_64 Linux release) 0.27.22, 0.26.72, 0.25.0 (the current minor and the two before it) SHA-256 of each release tarball, checked on every run
c2pa-python 0.37.10 (the locked version, in process), 0.37.12 the exact version, read out of process

The corpus is the three golden inputs (PNG, JPEG, WebP) signed by a Marker with the fixture chain, in five cases: anchored (expected Trusted), unanchored (Valid), tampered (one bit flipped in the image data, Invalid), edit (a rendition with the signed asset as its ingredient) and, with --tsa, timestamped (a DigiCert RFC 3161 token). Two validators disagree when their states or their failure-code sets differ.

Run: 15 cases (5 cases by 3 formats, --tsa included) by 5 validators. Every validator agreed on every case but one: c2patool 0.25.0 reports the valid but untrusted (unanchored) manifest as Invalid in all three formats, where c2patool 0.26.72 and 0.27.22 and both c2pa-python versions report Valid; the failure codes agree (signingCredential.untrusted), so only 0.25.0's state mapping differs. The finding is recorded as a known disagreement (validator, case, state, note, date), and the matrix then reports 3 known disagreements and 0 new ones. A disagreement that does not match a known entry (another validator, case, state or code set) is new and fails the run.

Laidline reruns the matrix every week and fails only on a new disagreement, an error verdict, or an enforced case with an unexpected state. The detect-time counterpart is the optional secondary validator (detect.secondary_validator, Grading): a real c2patool 0.27.22 agreed with c2pa-rs on Laidline's signed files. Adobe's Verify site is not in the matrix.

Docker images

The CPU Dockerfile has a shared base stage (the extras image,audio,video,text,server,evidence,kms,ingest, gcs and the TrustMark Q, B and C weights) and two targets: cpu (the sidecar and ingest worker, the default) and lambda (awslambdaric, AWS Lambda). The CUDA Dockerfile is the GPU sidecar: the same layout as the cpu target with torch 2.14.0 and torchvision 0.29.0 from the PyTorch cu130 index (the wheels bundle the CUDA 13 runtime; the host needs only the NVIDIA driver and the NVIDIA container toolkit). No image carries the VideoSeal or AudioSeal weights: they download on first use into $LAIDLINE_HOME/models (SHA-256 pinned). Both images install the TrustMark Q decoder's ONNX graph (decoder_Q.fp32.onnx), a pinned weight of the Laidline mirror: stage it (sh deploy/docker/stage-models.sh, from ~/.cache/laidline/models/trustmark/ or the mirror) into the build's models/ directory before building. The build checks its size and SHA-256 and fails when it is missing or different; --build-arg ALLOW_TORCH_Q=1 builds without it, and that image's keyframe reader runs Q on torch. The sizes below predate the graph (94.8 MB). Measured with Docker 29.1.3 / Compose 2.40.3, linux/amd64 (docker image ls DISK USAGE and CONTENT SIZE; docker image inspect .Size is the compressed figure):

image Dockerfile, target on disk (uncompressed) compressed layers build
compose-laidline:latest Dockerfile.cpu, cpu 2.84 GB 724.3 MB with the audio and video extras
laidline-lambda:local Dockerfile.cpu, lambda 2.69 GB 688.2 MB before the audio and video extras
laidline-cuda:local Dockerfile.cuda 10.4 GB 3.55 GB with the audio and video extras

Before the extras the cpu image was 2.69 GB on disk and 687.7 MB compressed; the audio and video extras (AudioSeal, PyAV, einops, omegaconf) add about 150 MB on disk and 37 MB compressed. The cpu and lambda images share the base layers. Pushed to ECR for the Lambda live run, the deployment image (the lambda target plus the config and chain) was 687.9 MB compressed; the Lambda container image limit is 10 GB uncompressed. torch comes from the PyTorch CPU index (about 200 MB) in Dockerfile.cpu; in Dockerfile.cuda torch, the bundled CUDA 13 libraries (cuBLAS, cuDNN, NCCL and the rest) and triton make up most of the 10.4 GB. The size drives the Lambda cold start: the first mark after a deploy took 61 to 67 s while Lambda read the layers (above).

The GPU image was built and run with NVIDIA Container Toolkit 1.20.1 (nvidia-ctk --version; Docker's default runtime left at runc, the compose service reserves the GPU with deploy.resources.reservations) on an NVIDIA GeForce RTX 5080 (16,303 MiB, driver 616.92). Inside the container torch reports True NVIDIA GeForce RTX 5080 13.0 (CUDA available, device, CUDA runtime). Cold start of laidline-cuda (compose service, profile gpu), from docker compose up -d returning to the first /readyz 200: 10.2 s (11.7 s including up -d itself, which created the two volumes). /readyz does not wait for the video weights: readiness is the TrustMark self-test mark (TrustMark runs on CPU in this image too), and the VideoSeal (228 MB (218 MiB)) and AudioSeal (57 MB) weights download into the laidline-models volume on the first video job (below). To keep that download off the first job, pre-fill the volume with docker compose --profile gpu run --rm laidline-cuda models fetch (laidline models fetch outside Compose); Video has the whole GPU setup.

Video marking on the RTX 5080

A live check sends a 5 s 1280x720 clip (120 frames at 24 fps, H.264 with an AAC tone track, 3.0 MB; the marked mp4 is 1.5 MB) through the async job path of the laidline-cuda container (POST /v1/mark/video, poll GET /v1/jobs/{id} once a second, fetch the result's asset), with watermark.device: cuda, watermark.video.preset: fast, crf 18 and 32 frames per model call. On the host a CPU Marker with the same signing chain then validates the C2PA manifest (Trusted) and decodes the VideoSeal payload (issuer 4021) from all 120 frames. Measured, three runs on one container:

run evidence timings_ms (probe / watermark / mux / sign / total) client wall time (upload to download)
first job (downloads the weights, loads VideoSeal and AudioSeal on cuda, first CUDA calls) 7 / 21,187 / 1,857 / 6 / 23,063 23.8 s
second job (warm) 3 / 2,153 / 103 / 8 / 2,270 3.2 s
third job (warm) 4 / 2,219 / 88 / 6 / 2,320 3.2 s

watermark is the VideoSeal embed on the GPU interleaved with the libx264 encode (preset fast) of the marked frames, so a warm job marks the 120 frames of 720p in about 2.2 s (about 55 frames per second, faster than real time at 24 fps); the recorded PSNR was 51.1 dB. The client wall time adds the upload, the one-second polling interval and the download. nvidia-smi (the whole GPU, sampled every 0.5 s) showed the memory in use go from 1,338 MiB before the first job (the host's own use) to 3,347 MiB once VideoSeal was loaded and 4,419 to 4,472 MiB once AudioSeal had loaded and the first embed had run, where it stayed while the container ran (the PyTorch caching allocator keeps its blocks): about 3.1 GiB for the sidecar. What the mark survives is in "Video robustness" above; how to run the job path is in Video.

Reference

  • Robustness: why each mark fails, the root causes and the headroom a decode-side fix could recover.
  • Grading: how a decode becomes a grade, and the per-signal figures quoted from this page.
  • Keys: the keyed payloads measured in the keyed audio sections.
  • Configuration: the watermark.*, detect.* and evidence.* settings these runs used.
  • Licences: third-party model and corpus terms for the benchmark data.