scripts/vendor_wllama.py
Rama main · réplica de lectura del repositorio original.
"""Reproduce the pinned, self-hosted WASM inference runtime. Never downloads weights.
wllama is llama.cpp compiled to WebAssembly. It is vendored because Safari on
iOS/iPadOS ships WebGPU disabled on most OS versions, so the WebGPU runtime
alone leaves those visitors without generation. WASM needs no GPU, no flag and
no manual install step; it is the automatic fallback backend.
Two archives are fetched from the npm registry, each checked against a pinned
SHA-256 before a single byte is extracted:
* `@wllama/wllama` 3.6.1 — the main runtime (ESM loader + the JSPI/Memory64
WebAssembly build). Used by Chromium-class browsers.
* `@wllama/wllama-compat` 3.6.1 — the Asyncify build for browsers without JSPI
or Memory64 (Safari, and Firefox unless it enables JSPI). The loader decides
which one to run (`needCompat()`); this script only makes both available
locally so the app never asks jsDelivr for them at run time.
Important: BOTH builds import a *shared* WebAssembly.Memory. Browsers only
allow that in a cross-origin-isolated page, so the WASM path needs COOP/COEP
(provided by the app server, or by the GitHub Pages service worker) even for a
single thread. See web/vendor/README.md.
Run: python scripts/vendor_wllama.py
Uses only Python's standard library; deployment does not need npm or this script.
"""
from hashlib import sha256
from io import BytesIO
from pathlib import Path
import tarfile
import gzip
from urllib.request import urlopen
VERSION = "3.6.1"
ROOT = Path(__file__).resolve().parents[1] / "web" / "vendor"
# (npm archive URL, pinned archive SHA-256, members to extract)
# member -> (filename we ship, expected sha256 of the extracted bytes)
ARCHIVES = (
(
f"https://registry.npmjs.org/@wllama/wllama/-/wllama-{VERSION}.tgz",
"866e9403a8d686e33d5df0512f507ab79fccfbf08902d4b7add4c3a7a706b539",
(
# The unminified esm/index.js is vendored on purpose: a visitor's
# console stack trace should be readable, and the gzip difference
# is small.
("package/esm/index.js", f"wllama-{VERSION}.js",
"ee4b31125271a8d525db06d59724ebdb79c3bda5396eb9e3245f64fd531faf6b"),
("package/esm/wasm/wllama.wasm", f"wllama-{VERSION}.wasm",
"6ca9fdd1b6c03206cd3a04e359b52c8f539896d6c5fb5d36243dded4a689f0ad"),
# The package spells it LICENCE; it is the MIT license for wllama/llama.cpp.
("package/LICENCE", f"wllama-{VERSION}.LICENSE",
"5866e3bd7e3cbd3f7c8bea6efd8a1e7fa7cc8de68c30f428aff7c6584a0fb720"),
),
),
(
f"https://registry.npmjs.org/@wllama/wllama-compat/-/wllama-compat-{VERSION}.tgz",
"c8215faa70ac9c0ebe724aaef82235a8ea45643d9ee9535c6ba308c0d57f0ee8",
(
# Asyncify worker glue. The loader fetches it as text and runs it
# inside a blob worker, so it is served as plain JavaScript.
("package/wasm/wllama.js", f"wllama-compat-{VERSION}.js",
"97f88f7bf26b17ead6be6e7c0c39d20e21661e71827eb5e3c9a468667fc249f5"),
("package/wasm/wllama.wasm", f"wllama-compat-{VERSION}.wasm",
"3c447fe82f5376b1202f909a414348421fe196ae069e1504010f55c0454f85f4"),
),
),
)
# The compat tarball ships no licence file (its package.json declares MIT).
# We keep the upstream MIT notice from the same repository and release.
COMPAT_LICENSE_FROM = ("wllama", f"wllama-{VERSION}.LICENSE")
COMPAT_LICENSE_TO = f"wllama-compat-{VERSION}.LICENSE"
def fetch_verified(url: str, expected_sha256: str) -> bytes:
with urlopen(url, timeout=180) as response:
data = response.read()
if sha256(data).hexdigest() != expected_sha256:
raise RuntimeError(f"{url.rsplit('/', 1)[-1]} archive checksum mismatch; refusing to vendor it")
return data
def main() -> None:
ROOT.mkdir(parents=True, exist_ok=True)
licence_bytes = None
for url, archive_sha, members in ARCHIVES:
data = fetch_verified(url, archive_sha)
with tarfile.open(fileobj=BytesIO(data), mode="r:gz") as archive:
for member, filename, expected in members:
content = archive.extractfile(member).read()
digest = sha256(content).hexdigest()
if digest != expected:
raise RuntimeError(f"{filename} checksum mismatch; refusing to vendor it")
(ROOT / filename).write_bytes(content)
if filename == COMPAT_LICENSE_FROM[1]:
licence_bytes = content
# Same reproducible-gzip convention as the WebGPU runtime: mtime=0 so
# two runs produce byte-identical artifacts and the diff stays empty.
if not filename.endswith(".LICENSE"):
(ROOT / (filename + ".gz")).write_bytes(gzip.compress(content, compresslevel=9, mtime=0))
print(f"{filename}: {digest} ({len(content)} bytes)")
if licence_bytes is None:
raise RuntimeError("upstream MIT licence was not extracted; refusing to vendor the compat build")
(ROOT / COMPAT_LICENSE_TO).write_bytes(licence_bytes)
print(f"{COMPAT_LICENSE_TO}: copied from {COMPAT_LICENSE_FROM[1]} (same upstream MIT notice)")
if __name__ == "__main__":
main()