From c2a62912af870a38426030635067eada449d6650 Mon Sep 17 00:00:00 2001 From: RuurdKuiper Date: Tue, 6 Oct 2026 16:53:41 +0200 Subject: [PATCH] Add Qwen3.8-27B WebGPU recurrent-state generation example --- js/README.md | 2 + js/qwen38-webgpu/.gitignore | 6 + js/qwen38-webgpu/LICENSE | 202 ++ js/qwen38-webgpu/README.md | 47 + js/qwen38-webgpu/download-model.mjs | 36 + js/qwen38-webgpu/export/.gitignore | 5 + js/qwen38-webgpu/export/README.md | 27 + js/qwen38-webgpu/export/export.py | 60 + .../export/requirements-export.lock | 41 + .../export/scripts/catalog_entry.py | 23 + .../export/scripts/check_adaptation.py | 47 + .../export/scripts/inspect_model.py | 55 + .../export/scripts/prepare_browser.py | 79 + .../export/scripts/shard_model.py | 77 + .../export/scripts/streaming_export.py | 167 ++ js/qwen38-webgpu/export/scripts/validate.py | 67 + js/qwen38-webgpu/export/setup.sh | 5 + js/qwen38-webgpu/index.html | 12 + js/qwen38-webgpu/model.json | 142 ++ js/qwen38-webgpu/package-lock.json | 2115 +++++++++++++++++ js/qwen38-webgpu/package.json | 23 + js/qwen38-webgpu/src/inference.worker.js | 284 +++ js/qwen38-webgpu/src/main.js | 54 + js/qwen38-webgpu/test-browser.mjs | 28 + js/qwen38-webgpu/validation.json | 69 + js/qwen38-webgpu/vite.config.js | 26 + 26 files changed, 3699 insertions(+) create mode 100644 js/qwen38-webgpu/.gitignore create mode 100644 js/qwen38-webgpu/LICENSE create mode 100644 js/qwen38-webgpu/README.md create mode 100644 js/qwen38-webgpu/download-model.mjs create mode 100644 js/qwen38-webgpu/export/.gitignore create mode 100644 js/qwen38-webgpu/export/README.md create mode 100644 js/qwen38-webgpu/export/export.py create mode 100644 js/qwen38-webgpu/export/requirements-export.lock create mode 100644 js/qwen38-webgpu/export/scripts/catalog_entry.py create mode 100644 js/qwen38-webgpu/export/scripts/check_adaptation.py create mode 100644 js/qwen38-webgpu/export/scripts/inspect_model.py create mode 100644 js/qwen38-webgpu/export/scripts/prepare_browser.py create mode 100644 js/qwen38-webgpu/export/scripts/shard_model.py create mode 100644 js/qwen38-webgpu/export/scripts/streaming_export.py create mode 100644 js/qwen38-webgpu/export/scripts/validate.py create mode 100644 js/qwen38-webgpu/export/setup.sh create mode 100644 js/qwen38-webgpu/index.html create mode 100644 js/qwen38-webgpu/model.json create mode 100644 js/qwen38-webgpu/package-lock.json create mode 100644 js/qwen38-webgpu/package.json create mode 100644 js/qwen38-webgpu/src/inference.worker.js create mode 100644 js/qwen38-webgpu/src/main.js create mode 100644 js/qwen38-webgpu/test-browser.mjs create mode 100644 js/qwen38-webgpu/validation.json create mode 100644 js/qwen38-webgpu/vite.config.js diff --git a/js/README.md b/js/README.md index dcd93fbd3..5093cb2e0 100644 --- a/js/README.md +++ b/js/README.md @@ -52,3 +52,5 @@ Click links for README of each examples. * [Phi-3-mini-4k-instruct](chat) - demonstrates how to run [Phi-3-mini-4k-instruct](https://huggingface.co/microsoft/Phi-3-mini-4k-instruct) in your browser using [onnxruntime-web](https://github.com/microsoft/onnxruntime/js) with webgpu. + +* [Qwen3.8-27B INT4](qwen38-webgpu) - demonstrates community-converted Qwen3.8-27B text generation with ONNX Runtime WebGPU, JSPI external-data loading and recurrent-state caching. diff --git a/js/qwen38-webgpu/.gitignore b/js/qwen38-webgpu/.gitignore new file mode 100644 index 000000000..3e18c3c38 --- /dev/null +++ b/js/qwen38-webgpu/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.models/ +results/ +.env +.DS_Store diff --git a/js/qwen38-webgpu/LICENSE b/js/qwen38-webgpu/LICENSE new file mode 100644 index 000000000..4dd2987ea --- /dev/null +++ b/js/qwen38-webgpu/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Ruurd Kuiper + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/js/qwen38-webgpu/README.md b/js/qwen38-webgpu/README.md new file mode 100644 index 000000000..3cf327b82 --- /dev/null +++ b/js/qwen38-webgpu/README.md @@ -0,0 +1,47 @@ +# Qwen3.8-27B in the browser with ONNX Runtime WebGPU + +A minimal text-generation example using a public **Qwen3.8-27B INT4** model pack, ONNX Runtime Web's **JSPI/Blob loader**, and hardware WebGPU. It runs independently of Clinstruct. All model operators execute locally on GPU, with CPU inference fallback disabled; JavaScript handles tokenization, sequence metadata and greedy sampling. + +**Weights: approximately 15.26 GB. Tested on a 48 GiB Apple Silicon Mac, Chrome 154, Metal 3 WebGPU.** Earlier integrated tests measured about 5–6 tokens/s; the standalone 32-token smoke test measured about 10 tokens/s. See [validation.json](validation.json) for exact timings. Other hardware and long contexts are unvalidated. This is a runnable engineering example, not a quality benchmark or an official Microsoft/Alibaba release. + +## Run + +Install Node.js ≥20.19. Use current Chrome or Edge with hardware WebGPU, `shader-f16` and WebAssembly JSPI, in an ordinary browser profile. + +```sh +git clone https://github.com/RuurdKuiper/qwen38-27b-onnx-webgpu.git +cd qwen38-27b-onnx-webgpu +npm ci +npm run download:model +npm run dev -- --port 5174 +``` + +Open `http://127.0.0.1:5174`, click **Load model**, then generate. The download script gets the [public Hugging Face pack](https://huggingface.co/Ruurd/Qwen3.8-27B-ONNX-WebGPU-INT4) at the immutable commit in `model.json` and verifies file sizes/SHA-256 checksums. Re-running it preserves verified completed shards. The local server only serves static assets; it runs no inference. Prompts remain in the browser. + +If the model is already installed elsewhere: + +```sh +MODEL_DIR=/absolute/path/to/model-pack npm run dev -- --port 5174 +``` + +`npm run build` bundles the application and pinned runtime, excluding weights. To deploy that build, serve the model pack separately at `/models/onnx/Qwen3.8-27B-GenAI-WebGPU-INT4/` on the same secure origin. Never bundle the weights into the application ZIP. + +## Model and generation contract + +This export requires the complete graph/state contract, not only `input_ids → logits`. The worker inspects `manifest.json` and `genai_config.json` and retains **128 state tensors on GPU**: 32 KV, 48 convolution and 48 recurrent. Each prompt starts fresh state; decoding retains state between tokens. Generation is batch-one contiguous unpadded text, 32 new tokens maximum in this UI, temperature zero, and thinking disabled. Qwen's three-axis position IDs are supplied explicitly. + +The native graph is preserved. `model.browser.onnx` replaces six known attention-mask metadata operations with host `seqlens_k` and `total_sequence_length` inputs to avoid CPU placement; weights and model computation are unchanged relative to the native export. INT4 block size is 32; both MatMul and Gather (untied embeddings) are quantized. Vision, MTP, paged attention, speculative decoding and CUDA-specific GatedDeltaNet are excluded. + +The example reports model bytes, download/load time, first-token latency, decoding speed, available JS heap statistics, GPU adapter/features and runtime warnings. Heap statistics exclude GPU allocations. The loader uses `onnxruntime-web@1.31.0-dev.20260914-8d85527a0`; Transformers.js 4.3.0 supplies tokenization only. The tested 48 GiB machine is not a measured minimum requirement. Large Blob loading failed in an incognito test. + +## Reproduce and test + +`export/` contains the pinned Python exporter environment and the official-builder wrapper. The memory-bounded serializer is an optional adapter around the official DEFAULT quantizer; it handles large intermediate weights and emits ≤1 GiB shards without changing quantization arithmetic. See `export/README.md` for commands and architecture limitations. + +With Vite running, `npm run test:browser` launches installed Chrome in a fresh ordinary profile, blocks external requests, checks arithmetic, ≥20 consecutive generated tokens, strict GPU execution and all 128 retained state tensors, and writes `results/browser.json`. For another origin set `DEMO_URL`. + +## Attribution + +Qwen3.8-27B is copyright 2026 Alibaba Cloud under Apache 2.0. The model repository includes its `LICENSE` and `NOTICE`. This browser example and conversion tooling are copyright 2026 Ruurd Kuiper under Apache 2.0; dependencies retain their own licenses. The worker originates from the tested Clinstruct integration and is reproduced here so other applications can use the same generation path. + +Upstream projects: [ONNX Runtime](https://github.com/microsoft/onnxruntime), [ONNX Runtime GenAI](https://github.com/microsoft/onnxruntime-genai), [Transformers.js](https://github.com/huggingface/transformers.js), [original checkpoint](https://huggingface.co/Qwen/Qwen3.8-27B). diff --git a/js/qwen38-webgpu/download-model.mjs b/js/qwen38-webgpu/download-model.mjs new file mode 100644 index 000000000..e74a25d73 --- /dev/null +++ b/js/qwen38-webgpu/download-model.mjs @@ -0,0 +1,36 @@ +// Download immutable public assets; the browser performs all inference locally. +import { createHash } from 'node:crypto'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { mkdir, readFile, rename, rm, stat } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { Readable, Transform } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +const config = JSON.parse(await readFile(new URL('./model.json', import.meta.url), 'utf8')); +const root = resolve(process.env.MODEL_DIR ?? `.models/${config.id}`); +await mkdir(root, { recursive: true }); +async function checksum(file) { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(file)) hash.update(chunk); + return hash.digest('hex'); +} +for (const file of config.files) { + if (!/^[A-Za-z0-9_.-]+$/.test(file.name) || ['.', '..'].includes(file.name)) throw new Error('Unexpected model filename.'); + const destination = resolve(root, file.name); + try { + if ((await stat(destination)).size === file.bytes && await checksum(destination) === file.sha256) { + console.log('Verified existing', file.name); continue; + } + } catch (error) { if (error.code !== 'ENOENT') throw error; } + const partial = destination + '.part'; + console.log('Downloading', file.name, file.bytes, 'bytes'); + const response = await fetch(`https://huggingface.co/${config.repo}/resolve/${config.revision}/${file.name}?download=true`); + if (!response.ok) throw new Error(`HTTP ${response.status}: ${file.name}`); + const hash = createHash('sha256'); let bytes = 0; + const verify = new Transform({ transform(chunk, encoding, callback) { bytes += chunk.length; hash.update(chunk); callback(null, chunk); } }); + try { + await pipeline(Readable.fromWeb(response.body), verify, createWriteStream(partial)); + if (bytes !== file.bytes || hash.digest('hex') !== file.sha256) throw new Error(`Integrity check failed: ${file.name}`); + await rename(partial, destination); + } catch (error) { await rm(partial, { force: true }); throw error; } +} +console.log('Verified model pack ready:', root); diff --git a/js/qwen38-webgpu/export/.gitignore b/js/qwen38-webgpu/export/.gitignore new file mode 100644 index 000000000..a25b00592 --- /dev/null +++ b/js/qwen38-webgpu/export/.gitignore @@ -0,0 +1,5 @@ +.venv/ +.work/ +node_modules/ +__pycache__/ +*.log diff --git a/js/qwen38-webgpu/export/README.md b/js/qwen38-webgpu/export/README.md new file mode 100644 index 000000000..5394d0326 --- /dev/null +++ b/js/qwen38-webgpu/export/README.md @@ -0,0 +1,27 @@ +# Reproduce the conversion + +Run from this repository's root. Requires `uv` and space for the ~55.56 GB original checkpoint, ~15.26 GB output and temporary files. The recorded export used a 48 GiB Mac. These dependencies pin the official builder/runtime tested on 6 October 2026. + +```sh +bash export/setup.sh +export/.venv/bin/python export/export.py \ + --model Qwen/Qwen3.8-27B \ + --revision 1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0 \ + --bounded \ + --output .models/Qwen3.8-27B-GenAI-WebGPU-INT4 +``` + +The command downloads the pinned checkpoint, exports INT4 with WebGPU, runs offline native GenAI sanity checks, adapts the known attention-mask metadata subgraph and emits the browser manifest. The native CPU check does not serve inference to the browser. `--skip-native` skips it. + +`--bounded` retains the official builder and DEFAULT weight-only quantizer, but serializes/quantizes one weight at a time to avoid retaining the whole FP16 intermediate model during serialization. The original baseline was killed while saving. The adapter copies independent protobuf descriptors, handles >2 GiB intermediate weights through temporary external files, quantizes both MatMul/Gather, and emits ≤1 GiB tensor-aligned shards. It matched all 491 initializer shapes/dtypes/hashes and graph nodes in a 2B cross-check against the ordinary exporter. This internal adapter is tied to the pinned builder version. + +Defaults are INT4 block size 32, decoder only, `exclude_mtp=true`, `state_window=0`, ordinary non-paged linear attention and MatMul/Gather quantization. No CUDA kernels, vision, MTP, speculative decoding or paged attention. Output must be empty; use a new directory for a retry. Reports are beside the output in `model-name.reports/`. + +The original/native graph is retained. The browser adapter removes exactly six known mask metadata nodes and rejects unfamiliar subgraphs. Different architectures need their own input/output/state and position-ID inspection and WebGPU operator validation; the current worker implements the Qwen graph contract. Optional offline graph/state equivalence check: + +```sh +export/.venv/bin/python export/scripts/check_adaptation.py \ + .models/Qwen3.8-27B-GenAI-WebGPU-INT4 +``` + +Serve the example and run `npm run test:browser` to test the result. The model's configured context length is not a measured browser capacity. Reproducible numerical serialization does not establish BF16 quality parity or compatibility with arbitrary GPUs. diff --git a/js/qwen38-webgpu/export/export.py b/js/qwen38-webgpu/export/export.py new file mode 100644 index 000000000..ea2c0c8db --- /dev/null +++ b/js/qwen38-webgpu/export/export.py @@ -0,0 +1,60 @@ +"""Reproduce the tested GenAI INT4/WebGPU export into a self-contained pack.""" +import argparse +import json +import os +from pathlib import Path +import subprocess +import sys + +# Set before importing huggingface_hub: ordinary HTTP avoids the observed Xet stall. +os.environ.setdefault('HF_HUB_DISABLE_XET', '1') +os.environ.setdefault('HF_HUB_DOWNLOAD_TIMEOUT', '120') +os.environ['PYTHONUNBUFFERED'] = '1' +from huggingface_hub import model_info, snapshot_download + +TOOL = Path(__file__).resolve().parent +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--model', required=True, help='Official Hugging Face checkpoint ID') +parser.add_argument('--revision', help='Resolve and record HEAD if omitted') +parser.add_argument('--output', type=Path, required=True, help='A new/empty model-pack directory') +parser.add_argument('--cache', type=Path, default=TOOL / '.work/cache') +parser.add_argument('--bounded', action='store_true', help='Use the validated per-weight serializer for large models') +parser.add_argument('--skip-native', action='store_true', help='Skip offline CPU sanity validation') +parser.add_argument('--extra-option', action='append', default=[], help='Additional builder key=value, e.g. block_size=32') +args = parser.parse_args() +root = args.output.resolve() +if root.exists() and any(root.iterdir()): + parser.error('Output must be empty. Use a new directory or remove the failed output before retrying.') +root.mkdir(parents=True, exist_ok=True) +notes = root.parent / (root.name + '.reports') +notes.mkdir(parents=True, exist_ok=True) +info = model_info(args.model, revision=args.revision) +snapshot = snapshot_download(args.model, revision=info.sha, cache_dir=str(args.cache.resolve()), + allow_patterns=['*.json', '*.jinja', '*.safetensors', '*.txt'], max_workers=8) +metadata = {'model': args.model, 'revision': info.sha, 'precision': 'int4', 'provider': 'webgpu', + 'boundedSerializer': args.bounded, 'extraOptions': args.extra_option} +(root / 'export-info.json').write_text(json.dumps(metadata, indent=2) + '\n') + +def run(label, command): + print(f'\n{label}', flush=True) + with (notes / (label + '.log')).open('w') as log: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=notes) + for line in process.stdout: + print(line, end='', flush=True) + log.write(line) + code = process.wait() + if code: raise SystemExit(f'{label} failed ({code}); see {notes}') + +builder = [sys.executable, str(TOOL / 'scripts/streaming_export.py')] if args.bounded else [sys.executable, '-m', 'onnxruntime_genai.models.builder'] +# MatMul/Gather also quantizes untied input embeddings, essential for the 27B pack. +run('export', builder + ['-m', args.model, '-i', snapshot, '-o', str(root), '-p', 'int4', '-e', 'webgpu', + '-c', str(args.cache.resolve()), '--extra_options', 'exclude_mtp=true', 'state_window=0', + 'op_types_to_quantize=MatMul/Gather', *args.extra_option]) +if not args.skip_native: + run('native-validation', [sys.executable, str(TOOL / 'scripts/validate.py'), str(root)]) +# Keep small exports' original external-data layout; split oversized baseline files. +if any(p.stat().st_size >= 2**31 for p in root.glob('*.onnx.data')): + run('sharding', [sys.executable, str(TOOL / 'scripts/shard_model.py'), str(root)]) +run('browser-adaptation', [sys.executable, str(TOOL / 'scripts/prepare_browser.py'), str(root)]) +run('inspection', [sys.executable, str(TOOL / 'scripts/inspect_model.py'), str(root)]) +print(f'\nReady for browser validation: {root}\nReports: {notes}') diff --git a/js/qwen38-webgpu/export/requirements-export.lock b/js/qwen38-webgpu/export/requirements-export.lock new file mode 100644 index 000000000..43bfc739a --- /dev/null +++ b/js/qwen38-webgpu/export/requirements-export.lock @@ -0,0 +1,41 @@ +annotated-doc==0.0.5 +anyio==4.15.1 +certifi==2026.7.22 +click==8.5.0 +filelock==4.0.12 +flatbuffers==25.12.19 +fsspec==2026.9.0 +h11==0.16.0 +hf-xet==1.6.0 +httpcore==1.0.9 +httpx==0.28.1 +huggingface-hub==1.33.0 +idna==3.20 +jinja2==3.1.6 +markdown-it-py==4.2.0 +markupsafe==3.0.4 +mdurl==0.1.2 +ml-dtypes==0.6.0 +mpmath==1.3.0 +networkx==3.7 +numpy==2.5.3 +onnx==1.23.2 +onnx-ir==1.0.0 +onnxruntime==1.30.0 +onnxruntime-genai==0.17.1 +packaging==26.3 +protobuf==7.36.2 +pygments==2.21.0 +pyyaml==6.0.3 +regex==2026.9.29 +rich==15.0.0 +safetensors==0.8.0 +setuptools==84.0.0 +shellingham==1.5.4 +sympy==1.14.0 +tokenizers==0.23.2 +torch==2.14.1 +tqdm==4.70.1 +transformers==5.18.0 +typer==0.27.2 +typing-extensions==4.16.0 diff --git a/js/qwen38-webgpu/export/scripts/catalog_entry.py b/js/qwen38-webgpu/export/scripts/catalog_entry.py new file mode 100644 index 000000000..ed508a8d0 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/catalog_entry.py @@ -0,0 +1,23 @@ +"""Print a catalog entry for a newly exported pack; review it before adding it.""" +import argparse +import json +from pathlib import Path + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('model_dir', type=Path) +parser.add_argument('--id', required=True) +parser.add_argument('--label', required=True) +parser.add_argument('--family', required=True) +parser.add_argument('--parameters', type=float, required=True) +parser.add_argument('--license', required=True) +parser.add_argument('--license-url', required=True) +args = parser.parse_args() +manifest = json.loads((args.model_dir / 'manifest.json').read_text()) +info = json.loads((args.model_dir / 'export-info.json').read_text()) +assert manifest.get('browserGraph'), 'Prepare/inspect the browser graph first.' +entry = {'id': args.id, 'label': args.label, 'family': args.family, 'parametersB': args.parameters, + 'repo': info['model'], 'revision': info['revision'], 'dtype': 'q4f16', 'runtime': 'genai', + 'loader': 'jspi', 'source': 'local-export', 'downloadMb': round(manifest['totalBytes'] / 1e6), + 'licenseName': args.license, 'licenseUrl': args.license_url, + 'files': [f['name'] for f in manifest['files']] + ['manifest.json']} +print(json.dumps(entry, indent=2)) diff --git a/js/qwen38-webgpu/export/scripts/check_adaptation.py b/js/qwen38-webgpu/export/scripts/check_adaptation.py new file mode 100644 index 000000000..17bbdceb9 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/check_adaptation.py @@ -0,0 +1,47 @@ +"""Compare original/adapted graph logits and every cache after prefill/decode.""" +import json +import sys +from pathlib import Path + +import numpy as np +import onnxruntime as ort + +root = Path(sys.argv[1]).resolve() +notes = root.parent / (root.name + ".reports") +notes.mkdir(parents=True, exist_ok=True) +config = json.loads((root / "genai_config.json").read_text()) +manifest = json.loads((root / "manifest.json").read_text()) +graph = next(g for g in manifest["graphs"] if g["file"] == config["model"]["decoder"]["filename"]) +original = ort.InferenceSession(str(root / graph["file"]), providers=["CPUExecutionProvider"]) +adapted = ort.InferenceSession(str(root / "model.browser.onnx"), providers=["CPUExecutionProvider"]) +reference = json.loads((notes / "native-validation-cpu.json").read_text())["tests"][0] +symbols = {"batch_size": 1, "past_sequence_length": 0, "kv_cache_dim": config["model"]["decoder"]["head_size"]} +state = {} +for value in graph["inputs"]: + if value["name"].startswith("past"): + shape = [symbols.get(d, d) for d in value["dims"]] + state[value["name"]] = np.zeros(shape, dtype=np.float16) +ids = reference["input_ids"] +past = 0 +checks = [] +for step in range(2): + total = past + len(ids) + feeds = {**state, "input_ids": np.asarray([ids], dtype=np.int64), + "position_ids": np.tile(np.arange(past, total, dtype=np.int64), (3, 1, 1)), + "attention_mask": np.ones((1, total), dtype=np.int64)} + original_outputs = dict(zip([v.name for v in original.get_outputs()], original.run(None, feeds))) + browser_feeds = {k: v for k, v in feeds.items() if k != "attention_mask"} + browser_feeds.update(seqlens_k=np.asarray([total - 1], dtype=np.int32), + total_sequence_length=np.asarray(total, dtype=np.int32)) + browser_outputs = dict(zip([v.name for v in adapted.get_outputs()], adapted.run(None, browser_feeds))) + equal = {name: np.array_equal(value, browser_outputs[name]) for name, value in original_outputs.items()} + assert all(equal.values()), f"Graph adaptation changed outputs: {equal}" + checks.append({"step": step, "inputTokens": len(ids), "outputsCompared": len(equal), "allBitwiseEqual": True}) + for name in state: + output = name.replace("past_key_values.", "present.").replace("past.", "present.") + state[name] = original_outputs[output] + ids = [int(np.argmax(original_outputs["logits"][0, -1]))] + past = total +result = {"status": "passed", "provider": "cpu", "purpose": "offline graph-adaptation equivalence check", "checks": checks} +(notes / "adaptation-equivalence.json").write_text(json.dumps(result, indent=2) + "\n") +print(json.dumps(result, indent=2)) diff --git a/js/qwen38-webgpu/export/scripts/inspect_model.py b/js/qwen38-webgpu/export/scripts/inspect_model.py new file mode 100644 index 000000000..0ef28a138 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/inspect_model.py @@ -0,0 +1,55 @@ +"""Read graph metadata without loading external weights; emit a browser manifest.""" +import json +import sys +from collections import Counter +from pathlib import Path + +import onnx + +root = Path(sys.argv[1]).resolve() +notes = root.parent / (root.name + ".reports") +notes.mkdir(parents=True, exist_ok=True) + +def tensor_info(value): + tensor = value.type.tensor_type + return { + "name": value.name, + "type": onnx.TensorProto.DataType.Name(tensor.elem_type).lower(), + "dims": [d.dim_param if d.dim_param else d.dim_value for d in tensor.shape.dim], + } + +graphs = [] +for path in sorted(root.glob("*.onnx")): + model = onnx.load(path, load_external_data=False) + data_files = set() + for tensor in model.graph.initializer: + for item in tensor.external_data: + if item.key == "location": + data_files.add(item.value) + graphs.append({ + "file": path.name, + "inputs": [tensor_info(v) for v in model.graph.input], + "outputs": [tensor_info(v) for v in model.graph.output], + "externalData": sorted(data_files), + "operators": dict(sorted(Counter(f"{n.domain or 'ai.onnx'}::{n.op_type}" for n in model.graph.node).items())), + "opsets": {v.domain or "ai.onnx": v.version for v in model.opset_import}, + "int4": [{"name": n.name, "bits": onnx.helper.get_attribute_value(next(a for a in n.attribute if a.name == "bits")), + "blockSize": onnx.helper.get_attribute_value(next(a for a in n.attribute if a.name == "block_size"))} + for n in model.graph.node if n.op_type == "MatMulNBits"], + "largestInitializers": sorted( + [{"name": t.name, "dims": list(t.dims), "type": onnx.TensorProto.DataType.Name(t.data_type).lower(), + "externalBytes": int(next((v.value for v in t.external_data if v.key == "length"), "0"))} + for t in model.graph.initializer], key=lambda t: t["externalBytes"], reverse=True)[:10], + }) +files = [{"name": str(p.relative_to(root)), "bytes": p.stat().st_size} for p in sorted(root.rglob("*")) if p.is_file() and p.name != "manifest.json"] +manifest = {"files": files, "totalBytes": sum(f["bytes"] for f in files), "graphs": graphs} +if (root / "browser-adaptation.json").exists(): + manifest["browserAdaptation"] = json.loads((root / "browser-adaptation.json").read_text()) + manifest["browserGraph"] = manifest["browserAdaptation"]["browserGraph"] +(root / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") +(notes / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") +for file in files: + print(f"{file['bytes']:>14,} bytes {file['name']}") +print(f"Total: {manifest['totalBytes']:,} bytes") +for graph in graphs: + print(json.dumps(graph, indent=2)) diff --git a/js/qwen38-webgpu/export/scripts/prepare_browser.py b/js/qwen38-webgpu/export/scripts/prepare_browser.py new file mode 100644 index 000000000..98f63626a --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/prepare_browser.py @@ -0,0 +1,79 @@ +"""Expose GQA sequence metadata as host inputs; leave model math/weights intact. + +This adapter only supports batch size one, contiguous unpadded text. It removes +the six attention-mask metadata nodes that ORT explicitly places on CPU. The +original GenAI export is retained for native validation. +""" +import json +import sys +from pathlib import Path + +import onnx +from onnx import TensorProto, helper + +root = Path(sys.argv[1]).resolve() +notes = root.parent / (root.name + ".reports") +notes.mkdir(parents=True, exist_ok=True) +config = json.loads((root / "genai_config.json").read_text()) +original = config["model"]["decoder"]["filename"] +model = onnx.load(root / original, load_external_data=False) +prefix = "/model/attn_mask_reformat/attn_mask_subgraph/" +nodes = [n for n in model.graph.node if n.name.startswith(prefix)] +expected = {"ReduceSum", "Sub", "Sub/Cast", "Shape", "Gather", "Gather/Cast"} +assert {n.name.removeprefix(prefix) for n in nodes} == expected, "Unknown mask subgraph; inspect before adapting" +replacements = { + prefix + "Sub/Cast/output_0": "seqlens_k", + prefix + "Gather/Cast/output_0": "total_sequence_length", +} +removed_outputs = {o for n in nodes for o in n.output} +remaining = [n for n in model.graph.node if n not in nodes] +gqa = [n for n in remaining if n.op_type == "GroupQueryAttention"] +assert gqa and all(n.input[5] in replacements and n.input[6] in replacements for n in gqa) +for node in remaining: + for index, name in enumerate(node.input): + if name in replacements: + node.input[index] = replacements[name] + else: + assert name not in removed_outputs, f"Unexpected mask-subgraph consumer: {node.name}" +del model.graph.node[:] +model.graph.node.extend(remaining) +mask = config["model"]["decoder"]["inputs"]["attention_mask"] +inputs = [v for v in model.graph.input if v.name != mask] +inputs.extend([ + helper.make_tensor_value_info("seqlens_k", TensorProto.INT32, ["batch_size"]), + helper.make_tensor_value_info("total_sequence_length", TensorProto.INT32, []), +]) +del model.graph.input[:] +model.graph.input.extend(inputs) +values = [v for v in model.graph.value_info if v.name not in removed_outputs] +del model.graph.value_info[:] +model.graph.value_info.extend(values) +browser = "model.browser.onnx" +onnx.save_model(model, root / browser) +# ORT's exporter uses an ai.onnx SimplifiedLayerNormalization extension that the +# generic ONNX checker may not recognize. Require the same failure on the untouched +# export; semantic validation is performed by check_adaptation.py through ORT. +checker_warning = None +try: + onnx.checker.check_model(str(root / browser)) +except onnx.checker.ValidationError as error: + try: + onnx.checker.check_model(str(root / original)) + except onnx.checker.ValidationError as baseline: + if str(error).split("\n")[0] != str(baseline).split("\n")[0]: + raise + checker_warning = str(error).split("\n")[0] + else: + raise +adaptation = { + "originalGraph": original, "browserGraph": browser, + "removedNodes": [{"name": n.name, "op": n.op_type} for n in nodes], + "newInputs": {"seqlens_k": "int32[1] = total tokens processed including this step - 1", + "total_sequence_length": "int32 scalar = total tokens processed including this step"}, + "restriction": "batch size 1, contiguous unpadded text only", + "weightsModified": False, + "onnxCheckerWarningAlsoPresentInOriginal": checker_warning, +} +(root / "browser-adaptation.json").write_text(json.dumps(adaptation, indent=2) + "\n") +(notes / "browser-adaptation.json").write_text(json.dumps(adaptation, indent=2) + "\n") +print(json.dumps(adaptation, indent=2)) diff --git a/js/qwen38-webgpu/export/scripts/shard_model.py b/js/qwen38-webgpu/export/scripts/shard_model.py new file mode 100644 index 000000000..ebb2d6e87 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/shard_model.py @@ -0,0 +1,77 @@ +"""Repack an official single-file export into <=1 GiB tensor-aligned shards. + +Numerical tensors and graph nodes are unchanged. Verify every copied byte before +publishing the new graph and removing the now-unreferenced original data file. +""" +import hashlib +import json +import sys +import os +from pathlib import Path +import onnx +from onnx import external_data_helper + +root = Path(sys.argv[1]).resolve() +notes = root.parent / (root.name + ".reports") +notes.mkdir(parents=True, exist_ok=True) +model_path = root / 'model.onnx' +model = onnx.load(model_path, load_external_data=False) +locations = {entry.value for tensor in model.graph.initializer for entry in tensor.external_data if entry.key == 'location'} +assert locations == {'model.onnx.data'}, 'Expected an unsharded export; do not run this on an already sharded model.' +limit = 1024**3 +original_files = set() +shards = [] +writer = None +index = -1 +checks = [] +try: + for tensor in model.graph.initializer: + ext = {entry.key: entry.value for entry in tensor.external_data} + if 'location' not in ext: + continue + original_files.add(ext['location']) + length = int(ext['length']) + if length > limit: + raise ValueError(f'Tensor exceeds shard limit: {tensor.name}, {length}') + padding = (-writer.tell()) % 64 if writer else 0 + if writer is None or writer.tell() + padding + length > limit: + if writer: writer.close() + index += 1 + location = f'model.onnx.data_{index:03d}' + shards.append(location) + writer = (root / location).open('wb') + padding = 0 + writer.write(b'\0' * padding) + offset = writer.tell() + digest = hashlib.sha256() + with (root / ext['location']).open('rb') as source: + source.seek(int(ext.get('offset', 0))) + remaining = length + while remaining: + data = source.read(min(remaining, 8 * 1024 * 1024)) + if not data: raise EOFError(tensor.name) + writer.write(data); digest.update(data); remaining -= len(data) + tensor.raw_data = b'' + external_data_helper.set_external_data(tensor, location=location, offset=offset, length=length) + tensor.ClearField('raw_data') + tensor.data_location = onnx.TensorProto.EXTERNAL + checks.append({'tensor': tensor.name, 'file': location, 'offset': offset, + 'bytes': length, 'sha256': digest.hexdigest()}) +finally: + if writer: writer.close() +for check in checks: + digest = hashlib.sha256() + with (root / check['file']).open('rb') as source: + source.seek(check['offset']); remaining = check['bytes'] + while remaining: + data = source.read(min(remaining, 8 * 1024 * 1024)); assert data + digest.update(data); remaining -= len(data) + assert digest.hexdigest() == check['sha256'], check['tensor'] +onnx.save_model(model, root / 'model.sharded.part.onnx') +(root / 'model.sharded.part.onnx').replace(model_path) +for name in original_files: + if name not in shards: (root / name).unlink() +report = {'tensorBytesVerified': sum(check['bytes'] for check in checks), 'tensors': checks, + 'shards': [{'file': name, 'bytes': (root / name).stat().st_size} for name in shards]} +(notes / 'sharding.json').write_text(json.dumps(report, indent=2) + '\n') +print(json.dumps({'verifiedTensorCount': len(checks), 'shards': report['shards']}, indent=2)) diff --git a/js/qwen38-webgpu/export/scripts/streaming_export.py b/js/qwen38-webgpu/export/scripts/streaming_export.py new file mode 100644 index 000000000..2c15cfdc7 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/streaming_export.py @@ -0,0 +1,167 @@ +"""Memory-bounded save adapter around the official builder and ORT quantizer. + +Only use if the baseline exporter exceeds this host's memory ceiling. This +serializes/quantizes one weight at a time with the official DEFAULT quantizer, +writing quantized bytes directly to their final external-data file. No model +operators, quantization settings or inference kernels are replaced. +""" +import gc +import importlib.util +import json +import os +import runpy +import sys +import math +from pathlib import Path + +import onnx +import numpy as np +import onnx_ir as ir +from onnx import helper, numpy_helper, external_data_helper +from onnxruntime.quantization.matmul_nbits_quantizer import DefaultWeightOnlyQuantizer, DefaultWeightOnlyQuantConfig, QuantFormat + +source = Path(importlib.util.find_spec('onnxruntime_genai.models.builder').origin).parent +sys.path.insert(0, str(source)) +from builders.base import Model + + +def memory_bounded_save(self, out_dir): + assert self.onnx_dtype == ir.DataType.INT4 and self.ep == 'webgpu' + assert getattr(self, 'quantization_algo', 'default') == 'default' + assert not getattr(self, 'int4_customized_weight_config', {}) + assert not getattr(self, 'exact_quant_overrides', {}) + assert not self.quant_attrs['use_qdq'] + self.model.graph.sort() + Model.stamp_build_metadata(self.model) + originals = {name: value.const_value for name, value in self.model.graph.initializers.items()} + # Serialize graph metadata without materializing the full FP16 model. + for name, value in self.model.graph.initializers.items(): + tensor = originals[name] + value.const_value = ir.ExternalTensor('pending', 0, 0, tensor.dtype, + shape=tensor.shape, name=name) + proto = ir.to_proto(self.model) + for name, value in self.model.graph.initializers.items(): + value.const_value = originals[name] + del proto.graph.initializer[:] + nodes = list(proto.graph.node) + del proto.graph.node[:] + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + shard_limit = int(os.environ.get('SHARD_BYTES', 1024 ** 3)) + shard_index = 0 + location = self.filename + f'.data_{shard_index:03d}' + shard_paths = [out / location] + stored = {} + records = [] + + with (out / location).open('wb') as data_file: + def store(tensor): + nonlocal data_file, location, shard_index + if tensor.name in stored: + return + raw = tensor.raw_data or numpy_helper.to_array(tensor).tobytes() + if len(raw) > shard_limit: + raise ValueError(f'Tensor exceeds shard limit: {tensor.name}, {len(raw)} bytes') + padding = (-data_file.tell()) % 64 + if data_file.tell() + padding + len(raw) > shard_limit: + data_file.close() + shard_index += 1 + location = self.filename + f'.data_{shard_index:03d}' + shard_paths.append(out / location) + data_file = (out / location).open('wb') + padding = 0 + data_file.write(b'\0' * padding) + offset = data_file.tell() + data_file.write(raw) + length = len(raw) + if not tensor.HasField('raw_data'): + tensor.raw_data = raw + external_data_helper.set_external_data(tensor, location=location, offset=offset, length=length) + tensor.ClearField('raw_data') + for field in ['float_data', 'int32_data', 'int64_data', 'double_data', 'uint64_data', 'string_data']: + tensor.ClearField(field) + tensor.data_location = onnx.TensorProto.EXTERNAL + # A child protobuf retains its parent graph's allocation arena. + # Keeping the mini-graph initializer would therefore retain the + # original FP16 payload even after ClearField('raw_data'). Copy only + # the small external-data descriptor into an independent message. + descriptor = onnx.TensorProto() + descriptor.CopyFrom(tensor) + stored[tensor.name] = descriptor + + excluded = set(self.quant_attrs['nodes_to_exclude']) + ops = set(self.quant_attrs['op_types_to_quantize']) + for index, node in enumerate(nodes): + if node.op_type not in ops or node.name in excluded: + proto.graph.node.append(node) + continue + weight_name = node.input[0 if node.op_type == 'Gather' else 1] + if weight_name not in originals: + proto.graph.node.append(node) + continue + tensor = originals[weight_name] + temporary_weight = None + elements = math.prod(int(dim) for dim in tensor.shape) + if elements * tensor.dtype.bitwidth // 8 >= 2**31: + # Protobuf cannot copy a >2 GiB message into a GraphProto. + # The official quantizer can consume external-data descriptors. + temporary_weight = out / '.quantize-weight.tmp' + array = np.ascontiguousarray(tensor.numpy()) + array.tofile(temporary_weight) + del array + weight = ir.to_proto(ir.ExternalTensor( + str(temporary_weight), 0, temporary_weight.stat().st_size, + tensor.dtype, shape=tensor.shape, name=weight_name)) + else: + weight = ir.to_proto(tensor) + mini = helper.make_graph([node], 'one_weight', [], [], [weight]) + del weight + quantizer = DefaultWeightOnlyQuantizer(DefaultWeightOnlyQuantConfig( + bits=self.quant_attrs['bits'], block_size=self.quant_attrs['matmul_block_size'], + is_symmetric=self.quant_attrs['is_symmetric'], + accuracy_level=self.quant_attrs['accuracy_level'], + quant_format=QuantFormat.QOperator, op_types_to_quantize=tuple(ops))) + result_nodes = quantizer.quantize(node, [mini]) + if temporary_weight: + temporary_weight.unlink() + assert all(n.op_type != node.op_type for n in result_nodes), node.name + proto.graph.node.extend(result_nodes) + result = mini + used = {name for n in result_nodes for name in n.input} + for value in result.initializer: + if value.name in used: + store(value) + records.append({'name': node.name, 'weight': weight_name, 'shardBytes': data_file.tell()}) + print(f'Stream-quantized {node.name} ({index + 1}/{len(nodes)}), {data_file.tell():,} bytes', flush=True) + # LazyTensor caches its cast result; release the temporary FP16 copy. + if isinstance(tensor, ir.LazyTensor): + tensor._tensor = None + del mini, quantizer, result, result_nodes, tensor + gc.collect() + + # Emit only initializers still referenced by the transformed graph. + used = {name for node in proto.graph.node for name in node.input} + for name in sorted(used): + if name in originals and name not in stored: + tensor = originals[name] + store(ir.to_proto(tensor)) + if isinstance(tensor, ir.LazyTensor): + tensor._tensor = None + del tensor + gc.collect() + proto.graph.initializer.extend(stored.values()) + data_file.close() + onnx.save_model(proto, out / self.filename) + notes = out.parent / (out.name + ".reports") + notes.mkdir(parents=True, exist_ok=True) + (notes / "streaming-quantization.json").write_text(json.dumps({ + 'adapter': 'per-weight serialization with official DEFAULT MatMulNBitsQuantizer', + 'settings': self.quant_attrs, 'quantizedNodes': records, + 'externalBytes': sum(path.stat().st_size for path in shard_paths), + 'shards': [{'file': path.name, 'bytes': path.stat().st_size} for path in shard_paths], + }, indent=2, default=str) + '\n') + + +Model.save_model = memory_bounded_save +if __name__ == '__main__': + runpy.run_path(str(source / 'builder.py'), run_name='__main__') diff --git a/js/qwen38-webgpu/export/scripts/validate.py b/js/qwen38-webgpu/export/scripts/validate.py new file mode 100644 index 000000000..555e5c6b6 --- /dev/null +++ b/js/qwen38-webgpu/export/scripts/validate.py @@ -0,0 +1,67 @@ +"""Independent native GenAI sanity check. CPU here is validation, never serving.""" +import json +import os +import argparse +import time +from pathlib import Path + +import numpy as np +import onnxruntime_genai as og +from transformers import AutoTokenizer + +parser = argparse.ArgumentParser() +parser.add_argument("model_dir", type=Path) +parser.add_argument("--provider", choices=["cpu", "webgpu"], default="cpu") +args = parser.parse_args() +root = args.model_dir.resolve() +notes = root.parent / (root.name + ".reports") +notes.mkdir(parents=True, exist_ok=True) +os.chdir(notes) +result = {"provider": args.provider, "purpose": "pre-browser export validation", "tests": []} +try: + config = og.Config(str(root)) + config.clear_providers() + if args.provider != "cpu": + config.append_provider(args.provider) + model = og.Model(config) + tokenizer = AutoTokenizer.from_pretrained(root, local_files_only=True) + for prompt in ["What is the capital of France?", "What is 12 * 13? Answer with the number only."]: + input_ids = tokenizer.apply_chat_template( + [{"role": "user", "content": prompt}], tokenize=True, + add_generation_prompt=True, enable_thinking=False, return_dict=False, + ) + params = og.GeneratorParams(model) + params.set_search_options(do_sample=False, max_length=len(input_ids) + 32) + start = time.perf_counter() + generator = og.Generator(model, params) + generator.append_tokens(np.asarray(input_ids, dtype=np.int32)) + generated = [] + first = None + while not generator.is_done() and len(generated) < 32: + generator.generate_next_token() + generated.append(int(generator.get_next_tokens()[0])) + if first is None: + first = time.perf_counter() + end = time.perf_counter() + item = {"prompt": prompt, "input_ids": input_ids, "generated_ids": generated, + "text": tokenizer.decode(generated, skip_special_tokens=True), + "first_token_ms": (first - start) * 1000 if first else None, + "elapsed_ms": (end - start) * 1000} + result["tests"].append(item) + print(json.dumps(item, indent=2), flush=True) + del generator + result["checks"] = { + "capital": "paris" in result["tests"][0]["text"].lower(), + "arithmetic": "156" in result["tests"][1]["text"], + } + if not all(result["checks"].values()): + raise RuntimeError("Native generation completed but failed the answer sanity checks") + result["status"] = "passed" +except Exception as error: + result["status"] = "failed" + result["error"] = f"{type(error).__name__}: {error}" + print(result["error"], flush=True) +finally: + (notes / f"native-validation-{args.provider}.json").write_text(json.dumps(result, indent=2) + "\n") +if result["status"] != "passed": + raise SystemExit(1) diff --git a/js/qwen38-webgpu/export/setup.sh b/js/qwen38-webgpu/export/setup.sh new file mode 100644 index 000000000..ce1f1bdc8 --- /dev/null +++ b/js/qwen38-webgpu/export/setup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +uv venv --python 3.12 .venv +uv pip install --python .venv/bin/python -r requirements-export.lock diff --git a/js/qwen38-webgpu/index.html b/js/qwen38-webgpu/index.html new file mode 100644 index 000000000..db1b88dc3 --- /dev/null +++ b/js/qwen38-webgpu/index.html @@ -0,0 +1,12 @@ + +Qwen3.8 27B · ONNX WebGPU + +

Qwen3.8 27B · ONNX Runtime WebGPU

+

Text generation on your GPU. Model download: approximately 15.26 GB. Tested on a 48 GiB Mac with Chrome; other hardware and long contexts are unvalidated.

+ +

+ +
Download the model pack first; see README.md. Use current Chrome or Edge on localhost with hardware WebGPU and JSPI support.
+
Measurements
+ + diff --git a/js/qwen38-webgpu/model.json b/js/qwen38-webgpu/model.json new file mode 100644 index 000000000..f6cd574f3 --- /dev/null +++ b/js/qwen38-webgpu/model.json @@ -0,0 +1,142 @@ +{ + "id": "Qwen3.8-27B-GenAI-WebGPU-INT4", + "repo": "Ruurd/Qwen3.8-27B-ONNX-WebGPU-INT4", + "revision": "1030027de90ff1d54cefc66a23f253667723a938", + "files": [ + { + "name": "LICENSE", + "bytes": 11544, + "sha256": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a" + }, + { + "name": "NOTICE", + "bytes": 1096, + "sha256": "4bc0702ac73ce597a7ac2d25d5d9593b48c736e8ccb4212b5c4166f88c160f67" + }, + { + "name": "README.md", + "bytes": 4458, + "sha256": "e1ded046cd423aed8cd49496016064e479ee9482b9de469f226803858b783ffd" + }, + { + "name": "SHA256SUMS", + "bytes": 2108, + "sha256": "76afd85e80a46a3b60a96af22fe159aeceb68642b404bef58ae7b4f2a0139935" + }, + { + "name": "browser-adaptation.json", + "bytes": 1124, + "sha256": "304421d2b3fdc54eb76cebb9c7039655924198f151ba3ad778bdc6e7b79700eb" + }, + { + "name": "chat_template.jinja", + "bytes": 8952, + "sha256": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041" + }, + { + "name": "genai_config.json", + "bytes": 2173, + "sha256": "ac9543ac591ee1471a728a7fbeb120d06ea1174d6f1eb5ffb4790122c0494797" + }, + { + "name": "manifest.json", + "bytes": 218401, + "sha256": "d366474182d88ae15a47f0cc93b9baea2a3a8a097ff9814559ef716de7747c9e" + }, + { + "name": "model.browser.onnx", + "bytes": 963019, + "sha256": "451ed1a88c5ed901aed2efbc528197a6017b821383b82635bf0a2505f4df680b" + }, + { + "name": "model.onnx", + "bytes": 966226, + "sha256": "b89aab881977645fa4dce3553eafa2e84ac539f18476ea4869e5be99533f218a" + }, + { + "name": "model.onnx.data_000", + "bytes": 1045463040, + "sha256": "eb0fb83ff03b03cf92a7578a0e9a5dc59e814e6ad408699bd1508ec0e735620e" + }, + { + "name": "model.onnx.data_001", + "bytes": 1070530560, + "sha256": "435e4a756d6b522f8f1c748fde103a903a609ecdc542f1f79f9015c569ecb11c" + }, + { + "name": "model.onnx.data_002", + "bytes": 1064632320, + "sha256": "aba624cbf26c09ade578e6ad49627011c41a25dceb992e87abf8b26a462c55ce" + }, + { + "name": "model.onnx.data_003", + "bytes": 1070530560, + "sha256": "8afa21d25dbc18567dda5e6b6a584e9bed63ae73975e28580f1e806cad8468ef" + }, + { + "name": "model.onnx.data_004", + "bytes": 1070530560, + "sha256": "fe6ee327be43d16ba57b8c14574f35027bfd0dc2be501e44217788ef075192e3" + }, + { + "name": "model.onnx.data_005", + "bytes": 1070530560, + "sha256": "852a0322f1be70c71c4ed4f439cb5dbf743375c6ddf3091093599d78ad1ffe7d" + }, + { + "name": "model.onnx.data_006", + "bytes": 1064632320, + "sha256": "ab02ba6ef850cafd2ff155b9e26b47bd6f2ffde5ad341dacc8136a39539a848e" + }, + { + "name": "model.onnx.data_007", + "bytes": 1070530560, + "sha256": "f85375e8e18c2e5fe155355a06a173c3e227a044abc1528a7cac752d4b92ad08" + }, + { + "name": "model.onnx.data_008", + "bytes": 1070530560, + "sha256": "151b6dc1d97aaaf0c1e19a795be6e67bf1c756a90a058be3577d9d33b7cd6ff0" + }, + { + "name": "model.onnx.data_009", + "bytes": 1070530560, + "sha256": "2e3e90070d2ae7cbf0924e8307bbced2737110f00c3353668f3ec6db6d67b0c5" + }, + { + "name": "model.onnx.data_010", + "bytes": 1064632320, + "sha256": "d3437f3cd78a14ca8d566504664229ccf72041566b5860a37a664c1852d2c498" + }, + { + "name": "model.onnx.data_011", + "bytes": 1070530560, + "sha256": "fd0010a73b25e54acc4d4f4a4c6b67f63c60a733ac5fcbe83b8d513119b761ca" + }, + { + "name": "model.onnx.data_012", + "bytes": 1070530560, + "sha256": "2ef25f57544afa1b284e4bbd5492fb5440b2365212fa5f75e9753436cc674110" + }, + { + "name": "model.onnx.data_013", + "bytes": 524943360, + "sha256": "f12f01e346adb0ab37596a2a71da252afebf7a297ccf688014196f0e08fbffbc" + }, + { + "name": "model.onnx.data_014", + "bytes": 835739648, + "sha256": "18667e7b2ac51e38a4c0fd435ec8d5b8459b2476770ba28fee59f9089b2cdbcf" + }, + { + "name": "tokenizer.json", + "bytes": 19989339, + "sha256": "a5cd9732badce41de57e6efce8302930ded1c1188c5f81feb2bd6c24c4a1941f" + }, + { + "name": "tokenizer_config.json", + "bytes": 1465, + "sha256": "004893c9690cd050e617619eef34e6a13fe58ac4a3f4174f38eed5e408a89636" + } + ] +} diff --git a/js/qwen38-webgpu/package-lock.json b/js/qwen38-webgpu/package-lock.json new file mode 100644 index 000000000..70f8fb068 --- /dev/null +++ b/js/qwen38-webgpu/package-lock.json @@ -0,0 +1,2115 @@ +{ + "name": "qwen38-27b-onnx-webgpu", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qwen38-27b-onnx-webgpu", + "version": "0.1.0", + "dependencies": { + "@huggingface/transformers": "4.3.0", + "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0" + }, + "devDependencies": { + "playwright": "1.63.0", + "vite": "7.3.6" + }, + "engines": { + "node": ">=20.19" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.2.0.tgz", + "integrity": "sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.3.0.tgz", + "integrity": "sha512-fL1A/WUZwouPrOlYxU5dzIwD2T5J781JiB2jDR8bFe5DwCj0Gfudq+NEXCMno49kQgajHA7xQkrRLJlqG1veEA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.10", + "@huggingface/tokenizers": "^0.2.0", + "onnxruntime-node": "1.30.0", + "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0", + "sharp": "^0.35.4" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", + "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", + "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", + "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", + "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", + "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", + "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", + "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", + "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", + "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", + "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", + "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", + "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", + "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", + "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", + "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", + "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", + "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", + "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", + "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", + "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", + "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", + "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", + "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", + "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", + "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/adm-zip": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/global-agent": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", + "license": "BSD-3-Clause", + "dependencies": { + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.30.0.tgz", + "integrity": "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.30.0.tgz", + "integrity": "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.6.0", + "global-agent": "^4.1.3", + "onnxruntime-common": "1.30.0" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.31.0-dev.20260914-8d85527a0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.31.0-dev.20260914-8d85527a0.tgz", + "integrity": "sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.31.0-dev.20260911-2a43ec07e", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.31.0-dev.20260911-2a43ec07e", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.0-dev.20260911-2a43ec07e.tgz", + "integrity": "sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rollup": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz", + "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/js/qwen38-webgpu/package.json b/js/qwen38-webgpu/package.json new file mode 100644 index 000000000..606991d14 --- /dev/null +++ b/js/qwen38-webgpu/package.json @@ -0,0 +1,23 @@ +{ + "name": "qwen38-27b-onnx-webgpu", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "test:browser": "node test-browser.mjs", + "download:model": "node download-model.mjs" + }, + "dependencies": { + "@huggingface/transformers": "4.3.0", + "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0" + }, + "devDependencies": { + "vite": "7.3.6", + "playwright": "1.63.0" + }, + "engines": { + "node": ">=20.19" + } +} diff --git a/js/qwen38-webgpu/src/inference.worker.js b/js/qwen38-webgpu/src/inference.worker.js new file mode 100644 index 000000000..e934be6af --- /dev/null +++ b/js/qwen38-webgpu/src/inference.worker.js @@ -0,0 +1,284 @@ +import * as ort from 'onnxruntime-web/jspi'; +import wasmUrl from 'onnxruntime-web/ort-wasm-simd-threaded.jspi.wasm?url'; +import wasmModuleUrl from 'onnxruntime-web/ort-wasm-simd-threaded.jspi.mjs?url'; +import { AutoTokenizer, env } from '@huggingface/transformers'; + +// Batch-one text generation for the official GenAI export. All model operators +// run on WebGPU; only tokenization, sequence metadata and sampling run in JS. +let session, tokenizer, chatTemplate, config, graph, bindings, device, modelRoot; +let activeRequestId = 0; +const report = { runtimeVersion: ort.env.versions.web, executionProviders: ['webgpu'], cpuFallbackDisabled: true, messages: [] }; +const tick = () => new Promise(resolve => setTimeout(resolve, 0)); +function log(level, ...args) { + const text = args.map(String).join(' '); + report.messages.push({ level, text }); + if (level === 'info') self.postMessage({ type: 'progress', requestId: activeRequestId, message: text }); + else console[level](...args); +} +function heap() { return performance.memory ? { usedJSHeapSize: performance.memory.usedJSHeapSize } : null; } +async function json(url) { + const response = await fetch(url); + if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`); + return response.json(); +} +const modelUrl = file => `${modelRoot}/${file}`; +// Match layer-specific names using the generated config, rather than guessing +// that every decoder layer has an ordinary key/value cache. +function cacheBindings() { + const pairs = [ + ['past_key_names', 'present_key_names'], ['past_value_names', 'present_value_names'], + ['past_conv_names', 'present_conv_names'], ['past_recurrent_names', 'present_recurrent_names'], + ]; + const result = []; + const decoder = config.model.decoder; + for (const [past, present] of pairs) { + if (!decoder.inputs[past]) continue; + for (let layer = 0; layer < decoder.num_hidden_layers; layer++) { + const input = decoder.inputs[past].replace('%d', layer); + if (!session.inputNames.includes(input)) continue; + const output = decoder.outputs[present].replace('%d', layer); + if (!session.outputNames.includes(output)) throw new Error(`Missing output for ${input}: ${output}`); + result.push({ input, output, metadata: graph.inputs.find((v) => v.name === input) }); + } + } + const supported = new Set([...result.map((v) => v.input), decoder.inputs.input_ids, + decoder.inputs.attention_mask, decoder.inputs.position_ids, 'seqlens_k', 'total_sequence_length']); + for (const name of session.inputNames) { + if (!supported.has(name)) throw new Error(`Unimplemented graph input: ${name}`); + } + return result; +} + +async function loadModel(modelId) { + modelRoot = `/models/onnx/${encodeURIComponent(modelId)}`; + if (session) return; + const loadStart = performance.now(); + if (typeof WebAssembly.Suspending !== 'function') throw new Error('This large model requires WebAssembly JSPI. Use current Chrome or Edge.'); + report.status = 'loading'; + log('info', 'Preparing WebGPU device…'); + if (!navigator.gpu) throw new Error('WebGPU unavailable. Use current Chrome or Edge on localhost.'); + const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' }); + if (!adapter || adapter.info.isFallbackAdapter) throw new Error('A hardware WebGPU adapter is required.'); + if (!adapter.features.has('shader-f16')) throw new Error('This FP16/INT4 export requires shader-f16.'); + const requiredFeatures = ['shader-f16']; + if (adapter.features.has('subgroups')) requiredFeatures.push('subgroups'); + const requiredLimits = { + maxBufferSize: adapter.limits.maxBufferSize, + maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, + }; + device = await adapter.requestDevice({ requiredFeatures, requiredLimits }); + device.addEventListener('uncapturederror', (e) => log('error', 'WebGPU:', e.error.message)); + device.lost.then((info) => log('error', 'WebGPU device lost:', info.reason, info.message)); + report.gpu = { vendor: adapter.info.vendor, architecture: adapter.info.architecture, + device: adapter.info.device, description: adapter.info.description, + isFallbackAdapter: adapter.info.isFallbackAdapter, + adapterFeatures: [...adapter.features], deviceFeatures: [...device.features], + deviceLimits: requiredLimits }; + ort.env.webgpu.device = device; + ort.env.wasm.numThreads = 1; + ort.env.wasm.wasmPaths = { wasm: wasmUrl, mjs: wasmModuleUrl }; + ort.env.logLevel = 'warning'; + + const manifest = await json(modelUrl('manifest.json')); + config = await json(modelUrl('genai_config.json')); + const graphFile = manifest.browserGraph; + if (!graphFile) throw new Error("Browser-adapted graph missing. Run prepare_browser.py and inspect_model.py."); + graph = manifest.graphs.find((g) => g.file === graphFile); + if (!graph) throw new Error('Decoder graph missing from manifest. Run scripts/inspect_model.py.'); + if (manifest.graphs.some((g) => g.file.includes('mtp')) || config.model.mtp) throw new Error('Export only the decoder with exclude_mtp=true.'); + report.graphFile = graphFile; + report.browserAdaptation = graphFile !== config.model.decoder.filename ? manifest.browserAdaptation : null; + report.exportBytes = manifest.totalBytes; + report.files = manifest.files; + const downloadStart = performance.now(); + const externalData = []; + let payloadBytes = 0; + // Fetch sequentially; retain exact external locations from the ONNX graph. + for (const path of graph.externalData) { + log('info', 'Downloading', path); + const response = await fetch(modelUrl(path)); + if (!response.ok) throw new Error(`Weight download failed: ${response.status}`); + const data = await response.blob(); + const expectedBytes = manifest.files.find(file => file.name === path)?.bytes; + if (expectedBytes !== undefined && data.size !== expectedBytes) throw new Error(`Incomplete weight file: ${path}`); + payloadBytes += data.size; + externalData.push({ path, data }); + } + const response = await fetch(modelUrl(graph.file)); + if (!response.ok) throw new Error(`Graph download failed: ${response.status}`); + const modelBytes = new Uint8Array(await response.arrayBuffer()); + payloadBytes += modelBytes.byteLength; + report.graphAndWeightDownloadBytes = payloadBytes; + report.downloadMs = performance.now() - downloadStart; + const preferredOutputLocation = Object.fromEntries(graph.outputs.map((v) => + [v.name, v.name === config.model.decoder.outputs.logits ? 'cpu' : 'gpu-buffer'])); + log('info', 'Creating WebGPU inference session…'); + const sessionStart = performance.now(); + session = await ort.InferenceSession.create(modelBytes, { + executionProviders: ['webgpu'], externalData, preferredOutputLocation, + freeDimensionOverrides: { batch_size: 1 }, + // Explicitly reject fallback to WASM CPU inference. + extra: { session: { disable_cpu_ep_fallback: '1' } }, + logSeverityLevel: 2, + }); + report.sessionCreateMs = performance.now() - sessionStart; + bindings = cacheBindings(); + report.cacheTensorCount = bindings.length; + report.graphInputs = session.inputNames; + report.graphOutputs = session.outputNames; + + env.allowRemoteModels = false; + env.allowLocalModels = true; + env.useBrowserCache = false; + env.localModelPath = '/models/onnx/'; + tokenizer = await AutoTokenizer.from_pretrained(modelId, { local_files_only: true }); + const templateResponse = await fetch(modelUrl('chat_template.jinja')); + if (!templateResponse.ok) throw new Error('Exported chat_template.jinja is missing.'); + chatTemplate = await templateResponse.text(); + report.loadMs = performance.now() - loadStart; + report.modelResources = performance.getEntriesByType('resource') + .filter((r) => new URL(r.name).pathname.startsWith(modelRoot + '/')) + .map((r) => ({ name: new URL(r.name).pathname, decodedBodySize: r.decodedBodySize, transferSize: r.transferSize })); + report.downloadedModelResourceBytes = report.modelResources.reduce((n, r) => n + r.decodedBodySize, 0); + report.heapAfterLoad = heap(); + report.status = 'ready'; +} + +function typedTensor(type, values, dims) { + const constructors = { int64: BigInt64Array, int32: Int32Array, float16: Uint16Array, + float: Float32Array, float32: Float32Array }; + const Constructor = constructors[type]; + if (!Constructor) throw new Error(`Unsupported graph tensor type: ${type}`); + const data = typeof values === 'number' ? new Constructor(values) : + Constructor.from(values, type === 'int64' ? BigInt : (v) => v); + return new ort.Tensor(type === 'float' ? 'float32' : type, data, dims); +} +function initialState(metadata) { + const symbols = { batch_size: 1, past_sequence_length: 0, kv_cache_dim: config.model.decoder.head_size }; + const dims = metadata.dims.map((v) => { + if (typeof v === 'number') return v; + if (v in symbols) return symbols[v]; + throw new Error(`Unresolved state dimension: ${v}`); + }); + return typedTensor(metadata.type, dims.reduce((a, b) => a * b, 1), dims); +} +function halfToFloat(bits) { + const sign = bits & 0x8000 ? -1 : 1; + const exponent = (bits >> 10) & 31; + const fraction = bits & 1023; + if (exponent === 31) return fraction ? NaN : sign * Infinity; + return sign * (exponent ? 2 ** (exponent - 15) * (1 + fraction / 1024) : 2 ** -14 * fraction / 1024); +} +async function sample(logits, temperature = 0) { + const data = await logits.getData(); + const vocab = logits.dims.at(-1); + const offset = data.length - vocab; + let best = -Infinity, token = -1; + const scores = new Float32Array(vocab); + for (let i = 0; i < vocab; i++) { + const value = logits.type === 'float16' && data instanceof Uint16Array ? halfToFloat(data[offset + i]) : data[offset + i]; + scores[i] = value; + if (Number.isNaN(value)) throw new Error('NaN in logits.'); + if (value > best) { best = value; token = i; } + } + if (token < 0 || !Number.isFinite(best)) throw new Error('Invalid logits.'); + if (temperature > 0) { + let total = 0; + for (let i = 0; i < vocab; i++) total += scores[i] = Math.exp((scores[i] - best) / temperature); + let target = Math.random() * total; + for (let i = 0; i < vocab; i++) if ((target -= scores[i]) <= 0) return i; + } + return token; +} + +async function generate({ messages, maxTokens, temperature, enableThinking }) { + if (!Number.isInteger(maxTokens) || maxTokens < 1 || maxTokens > 1024) throw new Error("maxTokens must be between 1 and 1024."); + if (!Number.isFinite(temperature) || temperature < 0 || temperature > 2) throw new Error("Invalid temperature."); + if (!session || !tokenizer) throw new Error('Load the model first.'); + const inputIds = tokenizer.apply_chat_template(messages, { + tokenize: true, return_tensor: false, return_dict: false, + add_generation_prompt: true, enable_thinking: enableThinking, chat_template: chatTemplate, + }); + if (!Array.isArray(inputIds) || !inputIds.every(Number.isInteger)) throw new Error('Unexpected tokenizer output.'); + if (inputIds.length + maxTokens > config.model.context_length) throw new Error("Prompt exceeds model context length."); + const decoder = config.model.decoder; + const byName = Object.fromEntries(graph.inputs.map((v) => [v.name, v])); + let state = Object.fromEntries(bindings.map((v) => [v.input, initialState(v.metadata)])); + let tokens = inputIds, pastLength = 0, firstTokenAt; + const generated = [], steps = []; + const start = performance.now(); + const eos = new Set([config.model.eos_token_id].flat()); + try { + for (let i = 0; i < maxTokens; i++) { + const feeds = { ...state }; + const ephemeral = []; + const put = (name, values, dims) => { + if (!name || !session.inputNames.includes(name)) return; + const tensor = typedTensor(byName[name].type, values, dims); + feeds[name] = tensor; ephemeral.push(tensor); + }; + put(decoder.inputs.input_ids, tokens, [1, tokens.length]); + put(decoder.inputs.attention_mask, Array(pastLength + tokens.length).fill(1), [1, pastLength + tokens.length]); + put('seqlens_k', [pastLength + tokens.length - 1], [1]); + put('total_sequence_length', [pastLength + tokens.length], []); + const positions = tokens.map((_, j) => pastLength + j); + put(decoder.inputs.position_ids, [...positions, ...positions, ...positions], [3, 1, tokens.length]); + let outputs; + try { outputs = await session.run(feeds); } + finally { for (const tensor of ephemeral) tensor.dispose(); } + try { + const token = await sample(outputs[decoder.outputs.logits], temperature); + if (firstTokenAt === undefined) firstTokenAt = performance.now(); + generated.push(token); + const nextState = {}; + for (const binding of bindings) { + const value = outputs[binding.output]; + if (!value) throw new Error(`Missing state output: ${binding.output}`); + if (value.location !== 'gpu-buffer') throw new Error(`State left GPU: ${binding.output} (${value.location})`); + nextState[binding.input] = value; + } + steps.push({ step: i, inputTokens: tokens.length, pastLength, stateTensorsRetained: bindings.length, + allStateOnGPU: true, kvLength: nextState[bindings.find((v) => v.input.startsWith('past_key_values.')).input].dims[2] }); + for (const tensor of Object.values(state)) tensor.dispose(); + state = nextState; + pastLength += tokens.length; + tokens = [token]; + if (eos.has(token)) break; + } finally { + const retained = new Set(Object.values(state)); + for (const tensor of Object.values(outputs)) if (!retained.has(tensor)) tensor.dispose(); + } + await tick(); + } + const end = performance.now(); + const result = { inputIds, generatedIds: generated, + promptTokens: inputIds.length, completionTokens: generated.length, + text: tokenizer.decode(generated, { skip_special_tokens: true }), + generatedTokens: generated.length, firstTokenMs: firstTokenAt - start, + tokensPerSecondAfterFirst: generated.length > 1 ? (generated.length - 1) * 1000 / (end - firstTokenAt) : null, + elapsedMs: end - start, heap: heap(), steps }; + result.decodeTokensPerSecond = result.tokensPerSecondAfterFirst; + report.lastGeneration = result; + return result; + } finally { for (const tensor of Object.values(state)) tensor.dispose(); } +} + +// Serialize requests so cache/session ownership cannot overlap. +let queue = Promise.resolve(); +self.onmessage = ({ data }) => { + queue = queue.then(async () => { + activeRequestId = data.requestId; + try { + let value; + if (data.type === 'load') { + await loadModel(data.modelId); + value = report; + } else if (data.type === 'generate') { + value = await generate(data); + } else throw new Error('Unknown GenAI worker request.'); + self.postMessage({ type: 'result', requestId: data.requestId, value }); + } catch (error) { + self.postMessage({ type: 'error', requestId: data.requestId, error: String(error) }); + } + }); +}; diff --git a/js/qwen38-webgpu/src/main.js b/js/qwen38-webgpu/src/main.js new file mode 100644 index 000000000..7ef36eb4e --- /dev/null +++ b/js/qwen38-webgpu/src/main.js @@ -0,0 +1,54 @@ +const modelId = 'Qwen3.8-27B-GenAI-WebGPU-INT4'; +let worker, ready = false, busy = false, requestId = 0; +const pending = new Map(); +const el = id => document.getElementById(id); +function controls() { el('load').disabled = busy || ready; el('generate').disabled = busy || !ready; el('unload').disabled = !worker; } +function unload() { + worker?.terminate(); worker = null; ready = false; busy = false; + for (const { reject } of pending.values()) reject(new Error('Model unloaded.')); + pending.clear(); controls(); el('status').textContent = 'Model unloaded.'; +} +function createWorker() { + worker = new Worker(new URL('./inference.worker.js', import.meta.url), { type: 'module' }); + worker.onmessage = ({ data }) => { + if (data.type === 'progress') { el('status').textContent = data.message; return; } + const task = pending.get(data.requestId); if (!task) return; + pending.delete(data.requestId); + data.type === 'error' ? task.reject(new Error(data.error)) : task.resolve(data.value); + }; + worker.onerror = event => { + for (const { reject } of pending.values()) reject(new Error(event.message)); + pending.clear(); + }; +} +function request(message) { + return new Promise((resolve, reject) => { + const id = ++requestId; pending.set(id, { resolve, reject }); worker.postMessage({ ...message, requestId: id }); + }); +} +async function load() { + if (busy || ready) return; + busy = true; controls(); createWorker(); + try { + const report = await request({ type: 'load', modelId }); + el('metrics').textContent = JSON.stringify(report, null, 2); ready = true; + el('status').textContent = 'Model ready. All model operators run on WebGPU; CPU inference fallback is disabled.'; + return report; + } catch (error) { unload(); el('status').textContent = String(error); throw error; } + finally { busy = false; controls(); } +} +async function generate(prompt = el('prompt').value) { + if (busy || !ready) throw new Error('Load the model first.'); + busy = true; controls(); el('status').textContent = 'Generating locally…'; + try { + const result = await request({ type: 'generate', messages: [{ role: 'user', content: prompt }], maxTokens: 32, temperature: 0, enableThinking: false }); + el('output').textContent = result.text; el('metrics').textContent = JSON.stringify(result, null, 2); + el('status').textContent = 'Generation complete.'; return result; + } catch (error) { el('status').textContent = String(error); throw error; } + finally { busy = false; controls(); } +} +el('load').onclick = () => void load().catch(() => {}); +el('generate').onclick = () => void generate().catch(() => {}); +el('unload').onclick = unload; +window.qwenDemo = { load, generate, unload }; +controls(); diff --git a/js/qwen38-webgpu/test-browser.mjs b/js/qwen38-webgpu/test-browser.mjs new file mode 100644 index 000000000..055874469 --- /dev/null +++ b/js/qwen38-webgpu/test-browser.mjs @@ -0,0 +1,28 @@ +import { chromium } from 'playwright'; +import { mkdir, writeFile } from 'node:fs/promises'; +const origin = process.env.DEMO_URL ?? 'http://127.0.0.1:5174'; +const context = await chromium.launchPersistentContext('', { channel: 'chrome', headless: true, ignoreDefaultArgs: ['--enable-unsafe-swiftshader'] }); +const report = { chrome: context.browser().version(), requests: [], warnings: [], tests: [] }; +try { + const page = context.pages()[0]; + page.on('console', m => { if (['warning', 'error'].includes(m.type())) report.warnings.push(m.text()); }); + page.on('pageerror', e => report.warnings.push(String(e))); + await page.route('**/*', route => { + const request = route.request(); report.requests.push({ url: request.url(), method: request.method() }); + return new URL(request.url()).origin === origin && ['GET', 'HEAD'].includes(request.method()) ? route.continue() : route.abort(); + }); + await page.goto(origin); + report.load = await page.evaluate(() => window.qwenDemo.load()); + for (const prompt of ['What is 12 * 13? Answer with the number only.', 'Explain how canals shaped Utrecht in at least forty words.']) { + report.tests.push(await page.evaluate(prompt => window.qwenDemo.generate(prompt), prompt)); + } + report.success = report.load.cpuFallbackDisabled && !report.load.gpu.isFallbackAdapter && report.load.cacheTensorCount === 128 + && report.tests[0].text.trim() === '156' && report.tests[1].completionTokens >= 20 + && report.tests.every(r => r.steps.every(s => s.allStateOnGPU && s.stateTensorsRetained === 128)); + await page.evaluate(() => window.qwenDemo.unload()); + console.log(JSON.stringify({ success: report.success, loadMs: report.load.loadMs, tests: report.tests.map(r => ({ text: r.text, tokens: r.completionTokens, tps: r.tokensPerSecondAfterFirst })) }, null, 2)); +} catch (error) { report.error = String(error); console.error(error); } +finally { + await mkdir('results', { recursive: true }); await writeFile('results/browser.json', JSON.stringify(report, null, 2) + '\n'); await context.close(); +} +if (!report.success) process.exitCode = 1; diff --git a/js/qwen38-webgpu/validation.json b/js/qwen38-webgpu/validation.json new file mode 100644 index 000000000..26e811819 --- /dev/null +++ b/js/qwen38-webgpu/validation.json @@ -0,0 +1,69 @@ +{ + "date": "2026-10-06", + "chrome": "154.0.8037.93", + "runtime": "1.31.0-dev.20260914-8d85527a0", + "hardware": { + "vendor": "apple", + "architecture": "metal-3", + "device": "", + "description": "", + "isFallbackAdapter": false, + "adapterFeatures": [ + "depth32float-stencil8", + "rg11b10ufloat-renderable", + "texture-formats-tier1", + "bgra8unorm-storage", + "texture-compression-bc", + "dual-source-blending", + "core-features-and-limits", + "float32-filterable", + "indirect-first-instance", + "texture-compression-astc-sliced-3d", + "float32-blendable", + "subgroup-size-control", + "texture-compression-astc", + "texture-compression-etc2", + "depth-clip-control", + "texture-compression-bc-sliced-3d", + "shader-f16", + "texture-formats-tier2", + "clip-distances", + "timestamp-query", + "primitive-index", + "texture-component-swizzle", + "subgroups" + ], + "deviceFeatures": [ + "core-features-and-limits", + "shader-f16", + "subgroups" + ], + "deviceLimits": { + "maxBufferSize": 4294967292, + "maxStorageBufferBindingSize": 4294967292 + } + }, + "packBytes": 15256750346, + "downloadBytes": 15235781067, + "loadMs": 18399.5, + "sessionCreateMs": 7218, + "cacheTensorCount": 128, + "cpuFallbackDisabled": true, + "success": true, + "tests": [ + { + "text": "156", + "completionTokens": 4, + "firstTokenMs": 1457.4000000022352, + "tokensPerSecondAfterFirst": 6.684491978654013, + "allStatesOnGpu": true + }, + { + "text": "Utrecht\u2019s urban layout was fundamentally defined by its canal system, which created a distinctive concentric ring structure. These waterways facilitated trade and transportation, allowing the", + "completionTokens": 32, + "firstTokenMs": 496.80000000074506, + "tokensPerSecondAfterFirst": 10.018744748241053, + "allStatesOnGpu": true + } + ] +} diff --git a/js/qwen38-webgpu/vite.config.js b/js/qwen38-webgpu/vite.config.js new file mode 100644 index 000000000..881e7f869 --- /dev/null +++ b/js/qwen38-webgpu/vite.config.js @@ -0,0 +1,26 @@ +import { createReadStream, statSync } from 'node:fs'; +import { resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; +const id = 'Qwen3.8-27B-GenAI-WebGPU-INT4'; +const modelDir = resolve(process.env.MODEL_DIR ?? fileURLToPath(new URL(`./.models/${id}/`, import.meta.url))); +export default defineConfig({ + plugins: [{ name: 'local-model-files', configureServer(server) { + server.middlewares.use((req, res, next) => { + const prefix = `/models/onnx/${id}/`, path = new URL(req.url, 'http://localhost').pathname; + if (!path.startsWith(prefix) || !['GET', 'HEAD'].includes(req.method)) return next(); + let file; + try { file = resolve(modelDir, decodeURIComponent(path.slice(prefix.length))); } + catch { res.writeHead(400).end(); return; } + if (!file.startsWith(modelDir + sep)) { res.writeHead(403).end(); return; } + let size; + try { const stat = statSync(file); if (!stat.isFile()) throw new Error(); size = stat.size; } + catch { res.writeHead(404).end('Model file missing; see README.md'); return; } + res.writeHead(200, { 'Content-Length': size, 'Content-Type': 'application/octet-stream', 'Cache-Control': 'no-store' }); + if (req.method === 'HEAD') { res.end(); return; } + const stream = createReadStream(file, { highWaterMark: 4 * 1024 * 1024 }); + stream.on('error', () => res.destroy()); res.on('close', () => stream.destroy()); stream.pipe(res); + }); + } }], + build: { target: 'es2022' }, +});