Robustness baseline
Read this if you integrate or evaluate Laidline's watermarks and need to know which edits a mark survives, why the others break it, and what a decode-side fix could recover. You will get the measured survival rates per medium, the mechanism behind each failure and the headroom before anything has to be re-embedded. Skip if you want the full result tables (Benchmarks) or how a result becomes a grade (Grading). Time: 20 minutes for sections 1 to 3; the rest is reference. Needs: nothing;
pip install laidline-benchreruns the suites.
In short
- A mark survives compression, resizing, mild crops, tone edits and codecs; it does not survive large crops, rotation, screenshots, tempo or pitch changes, heavy video compression or a paraphrase.
- Every failure is traced to a mechanism (a whole-frame resize, a 20 ms bit grid, an equal-weight average, a global z-test), so several gaps are decode-side and can be closed without re-marking existing assets (section 5 separates them from the ones that need a new embedding).
- Rates come with Wilson 95 percent intervals; read small samples (music n=30, video n=43) as coarse.
- Oracle rows are not product numbers: they estimate what a perfect resync could recover.
- Only benign edits are in scope; adversarial removal is not.
What each Laidline watermark survives today, why the failures happen, and how much a decode-side fix could recover
before anything is re-embedded. The numbers come from the suites in laidline-bench (laidline bench --help). The
full per-medium tables are in Benchmarks. Benign edits only: adversarial removal is out of scope.
New images are marked with PixelSeal by default
(watermark.image.scheme: pixelseal); the image numbers below are TrustMark's, the scheme PixelSeal replaced. What the
PixelSeal scheme survives with the product's decoder, its unmarked-photo test, the analytic false-accept bound, the
real-screenshot measurement and its timings are in Grading, "PixelSeal image scheme" (module
laidline_bench.pixelseal_product).
- How to read the numbers
- How each pipeline works
- Cross-media matrix
- Root causes
- Headroom and the hardening items
- Corrections and caveats recorded
1. How to read the numbers
The runs
| medium | scheme | corpus and n | device | package in compliance/data/ |
|---|---|---|---|---|
| image | TrustMark 0.9.2, BCH_5, 61-bit payload; Q, B and C each forced | COCO val2017, 500 photos (seed 0); after the 38 dB PSNR floor Q n=493, B n=484, C n=438; sweeps on the first 100 (Q 98, B 98, C 87) | cuda | robustness.json |
| image, unmarked | detect's Q/B/C chain (TrustMark), earlier run | COCO val2017, 5,000 unmarked photos | cuda | phantom.json |
| unmarked, all media (2026-10 null campaign) | the shipped defaults: PixelSeal default reads and keyed, TrustMark, VideoSeal unkeyed and keyed, keyframe TrustMark, AudioSeal unkeyed and keyed, SynthID-style and KGW text | 38,500 photos, 4,854 clips of 5 s, 5,000 each of single utterances, 30 s and 120 s joins, 4,979 music clips, 20,000 documents per scheme; blind positives of issuer 1 | cuda for image, video and audio; text on cpu | none: published in benchmarks |
| audio | AudioSeal 0.2.0, 16-bit issuer payload; batch and streaming (block) |
LibriSpeech test-clean speech n=500, Goldberg music n=30 (seed 0); sweeps on the first 100 speech clips and the 30 music clips; Twilio: 100 recorded calls | cuda | audio_robustness.json |
| video | VideoSeal 1.0 @870ca7f (92-bit payload, BCH t=16); keyframe TrustMark (one IDR frame per 1 s) | 43 Xiph derf clips of 5 s (keyframes: 2 skipped, so n=41 marked); sweeps on all clips | VideoSeal cuda:0, keyframes cpu | video_robustness.json |
| text | SynthID (temperature 1.0), KGW (temperature 1.0), KGW (greedy) | Qwen/Qwen2.5-0.5B-Instruct, 200 C4 realnewslike prompts per arm (50-token prefix, up to 300 new tokens), 500 human C4 continuations; sweeps on the first 100 prompts | cuda | text_robustness.json |
- Every run: RTX 5080, torch 2.14.0+cu130, TF32 off for matmul and cuDNN, cuDNN deterministic, cuDNN benchmark off
(the
machineblock of each result; the product's default,watermark.cuda_precision: strict), with two exceptions: the video keyframe scheme ran on the CPU, and the phantom run'smachineblock does not record the TF32 and cuDNN flags (the bench turns TF32 off on cuda). - The per-item records and the decoders' soft outputs (
*.arrays.npz) are kept with the original runs and are not part of the package; with them soft voting, windows, offsets and thresholds can be evaluated offline without a re-run, and only a new embedding needs a new run. The packages incompliance/data/ship the rates, the sweeps and "Where it breaks", not the per-item data.
Rates, intervals and small samples
Every rate comes with its Wilson 95% interval. With small n the interval matters more than the rate:
| n | all items pass | reads as at least | where |
|---|---|---|---|
| 493 | 100.0% | 99.2% | image main rows, variant Q |
| 500 | 100.0% | 99.2% | audio speech main rows |
| 200 | 100.0% | 98.1% | text main rows |
| 100 | 100.0% | 96.3% | audio speech and text sweeps |
| 98 | 100.0% | 96.2% | image sweeps, Q and B |
| 43 | 100.0% | 91.8% | video, VideoSeal |
| 30 | 100.0% | 88.6% | audio music |
- Music n=30: a single clip is 3.3% of the corpus. Read music rows as coarse.
- Video n=43 (41 for keyframes): one clip moves a rate from 100.0% to 97.7%, and 0 of 43 unmarked clips bounds the false-positive rate only below 8.2%. n=43 is the robustness sample; the false-positive evidence is the 2026-10 null campaign's 4,854 unmarked clips (upper bound 0.079%).
- Text sweeps n=100 and image sweeps n=98: a sweep point locates a break, not a precise rate.
Operating points and false positives
| medium | what counts | false positives, 2026-10 null campaign (current) | earlier run, kept as a dated record |
|---|---|---|---|
| image, PixelSeal (the default) | the exact 61-bit payload from the default reads; keyed: a verified tag | presence 0 of 38,500 unmarked photos (Wilson upper bound 0.0100 %); keyed tag accepted 0 of 38,500 (0.0100 %); 0 of 2,200 screenshots of unmarked photos (0.174 %); no read of any configuration, TrustMark's included, presented one of the 40,700 photos and screenshots as marked (benchmarks) | 0 of 5,000 COCO val2017 photos (upper bound 0.077 %) |
| image, TrustMark | detect's Q/B/C chain returns the exact 61-bit payload; a registry hit also needs the query's bytes or a pHash within 10 bits of the evidence entry | no hits in the campaign set, as above | chain phantom rate on 5,000 unmarked photos: 8 of 5000, 0.16% (0.081% to 0.32%); per variant Q 1, B 5, C 2. This bench's own unmarked originals: 2 of 500 (0.4%, 0.11 to 1.4%), both from B |
| audio, unkeyed (16-bit mark) | public presence at the shipped threshold 0.5; the issuer is never named | single utterances 9 of 5,000 (0.18 %, upper bound 0.34 %), 30 s joins 4 of 5,000 (0.08 %, 0.21 %), 120 s joins 12 of 5,000 (0.24 %, 0.42 %), music 361 of 4,979 (7.25 %, 8.00 %), over the 0.5 % line; sounds 191 of 5,000 (3.82 %), synthetic speech 2 of 2,000 (0.10 %) (benchmarks) | 0.75% of n=530 unmarked clips at 0.39 (0.29 to 1.9%); 0.38% at 0.5 |
audio, keyed (audioseal-p70-r1) |
a verified tag; public presence under the streaming-card rule | tag accepted 0 of 5,000 per speech stratum and 0 of 4,979 music (upper bound 0.077 %), 0 of 26,979 in all; public presence 0 on speech, music 2 of 4,979 (0.15 %), sounds 15 of 5,000 (0.49 %) | |
| video, VideoSeal | the exact 92-bit payload after BCH (t=16) and the CRC-16 | presence 0 of 4,854 unmarked 5 s clips (upper bound 0.079 %); keyed tag accepted 0 of 4,854 (0.079 %); a uniformly random read passes with probability 2^-60.9 (benchmarks) | 0 of 43 unmarked clips (0 to 8.2%); 0 of 1,740 keyed (upper bound 0.22 %); a uniformly random read passes with probability 2^-64.4 (the earlier, optimistic figure) |
| video, keyframes | the majority payload of the TrustMark reads, keyed: a verified tag | tag accepted 0 of 4,854 (0.079 %) | 0 of 43 unmarked clips (0 to 8.2%) |
| text | p < 0.001 from one z-test over the text for presence; grade B at p < 1e-5 over at least 200 tokens (KGW: tokens covered by distinct n-grams, exact binomial tail) | grade B 0 of 40,000 reads (20,000 documents per scheme, upper bound 0.02 % per scheme); presence KGW 32 and SynthID-style 19 of 20,000; the gate was chosen on the same documents, so this is not a held-out result (benchmarks) | human C4: SynthID 1 of 500 (0.2%, z max 3.31), KGW 0 of 500; unmarked generations: SynthID 1 of 200 (0.5%, z max 3.54), KGW 0 of 200 in both arms. Guard: binomial bound on the 0.1% limit, PASS (section 6). At the earlier 1e-3 grade B gate the campaign's 20,000 documents per scheme gave 19 SynthID-style and 32 KGW |
Audio columns: "detected" is the detector score at or above the threshold; "exact" is detected with all 16 issuer
bits right, which is what detect needs to name the issuer. Image and video rates are exact decodes; text rates are
p < 0.001 unless the column says grade B.
Oracle rows
An oracle row applies the true inverse of an attack before decoding: the true rotation angle, the true crop position on a mid-grey canvas of the original size, the photo's true box on a screenshot, a trim rounded up to a whole 20 ms audio block, a video crop padded back at its true offset. They are not product numbers. They estimate what a perfect decode-side resync could recover, so an oracle far above its attack says "search at decode time", and an oracle near its attack says "the mark itself is gone; change the embedding".
- The crop oracles (image centre and corner crop, video crop) are not guaranteed ceilings. The grey canvas blanks the area the crop lost, while the plain crop keeps its content at a new scale (TrustMark and VideoSeal resize the whole frame), so an oracle can fall below its attack. The image bench flags such points; in this run there were none (an earlier small pilot had one, at corner crop 80). Treat crop oracles as estimates.
- Text has no oracle rows.
Sweeps and "Where it breaks"
Each sweep reruns one attack family over a parameter ladder on a seeded subsample, with the same metrics as the main rows. Its "Where it breaks" row reads:
| column | meaning |
|---|---|
| holds >= 90% up to | the harshest parameter, going from mild to harsh, at which the rate is still at least 90% |
| holds >= 50% up to | the same at 50% |
| oracle ceiling | the oracle curve at the same points, where one exists |
| margins at first failing point | how far the failing items missed (below) |
| hint | a heuristic from the margins and the oracle, never a measurement |
Margins, per medium:
| medium | decoded | near | mid | far |
|---|---|---|---|---|
| image (100 raw bits, BCH_5 t=5) | <= 5 bit errors | <= 10 | <= 25 | above 25 |
| video, VideoSeal (248 codeword bits, t=16) | <= 16 | <= 32 | <= 62 | above 62 |
| video, keyframes (100 bits, t=5; the averaged vote) | <= 5 | <= 10 | <= 25 | above 25 |
| audio (16 bits, no ECC) | 0 errors | 1 to 2 | 3 to 4 | 5 or more |
| text (z of the p < 0.001 test) | p < 0.001 | at least half the z needed | a quarter | below |
Hints: mostly near suggests soft voting or a stronger ECC; mostly mid or far with an oracle of at least 80% suggests a resync search; mostly far with a low or no oracle suggests an embedding change. Text uses "more evidence" (near) and "windowed scoring" (embed) instead.
2. How each pipeline works
Paths are inside the installed laidline package unless they start with trustmark/ or audioseal/ (the installed
trustmark 0.9.2 and audioseal 0.2.0). Line numbers refer to the source that was measured and can shift between
releases.
Image: TrustMark 0.9.2
| stage | what happens | where |
|---|---|---|
| payload | 61 bits, version:3 | issuer_id:22 | asset_seq:36, under BCH_5 (5 correctable errors); the 100-bit packet adds 35 parity bits and 4 version bits |
watermark/payload.py:1, :16 |
| embed | Q first, then B, then C, keeping the first variant above the 38 dB PSNR floor | watermark/image/trustmark.py:115-137 |
| where the mark lives | one 256x256 residual from the encoder, colour-shift removed, upsampled over the whole frame (or over the centre square when the aspect ratio is above 2) and added to the pixels | trustmark/trustmark.py:465-507, :201-241 (ASPECT_RATIO_LIM 2.0 at :70) |
| sign and log | fingerprint phash64/v1, C2PA manifest and signature, evidence entry |
core/api.py:594-707, c2pa/sign.py:133 |
| detect | decode_variant resizes the whole frame to 245x245, thresholds the 100 decoder outputs at 0 and runs BCH on the hard bits; decode tries Q, B and C in turn |
watermark/image/trustmark.py:138-157, :166-172; trustmark/trustmark.py:121 |
| schema bits | trustmark reads the schema from raw bits 98 and 99, which sit outside the BCH | trustmark/datalayer.py:64 |
| not used | TrustMark's ROTATION (a 0/90/180/270 search) and DETECTFIRST (a box localiser) |
trustmark/trustmark.py:401-448 |
| registry confirmation | a hit counts only when the query's bytes match, or its pHash (of the whole query) is within fingerprint_max_distance 10 |
detect/watermark_check.py:56-115, core/config.py:361 |
A recovered crop would still be fingerprinted whole, so it would likely land at grade C (the pHash distance of crops is not measured; in keyed mode that C is on the full tier only, the public tier shows D or nothing): hardening a crop decode also means changing confirmation.
Audio: AudioSeal 0.2.0
| stage | what happens | where |
|---|---|---|
| payload | a bare 16-bit issuer id: no version, no CRC, no ECC | watermark/payload.py:111 |
| batch embed | the mono mid-signal is resampled to 16 kHz, generator_base computes the residual there, the residual is resampled back and added at the native rate |
watermark/audio/audioseal.py:206-228 |
streaming embed (block) |
each 20 ms (320 samples at 16 kHz) block is marked on its own with generator_streaming and added a whole number of blocks later; below 16 kHz the stream passes unmarked |
watermark/audio/audioseal.py:261-364 |
| detect | mono at 16 kHz through detector_base, then detector_streaming; detected when the score reaches detect_threshold (default 0.5) |
watermark/audio/audioseal.py:230-254, core/config.py:191 |
| score | prob is the share of samples whose per-sample score is above 0.5 |
audioseal/models.py:413-418 |
| bits | the sign of the whole-clip mean of the per-sample bit logits, read against a 20 ms grid anchored at the first sample | audioseal/models.py:420-430; watermark/audio/audioseal.py:21-23 |
| loudness | AudioSeal's NormalizationProcessor is built only when a checkpoint's config asks for it; all four Laidline checkpoints load with normalizer=None |
audioseal/builder.py:135-136, :152-153 |
| limits | detect refuses audio above 120 s (max_detect_duration_s); C2PA is written for wav, flac, mp3 and m4a only |
core/config.py:190; media/audio.py:37 (EMBEDDABLE_AUDIO) |
Where the mark lives, from the filters (speech, 100 clips): the batch bits need the low band (high-pass 300 Hz leaves
88.0% exact, 500 Hz 1.0%, while detection stays at 100.0%) and survive a 4 kHz low-pass (99.0%). The streaming
bits need the band above 6 kHz (low-pass 8 kHz leaves 99.0% exact, 6 kHz 0%). Streaming detection survives the
bench's gentle two-pole filters (lowpass_4k and bandpass_phone: 100.0% detected, 0% exact) but not the brick-wall
8 kHz round trip or G.711 (0% detected), which is why a phone leg removes the unkeyed streaming mark. A keyed stream's payload reads through simulated G.711 (Audio).
Audio: the keyed P70 payload
In keyed mode the 16 bare bits above become audioseal-p70-r1 (Payload keys, "Audio"):
- 70 bits under a BCH code (m = 7, t = 9);
- spread over 1.0 s cells of an 8 s cycle;
- read per 1.0 s segment on 20 ms grids and decoded per word (Chase-II p = 8);
- one tag check per call.
That fixes two of the 16-bit failures in "Root causes" below. The whole-clip mean is replaced by per-segment reads with error correction. The grid problem is handled two ways: the default read (CPU) covers files whose start is intact on the 20 ms grid, and deep scanning (GPU, off by default) searches the phase for trims, offsets and excerpts. Under the hardened decode a span read also reads a mark inside a longer recording (measured at 9 s in 30 s, 12 and 24 s in 120 s) at the phase search's own coarse peaks (98, 93 through Opus 24 kbit/s, and 98 of 100 on fresh units; Grading). On 2 CPU threads it adds 0.02 s to a 120 s detect that reads at the first peak (the call is about 67 s, mostly the hardened decode) and about 2.9 s when all three phases are read. Measured on 200 fresh clips (Benchmarks, "Keyed audio payload"):
| read | result |
|---|---|
| default read | 100 % through every binding codec except Opus at 12 kbit/s, which read 97.3 % (post-stratified, PASS) |
| deep scanning | 200 of 200 on every binding row, trims and offsets included; Opus 12 kbit/s 94.0 % (best effort) |
There were 0 wrong payloads and 0 own-accepts on 1,591 null clips and 1.2 million decoder-level nulls. Lines not met:
- Third-party label on own marks (closed by the rule shipped, below): under the two-grid rule that ran, one own mark was labelled third-party at Opus 12 kbit/s under deep scanning, and the same one-word mechanism showed on two more own marks in the forced reads. The rule now shipped needs the foreign head on three or more grids (chosen after that data; 0 of 31 such calls, no power). The powered measurement (3,000 of Laidline's own marks under foreign keys, each read scored as if no own payload had been chosen) failed its line (7 or more labels fail): the three-grid rule labelled 13 of 3,000 (the two-grid rule 80, 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 rule now shipped also needs each counted word to score at least 0.97, a floor chosen after that data and tested once on a fresh 3,000 own marks: 0 of 3,000 labelled (rate shown below 1 per 1,000), 2,999 of 3,000 own heads and 150 of 150 foreign marks kept; under Opus 12 kbit/s the product labelled 129 of 150 foreign marks, reported as measured (Payload keys, "Audio"; Benchmarks, "Third-party rule S97 and its hold-out"). Measured at full float32 with deterministic cuDNN, the shipped CUDA precision.
- Timing: in the quiet re-time the default 3-grid reader met every CPU time line. Its 30 s miss in the first run was load. The 5-grid reader, which is not the default, missed at 12 s.
- Timing (report only): one file-registry cell of the keyed audio timing check is unexplained and indicative. The measured channel stays within the bound stated in Payload keys residual 10 on every backend measured; the mitigations are the equal-work construction and the registry recommendation (the file registry is the development backend; production uses Postgres or S3).
- Stream against file: the stream path read 3 more than the file path at Opus 12 kbit/s. The line failed by its letter; it was accepted after the data.
- The 89-fresh-clip check of Opus 12 kbit/s on whole files is UNPROVEN at that size.
- GPU against CPU, as shipped (rules pre-registered): Laidline runs its models on a GPU at full float32 with deterministic cuDNN
(
watermark.cuda_precision: strict, the default; Configuration). Withdecode: hardenedand the keyed constants on both, the same 350 saved clips were read by the product on the GPU (the precision set by the product itself, as shipped underwatermark.device: auto) and on the CPU. The clips are deliberately hard: short marks inside longer clips, codec cuts and unmarked clips that came near a threshold; they are not population rates. The line, 0 units differing in a decision (presence on either tier, payload, the chosen payload, grade, status, tag, third-party label, deep-scan stage and payload read), passed: 0 of 350.- The path that reported presence differed on 1 unit, a 0.5 s mark inside a 30 s clip: both devices showed it present, the CPU on the windowed path and the GPU on the located path. The two devices read it at the same phase, but they disagree on the clip's last seconds, where the mark is not: AudioSeal's detector carries a recurrent (LSTM) layer through the clip, and the tiny arithmetic differences between the CPU's and the GPU's float32 kernels grow along it: on this clip, from about 1e-9 in a frame's score in the first second to a few hundredths from 18 s on, and up to 0.76 in the last 5 s. Scores that drift across 0.5 there change the shares of windows that hold no mark: the best 1 s window on both devices lies there, at 0.81 of its samples on the CPU (over the 0.70 threshold: windowed) and 0.64 on the GPU (under it, so the GPU reported the 0.5 s window on the mark itself: located), and the CPU's best 0.5 s window, which centres the cell statistic, moved there too.
- The cell statistic agreed within 1e-6 wherever both devices searched the same phase and cell span (226 of 256 compared). The phase or the span moved on the other 30, and 28 of those differed by more than 1e-4 (26 unmarked clips, 2 excerpts); none changed the cell gate's admission.
- Before this setting, the GPU ran torch's default reduced-precision (TF32) convolutions. The same 350 clips measured that way: 9 differed, 4 of them in presence (3 short excerpts; 1 unmarked 30 s clip shown present on the GPU's full tier) and 1 on the public tier, 0 in payload, tag, grade B or third-party label. The decode compares shares of samples scored above 0.5, and a gate statistic, with fixed thresholds; where a share or the statistic sits near its threshold, a small arithmetic change decides it the other way.
- Every published GPU figure for keyed audio detection (the cell gate's floor and hold-out counts, the excerpt rows, the third-party hold-out) was measured at full float32 with deterministic cuDNN, which is now what the product runs. The GPU records of this line are identical, field for field, to a full-precision run made with the bench's own switch (350 of 350), and the CPU records to that run's CPU records (350 of 350).
- Full float32 does not make the two devices bit-equal on long clips (the recurrent layer above): GPU and CPU agree on decisions on these 350 clips, not on every statistic.
Audio: voice calls (the 2026-10 matrix)
The keyed buffered stream, simulated phone paths, read with the shipped defaults of 2026-10-27 (rows and method in Benchmarks: Voice calls; the rows a voice bot can rely on in Audio). What limits the payload, in the order it was found:
- Length. Presence reads from 1.5 s delivered; the payload from 8.0 s delivered on the 20 ms grid (a stream fed 10 s delivers 9.5 s, since 0.5 s is held back at close). Claim 10 s.
- Alignment. The default read counts 320-sample blocks from the first sample. An excerpt starting 1 to 319 samples off
the grid read 6 of 100 (16 kHz) and 4 of 100 (24 kHz) against 100 of 100 on the grid; a pure delay of 2 to 480 samples
that is not a whole block read 0 of 50. Presence survives (77 to 81 %). This is what fails G.722 (22-sample delay) and
AMR-WB (95 samples), which read 50 of 50 and 48 of 50 once the delay is removed. The keyless phase search added after the
run (Payload keys, "Audio") targets it; it is off by default (
payload.phase_search) and was measured in the v2 matrix (Audio, "Voice calls"). - Codecs that destroy the mark. Opus at 8 kbit/s (4 of 50 even aligned), Speex (0 of 50) and AMR-NB (28 of 50 aligned). The mark's residual sits under 4 kHz, 23.5 dB under the speech; bandwidth is not the cause.
- Noise. White noise loses half the payloads at about 22 dB SNR; babble reads 94 % at 20 dB. Packet loss up to 10 % (repeat or zero fill) costs nothing measurable (100 of 100 per cell).
- Rooms. At 0 dB direct-to-reverb ratio the payload is usually lost (3 to 13 % at RT60 0.1 to 0.5 s) and presence stays at 83 to 94 %; +6 dB reads 67 % and +12 dB 100 % at RT60 0.3 s. Reverb followed by G.711 reads 3 %.
- Wrong payloads, not met. The line "0 wrong payloads" failed on 6 of 6,200 marked units (reverb, white noise at 20 dB, G.722), each with a failed tag and a payload of another issuer. No tier now shows a payload whose tag failed.
Video: VideoSeal 1.0 and the keyframe stopgap
| stage | what happens | where |
|---|---|---|
| payload | 92 bits, version:4 | issuer:24 | asset_seq:48 | CRC-16, under BCH(255, t=16) over 248 bits, padded to VideoSeal's 256-bit message |
watermark/video_ecc.py:1-5, :18 |
| embed | VideoSeal predicts the watermark on every 4th frame (at 256x256, from luma) and carries it to the next 3; the residual is JND-attenuated (it hides in texture) and added at full resolution; libx264 crf 18 in mp4 | watermark/video/videoseal.py:69-85, core/video.py:212-313 |
| audio track | marked with the batch AudioSeal mark; never decoded at detect | core/video.py:247-257, :316-328 |
| detect | up to 256 frames on a stride that is a multiple of 4, each resized to 256x256; the per-frame message logits are averaged with equal weight, then BCH and the CRC run | core/video.py:656-712, watermark/video/videoseal.py:111-134 |
| discarded | the per-frame detection channel preds[:, 0]; there is no windowing or segment vote |
watermark/video/videoseal.py:118 |
| keyframes | TrustMark Q on one forced IDR frame per second; detect reads the key frames plus the marking positions and takes a hard majority vote of whole payloads (keyed mode: the keyed layout uses Q only to embed and read, through the hardened Q-only reader) | watermark/video/keyframes.py:42-56, core/video.py:715-724 (decode_keyframes), :737 (_keyframe_candidates) |
Text: statistical mark at generation time
| stage | what happens | where |
|---|---|---|
| schemes | SynthID tournament (ngram 5, depth 30) by default; KGW (green-list ratio 0.25, bias 2.0, context width 1, so bigrams) for greedy traffic; keys by HKDF-SHA256 | core/config.py:160, watermark/text/keys.py:1-19 |
| where marking happens | during sampling: the vLLM logits processor (laidline-vllm) or generate_marked (laidline-transformers); the proxy only injects the key and records effectiveness |
server/proxy/text.py, core/api.py:857-927 |
| where the mark lives | in which tokens were chosen; an edit that changes a token also changes the context of the next n-gram | |
| detect | one z-test over the first max_detect_tokens tokens (8192; KGW at most 1024); present at p < 0.001 |
core/api.py:1022-1067, core/config.py:150-155, :170-172 |
| KGW scoring | each distinct n-gram counts once (repeated n-grams are skipped) | watermark/text/kgw.py:92-126 |
| grades | present over at least 200 tokens is grade B (SynthID: the tokens, score.tokens; KGW: scored + context_width), else grade D |
detect/text_check.py:26-64 |
| scope | only the configured scheme and the default key are scored; there is no windowed scoring | core/api.py:1051-1060 |
3. Cross-media matrix
Headline rates from the main rows. Audio cells read "detected / exact" at 0.39 when the two differ. A second value in a cell is the other mode or scheme named in the column header, where it differs. n/a: not applicable or not measured.
| attack family | image, Q (n=493) | audio, speech batch (n=500); streaming | video, VideoSeal (n=43); keyframes (n=41) | text, SynthID t=1 (n=200); KGW |
|---|---|---|---|---|
| compression | JPEG q50 96.8%; WebP q50 93.9%; JPEG q75 twice 98.8%; 256-colour palette 99.4% | Opus 12 kbps 99.6% / 96.2%; G.711 mu-law 100.0%; MP3 64 100.0%; AAC 64 100.0%. Streaming: G.711 mu-law 0%; Opus 12 100.0% / 70.6% | H.264 crf 23 93.0%, crf 28 53.5%, crf 35 14.0%; H.265 crf 28 27.9%; VP9 crf 33 83.7%; 1 Mbit/s cap 90.7%. Keyframes: crf 28 78.0% | n/a (Unicode and whitespace normalisation: 100.0%; KGW 99.5%) |
| geometric | rotate +2° 90.1%, -2° 93.1%; +5° 1.2%, -5° 2.6%; 90° 0%; centre crop 90% 100.0%, 75% 31.8%, 50% 0%; corner crop 75% 0%; resize 0.5 100.0%; aspect 0.8 93.9%; horizontal flip 100.0% | tempo 0.9 1.4% / 0.2%, 1.1 2.0% / 0.6%; pitch +2 semitones 0.4% / 0%; speed 1.1 1.2% / 0%. Streaming: tempo 0.9 99.8% / 90.4%; pitch +2 100.0% / 0% | rotate 5° 93.0%, 90° 95.3%; hflip 93.0%; centre crop 80% 62.8%; corner crop 80% 41.9%; rescale 0.5 62.8%. Keyframes: rotate 5° and 90° 0%; crop 80% 92.7%; rescale 0.5 90.2% | n/a |
| framing and overlay | 10% black border 0%; caption bar 99.6%; logo 100.0% | n/a | letterbox 34.9%; caption box 95.3%. Keyframes: letterbox 0%; caption box 82.9% | n/a |
| tone and colour (audio: level and EQ) | brightness x1.2 95.7%; contrast x0.7 100.0%; saturation 0 94.5%; gamma 1.5 99.8% | volume -12 dB 100.0% / 99.8%; +6 dB clipped 100.0%; compressor 100.0%; phone band 100.0%; low-pass 4 kHz 100.0% / 98.8%. Streaming: phone band and low-pass 4 kHz 100.0% / 0% | brightness and contrast 97.7%; greyscale 100.0%. Keyframes: 85.4% and 85.4% | n/a |
| noise, blur and local edits | noise sigma 5 100.0%; blur radius 2 99.6%; sharpen 100.0% | white noise 20 dB SNR 76.6% / 57.6%; reverb 100.0% / 5.8%. Streaming: noise 100.0% / 99.2% | n/a | delete 30% of words 97.0%; swap 30% 94.0%; insert 10% 100.0%; typos 5% 100.0%. KGW: 99.5%, 99.5%, 100.0%, 99.0% |
| temporal and offset | n/a | trim 10 ms 100.0% / 0%; trim 1 s 100.0%; middle half kept 54.4% / 5.8%. Streaming: trim 10 ms 48.4% / 0% | start trim 1.3 s 90.7%; middle 2 s 90.7%; every 10th frame dropped 97.7%; 15 fps 97.7%; speed x1.25 95.3%. Keyframes: both trims 0%; 15 fps 78.0% | first 100 tokens kept 100.0% (grade B 0%); first 50 99.5%. KGW: 100.0%, 96.5% |
| splice and mix | n/a | marked half joined to an unmarked half 100.0% / 99.6% (88.2% detected at 0.5); music bed 10 dB below 41.8% / 11.6%. Streaming: splice 100.0% (2.6% at 0.5); music bed 100.0% / 100.0% | first half of the frames marked 97.7%. Keyframes: 95.1% | mark as 25% of a human text 100.0%. KGW t=1 98.0%; KGW greedy 17.0% |
| neural re-generation | img2img not run | EnCodec 6 kbps 70.8% / 0%; Mimi 1.0% / 0%. Streaming: EnCodec 100.0% / 0% | n/a | paraphrase by the same model 49.5%; EN-DE-EN translation 89.5%. KGW: 57.0%, 97.5% |
| capture | screenshot (90% of a 1080p screen) 0% | real Twilio call (n=100) 99.0% / 98.0% | screen recording 69.8%. Keyframes: 0% | n/a |
| unmarked (false positives), earlier runs; the 2026-10 null campaign's are in "Operating points and false positives" | TrustMark chain 0.16% (n=5000); PixelSeal 0 of 38,500 in the campaign | 0.75% at 0.39, 0.38% at 0.5 (n=530); unkeyed 0.18%, 0.08%, 0.24% (single, 30 s, 120 s) and music 7.25% in the campaign | 0 of 43; 0 of 4,854 in the campaign | human text 0.2% (1 of 500); KGW 0%; grade B 0 of 40,000 in the campaign |
The KGW greedy arm (a small model decoding greedily) detects 95.0% at identity but reaches grade B on 1.0%: see section 4.
4. Root causes
Each failure traced to its mechanism, with the sweep that supports it. Rates are exact decodes (audio: issuer exact) unless marked "detected".
Image: the whole-frame resize and the hard BCH
| symptom | mechanism | evidence (sweeps: Q n=98) |
|---|---|---|
| a small rotation is lost | detect decodes the frame as it is; there is no rotation search, and the pattern turned by a few degrees misaligns with the decoder's grid | rotation holds 90.8% at 2°, 42.9% at 3°, 15.3% at 4°, 1.0% at 5°, 0% from 7°; the oracle (rotate back by the true angle) is 100.0% at every angle up to 15° |
| a quarter turn is lost | same; TrustMark's own ROTATION flag would try exactly these |
90°, 180°, 270°: 0%; oracle 100.0% |
| a centre crop is lost from 75% | the embed stretches one 256x256 pattern over the whole frame and detect squeezes the whole frame back to 245x245, so a crop is a rescale plus a shift of the pattern | holds 100.0% at 80%; 35.7% at 75%; 0% from 70%. Oracle (the crop pasted back at its true position): 100.0% at 75%, 92.9% at 70%, 62.2% at 65%, 17.3% at 60%, 0% at 50% |
| a 50% crop is lost even by the oracle | half of each side leaves too little of the one pattern for BCH_5, wherever it is placed | oracle_crop_50 0% (n=493) although its raw bits rise from 50.6% to 82.1% |
| uniform scaling is fine | the whole-frame resize makes the decoder scale-invariant | scale 0.25 98.0%, 3 100.0% |
| a screenshot or a border is lost | the photo fills only part of the frame the decoder squeezes, so the pattern shrinks and shifts | screenshot fill from 90% to 30% of the screen: 0%; oracle (the photo's box cut out): 100.0% down to 45%, 96.9% at 30%. 10% border (pad_10): 0% |
| non-uniform squash breaks late | a different scale per axis | aspect 0.8 94.9%, 0.7 62.2% (37 far of 37 failing; the bench's hint says embedding change, but there is no oracle, and a decode-side aspect or scale search would undo a squash: a resync/search candidate, unmeasured) |
| compression and blur fail near the BCH radius | hard decision on the 100 bits; 5 errors corrected, the sixth fails | JPEG holds 92.9% at q35, 77.6% at q25 (14 near, 7 mid, 1 far of 22 failing), 31.6% at q15; blur radius 4 76.5% |
| some near misses are schema misses | raw bits 98 and 99 pick TrustMark's schema and are outside the BCH, so one flipped schema bit fails a decode with at most 5 errors | Q: JPEG at q25 (2), blur radius 4 (3), rotation at 3° (4); C: centre crop at 75% (8) |
B and C tolerate rotation better than Q (at 5°: B 40.8%, C 28.7%; at 4°: B 81.6%, C 55.2%) and C tolerates the 75% crop better (74.7%). Horizontal flip survives on every variant (Q 100.0%); vertical flip is not a bench row, and a development probe found it does not decode.
Audio: the 20 ms bit grid and whole-clip averaging
| symptom | mechanism | evidence (speech, batch, n=100) |
|---|---|---|
| any trim that is not a whole number of 20 ms blocks loses the issuer | the 16 bits are read on a 20 ms grid anchored at the first sample; out of phase, bits flip while the score mostly survives | start offset 0 ms and 20 ms: 100.0% exact; 1, 2, 5, 8, 10, 12, 15, 18, 30 and 50 ms: 0% exact. Detection stays 100.0% at 1, 2, 10, 20, 30 and 50 ms but drops to 53.0% at 5 ms, 37.0% at 8 ms and 61.0% at 12 ms. Oracle (trim rounded up to a whole block): 100.0% at every offset |
| the offset misses are close | the phase error flips a few bits, not all | at 1 ms: 59 near, 41 mid of 100 failing (streaming: 100 far) |
| the main rows | the same grid | trim_10ms 100.0% detected, 0% exact; oracle_trim_10ms 100.0% exact; middle half kept (cut off the grid) 54.4% detected, 5.8% exact |
| a short marked share is not detected | prob is the share of marked samples over the whole clip, and the bits are a whole-clip mean, so unmarked audio dilutes both |
marked share 50%: 99.0% exact; 25%: 2.0% detected, 1.0% exact. Kept duration 0.5 s: 100.0% (a short clip is fine when it is all marked) |
| mixing costs the issuer before the score | likely: the bed shifts the whole-clip bit means sooner than it pulls the per-sample score below 0.5 | music bed 20 dB below: 99.0% detected, 79.0% exact; 15 dB: 84.0% / 40.0%; 10 dB: 37.0% / 9.0% |
| a 1% tempo change breaks the batch mark | likely: the batch message rests on one alignment held across the whole clip, and a tempo change drifts it; the saved 100 ms time profiles can confirm it | tempo 0.99: 18.0% detected, 17.0% exact; 0.98: 8.0% / 6.0%; 1.01: 19.0% / 18.0%. The streaming mark (each block marked on its own) holds 94.0% to 97.0% exact from 0.95 to 1.05 |
| pitch and speed are lost | resampling moves the content the detector reads; no oracle exists | pitch -0.5 and +0.5 semitones: 1.0% and 0% detected; speed 0.98: 1.0% |
| near misses under noise and codecs | 16 bare bits, no ECC, no redundancy | noise 25 dB: 89.0% (11 near of 11 failing); Opus 10 kbps: 72.0% (9 near, 1 mid); low-pass 3 kHz: 63.0% (37 near); high-pass 300 Hz: 88.0% (12 near) |
| quiet audio loses the issuer | no loudness normalisation before the detector | volume -12 dB 100.0%; -20 dB 90.0% detected, 56.0% exact (40 near of 41 failing); -30 dB 3.0% detected |
| the unkeyed streaming mark dies on a phone leg | its bits sit above 6 kHz; gentle two-pole filters keep detection (lowpass_4k, bandpass_phone: 100.0% detected, 0% exact), a brick-wall 8 kHz resample does not |
low-pass 8 kHz 99.0% exact, 6 kHz 0%; G.711 and the 8 kHz round trip 0% detected (n=500); keyed, the payload reads through simulated G.711 (Audio) |
Music (n=30) repeats the batch pattern with a weaker issuer read: 83.3% exact with no change, 23.3% to 40.0% after Opus 10 to 24 kbps (mostly near: 22 near of 22 failing at 24 kbps), and no clip exact after a 1% tempo change.
Video: the equal-weight average and the BCH radius
| symptom | mechanism | evidence (sweeps: n=43) |
|---|---|---|
| compression past crf 23 | frames are averaged with equal weight, then 248 hard bits go to BCH with t=16; heavier compression pushes the averaged read past the radius | H.264 crf 18 100.0%, 20 97.7%, 23 93.0%, 26 62.8%, 28 53.5%, 30 32.6%, 32 18.6%, 35 14.0%, 40 4.7% |
| the first failures are near the radius | the signal is still there; a soft decoder or a stronger code would take them | crf 26: 13 near, 4 mid of 17 failing; crf 23: 3 near of 3; VP9 crf 33: 7 near of 7; social re-encode: 11 near, 5 mid of 16 |
| heavy compression removes the mark | mostly far misses: likely beyond a decode-side fix | crf 35: 2 near, 8 mid, 27 far of 37 failing; H.265 crf 28: 17 far of 31 |
| more frames help | averaging gains over a quarter of the frames, so frame selection and weighting matter | exact on the first 25% of frames against all frames: crop 80% 34.9% against 62.8%; screen recording 51.2% against 69.8%; crf 28 48.8% against 53.5% |
| crops, bars and screen capture | the whole frame is resized to 256x256, as in TrustMark | centre crop holds 93.0% at 90%, 86.0% at 85%, 62.8% at 80%, 7.0% at 70%; oracle 93.0% at 80%, 76.7% at 70%, 23.3% at 60%. Letterbox 34.9%, screen recording 69.8% |
| downscaling | likely: fewer pixels carry each part of the pattern | scale 0.6 90.7%, 0.5 62.8%, 0.33 18.6% |
| rotation and flips survive | likely VideoSeal's training with geometric augmentations; the TrustMark keyframes need the pattern upright | rotate 5° 93.0%, 10° 81.4%; 90° 95.3%; hflip 93.0%. Keyframes: 0% at 5° and 90° |
| keyframe trims | the keyframe scheme reads fixed positions on a 1 s grid and votes whole payloads; a cut off the grid reads unmarked frames | start trim 0.1, 0.3, 0.6, 1.5, 2.5 s: 0%; 1 s: 95.1%. Kept segments decode only when they start on a whole second (3 s: 95.1%, 1 s: 92.7%; 4, 2 and 0.5 s: 0% or 2.4%). 10 fps: 0% |
VideoSeal margin counts (near, mid, far) can differ from the exact-decode rates by 1 to 2 clips: the 248-bit margin also counts the 28 zero-padding bits, which the decoder restores before BCH. The same holds for the VideoSeal margins in section 5.
VideoSeal splices hold (25% of the frames marked: 95.3%), so partial edits are not the weakness; compression and framing are. The keyframe scheme is more robust to compression and scale (crf 28 78.0%, scale 0.5 90.2%) but fails every edit that moves its grid.
Text: the global z-test and scheme-only scoring
| symptom | mechanism | evidence |
|---|---|---|
| marked text inside human text is diluted | one z-test over the whole scored window, no windows | SynthID (window 8192 tokens holds the whole mark in 500 of 500 embed rows): embed share 10% still 100.0%. KGW t=1: 99.0% at 25%, 32.0% at 10%. KGW greedy: 64.0% at 75%, 29.0% at 50%, 16.0% at 25% (sweeps n=100) |
| KGW embed misses at 10% are the cap, not dilution | KGW scores only the first 1024 tokens | KGW t=1 at 10%: 65 of 68 failing items had none of the mark inside the window, 3 part of it; at 25%: 0 of 4 |
| a text marked under another scheme or key is not scored | detect scores only watermark.text.scheme with the default key |
known gap; no row measures it |
| short texts detect but do not grade | p < 0.001 needs few tokens, grade B needs 200 | SynthID: p < 0.001 reached by 200 of 200, median 9 tokens, p90 15; length 25 tokens 98.0% detected; grade B 98.0% at 200 tokens, 0% at 150. KGW t=1: median 13 tokens, p90 36; 94.0% at 50 tokens, 72.0% at 25; grade B 100.0% at 300 kept tokens, 0% at 200 |
| greedy KGW grades D | greedy decoding on a 0.5B model repeats itself, and KGW scores each distinct n-gram once, so a 300-token text has few scored tokens | identity 95.0% detected, grade B 1.0%; scored mean 63 of 300 tokens; the 10 identity misses have distinct-bigram ratio 0.09 and repetition rate 0.91 (detected items 0.22 and 0.74) |
| heavy edits break the hashed n-grams | an edit changes the context of the n-gram that follows; SynthID hashes 5-grams, KGW (context width 1) bigrams | SynthID: delete 30% 97.0%, 40% 73.0%, 50% 30.0%; swap 30% 95.0%, 40% 38.0%; typos 10% 53.0%. KGW t=1: delete 50% 98.0%; swap 50% 79.0%; typos 10% 97.0%. Greedy KGW fails from 5% of words (delete 89.0%, insert 82.0%, swap 81.0%) |
| paraphrase | a rewrite keeps few n-grams | SynthID 49.5%, KGW t=1 57.0%, KGW greedy 30.5% |
5. Headroom and the hardening items
Image: the targets (variant Q)
| target | now (n=493) | oracle, main row | oracle, sweep (n=98) | decode-side search can reach it? | fix |
|---|---|---|---|---|---|
| centre crop 50% >= 80% | 0% | oracle_crop_50 0% |
0% at 50%; 62.2% at 65%; 92.9% at 70%; 100.0% at 75% | the oracle estimate suggests search alone cannot reach it (it recovers 70 to 75%, not 50%; the crop oracle is not a guaranteed ceiling) | tiled embedding (new embedding), plus multi-scale crop search and fingerprint confirmation for crops |
| rotation ±5° >= 90% | 1.2% (+5°), 2.6% (-5°) | oracle_rotate_+5 100.0% |
100.0% at every angle up to 15° | yes | rotation search, including 90° steps |
| screenshot >= 80% | 0% | oracle_screenshot 100.0% |
100.0% down to 45% fill, 96.9% at 30% | yes | canvas detection |
| compression unchanged | JPEG q50 96.8%, WebP q50 93.9% | re-run after every change | |||
| chain FPR <= 0.5% | 0.16% (0.081% to 0.32%) | fingerprint confirmation and a phantom re-run for every added search |
Every extra decode attempt is another chance of a phantom. Each search is paired with fingerprint confirmation and a phantom re-run; the saved unmarked soft outputs (500 originals in the image run, 5,000 photos in the phantom run, 100 logits per decoder each) let a search's false-positive cost be estimated offline before it ships.
The hardening items, mapped to the evidence
Decode-side items change only detect: cheap, and they help assets that are already marked. Embedding items need a new mark: existing assets keep today's robustness, and every bench reruns.
| item | medium | evidence | kind |
|---|---|---|---|
| rotation search (small angles) | image | rotation Q at 3°: "resync/search candidate" (22 near, 31 mid, 3 far of 56 failing; oracle 100.0%) | decode-side |
| quarter-turn search | image | quarter turns: 98 far of 98 failing, oracle 100.0% | decode-side (TrustMark's ROTATION does it) |
| crop and scale search | image, video | image centre crop Q at 75%: "resync/search candidate" (13 near, 35 mid, 15 far of 63; oracle 100.0%); video crop 80%: 62.8% against oracle 93.0% | decode-side |
| tiling | image | crop 50%: oracle 0% (raw bits 82.1%) | new embedding |
| aspect or scale search | image | aspect 0.7: 62.2%, 37 far of 37, no oracle | decode-side, unmeasured (a resync/search candidate) |
| canvas detection | image; video (hypothesis) | image screenshot fill: 98 far of 98, oracle 100.0%; pad_10 0%. Video letterbox 34.9% and screen recording 69.8% have no oracle and mixed margins: a hypothesis to test with an oracle row first |
decode-side (TrustMark's DETECTFIRST localiser is one option) |
| soft voting across variants and tiles, schema retry | image | JPEG Q at q25: "soft voting or stronger ECC candidate" (14 near of 22) plus 2 schema misses; "a fixed-schema decode fixes them" (Laidline accepts only BCH_5, so the schema bits can be ignored) | decode-side |
| fingerprint confirmation for crops | image, video | detect fingerprints the whole query, so a recovered crop lands at grade C | decode-side (detect) |
| audio offset search | audio | offset 1 ms: 59 near, 41 mid of 100; oracle 100.0% at every offset; streaming: 100 far, oracle 100.0% | decode-side (a search over the 320 sample phases of a block) |
| audio windowed detection | audio | marked share 25%: 2.0% detected; the saved 100 ms time profiles allow it offline | decode-side |
| loudness normalisation before decode | audio | volume -20 dB: 56.0% exact (40 near of 41); -30 dB: 3.0% detected | decode-side |
| audio redundancy and a sync pattern | audio | tempo 0.99: 17.0% exact; pitch ±0.5 semitones and speed 0.98: at most 1.0% detected; no oracle | new embedding |
| streaming mark in the phone band | audio | G.711 0%; low-pass 6 kHz 0% exact | new embedding |
| temporal weighting and voting | video | more frames help (the first 25% of frames decodes less than all frames: crop 80% 34.9% against 62.8%); per-frame logits and preds[:, 0] are saved for it |
decode-side |
| soft-decision BCH or a stronger code | video | crf 26: 13 near, 4 mid of 17 failing | decode-side for soft decoding; a stronger code is a new embedding |
| embedding strength | video | crf 35: 27 far of 37 | new embedding |
| keyframe window scan | video, keyframes | start trim 0.1 s: 41 far of 41; decodes again at a whole-second trim (95.1%) | decode-side (read every frame near each second) |
| windowed scoring | text | KGW t=1 embed 10%: "windowed scoring candidate" (6 near, 9 mid, 53 far of 68), 65 of 68 outside the 1024-token window; KGW greedy embed 75%: 24 near, 10 mid, 2 far of 36 | decode-side |
| scoring every scheme and active key | text | known gap | decode-side |
Proposed targets where none is set
Proposed targets, each grounded in the curve it names. Every one keeps its medium's false-positive target: audio below 1.0% at the operating point (not met for unkeyed music, 7.25%, in the 2026-10 null campaign), video 0 mis-decodes and 0 unmarked decodes, text the binomial guard on 0.1% for presence and a grade B gate at 1e-5.
| medium | row | now | proposed | why this level |
|---|---|---|---|---|
| audio, batch speech | start offset 1 to 50 ms, issuer exact | 0% at every offset off the 20 ms grid | >= 95% | oracle 100.0% at every offset; offset search |
| audio, batch speech | middle half kept, issuer exact | 5.8% | >= 90% | an aligned 0.5 s clip holds 100.0%; the loss is the grid phase |
| audio, batch speech | marked share 25%, detected | 2.0% | >= 90% | windowed detection over the saved time profiles |
| audio, batch speech | tempo 0.99 and 1.01, issuer exact | 17.0% and 18.0% | >= 90% (new embedding) | the streaming mark already holds 97.0% and 95.0% there |
| audio, streaming | G.711 mu-law, detected | 0% | >= 90% (new embedding) | the batch mark holds 100.0% |
| audio | codec rows (Opus 12, G.711, MP3, AAC) | 96.2% to 100.0% exact | unchanged | regression guard |
| video, VideoSeal | H.264 crf 23 | 93.0% | >= 97% | its 3 failures are all near (margin counts can differ from rates by 1 to 2 clips, section 4) |
| video, VideoSeal | social re-encode | 62.8% | >= 80% | 11 near, 5 mid of 16 failing |
| video, VideoSeal | H.264 crf 28 | 53.5% | >= 75% | the keyframe scheme reaches 78.0% on the same clips; 5 near, 15 mid of 20 failing |
| video, VideoSeal | centre crop 80% | 62.8% | >= 90% | oracle 93.0% |
| video, VideoSeal | letterbox, screen recording | 34.9%, 69.8% | >= 80%, if an oracle row confirms it | hypothesis: canvas detection; no oracle yet and mixed margins; identity is 100.0% |
| video, keyframes | start trim, kept segments | 0% off the 1 s grid | >= 90% | 95.1% when the cut is on the grid |
| text, KGW t=1 | mark as 10% of a human text | 32.0% | >= 90% | needs scoring past the 1,024-token cap (65 of 68 misses have none of the mark inside it) plus windowed scoring; the mark is intact |
| text, every arm | mark as 25% of a human text | SynthID 100.0%, KGW t=1 98.0%, KGW greedy 17.0% | within 5 points of the arm's identity row | windowed scoring removes the dilution, not the arm's own limit |
| text, SynthID | identity; delete, insert, swap 30% (sweeps) | 100.0%; 97.0%, 100.0%, 95.0% | unchanged | regression guard |
No target is proposed for paraphrase (49.5% to 57.0% on the sampled arms, 30.5% for greedy KGW), for greedy KGW grade B (1.0%) or for heavy video compression (crf 35 14.0%): each needs a scheme or embedding change beyond the current scheme.
Paraphrase is a designed limit, not an open item. A text watermark lives in the choice of tokens; a full rewrite replaces those choices, so no token-level scheme survives it, and Laidline makes no claim that it does. This sits inside the product's design rule for every medium: to strip a Laidline mark, an attacker has to destroy and rebuild the media rather than copy it, which makes the result their own work in substance. The measured rates above (about half the marks survive a same-model paraphrase, nearly all survive translation) are published as the limit. Text hardening is complete with the windowed scan and the KGW cap lift.
Audio presence on the public tier is limited to the fast and normalised reads: the public
detect tier shows an AudioSeal presence only when decode: fast or the hardened decode's fast or normalised path read
it, or when it is confirmed. Before the fine-read floor (below), the hardened decode flagged unmarked read speech on 0.68 % of single
utterances, 5.33 % of 30 s joins and 14.17 % of 120 s joins (0 of 1,247 other clips, an earlier bench), against 0.16 %, 0.67 % and
0.42 % for what the public tier shows. In the 2026-10 null campaign the public tier showed 0.18 %, 0.08 % and 0.24 % of
5,000 unmarked single utterances, 30 s joins and 120 s joins, and 7.25 % of 4,979 music clips. Fallback-only presence stays on the full tier, with a caveat stating the rates
(Grading, "Audio presence on the public tier").
The hardened audio fallback is gated by the fine-read floor (Grading, "Hardened audio decode"): a batch-card read found by the phase search or the windowed read counts only if its whole-clip or its 10 s fine-pass message confidence reaches 0.305. With the floor, the full tier flags unmarked read speech on 0.40 % of single utterances (Wilson 0.26 to 0.60 %), 0.67 % of 30 s joins (0.18 to 2.40 %) and 1.25 % of 120 s joins (0.43 to 3.61 %), and 0 of 1,247 other clips (an earlier bench; in the 2026-10 null campaign the full tier flagged 0.78 %, 0.66 % and 1.48 % of 5,000 single utterances, 30 s joins and 120 s joins and 654 of 4,979 music clips, 293 of them by the fallback paths alone). What the floor costs on marked audio: 1 of 3,192 issuer-exact reads on 37 ordinary rows. Its limit, published as measured: a mark shorter than the 10 s fine-pass excerpt inside long unmarked audio is protected only as far as it was measured. That is 29 of 30 fresh 3 s marks inside 30 s (one issuer-exact read lost) and 30 of 30 fresh 12 s marks inside 120 s.
6. Corrections and caveats recorded
| # | correction or caveat | evidence |
|---|---|---|
| 1 | AudioSeal's loudness normaliser is off. An earlier note ("the detector normalises loudness over the whole input") was wrong: all four checkpoints load with normalizer=None. Windowed audio detection is still wanted for memory, not for loudness. |
audioseal/builder.py:135-136; volume rows: -12 dB 100.0%, -20 dB 90.0% detected, -30 dB 3.0% |
| 2 | The shipped audio threshold is 0.5. 0.39 is the bench operating point (lowest threshold with under 1% false positives on 530 clips). Rates at the shipped setting are the "at 0.5" column. | core/config.py:191; 0.75% at 0.39, 0.38% at 0.5 |
| 3 | KGW detection changed: each distinct n-gram is scored once, and grade B counts the tokens the distinct n-grams cover. Greedy KGW on a small model now detects but grades D. | watermark/text/kgw.py:92-126, detect/text_check.py:58-64; KGW greedy 95.0% detected, grade B 1.0% |
| 4 | Text detect gap. Only the configured scheme and the default key are scored, with no windowed scoring. The embed rows measure the cost; KGW's 1024-token cap explains its misses at a 10% share (embed_window in the result). |
KGW t=1 embed 10%: 65 of 68 misses outside the window |
| 5 | The text FPR guard is a binomial bound on the 0.1% limit. At that limit 500 texts expect 0.5 flags, so one flag is not evidence of excess: the run's single flags pass (P(X>=1)=0.39 and 0.18). The guard cannot prove the rate is below 0.1% either: 0 of 500 still leaves a Wilson upper bound of 0.76%. | SynthID: 1 of 500 human (z max 3.31), 1 of 200 unmarked (z max 3.54); KGW 0 flags |
| 6 | GPU against CPU. In the pilot, image and audio were compared item by item: 75 identical decisions and 0 differ for each. Text was compared on rates only, and the rates differ (SynthID grade B 87.5% on the GPU against 100.0% on the CPU for the pilot's items), because the completions themselves differ between devices (device numerics and batch seeding); a GPU rerun of the first generation group was 48 of 48 identical, which shows GPU determinism, not CPU agreement. The phantom rate on the GPU is 8 of 5000 (0.16%), identical to the CPU run. Soft outputs are not bit-identical (0.15% of image raw bits flip on the 2-image check; logits differ by up to 3.3 after WebP), so in the full runs some image and audio rows moved from the published CPU numbers, all within their earlier Wilson intervals; Benchmarks lists them. | the GPU pilot comparison, the phantom result and its decoder.device_note |
| 7 | The earlier video numbers were embedded with TF32 on. This run has TF32 off, which changes the VideoSeal embed bytes; on 2 clips (akiyo_0 and park_joy_420_720p50_0; identity and h264_crf35), exact decodes matched and bit accuracy moved by up to about 0.04 on harsh rows. Across the 43 clips several rows moved, all inside the earlier Wilson intervals (Benchmarks). | machine.tf32_note |
| 8 | libx264 output is not always 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 and an exact decode can move at the edge. | diagnostics.reproducibility |
| 9 | The Twilio row is re-decoded from the earlier recordings (no new calls) on the GPU. It equals the earlier figure: 99.0% detected (94.6 to 99.8%), 98.0% exact; 0 of 10 control calls flagged. | telco in the audio result |
| 10 | Oracle rows estimate a resync ceiling; the crop oracles are not guaranteed ceilings (section 1). | "Oracle below the attack" lines in the image run |
| 11 | TrustMark schema bits 98 and 99 are outside the BCH. A flipped schema bit fails a decode with at most 5 raw errors; the image bench counts these as near and lists them. | "Schema-bit misses" lines |
| 12 | Horizontal flip survives on TrustMark; vertical did not decode in a development probe. Only horizontal is a bench row; the vertical result is not a bench measurement. | flip_h Q 100.0% |
| 13 | Text descriptors: distinct_ngram_ratio uses bigrams (KGW's unit at context width 1) and repetition_rate uses 4-grams. |
descriptors in the text result |
| 14 | Keyframe residual after the pooled-consistency rule. The hardened keyframe reader drops a payload one frame accepted when the other frames' reads of the same step, pooled, outscore it (the bridge clip: a soft decode at the BCH limit landed on a neighbouring codeword 14 of 96 bits away, another issuer, while the other marked frames read the embedded codeword just under the normalised gate). The rule needs that contrary evidence: a clip whose only readable frame is such a neighbour, with no other frame reading the embedded codeword above the score gate, still decodes the neighbour (grade D unkeyed; in keyed mode it is ineligible, and its issuer is shown as a claim only when it parses under the keyed layout, that is when the neighbour keeps the 3 version bits; one that changes them shows presence without a claim). The saved records hold 0 such clips; the population at risk is the one-frame decodes with no rival (1,162 of 1,254 in the earlier video run). | Grading ("Pooled consistency") |
Reference
- Benchmarks: the full result tables, latency, validators and image sizes, and how to rerun a suite.
- Grading: how a decode becomes a grade, per signal, and the per-signal limits.
- Keys: keyed payloads for image, audio and video, and their residuals.
- Configuration:
watermark.*anddetect.*settings quoted above.