name: WAI JPEG-AI whole decode (pinned inputs, required) # WAI states that the exact decoder, `wai::jpegai_decode::decode`, decodes an # ISO/IEC 6048 JPEG-AI codestream to RGB deterministically, so a hard binding # taken over one decode verifies a second one, and that on one reference # codestream its output stays within max|Δ| ≤ 2 of the reference decoder's # (recorded max|Δ| = 1; wai/SPEC.md §5, the wai.image.jpegai row). # Across machines the registry states less: it classes wai.image.jpegai as # NeuralEntropyExact, a conservative class that guarantees only the entropy decode # (the decoded symbols and their entropy parameters) to be byte-identical on every # machine. That the exact decoder's pinned output is the same on other machines is # a measurement, recorded in wai/extensions/determinism-tiers.md §6. The tests that # carry these statements (tests/jpegai_e2e_full.rs, # tests/jpegai_reconstruction_binding.rs) read their inputs from outside the tree # and used to report PASS without them. # # This job fetches those inputs from WAI's versioned models root (jpegai/v1/), # checks every file against the SHA-256 manifest committed in # wai/tools/jpegai-e2e-v1.sha256, and runs both tests with # WAI_REQUIRE_SOTA_VECTORS=1, so a missing input is a failure. What passes here: # - the whole chain, codestream → RGB, through the one public `decode()` call, # lands within max|Δ| ≤ 2 of the reference decoder's output, and at most 5% # of samples differ from it at all (the test's bar); # - two decodes in one process are byte-identical and the reconstruction binding # verifies the second and rejects a one-byte change; # - the decoded pixels hash to WAI_JPEGAI_EXPECT_SHA256 below: the value measured # from these inputs on an Apple Silicon Mac (aarch64 macOS, native) and # reproduced by an x86_64 Linux build (glibc, run under QEMU user-mode # emulation), and the value the four-platform comparison in determinism-tiers # §6 (macOS arm64, wasm32-wasip2, Linux arm64, Linux x86_64 under user-mode # emulation) found on every platform. This runner is x86_64 hardware, so the # job adds a decode on one more machine to that measurement. A match is a # measurement on this runner, not something the registry class promises; a # mismatch is a departure from the measured agreement. # The pinned decode is max|Δ| = 1 from the reference on 4.446% of samples # (on the Mac and the emulated x86_64 build), so a hash match also confirms # that recorded max|Δ| = 1 for this codestream; the test's own tolerance # above is looser than that. # # What this job does not cover: # - Other codestreams: it decodes one 256×256 test codestream, so the # max|Δ| ≤ 2 bound and the determinism statement are checked on that input # only. # - The extraction: the inputs are an extraction of the JPEG-AI # reference-software assets (tools/wai_jpegai_build_e2e_inputs.py), which are # not in the repository, so the extraction is not re-run here; the manifest # pins its published bytes. # - The ONNX-runtime decode (`neural::jpegai`, tests/jpegai_live.rs), which needs # those local assets and is not run in CI. It also needs the `neural` feature, # whose prebuilt ONNX Runtime does not link on the `linux` runner's image # (glibc 2.35; the library references glibc 2.38 symbols), which is where # wai-neural-e2e.yml's runs fail once they reach the Rust build. The # features built here (neural_int, provenance) link no prebuilt native # library. # The job depends on the models root being reachable; an outage fails the job. # # The path filters list the source files the two tests' decode path imports, # among them src/float_repro.rs (the exact decoder's `exp` and its roundings to # an integer) and src/int_kernels.rs (the fast convolution and parallel-map # kernels the integer networks run through). An entry for a file the tree does # not have matches nothing until the file exists. A module the decode path # starts importing belongs here too; otherwise a change to that module alone is # first checked by the weekly run. # One run per branch at a time. A push supersedes the run before it. # # There is one `macos` runner and it is the dev Mac, so every macOS job in this # repo queues on it. With no concurrency group, a session pushing four versions # of a branch queued four complete sets of runs and every one of them had to # drain: on 2026-10-05 a PR's 21 contexts settled none in 70 minutes behind a # backlog of 25 runs, and the queue grew faster than it cleared. Only the newest # commit's verdict is wanted, so the older runs are cancelled. # # Verified on this forge (Forgejo 16.0.5) in `ci-test` before being added here: # the key parses, and a newer push moves an older run to `cancelled` even while # it is still queued, which is the case that matters. # # `cancel-in-progress: true` also applies to `main`. That is deliberate and # safe here: main advances by merge commits whose tree contains the commits # before them, so the newer run covers the older one's changes. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: push: branches: [main] paths: - 'wai/wai-rs/src/jpegai_*.rs' - 'wai/wai-rs/src/reconstruction_binding.rs' # The set-form pin over the exact bundle (tests/jpegai_e2e_full.rs) verifies # through the pin module. - 'wai/wai-rs/src/pin.rs' - 'wai/wai-rs/src/container.rs' - 'wai/wai-rs/src/int_transformer.rs' - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_kernels.rs' - 'wai/wai-rs/src/float_repro.rs' - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/rans.rs' - 'wai/wai-rs/src/lib.rs' - 'wai/wai-rs/Cargo.toml' - 'wai/wai-rs/Cargo.lock' - 'wai/wai-rs/tests/jpegai_e2e_full.rs' - 'wai/wai-rs/tests/jpegai_reconstruction_binding.rs' - 'wai/wai-rs/tests/jpegai_cross_platform.rs' - 'wai/wai-rs/tests/support/**' - 'wai/tools/wai_jpegai_fetch_e2e_inputs.sh' - 'wai/tools/jpegai-e2e-v1.sha256' - '.github/workflows/wai-jpegai-e2e.yml' pull_request: paths: - 'wai/wai-rs/src/jpegai_*.rs' - 'wai/wai-rs/src/reconstruction_binding.rs' # The set-form pin over the exact bundle (tests/jpegai_e2e_full.rs) verifies # through the pin module. - 'wai/wai-rs/src/pin.rs' - 'wai/wai-rs/src/container.rs' - 'wai/wai-rs/src/int_transformer.rs' - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_kernels.rs' - 'wai/wai-rs/src/float_repro.rs' - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/rans.rs' - 'wai/wai-rs/src/lib.rs' - 'wai/wai-rs/Cargo.toml' - 'wai/wai-rs/Cargo.lock' - 'wai/wai-rs/tests/jpegai_e2e_full.rs' - 'wai/wai-rs/tests/jpegai_reconstruction_binding.rs' - 'wai/wai-rs/tests/jpegai_cross_platform.rs' - 'wai/wai-rs/tests/support/**' - 'wai/tools/wai_jpegai_fetch_e2e_inputs.sh' - 'wai/tools/jpegai-e2e-v1.sha256' - '.github/workflows/wai-jpegai-e2e.yml' # Weekly, independent of changes: the inputs live outside the repository, and # their disappearing should surface here, not at the next unrelated PR. schedule: - cron: '41 6 * * 1' jobs: jpegai-e2e: name: fetch pinned JPEG-AI inputs → whole decode + pixel hash, required runs-on: linux timeout-minutes: 30 env: WAI_REQUIRE_SOTA_VECTORS: '1' WAI_JPEGAI_DIR: ${{ github.workspace }}/jpegai_e2e # sha256 of the decoded RGB8 pixels for the jpegai/v1 inputs (see header). WAI_JPEGAI_EXPECT_SHA256: dbc494a5a670914edf55c07c48afc0dd348cd05d3a82fff293bf384b2bb69ea3 steps: - uses: actions/checkout@v5 # Bootstrap rustup: the docker runner image ships no Rust. - name: Install Rust 1.98.0 shell: bash run: | export PATH="$HOME/.cargo/bin:$PATH" command -v rustup >/dev/null 2>&1 || \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 rustup toolchain install 1.98.0 --profile minimal rustup default 1.98.0 echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Fetch the JPEG-AI inputs and check each against the pinned manifest shell: bash working-directory: wai run: tools/wai_jpegai_fetch_e2e_inputs.sh "$WAI_JPEGAI_DIR" - name: Whole decode vs the reference, determinism, pinned pixel hash (missing input = failure) shell: bash working-directory: wai/wai-rs run: | set -o pipefail log="${RUNNER_TEMP:-/tmp}/jpegai-tests.log" # Without the switch both tests would skip and report ok. [ "${WAI_REQUIRE_SOTA_VECTORS:-}" = 1 ] \ || { echo "::error::WAI_REQUIRE_SOTA_VECTORS must be 1 in this job"; exit 1; } export WAI_JPEGAI_CODESTREAM="$WAI_JPEGAI_DIR/test256.bin" export WAI_JPEGAI_WEIGHTS_DIR="$WAI_JPEGAI_DIR" export WAI_JPEGAI_NORM="$WAI_JPEGAI_DIR/norm.json" # --show-output rather than --nocapture: each test's output is printed # after the results, so it cannot split a `test X ... ok` line below. cargo test --release --no-default-features --features neural_int,provenance \ --test jpegai_e2e_full --test jpegai_reconstruction_binding --test jpegai_cross_platform \ -- --show-output 2>&1 | tee "$log" # A test that was renamed or compiled out would not fail — it would just # not run. Require each one by name, and the pinned-hash check by its line. for t in codestream_to_rgb_matches_reference_png real_decode_to_reconstruction_binding \ decode_matches_pinned_hashes jpegai_e2e_set_pin_verifies; do grep -q "^test $t \.\.\. ok$" "$log" || { echo "::error::$t did not run and pass"; exit 1; } done grep -q "matches the pinned WAI_JPEGAI_EXPECT_SHA256" "$log" \ || { echo "::error::the pinned pixel hash was not checked"; exit 1; } # A skip also prints `test X ... ok`; every skip writes a banner with # "SKIPPED" and "NOT VERIFIED" (tests/support/require_vectors.rs). if grep -nE 'SKIPPED|NOT VERIFIED' "$log"; then echo "::error::a JPEG-AI test skipped instead of checking its input" exit 1 fi