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
36 changed files with 9318 additions and 136 deletions
26
.github/workflows/wai-conformance.yml
vendored
26
.github/workflows/wai-conformance.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
15
wai/SPEC.md
15
wai/SPEC.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
500
wai/live-conformance/_wai_mark.py
Normal file
500
wai/live-conformance/_wai_mark.py
Normal 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)
|
||||
83
wai/live-conformance/synthesis-mark/README.md
Normal file
83
wai/live-conformance/synthesis-mark/README.md
Normal 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 .
|
||||
```
|
||||
1690
wai/live-conformance/synthesis-mark/battery.json
Normal file
1690
wai/live-conformance/synthesis-mark/battery.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
wai/live-conformance/synthesis-mark/scheme.json
Normal file
1
wai/live-conformance/synthesis-mark/scheme.json
Normal 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"}
|
||||
1918
wai/live-conformance/synthesis-mark/vectors.json
Normal file
1918
wai/live-conformance/synthesis-mark/vectors.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
403
wai/live-conformance/synthesis_mark_verify.py
Normal file
403
wai/live-conformance/synthesis_mark_verify.py
Normal 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]))
|
||||
|
|
@ -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 ""))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
146
wai/wai-rs/src/bin/wai_synthesis_mark_vectors.rs
Normal file
146
wai/wai-rs/src/bin/wai_synthesis_mark_vectors.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
724
wai/wai-rs/src/synthesis/mark.rs
Normal file
724
wai/wai-rs/src/synthesis/mark.rs
Normal 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,"));
|
||||
}
|
||||
}
|
||||
222
wai/wai-rs/src/synthesis_mark.rs
Normal file
222
wai/wai-rs/src/synthesis_mark.rs
Normal 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
|
||||
}
|
||||
352
wai/wai-rs/src/synthesis_mark/battery.rs
Normal file
352
wai/wai-rs/src/synthesis_mark/battery.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
963
wai/wai-rs/src/synthesis_mark/corpus.rs
Normal file
963
wai/wai-rs/src/synthesis_mark/corpus.rs
Normal 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(©_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(©_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
|
||||
}
|
||||
136
wai/wai-rs/tests/synthesis_mark.rs
Normal file
136
wai/wai-rs/tests/synthesis_mark.rs
Normal 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(§ion(&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(§ion(&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(§ion(&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()));
|
||||
}
|
||||
Loading…
Reference in a new issue