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:
Yuichiro Tachibana (Tsuchiya) 2024-04-17 03:54:34 +09:00 committed by GitHub
parent 55ef4a52c3
commit 522daf787a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 131 additions and 12 deletions

View File

@ -0,0 +1,5 @@
---
"gradio": patch
---
fix:Patch `async_save_url_to_cache` for Lite

View File

@ -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

View 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}

View 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()

View File

@ -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

View 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);
});