网上很多方案都是直接更换采用gradio-offline的包。但这个包很久不更新了并且是直接修改源码,所以自己打包也比较麻烦。因此这里提供了一种非侵入式的方法,动态替换部分资源的使用并重定向到本地静态资源,根据需要自己下载资源到本地就可以了。

本方案在gradio 5.34.0上测试通过。理论上兼容一个大版本应该没问题,如果有其他需求可以自己改一下。

使用方法

  1. gr_offline.py 放到你的项目中
  2. 导入补丁并在使用gradio之前调用 patch 函数
  3. 下载离线的静态资源

使用样例

from pathlib import Path

import gr_offline
import gradio as gr
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

RESOURCES_PATH = Path(__file__).parent.joinpath("resources")

gr_offline.patch(resources_path=RESOURCES_PATH, url_prefix="./offline")


app = FastAPI()
RESOURCES_PATH.mkdir(exist_ok=True)
app.mount("/offline", StaticFiles(directory=RESOURCES_PATH), name="offline")


def greet(name: str, intensity: int) -> str:
    return "Hello, " + name + "!" * int(intensity)


demo = gr.Interface(
    fn=greet,
    inputs=["text", "slider"],
    outputs=["text"],
)

app = gr.mount_gradio_app(app, demo, pwa=True, path="")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)

资源替换

  • CDN
    旧 URL: https://cdnjs.cloudflare.com/{...}
    新 URL: ./{url_prefix}/{...}

  • Google Font
    旧 URL: https://fonts.googleapis.com/css2?family={name}:wght@{weight}&display=swap
    新 本地文件: {resources_path}/css/{name}.css

    比如说,如果原始在线的CSS字体文件如下:

    @font-face {
        font-family: "Example Font";
        font-style: normal;
        font-weight: 400;
        src: url(https://exmaple.com/example-font.woff2) format("woff2");
    }
    

    你应该修改并将其存储到本地文件中:{resources_path}/css/Example Font.css:

    @font-face {
        font-family: "Example Font";
        font-style: normal;
        font-weight: 400;
        src: url({url_prefix}/font/example-font.woff2) format("woff2");
    }
    

    最后,下载在线的woff2字体到路径:{resources_path}/font/example-font.woff2

原始资源下载链接

  • https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.1/iframeResizer.contentWindow.min.js
  • https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600&display=swap

gr_offline.py

import os
import re
import types
from pathlib import Path

import jinja2
from fastapi.templating import Jinja2Templates
from gradio import routes
from gradio.themes.utils.fonts import GoogleFont


def _patch_env() -> None:
    os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"


def _patch_font(resources_path: Path) -> None:

    # Load css font from local file when server started
    # If you want to provide it as an external url, please ensure url started with http:// or https://
    # Otherwise external CSS fonts will not be loaded
    def _patched_stylesheet(self: GoogleFont) -> dict:
        file_path = resources_path.joinpath("css").joinpath(f"{self.name}.css")
        if file_path.is_file():
            return {
                "url": None,
                "css": file_path.read_text(),
            }
        else:
            raise FileNotFoundError(f"Font css file '{file_path}' not found")

    GoogleFont.stylesheet = _patched_stylesheet


def _patch_templates(url_prefix: str) -> None:
    remove_regex: list[re.Pattern] = [
        re.compile(
            r"<meta\b[^>]*?property=[\"']og:[^\"'>]*[\"'][^>]*?>",
            flags=re.IGNORECASE | re.DOTALL,
        ),
        re.compile(
            r"<meta\b[^>]*?name=[\"']twitter:[^\"'>]*[\"'][^>]*?>",
            flags=re.IGNORECASE | re.DOTALL,
        ),
        re.compile(
            r"<link\b[^>]*?href=[\"']?[^\"'>]*fonts.googleapis.com[^\"'>]*[\"']?[^>]*?>",
            flags=re.IGNORECASE | re.DOTALL,
        ),
        re.compile(
            r"<link\b[^>]*?href=[\"']?[^\"'>]*fonts.gstatic.com[^\"'>]*[\"']?[^>]*?>",
            flags=re.IGNORECASE | re.DOTALL,
        ),
    ]
    cdn_regex: list[re.Pattern] = [re.compile(r"https?://cdnjs.cloudflare.com.*?")]

    def _do_patch(html: str) -> str:
        for pattern in remove_regex:
            html = re.sub(pattern, "", html)
        for pattern in cdn_regex:
            html = re.sub(pattern, url_prefix, html)
        return html

    def _patched_render(self: jinja2.Template, *args, **kwargs) -> str:
        html = jinja2.Template.render(self, *args, **kwargs)
        return _do_patch(html)

    async def _patched_render_async(self: jinja2.Template, *args, **kwargs) -> str:
        html = await jinja2.Template.render_async(self, *args, **kwargs)
        return _do_patch(html)

    class PatchedJinja2Templates(Jinja2Templates):
        def get_template(self, name: str) -> jinja2.Template:
            template: jinja2.Template = super().get_template(name)
            template.render = types.MethodType(_patched_render, template)
            template.render_async = types.MethodType(_patched_render_async, template)
            return template

    routes.templates = PatchedJinja2Templates(directory=routes.STATIC_TEMPLATE_LIB)
    routes.templates.env.filters["toorjson"] = routes.toorjson


def patch(resources_path: Path, url_prefix: str) -> None:
    _patch_env()
    _patch_font(resources_path)
    _patch_templates(url_prefix)

Github:Gist