Ctrl K
ComunidadGuías…

scripts/vendor_wllama.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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()