Skip to content

Commit

Permalink
Lite v4 (#6398)
Browse files Browse the repository at this point in the history
* Fix vite.config.js detecting the development mode

* Fix the imports of @gradio/theme in js/app/src/lite/index.ts

* [WIP] Install Pydantic V1 and mock the RootModel class

* Remove Wasm WebSocket implementations

* Move ASGI-HTTP conversion logic from the worker to the worker-proxy so we have fine controls on the ASGI connection at the worker-proxy level for the HTTP stream connection impl in the future

* Fix asgi-types.ts

* Create `WasmWorkerEventSource` and inject the `EventSource` creation in @gradio/client

* Mock Pydantic V2's BaseModel

* Fix Pydantic V1 installation

* Make <ImageUploader /> and <ImagePreview /> Wasm-compatible

* Create `getHeaderValue()`

* Create `<DownloadLink />` for Wasm-compatible download and fix `<ImagePreview />` to use it

* Make `gr.Video()` Wasm-compatible avoiding unnecessary execution of ffprobe

* Move `<DownloadLink />` to @gradio/wasm and use it in `<VideoPreview />` too

* Fix `<DownloadLink />` making `href` optional and adding `rel="noopener noreferrer"`

* Make the download button of `<StaticAudio>` and `<Code />` Wasm-compatible

* Make the download button of `<FilePreview />` Wasm-compatible

* Improve the RootModel mock class for `.model_dump()` and `.model_json_schame()` to work

* Make `<UploadProgress />` Wasm-compatible

* Fix `WorkerProxy.httpRequest()` to use `decodeURIComponent()` to process `path` and `query_string`

* Fix `<InteractiveAudio />` to make its upload feature Wasm-compatible

* [WIP] Revert "Make `<UploadProgress />` Wasm-compatible"

This reverts commit f96b4b7.

* Fix Image styles

* Fix `<AudioPlayer />`'s `create_waveform()` to be Wasm-compatible

* add changeset

* formatting

* Fix js/image/shared/Image.svelte to render <img> immediately

* Fix js/image/shared/Image.svelte to avoid race condition

* Fix  js/image/shared/Image.svelte

* Fix  js/image/shared/Image.svelte

* Fix js/image/shared/Image.svelte removing unnecessary styles

* Fix js/video/shared/Video.svelte to use the passed  immediately without waiting for the async resolution

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: aliabd <ali.si3luwa@gmail.com>
Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
  • Loading branch information
4 people committed Dec 12, 2023
1 parent 9a6ff70 commit 67ddd40
Show file tree
Hide file tree
Showing 36 changed files with 765 additions and 664 deletions.
13 changes: 13 additions & 0 deletions .changeset/shiny-news-float.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions client/js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 13 additions & 9 deletions gradio/components/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down
49 changes: 48 additions & 1 deletion gradio/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions gradio/processing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 8 additions & 5 deletions js/app/src/lite/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions js/app/src/lite/websocket.ts → js/app/src/lite/sse.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { WorkerProxy } from "@gradio/wasm";
import { type WorkerProxy, WasmWorkerEventSource } from "@gradio/wasm";
import { is_self_host } from "@gradio/wasm/network";

/**
* A WebSocket factory that proxies requests to the worker,
* 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;
}
10 changes: 8 additions & 2 deletions js/app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -131,7 +132,7 @@ export default defineConfig(({ mode }) => {
}
},
plugins: [
resolve_svelte(mode === "development"),
resolve_svelte(development && !is_lite),

svelte({
inspector: true,
Expand All @@ -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(),
Expand Down
18 changes: 15 additions & 3 deletions js/audio/interactive/InteractiveAudio.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<script lang="ts">
import { onDestroy, createEventDispatcher } from "svelte";
import { getContext, onDestroy, createEventDispatcher } from "svelte";
import { Upload, ModifyUpload } from "@gradio/upload";
import { upload, prepare_files, type FileData } from "@gradio/client";
import {
upload,
prepare_files,
type FileData,
type upload_files
} from "@gradio/client";
import { BlockLabel } from "@gradio/atoms";
import { Music } from "@gradio/icons";
import AudioPlayer from "../player/AudioPlayer.svelte";
Expand Down Expand Up @@ -33,6 +38,9 @@
export let handle_reset_value: () => void = () => {};
export let editable = true;
// Needed for wasm support
const upload_fn = getContext<typeof upload_files>("upload_files");
$: dispatch("drag", dragging);
// TODO: make use of this
Expand Down Expand Up @@ -87,7 +95,11 @@
): Promise<void> => {
let _audio_blob = new File(blobs, "audio.wav");
const val = await prepare_files([_audio_blob], event === "stream");
value = ((await upload(val, root))?.filter(Boolean) as FileData[])[0];
value = (
(await upload(val, root, undefined, upload_fn))?.filter(
Boolean
) as FileData[]
)[0];
dispatch(event, value);
};
Expand Down
6 changes: 5 additions & 1 deletion js/audio/player/AudioPlayer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@
const create_waveform = (): void => {
waveform = WaveSurfer.create({
container: container,
url: value?.url,
...waveform_settings
});
resolve_wasm_src(value?.url).then((resolved_src) => {
if (resolved_src && waveform) {
return waveform.load(resolved_src);
}
});
};
$: if (container !== undefined) {
Expand Down
9 changes: 3 additions & 6 deletions js/audio/static/StaticAudio.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import AudioPlayer from "../player/AudioPlayer.svelte";
import { createEventDispatcher } from "svelte";
import type { FileData } from "@gradio/client";
import { DownloadLink } from "@gradio/wasm/svelte";
import type { WaveformOptions } from "../shared/types";
export let value: null | FileData = null;
Expand Down Expand Up @@ -39,13 +40,9 @@
{#if value !== null}
<div class="icon-buttons">
{#if show_download_button}
<a
href={value.url}
target={window.__is_colab__ ? "_blank" : null}
download={value.orig_name || value.path}
>
<DownloadLink href={value.url} download={value.orig_name || value.path}>
<IconButton Icon={Download} label={i18n("common.download")} />
</a>
</DownloadLink>
{/if}
{#if show_share_button}
<ShareButton
Expand Down
1 change: 1 addition & 0 deletions js/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@gradio/statustracker": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^",
"@gradio/wasm": "workspace:^",
"@lezer/common": "^1.0.2",
"@lezer/highlight": "^1.1.3",
"@lezer/markdown": "^1.0.2",
Expand Down
30 changes: 14 additions & 16 deletions js/code/shared/Download.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onDestroy } from "svelte";
import { fade } from "svelte/transition";
import { Download, Check } from "@gradio/icons";
import { DownloadLink } from "@gradio/wasm/svelte";
export let value: string;
export let language: string;
Expand Down Expand Up @@ -50,20 +51,21 @@
});
</script>

<a
download="file.{ext}"
href={download_value}
class:copied
on:click={copy_feedback}
>
<Download />
{#if copied}
<span class="check" transition:fade><Check /></span>
{/if}
</a>
<div class="container">
<DownloadLink
download="file.{ext}"
href={download_value}
on:click={copy_feedback}
>
<Download />
{#if copied}
<span class="check" transition:fade><Check /></span>
{/if}
</DownloadLink>
</div>

<style>
a {
.container {
position: relative;
cursor: pointer;
padding: 5px;
Expand All @@ -72,10 +74,6 @@
height: 22px;
}
.copied {
color: var(--color-green-500);
}
.check {
position: absolute;
top: 0;
Expand Down
3 changes: 2 additions & 1 deletion js/file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"@gradio/icons": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/utils": "workspace:^"
"@gradio/utils": "workspace:^",
"@gradio/wasm": "workspace:^"
},
"main": "./Index.svelte",
"main_changeset": true,
Expand Down

0 comments on commit 67ddd40

Please sign in to comment.