wai-mark:tent64: the second marking layer, a watermark recomputed from its payload, read with no key, measured against an attack battery (SYN-2) #407

Merged
dcharlot merged 10 commits from wai/synthesis-mark into main 2026-10-22 00:11:35 -04:00
36 changed files with 9318 additions and 136 deletions

View file

@ -127,7 +127,9 @@ on:
- 'wai/extensions/deriving-edge.md'
# tests/synthesis.rs requires this extension's §12 to list the codes the reference
# reports and its §10.2 table to pin the reference's icons, synthesis_verify.py reads
# both, and the registry's wire identifiers test reads its sections.
# both, tests/synthesis_mark.rs and synthesis_mark_verify.py read its scheme's
# definition, generator, figures, table and claims (§7.4 to §7.5.8), and the registry's
# wire identifiers test reads its sections.
- 'wai/extensions/synthesis.md'
# tests/ai_usage.rs requires this extension's §9 to list the codes the reference reports,
# ai_usage_verify.py reads that table, and the registry's wire identifiers test reads its
@ -1212,6 +1214,28 @@ jobs:
(cd wai-rs && cargo test --no-default-features --features synthesis --lib synthesis::)
(cd wai-rs && cargo test --no-default-features --features synthesis --test synthesis)
python3 -I live-conformance/synthesis_verify.py live-conformance/synthesis
# The registered watermark scheme wai-mark:tent64 (extensions/synthesis.md §7.4 to §7.8):
# the reference's unit tests (the code corrects 29 errors and its generator is the
# definition's, the layout, the embedding and the detector in every orientation, the
# battery's resampling, rotation and keyframe), then tests/synthesis_mark.rs in release,
# since it embeds and attacks every picture of the measurement sets (every file of
# live-conformance/synthesis-mark/ regenerates byte for byte, the measurement among them,
# and no other file is there; every embedding, detection and case gets what it states;
# the extension's definition, generator polynomial and shuffle are the reference's;
# §7.5.8 claims only attacks the measurement shows survived, and §7.5.6's figures are the
# measurement's); then the second implementation, synthesis_mark_verify.py with
# _wai_mark.py, written from the extension's text and sharing no code with wai-rs, which
# embeds, detects and decodes itself, runs the battery's picture attacks, verifies every
# case with synthesis_verify.py, mutates copies of the corpus and weakens its own verifier
# in turn; standard library only, run isolated (-I).
- name: synthesis watermark · wai-rs and synthesis_mark_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
(cd wai-rs && cargo test --no-default-features --features synthesis_mark --lib -- synthesis::mark synthesis_mark::)
(cd wai-rs && cargo test --release --no-default-features --features synthesis_mark --test synthesis_mark)
python3 -I live-conformance/synthesis_mark_verify.py live-conformance/synthesis-mark
# wai.ai.usage (extensions/ai-usage.md): the reference module's unit tests (RFC 9651's
# Dictionary parser against the vocabulary's examples and the RFC's edges, statements
# read and refused in their steps' order, the precedence rule, HTTP fields compared by

View file

@ -6,6 +6,50 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### `wai-mark:tent64`: the second marking layer, a watermark recomputed from its payload and read with no key
- **The scheme** ([synthesis](extensions/synthesis.md) §7.4, §7.5) fills the scheme slot of §7.4: a
mark in the luma of RGB8 pictures and video frames, registered by a term and the SHA-256 of its
definition. Each frame is cut into a 64 × 64 grid that follows the picture through a rescale; a
separable tent checkerboard in each cell is zero-mean, so brightness leaves a cell's statistic
unchanged and contrast scales it. A public SHA-256 stream shuffles the cells, signs them and
masks the code; the 8-byte payload is a BCH(255, 71) codeword shortened to 248 bits, each bit
carried by 16 cells scattered over the frame. Each bit gets the least amplitude, up to 12 luma
levels, that gives its statistic the sign the codeword asks with a margin of 3/2 of its weight
mass, so a frame is changed only as far as the margin asks. A video carries the whole payload
in every frame.
- **No other channel.** The embedding is a pinned integer derivation of the unmarked frames and
the payload: a verifier holding the unmarked frames recomputes the marked ones
(`mark_recompute_mismatch`), so the mark carries exactly its payload, a function of the claim,
and no viewer or session identifier. A sink that generates content marks it as part of the
reconstruction its tier holds (§7.8). The marking record gains `embedding` (the frames' size
and the digests of the unmarked and the marked frames).
- **Key-free detection** in the 8 quarter turns and mirrors, decoding within 29 bits; a
false-positive bound of 2^−61.39 derived from the code and checked against the words read from
unmarked pictures, which match the uniform model. The detection record gains `read` (the
frames it read), and a verifier that holds them runs the detector again
(`detection_irreproducible`). A conversion that does not mark again carries its input's mark,
and its detection agrees by inheritance (`agree-inherited`).
- **Robustness measured, not asserted.** An attack battery of integer attacks (re-encoding with
the crate's own `wai.video.int_motion` encoder and a staged base, the pinned derivations of §4,
area resampling, crops, quarter turns and mirrors, small rotations, noise from a fixed stream,
brightness, contrast, a local blur; for clips, inter coding and dropped frames), over
measurement sets the crate generates, gives the table of §7.5.8; the scheme claims to survive
only what every set and a set of photographs survived. It claims no crop and no small rotation.
PSNR and SSIM per picture, and a PSNR floor that depends on the frame's size alone.
- **Signpost and resolver** (§7.7): the marking's C2PA statement (`c2pa.watermarked.bound` and a
`c2pa.soft-binding` assertion naming the scheme and its digest), the openly disclosed mark
itself, and `/.well-known/wai-mark/<name>`, proposed, for the definition and for claims by
payload.
- **New codes**: `scheme_mismatch`, `mark_evidence_invalid`, `mark_input_mismatch`,
`mark_recompute_mismatch`, `detection_irreproducible`, `layer_not_proved`; the policy member
`second_layer`. `wai-mark:conformance` stays, as the scheme with no algorithm.
- **Reference and corpus**: `synthesis::mark` (feature `synthesis`) and `synthesis_mark` (feature
`synthesis_mark`), with `wai_synthesis_mark_vectors gen|verify|measure`;
[`live-conformance/synthesis-mark/`](live-conformance/synthesis-mark/), and its second
implementation `synthesis_mark_verify.py` with `_wai_mark.py`, which mutates the corpus and
weakens its own verifier in turn. Audio is not reached in this revision.
### `wai.media.exploration`: archives that replay, default-distance, a homogenization meter, and taste as a declared, owned objective
- **The extension** ([exploration](extensions/exploration.md)) registers the record

View file

@ -306,6 +306,7 @@ and a role are fixed once registered.
| `/.well-known/wai-grants/` | well-known | defined | http-delivery §8.4 |
| `/.well-known/wai-key-status` | well-known | defined | http-delivery §8.4 |
| `/.well-known/wai-keys` | well-known | defined | jwp-receipts §7.1 |
| `/.well-known/wai-mark/` | well-known | proposed | synthesis §7.7 |
| `GRANT_CHALLENGE` | setup-option | proposed | live-rooms §10.5.2 |
| `GRANT_PROOF` | setup-option | proposed | live-rooms §10.5.2 |
| `SECURE_OBJECT_KEY_ID` | catalog | defined | confidentiality §4.3 |
@ -385,6 +386,8 @@ and a role are fixed once registered.
| `wai-distance` | document-kind | defined | exploration §6.3 |
| `wai-key-status` | document-kind | defined | jwp-receipts §7.9 |
| `wai-label-policy` | document-kind | defined | synthesis §10.1 |
| `wai-mark:conformance` | scheme | defined | synthesis §7.4 |
| `wai-mark:tent64` | scheme | defined | synthesis §7.4 |
| `wai-marking` | document-kind | defined | synthesis §7.2 |
| `wai-membership` | document-kind | defined | contribution §3.2 |
| `wai-module-grant` | document-kind | defined | jwp-receipts §7.9 |

View file

@ -1164,7 +1164,20 @@ of two pinned integer derivations (a 2× upscale and a 3 × 3 denoise) is proved
the output from the input's decoded samples; any other assistive step can only assert it. The
signed claim is the first of two marking layers; the second is a watermark whose payload is the
claim's class and the head of its link hash, and a detection that disagrees with the claim is
refused (the watermark algorithm is a slot a later revision fills). A room's AI participant,
refused. The registered watermark, `wai-mark:tent64`, marks the luma of pictures and video by a
pinned integer derivation: a tent pattern on a 64 × 64 grid that follows the picture through a
rescale, each bit of a shortened BCH codeword carried by 16 cells, each given the least amplitude
up to 12 that meets its margin. A verifier holding the unmarked frames recomputes the marked ones,
so the mark carries its payload and nothing else (no viewer or session identifier can ride on
it), and a sink that generates content marks it as part of the reconstruction its tier holds.
Anyone reads it with no key, in any quarter turn or mirror, with a false-positive bound of
2^−61.39 derived and checked against unmarked pictures; a detection is run again on the frames it
names, and a payload a conversion carries agrees with the conversion's claim by inheritance. An
attack battery of integer attacks (re-encoding with the crate's own codecs, rescaling, crops,
rotations, noise, brightness, contrast, a local blur) measures what it survives, and it claims no
more: no crop and no small rotation. The marking's C2PA statement and the mark itself are the
signposts to the detection solution, and a well-known path resolves the scheme's definition. A
room's AI participant,
marked in its grant, states its machine origin in its chat, reactions and captions, and labels
every track it publishes. A review signed over one exact reconstruction is presented only by a
player whose decode path holds the review's tier and whose output is that reconstruction, so one

View file

@ -14,7 +14,12 @@
> output and compares it, so the claim that the edit is assistive is checked, not trusted. The
> signed claim is the first of two **marking layers**; the second is a watermark whose payload is
> a function of the claim, and a verifier refuses a detection that disagrees with it. The
> watermark algorithm itself is a slot this revision leaves to a later one. An **AI participant**
> registered watermark, `wai-mark:tent64`, is a mark in the luma of pictures and video embedded
> by a pinned integer derivation, so a verifier that holds the unmarked frames recomputes the
> marked ones and knows the mark carries its payload and nothing else; anyone reads it with no
> key, through a code whose false-positive bound is derived and checked; and an attack battery
> measures what it survives, which is all it claims. A **signpost** in the content and in the
> metadata names the detection solution, and a well-known path resolves it. An **AI participant**
> of a room is marked in its grant, and its messages, captions and tracks carry its machine
> origin. A **review** is a signed verdict over one exact reconstruction, which a player presents
> only for a presentation whose decode path holds the review's tier, so one review holds for
@ -22,18 +27,24 @@
> signed **log** shows whether it was. No new transport. The reference implementation is the
> `synthesis` module of `wai-rs` (feature `synthesis`); the corpus is
> [`live-conformance/synthesis/`](../live-conformance/synthesis/), and its second
> implementation is `live-conformance/synthesis_verify.py`.
> implementation is `live-conformance/synthesis_verify.py`. The scheme's battery and measurement
> are the `synthesis_mark` module (feature `synthesis_mark`); its corpus is
> [`live-conformance/synthesis-mark/`](../live-conformance/synthesis-mark/), and its second
> implementation `live-conformance/synthesis_mark_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision, each pinned to the version named: the Code of Practice on
> Transparency of AI-Generated Content, final text of 10 June 2026, Section 1 Measures 1.1 to 1.3
> and Section 2 Measures 1.1 and 1.2 and its Annex 1; the EU icons the Commission publishes for
> Transparency of AI-Generated Content, final text of 10 June 2026, Section 1 Measures 1.1 to 1.3,
> 2.1, 2.3 and 3.2 to 3.4 and its Glossary, and Section 2 Measures 1.1 and 1.2 and its Annex 1, as
> the Commission's document 129555, whose SHA-256 as fetched on 21 October 2026 was
> `7bd22c5a3c56eaefda27a5bf7a6118198ef2a9c9255241bd97abf7cdedf9bc28`; the EU icons the Commission publishes for
> that Annex, as fetched on 21 October 2026 from the page it last updated on 13 October 2026
> (§10.2); Regulation (EU) 2024/1689, Article 50, paragraphs 2, 4 and 5 as that Code reproduces
> them, and the last sentence of paragraph 2 in a reproduction of the Official Journal text (the
> Official Journal itself could not be fetched for this revision); the C2PA Technical
> Specification, version 2.4, §6.4, §18.15.2, §18.15.4.5 and §18.28; and the IPTC Digital Source
> Type vocabulary, whose most recent concept change is dated 2024-10-23. A section number of one
> Specification, version 2.4, §6.4, §9.3, §18.10, §18.15.2, §18.15.4.5, §18.15.4.8, §18.15.5 and
> §18.28; RFC 8615; and the IPTC Digital Source Type vocabulary, whose most recent concept change
> is dated 2024-10-23. A section number of one
> of those texts in this extension is that version's.
## 1. Scope
@ -41,13 +52,16 @@
In scope: the records and their encodings (§2); the classes, the steps and the origin claim
(§3); the two pinned integer derivations and the evidence that makes an assistive claim
checkable (§4); the digital source type (§5) and the C2PA statement (§6) a claim gives; the
two marking layers, their records and their agreement (§7); AI participants of rooms (§8); the
two marking layers, their records and their agreement, the registered watermark scheme, what it
survives, its signpost and its resolver (§7); AI participants of rooms (§8); the
review and when a player presents it (§9); when a label is shown, the EU icons pinned by digest,
and a player's log (§10); verification (§11).
Out of scope: a transport; how a generator, a codec or an editor works; the watermark algorithm
of the second marking layer and its detector, which a later revision registers in the slot of
§7.4; how a manifest is assembled, embedded and signed, which is C2PA's and
Out of scope: a transport; how a generator, a codec or an editor works; a watermark for audio or
for text, which this revision does not register (audio was in this revision's sight and is not
reached; text needs a scheme of its own); finding a mark again after a crop or a rotation, which
the registered scheme does not do (§7.5.8); how a manifest is assembled, embedded and signed,
which is C2PA's and
[provenance](provenance.md)'s; key discovery for the signers of these records, which a verifier
takes from [jwp-receipts](jwp-receipts.md) §7 as it does for any other key; and the rendering of a
label, beyond the asset it shows and the instants it shows it at.
@ -78,7 +92,10 @@ covers*): a tier covers what the sink emits.
Duties this extension supports: the two machine-readable marking layers of the Code of Practice
(Section 1, Measure 1.1: Sub-measure 1.1.1, signed and time-stamped metadata; Sub-measure 1.1.2,
an imperceptible watermark) and its richer provenance information (Measure 1.3: the model, the
timestamp, the type of operations); the disclosure of AI systems that interact with people
timestamp, the type of operations); a detection mechanism given as a public specification with
two implementations (Sub-measure 2.1.1), its signed results (Sub-measure 2.1.2), the reliability
and robustness measurements of Measures 3.2 and 3.3, and a signpost for the interoperability of
detection (Measure 3.4(c)(ii)); the disclosure of AI systems that interact with people
(Article 50(1)); and the placement of a label at first exposure, after interruptions and at
regular intervals (Section 2, Sub-measures 1.2.1 to 1.2.3). WAI states what its records carry and
what a verifier checks. It makes no statement about any regime, and a deployment confirms what a
@ -354,9 +371,8 @@ A watermark carrying a claim holds an 8-byte **payload**: the code of the claim'
`captured`, 1 `reconstructed`, 2 `assistive`, 3 `synthesized`), then the first seven bytes of
the claim's link hash. Read from the content alone, with no metadata, it says the class and points
at the claim. Seven bytes find a claim; they do not prove one, and the claim's signature does
that. The marking record names the scheme that reads it (§7.4); a signpost in the content naming
the detection solution, as the Code's Measure 3.4(c)(ii) describes one, is not part of this
revision.
that. The payload is the whole of what a mark carries: it names no viewer, no session, no device
and no account (§7.5.6).
The party that embeds the mark signs a **marking** record, `kind` `"wai-marking"`:
@ -368,38 +384,456 @@ The party that embeds the mark signs a **marking** record, `kind` `"wai-marking"
| `scheme` | term | the watermark scheme (§7.4) |
| `scheme_digest` | 32 B hex | the SHA-256 of the scheme's definition |
| `payload` | 8 B hex | the payload embedded |
| `embedding` | object or `null` | the frames the mark was embedded into (§7.6) |
| `issued_at` | date-time | when it signed it |
| `signer_pubkey`, `sig` | | §2 |
An `embedding` is an object of exactly these members: `frames`, `width` and `height`, integers
each at least 1, the count and size of the frames; `unmarked` and `marked`, each 32 B hex, the
SHA-256 of the unmarked and of the marked frames, each frame its canonical buffer (§2), frame
after frame. Otherwise `record_malformed`.
A verifier holds the claim the marking names (`claim_unknown`); that claim's subject is the
marking's object, and the marking's payload is the claim's (§7.2) (`marking_mismatch`).
marking's object, and the marking's payload is the claim's (§7.2) (`marking_mismatch`). Then
§7.6.
### 7.3 Detection and agreement
A detector that reads a watermark signs a **detection** record, `kind` `"wai-detection"`: the
members of a marking but `origin`, with `payload` the 8 bytes read, or `null` where it found no
mark. For each detection, in order, with the origin claim whose subject is the detection's
object (if the verifier holds one) and the first marking whose subject it is (if it holds one):
members of a marking but `origin` and `embedding`, with `payload` the 8 bytes read, or `null`
where it found no mark, and with `read`, the frames it read: an object of exactly the members
`frames`, `width` and `height` (integers, each at least 1) and `samples` (32 B hex, the SHA-256
of those frames' canonical buffers, frame after frame), or `null`; otherwise `record_malformed`.
It is the signed detection result of the Code's Sub-measure 2.1.2: the content by its hash, the
detection solution by the scheme's term and digest, and a time stamp; and its kind says the result
rests on a watermark (Measure 2.3), the claim being the metadata. For each detection, in order,
with the origin claim whose subject is the detection's object (if the verifier holds one) and the
first marking whose subject it is (if it holds one):
1. With a marking: the detection's `scheme` and `scheme_digest` are the marking's
(`detection_mismatch`).
2. With no payload read: where a marking says the object was marked, `watermark_absent`, the
2. Its scheme and the frames it read (§7.4, §7.6): `scheme_mismatch`, `mark_evidence_invalid`,
`samples_mismatch`, `detection_irreproducible`, `layer_not_proved`.
3. With no payload read: where a marking says the object was marked, `watermark_absent`, the
second layer having been removed; otherwise the object is **unmarked**.
3. With a payload read and a claim held: the payload is the claim's (§7.2); otherwise
`layers_disagree`. A content that carries the mark of a synthesized claim while its metadata
says captured, or the mark of another claim, is refused here. With no claim held, the
detection is **not checked**.
4. With a payload read and a claim held: the payload is the claim's (§7.2), and the detection
**agrees** (`agree`); or it is the payload of a claim the verifier holds that the claim reaches
through inputs along steps `convert` and `assist`, each input's claim held, and the detection
**agrees by inheritance** (`agree-inherited`): a conversion that does not mark its output again
carries its input's mark, and its claim names that input. Otherwise `layers_disagree`. A
content that carries the mark of a synthesized claim while its metadata says captured, or the
mark of another claim, is refused here. With no claim held, the detection is **not checked**
(`not-checked`).
### 7.4 The scheme slot
### 7.4 Schemes
This revision registers no watermark scheme. A scheme is named by a term `wai-mark:<name>` and
the digest of its definition, which a later revision pins with its embedding as an integer
derivation appended to a generative one, so that a sink that generates content embeds the mark
as part of the reconstruction it is held to, and with its key-free detector. Until then a
detection is a signed statement by its detector and nothing more. The conformance corpus names
`wai-mark:conformance`, whose definition is the text `wai-mark:conformance names no watermark
algorithm (synthesis.md section 7.4)` and which names no algorithm: it exercises the records and
their agreement, not a watermark.
A scheme is named by a term `wai-mark:<name>` and the SHA-256 of its definition. This revision
registers `wai-mark:tent64` (§7.5). Its definition is the JCS text (RFC 8785) of this object,
whose bytes are the corpus's `scheme.json` (§13):
```
{"amplitude":12,"cells_per_bit":16,"class_max":3,"code":[255,71,29],"field":"11d",
"generator":"140a722a1a468d36d87a25364e685922a1e56fd1a478c1d","grid":64,"length":248,
"luma":[77,150,29,128,8],"margin":[3,2],"max_frame_bytes":268435456,"media":["image","video"],
"min_height":128,"min_width":128,"orientations":8,"pattern":"tent","sample":"RGB8",
"scheme":"wai-mark:tent64","specification":"extensions/synthesis.md section 7.5",
"stream":"wai-mark:tent64 stream"}
```
(broken here into lines after commas between members; the definition itself has no line break,
and no space but those inside its strings), and its digest is
`4e1bd9f2cac9420fc276434751a3fb9b2b0916128f200bf147009a86168be7d6`. The definition pins the
scheme's parameters, and §7.5 is its algorithm. A marking or a detection that names
`wai-mark:tent64` under another digest is `scheme_mismatch`. A marking of the scheme states its
`embedding`, and a detection its `read`, of frames the scheme admits (§7.5.1): otherwise
`mark_evidence_invalid`.
`wai-mark:conformance` stays as a scheme with no algorithm: its definition is the text
`wai-mark:conformance names no watermark algorithm (synthesis.md section 7.4)`, and a record of
it states no frames (`mark_evidence_invalid`). It exercises the records and their agreement apart
from any algorithm, and stands for a scheme a verifier does not implement: a record of it, or of
any term this revision does not register, is a signed statement by its signer and nothing more,
checked against the claim (§7.2, §7.3) but neither recomputed nor run again (§7.6).
### 7.5 The scheme `wai-mark:tent64`
An imperceptible mark in the luma of RGB8 frames, embedded by a pinned integer derivation and read
back with no key. Integer throughout: the marked frames are a pure function of the unmarked frames
and the payload, the same bytes on every machine, and what the detector reads is a pure function
of the frames it reads.
#### 7.5.1 Frames
The scheme marks **frames**: RGB8 buffers (§2), each *W* × *H* pixels with *W* ≥ 128, *H* ≥ 128
and *W*·*H*·3 ≤ 2^28, and *F* ≥ 1 of them of one size, with *F*·*W*·*H*·3 ≤ 2^53 − 1. A still
picture is one frame; a video's frames are its decoded frames in presentation order. The **luma**
of a pixel is *Y* = (77·*R* + 150·*G* + 29·*B* + 128) >> 8. Frames of another size or count are
not admitted: the detector reads no mark from them, and a record that states them is
`mark_evidence_invalid`.
#### 7.5.2 The grid and the pattern
A frame is cut into 64 × 64 **cells** by the column edges *x*ᵢ = ⌊*i*·*W* / 64⌋ and the row edges
*y*ⱼ = ⌊*j*·*H* / 64⌋, *i* and *j* from 0 to 64. Cell *k* = 64·*j* + *i* holds the pixels with
*x*ᵢ ≤ *x* < *x*ᵢ₊₁ and *y*ⱼ ≤ *y* < *y*ⱼ₊₁; its sides are *L*ₓ = *x*ᵢ₊₁ − *x*ᵢ and
*L*ᵧ = *y*ⱼ₊₁ − *y*ⱼ, each at least 2, and its **area** is ν = *L*ₓ·*L*ᵧ. The grid is a fraction
of the frame, so it follows the picture through a rescale or a change of aspect.
The **tent** over a span of *L* samples, at position *u* from 0 to *L* − 1, is
*f*(*u*, *L*) = sgn(*c*)·(*L* − |2·|*c*| − *L*|) with *c* = 2*u* + 1 − *L* and sgn(0) = 0:
negative over the span's first half and positive over its second, odd about its centre, and
summing to 0. A pixel's **weight** is ω(*x*, *y*) = *f*(*x* − *x*ᵢ, *L*ₓ)·*f*(*y* − *y*ⱼ, *L*ᵧ) in
its cell; |ω| ≤ ν. A cell's **statistic** over a luma plane is *s*ₖ = Σ ω(*x*, *y*)·*Y*(*x*, *y*)
and its **mass** *q*ₖ = Σ |ω(*x*, *y*)|, each over the cell's pixels. A constant added to every
luma sample leaves a statistic unchanged; a gain scales it.
#### 7.5.3 The layout and the code
The **stream** is the sequence of 32-bit words, read big-endian, eight from each digest, of
SHA-256(`wai-mark:tent64 stream` ‖ *n*) for *n* = 0, 1, 2, …, the label in ASCII and *n* a 4-byte
big-endian counter. Its words are taken in this order:
1. the **shuffle** π: π starts as 0, 1, …, 4095; for *i* from 4095 down to 1, with *r* the next
word, the entries at *i* and at *r* mod (*i* + 1) are swapped;
2. the **signs** ε₀ … ε₄₀₉₅: +1 where the next word is even, −1 where it is odd;
3. the **mask** μ₀ … μ₂₄₇: the lowest bit of the next word.
Bit *j* of the code, *j* from 0 to 247, is **carried** by the 16 cells π(16·*j*) … π(16·*j* + 15);
the cells π(3968) … π(4095) carry nothing. π begins 1787, 1650, 1946, 384, and the corpus states
more of it (§13).
The **code** is the binary BCH code of length 255 and designed distance 59 over GF(2^8) defined
by *x*^8 + *x*^4 + *x*^3 + *x*^2 + 1, α a root: its generator *g*(*x*) is the least common
multiple of the minimal polynomials of α^1 … α^58, of degree 184, so the code has 71 information
bits and corrects 29 errors. Its coefficients from *x*^184 down, in hexadecimal, are
`140a722a1a468d36d87a25364e685922a1e56fd1a478c1d`. It is shortened to 248 bits: a payload's 64
bits *b*₀ … *b*₆₃, *b*₀ the most significant bit of its first byte, are
*m*(*x*) = Σ *b*ᵢ·*x*^(63 − *i*), its **codeword** is
*c*(*x*) = *m*(*x*)·*x*^184 + (*m*(*x*)·*x*^184 mod *g*(*x*)), and codeword bit *j* is the
coefficient of *x*^(247 − *j*): bits 0 to 63 are the payload's, the rest its parity. This code is
chosen because a mark's errors arrive as independent bits after the shuffle has scattered each
bit's cells over the frame, which a binary code corrects bit by bit; because its decoding is
algebraic, so every correct decoder gives the same result; and because a decoder that corrects
only within its radius has a false-positive probability that is an exact count (§7.5.6), which a
code that decodes by likelihood does not give.
Bit *j* is **transmitted** as *t*ⱼ = *c*ⱼ ⊕ μⱼ, with the sign σⱼ = +1 for 1 and −1 for 0. The mask
keeps a frame with no structure from reading as a codeword: a flat frame reads every bit as 0,
which unmasks to μ, and μ and its complement are each more than 29 bits from every codeword. A
bit's **statistic** over a luma plane is *T*ⱼ = Σ εₖ·*s*ₖ and its **mass** *Q*ⱼ = Σ *q*ₖ, each over
its 16 cells.
#### 7.5.4 Embedding
Given unmarked frames and a payload, each frame is marked on its own, under the same payload. For
bit *j* and an integer **amplitude** *a* from 0 to 12, the frame **at amplitude** *a* for bit *j*
has, at each pixel of each cell *k* that carries *j*, the delta
δ = sgn(*z*)·⌊(2·*a*·|*z*| + ν) / (2·ν)⌋, with *z* = σⱼ·εₖ·ω(*x*, *y*)
(*a*·*z*/ν rounded to the nearest integer, halves away from zero) added to each of *R*, *G* and
*B*, each sum clamped to 0 … 255. Bit *j*'s amplitude *a*ⱼ is the least *a* at which the luma of
the frame at amplitude *a* meets the **margin** 2·σⱼ·*T*ⱼ ≥ 3·*Q*ⱼ, and 12 where no *a* does. The
**marked frame** is the unmarked frame with every cell that carries a bit *j* changed at *a*ⱼ; the
cells that carry nothing are unchanged. The cells of different bits are disjoint, so each bit is
embedded on its own, and a bit's statistic in the marked frame is its statistic at *a*ⱼ.
The amplitude is informed by the frame: a bit whose statistic already meets the margin is left as
it is (*a*ⱼ = 0), and the frame is changed only as far as the margin asks, up to the cap, which
binds where the picture's own texture opposes the bit most. A bit that does not meet the margin
at 12 is left to the code.
**Video.** Every frame carries the whole payload, embedded on its own, and the detector sums the
bits' statistics over the frames it is given (§7.5.5). The payload is repeated in each frame,
rather than the codeword spread over a group of frames, so that any one frame, a clip of any
length, a clip with frames dropped or re-timed and a still taken from the video each carry the
whole payload, with no position in time to recover; and the frames read together give a stronger
reading than one.
#### 7.5.5 Detection
The detector is given frames, *F* of *W* × *H*; frames §7.5.1 does not admit carry no mark. It
tries the 8 **orientations** *o* = 0, 1, …, 7 in order. A frame **seen in orientation** *o* is the
*W*′ × *H*′ luma plane (*W*′ = *H* and *H*′ = *W* where bit 2 of *o* is set, otherwise *W* and *H*)
whose sample at (*x*, *y*) is the frame's luma at (*x*₂, *y*₂), where *x*₁ = *W*′ − 1 − *x* where
bit 0 of *o* is set and *x* otherwise, *y*₁ = *H*′ − 1 − *y* where bit 1 is set and *y* otherwise,
and (*x*₂, *y*₂) = (*y*₁, *x*₁) where bit 2 is set and (*x*₁, *y*₁) otherwise. The eight are the
quarter turns and mirrors, and one of them undoes any of them.
In orientation *o*, each bit's statistic is summed over the frames seen in *o*; the bit **read**,
*r*ⱼ, is 1 where that sum is above 0 and 0 otherwise; and the **word** is *r*ⱼ ⊕ μⱼ. The word is
decoded within a distance of 29: to the codeword at most 29 bits from it, which is unique where it
exists, or to none. A codeword whose first payload byte is at most 3, a class code, gives the
**payload**, with the orientation and the number of bits corrected, and the detector stops;
otherwise it tries the next orientation. Where none gives a payload, the frames carry **no mark**.
Any decoder that finds the codeword within 29 bits whenever there is one, and none otherwise, gives
this result: the reference finds the error locator, the shortest linear recurrence the
syndromes satisfy, searches the 248 positions for its roots, and checks that what it corrected has
no syndrome.
#### 7.5.6 What it guarantees
- **No other channel.** The marked frames are a function of the unmarked frames and the 8-byte
payload alone: no key, no seed and no choice is left to the embedder. A verifier that holds the
unmarked frames recomputes the marked ones and compares their digest (§7.6), and where they are
equal the content carries exactly the declared payload, which is a function of the claim, and
nothing else: no identifier of a viewer, a session, a device or an account can ride on such a
mark. A forensic watermark ([forensic-watermark](forensic-watermark.md)) serves the other
purpose: its variants differ from viewer to viewer by design, so that a leaked copy names the
session it came from. The two share no record, and a deployment that does both marks
AI-generated content with this scheme and sessions with that one.
- **Imperceptibility.** A sample moves by at most 12, and only in the cells that carry a bit. A
frame's **floor** is the PSNR its embedding cannot fall below:
10·log10(255²·3·*W*·*H* / *S*), *S* the sum over the carrying cells' samples of the square of the
delta at amplitude 12 with no clamp (a clamp only shortens a delta). It depends on the frame's
size alone: 36.66 dB at 512 × 512, 38.73 dB at 256 × 256, 35.76 dB at 768 × 512. No admitted
size's floor is below 20·log10(255/12) = 26.55 dB, which frames whose cells are all 2 × 2
pixels come within 0.14 dB of. Each picture's measured PSNR and SSIM are in §7.5.8.
- **False positives.** Model frames with no mark as giving, in each orientation, a word of 248
bits each 0 or 1 with probability ½ independently: the shuffle, the signs and the mask set each
read bit apart from the picture's own structure. In one orientation the word gives a payload
only when it lies within 29 bits of one of the 2^58 codewords whose first byte is a class code;
those balls are disjoint, so the probability is 2^58·*V*(248, 29) / 2^248, with
*V*(*n*, *t*) = Σ C(*n*, *i*) over *i* from 0 to *t*, and over the 8 orientations at most 8 times
that: 2^−61.39, about 3.3·10^−19. A clip is read once, so the bound is the same for it. The model
is checked against what the detector reads: the words read from the 32 unmarked pictures of the
measurement sets (§7.5.8) in their 8 orientations have a mean of 124.33 ones with a standard
deviation of 7.85, and the 31,744 pairs of those words from different pictures differ in a mean
of 123.91 bits with a standard deviation of 7.95 and in no fewer than 91; the model gives 124
and 7.87 for both. No unmarked picture of any set, under any attack of the battery, gives a
payload.
- **Key-free.** The detector needs no key and no secret: anyone with this text reads the mark,
which is what lets any party check the second layer against the first. It also lets anyone find
the mark and try to remove or overwrite it, or copy a payload into other content (§14).
#### 7.5.7 The battery
What the scheme survives is measured, not asserted. The **battery** applies each attack below to
marked frames, and to the same frames unmarked, and runs the detector (§7.5.5) on the result. The
attacks are integer functions of the frames, and the noise comes from a fixed stream, so a run of
the battery gives the same bytes on every machine. The attack at index *i* of a list, from 0, has
the seed *i*. Below, rnd(*n* / *d*) is *n* / *d* rounded to the nearest integer, halves away from
zero, and every result is clamped to 0 … 255.
Attacks on pictures, in the list's order:
- `none`.
- `int-motion-q4`, `-q8`, `-q16`, `-q32`, `-q48`: each frame encoded as a keyframe of
`wai.video.int_motion` ([integer-payloads](integer-payloads.md) §3) at step *q* and decoded:
each sample *v* becomes 128 + rnd((*v* − 128) / *q*)·*q*.
- `staged-base-q8`: each 2 × 2 block averaged, (*a* + *b* + *c* + *d* + 2) >> 2 per channel, to a
frame of half each side; that frame as `int-motion-q8`; and the result brought back to full size
by `wai-synthesis:upscale2` (§4.1): the base layer of a staged picture whose refinement is not
delivered.
- `denoise3`, `denoise3-x4`: `wai-synthesis:denoise3` (§4.2), once and four times.
- `scale-3/4`, `scale-1/2`, `scale-1/3`, `scale-1/4`, `scale-3/2`: **area resampling** to
⌊*W*·*n*/*d*⌋ × ⌊*H*·*n*/*d*⌋. Along an axis of *N* samples resampled to *M*, destination sample
*X* covers *X*·*N* to (*X* + 1)·*N*, and source sample *s* covers *s*·*M* to (*s* + 1)·*M*, in
units of 1/*M* of a source sample; the length of their overlap is *s*'s weight in *X*, and a
destination's weights sum to *N*. A destination value is ⌊(2·Σ + *N*ₓ·*N*ᵧ) / (2·*N*ₓ·*N*ᵧ)⌋,
Σ the sum over the source of the product of its two weights and its value.
- `upscale2`: `wai-synthesis:upscale2`.
- `aspect-3/4`: area resampling of the width alone to ⌊*W*·3/4⌋.
- `half-then-int-motion-q16`: `scale-1/2`, then `int-motion-q16`.
- `crop-1%`, `crop-2%`, `crop-5%`, `crop-10%`: ⌊*W*·*p*/200⌋ columns removed from each side and
⌊*H*·*p*/200⌋ rows from the top and from the bottom.
- `mirror`: the columns in reverse order.
- `rotate-90`: a quarter turn clockwise, to *H* × *W*, whose pixel (*x*′, *y*′) is the frame's
(*y*′, *H* − 1 − *x*′).
- `rotate-0.5`, `rotate-1`, `rotate-2`: a rotation about the centre by that many degrees, whose
cosine and sine at 2^16 are (65534, 572), (65526, 1144) and (65496, 2287). Destination pixel
(*x*, *y*) reads the frame at *X* = (cos·*u* + sin·*v* + (*W* − 1)·2^16) >> 1 and
*Y* = (−sin·*u* + cos·*v* + (*H* − 1)·2^16) >> 1, in units of 2^−16 of a pixel, with
*u* = 2*x* + 1 − *W* and *v* = 2*y* + 1 − *H*; with ξ = *X* >> 16, φ = *X* mod 2^16, and η, ψ
likewise from *Y*, its value is (*p*(ξ, η)·(2^16 − φ)·(2^16 − ψ) + *p*(ξ + 1, η)·φ·(2^16 − ψ) +
*p*(ξ, η + 1)·(2^16 − φ)·ψ + *p*(ξ + 1, η + 1)·φ·ψ + 2^31) >> 32, where *p* is the frame's
sample and 0 outside it.
- `noise-4`, `noise-16`: each sample *v*, in order (frame, row, column, then *R*, *G*, *B*), becomes
*v* + (*w* mod (2*s* + 1)) − *s*, *w* the next word of the stream of
SHA-256(`wai-mark:battery noise` ‖ seed ‖ *n*), the seed and *n* each 4 bytes big-endian, read as
§7.5.3 reads its stream.
- `brightness+32`, `brightness-32`: *v* + 32, *v* − 32.
- `contrast-1/2`, `contrast-3/2`: 128 + rnd((*v* − 128)·*n* / *d*).
- `local-blur`: the rectangle ⌊*W*/4⌋ ≤ *x* < ⌊3*W*/4⌋, ⌊*H*/4⌋ ≤ *y* < ⌊3*H*/4⌋, a quarter of the
picture, cut into blocks of 16 × 16 from its corner (the last ones shorter), each channel of each
block replaced by its mean, rounded half up: a large region blurred, as a face is blurred for
privacy (the Code's Measure 3.3).
Attacks on clips: `none`; `clip-int-motion-q8` and `clip-int-motion-q24`, the clip encoded as
`wai.video.int_motion` with motion (blocks of 16, a search radius of 4, half-pel vectors, step
*q*) by the reference encoder and decoded; `frame-drop`, the frames 0, 2, 4, …; `one-frame`, the
first frame alone; and `scale-1/2` and `noise-16`, as above, on every frame.
#### 7.5.8 What it survives
The **measurement sets** are generated by the reference from fixed seeds (§13): `spectral`, 16
pictures of 512 × 512 from `wai.generate.spectral`'s generator and its committed model; `leaves`,
16 dead-leaves pictures of 512 × 512, discs of random colour, size and shading painted over each
other, then noise of up to 2 a sample; and `clips`, 4 clips of 8 frames of 256 × 256, a window
panning over two pictures of each kind. Each picture is marked under its own payload. The
**photographs** are 24 lossless photographs of 768 × 512 from a test suite of image coding, not in
this repository, measured with the reference (§13) and named by the SHA-256 of their files in the
corpus's README; their column is informative.
Each cell gives the pictures that gave their payload, of those marked, and in parentheses the most
bits the code corrected among them. No picture gave another payload, and no unmarked picture, under
any attack, gave any.
| attack | `spectral` | `leaves` | photographs |
|---|---|---|---|
| `none` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `int-motion-q4` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `int-motion-q8` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `int-motion-q16` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `int-motion-q32` | 16 / 16 (0) | 16 / 16 (1) | 24 / 24 (4) |
| `int-motion-q48` | 16 / 16 (1) | 16 / 16 (4) | 24 / 24 (2) |
| `staged-base-q8` | 16 / 16 (0) | 16 / 16 (11) | 24 / 24 (20) |
| `denoise3` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (5) |
| `denoise3-x4` | 16 / 16 (0) | 16 / 16 (7) | 24 / 24 (24) |
| `scale-3/4` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (2) |
| `scale-1/2` | 16 / 16 (0) | 16 / 16 (1) | 24 / 24 (6) |
| `scale-1/3` | 16 / 16 (28) | 0 / 16 | 4 / 24 (29) |
| `scale-1/4` | 16 / 16 (0) | 16 / 16 (1) | 23 / 24 (19) |
| `scale-3/2` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `upscale2` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `aspect-3/4` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (2) |
| `half-then-int-motion-q16` | 16 / 16 (0) | 16 / 16 (4) | 24 / 24 (7) |
| `crop-1%` | 16 / 16 (0) | 2 / 16 (29) | 4 / 24 (26) |
| `crop-2%` | 0 / 16 | 0 / 16 | 0 / 24 |
| `crop-5%` | 0 / 16 | 0 / 16 | 0 / 24 |
| `crop-10%` | 0 / 16 | 0 / 16 | 0 / 24 |
| `mirror` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `rotate-90` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `rotate-0.5` | 16 / 16 (1) | 0 / 16 | 0 / 24 |
| `rotate-1` | 0 / 16 | 0 / 16 | 0 / 24 |
| `rotate-2` | 0 / 16 | 0 / 16 | 0 / 24 |
| `noise-4` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `noise-16` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `brightness+32` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (4) |
| `brightness-32` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (5) |
| `contrast-1/2` | 16 / 16 (0) | 16 / 16 (0) | 24 / 24 (1) |
| `contrast-3/2` | 16 / 16 (0) | 16 / 16 (1) | 24 / 24 (10) |
| `local-blur` | 16 / 16 (0) | 16 / 16 (3) | 24 / 24 (21) |
| attack on clips | `clips` |
|---|---|
| `none` | 4 / 4 (0) |
| `clip-int-motion-q8` | 4 / 4 (0) |
| `clip-int-motion-q24` | 4 / 4 (0) |
| `frame-drop` | 4 / 4 (0) |
| `one-frame` | 4 / 4 (2) |
| `scale-1/2` | 4 / 4 (0) |
| `noise-16` | 4 / 4 (0) |
The scheme **claims to survive** an attack only where every picture of every column, marked, gave
its payload and none, unmarked, gave any. For pictures, these attacks, each with the parameter
measured: `int-motion-q4`, `int-motion-q8`, `int-motion-q16`, `int-motion-q32`, `int-motion-q48`,
`staged-base-q8`, `denoise3`, `denoise3-x4`, `scale-3/4`, `scale-1/2`, `scale-3/2`, `upscale2`,
`aspect-3/4`, `half-then-int-motion-q16`, `mirror`, `rotate-90`, `noise-4`, `noise-16`,
`brightness+32`, `brightness-32`, `contrast-1/2`, `contrast-3/2`, `local-blur`. For clips:
`clip-int-motion-q8`, `clip-int-motion-q24`, `frame-drop`, `one-frame`, `scale-1/2`, `noise-16`.
It does **not** claim to survive a crop of any size, a rotation by a small angle, or area
resampling to 1/3 or 1/4, nor any parameter between or beyond those measured. A crop or a small
rotation moves the cells against the grid: half a cell turns a bit's statistic to its opposite,
and the detector searches no shift and no angle. Resampling to 1/4 survives on the measurement
sets, where a 512 × 512 cell of 8 × 8 becomes exactly 2 × 2, and lost one photograph; resampling to
1/3 lands cells on fractions of a pixel and is not survived.
Imperceptibility, as PSNR of the RGB8 buffer and SSIM of the luma (8 × 8 windows at a stride of 4,
*C*₁ = (0.01·255)², *C*₂ = (0.03·255)², the least over a clip's frames):
| set | PSNR, least … mean (dB) | floor (dB) | SSIM, least … mean |
|---|---|---|---|
| `spectral` | 44.86 … 45.02 | 36.66 | 0.9688 … 0.9732 |
| `leaves` | 43.05 … 43.69 | 36.66 | 0.9661 … 0.9696 |
| `clips` | 41.90 … 42.08 | 38.73 | 0.9439 … 0.9495 |
| photographs | 41.39 … 43.58 | 35.76 | 0.9761 … 0.9842 |
### 7.6 Proving a marking and reproducing a detection
A verifier is given, beside the records, the decoded frames of the objects it holds and the
**unmarked frames** of marked objects (§11.1). For each marking, after §7.2's checks, in this
order:
1. its scheme (§7.4): `scheme_mismatch`, `mark_evidence_invalid`;
2. for `wai-mark:tent64`, with the unmarked frames of its object held: they are
`frames`·`width`·`height`·3 bytes and their SHA-256 is the embedding's `unmarked`
(`mark_input_mismatch`), and the scheme's embedding of them (§7.5.4) under the marking's
payload has the SHA-256 `marked` (`mark_recompute_mismatch`). The marking is then **proved**;
otherwise it is **stated**;
3. with an embedding and the object's decoded frames held: their SHA-256 is the embedding's
`marked` (`samples_mismatch`);
4. under a policy that requires the second layer proved (§11.1), a marking that is not proved:
`layer_not_proved`.
A proved marking says that the object's frames are the scheme's embedding of those unmarked frames
under that payload, and nothing else. What it says of the unmarked frames is what the verifier
knows of them: for an object a sink generates at an exact tier (§7.8), the verifier generates them
itself and the proof covers the whole output; for unmarked frames a provider hands over, it covers
the marking step.
For each detection, at §7.3 step 2, in this order: its scheme (§7.4); with a `read` and the
object's decoded frames held, they are `frames`·`width`·`height`·3 bytes and their SHA-256 is
`samples` (`samples_mismatch`); and for `wai-mark:tent64`, the detector (§7.5.5) is run on them,
and the payload it reads, or its finding no mark, is the detection's `payload`
(`detection_irreproducible`). The detection is then **reproduced** (`reproduced`), and otherwise
**not run again** (`not-rerun`). Under a policy that requires the second layer proved, a detection
that is not reproduced is `layer_not_proved`.
So a detection that reports a mark in content that carries none, or in content altered beyond
what the scheme survives (§7.5.8), is refused wherever the verifier holds the content's frames: a
detection agrees with a claim only for a payload the content carries.
### 7.7 The signpost and the resolver
The Code's Measure 3.4(c) has its signatories implement an interoperability solution for their
detection mechanisms by 2 February 2027; one of the solutions it names (point ii) is a publicly
readable **signpost** in the content that tells the public which detection solution to use without
running every provider's. Its Glossary has a signpost solution be an openly disclosed marking
technique within the content, and its detection mechanism, that indicates which watermark
detection mechanism to employ where metadata is not available, and says it may be an imperceptible
machine-readable mark. This extension gives three:
1. **In the content.** `wai-mark:tent64` is itself openly disclosed and read with no key (§7.5.5).
Run on any content, its detector says whether the content carries a WAI mark, with the
false-positive bound of §7.5.6, and the payload it reads gives the class and the claim. The
claim's markings name every scheme marked into its object, each by term and digest, so the mark
is the signpost to the object's other marks as well.
2. **In the metadata.** Each marking gives a C2PA statement (C2PA 2.4 §18.15.5, §18.10),
`{"actions": A, "assertions": D}` written in JCS: `A` is `[{"action": "c2pa.watermarked.bound"}]`,
and `D` holds one soft-binding assertion, `{"label": "c2pa.soft-binding", "data": {"alg": S,
"alg-params": G, "blocks": [{"scope": {}, "value": P}]}}`, `S` the scheme's term, `G` its digest
and `P` the payload, each byte string written here in lowercase hexadecimal and carried as a
CBOR byte string in the manifest. The empty scope covers the whole content. The manifest's
assembler adds the action's hashed reference to the assertion (`relatedAssertions`, C2PA 2.4
§18.15.4.8). The scheme's term is not an entry of C2PA's soft-binding algorithm list, which C2PA
2.4 §9.3.2 recommends `alg` to name and does not require.
3. **A resolver.** A party that serves WAI content **MAY** answer
`GET /.well-known/wai-mark/<name>` (RFC 8615), for each scheme `wai-mark:<name>` it uses, with
the scheme's definition, the bytes whose SHA-256 is the scheme's digest, as `application/json`;
and `GET /.well-known/wai-mark/<name>/payload/<p>`, `<p>` a payload in lowercase hexadecimal,
with a JSON array of the texts of the origin claims it holds whose payload (§7.2) is `<p>`, empty
where it holds none. The definition names the section that specifies the detector, a public
specification any third party implements (the Code's Sub-measure 2.1.1 (i)), of which
`wai-rs` (`synthesis::mark`) and `live-conformance/_wai_mark.py` are two implementations
(§13). A client checks the
definition's digest against the term's before relying on it, and each claim it receives against
the payload it asked for and against the claim's signature. This revision does not register the
well-known name.
### 7.8 A sink that generates
Where an object's envelope generates (§3.3) and a marking of `wai-mark:tent64` names it, the
reconstruction a sink is held to is the scheme's embedding, under the marking's payload, of the
frames its generation produces: the sink generates, then marks, as one integer derivation appended
to the generative one, so the exact tier the generation holds (generation-equivalence,
[determinism-tiers](determinism-tiers.md) §2) covers the mark, and every viewer at that tier
receives the same marked frames. Its log's `present` entry states the marked frames' digest, the
embedding's `marked`; a review of the object names that digest as its reconstruction (§9.1); and a
player that presents the unmarked frames states an output the object does not decode to
(`present_output_irreproducible`, §9.2). A verifier that holds the envelope generates the unmarked
frames itself, and so proves the marking (§7.6) without taking anyone's unmarked frames on trust.
## 8. Live rooms
@ -614,13 +1048,15 @@ for that claim's object (`present_mismatch`). Under a label policy, in this orde
A verifier is given, each in a list: origin claims, marking records, detection records and
reviews; a label policy or none; a player's log or none; the envelopes of the objects it holds
(each named by its content hash); the decoded samples it holds, each with the content hash of
the object decoded; a room or none, as live-rooms §9.1 gives a room verifier its inputs; and a
**policy** of two members:
the object decoded; the unmarked frames it holds (§7.6), each with the content hash of the marked
object they are the unmarked frames of; a room or none, as live-rooms §9.1 gives a room verifier
its inputs; and a **policy** of three members:
| member | values | default | meaning |
|---|---|---|---|
| `dual_layer` | `when-given`, `required` | `when-given` | `required`: §7.1's rule applies (`layer_missing`) |
| `assistive` | `accept-asserted`, `require-proof` | `accept-asserted` | `require-proof`: §4.4 step 4 applies (`assistive_not_proved`) |
| `second_layer` | `accept-stated`, `require-proof` | `accept-stated` | `require-proof`: every marking is proved and every detection reproduced (§7.6, `layer_not_proved`) |
### 11.2 Steps
@ -636,9 +1072,13 @@ a place in it.
inputs and its envelope (§3.3: `input_mismatch`, `class_understated`); its evidence (§4.4:
`evidence_invalid`, `input_samples_mismatch`, `assistive_recompute_mismatch`,
`samples_mismatch`, `assistive_not_proved`).
3. **Marking.** The markings in order (§7.2: `claim_unknown`, `marking_mismatch`); the detections
in order (§7.3: `detection_mismatch`, `watermark_absent`, `layers_disagree`); then, under
`dual_layer` `required`, the origin claims in order (§7.1, `layer_missing`).
3. **Marking.** The markings in order (§7.2: `claim_unknown`, `marking_mismatch`; §7.6:
`scheme_mismatch`, `mark_evidence_invalid`, `mark_input_mismatch`, `mark_recompute_mismatch`,
`samples_mismatch`, `layer_not_proved`); the detections in order (§7.3 step 1,
`detection_mismatch`; step 2, by §7.6: `scheme_mismatch`, `mark_evidence_invalid`,
`samples_mismatch`, `detection_irreproducible`, `layer_not_proved`; steps 3 and 4:
`watermark_absent`, `layers_disagree`); then, under `dual_layer` `required`, the origin claims
in order (§7.1, `layer_missing`).
4. **Reviews**, in order (§9.1: `claim_unknown`, `review_subject_mismatch`).
5. **The room.** Live-rooms §9: a refusal there is this verification's, at the room. A gap is not
a refusal. Then its AI participants' tracks (§8.2, `ai_track_unlabelled`).
@ -652,8 +1092,10 @@ a place in it.
- for each origin claim, in order: its link hash, class and source, its C2PA statement (§6) as
written, and for an assist claim whether it is `proved`, `not-recomputed` or `asserted` (§4.4);
- for each detection, in order: its object and its result, `agree`, `unmarked` or `not-checked`
(§7.3);
- for each marking, in order: its object, its scheme, whether it is `proved` or `stated` (§7.6),
and its C2PA statement (§7.7) as written;
- for each detection, in order: its object, its result, `agree`, `agree-inherited`, `unmarked` or
`not-checked` (§7.3), and whether it was `reproduced` or `not-rerun` (§7.6);
- for each AI participant's track, in the order §8.2 reaches them: its digest and the claim that
labels it;
- for each `review` entry of the log: its index and the review's link hash;
@ -679,14 +1121,20 @@ The codes this extension adds, each with where it is raised:
| `evidence_invalid` | §4.3, §4.4 step 1 |
| `input_samples_mismatch` | §4.4 step 2 |
| `assistive_recompute_mismatch` | §4.4 step 2 |
| `samples_mismatch` | §4.4 step 3 |
| `samples_mismatch` | §4.4 step 3, §7.6 |
| `assistive_not_proved` | §4.4 step 4 |
| `claim_unknown` | §7.2, §9.1, §9.2 |
| `marking_mismatch` | §7.2 |
| `scheme_mismatch` | §7.4, §7.6 |
| `mark_evidence_invalid` | §7.4, §7.5.1, §7.6 |
| `mark_input_mismatch` | §7.6 |
| `mark_recompute_mismatch` | §7.6 |
| `detection_mismatch` | §7.3 |
| `detection_irreproducible` | §7.6 |
| `watermark_absent` | §7.3 |
| `layers_disagree` | §7.3 |
| `layer_missing` | §7.1 |
| `layer_not_proved` | §7.6, §11.1 |
| `ai_track_unlabelled` | §8.2 |
| `player_chain_broken` | §10.3 |
| `present_mismatch` | §10.4 |
@ -718,7 +1166,8 @@ states; refused, its reason and its place. It has:
a room whose AI host states its model in its chat and caption and labels its speech track; and a
review of the generated video presented on an exact path, with labels shown at first exposure,
after an ad break and within an interval.
- **Refused**: each reason of §12 at least once, each a control of one defect: among them a
- **Refused**: each reason of §12 at least once but the three only the scheme's corpus gives
(below), each a control of one defect: among them a
synthesized object labelled captured (`class_understated`), an assistive claim whose
recomputation differs (`assistive_recompute_mismatch`), the two marking layers disagreeing
(`layers_disagree`), a review presented on a non-exact path (`review_path_not_exact`), an AI
@ -737,6 +1186,48 @@ every signature with WAI's strict rule (`claim-conformance/_ed25519.py`) and eve
BLAKE3 written from its specification (`claim-conformance/_blake3.py`); and it mutates copies of
the corpus and requires every copy to be refused.
[`live-conformance/synthesis-mark/`](../live-conformance/synthesis-mark/) holds the registered
scheme's corpus, written from fixed seeds by `wai_synthesis_mark_vectors gen`: `scheme.json`, the
bytes of the scheme's definition (§7.4); the frames its vectors name, each a raw RGB8 file
`frames/<SHA-256>.rgb`; `vectors.json`; and `battery.json`. `vectors.json` holds the stream's
first values and the mask; the codewords of six payloads and the decoding of each with 0, 1, 29,
30 and 40 bits flipped; the embeddings of a generated still, a dead-leaves stand-in for a
photograph and a clip of three frames, each with its marked frames' digest and each bit's
amplitude; what the detector reads from stored frames; a **battery sample**, every picture attack
of §7.5.7 applied to the corpus's still, marked and unmarked, with the digest of each result and
what the detector reads from it; and **cases** of §11.1 whose frames are files. `battery.json` is
the measurement of §7.5.8 over the measurement sets: each picture's PSNR, floor and SSIM, each
attack's tallies, the unmarked detections, the words of §7.5.6 and the attacks every set
survived.
The cases accept a generated still whose marking is proved and whose detection is reproduced,
presented with a review at generation-equivalence (§7.8); a clip proved likewise; a re-encoding
whose detection agrees by inheritance; a crop beyond what the scheme survives, read as unmarked;
and a marking and a detection that are stated. They refuse a marking whose frames carry another
claim's payload and one whose frames have one bit more changed (`mark_recompute_mismatch`);
unmarked frames of another picture (`mark_input_mismatch`); decoded frames that are not the marked
ones (`samples_mismatch`); a detection of a mark in a photograph that carries none and one in the
crop (`detection_irreproducible`); a capture claim over the marked still (`layers_disagree`); a
marking not proved under a policy that requires it (`layer_not_proved`); the registered term under
another digest (`scheme_mismatch`); frames below the least size (`mark_evidence_invalid`); and a
player that presents the unmarked frame (`present_output_irreproducible`). The codes
`mark_input_mismatch`, `mark_recompute_mismatch` and `detection_irreproducible` need frames the
scheme admits, and only this corpus gives them.
`wai_synthesis_mark_vectors verify` and the test `tests/synthesis_mark.rs` check that every file
regenerates byte for byte and no other is there, the battery's among them; that `wai-rs` gives
every vector and case what it states; that §7.5.3's generator polynomial and §7.5.4's definition are
the reference's; and that §7.5.8's claims are attacks the measurement shows survived and §7.5.6's
figures are the measurement's. `wai_synthesis_mark_vectors measure <dir> <out>` runs the battery
over the PNG files of a directory; §7.5.8's photographs' column is its result over the files the
corpus's README names by digest, which this repository does not hold.
`live-conformance/synthesis_mark_verify.py` is the second implementation, written from this text
and sharing no code with `wai-rs`: it reads the scheme's definition and checks its digest; builds
the stream, the shuffle, the signs, the mask and the generator polynomial from §7.5.3; embeds and
detects, with a decoder of its own; applies every picture attack of §7.5.7 and requires the battery
sample's results; verifies every case with `synthesis_verify.py`; checks §7.5.8's claims against
`battery.json`'s tallies; and mutates copies of the corpus and requires every copy to be refused.
## 14. Security and privacy considerations
- A claim carries its signer's authority. A class is a statement; what this extension adds is
@ -744,11 +1235,25 @@ the corpus and requires every copy to be refused.
derivation is checked by recomputation (§4), that a generator's own envelope cannot be claimed
as anything but generated (§3.3), and that the second layer is checked against the first (§7).
A synthesized object whose producer signs no claim at all is caught only by the second layer,
and only once a watermark scheme is registered (§7.4).
and only where its producer marked it (§7.5).
- A watermark payload's seven bytes of link hash locate a claim; they are not a commitment, and a
mark can be copied into other content. A detection that agrees with a claim says the content
carries that claim's mark, not that the content is the claim's subject; the claim's subject is
bound by content hash.
- The registered scheme is read with no key (§7.5.6), so anyone can find the mark and try to
remove it: in the battery, a crop of 2% or a rotation of one degree leaves no mark the detector
reads in any picture measured (§7.5.8). Anyone can also embed a payload of their choosing in any
content, a real photograph among them. The scheme claims no resistance to removal or forgery,
which the Code's Measure 3.3 asks to be assessed: what it survives is the table's processing,
and nothing adversarial is claimed. A forged mark agrees only with the claim it names; that
claim's subject is bound by content hash, so the mark does not make the content its subject;
and content with a claim of its own that reads another claim's mark, not inherited through its
conversions, is refused (`layers_disagree`, §7.3).
- A mark carries its 8-byte payload and nothing else, and a verifier that holds the unmarked
frames proves it (§7.5.6, §7.6): no identifier of a viewer, a session, a device or an account.
The detector runs on the device that holds the content, and needs no upload. A resolver's
payload lookup (§7.7) tells the resolver which claim a client looks for; a client that does not
want that holds the claims itself, or asks a party it trusts.
- Evidence states digests of samples, never samples. A verifier that recomputes holds the input's
decoded samples, which it decoded itself.
- A player's log is signed under a key of the session and names objects, claims, labels and

View file

@ -25,6 +25,7 @@ and the independent verifier of each, beside it.
| [`access/`](access/) | [access-services](../extensions/access-services.md) (`wai.access.services`) | `wai_access_services_vectors gen` | `access_services_verify.py` |
| [`presentation/`](presentation/) | [ad-presentation](../extensions/ad-presentation.md) (`wai.live.ad_presentation`) | `wai_ad_presentation_vectors gen` | `ad_presentation_verify.py` |
| [`exploration/`](exploration/) | [exploration](../extensions/exploration.md) (`wai.media.exploration`) | `wai_exploration_vectors gen` | `exploration_verify.py` |
| [`synthesis-mark/`](synthesis-mark/) | [synthesis](../extensions/synthesis.md) §7.4 to §7.8 (the watermark scheme `wai-mark:tent64`) | `wai_synthesis_mark_vectors gen` | `synthesis_mark_verify.py` |
**Provenance.** Generated here: each directory's corpus is written by its row's generator
(`gen`, in `wai/wai-rs`), from records authored in code and from fixed seeds, and each
@ -57,6 +58,7 @@ python3 e2ee_verify.py e2ee
python3 live_watermark_verify.py watermark
python3 c2pa_live_verify.py c2pa-live
python3 synthesis_verify.py synthesis
python3 synthesis_mark_verify.py synthesis-mark
python3 ai_usage_verify.py usage
python3 contribution_verify.py contribution
python3 settlement_verify.py settlement

View file

@ -0,0 +1,500 @@
"""The registered watermark scheme `wai-mark:tent64` of extensions/synthesis.md §7.5, and the
attacks of its battery (§7.5.7), written from the extension's text.
Shared by the two Python verifiers of the synthesis corpora (synthesis_verify.py and
synthesis_mark_verify.py); it shares no code with wai-rs. Standard library only.
Frames are `bytes` of RGB8, frame after frame. Every function is integer arithmetic, so its
results are the extension's to the byte.
"""
import hashlib
SCHEME = "wai-mark:tent64"
GRID = 64
CELLS = GRID * GRID
LENGTH = 248 # the shortened code's length (§7.5.3)
PARITY = 184
T = 29 # the errors the code corrects
CELLS_PER_BIT = 16
AMPLITUDE = 12 # §7.5.4
MARGIN = (3, 2) # 2·σ·T ≥ 3·Q
MIN_SIDE = 128 # §7.5.1
MAX_FRAME_BYTES = 1 << 28
MAX_INT = (1 << 53) - 1
CLASS_MAX = 3
def sha256(b):
return hashlib.sha256(b).digest()
def stream(label, seed=None):
"""§7.5.3: 32-bit big-endian words, eight from each SHA-256(label ‖ [seed ‖] n)."""
n = 0
while True:
d = hashlib.sha256(label + (b"" if seed is None else seed.to_bytes(4, "big")) + n.to_bytes(4, "big")).digest()
for o in range(0, 32, 4):
yield int.from_bytes(d[o:o + 4], "big")
n += 1
def _layout():
r = stream(b"wai-mark:tent64 stream")
perm = list(range(CELLS))
for i in range(CELLS - 1, 0, -1):
j = next(r) % (i + 1)
perm[i], perm[j] = perm[j], perm[i]
signs = [1 if next(r) % 2 == 0 else -1 for _ in range(CELLS)]
mask = [next(r) & 1 for _ in range(LENGTH)]
return perm, signs, mask
PERM, SIGNS, MASK = _layout()
BIT_CELLS = [PERM[CELLS_PER_BIT * j: CELLS_PER_BIT * j + CELLS_PER_BIT] for j in range(LENGTH)]
# ---------------------------------------------------------------- the code, §7.5.3
EXP = [0] * 510
LOG = [0] * 256
_x = 1
for _i in range(255):
EXP[_i] = _x
LOG[_x] = _i
_x <<= 1
if _x & 0x100:
_x ^= 0x11D # x^8 + x^4 + x^3 + x^2 + 1
for _i in range(255, 510):
EXP[_i] = EXP[_i - 255]
def gmul(a, b):
return 0 if a == 0 or b == 0 else EXP[LOG[a] + LOG[b]]
def generator():
"""The least common multiple of the minimal polynomials of α^1 … α^58, as an integer whose
bit d is the coefficient of x^d."""
g, cosets = 1, []
for i in range(1, 2 * T + 1):
coset, j = [], i % 255
while j not in coset:
coset.append(j)
j = 2 * j % 255
if sorted(coset) in cosets:
continue
cosets.append(sorted(coset))
p = [1] # coefficients from x^0 up
for j in coset:
q = [0] * (len(p) + 1)
for d, c in enumerate(p):
q[d + 1] ^= c
q[d] ^= gmul(c, EXP[j])
p = q
assert all(c in (0, 1) for c in p), "a minimal polynomial is binary"
pm = sum(c << d for d, c in enumerate(p))
prod, a, b = 0, g, pm # product over GF(2)
while b:
if b & 1:
prod ^= a
a <<= 1
b >>= 1
g = prod
return g
G = generator()
def encode(payload):
"""The codeword of an 8-byte payload: bit j the coefficient of x^(247 − j)."""
m = int.from_bytes(payload, "big")
r = m << PARITY
for d in range(r.bit_length() - 1, PARITY - 1, -1):
if r >> d & 1:
r ^= G << (d - PARITY)
c = (m << PARITY) | r
return [c >> (LENGTH - 1 - j) & 1 for j in range(LENGTH)]
def _syndromes(c):
return [_eval(c, i) for i in range(1, 2 * T + 1)]
def _eval(c, i):
v = 0
e = 0
while c:
if c & 1:
v ^= EXP[i * e % 255]
c >>= 1
e += 1
return v
def decode(word):
"""Bounded-distance decoding within 29: (payload, bits corrected), or None where no codeword
is within 29 bits or the payload's first byte is above 3."""
c = 0
for b in word:
c = (c << 1) | b
s = _syndromes(c)
errors = 0
if any(s):
# The error locator: the shortest linear recurrence the syndromes satisfy, over GF(2^8).
lam, prev, L, m, bd = [1], [1], 0, 1, 1
for n in range(2 * T):
d = s[n]
for i in range(1, min(L, len(lam) - 1) + 1):
d ^= gmul(lam[i], s[n - i])
if d == 0:
m += 1
continue
coef = gmul(d, EXP[(255 - LOG[bd]) % 255])
old = lam[:]
lam = lam + [0] * max(0, len(prev) + m - len(lam))
for i, bv in enumerate(prev):
lam[i + m] ^= gmul(coef, bv)
if 2 * L <= n:
L, prev, bd, m = n + 1 - L, old, d, 1
else:
m += 1
while len(lam) > 1 and lam[-1] == 0:
lam.pop()
if len(lam) - 1 != L or L > T:
return None
roots = []
for e in range(255):
v = 0
for i, co in enumerate(lam):
if co:
v ^= gmul(co, EXP[(-e * i) % 255])
if v == 0:
roots.append(e)
if len(roots) != L or any(e >= LENGTH for e in roots):
return None
for e in roots:
c ^= 1 << e
if any(_syndromes(c)):
return None
errors = L
payload = (c >> PARITY).to_bytes(8, "big")
if payload[0] > CLASS_MAX:
return None
return payload, errors
# ---------------------------------------------------------------- frames, §7.5.1 and §7.5.2
def admits(w, h, n):
return w >= MIN_SIDE and h >= MIN_SIDE and n >= 1 and w * h * 3 <= MAX_FRAME_BYTES and w * h * 3 * n <= MAX_INT
def luma(r, g, b):
return (77 * r + 150 * g + 29 * b + 128) >> 8
def tent(u, length):
c = 2 * u + 1 - length
sg = (c > 0) - (c < 0)
return sg * (length - abs(2 * abs(c) - length))
def axis(n):
"""Per sample: (cell index, tent, cell length); and the edges."""
edges = [i * n // GRID for i in range(GRID + 1)]
cell, tents, lens = [], [], []
for i in range(GRID):
L = edges[i + 1] - edges[i]
for x in range(edges[i], edges[i + 1]):
cell.append(i)
tents.append(tent(x - edges[i], L))
lens.append(L)
return cell, tents, lens, edges
def luma_plane(frame):
return [luma(frame[p], frame[p + 1], frame[p + 2]) for p in range(0, len(frame), 3)]
def bit_statistics(y, w, h):
"""T_j over one luma plane of w × h."""
cx, fx, _, _ = axis(w)
cy, fy, _, _ = axis(h)
s = [0] * CELLS
for row in range(h):
acc = [0] * GRID
base = row * w
for col in range(w):
acc[cx[col]] += fx[col] * y[base + col]
r0, f = cy[row] * GRID, fy[row]
for i in range(GRID):
s[r0 + i] += f * acc[i]
return [sum(SIGNS[k] * s[k] for k in BIT_CELLS[j]) for j in range(LENGTH)]
# ---------------------------------------------------------------- embedding, §7.5.4
def embed(unmarked, w, h, n, payload):
"""The marked frames, and each frame's amplitudes (a string of 248 digits as base-36)."""
code = encode(payload)
sigma = [1 if code[j] ^ MASK[j] else -1 for j in range(LENGTH)]
_, fx, lx, ex = axis(w)
_, fy, ly, ey = axis(h)
fb = w * h * 3
out = bytearray(unmarked)
amps = []
for f in range(n):
frame = unmarked[f * fb:(f + 1) * fb]
chosen = []
for j in range(LENGTH):
sg = sigma[j]
pixels = [] # (offset, ε·ω, z, ν)
q = 0
for k in BIT_CELLS[j]:
i, jj = k % GRID, k // GRID
eps = SIGNS[k]
for row in range(ey[jj], ey[jj + 1]):
for col in range(ex[i], ex[i + 1]):
om = fx[col] * fy[row]
q += abs(om)
pixels.append(((row * w + col) * 3, eps * om, sg * eps * om, lx[col] * ly[row]))
def at(a):
t, vals = 0, []
for p, eo, z, nu in pixels:
d = (z > 0) - (z < 0)
d *= (2 * a * abs(z) + nu) // (2 * nu)
r = min(255, max(0, frame[p] + d))
g = min(255, max(0, frame[p + 1] + d))
b = min(255, max(0, frame[p + 2] + d))
t += eo * luma(r, g, b)
vals.append((p, r, g, b))
return t, vals
pick = None
for a in range(AMPLITUDE + 1):
t, vals = at(a)
if MARGIN[1] * sg * t >= MARGIN[0] * q:
pick = (a, vals)
break
if pick is None:
pick = (AMPLITUDE, at(AMPLITUDE)[1])
a, vals = pick
for p, r, g, b in vals:
out[f * fb + p] = r
out[f * fb + p + 1] = g
out[f * fb + p + 2] = b
chosen.append(a)
amps.append(chosen)
return bytes(out), amps
# ---------------------------------------------------------------- detection, §7.5.5
def orient(y, w, h, o):
ow, oh = (h, w) if o & 4 else (w, h)
out = [0] * (w * h)
for yy in range(oh):
y1 = oh - 1 - yy if o & 2 else yy
for xx in range(ow):
x1 = ow - 1 - xx if o & 1 else xx
sx, sy = (y1, x1) if o & 4 else (x1, y1)
out[yy * ow + xx] = y[sy * w + sx]
return out, ow, oh
def read_word(samples, w, h, n, o):
fb = w * h * 3
t = [0] * LENGTH
for f in range(n):
y, ow, oh = orient(luma_plane(samples[f * fb:(f + 1) * fb]), w, h, o)
for j, v in enumerate(bit_statistics(y, ow, oh)):
t[j] += v
return [(1 if t[j] > 0 else 0) ^ MASK[j] for j in range(LENGTH)]
def detect(samples, w, h, n):
"""(payload, orientation, errors) of the first orientation that decodes, or None."""
if not admits(w, h, n) or len(samples) != w * h * 3 * n:
return None
for o in range(8):
got = decode(read_word(samples, w, h, n, o))
if got is not None:
return got[0], o, got[1]
return None
# ---------------------------------------------------------------- the battery, §7.5.7
def rnd(n, d):
"""n / d rounded to the nearest integer, halves away from zero."""
sg = (n > 0) - (n < 0)
return sg * ((2 * abs(n) + d) // (2 * d))
def clamp(v):
return 0 if v < 0 else 255 if v > 255 else v
def int_motion(frame, q):
return bytes(clamp(128 + rnd(v - 128, q) * q) for v in frame)
def upscale2(px, w, h):
"""wai-synthesis:upscale2, §4.1."""
out = bytearray(4 * w * h * 3)
ow = 2 * w
for y in range(h):
y1 = min(y + 1, h - 1)
for x in range(w):
x1 = min(x + 1, w - 1)
for c in range(3):
a, b = px[(y * w + x) * 3 + c], px[(y * w + x1) * 3 + c]
cc, d = px[(y1 * w + x) * 3 + c], px[(y1 * w + x1) * 3 + c]
out[((2 * y) * ow + 2 * x) * 3 + c] = a
out[((2 * y) * ow + 2 * x + 1) * 3 + c] = (a + b + 1) >> 1
out[((2 * y + 1) * ow + 2 * x) * 3 + c] = (a + cc + 1) >> 1
out[((2 * y + 1) * ow + 2 * x + 1) * 3 + c] = (a + b + cc + d + 2) >> 2
return bytes(out)
def denoise3(px, w, h):
"""wai-synthesis:denoise3, §4.2."""
k = ((1, 2, 1), (2, 4, 2), (1, 2, 1))
out = bytearray(w * h * 3)
for y in range(h):
for x in range(w):
for c in range(3):
s = 8
for j in range(3):
yy = min(max(y + j - 1, 0), h - 1)
for i in range(3):
xx = min(max(x + i - 1, 0), w - 1)
s += k[j][i] * px[(yy * w + xx) * 3 + c]
out[(y * w + x) * 3 + c] = s >> 4
return bytes(out)
def _weights(n, m):
out = []
for d in range(m):
lo, hi = d * n, (d + 1) * n
row = []
for s in range(lo // m, -(-hi // m)):
o = min(hi, (s + 1) * m) - max(lo, s * m)
if o > 0:
row.append((s, o))
out.append(row)
return out
def area(px, w, h, nw, nh):
wx, wy = _weights(w, nw), _weights(h, nh)
den = w * h
out = bytearray(nw * nh * 3)
for dy, ry in enumerate(wy):
for dx, rx in enumerate(wx):
for c in range(3):
num = 0
for sy, oy in ry:
for sx, ox in rx:
num += oy * ox * px[(sy * w + sx) * 3 + c]
out[(dy * nw + dx) * 3 + c] = (2 * num + den) // (2 * den)
return bytes(out)
ROTATIONS = {"rotate-0.5": (65534, 572), "rotate-1": (65526, 1144), "rotate-2": (65496, 2287)}
def rotate(px, w, h, cs, sn):
one = 1 << 16
def p(x, y, c):
return 0 if x < 0 or y < 0 or x >= w or y >= h else px[(y * w + x) * 3 + c]
out = bytearray(w * h * 3)
for y in range(h):
for x in range(w):
u, v = 2 * x + 1 - w, 2 * y + 1 - h
X = (cs * u + sn * v + (w - 1) * one) >> 1
Y = (-sn * u + cs * v + (h - 1) * one) >> 1
xi, fx, yi, fy = X >> 16, X & 0xFFFF, Y >> 16, Y & 0xFFFF
for c in range(3):
val = (p(xi, yi, c) * (one - fx) * (one - fy) + p(xi + 1, yi, c) * fx * (one - fy)
+ p(xi, yi + 1, c) * (one - fx) * fy + p(xi + 1, yi + 1, c) * fx * fy)
out[(y * w + x) * 3 + c] = (val + (1 << 31)) >> 32
return bytes(out)
def attack(name, px, w, h, seed):
"""One picture attack of §7.5.7 on a single frame: (result, width, height)."""
if name == "none":
return px, w, h
if name.startswith("int-motion-q"):
return int_motion(px, int(name[len("int-motion-q"):])), w, h
if name == "staged-base-q8":
hw, hh = w // 2, h // 2
half = bytearray(hw * hh * 3)
for y in range(hh):
for x in range(hw):
for c in range(3):
s = sum(px[((2 * y + dy) * w + 2 * x + dx) * 3 + c] for dy in (0, 1) for dx in (0, 1))
half[(y * hw + x) * 3 + c] = (s + 2) >> 2
return upscale2(int_motion(bytes(half), 8), hw, hh), 2 * hw, 2 * hh
if name == "denoise3":
return denoise3(px, w, h), w, h
if name == "denoise3-x4":
for _ in range(4):
px = denoise3(px, w, h)
return px, w, h
if name.startswith("scale-"):
n, d = map(int, name[len("scale-"):].split("/"))
nw, nh = w * n // d, h * n // d
return area(px, w, h, nw, nh), nw, nh
if name == "upscale2":
return upscale2(px, w, h), 2 * w, 2 * h
if name == "aspect-3/4":
nw = w * 3 // 4
return area(px, w, h, nw, h), nw, h
if name == "half-then-int-motion-q16":
nw, nh = w // 2, h // 2
return int_motion(area(px, w, h, nw, nh), 16), nw, nh
if name.startswith("crop-"):
pc = int(name[len("crop-"):-1])
dx, dy = w * pc // 200, h * pc // 200
nw, nh = w - 2 * dx, h - 2 * dy
out = b"".join(px[((y * w) + dx) * 3:((y * w) + dx + nw) * 3] for y in range(dy, dy + nh))
return out, nw, nh
if name == "mirror":
return b"".join(px[(y * w + x) * 3:(y * w + x) * 3 + 3] for y in range(h) for x in range(w - 1, -1, -1)), w, h
if name == "rotate-90":
return b"".join(px[((h - 1 - xp) * w + yp) * 3:((h - 1 - xp) * w + yp) * 3 + 3] for yp in range(w) for xp in range(h)), h, w
if name in ROTATIONS:
cs, sn = ROTATIONS[name]
return rotate(px, w, h, cs, sn), w, h
if name.startswith("noise-"):
s = int(name[len("noise-"):])
r = stream(b"wai-mark:battery noise", seed)
return bytes(clamp(v + next(r) % (2 * s + 1) - s) for v in px), w, h
if name.startswith("brightness"):
b = int(name[len("brightness"):])
return bytes(clamp(v + b) for v in px), w, h
if name.startswith("contrast-"):
n, d = map(int, name[len("contrast-"):].split("/"))
return bytes(clamp(128 + rnd((v - 128) * n, d)) for v in px), w, h
if name == "local-blur":
out = bytearray(px)
x0, x1, y0, y1 = w // 4, 3 * w // 4, h // 4, 3 * h // 4
for by in range(y0, y1, 16):
for bx in range(x0, x1, 16):
ex, ey = min(bx + 16, x1), min(by + 16, y1)
cnt = (ex - bx) * (ey - by)
for c in range(3):
tot = sum(px[(y * w + x) * 3 + c] for y in range(by, ey) for x in range(bx, ex))
mval = (2 * tot + cnt) // (2 * cnt)
for y in range(by, ey):
for x in range(bx, ex):
out[(y * w + x) * 3 + c] = mval
return bytes(out), w, h
raise ValueError("no picture attack named %r" % name)

View file

@ -0,0 +1,83 @@
# live-conformance/synthesis-mark — the watermark scheme `wai-mark:tent64`
The corpus of the registered watermark scheme of
[`extensions/synthesis.md`](../../extensions/synthesis.md) §7.4 to §7.8, and of its measurement
(§7.5.7, §7.5.8):
- `scheme.json`: the scheme's definition, the bytes whose SHA-256 is the scheme's digest (§7.4).
- `frames/<SHA-256>.rgb`: every set of frames a vector names, raw RGB8, frame after frame, each
file named by the SHA-256 of its bytes.
- `vectors.json`:
- `scheme`: the term, the digest, the generator polynomial, the shuffle's first and last 16
entries, the first 16 signs, the mask, and the false-positive bound of §7.5.6;
- `codewords`, each a payload and its codeword (`0` and `1`, bit 0 first); `decodes`, each a
received word, how many bits of a codeword it flips, and the payload and the bits corrected
it decodes to, or `null`;
- `embeds`, each unmarked frames (`unmarked`, a file's digest), their count and size, a
payload, the marked frames' digest (`marked`), each bit's amplitude per frame (a hexadecimal
digit each, bit 0 first), the bits left short of the margin per frame, and the SSE;
- `detects`, each stored frames (`samples`), their count and size, and what the detector reads:
the `payload`, the `orientation` and the `errors` corrected, or `null`;
- `battery_sample`, each a picture attack of §7.5.7 (`attack`, at its `seed`) applied to the
still's marked or unmarked frame (`frames`, `which`): the result's size and digest, and what
the detector reads from it;
- `cases`, each a case of §11.1 with its `policy`, its records as JSON texts, its `objects`,
its `decoded` and `unmarked` frames, each the content hash of an `object` and a file's
digest, and what a verifier gives it, as `live-conformance/synthesis/` states its vectors'.
- `battery.json`: the measurement of §7.5.8 over the measurement sets: the sets, the attacks, each
picture's size, payload, SSE, PSNR, floor, SSIM and bits left short, each attack's tallies per
set (`detected`, `missed`, `wrong`, the most `errors` corrected), the unmarked pictures'
readings, the words read from unmarked pictures (§7.5.6), the false-positive bound, and the
attacks every set survived.
**Mark equivalence.** A conforming implementation gives every codeword, decoding, embedding,
detection and battery sample what the corpus states, and every case its outcome, reason, place
and statements. One that gives anything else does not conform.
**Provenance.** `wai_synthesis_mark_vectors gen` writes every file from fixed seeds: the
pictures are generated by the crate, the still and the measurement set `spectral` by
`wai.generate.spectral`'s generator with its committed model
(`wai-rs/tests/vectors/generate/generate_spectral_model.json`), the dead-leaves pictures and the
clips by `synthesis_mark::leaves` and `synthesis_mark::clips`; every key is SHA-256 of the
corpus's label and an index, every noise and every error pattern a word of a SHA-256 stream, and
Ed25519 signing is deterministic, so every file is the same bytes on every machine. No picture
comes from outside the repository.
The photographs' column of §7.5.8 is `wai_synthesis_mark_vectors measure` run over 24 lossless
photographs of 768 × 512, a test suite of image coding that this repository does not hold or
redistribute, measured where they were held. Their files, by SHA-256:
| file |
|---|
| `a56e27cbf5f843c048b6af1d6e090760e9c92fadba88b7dee0205918a37523bd` |
| `4f4b74a79237e311d72cad958237b5f7088d8bce1c82305ebefe1a70e3022dfd` |
| `e25ca1ff2f0c0cb5fdfd5f9b0a0bb21ac4c3de3c84a67f35b09a85d3306249db` |
| `e3b946107c5d3441c022f678d0c3caf1e224d81b1604ba840a4f88e562de61aa` |
| `10349e963c5c813d327852f82c1795fa4148d69fedffc4c589bee458e3ac3d53` |
| `363510303b715d4cbc384e1ce227e466b613a09e1b71ae985882bf8e7fbd9b18` |
| `b77d3f006f42414bb242222e0482e750c0fb9e5ee8d4bed2f6f11c5605fe54a4` |
| `ba23983c76b4832ee0e8af0592664756841a16779acd69f792e268fb6d13d6e7` |
| `6a4361c2fc194feb4edaa9f9a4a0620fb9943e460ac7fdf037fb0f6dd6607a7d` |
| `9dfb70f5867c29ff9ed6313683f19b3d867849e40fbc0c4c54a4a89df341cf23` |
| `7936814b58b5387fce2e4e2488b4ec830dadd95fa9520f358ddb30990b50f2b6` |
| `d78c37c2f04f23761ed2367dd77e2db584ddd4c3950833fecf89f199a8126980` |
| `bc34a3ce58dea09dce1704c997171602de90cb34d0c8503a988b77f473d39b08` |
| `55a94550ff18f3246c4074fd32b77b0c74447c26b6ad274d564d999c0450ba6e` |
| `7538cbb80cb9103606c48b806eae57d56c885c7f90b9b3be70a41160f9cbb683` |
| `a89c7268ccd4718ba424a99fc4643c572cf692ca6eae887185ceb4e9f11d2e54` |
| `37afcc89fbdcb76d9518e04b2fc011027e2f4cd14b3b2f83cefd721641a47c5b` |
| `1a9258c365988961d87a0598725b609139c303ad48a5aad6c503c3b1a87849aa` |
| `b7450b264b1b0a411390d8931b112c27905a992520fc90569dc4b920aa32bbdc` |
| `3b46c71e3b92a563820ba32936be8330c586c41f938efd94be938386aae4328a` |
| `ac958597c82073f6bb65129c68f72b651db5b9efd82e11547d07350214bc268b` |
| `1cee58eb1f2d9c7ebb254d208a03c783ce6cf2c4d8c2cf45e235dd23b4ce1b29` |
| `e3111a2fd4da24af15d6459ef9eacfe54106b38e27b4a21821b75c3f5d2d5baf` |
| `1071c68372cc5a01435c2c225a5cf7d4bb803846ec08bb6b3d6721b156d7cb96` |
From `wai/wai-rs`, the reference regenerates and checks the corpus; from this directory, the
second implementation checks it:
```
cargo run --release --no-default-features --features synthesis_mark --bin wai_synthesis_mark_vectors -- verify ../live-conformance/synthesis-mark
python3 -I ../synthesis_mark_verify.py .
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
{"amplitude":12,"cells_per_bit":16,"class_max":3,"code":[255,71,29],"field":"11d","generator":"140a722a1a468d36d87a25364e685922a1e56fd1a478c1d","grid":64,"length":248,"luma":[77,150,29,128,8],"margin":[3,2],"max_frame_bytes":268435456,"media":["image","video"],"min_height":128,"min_width":128,"orientations":8,"pattern":"tent","sample":"RGB8","scheme":"wai-mark:tent64","specification":"extensions/synthesis.md section 7.5","stream":"wai-mark:tent64 stream"}

File diff suppressed because it is too large Load diff

View file

@ -104,7 +104,8 @@
"about": "a capture, a neural codec's reconstruction of it, and two pinned integer derivations of that reconstruction, each recomputed from its input's decoded samples and proved (§3, §4)",
"policy": {
"dual_layer": "when-given",
"assistive": "require-proof"
"assistive": "require-proof",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -137,6 +138,7 @@
"samples": "7430b38a6b8f95698f735893754a8a929173b49c7dbf7ba1c171bac4b9a7d4cd87ebac85"
}
],
"unmarked": [],
"room": null,
"outcome": "accept",
"statements": {
@ -170,6 +172,7 @@
"assistive": "proved"
}
],
"markings": [],
"detections": [],
"ai_tracks": [],
"reviews_presented": [],
@ -183,7 +186,8 @@
"about": "a float super-resolution step that states the assistive class with attest evidence: asserted, not proved, under a policy that accepts assertions (§4.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -197,6 +201,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "accept",
"statements": {
@ -223,6 +228,7 @@
"assistive": "asserted"
}
],
"markings": [],
"detections": [],
"ai_tracks": [],
"reviews_presented": [],
@ -236,7 +242,8 @@
"about": "a generated video, a pinned algorithmic audio generation, an inpainted capture reviewed by a person, a composite of a capture and the generated video, and a transcode of it; each synthesized object marked in both layers, a detection agreeing with the video's claim, and one finding no mark on the capture (§5, §6, §7)",
"policy": {
"dual_layer": "required",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -247,15 +254,15 @@
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[{\"class\":\"synthesized\",\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"source\":\"trainedAlgorithmicMedia\"}],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[],\"operation\":\"wai.video.hevc\",\"oversight\":null,\"record\":null,\"sig\":\"1b766b2c66a402d8f0a5d3a79fd2a576060781f603e0f6b105a5400bc6b1358c1c096d2d51fe8fa469b7367907fd1cff54cfaa9e4b1000fccb89ea41e8b63a0f\",\"signer_pubkey\":\"ff703397c2545c095bb3a790fde7399961c3076bbefcac542792dc4c00acdd6c\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"convert\",\"subject\":{\"content_hash\":\"c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd\",\"type\":\"object\"}}"
],
"markings": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"1c6c99343e99185ae70d5c0d496628cbaa7e00bd50c6539f1a658ab82e663cbe9ea2302b9c125db6005fa5da8eb837f23f843f0e26af975ca63b794c8942610a\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"08c12d093fe7df981379a7bd0ba70f8b9fb490a705c987e1ed13f98b16b23663\",\"payload\":\"0308c12d093fe7df\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"551cebcec062e1e13ce1def1f411e4be23f1462ee02abcc2b847bd456a94ff65b7013e7bf0922f1c1239f6b6a9afd9f830cfa13a3d545d914848668c43e01f0e\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"e7a086610eb064e991d86cf59ca2239c6a1cadda6e824140efe8c1530898aef5\",\"payload\":\"03e7a086610eb064\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"ac99705a34d7f3c76e50f8ed8a18d4d30dfe59230220561bae5bb581656bbedc77b1bc343af487db21c68f5028b0399289818fcaa8fc3128f3de62e62ffe330e\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"501e6fb06de91e323282c5a8afd400d21adcabd99576bbc7a8b6b5fccf05524a\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"9dec524cf50ebf41ac92d3531d657d30ffe0b9c68c14aed8641b1a5c6affb014\",\"payload\":\"039dec524cf50ebf\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"0569b219a150db512882b331bf4a7fb17aee13c9494904f5f783cbf6bf405a98a68f56574b6bd11a5e41d3f463ca4206d60eae8d31cdb8f7019a9ab92e4dff0a\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"19db0b61e5caf49013bb45bed4e9d77f80abe99a4957e3b9a758c87a47ed41a1\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"bbf4d18b4bdf0851e5bcdf02d2ef994a8cdece8e4f160b6cd25a0d4bc3f1a14d\",\"payload\":\"03bbf4d18b4bdf08\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"b209591736654bb122938823a1aa1ad6a10c6e09cf12494a67be06175ddb7a06ddf1c31495c8ef054d6a35e48f9eef4f3c961e9730b3ea59226d79c0feffc20b\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd\"}"
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"08cb06fb6c5fd6d27ac67aff2e1555f89339245d0dbc415a455bbcda0bee4a7f8622e361358438e12d2620fea13d6e5ec8fe9a84e10077b5d49878fbb0cc1a08\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"08c12d093fe7df981379a7bd0ba70f8b9fb490a705c987e1ed13f98b16b23663\",\"payload\":\"0308c12d093fe7df\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"0d040a278e7524ee3c0c11f4529a5e668155078f30737c36b434dbaa831847c033c5a34e1e7d800f6bcfd59b837eb03b031655abec3ff56bcf4d313d1a1a1d02\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"e7a086610eb064e991d86cf59ca2239c6a1cadda6e824140efe8c1530898aef5\",\"payload\":\"03e7a086610eb064\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"239027f72efda6803b1c6911c91eec9a60f707e35c98b48a142241b5c40da65c794084004541699cf391537be2050fc976cf514e9f091e2361348cfca6c18c07\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"501e6fb06de91e323282c5a8afd400d21adcabd99576bbc7a8b6b5fccf05524a\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"9dec524cf50ebf41ac92d3531d657d30ffe0b9c68c14aed8641b1a5c6affb014\",\"payload\":\"039dec524cf50ebf\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"c43cf2097fef257db70ac1b272100f5db120117bcbf2037db7f3fcdc638090c649d53c899e881bf7de05acf082da9fb762324c975f2d0b45ca9eba95ee0e4407\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"19db0b61e5caf49013bb45bed4e9d77f80abe99a4957e3b9a758c87a47ed41a1\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"bbf4d18b4bdf0851e5bcdf02d2ef994a8cdece8e4f160b6cd25a0d4bc3f1a14d\",\"payload\":\"03bbf4d18b4bdf08\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"f14e6db321506ecae4d055640c12f4e2b33e56479aca3f8146fc2019f8332ab7a395946aa8a81c73411608ef024e3797d0d218030e5261fc0354668110b16804\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd\"}"
],
"detections": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"f10cf6cdf66a82d84042dc410bda131a98f5ef21730b4d56ee473ebf3c599efdcf78850458060f68db43054b270e149ffc9af1f113035286af3f25f33067c40f\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"af7d6d6160203509f907d0f592cba7b2199cd9fa58f2c8d282273c4511cfd525e0a9f36a7bbd96141bb126bb5985f1e3f3f25626005aa5988a9906c98fd2b604\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\"}"
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"3143378c4df8b1f7eeb1aa91075e614940886193a6b5b0567442dd7c18796717b193ad5f4bf2f794742694fd210ac70c35fb6d0061d8c3494cc86a827cecb103\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":null,\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"e334eedd5e3e8db5fe64cf45496fee4b60bd87ea8598f9f34f1e71f355ae91e87829f37b37cdada1de5ffe3bd5fad39359022399b2697f3abf4debabcdf5d808\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\"}"
],
"reviews": [],
"label_policy": null,
@ -269,6 +276,7 @@
"57414931670000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227065726365707475616c222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e68657663227d7d4000000094e921099bea0c893bce9d14f037d535670e1f3f3e04ef819bb5aaa20f70edac4d3d7518e875ab1bc990c7524dcc4d6731add333db596ef78a24984f9cd87bb8"
],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "accept",
"statements": {
@ -316,14 +324,48 @@
"assistive": null
}
],
"markings": [
{
"subject": "66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5",
"scheme": "wai-mark:conformance",
"status": "stated",
"c2pa": "{\"actions\":[{\"action\":\"c2pa.watermarked.bound\"}],\"assertions\":[{\"data\":{\"alg\":\"wai-mark:conformance\",\"alg-params\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"blocks\":[{\"scope\":{},\"value\":\"03ead74abe77e324\"}]},\"label\":\"c2pa.soft-binding\"}]}"
},
{
"subject": "4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917",
"scheme": "wai-mark:conformance",
"status": "stated",
"c2pa": "{\"actions\":[{\"action\":\"c2pa.watermarked.bound\"}],\"assertions\":[{\"data\":{\"alg\":\"wai-mark:conformance\",\"alg-params\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"blocks\":[{\"scope\":{},\"value\":\"0308c12d093fe7df\"}]},\"label\":\"c2pa.soft-binding\"}]}"
},
{
"subject": "501e6fb06de91e323282c5a8afd400d21adcabd99576bbc7a8b6b5fccf05524a",
"scheme": "wai-mark:conformance",
"status": "stated",
"c2pa": "{\"actions\":[{\"action\":\"c2pa.watermarked.bound\"}],\"assertions\":[{\"data\":{\"alg\":\"wai-mark:conformance\",\"alg-params\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"blocks\":[{\"scope\":{},\"value\":\"03e7a086610eb064\"}]},\"label\":\"c2pa.soft-binding\"}]}"
},
{
"subject": "19db0b61e5caf49013bb45bed4e9d77f80abe99a4957e3b9a758c87a47ed41a1",
"scheme": "wai-mark:conformance",
"status": "stated",
"c2pa": "{\"actions\":[{\"action\":\"c2pa.watermarked.bound\"}],\"assertions\":[{\"data\":{\"alg\":\"wai-mark:conformance\",\"alg-params\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"blocks\":[{\"scope\":{},\"value\":\"039dec524cf50ebf\"}]},\"label\":\"c2pa.soft-binding\"}]}"
},
{
"subject": "c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd",
"scheme": "wai-mark:conformance",
"status": "stated",
"c2pa": "{\"actions\":[{\"action\":\"c2pa.watermarked.bound\"}],\"assertions\":[{\"data\":{\"alg\":\"wai-mark:conformance\",\"alg-params\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"blocks\":[{\"scope\":{},\"value\":\"03bbf4d18b4bdf08\"}]},\"label\":\"c2pa.soft-binding\"}]}"
}
],
"detections": [
{
"subject": "66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5",
"result": "agree"
"result": "agree",
"rerun": "not-rerun"
},
{
"subject": "69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681",
"result": "unmarked"
"result": "unmarked",
"rerun": "not-rerun"
}
],
"ai_tracks": [],
@ -338,7 +380,8 @@
"about": "a room whose AI host's grant names its model: its chat and caption state that model, and its published speech track has a synthesized track claim naming it (§8)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"audio\",\"models\":[{\"digest\":\"857f14d62ade478f90d495a17495e1aa6608252205fe1b4e0bd43dfcf1456761\",\"type\":\"trained\"}],\"operation\":\"speech-synthesis\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"7e7b5b7821bc7928d1e8ae3ae2e25b2149ed3a624a6217db003658b9890afd9373c61d7d4d68e8dceb3664ee077006397b95ee3d2e0171caf73bc66bd6edfc09\",\"signer_pubkey\":\"26e40ba726b5d0bd3cba619ecd280967a9f28dff54c01f2c6bc27cd9e0e1e741\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"from_group\":0,\"track\":\"e0f5f40db551bfb20f862d5d6ffd64dbea588d2181d8dfe58849b4d9f15931a6\",\"type\":\"track\"}}"
@ -350,6 +393,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": {
"descriptor": "5752443101ba07665226a6cc29262167d963816c0949855436567469a282299e19951d15d86d1383a5809797241ba62c20bb2610b653449442b011405f246603c702124cc400406cd8e95d060040420f00000000000000080000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000204686f73740101000008040000000000000000000000000000000000000000000000000000000000000861756469656e63650001000018000000000000000000000000000000000000000000000000000000000000000001067370656563680101046f707573000001000000000000100000004000000080bb0000010f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008951f673cfa31eaf7f5b68c7755826574bdf858e85925f8ab4ae2122db97b97ce4f0359e91ccefc8caec38d6ce9b2e90d070141e68d23503d93ba41a40c6b904",
"grants": [
@ -383,6 +427,7 @@
"assistive": null
}
],
"markings": [],
"detections": [],
"ai_tracks": [
{
@ -401,7 +446,8 @@
"about": "a review of a generated video at decode-equivalence, presented by a player whose decode path holds that tier and whose output is the reconstruction reviewed; the label shown at first exposure, after an ad break and within the interval (§9, §10)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -430,6 +476,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "accept",
"statements": {
@ -442,6 +489,7 @@
"assistive": null
}
],
"markings": [],
"detections": [],
"ai_tracks": [],
"reviews_presented": [
@ -476,7 +524,8 @@
"about": "an origin claim with a member §3.1 does not list",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"note\":\"an extra member\",\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
@ -488,6 +537,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "record_malformed",
@ -501,7 +551,8 @@
"about": "a capture that names an input: a capture has none (§3.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[{\"class\":\"reconstructed\",\"content_hash\":\"5c9d10e4264fb166fafb81b5dc92f1e0aff2be288d2242dbd16c466d42c586f2\",\"origin\":\"4e09c93646b7a60b0321ca20646718f23ab4ea2712f7608d174544ac9c9a2cfb\",\"source\":\"digitalCapture\"}],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"0bdc921c0bff9e00a34c2ff518feee86373a682472262ca386b9907cb4b8d271a1c3a8350b2b8e11e467346de7eba1bd944825b8d2632f133016410dbd785507\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
@ -513,6 +564,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "record_malformed",
@ -526,7 +578,8 @@
"about": "an origin claim whose signature has one digit changed",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"00a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
@ -538,6 +591,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "bad_signature",
@ -551,7 +605,8 @@
"about": "an origin claim whose key is the identity point, of small order (SPEC §7 *Signatures* step 2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"0100000000000000000000000000000000000000000000000000000000000000\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
@ -563,6 +618,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "bad_key",
@ -576,7 +632,8 @@
"about": "two origin claims on one object",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -589,6 +646,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "claim_conflict",
@ -602,7 +660,8 @@
"about": "a transcode of a generated video that states the class reconstructed: a conversion keeps its input's class (§3.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}",
@ -615,6 +674,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "class_mismatch",
@ -628,7 +688,8 @@
"about": "a codec's reconstruction of a capture that states digitalCreation: a conversion keeps its input's source (§5)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -641,6 +702,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "source_mismatch",
@ -654,7 +716,8 @@
"about": "a reconstruction that restates its input's class as reconstructed where the input's own claim says captured (§3.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -667,6 +730,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "input_mismatch",
@ -680,7 +744,8 @@
"about": "a synthesized object labelled captured: the object is a generator's envelope (wai.generate.spectral, intent create), and its claim is a capture (§3.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"716ff62326cbb6e07cc61705672bac858cdadaf916bd05c49e66e7b28bef058e180d507460a0db3faa7dfed11eda5ed50ef6a27884024688218ef15d9869470d\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917\",\"type\":\"object\"}}"
@ -694,6 +759,7 @@
"574149316a0000007b22776169223a22312e30222c226d65646961223a22617564696f222c22696e74656e74223a22637265617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e67656e65726174652e737065637472616c227d7d0a0000007b22666f726d223a307d"
],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "class_understated",
@ -707,7 +773,8 @@
"about": "replicate evidence naming a derivation §4 does not pin",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -721,6 +788,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "evidence_invalid",
@ -734,7 +802,8 @@
"about": "an upscale whose evidence names the capture's samples, not those its input decodes to (§4.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -761,6 +830,7 @@
"samples": "7430b38a6b8f95698f735893754a8a929173b49c7dbf7ba1c171bac4b9a7d4cd87ebac85"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "input_samples_mismatch",
@ -774,7 +844,8 @@
"about": "an assistive claim whose recomputation differs: the delivered upscale has one sample altered after the derivation, and its evidence states the altered samples (§4.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -797,6 +868,7 @@
"samples": "8d03fc8a54c287e587a269a5bd2dc37445962a5d692a5d69540c9e635e7172b0459a9065c27184a5659b895ab1895ab11b153f3c68215dba0392b724c6b445d7869fe757f9e757f98a3b909586889fd180b5d16ecbd15cddaa87f084b3f084b3f960e1eda4efe1e7fcd8eab7cfed72e4cf6ff9b16cf9b16cf960e1eda4efe1e7fcd8eab7cfed72e4cf6ff9b16cf9b16c"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "assistive_recompute_mismatch",
@ -810,7 +882,8 @@
"about": "an upscale whose evidence recomputes, delivered as an object that decodes to other samples (§4.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -833,6 +906,7 @@
"samples": "8d03fc8a54c287e587a269a5bd2dc37445962a5d692a5d69540c9e635e7172b0459a9065c27184a5659b895ab1895ab11b153f3c68215dba0392b724c6b445d7869fe757f9e757f98a3b909586889fd180b5d16ecbd15cddaa87f084b3f084b3f960e1eda4efe1e7fcd8eab7cfed72e4cf6ff9b16cf9b16cf960e1eda4efe1e7fcd8eab7cfed72e4cf6ff9b16cf9b16c"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "samples_mismatch",
@ -846,7 +920,8 @@
"about": "a float super-resolution step with attest evidence, under a policy that requires every assistive claim to be proved (§4.4, §11.1)",
"policy": {
"dual_layer": "when-given",
"assistive": "require-proof"
"assistive": "require-proof",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
@ -860,6 +935,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "assistive_not_proved",
@ -873,13 +949,14 @@
"about": "a marking whose origin claim the verifier is not given (§7.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
],
"markings": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"1c6c99343e99185ae70d5c0d496628cbaa7e00bd50c6539f1a658ab82e663cbe9ea2302b9c125db6005fa5da8eb837f23f843f0e26af975ca63b794c8942610a\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"08cb06fb6c5fd6d27ac67aff2e1555f89339245d0dbc415a455bbcda0bee4a7f8622e361358438e12d2620fea13d6e5ec8fe9a84e10077b5d49878fbb0cc1a08\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [],
"reviews": [],
@ -887,6 +964,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "claim_unknown",
@ -900,13 +978,14 @@
"about": "a marking whose payload carries the class captured for a synthesized claim (§7.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
],
"markings": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"00ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"9d6ac7d8fa7777a8482722dde6801e7961a7104676de2585aff18895d46621b632001e912ad4fdde125dacf5bb8c9536ad762cd0d41fd9c32eda1c55e2b57205\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"00ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"ff60c472034f00cbfc2ac400680e842f68807b22262ba914f1dc76b1dbb35e79b7a34a910d1cb3a9ae1e9aa02b4eca0d49e8a7233b50ccc44a7ef4a30d60a904\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [],
"reviews": [],
@ -914,6 +993,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "marking_mismatch",
@ -927,22 +1007,24 @@
"about": "a detection under another scheme definition than the one the marking names (§7.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
],
"markings": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"1c6c99343e99185ae70d5c0d496628cbaa7e00bd50c6539f1a658ab82e663cbe9ea2302b9c125db6005fa5da8eb837f23f843f0e26af975ca63b794c8942610a\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"08cb06fb6c5fd6d27ac67aff2e1555f89339245d0dbc415a455bbcda0bee4a7f8622e361358438e12d2620fea13d6e5ec8fe9a84e10077b5d49878fbb0cc1a08\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"5dcbb4c23056dc3f420b43a59f490d5cb1082a48bb276f75977cd52f2e5cafc7\",\"sig\":\"b8e07dee2e8c029a288514a1b3e1cfbe0c8071b3cd3780aa10d41c7d3311efae2a02c9fa63591581b7eaba8faca7bc306d38a3cb5733daaa55ebcf38fac0b800\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"5dcbb4c23056dc3f420b43a59f490d5cb1082a48bb276f75977cd52f2e5cafc7\",\"sig\":\"45458c9c98c7d89ddc6c3e27200fe41ff7f0089bbebee8bddef5cc597c78abee1301baaa1dd334fb136d6d1ccd428fabdcf33ba66cfc872c114be47304588d0f\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "detection_mismatch",
@ -956,22 +1038,24 @@
"about": "a marked object in which the detector finds no mark: the second layer was stripped (§7.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
],
"markings": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"1c6c99343e99185ae70d5c0d496628cbaa7e00bd50c6539f1a658ab82e663cbe9ea2302b9c125db6005fa5da8eb837f23f843f0e26af975ca63b794c8942610a\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"08cb06fb6c5fd6d27ac67aff2e1555f89339245d0dbc415a455bbcda0bee4a7f8622e361358438e12d2620fea13d6e5ec8fe9a84e10077b5d49878fbb0cc1a08\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"25bf74671a41233c3d263f9789dfc6146c78099f01775ad9026f903524c68ef13eb177de5eac541f4dd48fad7800d14a5f2bc3dd5ea2eb271f43f26ac6fae208\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":null,\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"c80b1103ed4ad204db79a4032cd6c072da2321a60112757be69f3d8a4e045b038921c23baa944719065bc74a4d4ef01169dfeb258ec6a382b2c48328ca3a8f01\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "watermark_absent",
@ -985,20 +1069,22 @@
"about": "the two marking layers disagree: the photo's claim says captured, and the detector reads from it a synthesized mark that points at another claim (§7.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}"
],
"markings": [],
"detections": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"fe270dbd3df0b210b24468692f42a5a39c5f6038c9b2371c78beb91e7bd636cbd147f6bc6120b3ec417de5f27caea2e8f9f27b43884cf04dec39f55301f7370e\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\"}"
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"cff502cf46f42bf880778de6bf4b02bd440a66118252a9b1d07c1f99ada4e6f65b10dc8a89817f4f39f53024839794f769d90eb3fb3d3b99bb87798fbeb7840c\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\"}"
],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "layers_disagree",
@ -1012,7 +1098,8 @@
"about": "a synthesized object with no marking, under a policy that requires both layers (§7.1)",
"policy": {
"dual_layer": "required",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1024,6 +1111,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "layer_missing",
@ -1032,12 +1120,119 @@
0
]
},
{
"name": "scheme_mismatch",
"about": "a marking that names the registered scheme's term with the digest of another definition (§7.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
],
"markings": [
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:tent64\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"f97fbd1b39dcf2b28cafbbd1dd86effb4e1ccbbc63216d3047279c4c7470f1179e66efa4c0093a7965a349f310edecb3d4c70aac19593a3a3f6bfd40032c8807\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "scheme_mismatch",
"at": [
"marking",
0
]
},
{
"name": "mark_evidence_invalid",
"about": "a marking of the registered scheme that states no frames it embedded into, so nothing can recompute it (§7.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
],
"markings": [
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:tent64\",\"scheme_digest\":\"4e1bd9f2cac9420fc276434751a3fb9b2b0916128f200bf147009a86168be7d6\",\"sig\":\"c3e9762d1dfaed2bda77555893bac8caf14d832764367c8fa8633783f8cf74ff00f1c5d6f47b98f4bc86efe5c05d2cfe3f1b36adbb1ecf3eedf104dc3b6c4508\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}"
],
"detections": [],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "mark_evidence_invalid",
"at": [
"marking",
0
]
},
{
"name": "layer_not_proved",
"about": "markings of the conformance scheme, which names no algorithm and so can only be stated, under a policy that requires the second layer proved (§7.6, §11.1)",
"policy": {
"dual_layer": "required",
"assistive": "accept-asserted",
"second_layer": "require-proof"
},
"claims": [
"{\"class\":\"captured\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"capture\",\"oversight\":null,\"record\":null,\"sig\":\"80a29a68d5c2e94c740854784f5b7888baf3455414fb7972165fa420d2270c41cc98f1b6fdef1d9d742d335da9d9cc667d9389c938e47af7a7ea62cb1e4e9203\",\"signer_pubkey\":\"32a9d511e269b29d32c9a1e075f2711ab1ac857448fd64631c852b2abf5a3199\",\"source\":\"digitalCapture\",\"step\":\"capture\",\"subject\":{\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"type\":\"object\"}}",
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}",
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"audio\",\"models\":[{\"digest\":\"b47ce1b548507dcf199243a31dec452a7674e16181cbc6aa5c9d6ce8e729568e\",\"type\":\"algorithmic\"}],\"operation\":\"wai.generate.spectral\",\"oversight\":\"prompt_guided\",\"record\":null,\"sig\":\"93bf400b641704b0bc8599714bd9df962908c842cc9ec807fc74b8c3e44285e259d6c54b616a076e68ea8f8725e6bdb5792a804fb0082ddc0cc19b295b596a08\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"algorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917\",\"type\":\"object\"}}",
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[{\"class\":\"captured\",\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"origin\":\"e48406173ab889163527e5f734bd3c02ac1f47d8c30538903b9326fd886ffe31\",\"source\":\"digitalCapture\"}],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[{\"digest\":\"8cf24466f8c423296347a21fe427b09a94beb668ba22aac59a5173abf4a9517c\",\"type\":\"trained\"}],\"operation\":\"inpainting\",\"oversight\":\"human_validated\",\"record\":null,\"sig\":\"ab65ae0639749dd7ed50886934677d969d33cd53567ca7bd2d07f4fb8f2563313e8ba5aef66a5588a25582ac2034ed31b6d9b4b81fad050dd4fa0c01ca1ee20b\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"compositeWithTrainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"501e6fb06de91e323282c5a8afd400d21adcabd99576bbc7a8b6b5fccf05524a\",\"type\":\"object\"}}",
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[{\"class\":\"captured\",\"content_hash\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\",\"origin\":\"e48406173ab889163527e5f734bd3c02ac1f47d8c30538903b9326fd886ffe31\",\"source\":\"digitalCapture\"},{\"class\":\"synthesized\",\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"source\":\"trainedAlgorithmicMedia\"}],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"image\",\"models\":[],\"operation\":\"wai.derive.overlay_composition\",\"oversight\":null,\"record\":null,\"sig\":\"d66cb56f05da99a765ae25b5f65464a70c4295ae8f091c4d8f9ae93a781439245451a8c540b8790c0f56ed1bc2215efd0bc556430a45e67ccb97476476998107\",\"signer_pubkey\":\"08af3636c89e55c66c0498aacb199aeb6b3e3f3fd25fc4cf42b699a4b86f1070\",\"source\":\"compositeSynthetic\",\"step\":\"convert\",\"subject\":{\"content_hash\":\"19db0b61e5caf49013bb45bed4e9d77f80abe99a4957e3b9a758c87a47ed41a1\",\"type\":\"object\"}}",
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[{\"class\":\"synthesized\",\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"source\":\"trainedAlgorithmicMedia\"}],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[],\"operation\":\"wai.video.hevc\",\"oversight\":null,\"record\":null,\"sig\":\"1b766b2c66a402d8f0a5d3a79fd2a576060781f603e0f6b105a5400bc6b1358c1c096d2d51fe8fa469b7367907fd1cff54cfaa9e4b1000fccb89ea41e8b63a0f\",\"signer_pubkey\":\"ff703397c2545c095bb3a790fde7399961c3076bbefcac542792dc4c00acdd6c\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"convert\",\"subject\":{\"content_hash\":\"c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd\",\"type\":\"object\"}}"
],
"markings": [
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"ead74abe77e32490a3e184e17b4a7e8296c102dba77118b02fbebd20b75cd3f0\",\"payload\":\"03ead74abe77e324\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"08cb06fb6c5fd6d27ac67aff2e1555f89339245d0dbc415a455bbcda0bee4a7f8622e361358438e12d2620fea13d6e5ec8fe9a84e10077b5d49878fbb0cc1a08\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"08c12d093fe7df981379a7bd0ba70f8b9fb490a705c987e1ed13f98b16b23663\",\"payload\":\"0308c12d093fe7df\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"0d040a278e7524ee3c0c11f4529a5e668155078f30737c36b434dbaa831847c033c5a34e1e7d800f6bcfd59b837eb03b031655abec3ff56bcf4d313d1a1a1d02\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"4df75d76e850e6e24b6c19a3c6ca3e8fc6f14ad5029e80ba05c4076baa07a917\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"e7a086610eb064e991d86cf59ca2239c6a1cadda6e824140efe8c1530898aef5\",\"payload\":\"03e7a086610eb064\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"239027f72efda6803b1c6911c91eec9a60f707e35c98b48a142241b5c40da65c794084004541699cf391537be2050fc976cf514e9f091e2361348cfca6c18c07\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"501e6fb06de91e323282c5a8afd400d21adcabd99576bbc7a8b6b5fccf05524a\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"9dec524cf50ebf41ac92d3531d657d30ffe0b9c68c14aed8641b1a5c6affb014\",\"payload\":\"039dec524cf50ebf\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"c43cf2097fef257db70ac1b272100f5db120117bcbf2037db7f3fcdc638090c649d53c899e881bf7de05acf082da9fb762324c975f2d0b45ca9eba95ee0e4407\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"19db0b61e5caf49013bb45bed4e9d77f80abe99a4957e3b9a758c87a47ed41a1\"}",
"{\"embedding\":null,\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-marking\",\"origin\":\"bbf4d18b4bdf0851e5bcdf02d2ef994a8cdece8e4f160b6cd25a0d4bc3f1a14d\",\"payload\":\"03bbf4d18b4bdf08\",\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"f14e6db321506ecae4d055640c12f4e2b33e56479aca3f8146fc2019f8332ab7a395946aa8a81c73411608ef024e3797d0d218030e5261fc0354668110b16804\",\"signer_pubkey\":\"a56ef30c04f5b691ee3a2f648d5f5036cfed6b7db6514a23122c75e02deccf7c\",\"subject\":\"c61cb9797fc8bd007459550e2e21ab4989c36d3b26f4d7d10773dd03184621cd\"}"
],
"detections": [
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":\"03ead74abe77e324\",\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"3143378c4df8b1f7eeb1aa91075e614940886193a6b5b0567442dd7c18796717b193ad5f4bf2f794742694fd210ac70c35fb6d0061d8c3494cc86a827cecb103\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\"}",
"{\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-detection\",\"payload\":null,\"read\":null,\"scheme\":\"wai-mark:conformance\",\"scheme_digest\":\"73ab84afa47d6646e97f18a6bbfebad6f924a6f1ae6f5d78532d727d975acbf3\",\"sig\":\"e334eedd5e3e8db5fe64cf45496fee4b60bd87ea8598f9f34f1e71f355ae91e87829f37b37cdada1de5ffe3bd5fad39359022399b2697f3abf4debabcdf5d808\",\"signer_pubkey\":\"a4ddb8268591a53a9ee6446a9ee8d5917fa7b337a65f806b9d12708cf19322c6\",\"subject\":\"69c268ffc3b739ef3cddf708a9e9ffcdb360ead5e61f9d3e544b7cef55a7b681\"}"
],
"reviews": [],
"label_policy": null,
"player": [],
"objects": [
"57414931650000007b22776169223a22312e30222c226d65646961223a22696d616765222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e696d6167652e706e67227d7d30000000adcacffa0e72ec5a3c7669181b08b5692cd12204987cc37e77269c1f412c0b9256006715ab5c2f20f8a0fdc97c291ff1",
"57414931660000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227065726365707475616c222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e617631227d7d400000001c8af660db156fd4777ceafd936d90bf46e4ea9d79272f80bbcfdce23a6da360aa0772307063ca56ee4d1d2b75d5de565c3895a9c230ba3facfad6d1579caf53",
"574149316a0000007b22776169223a22312e30222c226d65646961223a22617564696f222c22696e74656e74223a22637265617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e67656e65726174652e737065637472616c227d7d0a0000007b22666f726d223a307d",
"57414931650000007b22776169223a22312e30222c226d65646961223a22696d616765222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e696d6167652e706e67227d7d3000000019e6efa99b020862b987bc8b1f342201b75f1dcbf6957eff0cc3f89482b7d9a5009a19d526c1a75e9ef3d303b8baaa94",
"57414931650000007b22776169223a22312e30222c226d65646961223a22696d616765222c22696e74656e74223a227265706c6963617465222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e696d6167652e706e67227d7d30000000b63b86d4ec9dd56c4dac4146be2804b51b5869d862a98670ae00d52261c72ac3f1df18647863e8cdeabff46339b9c24c",
"57414931670000007b22776169223a22312e30222c226d65646961223a22766964656f222c22696e74656e74223a227065726365707475616c222c226d6f64656c5f726571756972656d656e74223a7b226361706162696c697479223a227761692e766964656f2e68657663227d7d4000000094e921099bea0c893bce9d14f037d535670e1f3f3e04ef819bb5aaa20f70edac4d3d7518e875ab1bc990c7524dcc4d6731add333db596ef78a24984f9cd87bb8"
],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "layer_not_proved",
"at": [
"marking",
0
]
},
{
"name": "ai_track_unlabelled",
"about": "an AI participant publishes speech with no synthesized track claim for it (§8.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [],
"markings": [],
@ -1047,6 +1242,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": {
"descriptor": "5752443101ba07665226a6cc29262167d963816c0949855436567469a282299e19951d15d86d1383a5809797241ba62c20bb2610b653449442b011405f246603c702124cc400406cd8e95d060040420f00000000000000080000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000204686f73740101000008040000000000000000000000000000000000000000000000000000000000000861756469656e63650001000018000000000000000000000000000000000000000000000000000000000000000001067370656563680101046f707573000001000000000000100000004000000080bb0000010f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008951f673cfa31eaf7f5b68c7755826574bdf858e85925f8ab4ae2122db97b97ce4f0359e91ccefc8caec38d6ce9b2e90d070141e68d23503d93ba41a40c6b904",
"grants": [
@ -1083,7 +1279,8 @@
"about": "an AI participant's chat that states no origin: the room refuses it (live-rooms §9.2 step 6)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"audio\",\"models\":[{\"digest\":\"857f14d62ade478f90d495a17495e1aa6608252205fe1b4e0bd43dfcf1456761\",\"type\":\"trained\"}],\"operation\":\"speech-synthesis\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"7e7b5b7821bc7928d1e8ae3ae2e25b2149ed3a624a6217db003658b9890afd9373c61d7d4d68e8dceb3664ee077006397b95ee3d2e0171caf73bc66bd6edfc09\",\"signer_pubkey\":\"26e40ba726b5d0bd3cba619ecd280967a9f28dff54c01f2c6bc27cd9e0e1e741\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"from_group\":0,\"track\":\"e0f5f40db551bfb20f862d5d6ffd64dbea588d2181d8dfe58849b4d9f15931a6\",\"type\":\"track\"}}"
@ -1095,6 +1292,7 @@
"player": [],
"objects": [],
"decoded": [],
"unmarked": [],
"room": {
"descriptor": "5752443101ba07665226a6cc29262167d963816c0949855436567469a282299e19951d15d86d1383a5809797241ba62c20bb2610b653449442b011405f246603c702124cc400406cd8e95d060040420f00000000000000080000000200000000005745503101fe1f0000000000000000000000000000000000000000000000000000000000000004f4013200000040420f00000000000100000040420f00000000000204686f73740101000008040000000000000000000000000000000000000000000000000000000000000861756469656e63650001000018000000000000000000000000000000000000000000000000000000000000000001067370656563680101046f707573000001000000000000100000004000000080bb0000010f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008951f673cfa31eaf7f5b68c7755826574bdf858e85925f8ab4ae2122db97b97ce4f0359e91ccefc8caec38d6ce9b2e90d070141e68d23503d93ba41a40c6b904",
"grants": [
@ -1131,7 +1329,8 @@
"about": "a player's log whose third entry claims seq 3 (§10.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1160,6 +1359,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "player_chain_broken",
@ -1173,7 +1373,8 @@
"about": "a presentation that states the class assistive for a synthesized object (§10.3)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1196,6 +1397,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "present_mismatch",
@ -1209,7 +1411,8 @@
"about": "a presentation whose stated output is not what the object decodes to (§9.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1232,6 +1435,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "present_output_irreproducible",
@ -1245,7 +1449,8 @@
"about": "a review presented for an object the player has not presented (§9.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}",
@ -1263,6 +1468,7 @@
],
"objects": [],
"decoded": [],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "review_subject_mismatch",
@ -1276,7 +1482,8 @@
"about": "a rejected review presented as a review (§9.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1305,6 +1512,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "review_rejected",
@ -1318,7 +1526,8 @@
"about": "a review presented on a non-exact path: the player decoded through a float path, tier none, and presents a review made at decode-equivalence (§9.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1347,6 +1556,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "review_path_not_exact",
@ -1360,7 +1570,8 @@
"about": "a review over another reconstruction than the one the player presented (§9.2)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1389,6 +1600,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "review_reconstruction_mismatch",
@ -1402,7 +1614,8 @@
"about": "a label shown with another icon than the policy's (§10.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1426,6 +1639,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "label_asset_mismatch",
@ -1439,7 +1653,8 @@
"about": "a label not shown at first exposure: the player's log shows the generated video presented at 0 s and the label first at 5 s, past the half-second grace (§10.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1463,6 +1678,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "label_not_shown",
@ -1476,7 +1692,8 @@
"about": "no label after an ad break: presentation resumes at 20 s with no label shown since (§10.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1502,6 +1719,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "label_not_shown",
@ -1515,7 +1733,8 @@
"about": "no label within the policy's 30 s interval: the label last shown at 0 s, the video still presented at 45 s (§10.4)",
"policy": {
"dual_layer": "when-given",
"assistive": "accept-asserted"
"assistive": "accept-asserted",
"second_layer": "accept-stated"
},
"claims": [
"{\"class\":\"synthesized\",\"evidence\":null,\"inputs\":[],\"issued_at\":\"2026-10-21T09:00:00Z\",\"kind\":\"wai-origin\",\"media\":\"video\",\"models\":[{\"digest\":\"1f6d80520cd377fd820fa6727c88d5ef63bb3d4e88592c4915198d92f9d8fec0\",\"type\":\"trained\"}],\"operation\":\"video-generation\",\"oversight\":\"fully_autonomous\",\"record\":null,\"sig\":\"ca9048fd3de7f91e9d7942ca66b2416a447448a37c8008e883aba0ea8b12612cb54c404d1ef63e613d10cfe8753712420756e121e0567790d26cc93bd616d90a\",\"signer_pubkey\":\"05e74a13f7535dd2908c8fdd5b5b0fe6e28b49364593a60aca4461fda5c335ca\",\"source\":\"trainedAlgorithmicMedia\",\"step\":\"generate\",\"subject\":{\"content_hash\":\"66aef0dce547f18873c4d53fdbf1c23231f0ea7f438be6e340037bda899073f5\",\"type\":\"object\"}}"
@ -1540,6 +1759,7 @@
"samples": "c5ab2471427ff3fc018ffd5106fede9fa4155895fffa97099b03775d4b1a277cfb628b04"
}
],
"unmarked": [],
"room": null,
"outcome": "refuse",
"reason": "label_not_shown",

View file

@ -0,0 +1,403 @@
#!/usr/bin/env python3
"""Second implementation of the registered watermark scheme of extensions/synthesis.md
(§7.4 to §7.8), over the corpus `live-conformance/synthesis-mark/`.
Written from the extension's text. It shares no code with wai-rs: the scheme (the stream, the
shuffle, the signs, the mask, the BCH code with a decoder of its own, the embedding, the detector)
and the picture attacks of the battery are _wai_mark.py's, written from §7.5; every case is
verified by synthesis_verify.py, the synthesis corpus's second implementation, with §7.6's checks.
It reads from the extension the scheme's definition and digest (§7.4), its generator polynomial
and the shuffle's first entries (§7.5.3), the figures of §7.5.6, and the table and the claims of
§7.5.8.
python3 -I synthesis_mark_verify.py <live-conformance/synthesis-mark>
It checks that every frame file is named by its SHA-256; that the definition is §7.4's and has
§7.4's digest; that the layout and the code are §7.5.3's; that every codeword, decoding,
embedding and detection of the corpus is what this implementation gives; that every picture
attack of the battery sample gives the result and the reading the corpus states; that every case
gets the outcome, the reason, the place and the statements it states, and that the codes only this
corpus gives each refuse one; that §7.5.8's table is battery.json's and its claims are attacks the
tallies show survived, and §7.5.6's figures are battery.json's. Then it mutates copies of the
corpus and requires each to fail, and weakens its own verifier in turn (no recomputation, no
detection run again, no agreement by inheritance) and requires the corpus to catch each. Exit
status 0 only if all of that holds. Python 3.8 or later; standard library only.
"""
import copy
import hashlib
import json
import math
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import _wai_mark as MARK # noqa: E402
import synthesis_verify as SV # noqa: E402
TEXT = " ".join(SV._TEXT.split()) # the extension, its white space folded
def sha(b):
return hashlib.sha256(b).digest()
def sec(head):
return SV.section(SV._TEXT, head)
# ---------------------------------------------------------------- what the extension states
def text_definition():
"""§7.4: the definition, its lines joined with no white space."""
s = sec("### 7.4 ")
block = s.split("```")[1]
return "".join(line.strip() for line in block.strip().splitlines()).encode("utf-8")
def text_generator():
m = re.search(r"in hexadecimal, are\s+`([0-9a-f]+)`", sec("#### 7.5.3 "))
return m.group(1) if m else None
def text_shuffle_head():
m = re.search(r"π begins ([0-9, ]+?), and the corpus", " ".join(sec("#### 7.5.3 ").split()))
return [int(x) for x in m.group(1).split(", ")] if m else None
def text_figures():
"""§7.5.6's false-positive bound and the words' figures, in the order written."""
s = " ".join(sec("#### 7.5.6 ").split())
fp = re.search(r"2\^−([0-9.]+), about", s)
w = re.search(r"have a mean of ([0-9.]+) ones with a standard deviation of ([0-9.]+), and the ([0-9,]+) pairs of those words "
r"from different pictures differ in a mean of ([0-9.]+) bits with a standard deviation of ([0-9.]+) and in no fewer "
r"than ([0-9]+);", s)
return (fp.group(1) if fp else None), (w.groups() if w else None)
def text_tables():
"""§7.5.8's two tables: {attack: [cell, ...]}, pictures' and clips'."""
s = sec("#### 7.5.8 ")
pictures, clips = {}, {}
for line in s.splitlines():
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) == 4 and cells[0].startswith("`"):
pictures[cells[0].strip("`")] = cells[1:]
elif len(cells) == 2 and cells[0].startswith("`"):
clips[cells[0].strip("`")] = cells[1:]
return pictures, clips
def text_claims():
s = " ".join(sec("#### 7.5.8 ").split())
m = re.search(r"For pictures, these attacks, each with the parameter measured: (.*?)\. For clips: (.*?)\.", s)
if not m:
return None, None
return re.findall(r"`([^`]+)`", m.group(1)), re.findall(r"`([^`]+)`", m.group(2))
# ---------------------------------------------------------------- memoised work
_ATTACKED = {}
def attacked(px, w, h, name, seed):
k = (sha(px), w, h, name, seed)
if k not in _ATTACKED:
_ATTACKED[k] = MARK.attack(name, px, w, h, seed)
return _ATTACKED[k]
_DET = {}
def detect(px, w, h, n):
k = (sha(px), w, h, n)
if k not in _DET:
_DET[k] = MARK.detect(px, w, h, n)
return _DET[k]
_EMB = {}
def embed(px, w, h, n, payload):
k = (sha(px), w, h, n, payload)
if k not in _EMB:
marked, amps = MARK.embed(px, w, h, n, payload)
_EMB[k] = (sha(marked), ["".join("%x" % a for a in f) for f in amps])
return _EMB[k]
def detected_json(d):
return None if d is None else {"payload": d[0].hex(), "orientation": d[1], "errors": d[2]}
def bits(b):
return "".join("1" if x else "0" for x in b)
def fp_log2():
"""§7.5.6: log2 of 8·2^58·V(248, 29)/2^248, from an exact V."""
v = sum(math.comb(248, i) for i in range(30))
return "%.2f" % (3 + 58 + math.log2(v) - 248)
# ---------------------------------------------------------------- the checks
def check(doc, battery, scheme_bytes, frames, classes):
"""Failures of a corpus: every line names one."""
fails = []
def frame(h):
return frames.get(h)
for name, b in frames.items():
if sha(b).hex() != name:
fails.append("frame %s is not named by its SHA-256" % name[:16])
# §7.4: the definition.
if scheme_bytes != text_definition():
fails.append("scheme.json is not §7.4's definition")
if sha(scheme_bytes) != SV.SCHEME_DIGEST or doc["scheme"]["digest"] != SV.SCHEME_DIGEST.hex():
fails.append("the definition's digest is not §7.4's")
try:
d = json.loads(scheme_bytes)
want = {"amplitude": MARK.AMPLITUDE, "grid": MARK.GRID, "length": MARK.LENGTH, "cells_per_bit": MARK.CELLS_PER_BIT,
"margin": list(MARK.MARGIN), "min_width": MARK.MIN_SIDE, "min_height": MARK.MIN_SIDE, "code": [255, 71, MARK.T],
"max_frame_bytes": MARK.MAX_FRAME_BYTES, "class_max": MARK.CLASS_MAX, "orientations": 8, "scheme": MARK.SCHEME}
if any(d.get(k) != v for k, v in want.items()):
fails.append("the definition's parameters are not §7.5's")
except ValueError:
fails.append("scheme.json does not read")
# §7.5.3: the layout and the code.
g = "%x" % MARK.G
if g != text_generator() or doc["scheme"]["generator"] != g:
fails.append("the generator polynomial is not §7.5.3's")
if text_shuffle_head() != MARK.PERM[:4]:
fails.append("§7.5.3's first entries of the shuffle are not the stream's")
sc = doc["scheme"]
if (sc["shuffle_first"] != MARK.PERM[:16] or sc["shuffle_last"] != MARK.PERM[-16:] or sc["signs_first"] != MARK.SIGNS[:16]
or sc["mask"] != bits(MARK.MASK)):
fails.append("the corpus's layout is not the stream's")
if sc["false_positive_log2"] != fp_log2() or battery.get("false_positive_log2") != fp_log2():
fails.append("the false-positive bound is not §7.5.6's")
if MARK.decode(MARK.MASK) is not None or MARK.decode([1 - b for b in MARK.MASK]) is not None:
fails.append("the mask or its complement decodes")
for c in doc["codewords"]:
if bits(MARK.encode(bytes.fromhex(c["payload"]))) != c["codeword"]:
fails.append("codeword of %s" % c["payload"])
for x in doc["decodes"]:
got = MARK.decode([int(ch) for ch in x["word"]])
got = None if got is None else {"payload": got[0].hex(), "errors": got[1]}
if got != x["result"]:
fails.append("decoding of a word with %s bits flipped: %s" % (x["flipped"], got))
# §7.5.4, §7.5.5.
for e in doc["embeds"]:
u = frame(e["unmarked"])
if u is None:
fails.append("embed %s: no unmarked frames" % e["name"])
continue
m, amps = embed(u, e["width"], e["height"], e["frames"], bytes.fromhex(e["payload"]))
if m.hex() != e["marked"] or amps != e["amplitudes"]:
fails.append("embed %s: the marked frames or the amplitudes differ" % e["name"])
mk = frame(e["marked"])
if mk is None or sum((a - b) ** 2 for a, b in zip(u, mk)) != e["sse"]:
fails.append("embed %s: the marked frames are not held, or their SSE differs" % e["name"])
if any(sum(1 for a in f if int(a, 16) == MARK.AMPLITUDE) < s for f, s in zip(e["amplitudes"], e["short"])):
fails.append("embed %s: a bit left short is not at the cap" % e["name"])
for x in doc["detects"]:
px = frame(x["samples"])
got = None if px is None else detected_json(detect(px, x["width"], x["height"], x["frames"]))
if px is None or got != x["detected"]:
fails.append("detect %s: %s" % (x["name"], got))
# §7.5.7: the battery's sample, over the corpus's still.
still = next((e for e in doc["embeds"] if e["name"] == "still"), {"width": 0, "height": 0})
for x in doc["battery_sample"]:
px = frame(x["frames"])
if px is None:
fails.append("battery sample %s: no frames" % x["attack"])
continue
y, w, h = attacked(px, still["width"], still["height"], x["attack"], x["seed"])
got = detected_json(detect(y, w, h, 1))
if (sha(y).hex(), w, h, got) != (x["result_samples"], x["width"], x["height"], x["detected"]):
fails.append("battery sample %s of the %s still: %s" % (x["attack"], x["which"], got))
# §11 cases, with §7.6's checks.
for v in doc["cases"]:
case = dict(v, room=None)
try:
case["decoded"] = [{"object": d["object"], "samples": frame(d["frames"]).hex()} for d in v["decoded"]]
case["unmarked"] = [{"object": d["object"], "samples": frame(d["frames"]).hex()} for d in v["unmarked"]]
got = SV.outcome(case, classes)
except Exception as e: # noqa: BLE001 — a malformed copy fails, whatever it breaks
got = {"outcome": "unreadable: %r" % (e,)}
if got != SV.stated(v):
fails.append("case %s: %s" % (v["name"], json.dumps(got)[:300]))
# §7.5.8: the table and the claims.
pictures, clips = text_tables()
rows = {}
for r in battery["table"]:
rows.setdefault(r["attack"], {})[r["set"]] = r
fp = {}
for u in battery["unmarked"]:
fp[(u["attack"], u["set"])] = u["detections"]
def cell(r):
return "%d / %d" % (r["detected"], r["trials"]) + (" (%d)" % r["errors_max"] if r["detected"] else "")
for a in battery["image_attacks"]:
if [cell(rows[a]["spectral"]), cell(rows[a]["leaves"])] != pictures.get(a, [None, None, None])[:2]:
fails.append("§7.5.8's row for %s is not battery.json's" % a)
for a in battery["clip_attacks"]:
if [cell(rows[a]["clips"])] != clips.get(a):
fails.append("§7.5.8's clip row for %s is not battery.json's" % a)
survived = {"pictures": [], "clips": []}
for kind, attacks, sets in (("pictures", battery["image_attacks"], ["spectral", "leaves"]), ("clips", battery["clip_attacks"], ["clips"])):
for a in attacks:
if all(rows[a][s]["detected"] == rows[a][s]["trials"] and fp[(a, s)] == 0 for s in sets):
survived[kind].append(a)
if survived != battery["claims"]:
fails.append("battery.json's claims are not the attacks its tallies show survived")
cp, cc = text_claims()
if cp is None or not set(cp) <= set(survived["pictures"]) or not set(cc) <= set(survived["clips"]):
fails.append("§7.5.8 claims an attack the measurement does not show survived")
for a in cp or []:
cells = pictures.get(a, [])
if len(cells) != 3 or any(not re.fullmatch(r"(\d+) / \1 \(\d+\)", c) for c in cells):
fails.append("§7.5.8 claims %s, which a column of its table does not show survived" % a)
if any(b["wrong"] for b in battery["table"]):
fails.append("a picture gave another payload")
fpt, words = text_figures()
w = battery["words"]
if fpt is None or "-" + fpt != fp_log2() or words is None or list(words) != [
w["ones_mean"], w["ones_sd"], "{:,}".format(w["pairs"]), w["distance_mean"], w["distance_sd"], str(w["distance_min"])]:
fails.append("§7.5.6's figures are not battery.json's")
return fails
def flip(s, at):
return s[:at] + ("1" if s[at] == "0" else "0") + s[at + 1:]
def mutations(doc, battery, scheme_bytes, frames):
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
out = []
def mutate(label, f):
d, b, s, fr = copy.deepcopy(doc), copy.deepcopy(battery), scheme_bytes, dict(frames)
r = f(d, b, fr)
if r is not None:
s = r
out.append((label, d, b, s, fr))
vn = {v["name"]: k for k, v in enumerate(doc["cases"])}
still_marked = doc["embeds"][0]["marked"]
def flip_frame(d, b, fr):
x = bytearray(fr[still_marked])
x[100] ^= 1
fr[still_marked] = bytes(x)
def swap_unmarked(d, b, fr):
d["cases"][vn["still_proved"]]["unmarked"][0]["frames"] = d["embeds"][1]["unmarked"]
def tally(d, b, fr):
r = next(r for r in b["table"] if r["attack"] == "int-motion-q16" and r["set"] == "leaves")
r["detected"] -= 1
r["missed"] += 1
def unmarked_hit(d, b, fr):
next(u for u in b["unmarked"] if u["attack"] == "scale-1/2" and u["set"] == "spectral")["detections"] = 1
def inherited_as_agree(d, b, fr):
d["cases"][vn["copy_inherits"]]["statements"]["detections"][0]["result"] = "agree"
mutate("a byte of the marked still's frame file", flip_frame)
mutate("scheme.json with one more space", lambda d, b, fr: scheme_bytes + b" ")
mutate("the mask's first bit", lambda d, b, fr: d["scheme"].update(mask=flip(d["scheme"]["mask"], 0)))
mutate("a codeword's parity bit", lambda d, b, fr: d["codewords"][2].update(codeword=flip(d["codewords"][2]["codeword"], 200)))
mutate("a decoding of 29 errors stated as none", lambda d, b, fr: next(x for x in d["decodes"] if x["flipped"] == 29).update(result=None))
mutate("the still's marked digest", lambda d, b, fr: d["embeds"][0].update(marked=doc["embeds"][1]["marked"]))
mutate("one bit's amplitude", lambda d, b, fr: d["embeds"][0].update(amplitudes=[("1" if doc["embeds"][0]["amplitudes"][0][0] != "1" else "2")
+ doc["embeds"][0]["amplitudes"][0][1:]]))
mutate("the clip's detection stated in another orientation", lambda d, b, fr: d["detects"][2]["detected"].update(orientation=1))
mutate("a crop's sample reading stated as a mark",
lambda d, b, fr: next(x for x in d["battery_sample"] if x["attack"] == "crop-10%" and x["which"] == "marked").update(
detected=d["detects"][0]["detected"]))
mutate("the proved still's unmarked frame swapped for the photograph's", swap_unmarked)
mutate("the copy's inherited agreement stated as plain agreement", inherited_as_agree)
mutate("a marked leaves picture missed at int-motion-q16", tally)
mutate("an unmarked spectral picture read at scale-1/2", unmarked_hit)
mutate("the words' least distance", lambda d, b, fr: b["words"].update(distance_min=b["words"]["distance_min"] + 1))
return out
def weakened():
"""The verifier with one of §7.6's checks, or §7.3's inheritance, dropped."""
return [
("the marking not recomputed", "recomputes", lambda px, e, payload: True),
("the detection not run again", "reproduces", lambda px, r, payload: True),
("no agreement by inheritance", "inherits", lambda c, p, by_link: False),
]
def main(corpus_dir):
fails = []
with open(os.path.join(HERE, "..", "registry", "capabilities.json"), encoding="utf-8") as f:
classes = {c["capability"]: c["replicate"] for c in json.load(f)["capabilities"]}
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
with open(os.path.join(corpus_dir, "battery.json"), encoding="utf-8") as f:
battery = json.load(f)
with open(os.path.join(corpus_dir, "scheme.json"), "rb") as f:
scheme_bytes = f.read()
frames = {}
for n in sorted(os.listdir(os.path.join(corpus_dir, "frames"))):
with open(os.path.join(corpus_dir, "frames", n), "rb") as f:
frames[n[:-len(".rgb")]] = f.read()
base = check(doc, battery, scheme_bytes, frames, classes)
for b in base:
print("FAIL " + b)
print(("PASS" if not base else "FAIL") + " the scheme, the code, %d embeddings, %d detections, %d battery samples and %d cases"
% (len(doc["embeds"]), len(doc["detects"]), len(doc["battery_sample"]), len(doc["cases"])))
fails += base
seen = {v.get("reason") for v in doc["cases"]}
missing = [r for r in SV.MARK_ONLY if r not in seen]
print(("PASS" if not missing and len(SV.MARK_ONLY) == 3 else "FAIL") + " every code only this corpus gives (§13) refuses a case: "
+ ", ".join(SV.MARK_ONLY))
if missing or len(SV.MARK_ONLY) != 3:
fails.append("codes no case gives: %s" % missing)
for label, d, b, s, fr in mutations(doc, battery, scheme_bytes, frames):
caught = check(d, b, s, fr, classes)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:100] if caught else ""))
if not caught:
fails.append("a mutated copy verifies: %s" % label)
for label, name, fn in weakened():
saved = getattr(SV, name)
setattr(SV, name, fn)
try:
caught = check(doc, battery, scheme_bytes, frames, classes)
finally:
setattr(SV, name, saved)
print(("PASS" if caught else "FAIL") + " the corpus catches a verifier with %s%s" % (label, " (%s)" % caught[0][:100] if caught else ""))
if not caught:
fails.append("a weakened verifier passes: %s" % label)
for f in fails:
print("FAIL " + f)
print(("PASS" if not fails else "FAIL") + " synthesis-mark corpus")
return 1 if fails else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: synthesis_mark_verify.py <live-conformance/synthesis-mark>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))

View file

@ -31,6 +31,7 @@ sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
import live_events_verify as LEV # noqa: E402
import live_rooms_verify as LRV # noqa: E402
import _wai_mark as MARK # noqa: E402
import _ed25519 as ED # noqa: E402
from _blake3 import blake3 # noqa: E402
@ -397,14 +398,101 @@ DERIVATIONS = {UPSCALE2: (upscale2, 4), DENOISE3: (denoise3, 1)}
# ---------------------------------------------------------------- §7, §9, §10 the other records
def positive(v):
need(integer(v) >= 1)
return v
def frames_of(v, digests):
"""§7.2, §7.3: an embedding or a read: the frames' count and size, and digests."""
if v is None:
return None
members(v, ["frames", "width", "height"] + digests)
f = {k: positive(v[k]) for k in ("frames", "width", "height")}
f.update({k: hexb(v[k], 32) for k in digests})
return f
def marking_fields(v):
return {"subject": hexb(v["subject"], 32), "origin": hexb(v["origin"], 32), "scheme": term(v["scheme"]),
"scheme_digest": hexb(v["scheme_digest"], 32), "payload": hexb(v["payload"], 8)}
"scheme_digest": hexb(v["scheme_digest"], 32), "payload": hexb(v["payload"], 8),
"embedding": frames_of(v["embedding"], ["unmarked", "marked"])}
def detection_fields(v):
return {"subject": hexb(v["subject"], 32), "scheme": term(v["scheme"]), "scheme_digest": hexb(v["scheme_digest"], 32),
"payload": opt_hex(v["payload"], 8)}
"payload": opt_hex(v["payload"], 8), "read": frames_of(v["read"], ["samples"])}
def scheme_check(scheme, digest, frames):
"""§7.4: whether the record's scheme is the registered one; refuses a registered term under
another digest, a record of it with no frames the scheme admits, and frames stated for
wai-mark:conformance."""
if scheme == MARK.SCHEME:
if digest != SCHEME_DIGEST:
raise Refuse("scheme_mismatch")
if frames is None or not MARK.admits(frames["width"], frames["height"], frames["frames"]):
raise Refuse("mark_evidence_invalid")
return True
if scheme == "wai-mark:conformance" and frames is not None:
raise Refuse("mark_evidence_invalid")
return False
def marking_statement(m):
"""§7.7: the marking's C2PA statement, as its JCS text."""
return jcs({"actions": [{"action": "c2pa.watermarked.bound"}],
"assertions": [{"label": "c2pa.soft-binding",
"data": {"alg": m["scheme"], "alg-params": m["scheme_digest"].hex(),
"blocks": [{"scope": {}, "value": m["payload"].hex()}]}}]})
_EMBEDDED, _DETECTED = {}, {}
def embedded(px, e, payload):
"""§7.5.4, memoised: the SHA-256 of the marked frames."""
k = (hashlib.sha256(px).digest(), e["width"], e["height"], e["frames"], payload)
if k not in _EMBEDDED:
_EMBEDDED[k] = hashlib.sha256(MARK.embed(px, e["width"], e["height"], e["frames"], payload)[0]).digest()
return _EMBEDDED[k]
def detected(px, r):
"""§7.5.5, memoised: the payload read, or None."""
k = (hashlib.sha256(px).digest(), r["width"], r["height"], r["frames"])
if k not in _DETECTED:
got = MARK.detect(px, r["width"], r["height"], r["frames"])
_DETECTED[k] = None if got is None else got[0]
return _DETECTED[k]
def recomputes(px, e, payload):
"""§7.6 step 2: the embedding of the unmarked frames is the marked frames."""
return embedded(px, e, payload) == e["marked"]
def reproduces(px, r, payload):
"""§7.6: the detector, run again, reads the detection's payload."""
return detected(px, r) == payload
def inherits(c, p, by_link):
"""§7.3 step 4: p is the payload of a held claim c reaches through inputs along steps
convert and assist."""
seen, stack = set(), [c]
while stack:
x = stack.pop()
if x["step"] not in ("convert", "assist") or x["link"] in seen:
continue
seen.add(x["link"])
for i in x["inputs"]:
held = by_link.get(i["origin"])
if held is not None:
if payload_of(held) == p:
return True
stack.append(held)
return False
def review_fields(v):
@ -480,7 +568,8 @@ def track_digest(room, owner, media, layer):
def verify(case, classes):
"""§11.2: the statements of §11.3, or Refuse with its reason and place."""
st = {"claims": [], "detections": [], "ai_tracks": [], "reviews_presented": [], "labels": [], "visual_icon": None, "room": None}
st = {"claims": [], "markings": [], "detections": [], "ai_tracks": [], "reviews_presented": [], "labels": [], "visual_icon": None,
"room": None}
def each(key, kind, names, fields, place):
out = []
@ -493,10 +582,10 @@ def verify(case, classes):
# Step 1.
claims = each("claims", "wai-origin", ORIGIN, origin_fields, "claim")
markings = each("markings", "wai-marking", ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"],
marking_fields, "marking")
detections = each("detections", "wai-detection", ["kind", "subject", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"],
detection_fields, "detection")
markings = each("markings", "wai-marking", ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "embedding", "issued_at",
"signer_pubkey", "sig"], marking_fields, "marking")
detections = each("detections", "wai-detection", ["kind", "subject", "scheme", "scheme_digest", "payload", "read", "issued_at",
"signer_pubkey", "sig"], detection_fields, "detection")
reviews = each("reviews", "wai-review", ["kind", "subject", "origin", "tier", "reconstruction", "decoder", "variant", "scope", "verdict", "notes",
"role", "issued_at", "signer_pubkey", "sig"], review_fields, "review")
policy = None
@ -511,6 +600,8 @@ def verify(case, classes):
by_link = {c["link"]: c for c in claims}
objects = {blake3(OBJECT_DOMAIN + bytes.fromhex(o)): bytes.fromhex(o) for o in case["objects"]}
decoded = {bytes.fromhex(d["object"]): bytes.fromhex(d["samples"]) for d in case["decoded"]}
unmarked = {bytes.fromhex(d["object"]): bytes.fromhex(d["samples"]) for d in case["unmarked"]}
prove = case["policy"]["second_layer"] == "require-proof"
sha = lambda b: hashlib.sha256(b).digest() # noqa: E731
# Step 2.
@ -557,29 +648,70 @@ def verify(case, classes):
no("assistive_not_proved")
st["claims"].append({"link": c["link"].hex(), "class": c["class"], "source": c["source"], "c2pa": statement(c), "assistive": status})
# Step 3.
# Step 3: §7.2 and §7.6 for the markings.
for i, m in enumerate(markings):
def no(reason):
raise Refuse(reason, ["marking", i])
c = by_link.get(m["origin"])
if c is None:
raise Refuse("claim_unknown", ["marking", i])
no("claim_unknown")
if c["subject"] != ("object", m["subject"]) or payload_of(c) != m["payload"]:
raise Refuse("marking_mismatch", ["marking", i])
no("marking_mismatch")
e = m["embedding"]
try:
registered = scheme_check(m["scheme"], m["scheme_digest"], e)
except Refuse as r:
no(r.reason)
status = "stated"
if registered and m["subject"] in unmarked:
px = unmarked[m["subject"]]
if len(px) != e["frames"] * e["width"] * e["height"] * 3 or sha(px) != e["unmarked"]:
no("mark_input_mismatch")
if not recomputes(px, e, m["payload"]):
no("mark_recompute_mismatch")
status = "proved"
if e is not None and m["subject"] in decoded and sha(decoded[m["subject"]]) != e["marked"]:
no("samples_mismatch")
if prove and status != "proved":
no("layer_not_proved")
st["markings"].append({"subject": m["subject"].hex(), "scheme": m["scheme"], "status": status, "c2pa": marking_statement(m)})
# §7.3 and §7.6 for the detections.
for i, d in enumerate(detections):
def no(reason):
raise Refuse(reason, ["detection", i])
claim = next((c for c in claims if c["subject"] == ("object", d["subject"])), None)
marking = next((m for m in markings if m["subject"] == d["subject"]), None)
if marking is not None and (marking["scheme"], marking["scheme_digest"]) != (d["scheme"], d["scheme_digest"]):
raise Refuse("detection_mismatch", ["detection", i])
no("detection_mismatch")
r = d["read"]
try:
registered = scheme_check(d["scheme"], d["scheme_digest"], r)
except Refuse as x:
no(x.reason)
rerun = "not-rerun"
if r is not None and d["subject"] in decoded:
px = decoded[d["subject"]]
if len(px) != r["frames"] * r["width"] * r["height"] * 3 or sha(px) != r["samples"]:
no("samples_mismatch")
if registered:
if not reproduces(px, r, d["payload"]):
no("detection_irreproducible")
rerun = "reproduced"
if prove and rerun != "reproduced":
no("layer_not_proved")
if d["payload"] is None:
if marking is not None:
raise Refuse("watermark_absent", ["detection", i])
no("watermark_absent")
result = "unmarked"
elif claim is None:
result = "not-checked"
elif d["payload"] != payload_of(claim):
raise Refuse("layers_disagree", ["detection", i])
else:
elif d["payload"] == payload_of(claim):
result = "agree"
st["detections"].append({"subject": d["subject"].hex(), "result": result})
elif inherits(claim, d["payload"], by_link):
result = "agree-inherited"
else:
no("layers_disagree")
st["detections"].append({"subject": d["subject"].hex(), "result": result, "rerun": rerun})
if case["policy"]["dual_layer"] == "required":
for i, c in enumerate(claims):
if c["class"] == "synthesized" and c["subject"][0] == "object" and not any(m["origin"] == c["link"] for m in markings):
@ -722,6 +854,12 @@ for _line in section(_TEXT, "### 10.2 ").splitlines():
if _m:
ICONS[_m.group(2)] = _m.group(1)
REASONS = [l.split("`")[1] for l in section(_TEXT, "## 12. Reasons").splitlines() if l.startswith("| `")]
# §13: the codes only the scheme's corpus gives.
_m = re.search(r"The codes ((?:`[a-z_]+`(?:, | and )?)+) need frames", " ".join(section(_TEXT, "## 13. Conformance").split()))
MARK_ONLY = re.findall(r"`([a-z_]+)`", _m.group(1)) if _m else []
# §7.4: the registered scheme's digest.
_d = re.search(r"its digest is\s+`([0-9a-f]{64})`", section(_TEXT, "### 7.4 "))
SCHEME_DIGEST = bytes.fromhex(_d.group(1)) if _d else b""
def outcome(v, classes):
@ -838,14 +976,17 @@ def main(corpus_dir):
table = check(doc, classes)
if any("EU icons" in t for t in table):
fails.append("the corpus's EU icons differ from §10.2's table")
missing = [r for r in REASONS if r not in reasons_seen]
missing = [r for r in REASONS if r not in reasons_seen and r not in MARK_ONLY]
if len(MARK_ONLY) != 3 or not set(MARK_ONLY) <= set(REASONS) or len(SCHEME_DIGEST) != 32:
fails.append("§13's codes of the scheme's corpus, or §7.4's digest, do not read")
if missing:
fails.append("no vector refused with %s" % ", ".join(missing))
accepted = {v["name"] for v in doc["vectors"] if v["outcome"] == "accept"}
want = {"chain", "assistive_asserted", "generated", "ai_room", "certified"}
if not want <= accepted:
fails.append("vectors not accepted: %s" % ", ".join(sorted(want - accepted)))
print("%s every reason of §12 refuses a vector, and every case of §13 is accepted" % ("PASS" if not missing and want <= accepted else "FAIL"))
print("%s every reason of §12 but %s refuses a vector, and every case of §13 is accepted"
% ("PASS" if not missing and want <= accepted else "FAIL", ", ".join(MARK_ONLY)))
for label, d in mutations(doc):
caught = check(d, classes)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:100] if caught else ""))

View file

@ -2004,6 +2004,13 @@
"section": "§7.1",
"status": "defined"
},
{
"name": "/.well-known/wai-mark/",
"kind": "well-known",
"document": "extensions/synthesis.md",
"section": "§7.7",
"status": "proposed"
},
{
"name": "GRANT_CHALLENGE",
"kind": "setup-option",
@ -2557,6 +2564,20 @@
"section": "§10.1",
"status": "defined"
},
{
"name": "wai-mark:conformance",
"kind": "scheme",
"document": "extensions/synthesis.md",
"section": "§7.4",
"status": "defined"
},
{
"name": "wai-mark:tent64",
"kind": "scheme",
"document": "extensions/synthesis.md",
"section": "§7.4",
"status": "defined"
},
{
"name": "wai-marking",
"kind": "document-kind",

View file

@ -313,6 +313,13 @@ synthesis = ["live_room"]
# the agreement with the output's origin claim. Reuses synthesis (its records and derivations)
# and generate (the pinned generator its exact steps regenerate with); no new dependency.
contribution = ["synthesis", "generate"]
# The second marking layer's registered scheme, wai-mark:tent64 (extensions/synthesis.md §7.5),
# measured: the attack battery (re-encoding with wai.video.int_motion's encoder, the pinned
# derivations of §4, area resampling, crops, rotations, noise from a fixed stream, brightness,
# contrast, a local blur), the measurement sets generated from fixed seeds (wai.generate.spectral's
# generator and a dead-leaves generator), PSNR and SSIM, and the scheme's conformance corpus. The
# scheme itself is under `synthesis`; this adds no dependency.
synthesis_mark = ["synthesis", "video_int", "generate", "png_still"]
# AI usage preferences and consumption claims (extensions/ai-usage): `wai.ai.usage` — the usage
# statement (draft-ietf-aipref-vocab-08's Structured Field form, the TDM Reservation Protocol's
# properties and the Training and Data Mining Assertion's entries, verbatim) in an envelope's
@ -811,6 +818,11 @@ name = "wai_contribution_vectors"
path = "src/bin/wai_contribution_vectors.rs"
required-features = ["contribution"]
[[bin]]
name = "wai_synthesis_mark_vectors"
path = "src/bin/wai_synthesis_mark_vectors.rs"
required-features = ["synthesis_mark"]
[[bin]]
name = "wai_live_watermark_vectors"
path = "src/bin/wai_live_watermark_vectors.rs"
@ -1564,6 +1576,11 @@ name = "contribution"
path = "tests/contribution.rs"
required-features = ["contribution"]
[[test]]
name = "synthesis_mark"
path = "tests/synthesis_mark.rs"
required-features = ["synthesis_mark"]
[[test]]
name = "live_watermark"
path = "tests/live_watermark.rs"

View file

@ -0,0 +1,146 @@
//! Conformance corpus and measurement of the second marking layer's registered scheme
//! (`live-conformance/synthesis-mark/`, extensions/synthesis.md §7.5 and §13).
//!
//! wai_synthesis_mark_vectors gen <corpus-dir> write the corpus from fixed seeds
//! wai_synthesis_mark_vectors verify <corpus-dir> every file regenerates byte for
//! byte, no other file but README.md is
//! there, and every vector and case
//! gets what it states
//! wai_synthesis_mark_vectors measure <png-dir> <out> run the battery of §7.5.7 over every
//! 8-bit RGB PNG of a directory and
//! write the measurement, each picture
//! named by its file's SHA-256
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/synthesis_mark_verify.py`, written from the
//! extension's text and sharing no code with this binary.
use std::collections::BTreeMap;
use std::path::Path;
use wai::synthesis::{hex, mark};
use wai::synthesis_mark::battery::IMAGE_ATTACKS;
use wai::synthesis_mark::{corpus, Frames};
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2), a.get(3)) {
(Some("gen"), Some(d), None) => {
for (name, body) in corpus::generate() {
let p = Path::new(d).join(&name);
if let Some(dir) = p.parent()
&& let Err(e) = std::fs::create_dir_all(dir)
{
eprintln!("create {}: {e}", dir.display());
std::process::exit(1);
}
if let Err(e) = std::fs::write(&p, body) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
}
println!("wrote {d}");
}
(Some("verify"), Some(d), None) => std::process::exit(verify(Path::new(d))),
(Some("measure"), Some(d), Some(out)) => std::process::exit(measure(Path::new(d), Path::new(out))),
_ => {
eprintln!("usage: wai_synthesis_mark_vectors gen|verify <corpus-dir> | measure <png-dir> <out.json>");
std::process::exit(2);
}
}
}
/// Every file under `dir`, by its path relative to `dir`, but the top-level README.md.
fn read_tree(dir: &Path) -> std::io::Result<BTreeMap<String, Vec<u8>>> {
let mut out = BTreeMap::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d)? {
let e = e?;
let p = e.path();
if p.is_dir() {
stack.push(p);
continue;
}
let rel = p.strip_prefix(dir).expect("under the directory").to_string_lossy().replace('\\', "/");
if rel != "README.md" {
out.insert(rel, std::fs::read(&p)?);
}
}
}
Ok(out)
}
fn verify(dir: &Path) -> i32 {
let files = match read_tree(dir) {
Ok(f) => f,
Err(e) => {
eprintln!("read {}: {e}", dir.display());
return 1;
}
};
let generated: BTreeMap<String, Vec<u8>> = corpus::generate().into_iter().collect();
let same = files == generated;
if !same {
for k in generated.keys().filter(|k| files.get(*k) != generated.get(*k)) {
println!(" differs or missing: {k}");
}
for k in files.keys().filter(|k| !generated.contains_key(*k)) {
println!(" not generated: {k}");
}
}
println!("{} every file regenerates byte for byte from its seeds, and no other file is there", if same { "PASS" } else { "FAIL" });
let fails = corpus::verify(&files);
for f in &fails {
println!("FAIL {f}");
}
println!("{} every embedding, detection and case gets what the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
(!same || !fails.is_empty()) as i32
}
fn measure(dir: &Path, out: &Path) -> i32 {
let mut names: Vec<_> = match std::fs::read_dir(dir) {
Ok(r) => r.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "png")).collect(),
Err(e) => {
eprintln!("read {}: {e}", dir.display());
return 1;
}
};
names.sort();
let mut pics = Vec::new();
let mut files = Vec::new();
for p in &names {
let bytes = match std::fs::read(p) {
Ok(b) => b,
Err(e) => {
eprintln!("read {}: {e}", p.display());
return 1;
}
};
let (w, h, rgb) = match wai::png_still::decode(&bytes) {
Ok(x) => x,
Err(e) => {
eprintln!("{}: {e}", p.display());
return 1;
}
};
let digest = hex(&mark::sha256(&bytes));
files.push(serde_json::json!({"file": p.file_name().map(|n| n.to_string_lossy().into_owned()), "sha256": digest}));
pics.push((digest[..16].to_string(), Frames::one(rgb, w as usize, h as usize)));
}
let m = corpus::measure(&[("photographs", pics)], &IMAGE_ATTACKS);
let doc = serde_json::json!({
"about": "the battery of extensions/synthesis.md §7.5.7 over the PNG files named, written by wai_synthesis_mark_vectors measure",
"files": files,
"pictures": m.pictures,
"table": m.table,
"unmarked": m.unmarked,
"words": m.words_json(),
"claims": m.claims,
});
if let Err(e) = std::fs::write(out, serde_json::to_string_pretty(&doc).expect("JSON") + "\n") {
eprintln!("write {}: {e}", out.display());
return 1;
}
println!("wrote {}", out.display());
0
}

View file

@ -660,6 +660,9 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("wai-review", WireKind::DocumentKind, SYNTHESIS, "§9.1", WireStatus::Defined),
wire("wai-label-policy", WireKind::DocumentKind, SYNTHESIS, "§10.1", WireStatus::Defined),
wire("wai-player", WireKind::DocumentKind, SYNTHESIS, "§10.3", WireStatus::Defined),
wire("wai-mark:tent64", WireKind::Scheme, SYNTHESIS, "§7.4", WireStatus::Defined),
wire("wai-mark:conformance", WireKind::Scheme, SYNTHESIS, "§7.4", WireStatus::Defined),
wire("/.well-known/wai-mark/", WireKind::WellKnown, SYNTHESIS, "§7.7", WireStatus::Proposed),
wire("WAU1", WireKind::Magic, AI_USAGE, "§4.2", WireStatus::Defined),
wire("wai_ai_usage", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined),
wire("WAI-Usage-Claim", WireKind::HttpHeader, AI_USAGE, "§7.3", WireStatus::Proposed),

View file

@ -477,6 +477,13 @@ pub mod synthesis;
/// cbindgen:ignore
#[cfg(feature = "contribution")]
pub mod contribution;
// The second marking layer's registered scheme measured (extensions/synthesis.md §7.5.7): the
// attack battery, each attack an integer function of the frames, the measurement sets the crate
// generates from fixed seeds, the quality figures, and the scheme's conformance corpus. The
// scheme itself is synthesis::mark. Rust-only, like the live modules.
/// cbindgen:ignore
#[cfg(feature = "synthesis_mark")]
pub mod synthesis_mark;
// Forensic watermark sessions (extensions/forensic-watermark.md): `wai.live.watermark` — the edge's
// session record of the ETSI TS 104 002 A/B variants it embedded, bound to the session's grant by a
// salted commitment, the recovered sequence and the rule that matches it, and the room authority's

View file

@ -47,6 +47,7 @@ use crate::live_events::{Event, Payload};
use crate::live_room::{self as room, Detail};
pub mod corpus;
pub mod mark;
/// The domain every record of this extension is signed under (§2).
pub const SIGN_DOMAIN: &[u8] = b"wai:synthesis\x01";
@ -82,10 +83,16 @@ pub enum Reason {
AssistiveNotProved,
ClaimUnknown,
MarkingMismatch,
SchemeMismatch,
MarkEvidenceInvalid,
MarkInputMismatch,
MarkRecomputeMismatch,
DetectionMismatch,
DetectionIrreproducible,
WatermarkAbsent,
LayersDisagree,
LayerMissing,
LayerNotProved,
AiTrackUnlabelled,
PlayerChainBroken,
PresentMismatch,
@ -102,7 +109,7 @@ pub enum Reason {
impl Reason {
/// The codes this extension adds, in §12's order.
pub const ALL: [Reason; 29] = [
pub const ALL: [Reason; 35] = [
Reason::RecordMalformed,
Reason::BadKey,
Reason::BadSignature,
@ -118,10 +125,16 @@ impl Reason {
Reason::AssistiveNotProved,
Reason::ClaimUnknown,
Reason::MarkingMismatch,
Reason::SchemeMismatch,
Reason::MarkEvidenceInvalid,
Reason::MarkInputMismatch,
Reason::MarkRecomputeMismatch,
Reason::DetectionMismatch,
Reason::DetectionIrreproducible,
Reason::WatermarkAbsent,
Reason::LayersDisagree,
Reason::LayerMissing,
Reason::LayerNotProved,
Reason::AiTrackUnlabelled,
Reason::PlayerChainBroken,
Reason::PresentMismatch,
@ -152,10 +165,16 @@ impl Reason {
Reason::AssistiveNotProved => "assistive_not_proved",
Reason::ClaimUnknown => "claim_unknown",
Reason::MarkingMismatch => "marking_mismatch",
Reason::SchemeMismatch => "scheme_mismatch",
Reason::MarkEvidenceInvalid => "mark_evidence_invalid",
Reason::MarkInputMismatch => "mark_input_mismatch",
Reason::MarkRecomputeMismatch => "mark_recompute_mismatch",
Reason::DetectionMismatch => "detection_mismatch",
Reason::DetectionIrreproducible => "detection_irreproducible",
Reason::WatermarkAbsent => "watermark_absent",
Reason::LayersDisagree => "layers_disagree",
Reason::LayerMissing => "layer_missing",
Reason::LayerNotProved => "layer_not_proved",
Reason::AiTrackUnlabelled => "ai_track_unlabelled",
Reason::PlayerChainBroken => "player_chain_broken",
Reason::PresentMismatch => "present_mismatch",
@ -171,6 +190,11 @@ impl Reason {
}
}
/// The codes only a registered scheme's checks raise (§7.6), which the scheme's own corpus
/// (`live-conformance/synthesis-mark/`) gives; every other code of [`Reason::ALL`] is given by
/// the synthesis corpus.
pub const MARK_REASONS: [Reason; 3] = [Reason::MarkInputMismatch, Reason::MarkRecomputeMismatch, Reason::DetectionIrreproducible];
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
@ -709,11 +733,33 @@ pub fn derive(derivation: &str, input: &[u8], w: usize, h: usize) -> Option<Vec<
// §7 Marking and detection
// ---------------------------------------------------------------------------------------
pub const MARKING_MEMBERS: [&str; 9] = ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"];
pub const DETECTION_MEMBERS: [&str; 8] = ["kind", "subject", "scheme", "scheme_digest", "payload", "issued_at", "signer_pubkey", "sig"];
pub const MARKING_MEMBERS: [&str; 10] = ["kind", "subject", "origin", "scheme", "scheme_digest", "payload", "embedding", "issued_at", "signer_pubkey", "sig"];
pub const DETECTION_MEMBERS: [&str; 9] = ["kind", "subject", "scheme", "scheme_digest", "payload", "read", "issued_at", "signer_pubkey", "sig"];
pub const EMBEDDING_MEMBERS: [&str; 5] = ["frames", "width", "height", "unmarked", "marked"];
pub const READ_MEMBERS: [&str; 4] = ["frames", "width", "height", "samples"];
/// The scheme the conformance corpus names (§7.4): a slot, with no algorithm.
pub const CONFORMANCE_SCHEME: &str = "wai-mark:conformance";
/// The frames a marking embedded into (§7.2): their count and size, and the digests of the
/// unmarked and the marked frames.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Embedding {
pub frames: u64,
pub width: u64,
pub height: u64,
pub unmarked: [u8; 32],
pub marked: [u8; 32],
}
/// The frames a detection read (§7.3): their count and size, and their digest.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Read {
pub frames: u64,
pub width: u64,
pub height: u64,
pub samples: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct Marking {
pub link: [u8; 32],
@ -722,6 +768,7 @@ pub struct Marking {
pub scheme: String,
pub scheme_digest: [u8; 32],
pub payload: [u8; 8],
pub embedding: Option<Embedding>,
}
#[derive(Clone, Debug)]
@ -731,6 +778,27 @@ pub struct Detection {
pub scheme: String,
pub scheme_digest: [u8; 32],
pub payload: Option<[u8; 8]>,
pub read: Option<Read>,
}
fn positive(v: &Value) -> Option<u64> {
int(v).filter(|&n| n >= 1)
}
fn read_embedding(v: &Value) -> Option<Embedding> {
let o = exact(v, &EMBEDDING_MEMBERS)?;
Some(Embedding {
frames: positive(&o["frames"])?,
width: positive(&o["width"])?,
height: positive(&o["height"])?,
unmarked: hexn(&o["unmarked"])?,
marked: hexn(&o["marked"])?,
})
}
fn read_read(v: &Value) -> Option<Read> {
let o = exact(v, &READ_MEMBERS)?;
Some(Read { frames: positive(&o["frames"])?, width: positive(&o["width"])?, height: positive(&o["height"])?, samples: hexn(&o["samples"])? })
}
pub fn read_marking(text: &str) -> Result<Marking, Reason> {
@ -743,6 +811,7 @@ pub fn read_marking(text: &str) -> Result<Marking, Reason> {
scheme: term(&v["scheme"])?,
scheme_digest: hexn(&v["scheme_digest"])?,
payload: hexn(&v["payload"])?,
embedding: nullable(&v["embedding"], read_embedding)?,
})
};
let m = f().ok_or(Reason::RecordMalformed)?;
@ -759,6 +828,7 @@ pub fn read_detection(text: &str) -> Result<Detection, Reason> {
scheme: term(&v["scheme"])?,
scheme_digest: hexn(&v["scheme_digest"])?,
payload: nullable(&v["payload"], hexn)?,
read: nullable(&v["read"], read_read)?,
})
};
let d = f().ok_or(Reason::RecordMalformed)?;
@ -766,6 +836,64 @@ pub fn read_detection(text: &str) -> Result<Detection, Reason> {
Ok(d)
}
/// §7.3 step 3: whether a payload is that of a claim the verifier holds that the claim `c`
/// reaches through its inputs along steps that keep their input's content, `convert` and
/// `assist`: a conversion carries its input's mark.
fn inherits(c: &Origin, p: &[u8; 8], by_link: &BTreeMap<[u8; 32], &Origin>) -> bool {
let mut seen = BTreeSet::new();
let mut stack = vec![c];
while let Some(x) = stack.pop() {
if !matches!(x.step, Step::Convert | Step::Assist) || !seen.insert(x.link) {
continue;
}
for input in &x.inputs {
if let Some(held) = input.origin.and_then(|h| by_link.get(&h)) {
if held.mark_payload() == *p {
return true;
}
stack.push(*held);
}
}
}
false
}
/// §7.4: the scheme a record names. A registered term with another digest is
/// `scheme_mismatch`. A record of the registered scheme states the frames it embedded into or
/// read, of a size and count the scheme admits; one of the conformance scheme states none.
/// Otherwise `mark_evidence_invalid`. Returns whether the scheme is the registered one.
fn scheme_check(scheme: &str, digest: &[u8; 32], frames: Option<(u64, u64, u64)>) -> Result<bool, Reason> {
match scheme {
mark::SCHEME if *digest != mark::digest() => Err(Reason::SchemeMismatch),
mark::SCHEME => match frames {
Some((w, h, f)) if mark::admits(w, h, f) => Ok(true),
_ => Err(Reason::MarkEvidenceInvalid),
},
CONFORMANCE_SCHEME if frames.is_some() => Err(Reason::MarkEvidenceInvalid),
_ => Ok(false),
}
}
impl Marking {
/// §7.7: the marking's C2PA statement, the signpost a manifest carries: the action
/// `c2pa.watermarked.bound` and a `c2pa.soft-binding` assertion naming the scheme by its
/// term and digest, with the payload as its one block's value.
pub fn c2pa_statement(&self) -> Value {
json!({
"actions": [{"action": "c2pa.watermarked.bound"}],
"assertions": [{
"label": "c2pa.soft-binding",
"data": {"alg": self.scheme, "alg-params": hex(&self.scheme_digest), "blocks": [{"scope": {}, "value": hex(&self.payload)}]},
}],
})
}
/// The statement written as its JCS text.
pub fn export(&self) -> String {
jcs(&self.c2pa_statement()).expect("a statement in the subset")
}
}
// ---------------------------------------------------------------------------------------
// §9 Reviews
// ---------------------------------------------------------------------------------------
@ -977,6 +1105,9 @@ pub struct Policy {
pub dual_layer_required: bool,
/// `assistive` `require-proof`: every assistive claim's evidence is recomputed.
pub require_proof: bool,
/// `second_layer` `require-proof`: every marking is recomputed and every detection run
/// again (§7.6).
pub second_layer_proof: bool,
}
/// What a verifier is given (§11.1).
@ -992,6 +1123,9 @@ pub struct Case {
pub objects: Vec<Vec<u8>>,
/// Canonical buffers a sink decoded, by the content hash of the object decoded.
pub decoded: Vec<([u8; 32], Vec<u8>)>,
/// Unmarked frames (§7.6), by the content hash of the marked object they are the
/// unmarked reconstruction of.
pub unmarked: Vec<([u8; 32], Vec<u8>)>,
pub room: Option<RoomInput>,
pub policy: Policy,
}
@ -1031,6 +1165,7 @@ impl At {
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Report {
pub claims: Vec<Value>,
pub markings: Vec<Value>,
pub detections: Vec<Value>,
pub ai_tracks: Vec<Value>,
pub reviews_presented: Vec<Value>,
@ -1163,11 +1298,34 @@ fn run(case: &Case) -> Step6<Report> {
}
// Step 3: markings, detections, and the two layers.
let unmarked: BTreeMap<[u8; 32], &Vec<u8>> = case.unmarked.iter().map(|(h, b)| (*h, b)).collect();
for (i, m) in markings.iter().enumerate() {
let c = by_link.get(&m.origin).ok_or((At::Marking(i), Reason::ClaimUnknown))?;
let e = |r| (At::Marking(i), r);
let c = by_link.get(&m.origin).ok_or(e(Reason::ClaimUnknown))?;
if c.subject != Subject::Object(m.subject) || c.mark_payload() != m.payload {
return Err((At::Marking(i), Reason::MarkingMismatch));
return Err(e(Reason::MarkingMismatch));
}
let registered = scheme_check(&m.scheme, &m.scheme_digest, m.embedding.as_ref().map(|x| (x.width, x.height, x.frames))).map_err(e)?;
let mut status = "stated";
if let (true, Some(ev), Some(px)) = (registered, &m.embedding, unmarked.get(&m.subject)) {
let (w, h, f) = (ev.width as usize, ev.height as usize, ev.frames as usize);
if px.len() as u64 != ev.width * ev.height * 3 * ev.frames || sha256(&[px]) != ev.unmarked {
return Err(e(Reason::MarkInputMismatch));
}
if sha256(&[&mark::embed(px, w, h, f, &m.payload).0]) != ev.marked {
return Err(e(Reason::MarkRecomputeMismatch));
}
status = "proved";
}
if let (Some(ev), Some(b)) = (&m.embedding, decoded.get(&m.subject))
&& sha256(&[b]) != ev.marked
{
return Err(e(Reason::SamplesMismatch));
}
if case.policy.second_layer_proof && status != "proved" {
return Err(e(Reason::LayerNotProved));
}
report.markings.push(json!({"subject": hex(&m.subject), "scheme": m.scheme, "status": status, "c2pa": m.export()}));
}
for (i, d) in detections.iter().enumerate() {
let e = |r| (At::Detection(i), r);
@ -1178,14 +1336,33 @@ fn run(case: &Case) -> Step6<Report> {
{
return Err(e(Reason::DetectionMismatch));
}
let registered = scheme_check(&d.scheme, &d.scheme_digest, d.read.as_ref().map(|x| (x.width, x.height, x.frames))).map_err(e)?;
let mut rerun = "not-rerun";
if let (Some(r), Some(b)) = (&d.read, decoded.get(&d.subject)) {
let n = r.width.checked_mul(r.height).and_then(|x| x.checked_mul(3)).and_then(|x| x.checked_mul(r.frames));
if n != Some(b.len() as u64) || sha256(&[b]) != r.samples {
return Err(e(Reason::SamplesMismatch));
}
if registered {
let got = mark::detect(b, r.width as usize, r.height as usize, r.frames as usize).map(|x| x.payload);
if got != d.payload {
return Err(e(Reason::DetectionIrreproducible));
}
rerun = "reproduced";
}
}
if case.policy.second_layer_proof && rerun != "reproduced" {
return Err(e(Reason::LayerNotProved));
}
let result = match (d.payload, claim) {
(None, _) if marking.is_some() => return Err(e(Reason::WatermarkAbsent)),
(None, _) => "unmarked",
(Some(_), None) => "not-checked",
(Some(p), Some(c)) if p != c.mark_payload() => return Err(e(Reason::LayersDisagree)),
(Some(_), Some(_)) => "agree",
(Some(p), Some(c)) if p == c.mark_payload() => "agree",
(Some(p), Some(c)) if inherits(c, &p, &by_link) => "agree-inherited",
(Some(_), Some(_)) => return Err(e(Reason::LayersDisagree)),
};
report.detections.push(json!({"subject": hex(&d.subject), "result": result}));
report.detections.push(json!({"subject": hex(&d.subject), "result": result, "rerun": rerun}));
}
if case.policy.dual_layer_required {
for (i, c) in claims.iter().enumerate() {

View file

@ -285,6 +285,7 @@ fn marking(c: &Value, payload: Option<[u8; 8]>) -> Value {
"scheme": CONFORMANCE_SCHEME,
"scheme_digest": hex(&sha(&[SCHEME_DOC])),
"payload": hex(&p),
"embedding": null,
"issued_at": ISSUED,
}),
MARKER,
@ -299,6 +300,7 @@ fn detection(subject: &Value, payload: Option<[u8; 8]>, scheme_doc: &[u8]) -> Va
"scheme": CONFORMANCE_SCHEME,
"scheme_digest": hex(&sha(&[scheme_doc])),
"payload": payload.map(|p| hex(&p)),
"read": null,
"issued_at": ISSUED,
}),
DETECTOR,
@ -586,7 +588,7 @@ fn vectors() -> Vec<Vector> {
claims: [&photo, &coded, &up, &dn].iter().map(|c| text(c)).collect(),
objects: vec![o.photo.clone(), o.coded.clone(), o.upscaled.clone(), o.denoised.clone()],
decoded: chain_decoded.clone(),
policy: Policy { dual_layer_required: false, require_proof: true },
policy: Policy { require_proof: true, ..Default::default() },
..Default::default()
};
let generated = || Case {
@ -600,7 +602,7 @@ fn vectors() -> Vec<Vector> {
text(&detection(&photo["subject"]["content_hash"], None, SCHEME_DOC)),
],
objects: vec![o.photo.clone(), o.video.clone(), o.spectral.clone(), o.inpainted.clone(), o.composite.clone(), o.transcode.clone()],
policy: Policy { dual_layer_required: true, require_proof: false },
policy: Policy { dual_layer_required: true, ..Default::default() },
..Default::default()
};
let video_out = sha(&[&o.video_px]);
@ -821,6 +823,31 @@ fn vectors() -> Vec<Vector> {
At::Claim(0),
Reason::LayerMissing,
);
let renamed = signed(with(marking(&video, None), &["scheme"], json!(mark::SCHEME)), MARKER);
refuse(
"scheme_mismatch",
"a marking that names the registered scheme's term with the digest of another definition (§7.4)",
Case { claims: vec![text(&video)], markings: vec![text(&renamed)], ..Default::default() },
At::Marking(0),
Reason::SchemeMismatch,
);
let bare = signed(with(with(marking(&video, None), &["scheme"], json!(mark::SCHEME)), &["scheme_digest"], json!(hex(&mark::digest()))), MARKER);
refuse(
"mark_evidence_invalid",
"a marking of the registered scheme that states no frames it embedded into, so nothing can recompute it (§7.4)",
Case { claims: vec![text(&video)], markings: vec![text(&bare)], ..Default::default() },
At::Marking(0),
Reason::MarkEvidenceInvalid,
);
let mut not_proved = generated();
not_proved.policy.second_layer_proof = true;
refuse(
"layer_not_proved",
"markings of the conformance scheme, which names no algorithm and so can only be stated, under a policy that requires the second layer proved (§7.6, §11.1)",
not_proved,
At::Marking(0),
Reason::LayerNotProved,
);
let r = ai_room(Some(Said { origin: 2, model: Some(label(MODEL)) }));
refuse(
"ai_track_unlabelled",
@ -948,6 +975,7 @@ fn case_json(c: &Case) -> Map<String, Value> {
json!({
"dual_layer": if c.policy.dual_layer_required { "required" } else { "when-given" },
"assistive": if c.policy.require_proof { "require-proof" } else { "accept-asserted" },
"second_layer": if c.policy.second_layer_proof { "require-proof" } else { "accept-stated" },
}),
);
m.insert("claims".into(), json!(c.claims));
@ -958,6 +986,7 @@ fn case_json(c: &Case) -> Map<String, Value> {
m.insert("player".into(), json!(c.player));
m.insert("objects".into(), json!(c.objects.iter().map(|b| hex(b)).collect::<Vec<_>>()));
m.insert("decoded".into(), json!(c.decoded.iter().map(|(h, b)| json!({"object": hex(h), "samples": hex(b)})).collect::<Vec<_>>()));
m.insert("unmarked".into(), json!(c.unmarked.iter().map(|(h, b)| json!({"object": hex(h), "samples": hex(b)})).collect::<Vec<_>>()));
m.insert(
"room".into(),
match &c.room {
@ -973,7 +1002,7 @@ fn case_json(c: &Case) -> Map<String, Value> {
m
}
fn outcome_json(o: &Outcome) -> Map<String, Value> {
pub(crate) fn outcome_json(o: &Outcome) -> Map<String, Value> {
let mut m = Map::new();
match o {
Outcome::Refuse { at, reason } => {
@ -987,6 +1016,7 @@ fn outcome_json(o: &Outcome) -> Map<String, Value> {
"statements".into(),
json!({
"claims": r.claims,
"markings": r.markings,
"detections": r.detections,
"ai_tracks": r.ai_tracks,
"reviews_presented": r.reviews_presented,
@ -1000,6 +1030,10 @@ fn outcome_json(o: &Outcome) -> Map<String, Value> {
m
}
fn buffers(v: &Value) -> Option<Vec<([u8; 32], Vec<u8>)>> {
v.as_array()?.iter().map(|d| Some((unhex(d["object"].as_str()?)?.try_into().ok()?, unhex(d["samples"].as_str()?)?))).collect()
}
fn case_from_json(v: &Value) -> Option<Case> {
let strs = |k: &str| -> Option<Vec<String>> { v[k].as_array()?.iter().map(|x| x.as_str().map(String::from)).collect() };
let room = match &v["room"] {
@ -1019,13 +1053,14 @@ fn case_from_json(v: &Value) -> Option<Case> {
label_policy: v["label_policy"].as_str().map(String::from),
player: strs("player")?,
objects: v["objects"].as_array()?.iter().map(|x| unhex(x.as_str()?)).collect::<Option<_>>()?,
decoded: v["decoded"]
.as_array()?
.iter()
.map(|d| Some((unhex(d["object"].as_str()?)?.try_into().ok()?, unhex(d["samples"].as_str()?)?)))
.collect::<Option<_>>()?,
decoded: buffers(&v["decoded"])?,
unmarked: buffers(&v["unmarked"])?,
room,
policy: Policy { dual_layer_required: v["policy"]["dual_layer"] == "required", require_proof: v["policy"]["assistive"] == "require-proof" },
policy: Policy {
dual_layer_required: v["policy"]["dual_layer"] == "required",
require_proof: v["policy"]["assistive"] == "require-proof",
second_layer_proof: v["policy"]["second_layer"] == "require-proof",
},
})
}
@ -1050,7 +1085,7 @@ pub fn generate() -> Vec<(String, String)> {
m.extend(outcome_json(&got));
cases.push(Value::Object(m));
}
for r in Reason::ALL {
for r in Reason::ALL.into_iter().filter(|r| !MARK_REASONS.contains(r)) {
assert!(seen.contains(&r), "no vector of the corpus is refused with {r}");
}
let doc = json!({

View file

@ -0,0 +1,724 @@
//! The registered watermark scheme of the second marking layer, `wai-mark:tent64`
//! (extensions/synthesis.md §7.5).
//!
//! An imperceptible mark in the luma of RGB8 frames, embedded by a pinned integer
//! derivation and read back with no key:
//!
//! - **Grid.** Each frame is cut into 64 × 64 cells by the edges `⌊i·W/64⌋` and `⌊j·H/64⌋`,
//! so the grid follows the picture through any rescale or change of aspect.
//! - **Pattern.** In each cell a separable **tent** checkerboard: a weight `ω` that is the
//! product of a horizontal and a vertical tent, positive on one half of the cell and
//! negative on the other in each direction, zero-mean, so a change of brightness leaves
//! the cell's statistic `s = Σ ω·Y` unchanged and a change of contrast scales it.
//! - **Layout.** A public stream (SHA-256 of a fixed label and a counter) shuffles the 4096
//! cells, gives each a sign, and draws a mask over the code's 248 bits; bit `j` is carried
//! by 16 cells scattered over the frame.
//! - **Code.** The 8-byte payload is a codeword of the binary BCH code of length 255 and
//! designed distance 59 (it corrects 29 errors), shortened to 248 bits.
//! - **Embedding.** For each bit, the least amplitude `a ≤ 8` whose pattern gives the bit's
//! statistic the sign the masked codeword asks for, with a margin of 3/2 of the bit's
//! weight mass; the same delta is added to R, G and B, clamped. Integer throughout, so the
//! marked frames are a pure function of the unmarked frames and the payload.
//! - **Detection.** For each of the 8 orientations (the quarter turns and mirrors), the bits'
//! statistics summed over the frames are read as signs, unmasked and decoded; the first
//! orientation that decodes to a payload whose first byte is a class code (0 to 3) gives it.
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::sync::OnceLock;
use crate::jcs::jcs;
/// The scheme's term (§7.4).
pub const SCHEME: &str = "wai-mark:tent64";
/// Cells along each side of a frame.
pub const GRID: usize = 64;
/// Cells in a frame.
pub const CELLS: usize = GRID * GRID;
/// The code's length, and so the bits a frame carries.
pub const CODE_BITS: usize = 248;
/// The parity bits of the code.
pub const PARITY_BITS: usize = CODE_BITS - 64;
/// Cells that carry each bit.
pub const CELLS_PER_BIT: usize = 16;
/// The errors the code corrects.
pub const CORRECTS: usize = 29;
/// The largest amplitude the embedding uses, in luma levels at a cell's peak.
pub const AMPLITUDE: i64 = 12;
/// The margin a bit's statistic is given, as a fraction of its weight mass.
pub const MARGIN: (i64, i64) = (3, 2);
/// The least width and height of a frame.
pub const MIN_SIDE: u64 = 128;
/// The most bytes one frame holds.
pub const MAX_FRAME_BYTES: u64 = 1 << 28;
/// The orientations the detector tries.
pub const ORIENTATIONS: u8 = 8;
/// The largest code a payload's first byte holds (the class codes of §7.2).
pub const CLASS_MAX: u8 = 3;
/// The label of the layout's stream.
pub const STREAM_LABEL: &[u8] = b"wai-mark:tent64 stream";
/// The code's generator polynomial, the coefficient of `x^184` first, in hexadecimal.
pub const GENERATOR_HEX: &str = "140a722a1a468d36d87a25364e685922a1e56fd1a478c1d";
/// The scheme's definition (§7.5): the parameters its text pins, as a JSON value.
pub fn definition() -> Value {
json!({
"scheme": SCHEME,
"specification": "extensions/synthesis.md section 7.5",
"media": ["image", "video"],
"sample": "RGB8",
"min_width": MIN_SIDE,
"min_height": MIN_SIDE,
"max_frame_bytes": MAX_FRAME_BYTES,
"luma": [77, 150, 29, 128, 8],
"grid": GRID,
"pattern": "tent",
"stream": std::str::from_utf8(STREAM_LABEL).expect("ASCII"),
"field": "11d",
"code": [255, 71, CORRECTS],
"length": CODE_BITS,
"generator": GENERATOR_HEX,
"cells_per_bit": CELLS_PER_BIT,
"amplitude": AMPLITUDE,
"margin": [MARGIN.0, MARGIN.1],
"orientations": ORIENTATIONS,
"class_max": CLASS_MAX,
})
}
/// The definition's bytes: its JCS text.
pub fn definition_text() -> String {
jcs(&definition()).expect("a definition in the subset")
}
/// The scheme's digest: SHA-256 of its definition's bytes (§7.2's `scheme_digest`).
pub fn digest() -> [u8; 32] {
Sha256::digest(definition_text().as_bytes()).into()
}
// ---------------------------------------------------------------------------------------
// The layout: the stream, the shuffle, the signs and the mask
// ---------------------------------------------------------------------------------------
/// The public layout every frame shares.
pub struct Layout {
/// `perm[16·j + n]` is the `n`-th cell of bit `j`; the last 128 entries carry nothing.
pub perm: Vec<u16>,
/// Each cell's sign, +1 or −1.
pub sign: Vec<i64>,
/// The mask over the code's bits.
pub mask: [u8; CODE_BITS],
/// The bit a cell carries, or `None`.
pub bit_of_cell: Vec<Option<u16>>,
}
/// The stream's 32-bit words: SHA-256 of the label and a big-endian `u32` counter, from 0,
/// each digest read as eight big-endian words.
pub fn stream() -> impl Iterator<Item = u32> {
(0u32..).flat_map(|i| {
let d: [u8; 32] = Sha256::new().chain_update(STREAM_LABEL).chain_update(i.to_be_bytes()).finalize().into();
(0..8).map(move |o| u32::from_be_bytes(d[o * 4..o * 4 + 4].try_into().expect("four bytes")))
})
}
/// The layout, built once.
pub fn layout() -> &'static Layout {
static L: OnceLock<Layout> = OnceLock::new();
L.get_or_init(|| {
let mut r = stream();
let mut perm: Vec<u16> = (0..CELLS as u16).collect();
for i in (1..CELLS).rev() {
let j = r.next().expect("an endless stream") as usize % (i + 1);
perm.swap(i, j);
}
let sign: Vec<i64> = (0..CELLS).map(|_| if r.next().expect("an endless stream") & 1 == 0 { 1 } else { -1 }).collect();
let mut mask = [0u8; CODE_BITS];
for m in mask.iter_mut() {
*m = (r.next().expect("an endless stream") & 1) as u8;
}
let mut bit_of_cell = vec![None; CELLS];
for j in 0..CODE_BITS {
for n in 0..CELLS_PER_BIT {
bit_of_cell[perm[CELLS_PER_BIT * j + n] as usize] = Some(j as u16);
}
}
Layout { perm, sign, mask, bit_of_cell }
})
}
// ---------------------------------------------------------------------------------------
// The code: BCH(255, 71) over GF(2^8), shortened to (248, 64)
// ---------------------------------------------------------------------------------------
struct Gf {
exp: [u8; 510],
log: [u16; 256],
}
fn gf() -> &'static Gf {
static G: OnceLock<Gf> = OnceLock::new();
G.get_or_init(|| {
let mut exp = [0u8; 510];
let mut log = [0u16; 256];
let mut x: u16 = 1;
for (i, e) in exp.iter_mut().take(255).enumerate() {
*e = x as u8;
log[x as usize] = i as u16;
x <<= 1;
if x & 0x100 != 0 {
x ^= 0x11d;
}
}
let (lo, hi) = exp.split_at_mut(255);
hi.copy_from_slice(lo);
Gf { exp, log }
})
}
fn mul(a: u8, b: u8) -> u8 {
if a == 0 || b == 0 {
return 0;
}
let g = gf();
g.exp[g.log[a as usize] as usize + g.log[b as usize] as usize]
}
/// `α^e` for any integer `e`.
fn alpha(e: i64) -> u8 {
gf().exp[e.rem_euclid(255) as usize]
}
/// The generator polynomial as computed from its definition: the least common multiple of
/// the minimal polynomials of `α^1 … α^58` over GF(2). Bit `d` is the coefficient of `x^d`.
pub fn generator_bits() -> Vec<u8> {
let mut g: Vec<u8> = vec![1];
let mut seen: Vec<Vec<usize>> = Vec::new();
for i in 1..=2 * CORRECTS {
let mut coset = Vec::new();
let mut j = i % 255;
while !coset.contains(&j) {
coset.push(j);
j = (2 * j) % 255;
}
let mut key = coset.clone();
key.sort_unstable();
if seen.contains(&key) {
continue;
}
seen.push(key);
// The minimal polynomial: the product of (x − α^j) over the coset.
let mut p: Vec<u8> = vec![1];
for &j in &coset {
let r = alpha(j as i64);
let mut q = vec![0u8; p.len() + 1];
for (d, &c) in p.iter().enumerate() {
q[d + 1] ^= c;
q[d] ^= mul(c, r);
}
p = q;
}
assert!(p.iter().all(|&c| c <= 1), "a minimal polynomial is binary");
let mut out = vec![0u8; g.len() + p.len() - 1];
for (a, &ga) in g.iter().enumerate() {
for (b, &pb) in p.iter().enumerate() {
out[a + b] ^= ga & pb;
}
}
g = out;
}
g
}
fn generator() -> &'static [u8] {
static G: OnceLock<Vec<u8>> = OnceLock::new();
G.get_or_init(|| {
let g = generator_bits();
assert_eq!(g.len(), PARITY_BITS + 1);
g
})
}
/// The codeword of a payload: bit `j` is the coefficient of `x^(247 − j)`; bits 0 to 63
/// are the payload's, the most significant bit of its first byte first, and the rest the
/// remainder of `m(x)·x^184` divided by the generator.
pub fn encode(payload: &[u8; 8]) -> [u8; CODE_BITS] {
let g = generator();
let mut word = [0u8; CODE_BITS];
for (j, w) in word.iter_mut().take(64).enumerate() {
*w = (payload[j / 8] >> (7 - j % 8)) & 1;
}
let mut r = word;
for j in 0..64 {
if r[j] == 1 {
// Subtract g(x)·x^(183 − j): its x^184 coefficient sits at bit j.
for d in 0..=PARITY_BITS {
r[j + PARITY_BITS - d] ^= g[d];
}
}
}
word[64..].copy_from_slice(&r[64..]);
word
}
/// Bounded-distance decoding: the payload of the codeword within 29 bits of `word`, and
/// the bits that differ, or `None` where there is none or its first byte is above 3.
pub fn decode(word: &[u8; CODE_BITS]) -> Option<([u8; 8], usize)> {
let mut c = *word;
let syndromes = |c: &[u8; CODE_BITS]| -> Vec<u8> {
(1..=2 * CORRECTS as i64)
.map(|i| c.iter().enumerate().filter(|&(_, &b)| b == 1).fold(0u8, |s, (j, _)| s ^ alpha(i * (CODE_BITS - 1 - j) as i64)))
.collect()
};
let s = syndromes(&c);
let mut errors = 0;
if s.iter().any(|&x| x != 0) {
// The error locator Λ: the shortest linear recurrence the syndromes satisfy.
let g = gf();
let (mut lam, mut b) = (vec![1u8], vec![1u8]);
let (mut l, mut m, mut bd) = (0usize, 1usize, 1u8);
for n in 0..2 * CORRECTS {
let mut d = s[n];
for i in 1..=l.min(lam.len() - 1) {
d ^= mul(lam[i], s[n - i]);
}
if d == 0 {
m += 1;
continue;
}
let coef = mul(d, g.exp[(255 - g.log[bd as usize] as usize) % 255]);
let prev = lam.clone();
if lam.len() < b.len() + m {
lam.resize(b.len() + m, 0);
}
for (i, &bv) in b.iter().enumerate() {
lam[i + m] ^= mul(coef, bv);
}
if 2 * l <= n {
l = n + 1 - l;
b = prev;
bd = d;
m = 1;
} else {
m += 1;
}
}
while lam.len() > 1 && lam[lam.len() - 1] == 0 {
lam.pop();
}
if lam.len() - 1 != l || l > CORRECTS {
return None;
}
// The locator's roots: Λ(α^−e) = 0 marks an error at x^e.
let roots: Vec<usize> = (0..255)
.filter(|&e| lam.iter().enumerate().fold(0u8, |v, (i, &co)| v ^ mul(co, alpha(-(e as i64) * i as i64))) == 0)
.collect();
if roots.len() != l || roots.iter().any(|&e| e >= CODE_BITS) {
return None;
}
for e in roots {
c[CODE_BITS - 1 - e] ^= 1;
}
if syndromes(&c).iter().any(|&x| x != 0) {
return None;
}
errors = l;
}
let mut payload = [0u8; 8];
for j in 0..64 {
payload[j / 8] |= c[j] << (7 - j % 8);
}
(payload[0] <= CLASS_MAX).then_some((payload, errors))
}
// ---------------------------------------------------------------------------------------
// Frames, the grid and the pattern
// ---------------------------------------------------------------------------------------
/// Whether the scheme admits frames of this size and count (§7.5.1).
pub fn admits(width: u64, height: u64, frames: u64) -> bool {
width >= MIN_SIDE
&& height >= MIN_SIDE
&& frames >= 1
&& width.checked_mul(height).and_then(|p| p.checked_mul(3)).is_some_and(|b| b <= MAX_FRAME_BYTES)
&& (width * height * 3).checked_mul(frames).is_some_and(|b| b <= crate::jcs::MAX_INTEGER)
}
/// `(77·R + 150·G + 29·B + 128) >> 8`.
pub fn luma(r: u8, g: u8, b: u8) -> i64 {
(77 * r as i64 + 150 * g as i64 + 29 * b as i64 + 128) >> 8
}
/// The tent over a span of `l` samples at position `u`: `sgn(c)·(l − |2|c| − l|)` with
/// `c = 2u + 1 − l`.
pub fn tent(u: i64, l: i64) -> i64 {
let c = 2 * u + 1 - l;
c.signum() * (l - (2 * c.abs() - l).abs())
}
/// One axis of the grid: for each sample, its cell's index, the tent there and the
/// cell's length.
struct Axis {
cell: Vec<usize>,
tent: Vec<i64>,
len: Vec<i64>,
edges: Vec<usize>,
}
impl Axis {
fn new(n: usize) -> Axis {
let edges: Vec<usize> = (0..=GRID).map(|i| i * n / GRID).collect();
let (mut cell, mut tentv, mut len) = (Vec::with_capacity(n), Vec::with_capacity(n), Vec::with_capacity(n));
for i in 0..GRID {
let l = (edges[i + 1] - edges[i]) as i64;
for x in edges[i]..edges[i + 1] {
cell.push(i);
tentv.push(tent((x - edges[i]) as i64, l));
len.push(l);
}
}
Axis { cell, tent: tentv, len, edges }
}
}
/// The statistics of each cell of one luma plane of `w` × `h`: `s = Σ ω·Y`.
fn cell_statistics(y: &[i64], w: usize, h: usize) -> Vec<i64> {
let (ax, ay) = (Axis::new(w), Axis::new(h));
let mut s = vec![0i64; CELLS];
for row in 0..h {
let (cy, fy) = (ay.cell[row], ay.tent[row]);
// Each row's sums per column cell first, then weighted by the row's tent.
let mut acc = [0i64; GRID];
for col in 0..w {
acc[ax.cell[col]] += ax.tent[col] * y[row * w + col];
}
for (cx, a) in acc.iter().enumerate() {
s[cy * GRID + cx] += fy * a;
}
}
s
}
/// The bits' statistics of one luma plane: `T_j = Σ ε_k·s_k` over bit `j`'s cells.
fn bit_statistics(y: &[i64], w: usize, h: usize) -> [i64; CODE_BITS] {
let lay = layout();
let s = cell_statistics(y, w, h);
let mut t = [0i64; CODE_BITS];
for (j, tj) in t.iter_mut().enumerate() {
for n in 0..CELLS_PER_BIT {
let k = lay.perm[CELLS_PER_BIT * j + n] as usize;
*tj += lay.sign[k] * s[k];
}
}
t
}
fn luma_plane(rgb: &[u8]) -> Vec<i64> {
rgb.as_chunks::<3>().0.iter().map(|p| luma(p[0], p[1], p[2])).collect()
}
/// The delta at a pixel of weight `z` (the pattern's sign times `ω`) in a cell of area `nu`
/// at amplitude `a`: `a·z/ν` rounded to the nearest integer, halves away from zero.
pub fn delta(a: i64, z: i64, nu: i64) -> i64 {
z.signum() * ((2 * a * z.abs() + nu) / (2 * nu))
}
/// What embedding one frame did.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FrameReport {
/// Bits whose margin the largest amplitude did not reach.
pub short: usize,
/// The amplitude of each bit.
pub amplitudes: Vec<u8>,
}
/// The marked frames of `frames` frames of `w` × `h` RGB8 under `payload` (§7.5.4), and
/// what embedding each frame did. The caller checks [`admits`].
pub fn embed(unmarked: &[u8], w: usize, h: usize, frames: usize, payload: &[u8; 8]) -> (Vec<u8>, Vec<FrameReport>) {
let lay = layout();
let code = encode(payload);
let sigma: Vec<i64> = (0..CODE_BITS).map(|j| if code[j] ^ lay.mask[j] == 1 { 1 } else { -1 }).collect();
let (ax, ay) = (Axis::new(w), Axis::new(h));
let fb = w * h * 3;
let mut out = unmarked.to_vec();
let mut reports = Vec::with_capacity(frames);
for f in 0..frames {
let frame = &unmarked[f * fb..(f + 1) * fb];
let dst = &mut out[f * fb..(f + 1) * fb];
let mut report = FrameReport { short: 0, amplitudes: Vec::with_capacity(CODE_BITS) };
for (j, &sg) in sigma.iter().enumerate() {
let cells: Vec<usize> = (0..CELLS_PER_BIT).map(|n| lay.perm[CELLS_PER_BIT * j + n] as usize).collect();
// The bit's weight mass and its statistic at amplitude a.
let mut q = 0i64;
for &k in &cells {
let (cx, cy) = (k % GRID, k / GRID);
for row in ay.edges[cy]..ay.edges[cy + 1] {
for col in ax.edges[cx]..ax.edges[cx + 1] {
q += (ax.tent[col] * ay.tent[row]).abs();
}
}
}
let at = |a: i64, apply: Option<&mut [u8]>| -> i64 {
let mut t = 0i64;
let mut apply = apply;
for &k in &cells {
let (cx, cy) = (k % GRID, k / GRID);
let eps = lay.sign[k];
for row in ay.edges[cy]..ay.edges[cy + 1] {
for col in ax.edges[cx]..ax.edges[cx + 1] {
let omega = ax.tent[col] * ay.tent[row];
let d = delta(a, sg * eps * omega, ax.len[col] * ay.len[row]);
let p = (row * w + col) * 3;
let px = [0, 1, 2].map(|c| (frame[p + c] as i64 + d).clamp(0, 255) as u8);
t += eps * omega * luma(px[0], px[1], px[2]);
if let Some(dst) = apply.as_deref_mut() {
dst[p..p + 3].copy_from_slice(&px);
}
}
}
}
t
};
let chosen = (0..=AMPLITUDE).find(|&a| MARGIN.1 * sg * at(a, None) >= MARGIN.0 * q);
if chosen.is_none() {
report.short += 1;
}
let a = chosen.unwrap_or(AMPLITUDE);
at(a, Some(dst));
report.amplitudes.push(a as u8);
}
reports.push(report);
}
(out, reports)
}
/// The luma plane of a frame seen in orientation `o` (§7.5.5): transposed when `o & 4`,
/// then its rows reversed when `o & 2`, then its columns reversed when `o & 1`. Returns the
/// plane and its width and height.
pub fn orient(y: &[i64], w: usize, h: usize, o: u8) -> (Vec<i64>, usize, usize) {
let (ow, oh) = if o & 4 != 0 { (h, w) } else { (w, h) };
let mut out = vec![0i64; w * h];
for oy in 0..oh {
for ox in 0..ow {
let x1 = if o & 1 != 0 { ow - 1 - ox } else { ox };
let y1 = if o & 2 != 0 { oh - 1 - oy } else { oy };
let (sx, sy) = if o & 4 != 0 { (y1, x1) } else { (x1, y1) };
out[oy * ow + ox] = y[sy * w + sx];
}
}
(out, ow, oh)
}
/// What the detector read.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Detected {
pub payload: [u8; 8],
/// The orientation it decoded in.
pub orientation: u8,
/// The bits the code corrected.
pub errors: usize,
}
/// The word the detector reads in one orientation: the sign of each bit's statistic summed
/// over the frames, unmasked.
pub fn read_word(samples: &[u8], w: usize, h: usize, frames: usize, o: u8) -> [u8; CODE_BITS] {
let lay = layout();
let fb = w * h * 3;
let mut t = [0i64; CODE_BITS];
for f in 0..frames {
let (y, ow, oh) = orient(&luma_plane(&samples[f * fb..(f + 1) * fb]), w, h, o);
let ft = bit_statistics(&y, ow, oh);
for j in 0..CODE_BITS {
t[j] += ft[j];
}
}
let mut word = [0u8; CODE_BITS];
for j in 0..CODE_BITS {
word[j] = (t[j] > 0) as u8 ^ lay.mask[j];
}
word
}
/// The key-free detector (§7.5.5): the payload of the first orientation that decodes, or
/// `None`. Frames the scheme does not admit carry no mark.
pub fn detect(samples: &[u8], w: usize, h: usize, frames: usize) -> Option<Detected> {
if !admits(w as u64, h as u64, frames as u64) || samples.len() != w * h * 3 * frames {
return None;
}
(0..ORIENTATIONS).find_map(|o| decode(&read_word(samples, w, h, frames, o)).map(|(payload, errors)| Detected { payload, orientation: o, errors }))
}
/// The sum of squared differences the embedding can make in one frame at most: every
/// carrying cell at the largest amplitude, in all three channels, with no clamp (§7.5.6).
pub fn floor_sse(w: usize, h: usize) -> u64 {
let (ax, ay) = (Axis::new(w), Axis::new(h));
let lay = layout();
let mut sse = 0u64;
for row in 0..h {
for col in 0..w {
if lay.bit_of_cell[ay.cell[row] * GRID + ax.cell[col]].is_none() {
continue;
}
let d = delta(AMPLITUDE, ax.tent[col] * ay.tent[row], ax.len[col] * ay.len[row]);
sse += 3 * (d * d) as u64;
}
}
sse
}
/// SHA-256 of a buffer.
pub fn sha256(b: &[u8]) -> [u8; 32] {
Sha256::digest(b).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn hexs(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[test]
fn the_generator_is_the_definitions() {
let g = generator_bits();
assert_eq!(g.len(), 185, "degree 184: k = 71");
let mut v: u128 = 0;
let mut hi: u128 = 0;
for (d, &b) in g.iter().enumerate() {
if d < 128 {
v |= (b as u128) << d;
} else {
hi |= (b as u128) << (d - 128);
}
}
let hex = format!("{hi:x}{v:032x}");
assert_eq!(hex, GENERATOR_HEX);
}
#[test]
fn the_code_corrects_29_errors_and_no_more_is_claimed() {
let mut r = stream();
for trial in 0..64 {
let mut p = [0u8; 8];
for b in p.iter_mut() {
*b = r.next().unwrap() as u8;
}
p[0] &= 3;
let c = encode(&p);
assert_eq!(decode(&c), Some((p, 0)));
let mut e = c;
let n = trial % 30;
let mut flipped = Vec::new();
while flipped.len() < n {
let pos = r.next().unwrap() as usize % CODE_BITS;
if !flipped.contains(&pos) {
flipped.push(pos);
e[pos] ^= 1;
}
}
assert_eq!(decode(&e), Some((p, n)), "trial {trial}: {n} errors");
}
}
#[test]
fn a_class_above_3_is_no_payload() {
let p = [4u8, 1, 2, 3, 4, 5, 6, 7];
assert_eq!(decode(&encode(&p)), None);
}
#[test]
fn the_mask_and_its_complement_decode_to_nothing() {
let lay = layout();
assert_eq!(decode(&lay.mask), None);
let mut c = lay.mask;
for b in c.iter_mut() {
*b ^= 1;
}
assert_eq!(decode(&c), None);
}
#[test]
fn the_layout_is_a_permutation() {
let lay = layout();
let mut p = lay.perm.clone();
p.sort_unstable();
assert!(p.iter().enumerate().all(|(i, &v)| v as usize == i));
assert_eq!(lay.bit_of_cell.iter().filter(|b| b.is_none()).count(), CELLS - CODE_BITS * CELLS_PER_BIT);
}
#[test]
fn the_tent_is_odd_and_bounded() {
for l in 2..40i64 {
let v: Vec<i64> = (0..l).map(|u| tent(u, l)).collect();
assert_eq!(v.iter().sum::<i64>(), 0, "zero-mean over {l}");
assert!(v.iter().all(|x| x.abs() <= l));
for u in 0..l as usize {
assert_eq!(v[u], -v[l as usize - 1 - u]);
}
}
}
/// A frame of smooth structure and texture, from the stream.
fn picture(w: usize, h: usize, seed: u32) -> Vec<u8> {
let mut r = stream().skip(seed as usize * 7);
let mut px = Vec::with_capacity(w * h * 3);
let a = r.next().unwrap() as i64 % 7 + 3;
for y in 0..h {
for x in 0..w {
let base = ((x as i64 * a + y as i64 * 3) % 200) + 20;
for c in 0..3 {
let n = (r.next().unwrap() % 9) as i64 - 4;
px.push((base + n + c as i64 * 5).clamp(0, 255) as u8);
}
}
}
px
}
#[test]
fn a_marked_frame_carries_its_payload_and_only_the_derivation_changes_it() {
let (w, h) = (200, 136);
let unmarked = picture(w, h, 1);
let payload = [3u8, 0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03];
let (marked, rep) = embed(&unmarked, w, h, 1, &payload);
assert_eq!(rep.len(), 1);
let d = detect(&marked, w, h, 1).expect("a mark");
assert_eq!((d.payload, d.orientation), (payload, 0));
// The embedding is a function: the same inputs give the same bytes.
assert_eq!(embed(&unmarked, w, h, 1, &payload).0, marked);
// No pixel moved further than the amplitude.
assert!(unmarked.iter().zip(&marked).all(|(a, b)| (*a as i64 - *b as i64).abs() <= AMPLITUDE));
// Unmarked, it reads nothing.
assert_eq!(detect(&unmarked, w, h, 1), None);
}
#[test]
fn every_orientation_is_read() {
let (w, h) = (160, 128);
let payload = [3u8, 9, 8, 7, 6, 5, 4, 3];
let (marked, _) = embed(&picture(w, h, 2), w, h, 1, &payload);
for o in 0..ORIENTATIONS {
// The content that orientation o turns back into the marked frame: content
// pixel S(x, y) is the frame's (x, y), S as `orient` reads it.
let (ow, oh) = if o & 4 != 0 { (h, w) } else { (w, h) };
let mut c = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let x1 = if o & 1 != 0 { w - 1 - x } else { x };
let y1 = if o & 2 != 0 { h - 1 - y } else { y };
let (sx, sy) = if o & 4 != 0 { (y1, x1) } else { (x1, y1) };
c[(sy * ow + sx) * 3..(sy * ow + sx) * 3 + 3].copy_from_slice(&marked[(y * w + x) * 3..(y * w + x) * 3 + 3]);
}
}
let d = detect(&c, ow, oh, 1).unwrap_or_else(|| panic!("orientation {o}"));
assert_eq!(d.payload, payload, "orientation {o}");
assert_eq!(d.orientation, o);
}
}
#[test]
fn the_definition_is_stable() {
assert_eq!(hexs(&digest()).len(), 64);
assert!(definition_text().starts_with("{\"amplitude\":12,"));
}
}

View file

@ -0,0 +1,222 @@
//! What the second marking layer's registered scheme survives, measured
//! (extensions/synthesis.md §7.5.7 to §7.5.9), and the scheme's conformance corpus
//! (`live-conformance/synthesis-mark/`).
//!
//! The scheme itself, `wai-mark:tent64`, is [`crate::synthesis::mark`]: its embedding and its
//! key-free detector are part of the synthesis verifier, which recomputes a marking and runs a
//! detection again. This module holds what is not needed to verify:
//!
//! - [`battery`]: the attacks, each an integer function of the frames (re-encoding with the
//! crate's own integer codecs, rescaling, cropping, rotation, noise from a fixed stream,
//! brightness and contrast, a local blur, the quarter turns and mirrors), so a run of the
//! battery is the same bytes on every machine;
//! - the **measurement sets** ([`spectral_set`], [`leaves_set`], [`clips`]): pictures and
//! clips the crate generates from fixed seeds, the spectral generator of `wai.generate.*`
//! with its committed model and a dead-leaves generator of this module;
//! - the **quality figures** ([`psnr`], [`ssim`]) the measurement states per picture;
//! - [`corpus`]: the conformance corpus, its battery table and its verifier.
pub mod battery;
pub mod corpus;
use crate::synthesis::mark;
use sha2::{Digest, Sha256};
/// One picture or clip: `n` frames of `w` × `h` RGB8, frame after frame.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frames {
pub data: Vec<u8>,
pub w: usize,
pub h: usize,
pub n: usize,
}
impl Frames {
pub fn frame(&self, i: usize) -> &[u8] {
let fb = self.w * self.h * 3;
&self.data[i * fb..(i + 1) * fb]
}
pub fn one(data: Vec<u8>, w: usize, h: usize) -> Frames {
Frames { data, w, h, n: 1 }
}
}
/// A stream of 32-bit words: SHA-256 of `label`, `seed` as a big-endian `u32` and a
/// big-endian `u32` counter from 0, each digest read as eight big-endian words.
pub fn words(label: &[u8], seed: u32) -> impl Iterator<Item = u32> + use<> {
let label = label.to_vec();
(0u32..).flat_map(move |i| {
let d: [u8; 32] = Sha256::new().chain_update(&label).chain_update(seed.to_be_bytes()).chain_update(i.to_be_bytes()).finalize().into();
(0..8).map(move |o| u32::from_be_bytes(d[o * 4..o * 4 + 4].try_into().expect("four bytes")))
})
}
// ---------------------------------------------------------------------------------------
// The measurement sets
// ---------------------------------------------------------------------------------------
/// The side of a measurement set's pictures.
pub const SET_SIDE: usize = 512;
/// The pictures in each set.
pub const SET_SIZE: usize = 16;
/// The prompts of the spectral set, one picture each, seeded by its index.
pub const PROMPTS: [&str; SET_SIZE] = [
"a calm ocean horizon",
"fire and chaos",
"a tall forest",
"a spiral galaxy",
"a storm cloud",
"a desert dune at sunset",
"a jungle leaf",
"marble stone",
"a city grid",
"lava ember",
"an ice glacier",
"a nebula in the cosmos",
"a garden in blossom",
"a maze lattice",
"a whirlpool of water",
"a mountain in snow",
];
fn spectral_model() -> &'static crate::generate::SpectralModel {
static M: std::sync::OnceLock<crate::generate::SpectralModel> = std::sync::OnceLock::new();
M.get_or_init(|| {
let v: serde_json::Value = serde_json::from_str(include_str!("../tests/vectors/generate/generate_spectral_model.json")).expect("the committed model");
crate::generate::SpectralModel::from_value(&v)
})
}
/// A spectral picture: `wai.generate.spectral`'s generator with its committed model.
pub fn spectral(prompt: &str, seed: u64, w: usize, h: usize) -> Vec<u8> {
crate::generate::generate(spectral_model(), prompt, seed, 4, h, w)
}
/// The spectral set: [`PROMPTS`], each at [`SET_SIDE`] square, seed its index.
pub fn spectral_set() -> Vec<(String, Frames)> {
PROMPTS.iter().enumerate().map(|(i, p)| (format!("spectral-{i:02}"), Frames::one(spectral(p, i as u64, SET_SIDE, SET_SIDE), SET_SIDE, SET_SIDE))).collect()
}
/// A dead-leaves picture: 600 discs painted back to front over grey, each with a radius
/// `⌊r_max·16 / (16 + u)⌋` (at least 3) for `u` uniform in 0..240, a colour each channel of
/// which is uniform in 16..240, and a linear shading of slope up to 3/8 a pixel; then noise
/// uniform in −2..2 on every sample. Every draw is a word of [`words`] with the label
/// `wai-mark:battery leaves` and the seed.
pub fn leaves(seed: u32, w: usize, h: usize) -> Vec<u8> {
let mut r = words(b"wai-mark:battery leaves", seed);
let mut next = move || r.next().expect("an endless stream") as i64;
let mut px = vec![128u8; w * h * 3];
let rmax = (w.min(h) / 4) as i64;
for _ in 0..600 {
let (cx, cy) = (next() % w as i64, next() % h as i64);
let rad = (rmax * 16 / (16 + next() % 240)).max(3);
let base = [next() % 224 + 16, next() % 224 + 16, next() % 224 + 16];
let (gx, gy) = (next() % 7 - 3, next() % 7 - 3);
for y in (cy - rad).max(0)..(cy + rad + 1).min(h as i64) {
for x in (cx - rad).max(0)..(cx + rad + 1).min(w as i64) {
let (dx, dy) = (x - cx, y - cy);
if dx * dx + dy * dy > rad * rad {
continue;
}
let shade = (gx * dx + gy * dy).div_euclid(8);
let p = (y as usize * w + x as usize) * 3;
for c in 0..3 {
px[p + c] = (base[c] + shade).clamp(0, 255) as u8;
}
}
}
}
for v in px.iter_mut() {
*v = (*v as i64 + next() % 5 - 2).clamp(0, 255) as u8;
}
px
}
/// The dead-leaves set: seeds 0 to 15, each at [`SET_SIDE`] square.
pub fn leaves_set() -> Vec<(String, Frames)> {
(0..SET_SIZE as u32).map(|i| (format!("leaves-{i:02}"), Frames::one(leaves(i, SET_SIDE, SET_SIDE), SET_SIDE, SET_SIDE))).collect()
}
/// The side and length of the measurement clips.
pub const CLIP_SIDE: usize = 256;
pub const CLIP_FRAMES: usize = 8;
/// A clip: a [`CLIP_SIDE`] square window panning over a picture of [`SET_SIDE`], `(4, 6)`
/// pixels a frame from `(64, 32)`.
pub fn pan(src: &[u8]) -> Frames {
let mut data = Vec::with_capacity(CLIP_SIDE * CLIP_SIDE * 3 * CLIP_FRAMES);
for f in 0..CLIP_FRAMES {
let (x0, y0) = (64 + 6 * f, 32 + 4 * f);
for y in y0..y0 + CLIP_SIDE {
data.extend_from_slice(&src[(y * SET_SIDE + x0) * 3..(y * SET_SIDE + x0 + CLIP_SIDE) * 3]);
}
}
Frames { data, w: CLIP_SIDE, h: CLIP_SIDE, n: CLIP_FRAMES }
}
/// The clips: panning over two dead-leaves and two spectral pictures.
pub fn clips() -> Vec<(String, Frames)> {
vec![
("clip-leaves-a".into(), pan(&leaves(100, SET_SIDE, SET_SIDE))),
("clip-leaves-b".into(), pan(&leaves(101, SET_SIDE, SET_SIDE))),
("clip-spectral-a".into(), pan(&spectral("a river in the valley", 100, SET_SIDE, SET_SIDE))),
("clip-spectral-b".into(), pan(&spectral("a swirl of smoke", 101, SET_SIDE, SET_SIDE))),
]
}
// ---------------------------------------------------------------------------------------
// Quality
// ---------------------------------------------------------------------------------------
/// The sum of squared differences of two buffers.
pub fn sse(a: &[u8], b: &[u8]) -> u64 {
a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).pow(2) as u64).sum()
}
/// PSNR in decibels of `samples` samples with this SSE: `10·log10(255²·samples / sse)`.
pub fn psnr(sse: u64, samples: usize) -> f64 {
10.0 * ((255.0 * 255.0 * samples as f64) / sse.max(1) as f64).log10()
}
/// The PSNR floor of a frame of `w` × `h` (§7.5.6): the PSNR of the full-amplitude
/// embedding.
pub fn floor(w: usize, h: usize) -> f64 {
psnr(mark::floor_sse(w, h), w * h * 3)
}
/// SSIM of two frames' luma: the mean over 8 × 8 windows at a stride of 4 of
/// `(2μxμy + C1)(2σxy + C2) / ((μx² + μy² + C1)(σx² + σy² + C2))`, `C1 = (0.01·255)²`,
/// `C2 = (0.03·255)²`, population moments. Only additions, multiplications and divisions,
/// in a fixed order, so the same bits on every machine.
pub fn ssim(a: &[u8], b: &[u8], w: usize, h: usize) -> f64 {
let ya: Vec<f64> = a.as_chunks::<3>().0.iter().map(|p| mark::luma(p[0], p[1], p[2]) as f64).collect();
let yb: Vec<f64> = b.as_chunks::<3>().0.iter().map(|p| mark::luma(p[0], p[1], p[2]) as f64).collect();
let (c1, c2) = ((0.01f64 * 255.0).powi(2), (0.03f64 * 255.0).powi(2));
let (mut total, mut n) = (0.0f64, 0u64);
let mut y0 = 0;
while y0 + 8 <= h {
let mut x0 = 0;
while x0 + 8 <= w {
let (mut sa, mut sb, mut saa, mut sbb, mut sab) = (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64);
for y in y0..y0 + 8 {
for x in x0..x0 + 8 {
let (p, q) = (ya[y * w + x], yb[y * w + x]);
sa += p;
sb += q;
saa += p * p;
sbb += q * q;
sab += p * q;
}
}
let (ma, mb) = (sa / 64.0, sb / 64.0);
let (va, vb, cab) = (saa / 64.0 - ma * ma, sbb / 64.0 - mb * mb, sab / 64.0 - ma * mb);
total += ((2.0 * ma * mb + c1) * (2.0 * cab + c2)) / ((ma * ma + mb * mb + c1) * (va + vb + c2));
n += 1;
x0 += 4;
}
y0 += 4;
}
total / n as f64
}

View file

@ -0,0 +1,352 @@
//! The attack battery of extensions/synthesis.md §7.5.7: each attack an integer function of
//! the frames, so its result is the same bytes on every machine.
use super::{words, Frames};
use crate::int_video_enc::{encode, EncParams};
use crate::synthesis::{denoise3, upscale2};
/// An attack of the battery.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Attack {
/// The frames as marked.
None,
/// `wai.video.int_motion`'s encoder at step `q`, each frame on its own (one keyframe).
IntMotion(u32),
/// The base of a staged picture: each 2 × 2 block's rounded mean, encoded as
/// `wai.video.int_motion` at step `q`, then `wai-synthesis:upscale2`.
StagedBase(u32),
/// `wai-synthesis:denoise3`, applied `n` times.
Denoise(u8),
/// Area resampling to `⌊w·num/den⌋` × `⌊h·num/den⌋`.
Scale(u32, u32),
/// Area resampling of the width alone, to `⌊w·num/den⌋`.
Aspect(u32, u32),
/// `wai-synthesis:upscale2`.
Upscale2,
/// A centred crop keeping `100 − p` percent of each side.
Crop(u32),
/// The columns reversed.
Mirror,
/// A quarter turn clockwise.
Rotate90,
/// A rotation by a small angle about the centre, bilinear, by the `(cos, sin)` of
/// [`ROTATIONS`] at that index.
Rotate(usize),
/// Noise uniform in `−s..=s` on every sample.
Noise(u32),
/// A constant added to every sample.
Brightness(i32),
/// `128 + (v − 128)·num/den`, rounded half away from zero.
Contrast(u32, u32),
/// The central quarter of the picture's area replaced by the means of its 16 × 16
/// blocks.
LocalBlur,
/// Area resampling to half, then `wai.video.int_motion` at step `q`.
HalfThenIntMotion(u32),
/// A clip encoded as `wai.video.int_motion` with motion: blocks of 16, a search radius
/// of 4, half-pel vectors, step `q`.
ClipIntMotion(u32),
/// Every other frame of a clip, from the first.
FrameDrop,
/// The first frame of a clip alone.
OneFrame,
}
/// The rotations' angles in tenths of a degree, and their cosines and sines at `2^16`.
pub const ROTATIONS: [(u32, i64, i64); 3] = [(5, 65534, 572), (10, 65526, 1144), (20, 65496, 2287)];
/// The attacks applied to pictures, by name, in the table's order.
pub const IMAGE_ATTACKS: [(&str, Attack); 33] = [
("none", Attack::None),
("int-motion-q4", Attack::IntMotion(4)),
("int-motion-q8", Attack::IntMotion(8)),
("int-motion-q16", Attack::IntMotion(16)),
("int-motion-q32", Attack::IntMotion(32)),
("int-motion-q48", Attack::IntMotion(48)),
("staged-base-q8", Attack::StagedBase(8)),
("denoise3", Attack::Denoise(1)),
("denoise3-x4", Attack::Denoise(4)),
("scale-3/4", Attack::Scale(3, 4)),
("scale-1/2", Attack::Scale(1, 2)),
("scale-1/3", Attack::Scale(1, 3)),
("scale-1/4", Attack::Scale(1, 4)),
("scale-3/2", Attack::Scale(3, 2)),
("upscale2", Attack::Upscale2),
("aspect-3/4", Attack::Aspect(3, 4)),
("half-then-int-motion-q16", Attack::HalfThenIntMotion(16)),
("crop-1%", Attack::Crop(1)),
("crop-2%", Attack::Crop(2)),
("crop-5%", Attack::Crop(5)),
("crop-10%", Attack::Crop(10)),
("mirror", Attack::Mirror),
("rotate-90", Attack::Rotate90),
("rotate-0.5", Attack::Rotate(0)),
("rotate-1", Attack::Rotate(1)),
("rotate-2", Attack::Rotate(2)),
("noise-4", Attack::Noise(4)),
("noise-16", Attack::Noise(16)),
("brightness+32", Attack::Brightness(32)),
("brightness-32", Attack::Brightness(-32)),
("contrast-1/2", Attack::Contrast(1, 2)),
("contrast-3/2", Attack::Contrast(3, 2)),
("local-blur", Attack::LocalBlur),
];
/// The attacks applied to clips, by name, in the table's order.
pub const CLIP_ATTACKS: [(&str, Attack); 7] = [
("none", Attack::None),
("clip-int-motion-q8", Attack::ClipIntMotion(8)),
("clip-int-motion-q24", Attack::ClipIntMotion(24)),
("frame-drop", Attack::FrameDrop),
("one-frame", Attack::OneFrame),
("scale-1/2", Attack::Scale(1, 2)),
("noise-16", Attack::Noise(16)),
];
fn each_frame(f: &Frames, w: usize, h: usize, g: impl Fn(&[u8]) -> Vec<u8>) -> Frames {
let mut data = Vec::with_capacity(w * h * 3 * f.n);
for i in 0..f.n {
data.extend(g(f.frame(i)));
}
Frames { data, w, h, n: f.n }
}
/// `wai.video.int_motion` of one frame at step `q`: a keyframe, whose reconstruction is
/// `clamp(128 + r·q)` with `r` the sample less 128 divided by `q`, rounded half away from
/// zero (integer-payloads §3).
pub fn int_motion(frame: &[u8], w: usize, h: usize, q: u32) -> Vec<u8> {
let p = EncParams { block: 1, search: 0, mv_precision: 1, q };
encode(&[frame.to_vec()], h as u32, w as u32, &p).expect("a frame the encoder takes").recon.remove(0)
}
/// For each destination sample along an axis of `n` source samples resampled to `m`: the
/// source samples it covers and their overlaps, in units of `1/m` of a source sample; the
/// overlaps of one destination sample sum to `n`.
fn overlaps(n: usize, m: usize) -> Vec<Vec<(usize, u64)>> {
(0..m)
.map(|d| {
let (lo, hi) = (d * n, (d + 1) * n);
(lo / m..hi.div_ceil(m))
.filter_map(|s| {
let o = hi.min((s + 1) * m) as i64 - lo.max(s * m) as i64;
(o > 0).then_some((s, o as u64))
})
.collect()
})
.collect()
}
/// Area resampling of one frame to `nw` × `nh`: each destination sample the mean of the
/// source it covers, weighted by overlap, rounded half up.
pub fn area(frame: &[u8], w: usize, h: usize, nw: usize, nh: usize) -> Vec<u8> {
let (ox, oy) = (overlaps(w, nw), overlaps(h, nh));
let den = (w * h) as u64;
let mut out = vec![0u8; nw * nh * 3];
for (dy, ry) in oy.iter().enumerate() {
for (dx, rx) in ox.iter().enumerate() {
for c in 0..3 {
let mut num = 0u64;
for &(sy, wy) in ry {
for &(sx, wx) in rx {
num += wy * wx * frame[(sy * w + sx) * 3 + c] as u64;
}
}
out[(dy * nw + dx) * 3 + c] = ((2 * num + den) / (2 * den)) as u8;
}
}
}
out
}
/// `n / d` rounded to the nearest integer, halves away from zero.
fn rdiv(n: i64, d: i64) -> i64 {
n.signum() * ((2 * n.abs() + d) / (2 * d))
}
/// A rotation of one frame about its centre by the angle whose cosine and sine at `2^16`
/// are `(c, s)`: destination `(x, y)` reads the source at `(X, Y)`, where, at `2^16`,
/// `X = (c·u + s·v + (w − 1)·2^16) >> 1` and `Y = (−s·u + c·v + (h − 1)·2^16) >> 1` with
/// `u = 2x + 1 − w` and `v = 2y + 1 − h`, bilinearly from the four samples around it, a
/// sample outside the frame being 0, rounded half up.
pub fn rotate(frame: &[u8], w: usize, h: usize, c: i64, s: i64) -> Vec<u8> {
let at = |x: i64, y: i64, ch: usize| -> i64 {
if x < 0 || y < 0 || x >= w as i64 || y >= h as i64 { 0 } else { frame[(y as usize * w + x as usize) * 3 + ch] as i64 }
};
let one = 1i64 << 16;
let mut out = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let (u, v) = (2 * x as i64 + 1 - w as i64, 2 * y as i64 + 1 - h as i64);
let px = (c * u + s * v + (w as i64 - 1) * one) >> 1;
let py = (-s * u + c * v + (h as i64 - 1) * one) >> 1;
let (xi, fx, yi, fy) = (px >> 16, px & 0xffff, py >> 16, py & 0xffff);
for ch in 0..3 {
let v = at(xi, yi, ch) * (one - fx) * (one - fy)
+ at(xi + 1, yi, ch) * fx * (one - fy)
+ at(xi, yi + 1, ch) * (one - fx) * fy
+ at(xi + 1, yi + 1, ch) * fx * fy;
out[(y * w + x) * 3 + ch] = ((v + (1 << 31)) >> 32) as u8;
}
}
}
out
}
/// Apply an attack to frames, with `seed` for the noise's stream.
pub fn apply(a: Attack, f: &Frames, seed: u32) -> Frames {
let (w, h) = (f.w, f.h);
match a {
Attack::None => f.clone(),
Attack::IntMotion(q) => each_frame(f, w, h, |fr| int_motion(fr, w, h, q)),
Attack::StagedBase(q) => each_frame(f, w, h, |fr| {
let (hw, hh) = (w / 2, h / 2);
let mut half = vec![0u8; hw * hh * 3];
for y in 0..hh {
for x in 0..hw {
for c in 0..3 {
let s = |dx: usize, dy: usize| fr[((2 * y + dy) * w + 2 * x + dx) * 3 + c] as u32;
half[(y * hw + x) * 3 + c] = ((s(0, 0) + s(1, 0) + s(0, 1) + s(1, 1) + 2) >> 2) as u8;
}
}
}
upscale2(&int_motion(&half, hw, hh, q), hw, hh)
}),
Attack::Denoise(n) => each_frame(f, w, h, |fr| (0..n).fold(fr.to_vec(), |b, _| denoise3(&b, w, h))),
Attack::Scale(num, den) => {
let (nw, nh) = (w * num as usize / den as usize, h * num as usize / den as usize);
each_frame(f, nw, nh, |fr| area(fr, w, h, nw, nh))
}
Attack::Aspect(num, den) => {
let nw = w * num as usize / den as usize;
each_frame(f, nw, h, |fr| area(fr, w, h, nw, h))
}
Attack::Upscale2 => each_frame(f, 2 * w, 2 * h, |fr| upscale2(fr, w, h)),
Attack::Crop(p) => {
let (dx, dy) = (w * p as usize / 200, h * p as usize / 200);
let (nw, nh) = (w - 2 * dx, h - 2 * dy);
each_frame(f, nw, nh, |fr| {
let mut out = Vec::with_capacity(nw * nh * 3);
for y in dy..dy + nh {
out.extend_from_slice(&fr[(y * w + dx) * 3..(y * w + dx + nw) * 3]);
}
out
})
}
Attack::Mirror => each_frame(f, w, h, |fr| {
let mut out = Vec::with_capacity(w * h * 3);
for y in 0..h {
for x in (0..w).rev() {
out.extend_from_slice(&fr[(y * w + x) * 3..(y * w + x) * 3 + 3]);
}
}
out
}),
Attack::Rotate90 => each_frame(f, h, w, |fr| {
// Clockwise: destination (x', y') of h × w is source (y', h − 1 − x').
let mut out = Vec::with_capacity(w * h * 3);
for yp in 0..w {
for xp in 0..h {
let (sx, sy) = (yp, h - 1 - xp);
out.extend_from_slice(&fr[(sy * w + sx) * 3..(sy * w + sx) * 3 + 3]);
}
}
out
}),
Attack::Rotate(i) => {
let (_, c, s) = ROTATIONS[i];
each_frame(f, w, h, |fr| rotate(fr, w, h, c, s))
}
Attack::Noise(s) => {
let mut r = words(b"wai-mark:battery noise", seed);
let span = 2 * s + 1;
Frames { data: f.data.iter().map(|&v| (v as i64 + (r.next().expect("an endless stream") % span) as i64 - s as i64).clamp(0, 255) as u8).collect(), ..f.clone() }
}
Attack::Brightness(b) => Frames { data: f.data.iter().map(|&v| (v as i64 + b as i64).clamp(0, 255) as u8).collect(), ..f.clone() },
Attack::Contrast(num, den) => {
Frames { data: f.data.iter().map(|&v| (128 + rdiv((v as i64 - 128) * num as i64, den as i64)).clamp(0, 255) as u8).collect(), ..f.clone() }
}
Attack::LocalBlur => each_frame(f, w, h, |fr| {
let mut out = fr.to_vec();
let (x0, x1, y0, y1) = (w / 4, 3 * w / 4, h / 4, 3 * h / 4);
for by in (y0..y1).step_by(16) {
for bx in (x0..x1).step_by(16) {
let (ex, ey) = ((bx + 16).min(x1), (by + 16).min(y1));
let n = ((ex - bx) * (ey - by)) as u64;
for c in 0..3 {
let mut sum = 0u64;
for y in by..ey {
for x in bx..ex {
sum += fr[(y * w + x) * 3 + c] as u64;
}
}
let m = ((2 * sum + n) / (2 * n)) as u8;
for y in by..ey {
for x in bx..ex {
out[(y * w + x) * 3 + c] = m;
}
}
}
}
}
out
}),
Attack::HalfThenIntMotion(q) => {
let (nw, nh) = (w / 2, h / 2);
each_frame(f, nw, nh, |fr| int_motion(&area(fr, w, h, nw, nh), nw, nh, q))
}
Attack::ClipIntMotion(q) => {
let p = EncParams { block: 16, search: 4, mv_precision: 2, q };
let frames: Vec<Vec<u8>> = (0..f.n).map(|i| f.frame(i).to_vec()).collect();
let enc = encode(&frames, h as u32, w as u32, &p).expect("a clip the encoder takes");
Frames { data: enc.recon.concat(), ..f.clone() }
}
Attack::FrameDrop => {
let kept: Vec<usize> = (0..f.n).step_by(2).collect();
Frames { data: kept.iter().flat_map(|&i| f.frame(i).to_vec()).collect(), n: kept.len(), ..f.clone() }
}
Attack::OneFrame => Frames { data: f.frame(0).to_vec(), n: 1, ..f.clone() },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn area_resampling_keeps_a_flat_frame_and_halves_exactly() {
let flat = vec![77u8; 10 * 6 * 3];
assert!(area(&flat, 10, 6, 7, 4).iter().all(|&v| v == 77));
let mut f = vec![0u8; 4 * 2 * 3];
for (i, v) in f.iter_mut().enumerate() {
*v = (i * 10) as u8;
}
let half = area(&f, 4, 2, 2, 1);
// Each half is the mean of its 2 × 2 block, per channel.
let want = |x0: usize, c: usize| ((f[x0 * 3 + c] as u32 + f[(x0 + 1) * 3 + c] as u32 + f[(4 + x0) * 3 + c] as u32 + f[(5 + x0) * 3 + c] as u32 + 2) / 4) as u8;
assert_eq!(half, vec![want(0, 0), want(0, 1), want(0, 2), want(2, 0), want(2, 1), want(2, 2)]);
}
#[test]
fn the_overlaps_of_a_destination_sum_to_the_source() {
for (n, m) in [(512, 384), (512, 170), (100, 300), (7, 3)] {
for o in overlaps(n, m) {
assert_eq!(o.iter().map(|x| x.1).sum::<u64>(), n as u64);
}
}
}
#[test]
fn a_zero_rotation_is_the_identity() {
let f: Vec<u8> = (0..20 * 12 * 3).map(|i| (i * 7 % 251) as u8).collect();
assert_eq!(rotate(&f, 20, 12, 1 << 16, 0), f);
}
#[test]
fn a_keyframe_is_scalar_quantisation() {
let f: Vec<u8> = (0..16 * 16 * 3).map(|i| (i * 13 % 256) as u8).collect();
let r = int_motion(&f, 16, 16, 8);
for (a, b) in f.iter().zip(&r) {
assert_eq!(*b as i64, (128 + rdiv(*a as i64 - 128, 8) * 8).clamp(0, 255));
}
}
}

View file

@ -0,0 +1,963 @@
//! The conformance corpus of the second marking layer's registered scheme
//! (extensions/synthesis.md §7.5 to §7.8, §13): `live-conformance/synthesis-mark/`.
//!
//! [`generate`] writes it from fixed seeds: the scheme's definition (`scheme.json`, the bytes
//! whose SHA-256 is the scheme's digest); the frames its vectors name (`frames/<sha256>.rgb`,
//! raw RGB8, each named by its SHA-256); `vectors.json`, with the code's words, the
//! embeddings, the detections, a battery sample a second implementation runs again, and
//! verification cases of §11 whose markings are recomputed and whose detections are run
//! again; and `battery.json`, the measurement of §7.5.7 over the crate's own measurement sets.
//! Every key is SHA-256 of a label and an index, every picture is generated from a fixed seed
//! and Ed25519 signing is deterministic, so every file is the same bytes on every machine.
//!
//! [`verify`] checks a corpus directory against this module.
//! `live-conformance/synthesis_mark_verify.py` checks the same files from the extension's
//! text, sharing no code with this module.
use super::battery::{apply, Attack, CLIP_ATTACKS, IMAGE_ATTACKS};
use super::*;
use crate::jcs::jcs;
use crate::synthesis::corpus::outcome_json;
use crate::synthesis::{hex, link_hash, mark, read_origin, sign, At, Case, Outcome, Policy, Reason, MARK_REASONS};
use ed25519_dalek::SigningKey;
use serde_json::{json, Map, Value};
use std::collections::{BTreeMap, BTreeSet};
const ISSUED: &str = "2026-10-21T09:00:00Z";
const GENERATOR: u8 = 0;
const CODEC: u8 = 1;
const EDITOR: u8 = 2;
const CAMERA: u8 = 3;
const MARKER: u8 = 4;
const DETECTOR: u8 = 5;
const REVIEWER: u8 = 6;
const PLAYER: u8 = 7;
fn sha(b: &[u8]) -> [u8; 32] {
mark::sha256(b)
}
fn key(i: u8) -> SigningKey {
SigningKey::from_bytes(&sha(&[b"wai:synthesis-mark/corpus/key\x00".as_slice(), &[i]].concat()))
}
fn label(s: &str) -> [u8; 32] {
sha(&[b"wai:synthesis-mark/corpus/label\x00".as_slice(), s.as_bytes()].concat())
}
fn text(v: &Value) -> String {
jcs(v).expect("a record in the subset")
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
}
/// A `WAI1` envelope (SPEC §2) of this manifest and payload.
fn envelope(media: &str, intent: &str, cap: &str, payload: &[u8]) -> Vec<u8> {
let manifest = format!(r#"{{"wai":"1.0","media":"{media}","intent":"{intent}","model_requirement":{{"capability":"{cap}"}}}}"#);
let mut b = b"WAI1".to_vec();
b.extend_from_slice(&(manifest.len() as u32).to_le_bytes());
b.extend_from_slice(manifest.as_bytes());
b.extend_from_slice(&(payload.len() as u32).to_le_bytes());
b.extend_from_slice(payload);
b
}
fn ch(env: &[u8]) -> [u8; 32] {
crate::jwp_hash::content_hash(env)
}
fn bits(b: &[u8]) -> String {
b.iter().map(|x| if *x == 1 { '1' } else { '0' }).collect()
}
/// The payload of a measurement picture: class 3, then seven bytes of a label's digest.
pub fn battery_payload(name: &str) -> [u8; 8] {
let d = sha(&[b"wai-mark:battery payload\x00".as_slice(), name.as_bytes()].concat());
let mut p = [3u8; 8];
p[1..].copy_from_slice(&d[..7]);
p
}
// ---- the corpus's pictures ----
/// The corpus's still: a spectral picture of 224 × 160.
pub const STILL: (usize, usize) = (224, 160);
/// The corpus's clip: three frames of 160 × 128.
pub const CLIP: (usize, usize, usize) = (160, 128, 3);
struct Pictures {
still: Vec<u8>,
photo: Vec<u8>,
clip: Vec<u8>,
}
fn pictures() -> Pictures {
let (w, h) = STILL;
let (cw, chh, n) = CLIP;
let src = leaves(8, 192, 144);
let mut clip = Vec::with_capacity(cw * chh * 3 * n);
for f in 0..n {
let (x0, y0) = (8 * f, 4 * f);
for y in y0..y0 + chh {
clip.extend_from_slice(&src[(y * 192 + x0) * 3..(y * 192 + x0 + cw) * 3]);
}
}
Pictures { still: spectral("a lagoon at dawn", 7, w, h), photo: leaves(7, w, h), clip }
}
// ---- records ----
#[allow(clippy::too_many_arguments)]
fn claim(env: &[u8], media: &str, class: &str, step: &str, operation: &str, source: &str, models: Value, oversight: Value, inputs: Value, who: u8) -> Value {
sign(
json!({
"kind": "wai-origin",
"subject": {"type": "object", "content_hash": hex(&ch(env))},
"media": media,
"class": class,
"step": step,
"operation": operation,
"record": null,
"source": source,
"models": models,
"oversight": oversight,
"inputs": inputs,
"evidence": null,
"issued_at": ISSUED,
}),
&key(who),
)
}
fn input_of(c: &Value) -> Value {
json!({"content_hash": c["subject"]["content_hash"], "origin": hex(&link_hash(c)), "class": c["class"], "source": c["source"]})
}
fn payload_of(c: &Value) -> [u8; 8] {
read_origin(&text(c)).expect("a corpus claim").mark_payload()
}
fn embedding(frames: usize, w: usize, h: usize, unmarked: &[u8], marked: &[u8]) -> Value {
json!({"frames": frames, "width": w, "height": h, "unmarked": hex(&sha(unmarked)), "marked": hex(&sha(marked))})
}
fn read(frames: usize, w: usize, h: usize, samples: &[u8]) -> Value {
json!({"frames": frames, "width": w, "height": h, "samples": hex(&sha(samples))})
}
fn marking(c: &Value, payload: [u8; 8], embedding: Value) -> Value {
sign(
json!({
"kind": "wai-marking",
"subject": c["subject"]["content_hash"],
"origin": hex(&link_hash(c)),
"scheme": mark::SCHEME,
"scheme_digest": hex(&mark::digest()),
"payload": hex(&payload),
"embedding": embedding,
"issued_at": ISSUED,
}),
&key(MARKER),
)
}
fn detection(subject: &[u8; 32], payload: Option<[u8; 8]>, read: Value) -> Value {
sign(
json!({
"kind": "wai-detection",
"subject": hex(subject),
"scheme": mark::SCHEME,
"scheme_digest": hex(&mark::digest()),
"payload": payload.map(|p| hex(&p)),
"read": read,
"issued_at": ISSUED,
}),
&key(DETECTOR),
)
}
fn with(mut body: Value, path: &[&str], v: Value) -> Value {
let mut at = &mut body;
for p in &path[..path.len() - 1] {
at = at.get_mut(*p).expect("a member");
}
at[path[path.len() - 1]] = v;
body
}
fn resign(body: Value, who: u8) -> Value {
sign(body, &key(who))
}
fn review(c: &Value, reconstruction: &[u8; 32]) -> Value {
sign(
json!({
"kind": "wai-review",
"subject": c["subject"]["content_hash"],
"origin": hex(&link_hash(c)),
"tier": "generation-equivalence",
"reconstruction": hex(reconstruction),
"decoder": "builtin:[email protected]",
"variant": null,
"scope": "rendering",
"verdict": "approved",
"notes": null,
"role": null,
"issued_at": ISSUED,
}),
&key(REVIEWER),
)
}
/// A player's log of one presentation of `c` with the output digest `output`, then the
/// review `rv` presented for it.
fn log(c: &Value, output: &[u8; 32], rv: &Value) -> Vec<String> {
let session = label("player session");
let present = json!({
"type": "present",
"subject": c["subject"]["content_hash"],
"origin": hex(&link_hash(c)),
"class": c["class"],
"media": c["media"],
"tier": "generation-equivalence",
"decoder": "builtin:[email protected]",
"variant": null,
"output": hex(output),
});
let first = sign(json!({"kind": "wai-player", "session": hex(&session), "seq": 0, "prev": null, "at": 0, "entry": present}), &key(PLAYER));
let second = sign(
json!({
"kind": "wai-player",
"session": hex(&session),
"seq": 1,
"prev": hex(&link_hash(&first)),
"at": 1_000_000,
"entry": {"type": "review", "subject": c["subject"]["content_hash"], "review": hex(&link_hash(rv))},
}),
&key(PLAYER),
);
vec![text(&first), text(&second)]
}
// ---- vectors ----
enum Intent {
Accept,
Refuse(At, Reason),
}
struct Vector {
name: &'static str,
about: &'static str,
case: Case,
intent: Intent,
}
fn held(pairs: &[(&[u8], &[u8])]) -> Vec<([u8; 32], Vec<u8>)> {
pairs.iter().map(|(env, px)| (ch(env), px.to_vec())).collect()
}
/// Everything the corpus's cases are built from.
struct World {
p: Pictures,
still_m: Vec<u8>,
clip_m: Vec<u8>,
copy: Vec<u8>,
crop: Vec<u8>,
crop_wh: (usize, usize),
wrong_m: Vec<u8>,
hidden_m: Vec<u8>,
still_env: Vec<u8>,
clip_env: Vec<u8>,
copy_env: Vec<u8>,
crop_env: Vec<u8>,
photo_env: Vec<u8>,
relabel_env: Vec<u8>,
still: Value,
clip: Value,
copy_claim: Value,
crop_claim: Value,
photo: Value,
relabel: Value,
}
fn world() -> World {
let p = pictures();
let (w, h) = STILL;
let (cw, chh, n) = CLIP;
let still_env = envelope("image", "create", "wai.generate.spectral", br#"{"prompt":"a lagoon at dawn","seed":7}"#);
let clip_env = envelope("video", "replicate", "wai.video.int_motion", &label("clip payload"));
let copy_env = envelope("image", "replicate", "wai.image.png", &label("re-encoded copy"));
let crop_env = envelope("image", "replicate", "wai.image.png", &label("cropped copy"));
let photo_env = envelope("image", "replicate", "wai.image.png", &label("photograph"));
let relabel_env = envelope("image", "replicate", "wai.image.png", &label("relabelled copy"));
let trained = |s: &str| json!([{"digest": hex(&label(s)), "type": "trained"}]);
let still = claim(&still_env, "image", "synthesized", "generate", "wai.generate.spectral", "trainedAlgorithmicMedia", trained("image model"), json!("fully_autonomous"), json!([]), GENERATOR);
let clip = claim(&clip_env, "video", "synthesized", "generate", "text-to-video", "trainedAlgorithmicMedia", trained("video model"), json!("prompt_guided"), json!([]), GENERATOR);
let copy_claim = claim(&copy_env, "image", "synthesized", "convert", "wai.video.int_motion", "trainedAlgorithmicMedia", json!([]), Value::Null, json!([input_of(&still)]), CODEC);
let crop_claim = claim(&crop_env, "image", "synthesized", "convert", "wai.derive.crop", "trainedAlgorithmicMedia", json!([]), Value::Null, json!([input_of(&still)]), EDITOR);
let photo = claim(&photo_env, "image", "captured", "capture", "capture", "digitalCapture", json!([]), Value::Null, json!([]), CAMERA);
let relabel = claim(&relabel_env, "image", "captured", "capture", "capture", "digitalCapture", json!([]), Value::Null, json!([]), CAMERA);
let still_m = mark::embed(&p.still, w, h, 1, &payload_of(&still)).0;
let clip_m = mark::embed(&p.clip, cw, chh, n, &payload_of(&clip)).0;
let fs = Frames::one(still_m.clone(), w, h);
let copy = apply(Attack::IntMotion(16), &fs, 0).data;
let cropped = apply(Attack::Crop(10), &fs, 0);
let wrong_m = mark::embed(&p.still, w, h, 1, &payload_of(&copy_claim)).0;
let mut hidden_m = still_m.clone();
hidden_m[(57 * w + 101) * 3 + 1] ^= 1;
World {
p,
still_m,
clip_m,
copy,
crop: cropped.data,
crop_wh: (cropped.w, cropped.h),
wrong_m,
hidden_m,
still_env,
clip_env,
copy_env,
crop_env,
photo_env,
relabel_env,
still,
clip,
copy_claim,
crop_claim,
photo,
relabel,
}
}
fn vectors(x: &World) -> Vec<Vector> {
let (w, h) = STILL;
let (cw, chh, n) = CLIP;
let sp = payload_of(&x.still);
let still_mark = marking(&x.still, sp, embedding(1, w, h, &x.p.still, &x.still_m));
let still_det = detection(&ch(&x.still_env), Some(sp), read(1, w, h, &x.still_m));
let proved = Policy { dual_layer_required: true, second_layer_proof: true, ..Default::default() };
let still_case = |m: &Value, unmarked: &[u8], decoded: &[u8]| Case {
claims: vec![text(&x.still)],
markings: vec![text(m)],
objects: vec![x.still_env.clone()],
unmarked: held(&[(&x.still_env, unmarked)]),
decoded: held(&[(&x.still_env, decoded)]),
..Default::default()
};
let rv = review(&x.still, &sha(&x.still_m));
let mut out = vec![
Vector {
name: "still_proved",
about: "a generated still (a wai.generate.spectral envelope, intent create) marked by the scheme: its marking is recomputed from the generator's unmarked frame, its detection run again agrees with its claim, and a player presents the marked frame, the reconstruction the generation is held to, with a review of it (§7.5, §7.6, §7.8)",
case: Case {
detections: vec![text(&still_det)],
reviews: vec![text(&rv)],
player: log(&x.still, &sha(&x.still_m), &rv),
policy: proved,
..still_case(&still_mark, &x.p.still, &x.still_m)
},
intent: Intent::Accept,
},
Vector {
name: "clip_proved",
about: "a generated clip of three frames, each marked with the claim's payload; the marking is recomputed and the detection, over the three frames together, run again (§7.5.4, §7.5.5)",
case: Case {
claims: vec![text(&x.clip)],
markings: vec![text(&marking(&x.clip, payload_of(&x.clip), embedding(n, cw, chh, &x.p.clip, &x.clip_m)))],
detections: vec![text(&detection(&ch(&x.clip_env), Some(payload_of(&x.clip)), read(n, cw, chh, &x.clip_m)))],
objects: vec![x.clip_env.clone()],
unmarked: held(&[(&x.clip_env, &x.p.clip)]),
decoded: held(&[(&x.clip_env, &x.clip_m)]),
policy: proved,
..Default::default()
},
intent: Intent::Accept,
},
Vector {
name: "copy_inherits",
about: "a re-encoding of the marked still (wai.video.int_motion at step 16) whose own claim is a conversion of the still's: the detector reads the still's payload from it, which its claim inherits through the conversion (§7.3 step 3)",
case: Case {
claims: vec![text(&x.still), text(&x.copy_claim)],
markings: vec![text(&still_mark)],
detections: vec![text(&detection(&ch(&x.copy_env), Some(sp), read(1, w, h, &x.copy)))],
decoded: held(&[(&x.copy_env, &x.copy)]),
..Default::default()
},
intent: Intent::Accept,
},
Vector {
name: "crop_unmarked",
about: "a crop of the marked still keeping 90% of each side, beyond what the scheme survives: run again, the detector reads no mark, and the crop's own claim, the first layer, still says synthesized (§7.5.8)",
case: Case {
claims: vec![text(&x.still), text(&x.crop_claim)],
detections: vec![text(&detection(&ch(&x.crop_env), None, read(1, x.crop_wh.0, x.crop_wh.1, &x.crop)))],
decoded: held(&[(&x.crop_env, &x.crop)]),
..Default::default()
},
intent: Intent::Accept,
},
Vector {
name: "stated_only",
about: "the still's marking and detection given without the frames: both are stated, not recomputed, under the default policy (§7.6)",
case: Case { claims: vec![text(&x.still)], markings: vec![text(&still_mark)], detections: vec![text(&still_det)], ..Default::default() },
intent: Intent::Accept,
},
];
let mut refuse = |name: &'static str, about: &'static str, case: Case, at: At, reason: Reason| out.push(Vector { name, about, case, intent: Intent::Refuse(at, reason) });
let wrong_mark = marking(&x.still, sp, embedding(1, w, h, &x.p.still, &x.wrong_m));
refuse(
"wrong_payload",
"a marking that states the still's payload over frames that carry another claim's mark (the copy's): the derivation recomputed under the stated payload is not the frames it names (§7.6)",
still_case(&wrong_mark, &x.p.still, &x.wrong_m),
At::Marking(0),
Reason::MarkRecomputeMismatch,
);
let hidden_mark = marking(&x.still, sp, embedding(1, w, h, &x.p.still, &x.hidden_m));
refuse(
"hidden_channel",
"marked frames with one more bit changed after the derivation, a channel the payload does not account for: recomputation catches it (§7.6)",
still_case(&hidden_mark, &x.p.still, &x.hidden_m),
At::Marking(0),
Reason::MarkRecomputeMismatch,
);
refuse(
"unmarked_mismatch",
"the unmarked frames held are another picture's, not the ones the marking names (§7.6)",
still_case(&still_mark, &x.p.photo, &x.still_m),
At::Marking(0),
Reason::MarkInputMismatch,
);
refuse(
"marked_samples_mismatch",
"a marking that recomputes, delivered as an object whose decoded frame has one bit more changed (§7.6)",
still_case(&still_mark, &x.p.still, &x.hidden_m),
At::Marking(0),
Reason::SamplesMismatch,
);
refuse(
"detection_false",
"a detection that claims the photograph carries its claim's mark: run again, the detector reads none from it (§7.6)",
Case {
claims: vec![text(&x.photo)],
detections: vec![text(&detection(&ch(&x.photo_env), Some(payload_of(&x.photo)), read(1, w, h, &x.p.photo)))],
decoded: held(&[(&x.photo_env, &x.p.photo)]),
..Default::default()
},
At::Detection(0),
Reason::DetectionIrreproducible,
);
refuse(
"beyond_robustness",
"a detection that reports the still's mark in the crop that keeps 90% of each side, which the scheme does not survive: run again, the detector reads no mark, and the detection is refused rather than reported as agreeing (§7.5.8, §7.6)",
Case {
claims: vec![text(&x.still), text(&x.crop_claim)],
detections: vec![text(&detection(&ch(&x.crop_env), Some(sp), read(1, x.crop_wh.0, x.crop_wh.1, &x.crop)))],
decoded: held(&[(&x.crop_env, &x.crop)]),
..Default::default()
},
At::Detection(0),
Reason::DetectionIrreproducible,
);
refuse(
"false_capture",
"a capture claim over the marked still's frames: run again, the detector reads the generator's synthesized mark, which no conversion of the capture inherits (§7.3 step 3)",
Case {
claims: vec![text(&x.still), text(&x.relabel)],
detections: vec![text(&detection(&ch(&x.relabel_env), Some(sp), read(1, w, h, &x.still_m)))],
decoded: held(&[(&x.relabel_env, &x.still_m)]),
..Default::default()
},
At::Detection(0),
Reason::LayersDisagree,
);
refuse(
"not_proved",
"the still's marking without its unmarked frame, under a policy that requires the second layer proved (§7.6, §11.1)",
Case { claims: vec![text(&x.still)], markings: vec![text(&still_mark)], policy: proved, ..Default::default() },
At::Marking(0),
Reason::LayerNotProved,
);
refuse(
"scheme_mismatch",
"a marking of the registered term under another digest (§7.4)",
Case {
claims: vec![text(&x.still)],
markings: vec![text(&resign(with(still_mark.clone(), &["scheme_digest"], json!(hex(&label("another definition")))), MARKER))],
..Default::default()
},
At::Marking(0),
Reason::SchemeMismatch,
);
refuse(
"frames_too_small",
"a marking whose frames are 100 × 100, below the scheme's least side (§7.5.1)",
Case {
claims: vec![text(&x.still)],
markings: vec![text(&resign(with(with(still_mark.clone(), &["embedding", "width"], json!(100)), &["embedding", "height"], json!(100)), MARKER))],
..Default::default()
},
At::Marking(0),
Reason::MarkEvidenceInvalid,
);
refuse(
"mark_skipped",
"a player that presents the generator's unmarked frame: the reconstruction the generation is held to is the marked frame, so the output it states is not what the object decodes to (§7.8, §9.2)",
Case {
reviews: vec![text(&rv)],
player: log(&x.still, &sha(&x.p.still), &rv),
..still_case(&still_mark, &x.p.still, &x.still_m)
},
At::Player(0),
Reason::PresentOutputIrreproducible,
);
out
}
// ---- the scheme's own vectors ----
/// Payloads of the code vectors.
fn code_payloads() -> Vec<[u8; 8]> {
(0..6u8).map(|i| {
let d = label(&format!("code payload {i}"));
let mut p = [0u8; 8];
p.copy_from_slice(&d[..8]);
p[0] = i % 4;
p
}).collect()
}
/// Received words: a codeword with `e` bits flipped at positions the stream gives.
fn flipped(c: &[u8; mark::CODE_BITS], e: usize, seed: u32) -> [u8; mark::CODE_BITS] {
let mut r = words(b"wai-mark:corpus errors", seed);
let mut out = *c;
let mut done = BTreeSet::new();
while done.len() < e {
let j = r.next().expect("an endless stream") as usize % mark::CODE_BITS;
if done.insert(j) {
out[j] ^= 1;
}
}
out
}
fn detected_json(d: Option<mark::Detected>) -> Value {
d.map_or(Value::Null, |d| json!({"payload": hex(&d.payload), "orientation": d.orientation, "errors": d.errors}))
}
// ---- the measurement ----
#[derive(Default)]
struct Tally {
trials: u64,
detected: u64,
missed: u64,
wrong: u64,
errors_max: u64,
}
/// The measurement over sets of pictures or clips: each marked with its own payload, each
/// attack applied at the seed of its index, and the detector run on the marked and on the
/// unmarked frames.
pub struct Measure {
pub pictures: Vec<Value>,
pub table: Vec<Value>,
pub unmarked: Vec<Value>,
/// The words the detector reads from each unmarked, unattacked picture, in each
/// orientation, by picture.
words: Vec<(usize, [u8; mark::CODE_BITS])>,
/// Attacks every picture of every set survived, with no unmarked detection.
pub claims: Vec<String>,
}
/// Measure the scheme over `sets`, each a name and its pictures, under `attacks`.
pub fn measure(sets: &[(&str, Vec<(String, Frames)>)], attacks: &[(&str, Attack)]) -> Measure {
let mut m = Measure { pictures: vec![], table: vec![], unmarked: vec![], words: vec![], claims: vec![] };
let mut survived: Vec<bool> = vec![true; attacks.len()];
for (set, pics) in sets {
let mut tallies: Vec<Tally> = attacks.iter().map(|_| Tally::default()).collect();
let mut fp: Vec<u64> = vec![0; attacks.len()];
for (name, f) in pics {
let picture = m.pictures.len();
if mark::admits(f.w as u64, f.h as u64, f.n as u64) {
for o in 0..mark::ORIENTATIONS {
m.words.push((picture, mark::read_word(&f.data, f.w, f.h, f.n, o)));
}
}
let payload = battery_payload(name);
let (data, reports) = mark::embed(&f.data, f.w, f.h, f.n, &payload);
let marked = Frames { data, ..f.clone() };
let s = sse(&f.data, &marked.data);
let ssim_min = (0..f.n).map(|i| ssim(f.frame(i), marked.frame(i), f.w, f.h)).fold(f64::INFINITY, f64::min);
m.pictures.push(json!({
"name": name,
"set": set,
"width": f.w,
"height": f.h,
"frames": f.n,
"payload": hex(&payload),
"sse": s,
"psnr": format!("{:.2}", psnr(s, f.data.len())),
"floor": format!("{:.2}", floor(f.w, f.h)),
"ssim": format!("{:.4}", ssim_min),
"short": reports.iter().map(|r| r.short).sum::<usize>(),
}));
for (ai, (_, a)) in attacks.iter().enumerate() {
let y = apply(*a, &marked, ai as u32);
let t = &mut tallies[ai];
t.trials += 1;
match mark::detect(&y.data, y.w, y.h, y.n) {
Some(d) if d.payload == payload => {
t.detected += 1;
t.errors_max = t.errors_max.max(d.errors as u64);
}
Some(_) => t.wrong += 1,
None => t.missed += 1,
}
let u = apply(*a, f, ai as u32);
if mark::detect(&u.data, u.w, u.h, u.n).is_some() {
fp[ai] += 1;
}
}
}
for (ai, (an, _)) in attacks.iter().enumerate() {
let t = &tallies[ai];
if t.detected != t.trials || fp[ai] != 0 {
survived[ai] = false;
}
m.table.push(json!({"attack": an, "set": set, "trials": t.trials, "detected": t.detected, "missed": t.missed, "wrong": t.wrong, "errors_max": t.errors_max}));
m.unmarked.push(json!({"attack": an, "set": set, "trials": t.trials, "detections": fp[ai]}));
}
}
m.claims = attacks.iter().zip(&survived).filter(|(_, s)| **s).map(|((n, _), _)| n.to_string()).collect();
m
}
impl Measure {
/// The words read from unmarked pictures against the uniform model of §7.5.6: their
/// count; the mean and standard deviation of their ones; and over every pair of words
/// from two different pictures, the mean, standard deviation and least of their Hamming
/// distance. Under the model both means are 124 and both deviations 7.87.
pub fn words_json(&self) -> Value {
let stats = |xs: &[u64]| -> (f64, f64) {
let n = xs.len().max(1) as f64;
let mean = xs.iter().sum::<u64>() as f64 / n;
let var = xs.iter().map(|&x| (x as f64 - mean) * (x as f64 - mean)).sum::<f64>() / n;
(mean, var.sqrt())
};
let ones: Vec<u64> = self.words.iter().map(|(_, w)| w.iter().map(|&b| b as u64).sum()).collect();
let mut dist = Vec::new();
for (i, (pa, a)) in self.words.iter().enumerate() {
for (pb, b) in &self.words[i + 1..] {
if pa != pb {
dist.push(a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() as u64);
}
}
}
let ((om, osd), (dm, dsd)) = (stats(&ones), stats(&dist));
json!({
"words": self.words.len(),
"ones_mean": format!("{om:.2}"),
"ones_sd": format!("{osd:.2}"),
"pairs": dist.len(),
"distance_mean": format!("{dm:.2}"),
"distance_sd": format!("{dsd:.2}"),
"distance_min": dist.iter().min().copied().unwrap_or(0),
"model_mean": "124.00",
"model_sd": "7.87",
})
}
}
/// The false-positive bound of §7.5.6, as the base-2 logarithm of
/// `8 · 2^58 · V(248, 29) / 2^248`, `V(n, t) = Σ_{i ≤ t} C(n, i)` the volume of a Hamming
/// ball, to two decimals.
pub fn false_positive_log2() -> String {
let (n, mut term, mut v) = (mark::CODE_BITS as f64, 1.0f64, 0.0f64);
for i in 0..=mark::CORRECTS {
v += term;
term = term * (n - i as f64) / (i as f64 + 1.0);
}
format!("{:.2}", 3.0 + 58.0 + v.log2() - n)
}
// ---- the files ----
fn frame_name(b: &[u8]) -> String {
format!("frames/{}.rgb", hex(&sha(b)))
}
fn case_json(c: &Case, frames: &mut BTreeMap<String, Vec<u8>>) -> Map<String, Value> {
let mut put = |b: &Vec<u8>| -> String {
frames.insert(frame_name(b), b.clone());
hex(&sha(b))
};
let mut m = Map::new();
m.insert(
"policy".into(),
json!({
"dual_layer": if c.policy.dual_layer_required { "required" } else { "when-given" },
"assistive": if c.policy.require_proof { "require-proof" } else { "accept-asserted" },
"second_layer": if c.policy.second_layer_proof { "require-proof" } else { "accept-stated" },
}),
);
for (k, v) in [("claims", &c.claims), ("markings", &c.markings), ("detections", &c.detections), ("reviews", &c.reviews), ("player", &c.player)] {
m.insert(k.into(), json!(v));
}
m.insert("label_policy".into(), json!(c.label_policy));
m.insert("objects".into(), json!(c.objects.iter().map(|b| hex(b)).collect::<Vec<_>>()));
m.insert("decoded".into(), json!(c.decoded.iter().map(|(h, b)| json!({"object": hex(h), "frames": put(b)})).collect::<Vec<_>>()));
m.insert("unmarked".into(), json!(c.unmarked.iter().map(|(h, b)| json!({"object": hex(h), "frames": put(b)})).collect::<Vec<_>>()));
m
}
fn case_from_json(v: &Value, frames: &BTreeMap<String, Vec<u8>>) -> Option<Case> {
let strs = |k: &str| -> Option<Vec<String>> { v[k].as_array()?.iter().map(|x| x.as_str().map(String::from)).collect() };
let bufs = |k: &str| -> Option<Vec<([u8; 32], Vec<u8>)>> {
v[k].as_array()?
.iter()
.map(|d| Some((unhex(d["object"].as_str()?)?.try_into().ok()?, frames.get(&format!("frames/{}.rgb", d["frames"].as_str()?))?.clone())))
.collect()
};
Some(Case {
claims: strs("claims")?,
markings: strs("markings")?,
detections: strs("detections")?,
reviews: strs("reviews")?,
label_policy: v["label_policy"].as_str().map(String::from),
player: strs("player")?,
objects: v["objects"].as_array()?.iter().map(|x| unhex(x.as_str()?)).collect::<Option<_>>()?,
decoded: bufs("decoded")?,
unmarked: bufs("unmarked")?,
room: None,
policy: Policy {
dual_layer_required: v["policy"]["dual_layer"] == "required",
require_proof: v["policy"]["assistive"] == "require-proof",
second_layer_proof: v["policy"]["second_layer"] == "require-proof",
},
})
}
/// The battery's sample a second implementation runs again: every picture attack of the
/// battery applied to the corpus's marked and unmarked still, and what the detector reads.
fn battery_sample(x: &World, frames: &mut BTreeMap<String, Vec<u8>>) -> Vec<Value> {
let (w, h) = STILL;
let mut out = Vec::new();
for (which, data) in [("marked", &x.still_m), ("unmarked", &x.p.still)] {
frames.insert(frame_name(data), data.clone());
let f = Frames::one(data.clone(), w, h);
for (ai, (an, a)) in IMAGE_ATTACKS.iter().enumerate() {
let y = apply(*a, &f, ai as u32);
out.push(json!({
"frames": hex(&sha(data)),
"which": which,
"attack": an,
"seed": ai,
"width": y.w,
"height": y.h,
"result_samples": hex(&sha(&y.data)),
"detected": detected_json(mark::detect(&y.data, y.w, y.h, y.n)),
}));
}
}
out
}
/// The corpus: `scheme.json`, `vectors.json`, `battery.json` and the frames, as named files
/// and their bytes.
pub fn generate() -> Vec<(String, Vec<u8>)> {
let x = world();
let mut frames: BTreeMap<String, Vec<u8>> = BTreeMap::new();
let lay = mark::layout();
// The code.
let mut codewords = Vec::new();
let mut decodes = Vec::new();
for (i, p) in code_payloads().iter().enumerate() {
let c = mark::encode(p);
codewords.push(json!({"payload": hex(p), "codeword": bits(&c)}));
for e in [0usize, 1, 29, 30, 40] {
let word = flipped(&c, e, (i * 64 + e) as u32);
let got = mark::decode(&word);
decodes.push(json!({"word": bits(&word), "flipped": e, "result": got.map(|(p, n)| json!({"payload": hex(&p), "errors": n}))}));
}
}
decodes.push(json!({"word": bits(&lay.mask), "flipped": null, "result": null}));
// The embeddings.
let (w, h) = STILL;
let (cw, chh, n) = CLIP;
let mut embeds = Vec::new();
for (name, about, unmarked, fw, fh, fn_, payload) in [
("still", "the corpus's spectral still under its claim's payload", &x.p.still, w, h, 1, payload_of(&x.still)),
("photo", "the dead-leaves stand-in for a photograph under a payload of class captured", &x.p.photo, w, h, 1, payload_of(&x.photo)),
("clip", "the corpus's clip, three frames, under its claim's payload", &x.p.clip, cw, chh, n, payload_of(&x.clip)),
] {
let (marked, reports) = mark::embed(unmarked, fw, fh, fn_, &payload);
frames.insert(frame_name(unmarked), unmarked.clone());
frames.insert(frame_name(&marked), marked.clone());
embeds.push(json!({
"name": name,
"about": about,
"frames": fn_,
"width": fw,
"height": fh,
"unmarked": hex(&sha(unmarked)),
"payload": hex(&payload),
"marked": hex(&sha(&marked)),
"amplitudes": reports.iter().map(|r| r.amplitudes.iter().map(|a| format!("{a:x}")).collect::<String>()).collect::<Vec<_>>(),
"short": reports.iter().map(|r| r.short).collect::<Vec<_>>(),
"sse": sse(unmarked, &marked),
}));
}
// The detections: each stored frame set as it is, and every orientation of the still.
let mut detects = Vec::new();
let stored: Vec<(&str, &Vec<u8>, usize, usize, usize)> = vec![
("still marked", &x.still_m, w, h, 1),
("still unmarked", &x.p.still, w, h, 1),
("clip marked", &x.clip_m, cw, chh, n),
("clip unmarked", &x.p.clip, cw, chh, n),
("photo unmarked", &x.p.photo, w, h, 1),
("copy at step 16", &x.copy, w, h, 1),
("crop keeping 90%", &x.crop, x.crop_wh.0, x.crop_wh.1, 1),
("still under the copy's payload", &x.wrong_m, w, h, 1),
];
for (name, data, fw, fh, fn_) in stored {
frames.insert(frame_name(data), data.clone());
detects.push(json!({"name": name, "frames": fn_, "width": fw, "height": fh, "samples": hex(&sha(data)), "detected": detected_json(mark::detect(data, fw, fh, fn_))}));
}
// The cases.
let vs = vectors(&x);
let mut seen = BTreeSet::new();
let mut cases = Vec::new();
for v in &vs {
let got = crate::synthesis::verify(&v.case);
match (&v.intent, &got) {
(Intent::Accept, Outcome::Accept(_)) => {}
(Intent::Refuse(at, r), Outcome::Refuse { at: a, reason }) if at == a && r == reason => {
seen.insert(*r);
}
_ => panic!("case {} does not reach the outcome it was written to have: {got:?}", v.name),
}
let mut m = Map::new();
m.insert("name".into(), json!(v.name));
m.insert("about".into(), json!(v.about));
m.extend(case_json(&v.case, &mut frames));
m.extend(outcome_json(&got));
cases.push(Value::Object(m));
}
for r in MARK_REASONS {
assert!(seen.contains(&r), "no case of the corpus is refused with {r}");
}
let sample = battery_sample(&x, &mut frames);
let doc = json!({
"about": "the conformance corpus of the registered watermark scheme of extensions/synthesis.md §7.5, written by wai_synthesis_mark_vectors gen",
"scheme": {
"term": mark::SCHEME,
"digest": hex(&mark::digest()),
"definition": "scheme.json",
"generator": mark::GENERATOR_HEX,
"shuffle_first": lay.perm[..16].to_vec(),
"shuffle_last": lay.perm[mark::CELLS - 16..].to_vec(),
"signs_first": lay.sign[..16].to_vec(),
"mask": bits(&lay.mask),
"false_positive_log2": false_positive_log2(),
},
"codewords": codewords,
"decodes": decodes,
"embeds": embeds,
"detects": detects,
"battery_sample": sample,
"cases": cases,
});
// The measurement.
let images = measure(&[("spectral", spectral_set()), ("leaves", leaves_set())], &IMAGE_ATTACKS);
let video = measure(&[("clips", clips())], &CLIP_ATTACKS);
let battery = json!({
"about": "the measurement of extensions/synthesis.md §7.5.7, written by wai_synthesis_mark_vectors gen",
"sets": [
{"name": "spectral", "pictures": SET_SIZE, "width": SET_SIDE, "height": SET_SIDE, "source": "wai.generate.spectral's generator with its committed model, the prompts of synthesis_mark::PROMPTS, seed the index, 4 steps"},
{"name": "leaves", "pictures": SET_SIZE, "width": SET_SIDE, "height": SET_SIDE, "source": "synthesis_mark::leaves, seeds 0 to 15"},
{"name": "clips", "pictures": 4, "width": CLIP_SIDE, "height": CLIP_SIDE, "frames": CLIP_FRAMES, "source": "synthesis_mark::clips: a window panning over two dead-leaves and two spectral pictures"},
],
"image_attacks": IMAGE_ATTACKS.iter().map(|(n, _)| n).collect::<Vec<_>>(),
"clip_attacks": CLIP_ATTACKS.iter().map(|(n, _)| n).collect::<Vec<_>>(),
"pictures": images.pictures.iter().chain(&video.pictures).collect::<Vec<_>>(),
"table": images.table.iter().chain(&video.table).collect::<Vec<_>>(),
"unmarked": images.unmarked.iter().chain(&video.unmarked).collect::<Vec<_>>(),
"words": images.words_json(),
"false_positive_log2": false_positive_log2(),
"claims": {"pictures": images.claims, "clips": video.claims},
});
let mut files: Vec<(String, Vec<u8>)> = vec![
("scheme.json".into(), mark::definition_text().into_bytes()),
("vectors.json".into(), (serde_json::to_string_pretty(&doc).expect("JSON") + "\n").into_bytes()),
("battery.json".into(), (serde_json::to_string_pretty(&battery).expect("JSON") + "\n").into_bytes()),
];
files.extend(frames);
files
}
/// Check a corpus's files: every case gets the outcome it states, every embedding
/// recomputes, every detection reads what it states, every word decodes as stated.
pub fn verify(files: &BTreeMap<String, Vec<u8>>) -> Vec<String> {
let mut fails = Vec::new();
let Some(doc) = files.get("vectors.json").and_then(|s| serde_json::from_slice::<Value>(s).ok()) else {
return vec!["vectors.json does not read".into()];
};
let frames: BTreeMap<String, Vec<u8>> = files.iter().filter(|(k, _)| k.starts_with("frames/")).map(|(k, v)| (k.clone(), v.clone())).collect();
let get = |h: &Value| frames.get(&format!("frames/{}.rgb", h.as_str().unwrap_or_default())).cloned();
if files.get("scheme.json").map(|b| sha(b)) != Some(mark::digest()) {
fails.push("scheme.json is not the definition whose digest the scheme has".into());
}
for e in doc["embeds"].as_array().into_iter().flatten() {
let (Some(u), Some(p)) = (get(&e["unmarked"]), e["payload"].as_str().and_then(unhex)) else {
fails.push(format!("embed {}: does not read", e["name"]));
continue;
};
let dims = |k: &str| e[k].as_u64().unwrap_or(0) as usize;
let marked = mark::embed(&u, dims("width"), dims("height"), dims("frames"), &p.try_into().unwrap_or([0; 8])).0;
if hex(&sha(&marked)) != e["marked"].as_str().unwrap_or_default() {
fails.push(format!("embed {}: the marked frames differ", e["name"]));
}
}
for d in doc["detects"].as_array().into_iter().flatten() {
let Some(s) = get(&d["samples"]) else {
fails.push(format!("detect {}: no frames", d["name"]));
continue;
};
let dims = |k: &str| d[k].as_u64().unwrap_or(0) as usize;
if detected_json(mark::detect(&s, dims("width"), dims("height"), dims("frames"))) != d["detected"] {
fails.push(format!("detect {}: reads otherwise", d["name"]));
}
}
for v in doc["cases"].as_array().into_iter().flatten() {
let name = v["name"].as_str().unwrap_or("?");
let Some(c) = case_from_json(v, &frames) else {
fails.push(format!("{name}: the case does not read"));
continue;
};
let got = Value::Object(outcome_json(&crate::synthesis::verify(&c)));
let mut want = Map::new();
for k in ["outcome", "reason", "at", "statements"] {
if let Some(x) = v.get(k) {
want.insert(k.into(), x.clone());
}
}
if got != Value::Object(want) {
fails.push(format!("{name}: {got}"));
}
}
fails
}

View file

@ -0,0 +1,136 @@
//! The registered watermark scheme of extensions/synthesis.md §7.5 and its corpus
//! `live-conformance/synthesis-mark/`: every file regenerates byte for byte from its fixed seeds
//! (the measurement among them) and no other file is there; every embedding, detection and case
//! gets what it states; the extension's definition, generator polynomial and shuffle are the
//! reference's; and §7.5.8 claims only attacks the measurement shows survived, with §7.5.6's
//! figures the measurement's. Run it in release: the measurement embeds and attacks every
//! picture of the measurement sets.
#![cfg(feature = "synthesis_mark")]
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::PathBuf;
use wai::synthesis::mark;
use wai::synthesis_mark::corpus;
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn committed() -> BTreeMap<String, Vec<u8>> {
let root = wai_dir().join("live-conformance/synthesis-mark");
let mut out = BTreeMap::new();
let mut stack = vec![root.clone()];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).expect("the committed corpus").flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
continue;
}
let rel = p.strip_prefix(&root).expect("under the corpus").to_string_lossy().replace('\\', "/");
if rel != "README.md" {
out.insert(rel, std::fs::read(&p).expect("a file"));
}
}
}
out
}
fn extension() -> String {
std::fs::read_to_string(wai_dir().join("extensions/synthesis.md")).expect("the extension")
}
/// The text of the section that starts at `head`, up to the next heading of its level or above.
fn section(text: &str, head: &str) -> String {
let at = text.find(&format!("\n{head}")).unwrap_or_else(|| panic!("a section {head}"));
let level = head.chars().take_while(|c| *c == '#').count();
let end = text[at + 1..]
.match_indices("\n#")
.find(|(i, _)| text[at + 1 + i + 1..].chars().take_while(|c| *c == '#').count() <= level)
.map_or(text.len(), |(i, _)| at + 1 + i);
text[at..end].to_string()
}
fn fold(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
let files = committed();
let generated: BTreeMap<String, Vec<u8>> = corpus::generate().into_iter().collect();
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
let differ: Vec<&String> = generated.keys().filter(|k| files.get(*k) != generated.get(*k)).collect();
assert!(extra.is_empty() && differ.is_empty(), "extra {extra:?}, differ or missing {differ:?}: run `wai_synthesis_mark_vectors gen`");
}
#[test]
fn every_vector_and_case_gets_what_it_states() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// §7.4's definition is the reference's, with its digest; §7.5.3's generator polynomial and
/// the shuffle's first entries are the reference's.
#[test]
fn the_extension_pins_the_scheme() {
let text = extension();
let s74 = section(&text, "### 7.4 ");
let block = s74.split("```").nth(1).expect("§7.4's definition");
let def: String = block.lines().map(str::trim).collect();
assert_eq!(def, mark::definition_text(), "§7.4's definition");
let digest: String = mark::digest().iter().map(|b| format!("{b:02x}")).collect();
assert!(fold(&s74).contains(&format!("its digest is `{digest}`")), "§7.4's digest");
let s753 = fold(&section(&text, "#### 7.5.3 "));
assert!(s753.contains(&format!("in hexadecimal, are `{}`", mark::GENERATOR_HEX)), "§7.5.3's generator");
let p = &mark::layout().perm;
assert!(s753.contains(&format!("π begins {}, {}, {}, {},", p[0], p[1], p[2], p[3])), "§7.5.3's shuffle");
}
/// §7.5.8 claims only attacks every set survived with no unmarked reading; §7.5.6's figures are
/// battery.json's.
#[test]
fn the_extension_claims_only_what_the_measurement_shows() {
let text = extension();
let battery: Value = serde_json::from_slice(&committed()["battery.json"]).expect("battery.json");
let s758 = fold(&section(&text, "#### 7.5.8 "));
let at = s758.find("For pictures, these attacks, each with the parameter measured: ").expect("§7.5.8's claims");
let rest = &s758[at..];
let clips_at = rest.find(" For clips: ").expect("the clips' claims");
let names = |s: &str| -> Vec<String> { s.split('`').skip(1).step_by(2).map(String::from).collect() };
let pictures = names(&rest[..clips_at]);
let clips = names(&rest[clips_at..rest[clips_at..].find(". ").map_or(rest.len(), |e| clips_at + e)]);
let has = |kind: &str, a: &String| battery["claims"][kind].as_array().expect("claims").iter().any(|x| x == a);
assert!(!pictures.is_empty() && pictures.iter().all(|a| has("pictures", a)), "§7.5.8 claims {pictures:?}");
assert!(!clips.is_empty() && clips.iter().all(|a| has("clips", a)), "§7.5.8 claims {clips:?}");
let w = &battery["words"];
let s756 = fold(&section(&text, "#### 7.5.6 "));
let fp = battery["false_positive_log2"].as_str().expect("a bound").trim_start_matches('-').to_string();
assert!(s756.contains(&format!("2^−{fp}, about")), "§7.5.6's bound");
let pairs = w["pairs"].as_u64().expect("pairs");
let words = format!(
"have a mean of {} ones with a standard deviation of {}, and the {},{:03} pairs of those words from different pictures differ in a mean of {} bits with a standard deviation of {} and in no fewer than {};",
w["ones_mean"].as_str().unwrap(),
w["ones_sd"].as_str().unwrap(),
pairs / 1000,
pairs % 1000,
w["distance_mean"].as_str().unwrap(),
w["distance_sd"].as_str().unwrap(),
w["distance_min"]
);
assert!(s756.contains(&words), "§7.5.6's figures: {words}");
}
#[test]
fn the_corpus_holds_only_what_its_vectors_name() {
let files = committed();
let doc: Value = serde_json::from_slice(&files["vectors.json"]).expect("vectors.json");
let text = String::from_utf8_lossy(&files["vectors.json"]).to_string();
for k in files.keys().filter(|k| k.starts_with("frames/")) {
let h = &k["frames/".len()..k.len() - ".rgb".len()];
assert!(text.contains(h), "{k} is named by no vector");
assert_eq!(&mark::sha256(&files[k]).iter().map(|b| format!("{b:02x}")).collect::<String>(), h, "{k} is not named by its SHA-256");
}
assert!(doc["cases"].as_array().is_some_and(|c| !c.is_empty()));
}