Skip to content

Instantly share code, notes, and snippets.

@XFY9326
Last active July 2, 2025 16:59
Show Gist options
  • Save XFY9326/8c6c89950a8eeed32d4990f01ee0c587 to your computer and use it in GitHub Desktop.
Save XFY9326/8c6c89950a8eeed32d4990f01ee0c587 to your computer and use it in GitHub Desktop.
Simple patch for gradio to make it work offline

Gradio offline patch

Usage

  1. Put gr_offline.py in your project
  2. Import patch and call patch function before using gradio
  3. Download offline static resources

Example

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)

Resources

  • CDN
    Old url: https://cdnjs.cloudflare.com/{...}
    New url: ./{url_prefix}/{...}

  • Google Font
    Old url: https://fonts.googleapis.com/css2?family={name}:wght@{weight}&display=swap
    New local file: {resource_path}/css/{name}.css

    For example, if the original online css font file looks like:

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

    You should change and save a new local file in {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");
    }

    And then, download online woff2 fonts into {resources_path}/font/example-font.woff2

Raw resources download URL

  • 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment