mirror of
https://github.com/gradio-app/gradio.git
synced 2025-03-25 12:10:31 +08:00
Add Upload Button (#2591)
* add upload button * upload button * changelog * Update CHANGELOG.md * format * update file component * upload button fixes * label * remove fileupload changes * remove file component changes * restore lock file * pnpm lock file * Update CHANGELOG.md * fixes * fixes * more fixes * fix tests * fixes * lockfile * fix test * Update gradio/components.py Co-authored-by: Abubakar Abid <abubakar@huggingface.co> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
This commit is contained in:
parent
084b2b832e
commit
b5f22671a2
19
CHANGELOG.md
19
CHANGELOG.md
@ -2,6 +2,25 @@
|
||||
|
||||
## New Features:
|
||||
|
||||
### Upload Button
|
||||
There is now a new component called the `UploadButton` which is a file upload component but in button form! You can also specify what file types it should accept in the form of a list (ex: `image`, `video`, `audio`, `text`, or generic `file`). Added by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 2591](https://github.com/gradio-app/gradio/pull/2591).
|
||||
|
||||
Example of how it can be used:
|
||||
|
||||
```python
|
||||
import gradio as gr
|
||||
|
||||
def upload_file(files):
|
||||
file_paths = [file.name for file in files]
|
||||
return file_paths
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
file_output = gr.File()
|
||||
upload_button = gr.UploadButton("Click to Upload a File", file_types=["image", "video"], file_count="multiple")
|
||||
upload_button.upload(upload_file, upload_button, file_output)
|
||||
|
||||
demo.launch()
|
||||
```
|
||||
### Revamped API documentation page
|
||||
|
||||
New API Docs page with in-browser playground and updated aesthetics. [@gary149](https://github.com/gary149) in [PR 2652](https://github.com/gradio-app/gradio/pull/2652)
|
||||
|
1
demo/upload_button/DESCRIPTION.md
Normal file
1
demo/upload_button/DESCRIPTION.md
Normal file
@ -0,0 +1 @@
|
||||
A simple demo showcasing the upload button used with its `upload` event trigger.
|
12
demo/upload_button/run.py
Normal file
12
demo/upload_button/run.py
Normal file
@ -0,0 +1,12 @@
|
||||
import gradio as gr
|
||||
|
||||
def upload_file(files):
|
||||
file_paths = [file.name for file in files]
|
||||
return file_paths
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
file_output = gr.File()
|
||||
upload_button = gr.UploadButton("Click to Upload a File", file_types=["image", "video"], file_count="multiple")
|
||||
upload_button.upload(upload_file, upload_button, file_output)
|
||||
|
||||
demo.launch()
|
@ -42,6 +42,7 @@ from gradio.components import (
|
||||
Textbox,
|
||||
TimeSeries,
|
||||
Timeseries,
|
||||
UploadButton,
|
||||
Variable,
|
||||
Video,
|
||||
component,
|
||||
|
@ -2086,6 +2086,7 @@ class File(Changeable, Clearable, Uploadable, IOComponent, FileSerializable):
|
||||
value: Optional[str | List[str] | Callable] = None,
|
||||
*,
|
||||
file_count: str = "single",
|
||||
file_types: List[str] = None,
|
||||
type: str = "file",
|
||||
label: Optional[str] = None,
|
||||
show_label: bool = True,
|
||||
@ -2098,6 +2099,7 @@ class File(Changeable, Clearable, Uploadable, IOComponent, FileSerializable):
|
||||
Parameters:
|
||||
value: Default file to display, given as str file path. If callable, the function will be called whenever the app loads to set the initial value of the component.
|
||||
file_count: if single, allows user to upload one file. If "multiple", user uploads multiple files. If "directory", user uploads all files in selected directory. Return type will be list for each file in case of "multiple" or "directory".
|
||||
file_types: List of type of files to be uploaded. "file" allows any file to be uploaded, "image" allows only image files to be uploaded, "audio" allows only audio files to be uploaded, "video" allows only video files to be uploaded, "text" allows only text files to be uploaded.
|
||||
type: Type of value to be returned by component. "file" returns a temporary file object whose path can be retrieved by file_obj.name and original filename can be retrieved with file_obj.orig_name, "binary" returns an bytes object.
|
||||
label: component name in interface.
|
||||
show_label: if True, will display label.
|
||||
@ -2107,6 +2109,7 @@ class File(Changeable, Clearable, Uploadable, IOComponent, FileSerializable):
|
||||
"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.file_count = file_count
|
||||
self.file_types = file_types
|
||||
valid_types = ["file", "binary"]
|
||||
if type not in valid_types:
|
||||
raise ValueError(
|
||||
@ -2128,6 +2131,7 @@ class File(Changeable, Clearable, Uploadable, IOComponent, FileSerializable):
|
||||
def get_config(self):
|
||||
return {
|
||||
"file_count": self.file_count,
|
||||
"file_types": self.file_types,
|
||||
"value": self.value,
|
||||
**IOComponent.get_config(self),
|
||||
}
|
||||
@ -2761,6 +2765,139 @@ class Button(Clickable, IOComponent, SimpleSerializable):
|
||||
return IOComponent.style(self, **kwargs)
|
||||
|
||||
|
||||
@document("click", "upload", "style")
|
||||
class UploadButton(Clickable, Uploadable, IOComponent, SimpleSerializable):
|
||||
"""
|
||||
Used to create an upload button, when cicked allows a user to upload files that satisfy the specified file type or generic files (if file_type not set).
|
||||
Preprocessing: passes the uploaded file as a {file-object} or {List[file-object]} depending on `file_count` (or a {bytes}/{List{bytes}} depending on `type`)
|
||||
Postprocessing: expects function to return a {str} path to a file, or {List[str]} consisting of paths to files.
|
||||
Examples-format: a {str} path to a local file that populates the component.
|
||||
Demos: upload_button
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: str = "Upload a File",
|
||||
value: Optional[str | List[str] | Callable] = None,
|
||||
*,
|
||||
visible: bool = True,
|
||||
elem_id: Optional[str] = None,
|
||||
type: str = "file",
|
||||
file_count: str = "single",
|
||||
file_types: List[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
value: Default text for the button to display.
|
||||
type: Type of value to be returned by component. "file" returns a temporary file object whose path can be retrieved by file_obj.name and original filename can be retrieved with file_obj.orig_name, "binary" returns an bytes object.
|
||||
file_count: if single, allows user to upload one file. If "multiple", user uploads multiple files. If "directory", user uploads all files in selected directory. Return type will be list for each file in case of "multiple" or "directory".
|
||||
file_types: List of type of files to be uploaded. "file" allows any file to be uploaded, "image" allows only image files to be uploaded, "audio" allows only audio files to be uploaded, "video" allows only video files to be uploaded, "text" allows only text files to be uploaded.
|
||||
label: Text to display on the button. Defaults to "Upload a File".
|
||||
visible: If False, component will be hidden.
|
||||
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
|
||||
"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.type = type
|
||||
self.file_count = file_count
|
||||
self.file_types = file_types
|
||||
self.label = label
|
||||
IOComponent.__init__(
|
||||
self, label=label, visible=visible, elem_id=elem_id, value=value, **kwargs
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
return {
|
||||
"label": self.label,
|
||||
"value": self.value,
|
||||
"file_count": self.file_count,
|
||||
"file_types": self.file_types,
|
||||
**Component.get_config(self),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
value: Optional[str] = _Keywords.NO_VALUE,
|
||||
interactive: Optional[bool] = None,
|
||||
visible: Optional[bool] = None,
|
||||
):
|
||||
updated_config = {
|
||||
"interactive": interactive,
|
||||
"visible": visible,
|
||||
"value": value,
|
||||
"__type__": "update",
|
||||
}
|
||||
return IOComponent.add_interactive_to_config(updated_config, interactive)
|
||||
|
||||
def preprocess(self, x: List[Dict[str, str]] | None) -> str | List[str]:
|
||||
"""
|
||||
Parameters:
|
||||
x: List of JSON objects with filename as 'name' property and base64 data as 'data' property
|
||||
Returns:
|
||||
File objects in requested format
|
||||
"""
|
||||
if x is None:
|
||||
return None
|
||||
|
||||
def process_single_file(f):
|
||||
file_name, data, is_file = (
|
||||
f["name"],
|
||||
f["data"],
|
||||
f.get("is_file", False),
|
||||
)
|
||||
if self.type == "file":
|
||||
if is_file:
|
||||
file = processing_utils.create_tmp_copy_of_file(file_name)
|
||||
file.orig_name = file_name
|
||||
else:
|
||||
file = processing_utils.decode_base64_to_file(
|
||||
data, file_path=file_name
|
||||
)
|
||||
file.orig_name = file_name
|
||||
return file
|
||||
elif self.type == "bytes":
|
||||
if is_file:
|
||||
with open(file_name, "rb") as file_data:
|
||||
return file_data.read()
|
||||
return processing_utils.decode_base64_to_binary(data)[0]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unknown type: "
|
||||
+ str(self.type)
|
||||
+ ". Please choose from: 'file', 'bytes'."
|
||||
)
|
||||
|
||||
if self.file_count == "single":
|
||||
if isinstance(x, list):
|
||||
return process_single_file(x[0])
|
||||
else:
|
||||
return process_single_file(x)
|
||||
else:
|
||||
if isinstance(x, list):
|
||||
return [process_single_file(f) for f in x]
|
||||
else:
|
||||
return process_single_file(x)
|
||||
|
||||
def generate_sample(self):
|
||||
return deepcopy(media_data.BASE64_FILE)
|
||||
|
||||
def serialize(self, x: str, load_dir: str = "", called_directly: bool = False):
|
||||
serialized = FileSerializable.serialize(self, x, load_dir, called_directly)
|
||||
serialized["size"] = os.path.getsize(serialized["name"])
|
||||
return serialized
|
||||
|
||||
def style(self, *, full_width: Optional[bool] = None, **kwargs):
|
||||
"""
|
||||
This method can be used to change the appearance of the button component.
|
||||
Parameters:
|
||||
full_width: If True, will expand to fill parent container.
|
||||
"""
|
||||
if full_width is not None:
|
||||
self._style["full_width"] = full_width
|
||||
|
||||
return IOComponent.style(self, **kwargs)
|
||||
|
||||
|
||||
@document("change", "submit", "style")
|
||||
class ColorPicker(Changeable, Submittable, IOComponent, SimpleSerializable):
|
||||
"""
|
||||
|
@ -827,6 +827,7 @@ class TestFile:
|
||||
file_input = gr.File(label="Upload Your File")
|
||||
assert file_input.get_config() == {
|
||||
"file_count": "single",
|
||||
"file_types": None,
|
||||
"name": "file",
|
||||
"show_label": True,
|
||||
"label": "Upload Your File",
|
||||
|
@ -35,6 +35,7 @@
|
||||
"@gradio/tabs": "workspace:^0.0.1",
|
||||
"@gradio/theme": "workspace:^0.0.1",
|
||||
"@gradio/upload": "workspace:^0.0.1",
|
||||
"@gradio/uploadbutton": "workspace:^0.0.1",
|
||||
"@gradio/utils": "workspace:^0.0.1",
|
||||
"@gradio/video": "workspace:^0.0.1",
|
||||
"d3-dsv": "^3.0.1",
|
||||
|
@ -17,6 +17,7 @@
|
||||
export let label: string;
|
||||
export let show_label: boolean;
|
||||
export let file_count: string;
|
||||
export let file_types: Array<string> = ["file"];
|
||||
export let root_url: null | string;
|
||||
|
||||
export let loading_status: LoadingStatus;
|
||||
@ -42,6 +43,7 @@
|
||||
{show_label}
|
||||
value={_value}
|
||||
{file_count}
|
||||
{file_types}
|
||||
on:change={({ detail }) => (value = detail)}
|
||||
on:drag={({ detail }) => (dragging = detail)}
|
||||
on:change
|
||||
@ -52,6 +54,6 @@
|
||||
upload_text={$_("interface.click_to_upload")}
|
||||
/>
|
||||
{:else}
|
||||
<File value={_value} {label} {show_label} {file_count} />
|
||||
<File value={_value} {label} {show_label} />
|
||||
{/if}
|
||||
</Block>
|
||||
|
@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import type { FileData } from "@gradio/upload";
|
||||
import { UploadButton } from "@gradio/uploadbutton";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let style: Styles = {};
|
||||
export let elem_id: string = "";
|
||||
export let visible: boolean = true;
|
||||
export let label: string;
|
||||
export let value: null | FileData | Array<FileData>;
|
||||
export let file_count: string;
|
||||
export let file_types: Array<string> = ["file"];
|
||||
|
||||
async function handle_upload({ detail }: CustomEvent<FileData>) {
|
||||
value = detail;
|
||||
await tick();
|
||||
dispatch("change", value);
|
||||
dispatch("upload", detail);
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: FileData | null;
|
||||
upload: FileData;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<UploadButton
|
||||
{elem_id}
|
||||
{style}
|
||||
{visible}
|
||||
{file_count}
|
||||
{file_types}
|
||||
on:click
|
||||
on:load={handle_upload}
|
||||
>
|
||||
{$_(label)}
|
||||
</UploadButton>
|
2
ui/packages/app/src/components/UploadButton/index.ts
Normal file
2
ui/packages/app/src/components/UploadButton/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Component } from "./UploadButton.svelte";
|
||||
export const modes = ["static"];
|
@ -36,5 +36,6 @@ export const component_map = {
|
||||
tabitem: () => import("./TabItem"),
|
||||
textbox: () => import("./Textbox"),
|
||||
timeseries: () => import("./TimeSeries"),
|
||||
uploadbutton: () => import("./UploadButton"),
|
||||
video: () => import("./Video")
|
||||
};
|
||||
|
@ -11,7 +11,6 @@
|
||||
export let value: FileData | null;
|
||||
export let label: string;
|
||||
export let show_label: boolean;
|
||||
export let file_count: string;
|
||||
</script>
|
||||
|
||||
<BlockLabel {show_label} Icon={File} label={label || "File"} />
|
||||
|
@ -19,6 +19,7 @@
|
||||
export let label: string = "";
|
||||
export let show_label: boolean;
|
||||
export let file_count: string;
|
||||
export let file_types: Array<string> = ["file"];
|
||||
|
||||
async function handle_upload({ detail }: CustomEvent<FileData>) {
|
||||
value = detail;
|
||||
@ -38,8 +39,20 @@
|
||||
clear: undefined;
|
||||
drag: boolean;
|
||||
upload: FileData;
|
||||
error: string;
|
||||
}>();
|
||||
|
||||
let accept_file_types = "";
|
||||
try {
|
||||
file_types.forEach((type) => (accept_file_types += type + "/*, "));
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
dispatch("error", "Please set file_types to a list.");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let dragging = false;
|
||||
$: dispatch("drag", dragging);
|
||||
</script>
|
||||
@ -47,13 +60,18 @@
|
||||
<BlockLabel {show_label} Icon={File} label={label || "File"} />
|
||||
|
||||
{#if value === null && file_count === "single"}
|
||||
<Upload on:load={handle_upload} filetype="file" bind:dragging>
|
||||
<Upload on:load={handle_upload} filetype={accept_file_types} bind:dragging>
|
||||
{drop_text}
|
||||
<br />- {or_text} -<br />
|
||||
{upload_text}
|
||||
</Upload>
|
||||
{:else if value === null}
|
||||
<Upload on:load={handle_upload} filetype="file" {file_count} bind:dragging>
|
||||
<Upload
|
||||
on:load={handle_upload}
|
||||
filetype={accept_file_types}
|
||||
{file_count}
|
||||
bind:dragging
|
||||
>
|
||||
{drop_text}
|
||||
<br />- {or_text} -<br />
|
||||
{upload_text}
|
||||
|
11
ui/packages/upload-button/README.md
Normal file
11
ui/packages/upload-button/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# `@gradio/uploadbutton`
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { UploadButton } from "@gradio/uploadbutton";
|
||||
</script>
|
||||
|
||||
<button type="primary|secondary" href="string" on:click="{e.detail === href}">
|
||||
content
|
||||
</button>
|
||||
```
|
13
ui/packages/upload-button/package.json
Normal file
13
ui/packages/upload-button/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@gradio/uploadbutton",
|
||||
"version": "0.0.1",
|
||||
"description": "Gradio UI packages",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gradio/utils": "workspace:^0.0.1"
|
||||
}
|
||||
}
|
93
ui/packages/upload-button/src/UploadButton.svelte
Normal file
93
ui/packages/upload-button/src/UploadButton.svelte
Normal file
@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { get_styles } from "@gradio/utils";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { FileData } from "./types";
|
||||
|
||||
export let style: Styles = {};
|
||||
export let elem_id: string = "";
|
||||
export let visible: boolean = true;
|
||||
export let size: "sm" | "lg" = "lg";
|
||||
export let file_count: string;
|
||||
export let file_types: Array<string> = ["file"];
|
||||
export let include_file_metadata = true;
|
||||
|
||||
$: ({ classes } = get_styles(style, ["full_width"]));
|
||||
|
||||
let hidden_upload: HTMLInputElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
let accept_file_types = "";
|
||||
try {
|
||||
file_types.forEach((type) => (accept_file_types += type + "/*, "));
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
dispatch("error", "Please set file_types to a list.");
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const openFileUpload = () => {
|
||||
hidden_upload.click();
|
||||
};
|
||||
|
||||
const loadFiles = (files: FileList) => {
|
||||
let _files: Array<File> = Array.from(files);
|
||||
if (!files.length || !window.FileReader) {
|
||||
return;
|
||||
}
|
||||
if (file_count === "single") {
|
||||
_files = [files[0]];
|
||||
}
|
||||
var all_file_data: Array<FileData | string> = [];
|
||||
_files.forEach((f, i) => {
|
||||
let ReaderObj = new FileReader();
|
||||
ReaderObj.readAsDataURL(f);
|
||||
ReaderObj.onloadend = function () {
|
||||
all_file_data[i] = include_file_metadata
|
||||
? {
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
data: this.result as string
|
||||
}
|
||||
: (this.result as string);
|
||||
if (
|
||||
all_file_data.filter((x) => x !== undefined).length === files.length
|
||||
) {
|
||||
dispatch(
|
||||
"load",
|
||||
file_count == "single" ? all_file_data[0] : all_file_data
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const loadFilesFromUpload = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (!target.files) return;
|
||||
loadFiles(target.files);
|
||||
};
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="hidden-upload hidden"
|
||||
accept={accept_file_types}
|
||||
type="file"
|
||||
bind:this={hidden_upload}
|
||||
on:change={loadFilesFromUpload}
|
||||
multiple={file_count === "multiple" || undefined}
|
||||
webkitdirectory={file_count === "directory" || undefined}
|
||||
mozdirectory={file_count === "directory" || undefined}
|
||||
/>
|
||||
|
||||
<button
|
||||
on:click={openFileUpload}
|
||||
class:!hidden={!visible}
|
||||
class="gr-button gr-button-{size}
|
||||
{classes}"
|
||||
id={elem_id}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
1
ui/packages/upload-button/src/index.ts
Normal file
1
ui/packages/upload-button/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as UploadButton } from "./UploadButton.svelte";
|
7
ui/packages/upload-button/src/types.ts
Normal file
7
ui/packages/upload-button/src/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface FileData {
|
||||
name: string;
|
||||
orig_name?: string;
|
||||
size?: number;
|
||||
data: string;
|
||||
is_file?: boolean;
|
||||
}
|
@ -42,6 +42,7 @@
|
||||
"@gradio/tabs": "workspace:^0.0.1",
|
||||
"@gradio/theme": "workspace:^0.0.1",
|
||||
"@gradio/upload": "workspace:^0.0.1",
|
||||
"@gradio/video": "workspace:^0.0.1"
|
||||
"@gradio/video": "workspace:^0.0.1",
|
||||
"@gradio/uploadbutton": "workspace:^0.0.1"
|
||||
}
|
||||
}
|
||||
|
617
ui/pnpm-lock.yaml
generated
617
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user