wai upscale and wai tonemap: an exact derivation from the command line, with its record and its check #578
17 changed files with 1002 additions and 6 deletions
2
.github/workflows/wai-catalogue.yml
vendored
2
.github/workflows/wai-catalogue.yml
vendored
|
|
@ -139,7 +139,7 @@ jobs:
|
|||
ran "$RUNNER_TEMP/lib.log"
|
||||
cargo test --locked --no-default-features --features quickstart --bin wai --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli.log"
|
||||
ran "$RUNNER_TEMP/cli.log"
|
||||
cargo test --locked --no-default-features --features quickstart,generate,score,staged,video_int,audio_int,splat_int --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-full.log"
|
||||
cargo test --locked --no-default-features --features quickstart,generate,score,staged,video_int,audio_int,splat_int,exact_upscale,exact_tonemap --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-full.log"
|
||||
ran "$RUNNER_TEMP/cli-full.log"
|
||||
cargo test --locked --no-default-features --features quickstart,app --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-app.log"
|
||||
ran "$RUNNER_TEMP/cli-app.log"
|
||||
|
|
|
|||
3
.github/workflows/wai-conformance.yml
vendored
3
.github/workflows/wai-conformance.yml
vendored
|
|
@ -1271,7 +1271,7 @@ jobs:
|
|||
--test live_events --test live_room --test capture_take --test live_compose --test live_record \
|
||||
--test timed_text --test access_services --test live_metering --test energy_steering \
|
||||
--test live_moderation --test live_ads --test ad_presentation --test live_provenance \
|
||||
--test synthesis --test exact_upscale --test ai_usage --test contribution --test settlement \
|
||||
--test synthesis --test exact_upscale --test cli_derive --test ai_usage --test contribution --test settlement \
|
||||
--test browser_sink --test takedown --test influence --test direction --test co_performance \
|
||||
--test age_assurance --test bearer_receipts --test exploration --test co_watch \
|
||||
--test telemetry_aggregate --test exact_tonemap --test splat_exact --test live_watermark \
|
||||
|
|
@ -1847,6 +1847,7 @@ jobs:
|
|||
(cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --lib int_sr::)
|
||||
(cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --lib exact_upscale::)
|
||||
(cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --test exact_upscale)
|
||||
(cd wai-rs && cargo test --no-default-features --features "$WAI_FEATURES" --test cli_derive)
|
||||
python3 -I live-conformance/upscale_verify.py live-conformance/upscale
|
||||
# Robustness evidence for soft bindings (extensions/robustness-evidence.md): the
|
||||
# reference's unit tests (gamma is the rounded power, rates are exact and reduced, every
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ wire format may change and such changes are recorded here.
|
|||
never reached: it does not call `extract`), and `elliptic-curve` 0.14, which leaves no `crypto-bigint` 0.5 in this
|
||||
workspace. A caller that passes its own `c2pa::Signer` builds against 0.91.
|
||||
|
||||
### `wai upscale` and `wai tonemap`: an exact derivation from the command line, with its record and its check
|
||||
|
||||
- **`wai upscale run`** (`media.upscale`) upscales an 8-bit RGB PNG under a prior, and with `--record` signs the record
|
||||
and writes the input's envelope it names. **`wai upscale verify`** (`upscale.verify`) recomputes the record from them
|
||||
under `require-proof`, and holds the picture to it.
|
||||
- **`wai tonemap run`** (`media.tonemap`) renders a still's gain map at the headrooms named, and with `--record` signs
|
||||
the record. **`wai tonemap verify`** (`tonemap.verify`) recomputes it from its envelope, and holds the renditions to it.
|
||||
- The library functions under them, `exact_upscale::make` and `exact_tonemap::make`, return the record's body for a
|
||||
caller to sign. `exact_upscale` now also needs `png_still` and `receipt_claims`. The catalogue's verbs gain `upscale`
|
||||
and `tonemap`.
|
||||
|
||||
### The route record states what a hop delivered by its count and root, so a hop may deliver any number of objects
|
||||
|
||||
- **Route record version 3** ([extensions/route-record.md](extensions/route-record.md) §3.4, §5.2, §6, §6.1), on a
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ An operation is named `noun.verb`: lowercase ASCII, a singular noun, and a verb
|
|||
rule N3's closed list (create, list, show, update, delete, check, verify, run, build, export,
|
||||
import, start, stop, join, leave, grant, invite, revoke, rotate, tail, search, test, load, encode,
|
||||
decode, inspect, wrap, unwrap, fetch, generate, promote, rollback, lock, upgrade, setup, guide,
|
||||
stream), never a near-synonym (`get`, `describe` and `info` are `show`; `keygen`, `init` and
|
||||
stream, upscale, tonemap), never a near-synonym (`get`, `describe` and `info` are `show`; `keygen`, `init` and
|
||||
`new` are `create`). A command a person already types keeps its spelling as the operation's
|
||||
command line: `wai hello` is `toolchain.test`, `wai stage refine` is `stage.encode`. Its entry
|
||||
(format `wai.ops/1`) carries:
|
||||
|
|
|
|||
|
|
@ -1000,6 +1000,392 @@
|
|||
"sdk": "wai.media.generate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "media.upscale",
|
||||
"summary": "Upscale a PNG exactly, by an integer network under a pinned prior, with a signed record a verifier recomputes",
|
||||
"cli": "wai upscale run",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai upscale run <in.png> --prior <p.wsr> [--derivation sr2|sr4] -o <out.png>",
|
||||
" [--record <out.json> --envelope <in.wai> --key <seed>] [--issued-at <date-time>] [--json]",
|
||||
" an 8-bit RGB PNG; --record signs what exact-upscale §7 recomputes from --envelope and the prior"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "writes-project",
|
||||
"dry_run": true,
|
||||
"confirm": "when-replacing-an-unnamed-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "stable",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "An 8-bit RGB PNG.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"prior": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The WSR1 prior.",
|
||||
"x-cli": {
|
||||
"flag": "--prior"
|
||||
}
|
||||
},
|
||||
"derivation": {
|
||||
"type": "string",
|
||||
"description": "sr2 (the default) or sr4.",
|
||||
"x-cli": {
|
||||
"flag": "--derivation"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The picture, a PNG.",
|
||||
"x-cli": {
|
||||
"flag": "-o"
|
||||
}
|
||||
},
|
||||
"record": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The signed upscale record.",
|
||||
"x-cli": {
|
||||
"flag": "--record"
|
||||
}
|
||||
},
|
||||
"envelope": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The WAI1 of the input, which the record names.",
|
||||
"x-cli": {
|
||||
"flag": "--envelope"
|
||||
}
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The record's signing seed: 64 hexadecimal digits, or a file holding them.",
|
||||
"x-cli": {
|
||||
"flag": "--key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"prior",
|
||||
"out"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"width",
|
||||
"height",
|
||||
"subpixels",
|
||||
"files"
|
||||
],
|
||||
"description": "What `wai upscale run` or `wai tonemap run` made: the subpixels it would bill, each file it wrote with its digest, and the record's link hash"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": true,
|
||||
"needs": {
|
||||
"all": [
|
||||
"exact_upscale"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai media upscale",
|
||||
"mcp": null,
|
||||
"c": "wai_media_upscale",
|
||||
"rust": "wai::media::upscale",
|
||||
"sdk": "wai.media.upscale"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "upscale.verify",
|
||||
"summary": "An upscale record recomputed from its input and its prior, and the picture held to it",
|
||||
"cli": "wai upscale verify",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai upscale verify <record.json> --envelope <in.wai> --prior <p.wsr> [--picture <out.png>]",
|
||||
" the record's outcome as JSON (extensions/exact-upscale.md §7, under require-proof); exit 1 when it is refused"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "reads",
|
||||
"dry_run": false,
|
||||
"confirm": "never",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "stable",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"record": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The upscale record.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"envelope": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The WAI1 of the input, which the record names.",
|
||||
"x-cli": {
|
||||
"flag": "--envelope"
|
||||
}
|
||||
},
|
||||
"prior": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The WSR1 prior the record names.",
|
||||
"x-cli": {
|
||||
"flag": "--prior"
|
||||
}
|
||||
},
|
||||
"picture": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The picture: its samples are held to the record's output.",
|
||||
"x-cli": {
|
||||
"flag": "--picture"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"record",
|
||||
"envelope",
|
||||
"prior"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"outcome"
|
||||
],
|
||||
"description": "accept, with the record's link hash and the subpixels it states; or refuse, with the reason and where"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": false,
|
||||
"needs": {
|
||||
"all": [
|
||||
"exact_upscale"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai upscale verify",
|
||||
"mcp": null,
|
||||
"c": "wai_upscale_verify",
|
||||
"rust": "wai::upscale::verify",
|
||||
"sdk": "wai.upscale.verify"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "media.tonemap",
|
||||
"summary": "Render a still's gain map exactly at the headrooms named, with a signed record a verifier recomputes",
|
||||
"cli": "wai tonemap run",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai tonemap run <in.wai> --headrooms <h,h,...> --out <dir> [--input-class <class>]",
|
||||
" [--record <out.json> --key <seed>] [--issued-at <date-time>] [--json]",
|
||||
" a WAI2 of a PNG base and its gain map; a headroom is in 1/10000 of a stop; each rendition is RGB16"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "writes-project",
|
||||
"dry_run": true,
|
||||
"confirm": "when-replacing-an-unnamed-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "stable",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "A WAI2 of a PNG base and its gain map.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"headrooms": {
|
||||
"type": "string",
|
||||
"description": "The headrooms, in 1/10000 of a stop, joined by commas.",
|
||||
"x-cli": {
|
||||
"flag": "--headrooms"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The directory the renditions are written in.",
|
||||
"x-cli": {
|
||||
"flag": "--out"
|
||||
}
|
||||
},
|
||||
"input_class": {
|
||||
"type": "string",
|
||||
"description": "The input's class (captured).",
|
||||
"x-cli": {
|
||||
"flag": "--input-class"
|
||||
}
|
||||
},
|
||||
"record": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The signed rendition record.",
|
||||
"x-cli": {
|
||||
"flag": "--record"
|
||||
}
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The record's signing seed: 64 hexadecimal digits, or a file holding them.",
|
||||
"x-cli": {
|
||||
"flag": "--key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"headrooms",
|
||||
"out"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"width",
|
||||
"height",
|
||||
"subpixels",
|
||||
"files"
|
||||
],
|
||||
"description": "What `wai upscale run` or `wai tonemap run` made: the subpixels it would bill, each file it wrote with its digest, and the record's link hash"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": true,
|
||||
"needs": {
|
||||
"all": [
|
||||
"exact_tonemap"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai media tonemap",
|
||||
"mcp": null,
|
||||
"c": "wai_media_tonemap",
|
||||
"rust": "wai::media::tonemap",
|
||||
"sdk": "wai.media.tonemap"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tonemap.verify",
|
||||
"summary": "A rendition record recomputed from its envelope, and the renditions held to it",
|
||||
"cli": "wai tonemap verify",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai tonemap verify <record.json> --envelope <in.wai> [--renditions <dir>]",
|
||||
" the record's outcome as JSON (extensions/exact-tonemap.md §8, under require-proof); exit 1 when it is refused"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "reads",
|
||||
"dry_run": false,
|
||||
"confirm": "never",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "stable",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"record": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The rendition record.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"envelope": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The WAI2 the record names.",
|
||||
"x-cli": {
|
||||
"flag": "--envelope"
|
||||
}
|
||||
},
|
||||
"renditions": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The directory `wai tonemap run` wrote: each rendition is held to its picture.",
|
||||
"x-cli": {
|
||||
"flag": "--renditions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"record",
|
||||
"envelope"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"outcome"
|
||||
],
|
||||
"description": "accept, with the record's link hash and the subpixels it states; or refuse, with the reason and where"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": false,
|
||||
"needs": {
|
||||
"all": [
|
||||
"exact_tonemap"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai tonemap verify",
|
||||
"mcp": null,
|
||||
"c": "wai_tonemap_verify",
|
||||
"rust": "wai::tonemap::verify",
|
||||
"sdk": "wai.tonemap.verify"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "score.build",
|
||||
"summary": "A WSCR score from integer note fields",
|
||||
|
|
|
|||
|
|
@ -601,6 +601,9 @@ for the break (`companion-only-as-capability`, `companion-only-as-rendition`,
|
|||
|
||||
## 10. Conformance
|
||||
|
||||
`wai tonemap run` renders a still's gain map and writes its record, and `wai tonemap verify` checks
|
||||
them under `require-proof`.
|
||||
|
||||
[`live-conformance/tonemap/`](../live-conformance/tonemap/) holds the corpus, written by
|
||||
`wai_tonemap_vectors gen` from fixed seeds and from the integer video corpus: `vectors.json`
|
||||
(with the arithmetic of §3 and §5.4 at stated points and over whole ranges: `L2`, `E2`, the
|
||||
|
|
|
|||
|
|
@ -427,6 +427,9 @@ states an output its derivation does not give.
|
|||
|
||||
## 9. Conformance
|
||||
|
||||
`wai upscale run` makes a picture and its record, and `wai upscale verify` checks them under
|
||||
`require-proof`.
|
||||
|
||||
[`live-conformance/upscale/`](../live-conformance/upscale/) holds the corpus: `vectors.json`,
|
||||
written from fixed seeds by `wai_upscale_vectors gen`, the priors it reads in `priors/`, each
|
||||
pinned by its SHA-256 in the generator, and `sha256.json`, the digest of every derivation's
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ synthesis_mark = ["synthesis", "video_int", "generate", "png_still"]
|
|||
# frames of a span that were transmitted, derived exactly, edited by an unprovable step or
|
||||
# generated, each held to its origin claim (synthesis). Its claims are synthesis's, its residuals
|
||||
# staged's. SHA-256, BLAKE3 and Ed25519 only; no new dependency.
|
||||
exact_upscale = ["synthesis", "staged"]
|
||||
exact_upscale = ["synthesis", "staged", "png_still", "receipt_claims"]
|
||||
# Deep redundancy for Opus decoded exactly (extensions/opus-dred): `wai.audio.opus.dred.int` — the
|
||||
# DRED extension of draft-ietf-mlcodec-opus-dred-07 found in an Opus packet's padding
|
||||
# (draft-ietf-mlcodec-opus-extension-06's iterator, RFC 6716's framing and range decoder), its
|
||||
|
|
@ -1783,6 +1783,13 @@ required-features = ["generate"]
|
|||
|
||||
# `wai generate` and `wai score build|render` run as a user runs them, against the
|
||||
# committed generate vectors and score-conformance/.
|
||||
# `wai upscale` and `wai tonemap`, end to end: what they write is what each extension's verifier
|
||||
# proves.
|
||||
[[test]]
|
||||
name = "cli_derive"
|
||||
path = "tests/cli_derive.rs"
|
||||
required-features = ["exact_upscale", "exact_tonemap"]
|
||||
|
||||
[[test]]
|
||||
name = "cli_generate_score"
|
||||
path = "tests/cli_generate_score.rs"
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ mod generate;
|
|||
#[path = "wai_cli/score.rs"]
|
||||
mod score;
|
||||
|
||||
#[cfg(any(feature = "exact_upscale", feature = "exact_tonemap"))]
|
||||
#[path = "wai_cli/derive.rs"]
|
||||
mod derive;
|
||||
|
||||
#[cfg(feature = "platform_encode")]
|
||||
#[path = "wai_cli/encode.rs"]
|
||||
mod platform_encode_cli;
|
||||
|
|
|
|||
205
wai/wai-rs/src/bin/wai_cli/derive.rs
Normal file
205
wai/wai-rs/src/bin/wai_cli/derive.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
//! `wai upscale run` and `wai tonemap run`: an exact derivation of a picture, with the signed record
|
||||
//! a verifier recomputes, and their `verify` (extensions/exact-upscale.md, extensions/exact-tonemap.md).
|
||||
//!
|
||||
//! ```text
|
||||
//! wai upscale run <in.png> --prior <p.wsr> [--derivation sr2|sr4] -o <out.png>
|
||||
//! [--record <out.json> --envelope <in.wai> --key <seed>] [--issued-at <date-time>] [--json]
|
||||
//! wai upscale verify <record.json> --envelope <in.wai> --prior <p.wsr> [--picture <out.png>]
|
||||
//! wai tonemap run <in.wai> --headrooms <h,h,...> --out <dir> [--input-class <class>]
|
||||
//! [--record <out.json> --key <seed>] [--issued-at <date-time>] [--json]
|
||||
//! wai tonemap verify <record.json> --envelope <in.wai> [--renditions <dir>]
|
||||
//! ```
|
||||
//!
|
||||
//! Each writes what it derives, and with `--record` signs the record of it. `--json` prints the
|
||||
//! picture's size, the subpixels, each file with its SHA-256 and the record's link hash.
|
||||
|
||||
use super::opt;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn write(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
crate::out::write(path, bytes).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
fn read(path: &str) -> Result<Vec<u8>, String> {
|
||||
std::fs::read(path).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
/// The positional argument: the first that is neither a flag nor a flag's value.
|
||||
fn path_of<'a>(args: &'a [String], flags: &[&str]) -> Option<&'a str> {
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
if flags.contains(&args[i].as_str()) {
|
||||
i += 2;
|
||||
} else if args[i].starts_with('-') {
|
||||
i += 1;
|
||||
} else {
|
||||
return Some(&args[i]);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `--key`: 64 lowercase hexadecimal digits, or a file holding them.
|
||||
fn signing_key(args: &[String]) -> Result<SigningKey, String> {
|
||||
let s = opt(args, "--key").ok_or("--record needs --key")?;
|
||||
let text = if s.len() == 64 && s.bytes().all(|c| c.is_ascii_hexdigit()) { s.to_string() } else { String::from_utf8_lossy(&read(s)?).trim().to_string() };
|
||||
let seed: Vec<u8> = (text.len() == 64 && text.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)))
|
||||
.then(|| (0..32).map(|i| u8::from_str_radix(&text[2 * i..2 * i + 2], 16).expect("hexadecimal")).collect())
|
||||
.ok_or("--key is 64 lowercase hexadecimal digits, or a file holding them")?;
|
||||
Ok(SigningKey::from_bytes(&seed.try_into().expect("32 bytes")))
|
||||
}
|
||||
|
||||
fn issued_at(args: &[String]) -> String {
|
||||
opt(args, "--issued-at").map(String::from).unwrap_or_else(|| {
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
|
||||
wai::claim_receipt::rfc3339_utc(now)
|
||||
})
|
||||
}
|
||||
|
||||
/// What a derivation wrote, printed for a person or, with `--json`, as its output's schema.
|
||||
fn report(args: &[String], width: u64, height: u64, subpixels: u64, files: &[(String, Vec<u8>)], record: Option<[u8; 32]>) {
|
||||
let digest = |b: &[u8]| Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let link = record.map(|l| l.iter().map(|x| format!("{x:02x}")).collect::<String>());
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
let listed: Vec<Value> = files.iter().map(|(n, b)| json!({"file": n, "sha256": digest(b)})).collect();
|
||||
println!("{}", json!({"width": width, "height": height, "subpixels": subpixels, "files": listed, "record": link}));
|
||||
return;
|
||||
}
|
||||
for (n, b) in files {
|
||||
println!("wrote {n} ({} bytes), SHA-256 {}", b.len(), digest(b));
|
||||
}
|
||||
println!("{width}×{height}, {subpixels} subpixels");
|
||||
if let Some(l) = link {
|
||||
println!("record {l}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "exact_upscale")]
|
||||
pub fn upscale(args: &[String]) -> Result<(), String> {
|
||||
use wai::exact_upscale as ex;
|
||||
const FLAGS: [&str; 7] = ["--prior", "--derivation", "-o", "--record", "--envelope", "--key", "--issued-at"];
|
||||
let input = path_of(args, &FLAGS).ok_or("wai upscale run <in.png> --prior <p.wsr> -o <out.png>")?;
|
||||
let prior = read(opt(args, "--prior").ok_or("--prior is the WSR1 prior")?)?;
|
||||
let out = opt(args, "-o").ok_or("-o is the picture")?;
|
||||
let derivation = match opt(args, "--derivation").unwrap_or("sr2") {
|
||||
"sr2" => wai::int_sr::SR2,
|
||||
"sr4" => wai::int_sr::SR4,
|
||||
other => return Err(format!("--derivation {other}: sr2 or sr4")),
|
||||
};
|
||||
let (w, h, rgb) = wai::png_still::decode(&read(input)?).map_err(|e| format!("{input}: {e}"))?;
|
||||
let made = ex::make(&rgb, w, h, derivation, &prior, &issued_at(args))?;
|
||||
let mut files = vec![(out.to_string(), made.picture.clone())];
|
||||
let mut link = None;
|
||||
if let Some(path) = opt(args, "--record") {
|
||||
let envelope = opt(args, "--envelope").ok_or("--record needs --envelope: the record names the input's envelope, which a verifier needs")?;
|
||||
let record = ex::sign(made.body.clone(), &signing_key(args)?);
|
||||
let text = wai::jcs::jcs(&record).map_err(|e| format!("the record's canonical form: {e:?}"))?;
|
||||
files.push((path.to_string(), text.into_bytes()));
|
||||
files.push((envelope.to_string(), made.envelope.clone()));
|
||||
link = Some(ex::link_hash(&record));
|
||||
}
|
||||
for (n, b) in &files {
|
||||
write(n, b)?;
|
||||
}
|
||||
report(args, made.width, made.height, made.width * made.height * 3, &files, link);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A verification's outcome, printed as JSON; whether it accepted.
|
||||
fn outcome(refused: Option<(String, Value)>, link: Option<[u8; 32]>, subpixels: u64) -> bool {
|
||||
let hex = |b: [u8; 32]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let accepted = refused.is_none();
|
||||
let v = match refused {
|
||||
Some((reason, at)) => json!({"outcome": "refuse", "reason": reason, "at": at}),
|
||||
None => json!({"outcome": "accept", "record": link.map(hex), "subpixels": subpixels}),
|
||||
};
|
||||
println!("{}", serde_json::to_string_pretty(&v).expect("JSON"));
|
||||
accepted
|
||||
}
|
||||
|
||||
/// `wai upscale verify`: exact-upscale §7 under `require-proof`, and the picture held to the output.
|
||||
#[cfg(feature = "exact_upscale")]
|
||||
pub fn upscale_verify(args: &[String]) -> Result<bool, String> {
|
||||
use wai::exact_upscale as ex;
|
||||
let path = path_of(args, &["--envelope", "--prior", "--picture"]).ok_or("wai upscale verify <record.json> --envelope <in.wai> --prior <p.wsr>")?;
|
||||
let text = String::from_utf8_lossy(&read(path)?).into_owned();
|
||||
let envelope = read(opt(args, "--envelope").ok_or("--envelope is the WAI1 of the input")?)?;
|
||||
let prior = read(opt(args, "--prior").ok_or("--prior is the WSR1 prior")?)?;
|
||||
let refuse = |reason: &str, at: &str| Ok(outcome(Some((reason.to_string(), json!([at]))), None, 0));
|
||||
let Ok(env) = wai::container::Wai::from_bytes(&envelope) else { return refuse("input_unreadable", "envelope") };
|
||||
if env.capability() != wai::codecs::CAP_IMAGE_PNG {
|
||||
return refuse("input_unreadable", "envelope");
|
||||
}
|
||||
let Ok((_, _, rgb)) = wai::png_still::decode(&env.payload) else { return refuse("input_unreadable", "envelope") };
|
||||
let hash = wai::jwp_hash::content_hash(&envelope);
|
||||
let case = ex::Case {
|
||||
upscales: vec![text.clone()],
|
||||
objects: vec![envelope],
|
||||
decoded: vec![(hash, rgb)],
|
||||
priors: vec![prior],
|
||||
policy: ex::Policy { require_proof: true, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
if let ex::Outcome::Refuse { at, reason } = ex::verify(&case) {
|
||||
return Ok(outcome(Some((reason.code().to_string(), at.to_json())), None, 0));
|
||||
}
|
||||
let u = ex::read_upscale(&text).map_err(|r| format!("{path}: {}", r.code()))?;
|
||||
let (w, h) = u.output_size();
|
||||
if let Some(picture) = opt(args, "--picture") {
|
||||
let held = wai::png_still::decode(&read(picture)?).ok().is_some_and(|(pw, ph, s)| (u64::from(pw), u64::from(ph)) == (w, h) && Sha256::digest(&s).as_slice() == u.output);
|
||||
if !held {
|
||||
return refuse("picture_mismatch", "picture");
|
||||
}
|
||||
}
|
||||
Ok(outcome(None, Some(u.link), w * h * 3))
|
||||
}
|
||||
|
||||
/// `wai tonemap verify`: exact-tonemap §8 under `require-proof`, and each rendition held to its picture.
|
||||
#[cfg(feature = "exact_tonemap")]
|
||||
pub fn tonemap_verify(args: &[String]) -> Result<bool, String> {
|
||||
use wai::exact_tonemap as tm;
|
||||
let path = path_of(args, &["--envelope", "--renditions"]).ok_or("wai tonemap verify <record.json> --envelope <in.wai>")?;
|
||||
let text = String::from_utf8_lossy(&read(path)?).into_owned();
|
||||
let mut held = tm::Held::default();
|
||||
held.hold_envelope(read(opt(args, "--envelope").ok_or("--envelope is the WAI2 the record names")?)?);
|
||||
if let tm::Outcome::Refuse { at, reason } = tm::verify(std::slice::from_ref(&text), &held, tm::Policy::RequireProof) {
|
||||
return Ok(outcome(Some((reason.code().to_string(), Value::Array(at))), None, 0));
|
||||
}
|
||||
let r = tm::read_record(&text).map_err(|e| format!("{path}: {}", e.code()))?;
|
||||
if let Some(dir) = opt(args, "--renditions") {
|
||||
for (k, p) in r.pictures.iter().enumerate() {
|
||||
let held = std::fs::read(format!("{dir}/rendition-h{}.rgb16", p.headroom)).is_ok_and(|b| tm::digest(&b) == p.output);
|
||||
if !held {
|
||||
return Ok(outcome(Some(("rendition_mismatch".to_string(), json!(["renditions", k]))), None, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(outcome(None, Some(r.link), r.pictures.len() as u64 * r.width * r.height * 3))
|
||||
}
|
||||
|
||||
#[cfg(feature = "exact_tonemap")]
|
||||
pub fn tonemap(args: &[String]) -> Result<(), String> {
|
||||
use wai::exact_tonemap as tm;
|
||||
const FLAGS: [&str; 6] = ["--headrooms", "--out", "--input-class", "--record", "--key", "--issued-at"];
|
||||
let input = path_of(args, &FLAGS).ok_or("wai tonemap run <in.wai> --headrooms <h,h,...> --out <dir>")?;
|
||||
let dir = opt(args, "--out").ok_or("--out is the directory the renditions are written in")?;
|
||||
let headrooms = opt(args, "--headrooms").ok_or("--headrooms are the headrooms, in 1/10000 of a stop")?;
|
||||
let headrooms: Vec<u64> = headrooms.split(',').map(|h| h.parse().map_err(|_| format!("--headrooms: {h} is not an integer"))).collect::<Result<_, _>>()?;
|
||||
let made = tm::make(&read(input)?, &headrooms, opt(args, "--input-class").unwrap_or("captured"), &issued_at(args))?;
|
||||
let mut files: Vec<(String, Vec<u8>)> = made.renditions.iter().map(|(h, b)| (format!("{dir}/rendition-h{h}.rgb16"), b.clone())).collect();
|
||||
let mut link = None;
|
||||
if let Some(path) = opt(args, "--record") {
|
||||
let record = tm::sign(made.body.clone(), &signing_key(args)?);
|
||||
let text = wai::jcs::jcs(&record).map_err(|e| format!("the record's canonical form: {e:?}"))?;
|
||||
files.push((path.to_string(), text.into_bytes()));
|
||||
link = Some(tm::link_hash(&record));
|
||||
}
|
||||
crate::out::create_dir_all(dir).map_err(|e| format!("{dir}: {e}"))?;
|
||||
for (n, b) in &files {
|
||||
write(n, b)?;
|
||||
}
|
||||
report(args, made.width, made.height, made.renditions.len() as u64 * made.width * made.height * 3, &files, link);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -550,6 +550,16 @@ gated!(media_generate, "media.generate", cfg(feature = "generate"), |a| {
|
|||
crate::generate::main(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(media_upscale, "media.upscale", cfg(feature = "exact_upscale"), |a| {
|
||||
crate::derive::upscale(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(media_tonemap, "media.tonemap", cfg(feature = "exact_tonemap"), |a| {
|
||||
crate::derive::tonemap(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(upscale_verify, "upscale.verify", cfg(feature = "exact_upscale"), |a| Ok(crate::derive::upscale_verify(a)?));
|
||||
gated!(tonemap_verify, "tonemap.verify", cfg(feature = "exact_tonemap"), |a| Ok(crate::derive::tonemap_verify(a)?));
|
||||
gated!(score_build, "score.build", cfg(feature = "score"), |a| {
|
||||
crate::score::main(&with(&["build"], a))?;
|
||||
Ok(true)
|
||||
|
|
@ -620,6 +630,10 @@ pub const IMPLS: &[Impl] = &[
|
|||
Impl { id: "media.encode", run: media_encode, json: Json::Text },
|
||||
Impl { id: "media.decode", run: media_decode, json: Json::Text },
|
||||
Impl { id: "media.generate", run: media_generate, json: Json::Own },
|
||||
Impl { id: "media.upscale", run: media_upscale, json: Json::Own },
|
||||
Impl { id: "media.tonemap", run: media_tonemap, json: Json::Own },
|
||||
Impl { id: "upscale.verify", run: upscale_verify, json: Json::Own },
|
||||
Impl { id: "tonemap.verify", run: tonemap_verify, json: Json::Own },
|
||||
Impl { id: "score.build", run: score_build, json: Json::Own },
|
||||
Impl { id: "score.decode", run: score_render, json: Json::Own },
|
||||
Impl { id: "encoder.list", run: encoder_list, json: Json::Text },
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ pub const ENVELOPE: &str = "envelope/1";
|
|||
pub const VERBS: &[&str] = &[
|
||||
"create", "list", "show", "update", "delete", "check", "verify", "run", "build", "export", "import", "start", "stop",
|
||||
"join", "leave", "grant", "invite", "revoke", "rotate", "tail", "search", "test", "load", "encode", "decode", "inspect",
|
||||
"wrap", "unwrap", "fetch", "generate", "promote", "rollback", "lock", "upgrade", "setup", "guide", "stream",
|
||||
"wrap", "unwrap", "fetch", "generate", "promote", "rollback", "lock", "upgrade", "setup", "guide", "stream", "upscale",
|
||||
"tonemap",
|
||||
];
|
||||
|
||||
/// Verbs that are another verb's near-synonym, with the verb to use: an id never ends in one.
|
||||
|
|
@ -289,6 +290,8 @@ const S_TEMPLATES: &str = r#"{"type":"array","items":{"type":"object","required"
|
|||
const S_DEV_CHECK: &str = r#"{"type":"object","required":["ready","seconds","steps","checks","origin"]}"#;
|
||||
const S_DEV_READY: &str = r#"{"type":"object","required":["ready"],"properties":{"ready":{"type":"boolean"},"origin":{"type":"string"}}}"#;
|
||||
const S_GENERATE: &str = r#"{"type":"object","description":"The picture's or instruction object's digests, as `wai generate --json` prints them"}"#;
|
||||
const S_DERIVED: &str = r#"{"type":"object","required":["width","height","subpixels","files"],"description":"What `wai upscale run` or `wai tonemap run` made: the subpixels it would bill, each file it wrote with its digest, and the record's link hash"}"#;
|
||||
const S_DERIVED_VERIFY: &str = r#"{"type":"object","required":["outcome"],"description":"accept, with the record's link hash and the subpixels it states; or refuse, with the reason and where"}"#;
|
||||
const S_SCORE: &str = r#"{"type":"object","description":"The score's digests, as `wai score build|render --json` print them"}"#;
|
||||
const S_BREAKING: &str = r#"{"type":"object","required":["against","findings"],"description":"Each finding's id, category, what changed and whether the CHANGELOG acknowledges it"}"#;
|
||||
const S_CONFORM: &str = r#"{"type":"object","required":["conformant","cases","passed","failed"]}"#;
|
||||
|
|
@ -507,6 +510,80 @@ encode avc|hevc <in.y4m> [-o out.wai] [--encoder auto|platform|va|gpu-driver|sof
|
|||
needs: Needs::All(&["generate"]),
|
||||
..OP
|
||||
},
|
||||
Op {
|
||||
id: "media.upscale",
|
||||
summary: "Upscale a PNG exactly, by an integer network under a pinned prior, with a signed record a verifier recomputes",
|
||||
cli: Some("upscale run"),
|
||||
usage: "upscale run <in.png> --prior <p.wsr> [--derivation sr2|sr4] -o <out.png>
|
||||
[--record <out.json> --envelope <in.wai> --key <seed>] [--issued-at <date-time>] [--json]
|
||||
an 8-bit RGB PNG; --record signs what exact-upscale §7 recomputes from --envelope and the prior",
|
||||
effect: Effect::WritesProject,
|
||||
input: &[
|
||||
pos("path", Ty::Path, true, "An 8-bit RGB PNG."),
|
||||
flag("prior", "--prior", Ty::Path, true, "The WSR1 prior."),
|
||||
flag("derivation", "--derivation", Ty::String, false, "sr2 (the default) or sr4."),
|
||||
flag("out", "-o", Ty::Path, true, "The picture, a PNG."),
|
||||
flag("record", "--record", Ty::Path, false, "The signed upscale record."),
|
||||
flag("envelope", "--envelope", Ty::Path, false, "The WAI1 of the input, which the record names."),
|
||||
flag("key", "--key", Ty::String, false, "The record's signing seed: 64 hexadecimal digits, or a file holding them."),
|
||||
],
|
||||
output: Output::Json(S_DERIVED),
|
||||
receipt: true,
|
||||
needs: Needs::All(&["exact_upscale"]),
|
||||
..OP
|
||||
},
|
||||
Op {
|
||||
id: "upscale.verify",
|
||||
summary: "An upscale record recomputed from its input and its prior, and the picture held to it",
|
||||
cli: Some("upscale verify"),
|
||||
usage: "upscale verify <record.json> --envelope <in.wai> --prior <p.wsr> [--picture <out.png>]
|
||||
the record's outcome as JSON (extensions/exact-upscale.md §7, under require-proof); exit 1 when it is refused",
|
||||
input: &[
|
||||
pos("record", Ty::Path, true, "The upscale record."),
|
||||
flag("envelope", "--envelope", Ty::Path, true, "The WAI1 of the input, which the record names."),
|
||||
flag("prior", "--prior", Ty::Path, true, "The WSR1 prior the record names."),
|
||||
flag("picture", "--picture", Ty::Path, false, "The picture: its samples are held to the record's output."),
|
||||
],
|
||||
output: Output::Json(S_DERIVED_VERIFY),
|
||||
needs: Needs::All(&["exact_upscale"]),
|
||||
..OP
|
||||
},
|
||||
Op {
|
||||
id: "media.tonemap",
|
||||
summary: "Render a still's gain map exactly at the headrooms named, with a signed record a verifier recomputes",
|
||||
cli: Some("tonemap run"),
|
||||
usage: "tonemap run <in.wai> --headrooms <h,h,...> --out <dir> [--input-class <class>]
|
||||
[--record <out.json> --key <seed>] [--issued-at <date-time>] [--json]
|
||||
a WAI2 of a PNG base and its gain map; a headroom is in 1/10000 of a stop; each rendition is RGB16",
|
||||
effect: Effect::WritesProject,
|
||||
input: &[
|
||||
pos("path", Ty::Path, true, "A WAI2 of a PNG base and its gain map."),
|
||||
flag("headrooms", "--headrooms", Ty::String, true, "The headrooms, in 1/10000 of a stop, joined by commas."),
|
||||
flag("out", "--out", Ty::Path, true, "The directory the renditions are written in."),
|
||||
flag("input_class", "--input-class", Ty::String, false, "The input's class (captured)."),
|
||||
flag("record", "--record", Ty::Path, false, "The signed rendition record."),
|
||||
flag("key", "--key", Ty::String, false, "The record's signing seed: 64 hexadecimal digits, or a file holding them."),
|
||||
],
|
||||
output: Output::Json(S_DERIVED),
|
||||
receipt: true,
|
||||
needs: Needs::All(&["exact_tonemap"]),
|
||||
..OP
|
||||
},
|
||||
Op {
|
||||
id: "tonemap.verify",
|
||||
summary: "A rendition record recomputed from its envelope, and the renditions held to it",
|
||||
cli: Some("tonemap verify"),
|
||||
usage: "tonemap verify <record.json> --envelope <in.wai> [--renditions <dir>]
|
||||
the record's outcome as JSON (extensions/exact-tonemap.md §8, under require-proof); exit 1 when it is refused",
|
||||
input: &[
|
||||
pos("record", Ty::Path, true, "The rendition record."),
|
||||
flag("envelope", "--envelope", Ty::Path, true, "The WAI2 the record names."),
|
||||
flag("renditions", "--renditions", Ty::Path, false, "The directory `wai tonemap run` wrote: each rendition is held to its picture."),
|
||||
],
|
||||
output: Output::Json(S_DERIVED_VERIFY),
|
||||
needs: Needs::All(&["exact_tonemap"]),
|
||||
..OP
|
||||
},
|
||||
Op {
|
||||
id: "score.build",
|
||||
summary: "A WSCR score from integer note fields",
|
||||
|
|
@ -1819,6 +1896,8 @@ pub fn feature_on(feature: &str) -> Option<bool> {
|
|||
"video_manifest" => cfg!(feature = "video_manifest"),
|
||||
"key_discovery" => cfg!(feature = "key_discovery"),
|
||||
"generate" => cfg!(feature = "generate"),
|
||||
"exact_upscale" => cfg!(feature = "exact_upscale"),
|
||||
"exact_tonemap" => cfg!(feature = "exact_tonemap"),
|
||||
"score" => cfg!(feature = "score"),
|
||||
"staged" => cfg!(feature = "staged"),
|
||||
"video_int" => cfg!(feature = "video_int"),
|
||||
|
|
|
|||
|
|
@ -1300,6 +1300,68 @@ pub fn read_record(text: &str) -> Result<Record, Reason> {
|
|||
|
||||
// ─────────────────────────────── §8 verification ───────────────────────────────
|
||||
|
||||
/// What a tone mapping of a still makes: each headroom's rendition, ascending, and the body of
|
||||
/// their record (§7.1), which [`sign`] signs.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Rendered {
|
||||
pub width: u64,
|
||||
pub height: u64,
|
||||
/// (headroom, the rendition's `RGB16` bytes).
|
||||
pub renditions: Vec<(u64, Vec<u8>)>,
|
||||
pub body: Value,
|
||||
}
|
||||
|
||||
/// Render a `WAI2` of a still's PNG (rendition 0) and its gain map at `headrooms` (§4.4), and write
|
||||
/// its record's body (§7.1): its input of `input_class`, its own class the least that allows (§7.2).
|
||||
pub fn make(envelope: &[u8], headrooms: &[u64], input_class: &str, issued_at: &str) -> Result<Rendered, String> {
|
||||
use crate::container::{companion_indices, WaiMulti};
|
||||
if !envelope.starts_with(crate::container::MAGIC_V2) {
|
||||
return Err("not a WAI2 envelope: a gain map is a companion, which only WAI2 carries".into());
|
||||
}
|
||||
let env = WaiMulti::from_bytes(envelope).map_err(|e| e.to_string())?;
|
||||
env.check_component_roles().map_err(|e| e.code().to_string())?;
|
||||
let entries = &env.manifest.renditions;
|
||||
if !entries.first().is_some_and(|m| m.is_selectable() && m.capability == crate::codecs::CAP_IMAGE_PNG) {
|
||||
return Err(format!("rendition 0 is not a selectable {}", crate::codecs::CAP_IMAGE_PNG));
|
||||
}
|
||||
let index = companion_indices(entries, 0).into_iter().find(|&i| entries.get(i).is_some_and(|m| m.is_companion() && m.capability == CAP_GAINMAP)).ok_or_else(|| format!("no {CAP_GAINMAP} companion of rendition 0"))?;
|
||||
let companion = &env.renditions[index];
|
||||
let gain = read_gainmap(companion).map_err(|r| format!("the gain map: {}", r.code()))?;
|
||||
let (w, h, base) = crate::png_still::decode(&env.renditions[0]).map_err(|e| format!("the base: {e}"))?;
|
||||
if u64::from(w) > MAX_SIDE || u64::from(h) > MAX_SIDE {
|
||||
return Err(format!("the base is {w}×{h}, past {MAX_SIDE} on a side"));
|
||||
}
|
||||
let mut ordered = headrooms.to_vec();
|
||||
ordered.sort_unstable();
|
||||
ordered.dedup();
|
||||
if ordered.is_empty() || ordered.len() > MAX_PICTURES || ordered.len() != headrooms.len() {
|
||||
return Err(format!("1 to {MAX_PICTURES} distinct headrooms"));
|
||||
}
|
||||
if let Some(h) = ordered.iter().find(|h| **h > MAX_HEADROOM) {
|
||||
return Err(format!("the headroom {h} is past {MAX_HEADROOM}"));
|
||||
}
|
||||
let rank = CLASSES.iter().position(|c| *c == input_class).ok_or_else(|| format!("the class {input_class} is not one of {CLASSES:?}"))?;
|
||||
let mut renditions = Vec::with_capacity(ordered.len());
|
||||
for headroom in ordered {
|
||||
let out = gain.render(&base, w, h, headroom).map_err(|r| format!("headroom {headroom}: {}", r.code()))?;
|
||||
renditions.push((headroom, rgb16_bytes(&out)));
|
||||
}
|
||||
let object = hex(&crate::jwp_hash::content_hash(envelope));
|
||||
let input = hex(&digest(&base));
|
||||
let pictures: Vec<Value> = renditions.iter().map(|(headroom, b)| json!({"frame": 0, "headroom": headroom, "input": input, "output": hex(&digest(b))})).collect();
|
||||
let body = json!({
|
||||
"kind": KIND,
|
||||
"derivation": GAINMAP,
|
||||
"base": {"object": object, "rendition": 0, "width": w, "height": h},
|
||||
"companion": {"index": index, "sha256": hex(&digest(companion))},
|
||||
"pictures": pictures,
|
||||
"class": CLASSES[rank.max(1)],
|
||||
"inputs": [{"content_hash": object, "origin": Value::Null, "class": input_class}],
|
||||
"issued_at": issued_at,
|
||||
});
|
||||
Ok(Rendered { width: u64::from(w), height: u64::from(h), renditions, body })
|
||||
}
|
||||
|
||||
/// The policy of §8.1.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Policy {
|
||||
|
|
|
|||
|
|
@ -490,6 +490,47 @@ pub fn encode_residual(upscaled: &[u8], target: &[u8], w: u32, h: u32, q: u32) -
|
|||
// §7 Verification
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
/// What an upscale makes: the input's envelope, the output's samples and the picture they are, and
|
||||
/// the body of their record (§4.2), which [`sign`] signs.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Upscaled {
|
||||
/// The `WAI1` of the input's PNG, the object the record names.
|
||||
pub envelope: Vec<u8>,
|
||||
pub samples: Vec<u8>,
|
||||
pub picture: Vec<u8>,
|
||||
pub width: u64,
|
||||
pub height: u64,
|
||||
pub body: Value,
|
||||
}
|
||||
|
||||
/// Upscale an `RGB8` picture of `w × h` by `derivation` under `prior` (§3), and write its record's
|
||||
/// body (§4.2): no residual and no target. A prior that does not load, or a picture past §3.3's
|
||||
/// limits, has no output.
|
||||
pub fn make(rgb: &[u8], w: u32, h: u32, derivation: &str, prior: &[u8], issued_at: &str) -> Result<Upscaled, String> {
|
||||
use crate::container::{Conditioning, Manifest, ModelRequirement, Wai};
|
||||
let loaded = int_sr::load(prior).map_err(|e| format!("the prior does not load: {e:?}"))?;
|
||||
let samples = int_sr::derive(derivation, &loaded, rgb, w as usize, h as usize).map_err(|e| format!("{derivation}: {e:?}"))?;
|
||||
let f = int_sr::factor(derivation).ok_or_else(|| format!("{derivation} is not a derivation"))? as u32;
|
||||
let (width, height) = (w * f, h * f);
|
||||
let manifest = Manifest::new("image", "replicate", ModelRequirement::new(crate::codecs::CAP_IMAGE_PNG), Conditioning::new("png"));
|
||||
let envelope = Wai::new(manifest, crate::png_still::encode(rgb, w, h)).to_bytes().map_err(|e| format!("the envelope: {e}"))?;
|
||||
let digest = hex(&sha256(&[&samples]));
|
||||
let body = json!({
|
||||
"kind": "wai-upscale",
|
||||
"object": hex(&crate::jwp_hash::content_hash(&envelope)),
|
||||
"input": {"width": w, "height": h, "samples": hex(&sha256(&[rgb]))},
|
||||
"derivation": derivation,
|
||||
"prior": hex(&sha256(&[prior])),
|
||||
"upscaled": digest,
|
||||
"residual": Value::Null,
|
||||
"output": digest,
|
||||
"target": Value::Null,
|
||||
"issued_at": issued_at,
|
||||
});
|
||||
let picture = crate::png_still::encode(&samples, width, height);
|
||||
Ok(Upscaled { envelope, samples, picture, width: width as u64, height: height as u64, body })
|
||||
}
|
||||
|
||||
/// A verifier's policy (§7.1): synthesis's two members and `upscale`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Policy {
|
||||
|
|
|
|||
150
wai/wai-rs/tests/cli_derive.rs
Normal file
150
wai/wai-rs/tests/cli_derive.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
//! `wai upscale` and `wai tonemap` end to end: what each runs and writes is what its extension's verifier
|
||||
//! proves under `require-proof`, from the files it wrote alone.
|
||||
#![cfg(all(feature = "exact_upscale", feature = "exact_tonemap"))]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use wai::exact_tonemap as tm;
|
||||
use wai::exact_upscale as ex;
|
||||
|
||||
const SEED: &str = "0707070707070707070707070707070707070707070707070707070707070707";
|
||||
|
||||
fn wai_in(d: &Path, args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(d).args(args).output().expect("wai runs")
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("wai-cli-derive-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn ok(o: &Output) -> &Output {
|
||||
assert!(o.status.success(), "{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));
|
||||
o
|
||||
}
|
||||
|
||||
fn prior_path() -> String {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../live-conformance/upscale/priors/generic.wsr").to_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_upscale_writes_what_exact_upscale_proves() {
|
||||
let d = scratch("upscale");
|
||||
let rgb: Vec<u8> = (0..8u32 * 6).flat_map(|i| [(i * 7 % 256) as u8, (i * 3 % 256) as u8, (i % 256) as u8]).collect();
|
||||
std::fs::write(d.join("in.png"), wai::png_still::encode(&rgb, 8, 6)).unwrap();
|
||||
let prior = prior_path();
|
||||
let o = wai_in(&d, &["upscale", "run", "in.png", "--prior", &prior, "-o", "out.png", "--record", "r.json", "--envelope", "in.wai", "--key", SEED, "--json"]);
|
||||
let v: serde_json::Value = serde_json::from_slice(&ok(&o).stdout).unwrap();
|
||||
let v = &v["data"];
|
||||
assert_eq!((&v["width"], &v["height"], &v["subpixels"]), (&serde_json::json!(16), &serde_json::json!(12), &serde_json::json!(16 * 12 * 3)));
|
||||
|
||||
let text = std::fs::read_to_string(d.join("r.json")).unwrap();
|
||||
let envelope = std::fs::read(d.join("in.wai")).unwrap();
|
||||
let (pw, ph, samples) = wai::png_still::decode(&std::fs::read(d.join("out.png")).unwrap()).unwrap();
|
||||
assert_eq!((pw, ph), (16, 12));
|
||||
let u = ex::read_upscale(&text).unwrap();
|
||||
use sha2::{Digest, Sha256};
|
||||
assert_eq!(Sha256::digest(&samples).as_slice(), u.output, "the picture is the record's output");
|
||||
let case = |priors: Vec<Vec<u8>>| ex::Case {
|
||||
upscales: vec![text.clone()],
|
||||
objects: vec![envelope.clone()],
|
||||
decoded: vec![(wai::jwp_hash::content_hash(&envelope), rgb.clone())],
|
||||
priors,
|
||||
policy: ex::Policy { require_proof: true, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
assert!(matches!(ex::verify(&case(vec![std::fs::read(&prior).unwrap()])), ex::Outcome::Accept(_)));
|
||||
assert!(!matches!(ex::verify(&case(vec![])), ex::Outcome::Accept(_)), "with no prior, nothing is proved");
|
||||
|
||||
// `wai upscale verify` holds what it wrote, and refuses what it did not.
|
||||
let verify = |d: &Path, args: &[&str]| -> (bool, serde_json::Value) {
|
||||
let o = wai_in(d, &[&["upscale", "verify", "r.json", "--envelope", "in.wai"], args].concat());
|
||||
(o.status.success(), serde_json::from_slice(&o.stdout).unwrap_or(serde_json::Value::Null))
|
||||
};
|
||||
let (good, v) = verify(&d, &["--prior", &prior, "--picture", "out.png"]);
|
||||
assert!(good, "{v}");
|
||||
assert_eq!((&v["outcome"], &v["subpixels"]), (&serde_json::json!("accept"), &serde_json::json!(16 * 12 * 3)));
|
||||
let mut other = samples.clone();
|
||||
other[0] ^= 1;
|
||||
std::fs::write(d.join("other.png"), wai::png_still::encode(&other, 16, 12)).unwrap();
|
||||
let (good, v) = verify(&d, &["--prior", &prior, "--picture", "other.png"]);
|
||||
assert!(!good && v["reason"] == "picture_mismatch", "{v}");
|
||||
let title = Path::new(env!("CARGO_MANIFEST_DIR")).join("../live-conformance/upscale/priors/title.wsr");
|
||||
let (good, v) = verify(&d, &["--prior", title.to_str().unwrap()]);
|
||||
assert!(!good && v["reason"] == "upscale_not_proved", "another prior proves nothing: {v}");
|
||||
|
||||
// The picture alone needs no key; a record does, and names the envelope.
|
||||
ok(&wai_in(&d, &["upscale", "run", "in.png", "--prior", &prior, "-o", "bare.png"]));
|
||||
assert_eq!(std::fs::read(d.join("bare.png")).unwrap(), std::fs::read(d.join("out.png")).unwrap(), "the derivation is exact");
|
||||
assert!(!wai_in(&d, &["upscale", "run", "in.png", "--prior", &prior, "-o", "x.png", "--record", "x.json", "--envelope", "x.wai"]).status.success(), "a record needs a key");
|
||||
assert!(!wai_in(&d, &["upscale", "run", "in.png", "--prior", &prior, "-o", "x.png", "--record", "x.json", "--key", SEED]).status.success(), "a record needs its envelope");
|
||||
assert!(!wai_in(&d, &["upscale", "run", "in.png", "--prior", &prior, "-o", "x.png", "--derivation", "sr3"]).status.success());
|
||||
std::fs::write(d.join("grey.png"), wai::png_still::encode_grey(&[9u8; 48], 8, 6)).unwrap();
|
||||
assert!(!wai_in(&d, &["upscale", "run", "grey.png", "--prior", &prior, "-o", "x.png"]).status.success(), "the reader holds 8-bit RGB");
|
||||
}
|
||||
|
||||
fn hdr_envelope(w: u32, h: u32) -> Vec<u8> {
|
||||
use wai::container::{Rendition, WaiMulti};
|
||||
let e = tm::corpus::encode(&tm::corpus::scene(0, w, h), w, h, 4, false, (1, 1), false);
|
||||
let companion = tm::wgm1(&e.metadata.to_bytes(), &wai::png_still::encode_grey(&e.map, e.map_w, e.map_h));
|
||||
WaiMulti::new(
|
||||
"image",
|
||||
"replicate",
|
||||
serde_json::Value::Null,
|
||||
vec![
|
||||
Rendition::media(wai::codecs::CAP_IMAGE_PNG, "png", wai::png_still::encode(&e.base, w, h)).with_id("base").with_payload_digest(),
|
||||
Rendition::companion(tm::CAP_GAINMAP, companion, wai::codecs::CAP_IMAGE_PNG).with_id("gainmap").with_companion_of_id("base").with_payload_digest(),
|
||||
],
|
||||
)
|
||||
.to_bytes()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tone_mapping_writes_what_exact_tonemap_proves() {
|
||||
let d = scratch("tonemap");
|
||||
let envelope = hdr_envelope(16, 12);
|
||||
std::fs::write(d.join("in.wai"), &envelope).unwrap();
|
||||
let o = wai_in(&d, &["tonemap", "run", "in.wai", "--headrooms", "20000,10000", "--out", "r", "--record", "r.json", "--key", SEED, "--json"]);
|
||||
let v: serde_json::Value = serde_json::from_slice(&ok(&o).stdout).unwrap();
|
||||
assert_eq!(v["data"]["subpixels"], 2 * 16 * 12 * 3);
|
||||
|
||||
let text = std::fs::read_to_string(d.join("r.json")).unwrap();
|
||||
let r = tm::read_record(&text).unwrap();
|
||||
// Ascending by headroom, each file the bytes its picture states.
|
||||
assert_eq!(r.pictures.iter().map(|p| p.headroom).collect::<Vec<_>>(), vec![10_000, 20_000]);
|
||||
for p in &r.pictures {
|
||||
let b = std::fs::read(d.join(format!("r/rendition-h{}.rgb16", p.headroom))).unwrap();
|
||||
assert_eq!(tm::digest(&b), p.output, "headroom {}", p.headroom);
|
||||
}
|
||||
let mut held = tm::Held::default();
|
||||
held.hold_envelope(envelope);
|
||||
assert!(matches!(tm::verify(std::slice::from_ref(&text), &held, tm::Policy::RequireProof), tm::Outcome::Accept(_)));
|
||||
assert!(!matches!(tm::verify(std::slice::from_ref(&text), &tm::Held::default(), tm::Policy::RequireProof), tm::Outcome::Accept(_)), "with no envelope, nothing is proved");
|
||||
|
||||
// `wai tonemap verify` holds what it wrote, and refuses what it did not.
|
||||
let o = wai_in(&d, &["tonemap", "verify", "r.json", "--envelope", "in.wai", "--renditions", "r"]);
|
||||
let v: serde_json::Value = serde_json::from_slice(&ok(&o).stdout).unwrap();
|
||||
assert_eq!((&v["outcome"], &v["subpixels"]), (&serde_json::json!("accept"), &serde_json::json!(2 * 16 * 12 * 3)));
|
||||
let path = d.join("r/rendition-h20000.rgb16");
|
||||
let mut b = std::fs::read(&path).unwrap();
|
||||
b[0] ^= 1;
|
||||
std::fs::write(&path, b).unwrap();
|
||||
let o = wai_in(&d, &["tonemap", "verify", "r.json", "--envelope", "in.wai", "--renditions", "r"]);
|
||||
let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert!(!o.status.success() && v["reason"] == "rendition_mismatch" && v["at"] == serde_json::json!(["renditions", 1]), "{v}");
|
||||
std::fs::write(d.join("other.wai"), hdr_envelope(8, 6)).unwrap();
|
||||
let o = wai_in(&d, &["tonemap", "verify", "r.json", "--envelope", "other.wai"]);
|
||||
let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert!(!o.status.success() && v["reason"] == "rendition_not_proved", "another envelope proves nothing: {v}");
|
||||
|
||||
assert!(!wai_in(&d, &["tonemap", "run", "in.wai", "--headrooms", "10,10", "--out", "x"]).status.success(), "headrooms are distinct");
|
||||
assert!(!wai_in(&d, &["tonemap", "run", "in.wai", "--headrooms", "60001", "--out", "x"]).status.success(), "past the largest headroom");
|
||||
assert!(!wai_in(&d, &["tonemap", "run", "in.wai", "--headrooms", "ten", "--out", "x"]).status.success());
|
||||
assert!(!wai_in(&d, &["tonemap", "run", "in.wai", "--headrooms", "10", "--out", "x", "--input-class", "invented"]).status.success());
|
||||
std::fs::write(d.join("one.wai"), b"WAI1 not this").unwrap();
|
||||
assert!(!wai_in(&d, &["tonemap", "run", "one.wai", "--headrooms", "10", "--out", "x"]).status.success(), "a gain map needs a WAI2");
|
||||
}
|
||||
|
|
@ -490,6 +490,32 @@ fn samples(d: &Path, origin_port: u16) -> Vec<(&'static str, Vec<String>, bool)>
|
|||
let model = wai_dir().join("wai-rs/tests/vectors/generate/generate_spectral_model.json");
|
||||
more.push(("media.generate", s(&["generate", "--model", model.to_str().unwrap(), "--prompt", "a calm spectral horizon", "--seed", "7", "--steps", "1", "--size", "16x16", "-o", "g.rgb"]), true));
|
||||
}
|
||||
#[cfg(feature = "exact_upscale")]
|
||||
{
|
||||
let prior = wai_dir().join("live-conformance/upscale/priors/generic.wsr");
|
||||
std::fs::write(d.join("small.png"), wai::png_still::encode(&[9u8; 3 * 8 * 6], 8, 6)).unwrap();
|
||||
more.push(("media.upscale", s(&["upscale", "run", "small.png", "--prior", prior.to_str().unwrap(), "-o", "up.png"]), true));
|
||||
}
|
||||
#[cfg(feature = "exact_tonemap")]
|
||||
{
|
||||
use wai::exact_tonemap as tm;
|
||||
let (w, h) = (16, 12);
|
||||
let e = tm::corpus::encode(&tm::corpus::scene(0, w, h), w, h, 4, false, (1, 1), false);
|
||||
let companion = tm::wgm1(&e.metadata.to_bytes(), &wai::png_still::encode_grey(&e.map, e.map_w, e.map_h));
|
||||
let envelope = wai::container::WaiMulti::new(
|
||||
"image",
|
||||
"replicate",
|
||||
serde_json::Value::Null,
|
||||
vec![
|
||||
wai::container::Rendition::media(wai::codecs::CAP_IMAGE_PNG, "png", wai::png_still::encode(&e.base, w, h)).with_id("base").with_payload_digest(),
|
||||
wai::container::Rendition::companion(tm::CAP_GAINMAP, companion, wai::codecs::CAP_IMAGE_PNG).with_id("gainmap").with_companion_of_id("base").with_payload_digest(),
|
||||
],
|
||||
)
|
||||
.to_bytes()
|
||||
.unwrap();
|
||||
std::fs::write(d.join("hdr.wai"), envelope).unwrap();
|
||||
more.push(("media.tonemap", s(&["tonemap", "run", "hdr.wai", "--headrooms", "20000", "--out", "renditions"]), true));
|
||||
}
|
||||
#[cfg(feature = "score")]
|
||||
{
|
||||
let notes = wai_dir().join("score-conformance/notes/scale.json");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// GENERATED by scripts/actions.mjs from the WAI operation catalogue. Do not edit: change the
|
||||
// catalogue (wai-rs/src/catalogue.rs, exported by `wai ops export`), then run
|
||||
// `node scripts/actions.mjs`. `node scripts/actions.mjs --check` fails when this file differs.
|
||||
export const CATALOGUE = {"format":"wai.ops/1","toolchain":"0.4.0","sha256":"7168ac79a55b09ed96bb81d0ada8602e01358164135d0de4f6b2077830ef8497"};
|
||||
export const CATALOGUE = {"format":"wai.ops/1","toolchain":"0.4.0","sha256":"27cbe1a11c9526c51f021e8b07c9107cdcd94f2a72c80bc8e1f7fb0b2f4b117a"};
|
||||
export const ACTIONS = [
|
||||
{"id":"toolchain.test","noun":"toolchain","verb":"test","menu":"Toolchain","item":"Test","palette":"toolchain test","summary":"WAI's four steps (name, dispatch, rebuild, receipt) on a picture made here, its receipt checked offline","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai hello","mcp":null,"route":null,"call":"toolchain_test"},
|
||||
{"id":"project.create","noun":"project","verb":"create","menu":"Project","item":"Create","palette":"project create","summary":"A new project from one of the examples, written into a new or empty directory; --list names the templates","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai create","mcp":null,"route":null,"call":"project_create"},
|
||||
|
|
@ -13,6 +13,10 @@ export const ACTIONS = [
|
|||
{"id":"media.encode","noun":"media","verb":"encode","menu":"Media","item":"Encode","palette":"media encode","summary":"Encode a file with a codec this build holds, in an envelope; AVC and HEVC through the device's own encoders, with a signed record","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai encode","mcp":null,"route":null,"call":"media_encode"},
|
||||
{"id":"media.decode","noun":"media","verb":"decode","menu":"Media","item":"Decode","palette":"media decode","summary":"Decode an envelope through SPEC §4's dispatch to raw samples, with a .meta file beside them","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai decode","mcp":null,"route":null,"call":"media_decode"},
|
||||
{"id":"media.generate","noun":"media","verb":"generate","menu":"Media","item":"Generate","palette":"media generate","summary":"Make a wai.generate.spectral picture natively and print its SHA-256, or write its instruction object","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai generate","mcp":null,"route":null,"call":"media_generate"},
|
||||
{"id":"media.upscale","noun":"media","verb":"upscale","menu":"Media","item":"Upscale","palette":"media upscale","summary":"Upscale a PNG exactly, by an integer network under a pinned prior, with a signed record a verifier recomputes","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai upscale run","mcp":null,"route":null,"call":"media_upscale"},
|
||||
{"id":"upscale.verify","noun":"upscale","verb":"verify","menu":"Upscale","item":"Verify","palette":"upscale verify","summary":"An upscale record recomputed from its input and its prior, and the picture held to it","placement":"local","effect":"reads","stability":"stable","toolset":"core","cli":"wai upscale verify","mcp":null,"route":null,"call":"upscale_verify"},
|
||||
{"id":"media.tonemap","noun":"media","verb":"tonemap","menu":"Media","item":"Tonemap","palette":"media tonemap","summary":"Render a still's gain map exactly at the headrooms named, with a signed record a verifier recomputes","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai tonemap run","mcp":null,"route":null,"call":"media_tonemap"},
|
||||
{"id":"tonemap.verify","noun":"tonemap","verb":"verify","menu":"Tonemap","item":"Verify","palette":"tonemap verify","summary":"A rendition record recomputed from its envelope, and the renditions held to it","placement":"local","effect":"reads","stability":"stable","toolset":"core","cli":"wai tonemap verify","mcp":null,"route":null,"call":"tonemap_verify"},
|
||||
{"id":"score.build","noun":"score","verb":"build","menu":"Score","item":"Build","palette":"score build","summary":"A WSCR score from integer note fields","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai score build","mcp":null,"route":null,"call":"score_build"},
|
||||
{"id":"score.decode","noun":"score","verb":"decode","menu":"Score","item":"Decode","palette":"score decode","summary":"A score's reference mixdown and its digests (pcm_sha256, mixdown_blake3, score_hash)","placement":"local","effect":"writes-project","stability":"stable","toolset":"core","cli":"wai score render","mcp":null,"route":null,"call":"score_decode"},
|
||||
{"id":"encoder.list","noun":"encoder","verb":"list","menu":"Encoder","item":"List","palette":"encoder list","summary":"What each encoder interface offers on this device, for AVC and HEVC","placement":"local","effect":"reads","stability":"preview","toolset":"core","cli":"wai encoders","mcp":null,"route":null,"call":"encoder_list"},
|
||||
|
|
|
|||
Loading…
Reference in a new issue