diff --git a/.gitignore b/.gitignore index 16764c30..b7b36c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,15 @@ node_modules/ test/anonymous/.test-profile/ +# Vendored @huggingface/transformers — Chrome's build is committed +# (see src/chrome/vendor/transformers/README.md for versioning notes). +# Firefox's stays gitignored because the Firefox WebGPU provider is +# still a stub; once it's wired, copy the chrome vendor over and +# remove the firefox exclusion below. +src/firefox/vendor/transformers/*.js +src/firefox/vendor/transformers/*.wasm +src/firefox/vendor/transformers/*.mjs + # ---- SocialMediaDownloader test suite ---- # Cookies / session state if someone ever points the profile here smd-test-profile/ diff --git a/dist/webbrain-chrome-9.0.2.zip b/dist/webbrain-chrome-9.0.2.zip new file mode 100644 index 00000000..905b0148 Binary files /dev/null and b/dist/webbrain-chrome-9.0.2.zip differ diff --git a/dist/webbrain-firefox-9.0.2.zip b/dist/webbrain-firefox-9.0.2.zip new file mode 100644 index 00000000..987b33e3 Binary files /dev/null and b/dist/webbrain-firefox-9.0.2.zip differ diff --git a/manifest.json b/manifest.json index 794754bb..d8954ee0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WebBrain", - "version": "8.0.3", + "version": "9.0.10", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "sidePanel", diff --git a/package-lock.json b/package-lock.json index 16f33bd2..e826c7cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,702 @@ { "name": "webbrain", - "version": "8.0.3", + "version": "9.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webbrain", - "version": "8.0.3", + "version": "9.0.10", "license": "MIT", + "dependencies": { + "@huggingface/transformers": "^4.2.0" + }, "devDependencies": { "playwright": "^1.48.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", + "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "sharp": "^0.34.5" + } + }, + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "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.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "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/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "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/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, + "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/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.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -27,6 +712,151 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "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/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.26.0-dev.20260416-b7804b056c", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", + "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "license": "MIT" + }, + "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.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", @@ -58,6 +888,155 @@ "engines": { "node": ">=18" } + }, + "node_modules/protobufjs": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz", + "integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==", + "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/inquire": "^1.1.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/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "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.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 1fe2665f..a5f2b322 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webbrain", - "version": "8.0.3", + "version": "9.0.10", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "private": true, "type": "module", @@ -20,5 +20,8 @@ "type": "git", "url": "https://github.com/esokullu/webbrain.git" }, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@huggingface/transformers": "^4.2.0" + } } diff --git a/src/chrome/ARCHITECTURE.md b/src/chrome/ARCHITECTURE.md index bdde09b9..4fd2180b 100644 --- a/src/chrome/ARCHITECTURE.md +++ b/src/chrome/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Chrome Extension — Architecture -> Version 8.0.3 · Manifest V3 · Service Worker background +> Version 9.0.10 · Manifest V3 · Service Worker background ## High-Level Overview diff --git a/src/chrome/manifest.json b/src/chrome/manifest.json index bbbcd1f5..7a142f36 100644 --- a/src/chrome/manifest.json +++ b/src/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WebBrain", - "version": "8.0.3", + "version": "9.0.10", "description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.", "permissions": [ "sidePanel", @@ -29,6 +29,8 @@ "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src * data: blob:;" }, + "cross_origin_embedder_policy": { "value": "require-corp" }, + "cross_origin_opener_policy": { "value": "same-origin" }, "background": { "service_worker": "src/background.js", "type": "module" diff --git a/src/chrome/src/background.js b/src/chrome/src/background.js index 8c731418..2eb98931 100644 --- a/src/chrome/src/background.js +++ b/src/chrome/src/background.js @@ -597,6 +597,14 @@ async function handleMessage(msg, sender) { } } + case 'clear_webgpu_cache': { + const provider = providerManager.providers.get('webgpu_qwen3'); + if (!provider || typeof provider.clearCache !== 'function') { + return { ok: false, error: 'WebGPU provider not available' }; + } + return await provider.clearCache(); + } + case 'list_provider_models': { return await providerManager.listProviderModels(msg.providerId); } diff --git a/src/chrome/src/offscreen/inference-worker.js b/src/chrome/src/offscreen/inference-worker.js new file mode 100644 index 00000000..ac6e168f --- /dev/null +++ b/src/chrome/src/offscreen/inference-worker.js @@ -0,0 +1,410 @@ +/** + * WebGPU inference worker. + * + * Why this is a separate Worker and not inline in offscreen.js: the + * HuggingFace transformers.js WebGPU demos all run in a dedicated + * Worker, and when we tried running inference directly in the + * offscreen-document main thread on an Apple M4 Max with everything + * else configured correctly (WebGPU EP registered, crossOriginIsolated, + * SharedArrayBuffer available, asyncify wasm, fp16 dtype) we still hit + * `std::bad_alloc` from `OrtRun` — the same demo configuration that + * runs fine in the HF demo space. The offscreen-doc context appears to + * give the wasm allocator a tighter heap ceiling than a regular page. + * + * Workers get their own V8 isolate with its own heap budget, and the + * wasm Memory is bound to the worker's address space. Moving the + * pipeline + inference here matches the HF demo architecture and gives + * us the heap headroom Qwen-class models need. + * + * Protocol: offscreen.js sends typed messages over postMessage with a + * correlation `id`; this worker replies with `{ id, ok, ... }` or + * pushes unsolicited `{ type: 'progress', ... }` events during model + * download. + * + * Why not run this in a Worker spawned BY the service worker (skipping + * the offscreen doc entirely)? Workers spawned from MV3 SWs are + * themselves service workers (no SharedArrayBuffer, no WebGPU, the + * very limits we already hit). Dedicated workers can only be spawned + * from documents, so we still need the offscreen page as the parent. + */ + +let _libPromise = null; // Promise resolving to transformers module +let _libraryVersion = null; // for diagnostics +let _activePipelineKey = null; // `${modelId}|${dtype}|${device}` +let _activePipeline = null; // text-generation pipeline instance +let _config = null; // { transformersUrl, wasmMjsUrl, wasmUrl } from init +let _outputLocationMode = 'auto'; // 'auto' | 'gpu-buffer' +let _runtimeDeviceMode = 'webgpu'; // 'webgpu' | 'wasm' + +/** + * Dynamic import transformers.js using the URL passed from offscreen.js. + * The worker can't use chrome.runtime.getURL() reliably across all + * Chrome versions, so the parent computes the extension URLs and ships + * them in the init message. + */ +async function loadLibrary() { + if (_libPromise) return _libPromise; + if (!_config) throw new Error('worker not initialized — init message must arrive before any chat/probe'); + _libPromise = (async () => { + let lib; + try { + lib = await import(_config.transformersUrl); + } catch (e) { + _libPromise = null; + throw new Error( + 'transformers.js library not vendored. See ' + + 'src/chrome/vendor/transformers/README.md for how to drop the ' + + 'build in. Underlying error: ' + (e?.message || String(e)) + ); + } + _libraryVersion = lib.env?.version || lib.VERSION || 'unknown'; + if (lib.env) { + lib.env.allowLocalModels = false; + lib.env.allowRemoteModels = true; + // Cache API rejects chrome-extension:// URLs; we don't need the + // wasm cache anyway since our wasm is already local. + lib.env.useWasmCache = false; + // Pin wasmPaths to OUR vendored asyncify variant — the one with + // WebGPU EP bindings (webgpuInit etc.). See README troubleshooting + // table for why .asyncify and not .jsep. + try { + if (lib.env.backends?.onnx?.wasm) { + lib.env.backends.onnx.wasm.wasmPaths = { + mjs: _config.wasmMjsUrl, + wasm: _config.wasmUrl, + }; + } + } catch { /* future library shape change → fall back to defaults */ } + } + return lib; + })(); + return _libPromise; +} + +async function getPipeline(modelId, dtype, device, outputLocationMode = _outputLocationMode, runtimeDeviceMode = _runtimeDeviceMode) { + // Cache key includes dtype + device so editing those in Settings + // rebuilds the pipeline instead of silently reusing the old one. + const effectiveDevice = runtimeDeviceMode || device || 'webgpu'; + const key = `${modelId}|${dtype || 'default'}|${effectiveDevice}|${outputLocationMode}`; + if (_activePipeline && _activePipelineKey === key) return _activePipeline; + const lib = await loadLibrary(); + const { pipeline } = lib; + + // Diagnostic log to the worker's console. Worker logs show up in + // DevTools → Sources → Workers panel, or in the offscreen doc's + // console when filtered to "verbose" + the worker context. + try { + const gpuAvailable = typeof navigator !== 'undefined' && 'gpu' in navigator; + let adapterInfo = null; + if (gpuAvailable) { + try { + const adapter = await navigator.gpu.requestAdapter(); + adapterInfo = adapter ? { + isFallbackAdapter: adapter.isFallbackAdapter, + features: [...(adapter.features || [])].slice(0, 8), + } : null; + } catch (e) { adapterInfo = { error: e.message }; } + } + console.log('[webgpu-worker] pipeline init', { + modelId, dtype, device: effectiveDevice, + libraryVersion: _libraryVersion, + navigatorGpu: gpuAvailable, + adapter: adapterInfo, + hasWebgpuBackend: !!lib.env?.backends?.onnx?.webgpu, + wasmPaths: lib.env?.backends?.onnx?.wasm?.wasmPaths, + crossOriginIsolated: typeof crossOriginIsolated !== 'undefined' ? crossOriginIsolated : 'n/a', + hasSharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined', + }); + } catch { /* logging must never break inference */ } + + // Free the previous pipeline before loading a new one — two 500MB+ + // models pinned simultaneously is a recipe for OOM on integrated GPUs. + if (_activePipeline && _activePipeline.dispose) { + try { await _activePipeline.dispose(); } catch {} + } + const pipelineOptions = { + device: effectiveDevice, + dtype: dtype || 'q4f16', + progress_callback: (ev) => postProgress(modelId, ev), + }; + if (outputLocationMode === 'gpu-buffer') { + pipelineOptions.session_options = { preferredOutputLocation: 'gpu-buffer' }; + } + _activePipeline = await pipeline('text-generation', modelId, pipelineOptions); + _activePipelineKey = key; + return _activePipeline; +} + +async function disposeActivePipeline() { + if (_activePipeline && _activePipeline.dispose) { + try { await _activePipeline.dispose(); } catch {} + } + _activePipeline = null; + _activePipelineKey = null; +} + +/** + * Forward a model-download progress event to the parent (offscreen + * doc). 200ms-per-file throttling for the continuous 'progress' + * stream; state transitions always pass through. + */ +const _progressLastEmitted = new Map(); +function postProgress(modelId, ev) { + try { + const file = ev?.file || ev?.name || ''; + const status = ev?.status || ''; + if (status === 'progress') { + const key = `${modelId}|${file}`; + const now = Date.now(); + const last = _progressLastEmitted.get(key) || 0; + if (now - last < 200) return; + _progressLastEmitted.set(key, now); + } + self.postMessage({ + type: 'progress', + modelId, status, file, + loaded: ev?.loaded || 0, + total: ev?.total || 0, + progress: ev?.progress || 0, + }); + } catch { /* never let a UI update break the download */ } +} + +function extractGeneratedText(output) { + if (!output) return ''; + if (typeof output === 'string') return output; + if (Array.isArray(output)) { + const first = output[0]; + if (!first) return ''; + if (typeof first === 'string') return first; + if (first.generated_text) { + if (typeof first.generated_text === 'string') return first.generated_text; + if (Array.isArray(first.generated_text)) { + for (let i = first.generated_text.length - 1; i >= 0; i--) { + const m = first.generated_text[i]; + if (m?.role === 'assistant' && typeof m.content === 'string') return m.content; + } + } + } + } + return JSON.stringify(output); +} + +function extractToolCalls(text) { + if (!text || typeof text !== 'string') return null; + if (!text.includes('')) return null; + const calls = []; + const re = /\s*([\s\S]*?)\s*<\/tool_call>/g; + let match; + let idx = 0; + while ((match = re.exec(text)) !== null) { + try { + const obj = JSON.parse(match[1]); + if (obj && obj.name) { + calls.push({ + id: `webgpu_call_${Date.now()}_${idx++}`, + type: 'function', + function: { + name: obj.name, + arguments: typeof obj.arguments === 'string' + ? obj.arguments + : JSON.stringify(obj.arguments || {}), + }, + }); + } + } catch { /* malformed tool-call block — ignore */ } + } + return calls.length > 0 ? calls : null; +} + +// ─── Message dispatch ───────────────────────────────────────────────────── + +self.addEventListener('message', async (e) => { + const { id, type, payload } = e.data || {}; + + // init: must arrive before any other message — carries the + // extension-origin URLs the worker can't synthesize on its own. + if (type === 'init') { + _config = payload; + self.postMessage({ id, ok: true }); + return; + } + + if (type === 'probe') { + try { + await loadLibrary(); + const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator; + let isFallbackAdapter = null; + let adapterFeatures = null; + if (hasWebGPU) { + try { + const adapter = await navigator.gpu.requestAdapter(); + if (adapter) { + isFallbackAdapter = adapter.isFallbackAdapter; + adapterFeatures = [...(adapter.features || [])].slice(0, 8); + } else { + isFallbackAdapter = true; + } + } catch { /* report what we can */ } + } + self.postMessage({ + id, ok: true, + libraryVersion: _libraryVersion, + device: hasWebGPU ? 'webgpu' : 'wasm', + hasWebGPU, + isFallbackAdapter, + adapterFeatures, + }); + } catch (err) { + self.postMessage({ id, ok: false, error: err.message }); + } + return; + } + + if (type === 'clear-cache') { + try { + await disposeActivePipeline(); + _libPromise = null; + const deleted = []; + for (const name of ['transformers-cache', 'experimental_transformers-hash-cache']) { + if (await caches.delete(name)) deleted.push(name); + } + self.postMessage({ id, ok: true, deletedCaches: deleted }); + } catch (err) { + self.postMessage({ id, ok: false, error: err.message }); + } + return; + } + + if (type === 'chat') { + try { + const { modelId, dtype, device, messages, options } = payload; + let effectiveDtype = dtype; + let pipe; + try { + pipe = await getPipeline(modelId, effectiveDtype, device); + } catch (initErr) { + const initMsg = initErr?.message || String(initErr); + const missingKernel = initMsg.includes('Kernel not found') || initMsg.includes('GatherBlockQuantized'); + const usingWasm = _runtimeDeviceMode === 'wasm'; + if (!missingKernel || !usingWasm) throw initErr; + // If a previous turn switched us to wasm and this quantized model + // can't initialize there, immediately reset back to webgpu and retry. + _runtimeDeviceMode = 'webgpu'; + _outputLocationMode = 'auto'; + await disposeActivePipeline(); + pipe = await getPipeline(modelId, effectiveDtype, device, _outputLocationMode, _runtimeDeviceMode); + } + const opts = options || {}; + const generateArgs = { + max_new_tokens: opts.maxTokens || 1024, + temperature: opts.temperature ?? 0.7, + do_sample: (opts.temperature ?? 0.7) > 0, + return_full_text: false, + }; + if (opts.tools && opts.tools.length > 0) { + // OpenAI sends {type:'function', function:{name,...}}; Qwen's + // template wants just the inner function object. + generateArgs.tools = opts.tools.map(t => t.function || t); + } + let output; + const runWithRetries = async () => { + try { + return await pipe(messages || [], generateArgs); + } catch (err) { + const msg = err?.message || String(err); + const cpuError = msg.includes('The data is not on CPU'); + const mapError = msg.includes('Failed to download data from buffer') || msg.includes("Failed to execute 'mapAsync'"); + const unalignedAccess = msg.includes('operation does not support unaligned accesses'); + if (!cpuError && !mapError && !unalignedAccess) throw err; + + // First retry: toggle output mode inside WebGPU. + _outputLocationMode = mapError ? 'gpu-buffer' : 'auto'; + _runtimeDeviceMode = 'webgpu'; + await disposeActivePipeline(); + pipe = await getPipeline(modelId, dtype, device, _outputLocationMode, _runtimeDeviceMode); + try { + return await pipe(messages || [], generateArgs); + } catch (retryErr) { + const retryMsg = retryErr?.message || String(retryErr); + const retryMapError = retryMsg.includes('Failed to download data from buffer') || retryMsg.includes("Failed to execute 'mapAsync'"); + if (!retryMapError) throw retryErr; + + const loweredDtype = String(effectiveDtype || dtype || '').toLowerCase(); + const quantized = loweredDtype.startsWith('q') || loweredDtype.includes('int8') || loweredDtype.includes('int4'); + if (quantized) { + // WASM path for quantized models often misses GatherBlockQuantized; + // instead of failing immediately, retry on WebGPU with fp16 first. + effectiveDtype = 'fp16'; + _runtimeDeviceMode = 'webgpu'; + _outputLocationMode = 'auto'; + await disposeActivePipeline(); + pipe = await getPipeline(modelId, effectiveDtype, device, _outputLocationMode, _runtimeDeviceMode); + try { + return await pipe(messages || [], generateArgs); + } catch (fp16Err) { + const fp16Msg = fp16Err?.message || String(fp16Err); + throw new Error( + `webgpu map/buffer retry failed for quantized dtype (${dtype || 'unknown'}), ` + + `and fp16 WebGPU retry also failed. WASM fallback remains disabled for quantized models ` + + `because CPUExecutionProvider often lacks GatherBlockQuantized kernels. ` + + `Try smaller context/tokens. Underlying errors: quantized=${retryMsg}; fp16=${fp16Msg}` + ); + } + } + + // Second retry: WebGPU buffer is still unstable; fall back to WASM. + _outputLocationMode = 'auto'; + _runtimeDeviceMode = 'wasm'; + await disposeActivePipeline(); + pipe = await getPipeline(modelId, dtype, device, _outputLocationMode, _runtimeDeviceMode); + try { + return await pipe(messages || [], generateArgs); + } catch (wasmErr) { + const wasmMsg = wasmErr?.message || String(wasmErr); + const missingKernel = wasmMsg.includes('Kernel not found') || wasmMsg.includes('GatherBlockQuantized'); + if (missingKernel) { + _runtimeDeviceMode = 'webgpu'; + await disposeActivePipeline(); + throw new Error( + `WASM fallback unsupported for this quantized model (${modelId}). ` + + `ONNX Runtime CPU/WASM is missing GatherBlockQuantized kernels. ` + + `Please keep device=webgpu and reduce context/tokens, or choose a WASM-compatible model. ` + + `Underlying error: ${wasmMsg}` + ); + } + throw wasmErr; + } + } + + // Some WebGPU kernels reject 4-bit layouts with unaligned access + // requirements. Retry once on fp16 to use aligned tensors. + if (unalignedAccess) { + effectiveDtype = 'fp16'; + _runtimeDeviceMode = 'webgpu'; + _outputLocationMode = 'auto'; + await disposeActivePipeline(); + pipe = await getPipeline(modelId, effectiveDtype, device, _outputLocationMode, _runtimeDeviceMode); + return await pipe(messages || [], generateArgs); + } + } + }; + output = await runWithRetries(); + const text = extractGeneratedText(output); + const toolCalls = extractToolCalls(text); + self.postMessage({ + id, ok: true, + content: toolCalls ? '' : text, + toolCalls: toolCalls || null, + usage: null, + raw: { rawText: text }, + }); + } catch (err) { + self.postMessage({ id, ok: false, error: err.message }); + } + return; + } + + // Unknown message type — reply with error so callers don't hang. + self.postMessage({ id, ok: false, error: `unknown worker message type: ${type}` }); +}); diff --git a/src/chrome/src/offscreen/offscreen.html b/src/chrome/src/offscreen/offscreen.html index 7d097c4b..43adff49 100644 --- a/src/chrome/src/offscreen/offscreen.html +++ b/src/chrome/src/offscreen/offscreen.html @@ -1,21 +1,16 @@ - - - - - - - - + type="module" is required so offscreen.js can dynamically import the + vendored library. NOTE: the vendored transformers.web.min.js has its + internal `import("onnxruntime-web/webgpu")` bare specifier rewritten + to a relative path during vendoring — see + src/chrome/vendor/transformers/README.md. We deliberately do NOT use a + diff --git a/src/chrome/src/offscreen/offscreen.js b/src/chrome/src/offscreen/offscreen.js index 6ecfa6fe..237111f6 100644 --- a/src/chrome/src/offscreen/offscreen.js +++ b/src/chrome/src/offscreen/offscreen.js @@ -1,13 +1,29 @@ /** - * Offscreen document — fetch proxy for local network LLM servers. + * Offscreen document — host for tasks the MV3 service worker can't do + * itself. * - * Chrome MV3 service workers can't always reach HTTP servers on the local - * network (Private Network Access + CORS restrictions). This offscreen - * document receives fetch requests from the service worker, makes them - * from a regular page context (which has different networking rules), - * and sends the response back. + * 1. `offscreen-fetch` — fetch() proxy for local network LLM servers. + * The SW can't always reach 192.168.* / 127.* directly due to PNA + + * CORS; this page context can. See providers/fetch-with-fallback.js. + * + * 2. `webgpu-chat` / `webgpu-probe` — runs the WebGPU + ONNX local + * LLM (default Qwen 3.5 0.8B) via a dedicated Web Worker spawned + * from this document. The Worker (inference-worker.js) owns the + * transformers.js pipeline; this file just proxies messages. + * + * WHY a Worker: direct inference in this offscreen-doc main thread + * OOMs even with everything configured correctly (WebGPU EP, + * crossOriginIsolated, SharedArrayBuffer, asyncify wasm). Workers + * get their own V8 isolate with its own heap; this matches what + * the HuggingFace WebGPU demo space does. See the worker file's + * header for the full story. + * + * Library is vendored under src/chrome/vendor/transformers/ — see + * the README there for how to drop the build in. */ +// ─── Local-network fetch proxy ──────────────────────────────────────────── + chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type !== 'offscreen-fetch') return false; @@ -47,3 +63,134 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { return true; // keep sendResponse channel open for async }); + +// ─── WebGPU LLM inference (proxied to worker) ───────────────────────────── + +let _worker = null; // the spawned inference Worker +let _workerInitPromise = null; // resolves once init message has acked +let _nextRequestId = 1; +const _pendingRequests = new Map(); // id → {resolve, reject} + +/** + * Lazily spawn the inference worker on the first webgpu-* message. + * The worker URL must point at the packaged worker file — same-origin + * to the offscreen doc, no special manifest declaration needed + * (workers spawned from extension pages don't need + * web_accessible_resources). type:'module' so the worker can use + * dynamic import() to pull in the vendored transformers.js. + * + * Init message ships the extension-origin URLs the worker needs; + * chrome.runtime.getURL() may not be available in all Chrome worker + * contexts, so we compute them here once. + */ +async function ensureWorker() { + if (_worker && _workerInitPromise) return _workerInitPromise; + _worker = new Worker(chrome.runtime.getURL('src/offscreen/inference-worker.js'), { + type: 'module', + }); + _worker.addEventListener('message', onWorkerMessage); + _worker.addEventListener('error', (e) => { + // Worker hard-failed (parse error, unhandled throw at top level). + // Reject every in-flight request so callers see a real error + // instead of hanging forever. + console.error('[webgpu] worker error', e); + for (const { reject } of _pendingRequests.values()) { + reject(new Error('inference worker errored: ' + (e.message || 'unknown'))); + } + _pendingRequests.clear(); + _worker = null; + _workerInitPromise = null; + }); + _workerInitPromise = sendToWorker('init', { + transformersUrl: chrome.runtime.getURL('vendor/transformers/transformers.web.js'), + wasmMjsUrl: chrome.runtime.getURL('vendor/transformers/ort-wasm-simd-threaded.asyncify.mjs'), + wasmUrl: chrome.runtime.getURL('vendor/transformers/ort-wasm-simd-threaded.asyncify.wasm'), + }); + return _workerInitPromise; +} + +/** + * Send a typed message to the worker and await its `{ id, ok, ... }` + * reply. Unsolicited messages (`{ type: 'progress', ... }`) have no + * id and bypass this map — see onWorkerMessage. + */ +function sendToWorker(type, payload) { + const id = _nextRequestId++; + return new Promise((resolve, reject) => { + _pendingRequests.set(id, { resolve, reject }); + _worker.postMessage({ id, type, payload }); + }); +} + +function onWorkerMessage(e) { + const data = e.data || {}; + if (data.type === 'progress') { + // Unsolicited progress event — relay to the side panel. Same + // shape we used before the Worker refactor so the UI code on the + // sidepanel side didn't have to change. + chrome.runtime.sendMessage({ + target: 'sidepanel', + action: 'model_download', + modelId: data.modelId, + status: data.status, + file: data.file, + loaded: data.loaded, + total: data.total, + progress: data.progress, + }).catch(() => { /* no listener — fine, progress UI is best-effort */ }); + return; + } + const pending = _pendingRequests.get(data.id); + if (!pending) return; // late reply after error/timeout + _pendingRequests.delete(data.id); + if (data.ok) pending.resolve(data); + else pending.reject(new Error(data.error || 'worker reported error with no message')); +} + +chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + if (msg.type !== 'webgpu-probe') return false; + (async () => { + try { + await ensureWorker(); + const res = await sendToWorker('probe', {}); + sendResponse(res); + } catch (e) { + sendResponse({ ok: false, error: e.message }); + } + })(); + return true; +}); + +chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + if (msg.type !== 'webgpu-clear-cache') return false; + (async () => { + try { + await ensureWorker(); + const res = await sendToWorker('clear-cache', {}); + sendResponse(res); + } catch (e) { + sendResponse({ ok: false, error: e.message }); + } + })(); + return true; +}); + +chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + if (msg.type !== 'webgpu-chat') return false; + (async () => { + try { + await ensureWorker(); + const res = await sendToWorker('chat', { + modelId: msg.model, + dtype: msg.dtype, + device: msg.device, + messages: msg.messages || [], + options: msg.options || {}, + }); + sendResponse(res); + } catch (e) { + sendResponse({ ok: false, error: e.message }); + } + })(); + return true; +}); diff --git a/src/chrome/src/providers/manager.js b/src/chrome/src/providers/manager.js index 539a002e..fa03f265 100644 --- a/src/chrome/src/providers/manager.js +++ b/src/chrome/src/providers/manager.js @@ -1,6 +1,7 @@ import { LlamaCppProvider } from './llamacpp.js'; import { OpenAICompatibleProvider } from './openai.js'; import { AnthropicProvider, AnthropicOAuthProvider } from './anthropic.js'; +import { WebGPUProvider } from './webgpu.js'; /** * Manages LLM provider instances and persists configuration. @@ -84,6 +85,24 @@ export class ProviderManager { supportsVision: true, enabled: true, }, + // Browser-native local: WebGPU + ONNX runtime via @huggingface/transformers. + // No separate server needed — model weights download from HF Hub on + // first use (~500MB for q4 Qwen 3 0.6B), cached in IndexedDB by the + // library, inference runs in the offscreen document on the user's + // GPU. Default disabled because the first-run download is substantial + // and the library has to be vendored — see + // src/chrome/vendor/transformers/README.md. + webgpu_qwen3: { + type: 'webgpu', + category: 'local', + label: 'WebGPU', + model: 'onnx-community/gemma-4-E2B-it-ONNX', + dtype: 'q4f16', + device: 'webgpu', + supportsVision: false, + useCompactPrompt: true, + enabled: false, + }, ollama: { type: 'openai', category: 'local', @@ -248,6 +267,8 @@ export class ProviderManager { return new AnthropicProvider(config); case 'anthropic_oauth': return new AnthropicOAuthProvider(config); + case 'webgpu': + return new WebGPUProvider(config); default: throw new Error(`Unknown provider type: ${config.type}`); } diff --git a/src/chrome/src/providers/webgpu.js b/src/chrome/src/providers/webgpu.js new file mode 100644 index 00000000..c25870dc --- /dev/null +++ b/src/chrome/src/providers/webgpu.js @@ -0,0 +1,233 @@ +/** + * WebGPU Provider — runs Qwen / similar small models entirely in the + * browser via WebGPU + ONNX (@huggingface/transformers). + * + * Why this exists: the other "local" providers (llama.cpp, ollama, lmstudio) + * still require the user to install + run a separate server. This one needs + * nothing — model weights download from HuggingFace on first use, cached in + * IndexedDB by transformers.js, and inference runs on the user's GPU through + * the extension's offscreen document. + * + * Architecture: + * service worker (background) + * └─ WebGPUProvider.chat() ──┐ + * ▼ chrome.runtime.sendMessage + * offscreen document ──┐ + * └─ @huggingface/transformers │ + * pipeline('text-generation', model, { device: 'webgpu' }) + * (loaded lazily on first chat call) + * + * The offscreen document is required because service workers don't have + * WebGPU access (or DOM, or IndexedDB the way transformers.js wants it). + * Webbrain already uses an offscreen doc for the local-network fetch proxy + * — we reuse the same document and add a new message handler there. + * + * Tool use: Qwen 3.5 supports structured tool calls via its ChatML template, + * but at 0.8B params reliability is still best-effort. supportsTools = true, + * but the settings UI nudges users toward Ask mode for this provider; Act + * mode is available with a "small model, expect breakage" hint. + * + * First-run cost: ~600MB download (q4f16 quant of Qwen3.5-0.8B). Cached in + * IndexedDB by transformers.js so subsequent runs are instant. + * + * Library vendoring: the @huggingface/transformers ESM build is large (~5MB + * + a ~30MB onnxruntime-web WASM blob) — too big to commit. See + * src/chrome/vendor/transformers/README.md for how to drop the file in. + * The provider returns a clear error when the library is missing. + */ + +import { BaseLLMProvider } from './base.js'; + +export class WebGPUProvider extends BaseLLMProvider { + constructor(config = {}) { + super(config); + this.model = config.model || 'onnx-community/gemma-4-E2B-it-ONNX'; + // dtype: 'q4f16' = 4-bit weights + fp16 activations. Plain 'q4' uses + // fp32 activations, whose intermediate buffers blow past the WASM + // 2GB heap mid-inference (std::bad_alloc out of OrtRun). 'q4f16' is + // also the dtype the transformers.js team recommends for Qwen 3 on + // WebGPU. Override via config.dtype if you want fp16/int8/etc. + this.dtype = config.dtype || 'q4f16'; + // device:'webgpu' is the default; setting it explicitly here lets us + // override to 'wasm' in tests or as a fallback when WebGPU is absent. + this.device = config.device || 'webgpu'; + } + + get name() { + return 'webgpu'; + } + + get supportsTools() { + // Qwen 3 has a tool-call template. Reliability at 0.6B is best-effort — + // see the settings UI hint that nudges users toward Ask mode. + return true; + } + + get supportsVision() { + // Qwen 3 0.6B is text-only. If we add a Qwen-VL or Gemma-3n entry later, + // that provider config can opt-in via the same shape used elsewhere. + return !!this.config.supportsVision; + } + + get useCompactPrompt() { + // Default ON for this provider — 0.6B context budget is tight and the + // compact prompt is exactly the workload it was written for. + return this.config.useCompactPrompt !== false; + } + + async chat(messages, options = {}) { + const res = await this._dispatch({ + type: 'webgpu-chat', + model: this.model, + dtype: this.dtype, + device: this.device, + messages, + options: { + temperature: options.temperature, + maxTokens: options.maxTokens, + tools: options.tools || null, + toolChoice: options.toolChoice || 'auto', + }, + }); + if (!res || res.error) { + throw new Error(`webgpu: ${res?.error || 'no response from offscreen document'}`); + } + return { + content: res.content || '', + toolCalls: res.toolCalls || null, + usage: res.usage || null, + raw: res.raw || null, + }; + } + + async *chatStream(messages, options = {}) { + // v1: no true token streaming. The 0.6B model finishes a normal turn + // in a couple of seconds on most laptops; the round-trip-and-yield + // simplification lets us ship the provider without first solving the + // background↔offscreen chunked-message router. Upgrade target: when + // somebody asks for it, swap this for a streamId-based subscription + // that forwards transformers.js `streamer` callbacks back to the SW. + const result = await this.chat(messages, options); + if (result.toolCalls && result.toolCalls.length > 0) { + // Convention shared with openai.js / llamacpp.js: when a chunk has + // type:'tool_call', `content` IS the tool_calls array (not text). + // processMessageStream() in agent.js reads chunk.content for tool + // deltas — yielding text in `content` here would drop the tool call. + yield { type: 'tool_call', content: result.toolCalls }; + } else { + yield { type: 'text', content: result.content }; + } + yield { type: 'done', usage: result.usage }; + } + + async testConnection() { + // Don't trigger a full model load just to test — that's 500MB and + // tens of seconds. Instead, just verify the offscreen document is + // reachable and the library is vendored. + try { + const res = await this._dispatch({ type: 'webgpu-probe' }); + if (!res || res.error) { + return { ok: false, error: res?.error || 'offscreen probe failed' }; + } + // Surface the "software WebGPU adapter" case as a failed + // testConnection — running a sub-1B model on SwiftShader / Lavapipe + // OOMs the WASM heap before generating a single token. Better to + // warn here than to silently waste a 500MB download. + if (res.hasWebGPU === false) { + return { + ok: false, + error: 'WebGPU not available in this browser. Open chrome://gpu and ' + + 'check the "WebGPU" row — if it says Disabled or Software only, ' + + 'this provider can\'t run on this machine.', + }; + } + if (res.isFallbackAdapter === true) { + return { + ok: false, + error: 'WebGPU is using a software fallback adapter (SwiftShader / ' + + 'Lavapipe). Inference will exhaust the WASM heap. Enable hardware ' + + 'WebGPU at chrome://flags/#enable-unsafe-webgpu, or run on a ' + + 'machine with a supported GPU.', + }; + } + return { + ok: true, + model: this.model, + device: res.device || this.device, + libraryVersion: res.libraryVersion || null, + adapterFeatures: res.adapterFeatures || null, + }; + } catch (e) { + return { ok: false, error: e.message }; + } + } + + async clearCache() { + try { + const res = await this._dispatch({ type: 'webgpu-clear-cache' }); + if (!res || res.error) return { ok: false, error: res?.error || 'clear-cache failed' }; + return { ok: true, deletedCaches: res.deletedCaches || [] }; + } catch (e) { + return { ok: false, error: e.message }; + } + } + + /** + * Send a message to the offscreen document and await its reply. + * Lazily creates the offscreen doc if it doesn't exist yet — same + * pattern as fetch-with-fallback.js. + */ + async _dispatch(msg) { + await ensureOffscreen(); + return new Promise((resolve, reject) => { + try { + chrome.runtime.sendMessage(msg, (response) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(response); + } + }); + } catch (e) { + reject(e); + } + }); + } +} + +// ─── Offscreen lifecycle ────────────────────────────────────────────────── +// +// Mirror of the helper in fetch-with-fallback.js, but kept local so the +// provider has no circular dep on the fetch layer. Both ultimately drive +// the same `src/offscreen/offscreen.html` document. + +let _offscreenReady = false; + +async function ensureOffscreen() { + if (_offscreenReady) return; + try { + const existing = await chrome.offscreen.hasDocument?.(); + if (existing) { + _offscreenReady = true; + return; + } + } catch { /* hasDocument unsupported on older Chrome — fall through */ } + try { + await chrome.offscreen.createDocument({ + url: 'src/offscreen/offscreen.html', + // WORKERS is the closest documented reason for "I want WebGPU + a + // long-lived JS runtime". LOCAL_STORAGE works too and is what the + // existing fetch path uses; we match it so we don't fight over + // recreating the doc with a different reason. + reasons: ['LOCAL_STORAGE'], + justification: 'Run local LLM inference on WebGPU', + }); + _offscreenReady = true; + } catch (e) { + if (e.message?.includes('already exists') || e.message?.includes('Only a single offscreen')) { + _offscreenReady = true; + } else { + throw e; + } + } +} diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index 5b3417cb..be639a73 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -62,6 +62,11 @@ export default { 'sp.clarify.input_placeholder_with_options': 'Or type a different answer…', 'sp.clarify.your_answer': 'Your answer:', + 'sp.model_download.headline': 'Downloading {model} — {mb} / {total_mb} MB', + 'sp.model_download.queued': 'Queued', + 'sp.model_download.file_done': 'File complete', + 'sp.model_download.ready': '{model} ready', + 'sp.step.details': 'details', 'sp.step.input_label': 'Input', 'sp.step.result_label': 'Result', @@ -166,6 +171,11 @@ export default { 'st.provider.field.supports_vision': 'Model supports vision (multimodal)', 'st.provider.field.compact_prompt': 'Compact prompt (drops some guardrails — opt in only for models under 8B)', 'st.provider.field.model_loaded_hint': 'leave blank to use loaded model', + 'st.provider.field.dtype': 'Quantization (e.g. q4, q8, fp16)', + 'st.providers.clear_cache': 'Clear Model Cache', + 'st.providers.clearing_cache': 'Clearing cached model files…', + 'st.providers.cache_cleared': 'Model cache cleared. The model will re-download on next use.', + 'st.providers.cache_clear_failed': 'Failed to clear cache: {error}', 'st.vision.heading': 'Vision', 'st.vision.desc': 'If set, screenshots are sent to this model for reading the page, while the active provider handles planning and tool calls. Leave blank to use the active provider for vision too. OpenAI-compatible endpoints only.', diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index cd14cc2d..f403e0a8 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -6,7 +6,7 @@ import { t, getLocale, setLocale, LANGUAGES } from './i18n.js'; // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '8.0.3'; +const EXT_VERSION = '9.0.10'; const providersContainer = document.getElementById('providers'); const verboseToggle = document.getElementById('toggle-verbose'); @@ -530,6 +530,17 @@ function renderProviders() { { key: 'useCompactPrompt', labelKey: 'st.provider.field.compact_prompt', type: 'checkbox' }, ], }, + // WebGPU + ONNX in-browser provider. The model field takes a HuggingFace + // repo id (e.g. onnx-community/Qwen3-0.6B-ONNX); dtype lets the user pick + // a quantization variant if their GPU has the memory for it (q4 is the + // safe default for ~6GB VRAM laptops). + webgpu_qwen3: { + fields: [ + { key: 'model', labelKey: 'st.provider.field.model', type: 'text', placeholder: 'onnx-community/gemma-4-E2B-it-ONNX' }, + { key: 'dtype', labelKey: 'st.provider.field.dtype', type: 'text', placeholder: 'q4f16' }, + { key: 'useCompactPrompt', labelKey: 'st.provider.field.compact_prompt', type: 'checkbox' }, + ], + }, ollama: { fields: [ { key: 'baseUrl', labelKey: 'st.provider.field.server_url', type: 'text', placeholder: 'http://localhost:11434/v1' }, @@ -702,6 +713,7 @@ function renderProviders() { ${!isActive ? `` : ''} + ${config.type === 'webgpu' ? `` : ''}
`; @@ -731,6 +743,9 @@ function renderProviders() { document.querySelectorAll('.btn-load-models').forEach(btn => { btn.addEventListener('click', () => loadProviderModels(btn.dataset.provider)); }); + document.querySelectorAll('.btn-clear-cache').forEach(btn => { + btn.addEventListener('click', () => clearWebGPUCache(btn.dataset.provider)); + }); // OAuth-Claude-specific bindings. These only fire if the OAuth card is // currently rendered (i.e., expanded — collapsed bodies aren't in DOM). document.querySelectorAll('.btn-claude-signin').forEach(btn => { @@ -803,10 +818,11 @@ function wrapCollapsibleCard(id, config, isActive, bodyHtml) { // LM Studio defaults to whatever's loaded) just renders nothing rather // than a placeholder, to avoid pretending we know what they're running. const modelStr = (config.model && String(config.model).trim()) || ''; + const providerTitle = id === 'webgpu_qwen3' ? 'WebGPU' : (config.label || id); header.innerHTML = `
- ${escapeHtml(config.label || id)} + ${escapeHtml(providerTitle)} ${escapeHtml(config.type)} ${config.category ? `${escapeHtml(config.category)}` : ''} ${modelStr ? `${escapeHtml(modelStr)}` : ''} @@ -1010,6 +1026,21 @@ async function testProvider(id) { } } +async function clearWebGPUCache(id) { + const testEl = document.getElementById(`test-${id}`); + testEl.className = 'test-result show'; + testEl.textContent = t('st.providers.clearing_cache'); + testEl.style.color = 'var(--text2)'; + try { + const res = await sendToBackground('clear_webgpu_cache'); + testEl.className = 'test-result show ok'; + testEl.textContent = t('st.providers.cache_cleared'); + } catch (e) { + testEl.className = 'test-result show fail'; + testEl.textContent = t('st.providers.cache_clear_failed', { error: e.message }); + } +} + /** * Snapshot any unsaved field values from the current DOM back into * providersData so a subsequent renderProviders() preserves them. diff --git a/src/chrome/src/ui/sidepanel.js b/src/chrome/src/ui/sidepanel.js index 4ff08c8c..a730407b 100644 --- a/src/chrome/src/ui/sidepanel.js +++ b/src/chrome/src/ui/sidepanel.js @@ -605,6 +605,18 @@ function summarizeLastTranscript() { if (sendBtn) sendBtn.click(); } +// Model-download progress (WebGPU provider's first-run weights download +// from HF Hub). Broadcast by the offscreen document via chrome.runtime +// .sendMessage with action='model_download'. Listener is separate from +// the agent_update one below because download events fire outside of any +// chat turn (the user clicks Test Connection or sends their first message; +// the model loads in the background; we want to surface progress without +// hijacking the agent's normal status pipeline). +chrome.runtime.onMessage.addListener((msg) => { + if (msg.target !== 'sidepanel' || msg.action !== 'model_download') return; + renderModelDownloadProgress(msg); +}); + chrome.runtime.onMessage.addListener((msg) => { if (msg.target !== 'sidepanel' || msg.action !== 'agent_update') return; @@ -719,6 +731,100 @@ chrome.runtime.onMessage.addListener((msg) => { } }); +/** + * Render / update the WebGPU model-download progress card. Lives at the + * top of the messages container while a download is active; auto-dismisses + * a couple of seconds after the 'ready' event so the user sees confirmation + * before it disappears. + * + * Aggregates across multiple in-flight files: the model has ~8 files (the + * .onnx tensor blob, tokenizer, config, generation_config, etc.) and they + * arrive in parallel. We sum loaded/total across files for the headline + * progress, and show the currently-downloading filename below it. + */ +const _modelDownloadState = { + cardEl: null, + files: new Map(), // file → {loaded, total, status} + dismissTimer: null, +}; + +function renderModelDownloadProgress(msg) { + const { status, file, loaded, total, modelId } = msg; + + // Track per-file progress so totals are accurate even with parallel + // downloads. 'done' / 'ready' don't have byte counts; preserve the + // last-known totals. + if (file) { + const prev = _modelDownloadState.files.get(file) || { loaded: 0, total: 0 }; + _modelDownloadState.files.set(file, { + loaded: status === 'done' ? (prev.total || prev.loaded) : (loaded || prev.loaded), + total: total || prev.total, + status, + }); + } + + // Build or find the card. + if (!_modelDownloadState.cardEl) { + const card = document.createElement('div'); + card.className = 'model-download-card'; + card.innerHTML = ` +
+
+
+ `; + if (messagesEl) messagesEl.prepend(card); + _modelDownloadState.cardEl = card; + } + const card = _modelDownloadState.cardEl; + const headline = card.querySelector('.model-download-headline'); + const fill = card.querySelector('.model-download-bar-fill'); + const detail = card.querySelector('.model-download-detail'); + + if (status === 'ready') { + headline.textContent = (typeof t === 'function' ? t('sp.model_download.ready', { model: modelId }) : `${modelId} ready`) || `${modelId} ready`; + fill.style.width = '100%'; + fill.classList.add('done'); + detail.textContent = ''; + // Linger briefly so the user sees "ready", then fade out. + if (_modelDownloadState.dismissTimer) clearTimeout(_modelDownloadState.dismissTimer); + _modelDownloadState.dismissTimer = setTimeout(() => { + if (_modelDownloadState.cardEl) { + _modelDownloadState.cardEl.remove(); + _modelDownloadState.cardEl = null; + _modelDownloadState.files.clear(); + } + }, 1800); + scrollToBottom(); + return; + } + + // Aggregate across files for the headline bar. + let totalLoaded = 0; + let totalSize = 0; + for (const f of _modelDownloadState.files.values()) { + totalLoaded += f.loaded || 0; + totalSize += f.total || 0; + } + const pct = totalSize > 0 ? Math.min(100, Math.round((totalLoaded / totalSize) * 100)) : 0; + fill.style.width = `${pct}%`; + fill.classList.remove('done'); + + const headlineText = (typeof t === 'function' + ? t('sp.model_download.headline', { model: modelId, mb: Math.round(totalLoaded / 1048576), total_mb: Math.round(totalSize / 1048576) || '?' }) + : null) || `Downloading ${modelId} — ${Math.round(totalLoaded / 1048576)} / ${Math.round(totalSize / 1048576) || '?'} MB`; + headline.textContent = headlineText; + + if (status === 'progress' || status === 'download') { + detail.textContent = file || ''; + } else if (status === 'initiate') { + detail.textContent = (typeof t === 'function' ? t('sp.model_download.queued') : 'Queued') + (file ? ` · ${file}` : ''); + } else if (status === 'done') { + detail.textContent = (typeof t === 'function' ? t('sp.model_download.file_done') : 'File complete') + (file ? ` · ${file}` : ''); + } + + scrollToBottom(); +} + /** * Render a clarify() prompt inside the current assistant message. Shows the * question, optional "reason" hint, suggested-option buttons, and a free- diff --git a/src/chrome/styles/sidepanel.css b/src/chrome/styles/sidepanel.css index 34c5fac6..f4caaff2 100644 --- a/src/chrome/styles/sidepanel.css +++ b/src/chrome/styles/sidepanel.css @@ -1002,3 +1002,47 @@ body { cursor: pointer; } .btn-summarize-recording:hover { background: var(--accent2, #5a52d5); } + +/* WebGPU model-download progress — shown at the top of the messages + container while the local Qwen 3 weights pull from HF Hub on first + run. Auto-dismisses ~2s after the 'ready' event. */ +.model-download-card { + margin: 8px 0; + padding: 10px 12px; + border: 1px solid var(--accent); + border-radius: var(--radius, 6px); + background: var(--accent-dim, rgba(33, 150, 243, 0.08)); + display: flex; + flex-direction: column; + gap: 6px; +} +.model-download-headline { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} +.model-download-bar { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; + overflow: hidden; +} +.model-download-bar-fill { + height: 100%; + width: 0%; + background: var(--accent); + transition: width 0.2s ease-out; + border-radius: 3px; +} +.model-download-bar-fill.done { + background: var(--success, #4caf50); +} +.model-download-detail { + font-size: 11px; + color: var(--text-secondary); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/chrome/vendor/transformers/README.md b/src/chrome/vendor/transformers/README.md new file mode 100644 index 00000000..0ae58216 --- /dev/null +++ b/src/chrome/vendor/transformers/README.md @@ -0,0 +1,156 @@ +# Vendored @huggingface/transformers + +The WebGPU provider (`src/providers/webgpu.js`) loads the in-browser ONNX +runtime via `@huggingface/transformers`. The relevant build files are +committed in this directory so the extension works straight from a +fresh `git clone` — no per-developer vendoring step. + +## What's here + +| File / dir | Source | Purpose | +| --- | --- | --- | +| `transformers.web.js` | `node_modules/@huggingface/transformers/dist/` | Browser ESM bundle, UNMINIFIED (patched, see below) | +| `ort.webgpu.mjs` | `node_modules/onnxruntime-web/dist/` | WebGPU backend, UNMINIFIED (~662KB) | +| `onnxruntime-common/` (21 files) | `node_modules/onnxruntime-common/dist/esm/` | `Tensor` + session types, imported by transformers.web.js (~85KB total) | +| `ort-wasm-simd-threaded.jsep.mjs` | `node_modules/onnxruntime-web/dist/` | JSEP (WebGPU) wasm loader | +| `ort-wasm-simd-threaded.jsep.wasm` | `node_modules/onnxruntime-web/dist/` | JSEP (WebGPU) wasm runtime (~25MB) | +| `ort-wasm-simd-threaded.asyncify.mjs` | `node_modules/onnxruntime-web/dist/` | Asyncify wasm loader | +| `ort-wasm-simd-threaded.asyncify.wasm` | `node_modules/onnxruntime-web/dist/` | Asyncify wasm runtime (~23MB) — used when ops fall back from WebGPU to CPU | + +**Why unminified.** Chrome Web Store and AMO require readable source for +review; minified blobs can get review delays or outright rejection. +The unminified builds are larger (1.1MB vs 422KB for transformers, +662KB vs 111KB for the webgpu backend) but still well within the +extension package budget, and they don't impact runtime — the browser +just sees more JS to parse, which is microseconds. + +The `.web.js` build is the browser ESM variant — not +`transformers.js` (dual ESM/CJS), not `transformers.node.*`. The +import path in `src/offscreen/offscreen.js` is hard-coded to +`transformers.web.js`; if you change which build is vendored, update +that import. + +### Patch: rewrite the bare specifiers + +The upstream `transformers.web.js` contains TWO bare module specifiers +the browser can't resolve without an import map or a bundler — MV3's +CSP (`script-src 'self'`) can block inline import maps on some Chrome +versions, so we patch both at vendoring time: + +```bash +# 1. onnxruntime-web's WebGPU backend +sed -i 's|"onnxruntime-web/webgpu"|"./ort.webgpu.mjs"|' \ + src/chrome/vendor/transformers/transformers.web.js + +# 2. onnxruntime-common (Tensor + session types). transformers.web.js +# has `import { Tensor } from "onnxruntime-common";`. We vendor the +# onnxruntime-common ESM tree under ./onnxruntime-common/ and point +# the import at its index.js. +sed -i 's|"onnxruntime-common"|"./onnxruntime-common/index.js"|' \ + src/chrome/vendor/transformers/transformers.web.js +``` + +Each occurs exactly once per release (a third hit for the +`@huggingface/transformers` literal at line ~10667 is inside a JSDoc +example string, not a real import — leave it). After sed-ing, verify: + +```bash +grep -E '(import|export)[^"]*from\s+"[a-zA-Z@]' \ + src/chrome/vendor/transformers/transformers.web.js \ + | grep -v '^\s*//' | grep -v '^\s*\*' +# expected: empty output +``` + +## Current vendored version + +| webbrain | @huggingface/transformers | onnxruntime-web | +| --- | --- | --- | +| 7.4.0+ | 4.2.0 | (matched, transitive dep of transformers) | + +## Updating + +```bash +npm install @huggingface/transformers@latest +cp node_modules/@huggingface/transformers/dist/transformers.web.js \ + src/chrome/vendor/transformers/ +cp node_modules/onnxruntime-web/dist/ort.webgpu.mjs \ + src/chrome/vendor/transformers/ +cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.mjs \ + src/chrome/vendor/transformers/ +cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.jsep.wasm \ + src/chrome/vendor/transformers/ +cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.mjs \ + src/chrome/vendor/transformers/ +cp node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.wasm \ + src/chrome/vendor/transformers/ + +# onnxruntime-common (21 small .js files, ~85KB) — wholesale-copy +# the ESM tree. +rm -rf src/chrome/vendor/transformers/onnxruntime-common +mkdir src/chrome/vendor/transformers/onnxruntime-common +cp node_modules/onnxruntime-common/dist/esm/*.js \ + src/chrome/vendor/transformers/onnxruntime-common/ + +# Re-apply the bare-specifier patches: +sed -i 's|"onnxruntime-web/webgpu"|"./ort.webgpu.mjs"|' \ + src/chrome/vendor/transformers/transformers.web.js +sed -i 's|"onnxruntime-common"|"./onnxruntime-common/index.js"|' \ + src/chrome/vendor/transformers/transformers.web.js +``` + +Then bump the version row in the table above, commit, and re-run the +extension to verify Qwen 3 still loads. + +## Troubleshooting + +Error cascade you may walk through on first wiring up a new model: + +| Error | Cause | Fix | +| --- | --- | --- | +| `Failed to resolve module specifier "onnxruntime-web/webgpu"` (or `"onnxruntime-common"`) | The vendored `transformers.web.js` wasn't sed-patched. | Re-run the two `sed` commands above. | +| `Failed to fetch dynamically imported module .../ort-wasm-simd-threaded.asyncify.mjs` | transformers.js's init code (line ~7786 of `transformers.web.js`) auto-sets `wasmPaths` to the asyncify variant for non-Safari browsers. If you didn't override it AND didn't vendor asyncify, the wasm fetch fails. | Either vendor asyncify, or — better — override `wasmPaths` to point at the `.jsep` variant (what `offscreen.js` does). | +| `std::bad_alloc` from `OrtRun` despite WebGPU EP registered and `crossOriginIsolated:true` | Inference runs in the offscreen-doc main thread, which appears to have a tighter wasm heap ceiling than a regular page. The HF demo space works because it runs in a Web Worker. | Inference is now in `src/offscreen/inference-worker.js` (a dedicated module Worker spawned by `offscreen.js`). If you reorganize this, keep inference in a Worker — the offscreen-doc main thread is not enough. | +| `Integer overflow` from `safeint.h:17` during `OrtRun` | The `q4f16` kernel path for Qwen 3 has an int32 shape calc that overflows on some Chrome/GPU combos. | Settings → WebGPU provider → set `dtype` to `fp16`. Doubles the download (~1.2GB) but uses stable single-precision kernels throughout. | +| `no available backend found` (no specific error after) | WebGPU adapter unavailable AND no WASM fallback variants vendored. | Confirm `chrome://gpu` shows WebGPU enabled; otherwise vendor the plain `ort-wasm-simd-threaded.{mjs,wasm}` pair. | + +## Why vendored and not loaded from a CDN? + +Manifest V3 extensions' CSP is `script-src 'self' 'wasm-unsafe-eval'`. +Remote scripts (`