diff --git a/.changeset/shiny-news-float.md b/.changeset/shiny-news-float.md new file mode 100644 index 000000000000..62e125ee5a37 --- /dev/null +++ b/.changeset/shiny-news-float.md @@ -0,0 +1,13 @@ +--- +"@gradio/app": minor +"@gradio/audio": minor +"@gradio/client": minor +"@gradio/code": minor +"@gradio/file": minor +"@gradio/image": minor +"@gradio/video": minor +"@gradio/wasm": minor +"gradio": minor +--- + +feat:Lite v4 diff --git a/client/js/src/client.ts b/client/js/src/client.ts index 7ff732df7893..2135324f76b5 100644 --- a/client/js/src/client.ts +++ b/client/js/src/client.ts @@ -178,7 +178,7 @@ interface Client { export function api_factory( fetch_implementation: typeof fetch, - WebSocket_factory: (url: URL) => WebSocket + EventSource_factory: (url: URL) => EventSource ): Client { return { post_data, upload_files, client, handle_blob }; @@ -546,7 +546,7 @@ export function api_factory( url.searchParams.set("__sign", jwt); } - websocket = WebSocket_factory(url); + websocket = new WebSocket(url); websocket.onclose = (evt) => { if (!evt.wasClean) { @@ -667,7 +667,7 @@ export function api_factory( )}/queue/join?${url_params ? url_params + "&" : ""}${params}` ); - eventSource = new EventSource(url); + eventSource = EventSource_factory(url); eventSource.onmessage = async function (event) { const _data = JSON.parse(event.data); @@ -1007,7 +1007,7 @@ export function api_factory( export const { post_data, upload_files, client, handle_blob } = api_factory( fetch, - (...args) => new WebSocket(...args) + (...args) => new EventSource(...args) ); function transform_output( diff --git a/gradio/components/video.py b/gradio/components/video.py index 273a32b548bc..fc66320ada10 100644 --- a/gradio/components/video.py +++ b/gradio/components/video.py @@ -165,16 +165,20 @@ def preprocess(self, payload: VideoData | None) -> str | None: uploaded_format = file_name.suffix.replace(".", "") needs_formatting = self.format is not None and uploaded_format != self.format flip = self.sources == ["webcam"] and self.mirror_webcam - duration = processing_utils.get_video_length(file_name) - if self.min_length is not None and duration < self.min_length: - raise gr.Error( - f"Video is too short, and must be at least {self.min_length} seconds" - ) - if self.max_length is not None and duration > self.max_length: - raise gr.Error( - f"Video is too long, and must be at most {self.max_length} seconds" - ) + if self.min_length is not None or self.max_length is not None: + # With this if-clause, avoid unnecessary execution of `processing_utils.get_video_length`. + # This is necessary for the Wasm-mode, because it uses ffprobe, which is not available in the browser. + duration = processing_utils.get_video_length(file_name) + if self.min_length is not None and duration < self.min_length: + raise gr.Error( + f"Video is too short, and must be at least {self.min_length} seconds" + ) + if self.max_length is not None and duration > self.max_length: + raise gr.Error( + f"Video is too long, and must be at most {self.max_length} seconds" + ) + if needs_formatting or flip: format = f".{self.format if needs_formatting else uploaded_format}" output_options = ["-vf", "hflip", "-c:a", "copy"] if flip else [] diff --git a/gradio/data_classes.py b/gradio/data_classes.py index 86b89408eb88..8a87f7ac5a3c 100644 --- a/gradio/data_classes.py +++ b/gradio/data_classes.py @@ -11,9 +11,56 @@ from fastapi import Request from gradio_client.utils import traverse -from pydantic import BaseModel, RootModel, ValidationError from typing_extensions import Literal +from . import wasm_utils + +if not wasm_utils.IS_WASM: + from pydantic import BaseModel, RootModel, ValidationError # type: ignore +else: + # XXX: Currently Pyodide V2 is not available on Pyodide, + # so we install V1 for the Wasm version. + from typing import Generic, TypeVar + + from pydantic import BaseModel as BaseModelV1 + from pydantic import ValidationError, schema_of + + # Map V2 method calls to V1 implementations. + # Ref: https://docs.pydantic.dev/latest/migration/#changes-to-pydanticbasemodel + class BaseModel(BaseModelV1): + pass + + BaseModel.model_dump = BaseModel.dict # type: ignore + BaseModel.model_json_schema = BaseModel.schema # type: ignore + + # RootModel is not available in V1, so we create a dummy class. + PydanticUndefined = object() + RootModelRootType = TypeVar("RootModelRootType") + + class RootModel(BaseModel, Generic[RootModelRootType]): + root: RootModelRootType + + def __init__(self, root: RootModelRootType = PydanticUndefined, **data): + if data: + if root is not PydanticUndefined: + raise ValueError( + '"RootModel.__init__" accepts either a single positional argument or arbitrary keyword arguments' + ) + root = data # type: ignore + # XXX: No runtime validation is executed. + super().__init__(root=root) # type: ignore + + def dict(self, **kwargs): + return super().dict(**kwargs)["root"] + + @classmethod + def schema(cls, **kwargs): + # XXX: kwargs are ignored. + return schema_of(cls.__fields__["root"].type_) # type: ignore + + RootModel.model_dump = RootModel.dict # type: ignore + RootModel.model_json_schema = RootModel.schema # type: ignore + class PredictBody(BaseModel): class Config: diff --git a/gradio/processing_utils.py b/gradio/processing_utils.py index e46318740989..35bdcd19f925 100644 --- a/gradio/processing_utils.py +++ b/gradio/processing_utils.py @@ -712,6 +712,10 @@ def convert_video_to_playable_mp4(video_path: str) -> str: def get_video_length(video_path: str | Path): + if wasm_utils.IS_WASM: + raise wasm_utils.WasmUnsupportedError( + "Video duration is not supported in the Wasm mode." + ) duration = subprocess.check_output( [ "ffprobe", diff --git a/js/app/src/lite/index.ts b/js/app/src/lite/index.ts index ce9f682e0d2a..163123c4fe20 100644 --- a/js/app/src/lite/index.ts +++ b/js/app/src/lite/index.ts @@ -1,9 +1,12 @@ -import "@gradio/theme"; +import "@gradio/theme/src/reset.css"; +import "@gradio/theme/src/global.css"; +import "@gradio/theme/src/pollen.css"; +import "@gradio/theme/src/typography.css"; import type { SvelteComponent } from "svelte"; import { WorkerProxy, type WorkerProxyOptions } from "@gradio/wasm"; import { api_factory } from "@gradio/client"; import { wasm_proxied_fetch } from "./fetch"; -import { wasm_proxied_WebSocket_factory } from "./websocket"; +import { wasm_proxied_EventSource_factory } from "./sse"; import { wasm_proxied_mount_css, mount_prebuilt_css } from "./css"; import type { mount_css } from "../css"; import Index from "../Index.svelte"; @@ -101,12 +104,12 @@ export function create(options: Options): GradioAppController { const overridden_fetch: typeof fetch = (input, init?) => { return wasm_proxied_fetch(worker_proxy, input, init); }; - const WebSocket_factory = (url: URL): WebSocket => { - return wasm_proxied_WebSocket_factory(worker_proxy, url); + const EventSource_factory = (url: URL): EventSource => { + return wasm_proxied_EventSource_factory(worker_proxy, url); }; const { client, upload_files } = api_factory( overridden_fetch, - WebSocket_factory + EventSource_factory ); const overridden_mount_css: typeof mount_css = async (url, target) => { return wasm_proxied_mount_css(worker_proxy, url, target); diff --git a/js/app/src/lite/websocket.ts b/js/app/src/lite/sse.ts similarity index 56% rename from js/app/src/lite/websocket.ts rename to js/app/src/lite/sse.ts index 0e97d5b96633..7b2e6038a689 100644 --- a/js/app/src/lite/websocket.ts +++ b/js/app/src/lite/sse.ts @@ -1,4 +1,4 @@ -import type { WorkerProxy } from "@gradio/wasm"; +import { type WorkerProxy, WasmWorkerEventSource } from "@gradio/wasm"; import { is_self_host } from "@gradio/wasm/network"; /** @@ -6,14 +6,14 @@ import { is_self_host } from "@gradio/wasm/network"; * which also falls back to the original WebSocket() for external resource requests. */ -export function wasm_proxied_WebSocket_factory( +export function wasm_proxied_EventSource_factory( worker_proxy: WorkerProxy, url: URL -): WebSocket { +): EventSource { if (!is_self_host(url)) { console.debug("Fallback to original WebSocket"); - return new WebSocket(url); + return new EventSource(url); } - return worker_proxy.openWebSocket(url.pathname) as unknown as WebSocket; + return new WasmWorkerEventSource(worker_proxy, url) as unknown as EventSource; } diff --git a/js/app/vite.config.ts b/js/app/vite.config.ts index b60881274664..fed40d27dce7 100644 --- a/js/app/vite.config.ts +++ b/js/app/vite.config.ts @@ -47,6 +47,7 @@ export default defineConfig(({ mode }) => { "dev:custom": "../../gradio/templates/frontend" }; const production = mode === "production" || mode === "production:lite"; + const development = mode === "development" || mode === "development:lite"; const is_lite = mode.endsWith(":lite"); return { @@ -131,7 +132,7 @@ export default defineConfig(({ mode }) => { } }, plugins: [ - resolve_svelte(mode === "development"), + resolve_svelte(development && !is_lite), svelte({ inspector: true, @@ -150,7 +151,12 @@ export default defineConfig(({ mode }) => { } }) }), - generate_dev_entry({ enable: mode !== "development" && mode !== "test" }), + generate_dev_entry({ + enable: + !development && + !is_lite && // At the moment of https://github.com/gradio-app/gradio/pull/6398, I skipped to make Gradio-lite work custom component. Will do it, and remove this condition. + mode !== "test" + }), inject_ejs(), generate_cdn_entry({ version: GRADIO_VERSION, cdn_base: CDN_BASE }), handle_ce_css(), diff --git a/js/audio/interactive/InteractiveAudio.svelte b/js/audio/interactive/InteractiveAudio.svelte index 1652aa599bfa..2db20039d464 100644 --- a/js/audio/interactive/InteractiveAudio.svelte +++ b/js/audio/interactive/InteractiveAudio.svelte @@ -1,7 +1,12 @@ - - - {#if copied} - - {/if} - +
+ + + {#if copied} + + {/if} + +