mirror of
https://github.com/gradio-app/gradio.git
synced 2025-03-25 12:10:31 +08:00
Patch async_save_url_to_cache
for Lite (#8026)
* Update `async_save_url_to_cache` to work on Wasm * Refactoring `save_url_to_cache` * add changeset * Fix * Use pyodide.http as a custom transport of httpx * Use urllib3 as a custom transport of httpx to make sync http requests * Add an E2E test case to detect the bugs on remote resource caching * add changeset * Add image_remote_url E2E test * add changeset --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
55ef4a52c3
commit
522daf787a
5
.changeset/silly-seals-care.md
Normal file
5
.changeset/silly-seals-care.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"gradio": patch
|
||||
---
|
||||
|
||||
fix:Patch `async_save_url_to_cache` for Lite
|
@ -39,7 +39,8 @@ const lite = defineConfig(base, {
|
||||
"**/file_component_events.spec.ts",
|
||||
"**/chatbot_multimodal.spec.ts",
|
||||
"**/kitchen_sink.spec.ts",
|
||||
"**/gallery_component_events.spec.ts"
|
||||
"**/gallery_component_events.spec.ts",
|
||||
"**/image_remote_url.spec.ts" // To detect the bugs on Lite fixed in https://github.com/gradio-app/gradio/pull/8011 and https://github.com/gradio-app/gradio/pull/8026
|
||||
],
|
||||
workers: 1,
|
||||
retries: 3
|
||||
|
1
demo/image_remote_url/run.ipynb
Normal file
1
demo/image_remote_url/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_remote_url"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def fn(im):\n", " return im, \"https://picsum.photos/400/300\"\n", "\n", "\n", "demo = gr.Interface(\n", " fn=fn,\n", " inputs=gr.Image(\"https://picsum.photos/300/200\", label=\"InputImage\"),\n", " outputs=[gr.Image(label=\"Loopback\"), gr.Image(label=\"RemoteImage\")],\n", " examples=[\n", " [\"https://picsum.photos/640/480\"]\n", " ]\n", ")\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
19
demo/image_remote_url/run.py
Normal file
19
demo/image_remote_url/run.py
Normal file
@ -0,0 +1,19 @@
|
||||
import gradio as gr
|
||||
|
||||
|
||||
def fn(im):
|
||||
return im, "https://picsum.photos/400/300"
|
||||
|
||||
|
||||
demo = gr.Interface(
|
||||
fn=fn,
|
||||
inputs=gr.Image("https://picsum.photos/300/200", label="InputImage"),
|
||||
outputs=[gr.Image(label="Loopback"), gr.Image(label="RemoteImage")],
|
||||
examples=[
|
||||
["https://picsum.photos/640/480"]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -16,7 +16,6 @@ from typing import TYPE_CHECKING, Any
|
||||
import aiofiles
|
||||
import httpx
|
||||
import numpy as np
|
||||
import urllib3
|
||||
from gradio_client import utils as client_utils
|
||||
from PIL import Image, ImageOps, PngImagePlugin
|
||||
|
||||
@ -28,7 +27,76 @@ with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # Ignore pydub warning if ffmpeg is not installed
|
||||
from pydub import AudioSegment
|
||||
|
||||
async_client = httpx.AsyncClient()
|
||||
if wasm_utils.IS_WASM:
|
||||
import pyodide.http # type: ignore
|
||||
import urllib3
|
||||
|
||||
# NOTE: In the Wasm env, we use urllib3 to make HTTP requests. See https://github.com/gradio-app/gradio/issues/6837.
|
||||
class Urllib3ResponseSyncByteStream(httpx.SyncByteStream):
|
||||
def __init__(self, response) -> None:
|
||||
self.response = response
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.response.stream()
|
||||
|
||||
class Urllib3Transport(httpx.BaseTransport):
|
||||
def __init__(self):
|
||||
self.pool = urllib3.PoolManager()
|
||||
|
||||
def handle_request(self, request: httpx.Request) -> httpx.Response:
|
||||
url = str(request.url)
|
||||
method = request.method
|
||||
headers = dict(request.headers)
|
||||
body = None if method in ["GET", "HEAD"] else request.read()
|
||||
|
||||
response = self.pool.request(
|
||||
headers=headers,
|
||||
method=method,
|
||||
url=url,
|
||||
body=body,
|
||||
preload_content=False, # Stream the content
|
||||
)
|
||||
|
||||
return httpx.Response(
|
||||
status_code=response.status,
|
||||
headers=response.headers,
|
||||
stream=Urllib3ResponseSyncByteStream(response),
|
||||
)
|
||||
|
||||
sync_transport = Urllib3Transport()
|
||||
|
||||
class PyodideHttpResponseAsyncByteStream(httpx.AsyncByteStream):
|
||||
def __init__(self, response) -> None:
|
||||
self.response = response
|
||||
|
||||
async def __aiter__(self):
|
||||
yield await self.response.bytes()
|
||||
|
||||
class PyodideHttpTransport(httpx.AsyncBaseTransport):
|
||||
async def handle_async_request(
|
||||
self,
|
||||
request: httpx.Request,
|
||||
) -> httpx.Response:
|
||||
url = str(request.url)
|
||||
method = request.method
|
||||
headers = dict(request.headers)
|
||||
body = None if method in ["GET", "HEAD"] else await request.aread()
|
||||
response = await pyodide.http.pyfetch(
|
||||
url, method=method, headers=headers, body=body
|
||||
)
|
||||
return httpx.Response(
|
||||
status_code=response.status,
|
||||
headers=response.headers,
|
||||
stream=PyodideHttpResponseAsyncByteStream(response),
|
||||
)
|
||||
|
||||
async_transport = PyodideHttpTransport()
|
||||
else:
|
||||
sync_transport = None
|
||||
async_transport = None
|
||||
|
||||
sync_client = httpx.Client(transport=sync_transport)
|
||||
async_client = httpx.AsyncClient(transport=async_transport)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -209,15 +277,10 @@ def save_url_to_cache(url: str, cache_dir: str) -> str:
|
||||
full_temp_file_path = str(abspath(temp_dir / name))
|
||||
|
||||
if not Path(full_temp_file_path).exists():
|
||||
# NOTE: We use urllib3 instead of httpx because it works in the Wasm environment. See https://github.com/gradio-app/gradio/issues/6837.
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request(
|
||||
"GET",
|
||||
url,
|
||||
preload_content=False,
|
||||
)
|
||||
with open(full_temp_file_path, "wb") as f:
|
||||
for chunk in response.stream():
|
||||
with sync_client.stream("GET", url, follow_redirects=True) as r, open(
|
||||
full_temp_file_path, "wb"
|
||||
) as f:
|
||||
for chunk in r.iter_raw():
|
||||
f.write(chunk)
|
||||
|
||||
return full_temp_file_path
|
||||
|
30
js/app/test/image_remote_url.spec.ts
Normal file
30
js/app/test/image_remote_url.spec.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { test, expect } from "@gradio/tootils";
|
||||
|
||||
test("Image displays remote image correctly", async ({ page }) => {
|
||||
const example_image = page.locator(
|
||||
'div.block:has(div.label:has-text("Examples")) img'
|
||||
);
|
||||
const input_image = page.locator(
|
||||
'div.block:has(label:has-text("InputImage")) img'
|
||||
);
|
||||
const loopback_image = page.locator(
|
||||
'div.block:has(label:has-text("Loopback")) img'
|
||||
);
|
||||
const remote_output_image = page.locator(
|
||||
'div.block:has(label:has-text("RemoteImage")) img'
|
||||
);
|
||||
const submit_button = page.locator('button:has-text("Submit")');
|
||||
|
||||
await expect(example_image).toHaveJSProperty("complete", true);
|
||||
await expect(example_image).not.toHaveJSProperty("naturalWidth", 0);
|
||||
|
||||
await expect(input_image).toHaveJSProperty("complete", true);
|
||||
await expect(input_image).not.toHaveJSProperty("naturalWidth", 0);
|
||||
|
||||
await submit_button.click();
|
||||
|
||||
await expect(loopback_image).toHaveJSProperty("complete", true);
|
||||
await expect(loopback_image).not.toHaveJSProperty("naturalWidth", 0);
|
||||
await expect(remote_output_image).toHaveJSProperty("complete", true);
|
||||
await expect(remote_output_image).not.toHaveJSProperty("naturalWidth", 0);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user