"""Export JPEG-AI decoder weights from the ISO reference ONNX assets. The byte-exact decoder in `wai-rs` (`jpegai_synthesis`, `jpegai_pipeline`, `jpegai_icci`) is driven by weights lifted straight out of the reference `jpegai-vm-assets` ONNX files. Two regimes, and using the wrong one silently produces plausible-but-wrong output: float nets — synthesis (model_y / model_uv), hyper_decoder, MCM stages. Exported as 16-bit per-tensor symmetric PTQ: conv weight [O, I/g, kh, kw] -> int16 + per-tensor mult = round(scale * 2^31), shift = 31 bias / LayerNorm / temperature -> Q16 (round(v * 2^16)) This is what `jpegai_synthesis::Weights::from_json` expects. integer net — hyper_scale_decoder. Already integer end to end (int32 conv weights, integer-valued Pow exponents); exported VERBATIM with no quantization, for `jpegai_synthesis::IntNet::from_json`. Quantizing it would corrupt an exactly-representable net. Reading the file is dependency-free: ONNX is protobuf, and we only need the initializer table, so there is no `onnx` / `protobuf` / `torch` requirement. ModelProto.graph = 7 GraphProto.initializer = 5 TensorProto{ dims=1, data_type=2, name=8, raw_data=9 } data_type: 1 = float32, 6 = int32, 7 = int64 Usage ----- # float net -> Q16 PTQ (synthesis / hyper_decoder / MCM stage) python wai_jpegai_export_weights.py ptq synthesis16.onnx synth16_y.json # integer net -> verbatim python wai_jpegai_export_weights.py int hyper_scale_decoder.onnx hs_y.json # just look at what is inside python wai_jpegai_export_weights.py list model_15.onnx Caveat worth knowing: the eICCI post-filter models under `post_filters/` export the model's `forward`, which the reference decoder never calls (it calls `process_dwt444_2` + `process_comp`), and their initializers are BatchNorm-folded and incomplete — no `BNY`, no `scaleU`/`scaleV`, no per-residual-block `scale`. For eICCI take the reference's PyTorch `state_dict` instead; `list` is still useful there for inspection. """ import json import struct import sys from collections import OrderedDict SHIFT = 31 # conv requant shift, matches int_transformer::conv_grouped Q16 = 1 << 16 # bias / LN / temperature fixed-point scale INT16_MAX = 32767 # ---- dependency-free protobuf reader ---- def _varint(buf, i): shift = value = 0 while True: byte = buf[i] i += 1 value |= (byte & 0x7F) << shift if not byte & 0x80: return value, i shift += 7 def _fields(buf): """Yield (field_number, wire_type, payload) for one protobuf message.""" i = 0 while i < len(buf): tag, i = _varint(buf, i) field, wire = tag >> 3, tag & 7 if wire == 0: value, i = _varint(buf, i) yield field, 0, value elif wire == 2: length, i = _varint(buf, i) yield field, 2, buf[i:i + length] i += length elif wire == 1: yield field, 1, buf[i:i + 8] i += 8 elif wire == 5: yield field, 5, buf[i:i + 4] i += 4 else: raise ValueError(f"unsupported wire type {wire}") def _parse_tensor(buf): dims, name, dtype, raw = [], "", 0, b"" for field, wire, payload in _fields(buf): if field == 1: if wire == 0: dims.append(payload) elif wire == 2: # packed dims j = 0 while j < len(payload): d, j = _varint(payload, j) dims.append(d) elif field == 2 and wire == 0: dtype = payload elif field == 8 and wire == 2: name = payload.decode("utf-8", "replace") elif field == 9 and wire == 2: raw = payload return name, dims, dtype, raw def initializers(path): """name -> (dims, data_type, raw_bytes) for every initializer, in file order.""" out = OrderedDict() model = open(path, "rb").read() for field, wire, payload in _fields(model): if field == 7 and wire == 2: # ModelProto.graph for gfield, gwire, gpayload in _fields(payload): if gfield == 5 and gwire == 2: # GraphProto.initializer name, dims, dtype, raw = _parse_tensor(gpayload) out[name] = (dims, dtype, raw) return out def _prod(xs): n = 1 for x in xs: n *= x return n def _decode(dims, dtype, raw): """raw bytes -> list of numbers, or None if the tensor is empty/unsupported.""" n = _prod(dims) if not raw: return None if dtype == 1 and len(raw) == 4 * n: return list(struct.unpack(f"<{n}f", raw)) if dtype == 6 and len(raw) == 4 * n: return list(struct.unpack(f"<{n}i", raw)) if dtype == 7 and len(raw) == 8 * n: return list(struct.unpack(f"<{n}q", raw)) return None # ---- export modes ---- def export_ptq(src, dst, strip_prefix=True): """Float net -> 16-bit symmetric PTQ, the layout `Weights::from_json` reads.""" import re result = OrderedDict() n_conv = n_scalar = total = 0 worst = 0.0 for name, (dims, dtype, raw) in initializers(src).items(): values = _decode(dims, dtype, raw) if values is None or dtype != 1: continue total += len(values) # the reference wraps modules as `hop_.inst.`; the decoder # addresses them by the bare module path key = re.sub(r"^hop_\w+\.inst\.", "", name) if strip_prefix else name if len(dims) == 4 and dims[0] > 1: # conv weight [O, I/g, kh, kw] amax = max((abs(v) for v in values), default=0.0) or 1e-12 scale = amax / INT16_MAX quant = [max(-32768, min(INT16_MAX, round(v / scale))) for v in values] worst = max(worst, max(abs(v - round(v / scale) * scale) for v in values)) result[key] = {"kind": "conv", "dims": dims, "shift": SHIFT, "mult": round(scale * (1 << SHIFT)), "w": quant} n_conv += 1 else: # bias / LN gamma,beta / temperature result[key] = {"kind": "q16", "dims": dims, "v": [round(v * Q16) for v in values]} n_scalar += 1 json.dump(result, open(dst, "w")) print(f"{n_conv} conv + {n_scalar} scalar tensors ({total:,} params) -> {dst}") print(f"conv round-trip worst |error| = {worst:.3e} " f"({'lossless at 16 bits' if worst < 1e-3 else 'CHECK'})") def export_int(src, dst): """Integer net -> verbatim. Pow exponents are integer-valued floats.""" result = OrderedDict() for name, (dims, dtype, raw) in initializers(src).items(): values = _decode(dims, dtype, raw) if values is None: continue if dtype == 1: # integer-valued float (Pow exponent) values = [int(round(v)) for v in values] result[name] = {"dims": dims, "v": values} json.dump(result, open(dst, "w")) print(f"{len(result)} integer tensors -> {dst}") for name, entry in result.items(): lo, hi = min(entry["v"]), max(entry["v"]) print(f" {name:24s} dims={entry['dims']} range=[{lo}, {hi}]") def list_tensors(src): dtypes = {1: "f32", 6: "i32", 7: "i64"} total = 0 for name, (dims, dtype, raw) in initializers(src).items(): n = _prod(dims) total += n print(f" {name:38s} {dtypes.get(dtype, dtype):4s} dims={dims}") print(f"{total:,} params") def main(argv): if len(argv) < 3: print(__doc__) return 1 mode, src = argv[1], argv[2] if mode == "list": list_tensors(src) elif mode == "ptq": export_ptq(src, argv[3]) elif mode == "int": export_int(src, argv[3]) else: print(f"unknown mode {mode!r}; expected list | ptq | int") return 1 return 0 if __name__ == "__main__": sys.exit(main(sys.argv))