Files
gradio-app--gradio/scripts/download_offline_assets.py
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

222 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Download static assets required for offline Gradio usage."""
from __future__ import annotations
import re
import shutil
import ssl
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
STATIC = ROOT / "gradio" / "templates" / "frontend" / "static"
FONTS_DIR = STATIC / "fonts"
WEIGHT_TO_FILENAME = {
100: "Thin",
200: "ExtraLight",
300: "Light",
400: "Regular",
500: "Medium",
600: "SemiBold",
700: "Bold",
800: "ExtraBold",
900: "Black",
}
THEME_FONTS: dict[str, list[int]] = {
"Source Sans Pro": [400, 600],
"IBM Plex Sans": [400, 600],
"IBM Plex Mono": [400, 500, 600],
"Montserrat": [400, 700],
"Quicksand": [400, 700],
"Inconsolata": [400, 700],
"Lexend": [400, 600],
"Space Mono": [400],
"Source Sans 3": [400, 600],
"Source Code Pro": [400, 600],
"Chakra Petch": [400, 600],
"Share Tech Mono": [400],
"Lora": [400, 600],
"Ubuntu": [400],
"Roboto Mono": [400, 600],
"Press Start 2P": [400],
}
# Baseline Bokeh version to bundle. The version that gradio's Plot component
# actually requests at runtime is `bokeh.__version__` of the installed package,
# so `bokeh_versions()` always adds that too -- otherwise the frontend 404s on
# /static/bokeh/{installed_version}/ and falls back to the CDN (which defeats
# offline support).
BOKEH_VERSIONS = ["3.8.1"]
BOKEH_FILES = [
"bokeh-{version}.min.js",
"bokeh-widgets-{version}.min.js",
"bokeh-tables-{version}.min.js",
"bokeh-gl-{version}.min.js",
"bokeh-api-{version}.min.js",
]
USER_AGENT = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
def _ssl_context() -> ssl.SSLContext:
# python.org Python on macOS ships without a usable system CA bundle, so the
# default context can't verify TLS. Prefer certifi's bundle when available
# (it's a transitive Gradio dependency). This still performs full
# certificate verification -- it does not disable it.
try:
import certifi
except ImportError:
return ssl.create_default_context()
return ssl.create_default_context(cafile=certifi.where())
def fetch(url: str) -> bytes:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request, timeout=60, context=_ssl_context()) as response:
return response.read()
LEGACY_FONT_DIRS = {
"Source Sans Pro": "SourceSansPro",
}
def migrate_legacy_font_dirs() -> None:
for legacy, modern in LEGACY_FONT_DIRS.items():
legacy_dir = FONTS_DIR / legacy
modern_dir = FONTS_DIR / modern
if not legacy_dir.exists():
continue
modern_dir.mkdir(parents=True, exist_ok=True)
for font_file in legacy_dir.glob("*.woff2"):
dest = modern_dir / font_file.name
if not dest.exists():
shutil.copy2(font_file, dest)
shutil.rmtree(legacy_dir)
def download_fonts() -> None:
migrate_legacy_font_dirs()
for family, weights in THEME_FONTS.items():
dir_name = family.replace(" ", "")
family_dir = FONTS_DIR / dir_name
family_dir.mkdir(parents=True, exist_ok=True)
# Skip the network round-trip entirely when every weight is already on
# disk, so cached/air-gapped rebuilds don't need to reach Google Fonts.
if all(
(family_dir / f"{dir_name}-{WEIGHT_TO_FILENAME.get(w, str(w))}.woff2").exists()
for w in weights
):
continue
weight_spec = ";".join(str(w) for w in weights)
css_url = (
f"https://fonts.googleapis.com/css2?family="
f"{family.replace(' ', '+')}:wght@{weight_spec}&display=swap"
)
css_text = fetch(css_url).decode("utf-8")
blocks = re.findall(
r"font-weight:\s*(\d+);.*?src:\s*url\((https://[^)]+)\)\s*format\('woff2'\)",
css_text,
re.DOTALL,
)
for weight_str, woff2_url in blocks:
weight = int(weight_str)
if weight not in weights:
continue
file_weight = WEIGHT_TO_FILENAME.get(weight, str(weight))
dest = family_dir / f"{dir_name}-{file_weight}.woff2"
if dest.exists():
continue
dest.write_bytes(fetch(woff2_url))
print(f" font: {dest.relative_to(ROOT)}")
def download_iframe_resizer() -> None:
dest_dir = STATIC / "js"
dest_dir.mkdir(parents=True, exist_ok=True)
dest = dest_dir / "iframeResizer.contentWindow.min.js"
# Pin the iframe-resizer *child* to v4.3.1. It has to match the iframe-resizer
# *parent* that HF Spaces runs to embed apps: a v5 child speaks a different
# postMessage protocol that the v4 parent can't handshake with, so
# `window.parentIFrame` never appears in the embedded app and the height
# reporting in Blocks.svelte silently never runs (the app won't size to its
# content on Spaces). v4.3.1 is also MIT-licensed, whereas v5 is GPL-3.0.
# `@iframe-resizer/child` in node_modules is v5 (a devDependency of the spaces
# test harness), so fetch the pinned v4.3.1 build rather than copying it.
url = (
"https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.1/"
"iframeResizer.contentWindow.min.js"
)
dest.write_bytes(fetch(url))
print(f" iframe-resizer: {dest.relative_to(ROOT)}")
def bokeh_versions() -> list[str]:
versions = list(BOKEH_VERSIONS)
try:
import bokeh # type: ignore
if bokeh.__version__ not in versions:
versions.append(bokeh.__version__)
except ImportError:
pass
return versions
def download_bokeh() -> None:
for version in bokeh_versions():
version_dir = STATIC / "bokeh" / version
version_dir.mkdir(parents=True, exist_ok=True)
for pattern in BOKEH_FILES:
filename = pattern.format(version=version)
dest = version_dir / filename
if dest.exists():
continue
if filename == f"bokeh-{version}.min.js":
base = "https://cdn.bokeh.org/bokeh/release"
else:
base = "https://cdn.pydata.org/bokeh/release"
url = f"{base}/{filename}"
dest.write_bytes(fetch(url))
print(f" bokeh: {dest.relative_to(ROOT)}")
def download_ffmpeg() -> None:
dest_dir = STATIC / "ffmpeg"
dest_dir.mkdir(parents=True, exist_ok=True)
core_dir = ROOT / "node_modules" / "@ffmpeg" / "core" / "dist" / "esm"
files = ["ffmpeg-core.js", "ffmpeg-core.wasm"]
for name in files:
src = core_dir / name
dest = dest_dir / name
if src.exists():
shutil.copy2(src, dest)
print(f" ffmpeg: {dest.relative_to(ROOT)}")
elif not dest.exists():
base = "https://unpkg.com/@ffmpeg/core@0.12.4/dist/esm"
dest.write_bytes(fetch(f"{base}/{name}"))
print(f" ffmpeg: {dest.relative_to(ROOT)}")
def main() -> None:
print("Downloading offline assets...")
print("fonts:")
download_fonts()
print("iframe-resizer:")
download_iframe_resizer()
print("bokeh:")
download_bokeh()
print("ffmpeg:")
download_ffmpeg()
print("Done.")
if __name__ == "__main__":
main()