Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .gitattributes +8 -0
- .gitignore +13 -0
- README.md +567 -12
- __init__.py +0 -0
- app.py +108 -0
- css.css +157 -0
- requirements.txt +2 -0
- space.py +229 -0
- src/.gitignore +13 -0
- src/.vscode/launch.json +29 -0
- src/README.md +567 -0
- src/README_TEMPLATE.md +565 -0
- src/backend/gradio_mediagallery/__init__.py +4 -0
- src/backend/gradio_mediagallery/helpers.py +315 -0
- src/backend/gradio_mediagallery/mediagallery.py +281 -0
- src/backend/gradio_mediagallery/templates/component/assets/worker-BAOIWoxA.js +1 -0
- src/backend/gradio_mediagallery/templates/component/index.js +0 -0
- src/backend/gradio_mediagallery/templates/component/style.css +1 -0
- src/backend/gradio_mediagallery/templates/example/index.js +342 -0
- src/backend/gradio_mediagallery/templates/example/style.css +1 -0
- src/demo/__init__.py +0 -0
- src/demo/app.py +108 -0
- src/demo/css.css +157 -0
- src/demo/requirements.txt +2 -0
- src/demo/space.py +229 -0
- src/examples/folder1/Jensen.jpeg +3 -0
- src/examples/folder1/newton_0.jpg +3 -0
- src/examples/folder1/newton_2.png +3 -0
- src/examples/folder1/newton_3.jpg +0 -0
- src/examples/folder1/sam1.jpg +3 -0
- src/examples/folder2/SampleVideo 720x480.mp4 +3 -0
- src/examples/folder2/butterfly_input.jpg +3 -0
- src/examples/folder2/lemons_input.jpg +3 -0
- src/examples/folder2/vermeer.jpg +0 -0
- src/examples/image_with_meta.png +3 -0
- src/frontend/Example.svelte +143 -0
- src/frontend/Gallery.css +471 -0
- src/frontend/Index.svelte +105 -0
- src/frontend/gradio.config.js +9 -0
- src/frontend/package-lock.json +0 -0
- src/frontend/package.json +54 -0
- src/frontend/shared/Gallery.svelte +1099 -0
- src/frontend/shared/utils.ts +18 -0
- src/frontend/tsconfig.json +14 -0
- src/frontend/types.ts +11 -0
- src/log.txt +0 -0
- src/pyproject.toml +54 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
src/examples/folder1/Jensen.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
src/examples/folder1/newton_0.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
src/examples/folder1/newton_2.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
src/examples/folder1/sam1.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
src/examples/folder2/butterfly_input.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
src/examples/folder2/lemons_input.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
src/examples/folder2/SampleVideo[[:space:]]720x480.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
src/examples/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.eggs/
|
| 2 |
+
dist/
|
| 3 |
+
.vscode/
|
| 4 |
+
*.pyc
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
__tmp/*
|
| 9 |
+
*.pyi
|
| 10 |
+
.mypycache
|
| 11 |
+
.ruff_cache
|
| 12 |
+
node_modules
|
| 13 |
+
backend/**/templates/
|
README.md
CHANGED
|
@@ -1,12 +1,567 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags: [gradio-custom-component, gallery]
|
| 3 |
+
title: gradio_mediagallery
|
| 4 |
+
short_description: A gradio custom component
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: gradio
|
| 8 |
+
pinned: false
|
| 9 |
+
app_file: space.py
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# `gradio_mediagallery`
|
| 13 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
|
| 14 |
+
|
| 15 |
+
Python library for easily interacting with trained machine learning models
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install gradio_mediagallery gradio_folderexplorer
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from typing import Any, List
|
| 27 |
+
import gradio as gr
|
| 28 |
+
from gradio_folderexplorer import FolderExplorer
|
| 29 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 30 |
+
from gradio_mediagallery import MediaGallery
|
| 31 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 32 |
+
import os
|
| 33 |
+
|
| 34 |
+
# Configuration constant for the root directory containing media files
|
| 35 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 36 |
+
|
| 37 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 38 |
+
"""
|
| 39 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 46 |
+
"""
|
| 47 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 48 |
+
return [gr.skip()] * len(output_fields)
|
| 49 |
+
|
| 50 |
+
return transfer_metadata(
|
| 51 |
+
output_fields=output_fields,
|
| 52 |
+
metadata=image_data._data,
|
| 53 |
+
remove_prefix_from_keys=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# UI layout and logic
|
| 57 |
+
with gr.Blocks() as demo:
|
| 58 |
+
"""
|
| 59 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 60 |
+
"""
|
| 61 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 62 |
+
gr.Markdown(
|
| 63 |
+
"""
|
| 64 |
+
**To Test:**
|
| 65 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 66 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 67 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 68 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 69 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
with gr.Row(equal_height=True):
|
| 73 |
+
with gr.Column(scale=1, min_width=300):
|
| 74 |
+
folder_explorer = FolderExplorer(
|
| 75 |
+
label="Select a Folder",
|
| 76 |
+
root_dir=ROOT_DIR_PATH,
|
| 77 |
+
value=ROOT_DIR_PATH
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
with gr.Column(scale=3):
|
| 81 |
+
gallery = MediaGallery(
|
| 82 |
+
label="Media in Folder",
|
| 83 |
+
columns=6,
|
| 84 |
+
height="auto",
|
| 85 |
+
preview=False,
|
| 86 |
+
show_download_button=False,
|
| 87 |
+
only_custom_metadata=False,
|
| 88 |
+
popup_metadata_width="40%",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
gr.Markdown("## Metadata Viewer")
|
| 92 |
+
with gr.Row():
|
| 93 |
+
model_box = gr.Textbox(label="Model")
|
| 94 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 95 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 96 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 97 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 98 |
+
|
| 99 |
+
# Event handling
|
| 100 |
+
output_fields = [
|
| 101 |
+
model_box,
|
| 102 |
+
fnumber_box,
|
| 103 |
+
iso_box,
|
| 104 |
+
s_churn,
|
| 105 |
+
description_box
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
# Populate the gallery when the folder changes
|
| 109 |
+
folder_explorer.change(
|
| 110 |
+
fn=load_media_from_folder,
|
| 111 |
+
inputs=folder_explorer,
|
| 112 |
+
outputs=gallery
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Populate the gallery on initial load
|
| 116 |
+
demo.load(
|
| 117 |
+
fn=load_media_from_folder,
|
| 118 |
+
inputs=folder_explorer,
|
| 119 |
+
outputs=gallery
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Handle the load_metadata event from MediaGallery
|
| 123 |
+
gallery.load_metadata(
|
| 124 |
+
fn=handle_load_metadata,
|
| 125 |
+
inputs=None,
|
| 126 |
+
outputs=output_fields
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
"""
|
| 131 |
+
Launches the Gradio interface in debug mode.
|
| 132 |
+
"""
|
| 133 |
+
demo.launch(debug=True)
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## `MediaGallery`
|
| 137 |
+
|
| 138 |
+
### Initialization
|
| 139 |
+
|
| 140 |
+
<table>
|
| 141 |
+
<thead>
|
| 142 |
+
<tr>
|
| 143 |
+
<th align="left">name</th>
|
| 144 |
+
<th align="left" style="width: 25%;">type</th>
|
| 145 |
+
<th align="left">default</th>
|
| 146 |
+
<th align="left">description</th>
|
| 147 |
+
</tr>
|
| 148 |
+
</thead>
|
| 149 |
+
<tbody>
|
| 150 |
+
<tr>
|
| 151 |
+
<td align="left"><code>value</code></td>
|
| 152 |
+
<td align="left" style="width: 25%;">
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
Sequence[
|
| 156 |
+
np.ndarray | PIL.Image.Image | str | Path | tuple
|
| 157 |
+
]
|
| 158 |
+
| Callable
|
| 159 |
+
| None
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
</td>
|
| 163 |
+
<td align="left"><code>None</code></td>
|
| 164 |
+
<td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
|
| 165 |
+
</tr>
|
| 166 |
+
|
| 167 |
+
<tr>
|
| 168 |
+
<td align="left"><code>file_types</code></td>
|
| 169 |
+
<td align="left" style="width: 25%;">
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
list[str] | None
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
</td>
|
| 176 |
+
<td align="left"><code>None</code></td>
|
| 177 |
+
<td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
|
| 178 |
+
</tr>
|
| 179 |
+
|
| 180 |
+
<tr>
|
| 181 |
+
<td align="left"><code>label</code></td>
|
| 182 |
+
<td align="left" style="width: 25%;">
|
| 183 |
+
|
| 184 |
+
```python
|
| 185 |
+
str | I18nData | None
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
</td>
|
| 189 |
+
<td align="left"><code>None</code></td>
|
| 190 |
+
<td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
|
| 191 |
+
</tr>
|
| 192 |
+
|
| 193 |
+
<tr>
|
| 194 |
+
<td align="left"><code>every</code></td>
|
| 195 |
+
<td align="left" style="width: 25%;">
|
| 196 |
+
|
| 197 |
+
```python
|
| 198 |
+
Timer | float | None
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
</td>
|
| 202 |
+
<td align="left"><code>None</code></td>
|
| 203 |
+
<td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
|
| 204 |
+
</tr>
|
| 205 |
+
|
| 206 |
+
<tr>
|
| 207 |
+
<td align="left"><code>inputs</code></td>
|
| 208 |
+
<td align="left" style="width: 25%;">
|
| 209 |
+
|
| 210 |
+
```python
|
| 211 |
+
Component | Sequence[Component] | set[Component] | None
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
</td>
|
| 215 |
+
<td align="left"><code>None</code></td>
|
| 216 |
+
<td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
|
| 217 |
+
</tr>
|
| 218 |
+
|
| 219 |
+
<tr>
|
| 220 |
+
<td align="left"><code>show_label</code></td>
|
| 221 |
+
<td align="left" style="width: 25%;">
|
| 222 |
+
|
| 223 |
+
```python
|
| 224 |
+
bool | None
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
</td>
|
| 228 |
+
<td align="left"><code>None</code></td>
|
| 229 |
+
<td align="left">if True, will display label.</td>
|
| 230 |
+
</tr>
|
| 231 |
+
|
| 232 |
+
<tr>
|
| 233 |
+
<td align="left"><code>container</code></td>
|
| 234 |
+
<td align="left" style="width: 25%;">
|
| 235 |
+
|
| 236 |
+
```python
|
| 237 |
+
bool
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
</td>
|
| 241 |
+
<td align="left"><code>True</code></td>
|
| 242 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 243 |
+
</tr>
|
| 244 |
+
|
| 245 |
+
<tr>
|
| 246 |
+
<td align="left"><code>scale</code></td>
|
| 247 |
+
<td align="left" style="width: 25%;">
|
| 248 |
+
|
| 249 |
+
```python
|
| 250 |
+
int | None
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
</td>
|
| 254 |
+
<td align="left"><code>None</code></td>
|
| 255 |
+
<td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
|
| 256 |
+
</tr>
|
| 257 |
+
|
| 258 |
+
<tr>
|
| 259 |
+
<td align="left"><code>min_width</code></td>
|
| 260 |
+
<td align="left" style="width: 25%;">
|
| 261 |
+
|
| 262 |
+
```python
|
| 263 |
+
int
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
</td>
|
| 267 |
+
<td align="left"><code>160</code></td>
|
| 268 |
+
<td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
|
| 269 |
+
</tr>
|
| 270 |
+
|
| 271 |
+
<tr>
|
| 272 |
+
<td align="left"><code>visible</code></td>
|
| 273 |
+
<td align="left" style="width: 25%;">
|
| 274 |
+
|
| 275 |
+
```python
|
| 276 |
+
bool | Literal["hidden"]
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
</td>
|
| 280 |
+
<td align="left"><code>True</code></td>
|
| 281 |
+
<td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
|
| 282 |
+
</tr>
|
| 283 |
+
|
| 284 |
+
<tr>
|
| 285 |
+
<td align="left"><code>elem_id</code></td>
|
| 286 |
+
<td align="left" style="width: 25%;">
|
| 287 |
+
|
| 288 |
+
```python
|
| 289 |
+
str | None
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
</td>
|
| 293 |
+
<td align="left"><code>None</code></td>
|
| 294 |
+
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 295 |
+
</tr>
|
| 296 |
+
|
| 297 |
+
<tr>
|
| 298 |
+
<td align="left"><code>elem_classes</code></td>
|
| 299 |
+
<td align="left" style="width: 25%;">
|
| 300 |
+
|
| 301 |
+
```python
|
| 302 |
+
list[str] | str | None
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
</td>
|
| 306 |
+
<td align="left"><code>None</code></td>
|
| 307 |
+
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 308 |
+
</tr>
|
| 309 |
+
|
| 310 |
+
<tr>
|
| 311 |
+
<td align="left"><code>render</code></td>
|
| 312 |
+
<td align="left" style="width: 25%;">
|
| 313 |
+
|
| 314 |
+
```python
|
| 315 |
+
bool
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
</td>
|
| 319 |
+
<td align="left"><code>True</code></td>
|
| 320 |
+
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
|
| 321 |
+
</tr>
|
| 322 |
+
|
| 323 |
+
<tr>
|
| 324 |
+
<td align="left"><code>key</code></td>
|
| 325 |
+
<td align="left" style="width: 25%;">
|
| 326 |
+
|
| 327 |
+
```python
|
| 328 |
+
int | str | tuple[int | str, ...] | None
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
</td>
|
| 332 |
+
<td align="left"><code>None</code></td>
|
| 333 |
+
<td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
|
| 334 |
+
</tr>
|
| 335 |
+
|
| 336 |
+
<tr>
|
| 337 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 338 |
+
<td align="left" style="width: 25%;">
|
| 339 |
+
|
| 340 |
+
```python
|
| 341 |
+
list[str] | str | None
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
</td>
|
| 345 |
+
<td align="left"><code>"value"</code></td>
|
| 346 |
+
<td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
|
| 347 |
+
</tr>
|
| 348 |
+
|
| 349 |
+
<tr>
|
| 350 |
+
<td align="left"><code>columns</code></td>
|
| 351 |
+
<td align="left" style="width: 25%;">
|
| 352 |
+
|
| 353 |
+
```python
|
| 354 |
+
int | None
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
</td>
|
| 358 |
+
<td align="left"><code>2</code></td>
|
| 359 |
+
<td align="left">Represents the number of images that should be shown in one row.</td>
|
| 360 |
+
</tr>
|
| 361 |
+
|
| 362 |
+
<tr>
|
| 363 |
+
<td align="left"><code>rows</code></td>
|
| 364 |
+
<td align="left" style="width: 25%;">
|
| 365 |
+
|
| 366 |
+
```python
|
| 367 |
+
int | None
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
</td>
|
| 371 |
+
<td align="left"><code>None</code></td>
|
| 372 |
+
<td align="left">Represents the number of rows in the image grid.</td>
|
| 373 |
+
</tr>
|
| 374 |
+
|
| 375 |
+
<tr>
|
| 376 |
+
<td align="left"><code>height</code></td>
|
| 377 |
+
<td align="left" style="width: 25%;">
|
| 378 |
+
|
| 379 |
+
```python
|
| 380 |
+
int | float | str | None
|
| 381 |
+
```
|
| 382 |
+
|
| 383 |
+
</td>
|
| 384 |
+
<td align="left"><code>None</code></td>
|
| 385 |
+
<td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
|
| 386 |
+
</tr>
|
| 387 |
+
|
| 388 |
+
<tr>
|
| 389 |
+
<td align="left"><code>allow_preview</code></td>
|
| 390 |
+
<td align="left" style="width: 25%;">
|
| 391 |
+
|
| 392 |
+
```python
|
| 393 |
+
bool
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
</td>
|
| 397 |
+
<td align="left"><code>True</code></td>
|
| 398 |
+
<td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
|
| 399 |
+
</tr>
|
| 400 |
+
|
| 401 |
+
<tr>
|
| 402 |
+
<td align="left"><code>preview</code></td>
|
| 403 |
+
<td align="left" style="width: 25%;">
|
| 404 |
+
|
| 405 |
+
```python
|
| 406 |
+
bool | None
|
| 407 |
+
```
|
| 408 |
+
|
| 409 |
+
</td>
|
| 410 |
+
<td align="left"><code>None</code></td>
|
| 411 |
+
<td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
|
| 412 |
+
</tr>
|
| 413 |
+
|
| 414 |
+
<tr>
|
| 415 |
+
<td align="left"><code>selected_index</code></td>
|
| 416 |
+
<td align="left" style="width: 25%;">
|
| 417 |
+
|
| 418 |
+
```python
|
| 419 |
+
int | None
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
</td>
|
| 423 |
+
<td align="left"><code>None</code></td>
|
| 424 |
+
<td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
|
| 425 |
+
</tr>
|
| 426 |
+
|
| 427 |
+
<tr>
|
| 428 |
+
<td align="left"><code>object_fit</code></td>
|
| 429 |
+
<td align="left" style="width: 25%;">
|
| 430 |
+
|
| 431 |
+
```python
|
| 432 |
+
Literal[
|
| 433 |
+
"contain", "cover", "fill", "none", "scale-down"
|
| 434 |
+
]
|
| 435 |
+
| None
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
</td>
|
| 439 |
+
<td align="left"><code>None</code></td>
|
| 440 |
+
<td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
|
| 441 |
+
</tr>
|
| 442 |
+
|
| 443 |
+
<tr>
|
| 444 |
+
<td align="left"><code>show_share_button</code></td>
|
| 445 |
+
<td align="left" style="width: 25%;">
|
| 446 |
+
|
| 447 |
+
```python
|
| 448 |
+
bool | None
|
| 449 |
+
```
|
| 450 |
+
|
| 451 |
+
</td>
|
| 452 |
+
<td align="left"><code>None</code></td>
|
| 453 |
+
<td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 454 |
+
</tr>
|
| 455 |
+
|
| 456 |
+
<tr>
|
| 457 |
+
<td align="left"><code>show_download_button</code></td>
|
| 458 |
+
<td align="left" style="width: 25%;">
|
| 459 |
+
|
| 460 |
+
```python
|
| 461 |
+
bool | None
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
</td>
|
| 465 |
+
<td align="left"><code>True</code></td>
|
| 466 |
+
<td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
|
| 467 |
+
</tr>
|
| 468 |
+
|
| 469 |
+
<tr>
|
| 470 |
+
<td align="left"><code>interactive</code></td>
|
| 471 |
+
<td align="left" style="width: 25%;">
|
| 472 |
+
|
| 473 |
+
```python
|
| 474 |
+
bool | None
|
| 475 |
+
```
|
| 476 |
+
|
| 477 |
+
</td>
|
| 478 |
+
<td align="left"><code>None</code></td>
|
| 479 |
+
<td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
|
| 480 |
+
</tr>
|
| 481 |
+
|
| 482 |
+
<tr>
|
| 483 |
+
<td align="left"><code>type</code></td>
|
| 484 |
+
<td align="left" style="width: 25%;">
|
| 485 |
+
|
| 486 |
+
```python
|
| 487 |
+
Literal["numpy", "pil", "filepath"]
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
</td>
|
| 491 |
+
<td align="left"><code>"filepath"</code></td>
|
| 492 |
+
<td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
|
| 493 |
+
</tr>
|
| 494 |
+
|
| 495 |
+
<tr>
|
| 496 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 497 |
+
<td align="left" style="width: 25%;">
|
| 498 |
+
|
| 499 |
+
```python
|
| 500 |
+
bool
|
| 501 |
+
```
|
| 502 |
+
|
| 503 |
+
</td>
|
| 504 |
+
<td align="left"><code>True</code></td>
|
| 505 |
+
<td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 506 |
+
</tr>
|
| 507 |
+
|
| 508 |
+
<tr>
|
| 509 |
+
<td align="left"><code>only_custom_metadata</code></td>
|
| 510 |
+
<td align="left" style="width: 25%;">
|
| 511 |
+
|
| 512 |
+
```python
|
| 513 |
+
bool
|
| 514 |
+
```
|
| 515 |
+
|
| 516 |
+
</td>
|
| 517 |
+
<td align="left"><code>True</code></td>
|
| 518 |
+
<td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
|
| 519 |
+
</tr>
|
| 520 |
+
|
| 521 |
+
<tr>
|
| 522 |
+
<td align="left"><code>popup_metadata_width</code></td>
|
| 523 |
+
<td align="left" style="width: 25%;">
|
| 524 |
+
|
| 525 |
+
```python
|
| 526 |
+
int | str
|
| 527 |
+
```
|
| 528 |
+
|
| 529 |
+
</td>
|
| 530 |
+
<td align="left"><code>500</code></td>
|
| 531 |
+
<td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
|
| 532 |
+
</tr>
|
| 533 |
+
</tbody></table>
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
### Events
|
| 537 |
+
|
| 538 |
+
| name | description |
|
| 539 |
+
|:-----|:------------|
|
| 540 |
+
| `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
|
| 541 |
+
| `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
|
| 542 |
+
| `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
|
| 543 |
+
| `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
|
| 544 |
+
| `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
|
| 545 |
+
| `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
### User function
|
| 550 |
+
|
| 551 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 552 |
+
|
| 553 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 554 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 555 |
+
|
| 556 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 557 |
+
|
| 558 |
+
- **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
|
| 559 |
+
- **As input:** Should return, the output data received by the component from the user's function in the backend.
|
| 560 |
+
|
| 561 |
+
```python
|
| 562 |
+
def predict(
|
| 563 |
+
value: Any
|
| 564 |
+
) -> list | None:
|
| 565 |
+
return value
|
| 566 |
+
```
|
| 567 |
+
|
__init__.py
ADDED
|
File without changes
|
app.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, List
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from gradio_folderexplorer import FolderExplorer
|
| 4 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 5 |
+
from gradio_mediagallery import MediaGallery
|
| 6 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Configuration constant for the root directory containing media files
|
| 10 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 11 |
+
|
| 12 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 13 |
+
"""
|
| 14 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 21 |
+
"""
|
| 22 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 23 |
+
return [gr.skip()] * len(output_fields)
|
| 24 |
+
|
| 25 |
+
return transfer_metadata(
|
| 26 |
+
output_fields=output_fields,
|
| 27 |
+
metadata=image_data._data,
|
| 28 |
+
remove_prefix_from_keys=True
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# UI layout and logic
|
| 32 |
+
with gr.Blocks() as demo:
|
| 33 |
+
"""
|
| 34 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 35 |
+
"""
|
| 36 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 37 |
+
gr.Markdown(
|
| 38 |
+
"""
|
| 39 |
+
**To Test:**
|
| 40 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 41 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 42 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 43 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 44 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
with gr.Row(equal_height=True):
|
| 48 |
+
with gr.Column(scale=1, min_width=300):
|
| 49 |
+
folder_explorer = FolderExplorer(
|
| 50 |
+
label="Select a Folder",
|
| 51 |
+
root_dir=ROOT_DIR_PATH,
|
| 52 |
+
value=ROOT_DIR_PATH
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
with gr.Column(scale=3):
|
| 56 |
+
gallery = MediaGallery(
|
| 57 |
+
label="Media in Folder",
|
| 58 |
+
columns=6,
|
| 59 |
+
height="auto",
|
| 60 |
+
preview=False,
|
| 61 |
+
show_download_button=False,
|
| 62 |
+
only_custom_metadata=False,
|
| 63 |
+
popup_metadata_width="40%",
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
gr.Markdown("## Metadata Viewer")
|
| 67 |
+
with gr.Row():
|
| 68 |
+
model_box = gr.Textbox(label="Model")
|
| 69 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 70 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 71 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 72 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 73 |
+
|
| 74 |
+
# Event handling
|
| 75 |
+
output_fields = [
|
| 76 |
+
model_box,
|
| 77 |
+
fnumber_box,
|
| 78 |
+
iso_box,
|
| 79 |
+
s_churn,
|
| 80 |
+
description_box
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
# Populate the gallery when the folder changes
|
| 84 |
+
folder_explorer.change(
|
| 85 |
+
fn=load_media_from_folder,
|
| 86 |
+
inputs=folder_explorer,
|
| 87 |
+
outputs=gallery
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Populate the gallery on initial load
|
| 91 |
+
demo.load(
|
| 92 |
+
fn=load_media_from_folder,
|
| 93 |
+
inputs=folder_explorer,
|
| 94 |
+
outputs=gallery
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Handle the load_metadata event from MediaGallery
|
| 98 |
+
gallery.load_metadata(
|
| 99 |
+
fn=handle_load_metadata,
|
| 100 |
+
inputs=None,
|
| 101 |
+
outputs=output_fields
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
"""
|
| 106 |
+
Launches the Gradio interface in debug mode.
|
| 107 |
+
"""
|
| 108 |
+
demo.launch(debug=True)
|
css.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html {
|
| 2 |
+
font-family: Inter;
|
| 3 |
+
font-size: 16px;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
-webkit-text-size-adjust: 100%;
|
| 7 |
+
background: #fff;
|
| 8 |
+
color: #323232;
|
| 9 |
+
-webkit-font-smoothing: antialiased;
|
| 10 |
+
-moz-osx-font-smoothing: grayscale;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--space: 1;
|
| 16 |
+
--vspace: calc(var(--space) * 1rem);
|
| 17 |
+
--vspace-0: calc(3 * var(--space) * 1rem);
|
| 18 |
+
--vspace-1: calc(2 * var(--space) * 1rem);
|
| 19 |
+
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
| 20 |
+
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.app {
|
| 24 |
+
max-width: 748px !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prose p {
|
| 28 |
+
margin: var(--vspace) 0;
|
| 29 |
+
line-height: var(--vspace * 2);
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
code {
|
| 34 |
+
font-family: "Inconsolata", sans-serif;
|
| 35 |
+
font-size: 16px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1,
|
| 39 |
+
h1 code {
|
| 40 |
+
font-weight: 400;
|
| 41 |
+
line-height: calc(2.5 / var(--space) * var(--vspace));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 code {
|
| 45 |
+
background: none;
|
| 46 |
+
border: none;
|
| 47 |
+
letter-spacing: 0.05em;
|
| 48 |
+
padding-bottom: 5px;
|
| 49 |
+
position: relative;
|
| 50 |
+
padding: 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h2 {
|
| 54 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 55 |
+
line-height: 1em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h3,
|
| 59 |
+
h3 code {
|
| 60 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 61 |
+
line-height: 1em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h4,
|
| 65 |
+
h5,
|
| 66 |
+
h6 {
|
| 67 |
+
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
| 68 |
+
line-height: var(--vspace);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.bigtitle,
|
| 72 |
+
h1,
|
| 73 |
+
h1 code {
|
| 74 |
+
font-size: calc(8px * 4.5);
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.title,
|
| 79 |
+
h2,
|
| 80 |
+
h2 code {
|
| 81 |
+
font-size: calc(8px * 3.375);
|
| 82 |
+
font-weight: lighter;
|
| 83 |
+
word-break: break-word;
|
| 84 |
+
border: none;
|
| 85 |
+
background: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.subheading1,
|
| 89 |
+
h3,
|
| 90 |
+
h3 code {
|
| 91 |
+
font-size: calc(8px * 1.8);
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
border: none;
|
| 94 |
+
background: none;
|
| 95 |
+
letter-spacing: 0.1em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
h2 code {
|
| 100 |
+
padding: 0;
|
| 101 |
+
position: relative;
|
| 102 |
+
letter-spacing: 0.05em;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
blockquote {
|
| 106 |
+
font-size: calc(8px * 1.1667);
|
| 107 |
+
font-style: italic;
|
| 108 |
+
line-height: calc(1.1667 * var(--vspace));
|
| 109 |
+
margin: var(--vspace-2) var(--vspace-2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.subheading2,
|
| 113 |
+
h4 {
|
| 114 |
+
font-size: calc(8px * 1.4292);
|
| 115 |
+
text-transform: uppercase;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.subheading3,
|
| 120 |
+
h5 {
|
| 121 |
+
font-size: calc(8px * 1.2917);
|
| 122 |
+
line-height: calc(1.2917 * var(--vspace));
|
| 123 |
+
|
| 124 |
+
font-weight: lighter;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
letter-spacing: 0.15em;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h6 {
|
| 130 |
+
font-size: calc(8px * 1.1667);
|
| 131 |
+
font-size: 1.1667em;
|
| 132 |
+
font-weight: normal;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
font-family: "le-monde-livre-classic-byol", serif !important;
|
| 135 |
+
letter-spacing: 0px !important;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#start .md > *:first-child {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
h2 + h3 {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.md hr {
|
| 147 |
+
border: none;
|
| 148 |
+
border-top: 1px solid var(--block-border-color);
|
| 149 |
+
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
| 150 |
+
}
|
| 151 |
+
.prose ul {
|
| 152 |
+
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.gap {
|
| 156 |
+
gap: 0;
|
| 157 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio_mediagallery
|
| 2 |
+
gradio_folderexplorer
|
space.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from app import demo as app
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
_docs = {'MediaGallery': {'description': 'Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.\nIf used as an output, the user can click on individual images or videos to view them at a higher resolution.\n', 'members': {'__init__': {'value': {'type': 'Sequence[\n np.ndarray | PIL.Image.Image | str | Path | tuple\n ]\n | Callable\n | None', 'default': 'None', 'description': 'List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'file_types': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of file extensions or types of files to be uploaded (e.g. [\'image\', \'.mp4\']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'visible': {'type': 'bool | Literal["hidden"]', 'default': 'True', 'description': 'If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'columns': {'type': 'int | None', 'default': '2', 'description': 'Represents the number of images that should be shown in one row.'}, 'rows': {'type': 'int | None', 'default': 'None', 'description': 'Represents the number of rows in the image grid.'}, 'height': {'type': 'int | float | str | None', 'default': 'None', 'description': 'The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.'}, 'allow_preview': {'type': 'bool', 'default': 'True', 'description': 'If True, images in the gallery will be enlarged when they are clicked. Default is True.'}, 'preview': {'type': 'bool | None', 'default': 'None', 'description': 'If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.'}, 'selected_index': {'type': 'int | None', 'default': 'None', 'description': 'The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.'}, 'object_fit': {'type': 'Literal[\n "contain", "cover", "fill", "none", "scale-down"\n ]\n | None', 'default': 'None', 'description': 'CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_download_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"filepath"', 'description': 'The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '500', 'description': 'The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").'}}, 'postprocess': {'value': {'type': 'list | None', 'description': "The output data received by the component from the user's function in the backend."}}, 'preprocess': {'return': {'type': 'Any', 'description': "The preprocessed input data sent to the user's function in the backend."}, 'value': None}}, 'events': {'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'delete': {'type': None, 'default': None, 'description': 'This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data'}, 'preview_close': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is closed by the user'}, 'preview_open': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is opened by the user'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata."}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'MediaGallery': []}}}
|
| 7 |
+
|
| 8 |
+
abs_path = os.path.join(os.path.dirname(__file__), "css.css")
|
| 9 |
+
|
| 10 |
+
with gr.Blocks(
|
| 11 |
+
css=abs_path,
|
| 12 |
+
theme=gr.themes.Ocean(
|
| 13 |
+
font_mono=[
|
| 14 |
+
gr.themes.GoogleFont("Inconsolata"),
|
| 15 |
+
"monospace",
|
| 16 |
+
],
|
| 17 |
+
),
|
| 18 |
+
) as demo:
|
| 19 |
+
gr.Markdown(
|
| 20 |
+
"""
|
| 21 |
+
# `gradio_mediagallery`
|
| 22 |
+
|
| 23 |
+
<div style="display: flex; gap: 7px;">
|
| 24 |
+
<a href="https://pypi.org/project/gradio_mediagallery/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_mediagallery"></a>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
Python library for easily interacting with trained machine learning models
|
| 28 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 29 |
+
app.render()
|
| 30 |
+
gr.Markdown(
|
| 31 |
+
"""
|
| 32 |
+
## Installation
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install gradio_mediagallery
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from typing import Any, List
|
| 42 |
+
import gradio as gr
|
| 43 |
+
from gradio_folderexplorer import FolderExplorer
|
| 44 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 45 |
+
from gradio_mediagallery import MediaGallery
|
| 46 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 47 |
+
import os
|
| 48 |
+
|
| 49 |
+
# Configuration constant for the root directory containing media files
|
| 50 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 51 |
+
|
| 52 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 53 |
+
\"\"\"
|
| 54 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 61 |
+
\"\"\"
|
| 62 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 63 |
+
return [gr.skip()] * len(output_fields)
|
| 64 |
+
|
| 65 |
+
return transfer_metadata(
|
| 66 |
+
output_fields=output_fields,
|
| 67 |
+
metadata=image_data._data,
|
| 68 |
+
remove_prefix_from_keys=True
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# UI layout and logic
|
| 72 |
+
with gr.Blocks() as demo:
|
| 73 |
+
\"\"\"
|
| 74 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 75 |
+
\"\"\"
|
| 76 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 77 |
+
gr.Markdown(
|
| 78 |
+
\"\"\"
|
| 79 |
+
**To Test:**
|
| 80 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 81 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 82 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 83 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 84 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 85 |
+
\"\"\"
|
| 86 |
+
)
|
| 87 |
+
with gr.Row(equal_height=True):
|
| 88 |
+
with gr.Column(scale=1, min_width=300):
|
| 89 |
+
folder_explorer = FolderExplorer(
|
| 90 |
+
label="Select a Folder",
|
| 91 |
+
root_dir=ROOT_DIR_PATH,
|
| 92 |
+
value=ROOT_DIR_PATH
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
with gr.Column(scale=3):
|
| 96 |
+
gallery = MediaGallery(
|
| 97 |
+
label="Media in Folder",
|
| 98 |
+
columns=6,
|
| 99 |
+
height="auto",
|
| 100 |
+
preview=False,
|
| 101 |
+
show_download_button=False,
|
| 102 |
+
only_custom_metadata=False,
|
| 103 |
+
popup_metadata_width="40%",
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
gr.Markdown("## Metadata Viewer")
|
| 107 |
+
with gr.Row():
|
| 108 |
+
model_box = gr.Textbox(label="Model")
|
| 109 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 110 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 111 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 112 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 113 |
+
|
| 114 |
+
# Event handling
|
| 115 |
+
output_fields = [
|
| 116 |
+
model_box,
|
| 117 |
+
fnumber_box,
|
| 118 |
+
iso_box,
|
| 119 |
+
s_churn,
|
| 120 |
+
description_box
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
# Populate the gallery when the folder changes
|
| 124 |
+
folder_explorer.change(
|
| 125 |
+
fn=load_media_from_folder,
|
| 126 |
+
inputs=folder_explorer,
|
| 127 |
+
outputs=gallery
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Populate the gallery on initial load
|
| 131 |
+
demo.load(
|
| 132 |
+
fn=load_media_from_folder,
|
| 133 |
+
inputs=folder_explorer,
|
| 134 |
+
outputs=gallery
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Handle the load_metadata event from MediaGallery
|
| 138 |
+
gallery.load_metadata(
|
| 139 |
+
fn=handle_load_metadata,
|
| 140 |
+
inputs=None,
|
| 141 |
+
outputs=output_fields
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
\"\"\"
|
| 146 |
+
Launches the Gradio interface in debug mode.
|
| 147 |
+
\"\"\"
|
| 148 |
+
demo.launch(debug=True)
|
| 149 |
+
```
|
| 150 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
gr.Markdown("""
|
| 154 |
+
## `MediaGallery`
|
| 155 |
+
|
| 156 |
+
### Initialization
|
| 157 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 158 |
+
|
| 159 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["members"]["__init__"], linkify=[])
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
gr.Markdown("### Events")
|
| 163 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["events"], linkify=['Event'])
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
gr.Markdown("""
|
| 169 |
+
|
| 170 |
+
### User function
|
| 171 |
+
|
| 172 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 173 |
+
|
| 174 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 175 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 176 |
+
|
| 177 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 178 |
+
|
| 179 |
+
- **As input:** Is passed, the preprocessed input data sent to the user's function in the backend.
|
| 180 |
+
- **As output:** Should return, the output data received by the component from the user's function in the backend.
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
def predict(
|
| 184 |
+
value: Any
|
| 185 |
+
) -> list | None:
|
| 186 |
+
return value
|
| 187 |
+
```
|
| 188 |
+
""", elem_classes=["md-custom", "MediaGallery-user-fn"], header_links=True)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
demo.load(None, js=r"""function() {
|
| 194 |
+
const refs = {};
|
| 195 |
+
const user_fn_refs = {
|
| 196 |
+
MediaGallery: [], };
|
| 197 |
+
requestAnimationFrame(() => {
|
| 198 |
+
|
| 199 |
+
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
| 200 |
+
if (refs.length > 0) {
|
| 201 |
+
const el = document.querySelector(`.${key}-user-fn`);
|
| 202 |
+
if (!el) return;
|
| 203 |
+
refs.forEach(ref => {
|
| 204 |
+
el.innerHTML = el.innerHTML.replace(
|
| 205 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 206 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 207 |
+
);
|
| 208 |
+
})
|
| 209 |
+
}
|
| 210 |
+
})
|
| 211 |
+
|
| 212 |
+
Object.entries(refs).forEach(([key, refs]) => {
|
| 213 |
+
if (refs.length > 0) {
|
| 214 |
+
const el = document.querySelector(`.${key}`);
|
| 215 |
+
if (!el) return;
|
| 216 |
+
refs.forEach(ref => {
|
| 217 |
+
el.innerHTML = el.innerHTML.replace(
|
| 218 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 219 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 220 |
+
);
|
| 221 |
+
})
|
| 222 |
+
}
|
| 223 |
+
})
|
| 224 |
+
})
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
""")
|
| 228 |
+
|
| 229 |
+
demo.launch()
|
src/.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.eggs/
|
| 2 |
+
dist/
|
| 3 |
+
.vscode/
|
| 4 |
+
*.pyc
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*$py.class
|
| 8 |
+
__tmp/*
|
| 9 |
+
*.pyi
|
| 10 |
+
.mypycache
|
| 11 |
+
.ruff_cache
|
| 12 |
+
node_modules
|
| 13 |
+
backend/**/templates/
|
src/.vscode/launch.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Python Debugger: Current File",
|
| 9 |
+
"type": "debugpy",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"program": "${file}",
|
| 12 |
+
"console": "integratedTerminal",
|
| 13 |
+
"justMyCode": false
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"name": "Gradio dev (Python attach)",
|
| 17 |
+
"type": "debugpy",
|
| 18 |
+
"request": "attach",
|
| 19 |
+
"processId": "${command:pickProcess}",
|
| 20 |
+
"justMyCode": false
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"name": "Gradio dev (Svelte attach)",
|
| 24 |
+
"type": "chrome",
|
| 25 |
+
"request": "attach",
|
| 26 |
+
"port": 9222,
|
| 27 |
+
}
|
| 28 |
+
]
|
| 29 |
+
}
|
src/README.md
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags: [gradio-custom-component, gallery]
|
| 3 |
+
title: gradio_mediagallery
|
| 4 |
+
short_description: A gradio custom component
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: gradio
|
| 8 |
+
pinned: false
|
| 9 |
+
app_file: space.py
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# `gradio_mediagallery`
|
| 13 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
|
| 14 |
+
|
| 15 |
+
Python library for easily interacting with trained machine learning models
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install gradio_mediagallery gradio_folderexplorer
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from typing import Any, List
|
| 27 |
+
import gradio as gr
|
| 28 |
+
from gradio_folderexplorer import FolderExplorer
|
| 29 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 30 |
+
from gradio_mediagallery import MediaGallery
|
| 31 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 32 |
+
import os
|
| 33 |
+
|
| 34 |
+
# Configuration constant for the root directory containing media files
|
| 35 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 36 |
+
|
| 37 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 38 |
+
"""
|
| 39 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 46 |
+
"""
|
| 47 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 48 |
+
return [gr.skip()] * len(output_fields)
|
| 49 |
+
|
| 50 |
+
return transfer_metadata(
|
| 51 |
+
output_fields=output_fields,
|
| 52 |
+
metadata=image_data._data,
|
| 53 |
+
remove_prefix_from_keys=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# UI layout and logic
|
| 57 |
+
with gr.Blocks() as demo:
|
| 58 |
+
"""
|
| 59 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 60 |
+
"""
|
| 61 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 62 |
+
gr.Markdown(
|
| 63 |
+
"""
|
| 64 |
+
**To Test:**
|
| 65 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 66 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 67 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 68 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 69 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 70 |
+
"""
|
| 71 |
+
)
|
| 72 |
+
with gr.Row(equal_height=True):
|
| 73 |
+
with gr.Column(scale=1, min_width=300):
|
| 74 |
+
folder_explorer = FolderExplorer(
|
| 75 |
+
label="Select a Folder",
|
| 76 |
+
root_dir=ROOT_DIR_PATH,
|
| 77 |
+
value=ROOT_DIR_PATH
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
with gr.Column(scale=3):
|
| 81 |
+
gallery = MediaGallery(
|
| 82 |
+
label="Media in Folder",
|
| 83 |
+
columns=6,
|
| 84 |
+
height="auto",
|
| 85 |
+
preview=False,
|
| 86 |
+
show_download_button=False,
|
| 87 |
+
only_custom_metadata=False,
|
| 88 |
+
popup_metadata_width="40%",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
gr.Markdown("## Metadata Viewer")
|
| 92 |
+
with gr.Row():
|
| 93 |
+
model_box = gr.Textbox(label="Model")
|
| 94 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 95 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 96 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 97 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 98 |
+
|
| 99 |
+
# Event handling
|
| 100 |
+
output_fields = [
|
| 101 |
+
model_box,
|
| 102 |
+
fnumber_box,
|
| 103 |
+
iso_box,
|
| 104 |
+
s_churn,
|
| 105 |
+
description_box
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
# Populate the gallery when the folder changes
|
| 109 |
+
folder_explorer.change(
|
| 110 |
+
fn=load_media_from_folder,
|
| 111 |
+
inputs=folder_explorer,
|
| 112 |
+
outputs=gallery
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Populate the gallery on initial load
|
| 116 |
+
demo.load(
|
| 117 |
+
fn=load_media_from_folder,
|
| 118 |
+
inputs=folder_explorer,
|
| 119 |
+
outputs=gallery
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Handle the load_metadata event from MediaGallery
|
| 123 |
+
gallery.load_metadata(
|
| 124 |
+
fn=handle_load_metadata,
|
| 125 |
+
inputs=None,
|
| 126 |
+
outputs=output_fields
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
"""
|
| 131 |
+
Launches the Gradio interface in debug mode.
|
| 132 |
+
"""
|
| 133 |
+
demo.launch(debug=True)
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## `MediaGallery`
|
| 137 |
+
|
| 138 |
+
### Initialization
|
| 139 |
+
|
| 140 |
+
<table>
|
| 141 |
+
<thead>
|
| 142 |
+
<tr>
|
| 143 |
+
<th align="left">name</th>
|
| 144 |
+
<th align="left" style="width: 25%;">type</th>
|
| 145 |
+
<th align="left">default</th>
|
| 146 |
+
<th align="left">description</th>
|
| 147 |
+
</tr>
|
| 148 |
+
</thead>
|
| 149 |
+
<tbody>
|
| 150 |
+
<tr>
|
| 151 |
+
<td align="left"><code>value</code></td>
|
| 152 |
+
<td align="left" style="width: 25%;">
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
Sequence[
|
| 156 |
+
np.ndarray | PIL.Image.Image | str | Path | tuple
|
| 157 |
+
]
|
| 158 |
+
| Callable
|
| 159 |
+
| None
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
</td>
|
| 163 |
+
<td align="left"><code>None</code></td>
|
| 164 |
+
<td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
|
| 165 |
+
</tr>
|
| 166 |
+
|
| 167 |
+
<tr>
|
| 168 |
+
<td align="left"><code>file_types</code></td>
|
| 169 |
+
<td align="left" style="width: 25%;">
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
list[str] | None
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
</td>
|
| 176 |
+
<td align="left"><code>None</code></td>
|
| 177 |
+
<td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
|
| 178 |
+
</tr>
|
| 179 |
+
|
| 180 |
+
<tr>
|
| 181 |
+
<td align="left"><code>label</code></td>
|
| 182 |
+
<td align="left" style="width: 25%;">
|
| 183 |
+
|
| 184 |
+
```python
|
| 185 |
+
str | I18nData | None
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
</td>
|
| 189 |
+
<td align="left"><code>None</code></td>
|
| 190 |
+
<td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
|
| 191 |
+
</tr>
|
| 192 |
+
|
| 193 |
+
<tr>
|
| 194 |
+
<td align="left"><code>every</code></td>
|
| 195 |
+
<td align="left" style="width: 25%;">
|
| 196 |
+
|
| 197 |
+
```python
|
| 198 |
+
Timer | float | None
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
</td>
|
| 202 |
+
<td align="left"><code>None</code></td>
|
| 203 |
+
<td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
|
| 204 |
+
</tr>
|
| 205 |
+
|
| 206 |
+
<tr>
|
| 207 |
+
<td align="left"><code>inputs</code></td>
|
| 208 |
+
<td align="left" style="width: 25%;">
|
| 209 |
+
|
| 210 |
+
```python
|
| 211 |
+
Component | Sequence[Component] | set[Component] | None
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
</td>
|
| 215 |
+
<td align="left"><code>None</code></td>
|
| 216 |
+
<td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
|
| 217 |
+
</tr>
|
| 218 |
+
|
| 219 |
+
<tr>
|
| 220 |
+
<td align="left"><code>show_label</code></td>
|
| 221 |
+
<td align="left" style="width: 25%;">
|
| 222 |
+
|
| 223 |
+
```python
|
| 224 |
+
bool | None
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
</td>
|
| 228 |
+
<td align="left"><code>None</code></td>
|
| 229 |
+
<td align="left">if True, will display label.</td>
|
| 230 |
+
</tr>
|
| 231 |
+
|
| 232 |
+
<tr>
|
| 233 |
+
<td align="left"><code>container</code></td>
|
| 234 |
+
<td align="left" style="width: 25%;">
|
| 235 |
+
|
| 236 |
+
```python
|
| 237 |
+
bool
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
</td>
|
| 241 |
+
<td align="left"><code>True</code></td>
|
| 242 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 243 |
+
</tr>
|
| 244 |
+
|
| 245 |
+
<tr>
|
| 246 |
+
<td align="left"><code>scale</code></td>
|
| 247 |
+
<td align="left" style="width: 25%;">
|
| 248 |
+
|
| 249 |
+
```python
|
| 250 |
+
int | None
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
</td>
|
| 254 |
+
<td align="left"><code>None</code></td>
|
| 255 |
+
<td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
|
| 256 |
+
</tr>
|
| 257 |
+
|
| 258 |
+
<tr>
|
| 259 |
+
<td align="left"><code>min_width</code></td>
|
| 260 |
+
<td align="left" style="width: 25%;">
|
| 261 |
+
|
| 262 |
+
```python
|
| 263 |
+
int
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
</td>
|
| 267 |
+
<td align="left"><code>160</code></td>
|
| 268 |
+
<td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
|
| 269 |
+
</tr>
|
| 270 |
+
|
| 271 |
+
<tr>
|
| 272 |
+
<td align="left"><code>visible</code></td>
|
| 273 |
+
<td align="left" style="width: 25%;">
|
| 274 |
+
|
| 275 |
+
```python
|
| 276 |
+
bool | Literal["hidden"]
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
</td>
|
| 280 |
+
<td align="left"><code>True</code></td>
|
| 281 |
+
<td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
|
| 282 |
+
</tr>
|
| 283 |
+
|
| 284 |
+
<tr>
|
| 285 |
+
<td align="left"><code>elem_id</code></td>
|
| 286 |
+
<td align="left" style="width: 25%;">
|
| 287 |
+
|
| 288 |
+
```python
|
| 289 |
+
str | None
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
</td>
|
| 293 |
+
<td align="left"><code>None</code></td>
|
| 294 |
+
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 295 |
+
</tr>
|
| 296 |
+
|
| 297 |
+
<tr>
|
| 298 |
+
<td align="left"><code>elem_classes</code></td>
|
| 299 |
+
<td align="left" style="width: 25%;">
|
| 300 |
+
|
| 301 |
+
```python
|
| 302 |
+
list[str] | str | None
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
</td>
|
| 306 |
+
<td align="left"><code>None</code></td>
|
| 307 |
+
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 308 |
+
</tr>
|
| 309 |
+
|
| 310 |
+
<tr>
|
| 311 |
+
<td align="left"><code>render</code></td>
|
| 312 |
+
<td align="left" style="width: 25%;">
|
| 313 |
+
|
| 314 |
+
```python
|
| 315 |
+
bool
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
</td>
|
| 319 |
+
<td align="left"><code>True</code></td>
|
| 320 |
+
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
|
| 321 |
+
</tr>
|
| 322 |
+
|
| 323 |
+
<tr>
|
| 324 |
+
<td align="left"><code>key</code></td>
|
| 325 |
+
<td align="left" style="width: 25%;">
|
| 326 |
+
|
| 327 |
+
```python
|
| 328 |
+
int | str | tuple[int | str, ...] | None
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
</td>
|
| 332 |
+
<td align="left"><code>None</code></td>
|
| 333 |
+
<td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
|
| 334 |
+
</tr>
|
| 335 |
+
|
| 336 |
+
<tr>
|
| 337 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 338 |
+
<td align="left" style="width: 25%;">
|
| 339 |
+
|
| 340 |
+
```python
|
| 341 |
+
list[str] | str | None
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
</td>
|
| 345 |
+
<td align="left"><code>"value"</code></td>
|
| 346 |
+
<td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
|
| 347 |
+
</tr>
|
| 348 |
+
|
| 349 |
+
<tr>
|
| 350 |
+
<td align="left"><code>columns</code></td>
|
| 351 |
+
<td align="left" style="width: 25%;">
|
| 352 |
+
|
| 353 |
+
```python
|
| 354 |
+
int | None
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
</td>
|
| 358 |
+
<td align="left"><code>2</code></td>
|
| 359 |
+
<td align="left">Represents the number of images that should be shown in one row.</td>
|
| 360 |
+
</tr>
|
| 361 |
+
|
| 362 |
+
<tr>
|
| 363 |
+
<td align="left"><code>rows</code></td>
|
| 364 |
+
<td align="left" style="width: 25%;">
|
| 365 |
+
|
| 366 |
+
```python
|
| 367 |
+
int | None
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
</td>
|
| 371 |
+
<td align="left"><code>None</code></td>
|
| 372 |
+
<td align="left">Represents the number of rows in the image grid.</td>
|
| 373 |
+
</tr>
|
| 374 |
+
|
| 375 |
+
<tr>
|
| 376 |
+
<td align="left"><code>height</code></td>
|
| 377 |
+
<td align="left" style="width: 25%;">
|
| 378 |
+
|
| 379 |
+
```python
|
| 380 |
+
int | float | str | None
|
| 381 |
+
```
|
| 382 |
+
|
| 383 |
+
</td>
|
| 384 |
+
<td align="left"><code>None</code></td>
|
| 385 |
+
<td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
|
| 386 |
+
</tr>
|
| 387 |
+
|
| 388 |
+
<tr>
|
| 389 |
+
<td align="left"><code>allow_preview</code></td>
|
| 390 |
+
<td align="left" style="width: 25%;">
|
| 391 |
+
|
| 392 |
+
```python
|
| 393 |
+
bool
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
</td>
|
| 397 |
+
<td align="left"><code>True</code></td>
|
| 398 |
+
<td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
|
| 399 |
+
</tr>
|
| 400 |
+
|
| 401 |
+
<tr>
|
| 402 |
+
<td align="left"><code>preview</code></td>
|
| 403 |
+
<td align="left" style="width: 25%;">
|
| 404 |
+
|
| 405 |
+
```python
|
| 406 |
+
bool | None
|
| 407 |
+
```
|
| 408 |
+
|
| 409 |
+
</td>
|
| 410 |
+
<td align="left"><code>None</code></td>
|
| 411 |
+
<td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
|
| 412 |
+
</tr>
|
| 413 |
+
|
| 414 |
+
<tr>
|
| 415 |
+
<td align="left"><code>selected_index</code></td>
|
| 416 |
+
<td align="left" style="width: 25%;">
|
| 417 |
+
|
| 418 |
+
```python
|
| 419 |
+
int | None
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
</td>
|
| 423 |
+
<td align="left"><code>None</code></td>
|
| 424 |
+
<td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
|
| 425 |
+
</tr>
|
| 426 |
+
|
| 427 |
+
<tr>
|
| 428 |
+
<td align="left"><code>object_fit</code></td>
|
| 429 |
+
<td align="left" style="width: 25%;">
|
| 430 |
+
|
| 431 |
+
```python
|
| 432 |
+
Literal[
|
| 433 |
+
"contain", "cover", "fill", "none", "scale-down"
|
| 434 |
+
]
|
| 435 |
+
| None
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
</td>
|
| 439 |
+
<td align="left"><code>None</code></td>
|
| 440 |
+
<td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
|
| 441 |
+
</tr>
|
| 442 |
+
|
| 443 |
+
<tr>
|
| 444 |
+
<td align="left"><code>show_share_button</code></td>
|
| 445 |
+
<td align="left" style="width: 25%;">
|
| 446 |
+
|
| 447 |
+
```python
|
| 448 |
+
bool | None
|
| 449 |
+
```
|
| 450 |
+
|
| 451 |
+
</td>
|
| 452 |
+
<td align="left"><code>None</code></td>
|
| 453 |
+
<td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 454 |
+
</tr>
|
| 455 |
+
|
| 456 |
+
<tr>
|
| 457 |
+
<td align="left"><code>show_download_button</code></td>
|
| 458 |
+
<td align="left" style="width: 25%;">
|
| 459 |
+
|
| 460 |
+
```python
|
| 461 |
+
bool | None
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
</td>
|
| 465 |
+
<td align="left"><code>True</code></td>
|
| 466 |
+
<td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
|
| 467 |
+
</tr>
|
| 468 |
+
|
| 469 |
+
<tr>
|
| 470 |
+
<td align="left"><code>interactive</code></td>
|
| 471 |
+
<td align="left" style="width: 25%;">
|
| 472 |
+
|
| 473 |
+
```python
|
| 474 |
+
bool | None
|
| 475 |
+
```
|
| 476 |
+
|
| 477 |
+
</td>
|
| 478 |
+
<td align="left"><code>None</code></td>
|
| 479 |
+
<td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
|
| 480 |
+
</tr>
|
| 481 |
+
|
| 482 |
+
<tr>
|
| 483 |
+
<td align="left"><code>type</code></td>
|
| 484 |
+
<td align="left" style="width: 25%;">
|
| 485 |
+
|
| 486 |
+
```python
|
| 487 |
+
Literal["numpy", "pil", "filepath"]
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
</td>
|
| 491 |
+
<td align="left"><code>"filepath"</code></td>
|
| 492 |
+
<td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
|
| 493 |
+
</tr>
|
| 494 |
+
|
| 495 |
+
<tr>
|
| 496 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 497 |
+
<td align="left" style="width: 25%;">
|
| 498 |
+
|
| 499 |
+
```python
|
| 500 |
+
bool
|
| 501 |
+
```
|
| 502 |
+
|
| 503 |
+
</td>
|
| 504 |
+
<td align="left"><code>True</code></td>
|
| 505 |
+
<td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 506 |
+
</tr>
|
| 507 |
+
|
| 508 |
+
<tr>
|
| 509 |
+
<td align="left"><code>only_custom_metadata</code></td>
|
| 510 |
+
<td align="left" style="width: 25%;">
|
| 511 |
+
|
| 512 |
+
```python
|
| 513 |
+
bool
|
| 514 |
+
```
|
| 515 |
+
|
| 516 |
+
</td>
|
| 517 |
+
<td align="left"><code>True</code></td>
|
| 518 |
+
<td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
|
| 519 |
+
</tr>
|
| 520 |
+
|
| 521 |
+
<tr>
|
| 522 |
+
<td align="left"><code>popup_metadata_width</code></td>
|
| 523 |
+
<td align="left" style="width: 25%;">
|
| 524 |
+
|
| 525 |
+
```python
|
| 526 |
+
int | str
|
| 527 |
+
```
|
| 528 |
+
|
| 529 |
+
</td>
|
| 530 |
+
<td align="left"><code>500</code></td>
|
| 531 |
+
<td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
|
| 532 |
+
</tr>
|
| 533 |
+
</tbody></table>
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
### Events
|
| 537 |
+
|
| 538 |
+
| name | description |
|
| 539 |
+
|:-----|:------------|
|
| 540 |
+
| `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
|
| 541 |
+
| `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
|
| 542 |
+
| `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
|
| 543 |
+
| `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
|
| 544 |
+
| `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
|
| 545 |
+
| `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
### User function
|
| 550 |
+
|
| 551 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 552 |
+
|
| 553 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 554 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 555 |
+
|
| 556 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 557 |
+
|
| 558 |
+
- **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
|
| 559 |
+
- **As input:** Should return, the output data received by the component from the user's function in the backend.
|
| 560 |
+
|
| 561 |
+
```python
|
| 562 |
+
def predict(
|
| 563 |
+
value: Any
|
| 564 |
+
) -> list | None:
|
| 565 |
+
return value
|
| 566 |
+
```
|
| 567 |
+
|
src/README_TEMPLATE.md
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags: [gradio-custom-component, gallery]
|
| 3 |
+
title: gradio_mediagallery
|
| 4 |
+
short_description: A gradio custom component
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: gradio
|
| 8 |
+
pinned: false
|
| 9 |
+
app_file: space.py
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# `gradio_mediagallery`
|
| 13 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_mediagallery"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_mediagallery'>Component GitHub Code</a></span></p>
|
| 14 |
+
|
| 15 |
+
Python library for easily interacting with trained machine learning models
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install gradio_mediagallery gradio_folderexplorer
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from typing import Any, List
|
| 27 |
+
import gradio as gr
|
| 28 |
+
from gradio_folderexplorer import FolderExplorer
|
| 29 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 30 |
+
from gradio_mediagallery import MediaGallery
|
| 31 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 32 |
+
import os
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# --- Configuration Constants ---
|
| 37 |
+
ROOT_DIR_PATH = "./examples" # Use uma pasta com imagens com metadados para teste
|
| 38 |
+
|
| 39 |
+
# --- Event Callback Function ---
|
| 40 |
+
|
| 41 |
+
# Esta função é chamada quando o evento `load_metadata` é disparado do frontend.
|
| 42 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 43 |
+
"""
|
| 44 |
+
Processes image metadata by calling the agnostic `transfer_metadata` helper.
|
| 45 |
+
"""
|
| 46 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 47 |
+
return [gr.skip()] * len(output_fields)
|
| 48 |
+
|
| 49 |
+
# Call the agnostic helper function to do the heavy lifting.
|
| 50 |
+
return transfer_metadata(
|
| 51 |
+
output_fields=output_fields,
|
| 52 |
+
metadata=image_data._data,
|
| 53 |
+
remove_prefix_from_keys=True
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# --- UI Layout and Logic ---
|
| 57 |
+
|
| 58 |
+
with gr.Blocks() as demo:
|
| 59 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 60 |
+
gr.Markdown(
|
| 61 |
+
"""
|
| 62 |
+
**To Test:**
|
| 63 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 64 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 65 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 66 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 67 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 68 |
+
"""
|
| 69 |
+
)
|
| 70 |
+
with gr.Row(equal_height=True):
|
| 71 |
+
with gr.Column(scale=1, min_width=300):
|
| 72 |
+
folder_explorer = FolderExplorer(
|
| 73 |
+
label="Select a Folder",
|
| 74 |
+
root_dir=ROOT_DIR_PATH,
|
| 75 |
+
value=ROOT_DIR_PATH
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
with gr.Column(scale=3):
|
| 79 |
+
# Usando nosso MediaGallery customizado
|
| 80 |
+
gallery = MediaGallery(
|
| 81 |
+
label="Media in Folder",
|
| 82 |
+
columns=6,
|
| 83 |
+
height="auto",
|
| 84 |
+
preview=False,
|
| 85 |
+
show_download_button=False,
|
| 86 |
+
only_custom_metadata=False, # Agora mostra todos os metadados
|
| 87 |
+
popup_metadata_width="40%", # Popup mais largo
|
| 88 |
+
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
gr.Markdown("## Metadata Viewer")
|
| 92 |
+
with gr.Row():
|
| 93 |
+
model_box = gr.Textbox(label="Model")
|
| 94 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 95 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 96 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 97 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 98 |
+
# --- Event Handling ---
|
| 99 |
+
|
| 100 |
+
# Evento para popular a galeria quando a pasta muda
|
| 101 |
+
folder_explorer.change(
|
| 102 |
+
fn=load_media_from_folder,
|
| 103 |
+
inputs=folder_explorer,
|
| 104 |
+
outputs=gallery
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# Evento para popular a galeria no carregamento inicial
|
| 108 |
+
demo.load(
|
| 109 |
+
fn=load_media_from_folder,
|
| 110 |
+
inputs=folder_explorer,
|
| 111 |
+
outputs=gallery
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
output_fields = [
|
| 115 |
+
model_box,
|
| 116 |
+
fnumber_box,
|
| 117 |
+
iso_box,
|
| 118 |
+
s_churn,
|
| 119 |
+
description_box
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
# --- NOVO EVENTO DE METADADOS ---
|
| 123 |
+
# Liga o evento `load_metadata` do nosso MediaGallery à função de callback.
|
| 124 |
+
gallery.load_metadata(
|
| 125 |
+
fn=handle_load_metadata,
|
| 126 |
+
inputs=None, # O dado vem do payload do evento, não de um input explícito.
|
| 127 |
+
outputs=output_fields
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
demo.launch(debug=True)
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
## `MediaGallery`
|
| 135 |
+
|
| 136 |
+
### Initialization
|
| 137 |
+
|
| 138 |
+
<table>
|
| 139 |
+
<thead>
|
| 140 |
+
<tr>
|
| 141 |
+
<th align="left">name</th>
|
| 142 |
+
<th align="left" style="width: 25%;">type</th>
|
| 143 |
+
<th align="left">default</th>
|
| 144 |
+
<th align="left">description</th>
|
| 145 |
+
</tr>
|
| 146 |
+
</thead>
|
| 147 |
+
<tbody>
|
| 148 |
+
<tr>
|
| 149 |
+
<td align="left"><code>value</code></td>
|
| 150 |
+
<td align="left" style="width: 25%;">
|
| 151 |
+
|
| 152 |
+
```python
|
| 153 |
+
Sequence[
|
| 154 |
+
np.ndarray | PIL.Image.Image | str | Path | tuple
|
| 155 |
+
]
|
| 156 |
+
| Callable
|
| 157 |
+
| None
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
</td>
|
| 161 |
+
<td align="left"><code>None</code></td>
|
| 162 |
+
<td align="left">List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
|
| 163 |
+
</tr>
|
| 164 |
+
|
| 165 |
+
<tr>
|
| 166 |
+
<td align="left"><code>file_types</code></td>
|
| 167 |
+
<td align="left" style="width: 25%;">
|
| 168 |
+
|
| 169 |
+
```python
|
| 170 |
+
list[str] | None
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
</td>
|
| 174 |
+
<td align="left"><code>None</code></td>
|
| 175 |
+
<td align="left">List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.</td>
|
| 176 |
+
</tr>
|
| 177 |
+
|
| 178 |
+
<tr>
|
| 179 |
+
<td align="left"><code>label</code></td>
|
| 180 |
+
<td align="left" style="width: 25%;">
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
str | I18nData | None
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
</td>
|
| 187 |
+
<td align="left"><code>None</code></td>
|
| 188 |
+
<td align="left">the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
|
| 189 |
+
</tr>
|
| 190 |
+
|
| 191 |
+
<tr>
|
| 192 |
+
<td align="left"><code>every</code></td>
|
| 193 |
+
<td align="left" style="width: 25%;">
|
| 194 |
+
|
| 195 |
+
```python
|
| 196 |
+
Timer | float | None
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
</td>
|
| 200 |
+
<td align="left"><code>None</code></td>
|
| 201 |
+
<td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
|
| 202 |
+
</tr>
|
| 203 |
+
|
| 204 |
+
<tr>
|
| 205 |
+
<td align="left"><code>inputs</code></td>
|
| 206 |
+
<td align="left" style="width: 25%;">
|
| 207 |
+
|
| 208 |
+
```python
|
| 209 |
+
Component | Sequence[Component] | set[Component] | None
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
</td>
|
| 213 |
+
<td align="left"><code>None</code></td>
|
| 214 |
+
<td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
|
| 215 |
+
</tr>
|
| 216 |
+
|
| 217 |
+
<tr>
|
| 218 |
+
<td align="left"><code>show_label</code></td>
|
| 219 |
+
<td align="left" style="width: 25%;">
|
| 220 |
+
|
| 221 |
+
```python
|
| 222 |
+
bool | None
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
</td>
|
| 226 |
+
<td align="left"><code>None</code></td>
|
| 227 |
+
<td align="left">if True, will display label.</td>
|
| 228 |
+
</tr>
|
| 229 |
+
|
| 230 |
+
<tr>
|
| 231 |
+
<td align="left"><code>container</code></td>
|
| 232 |
+
<td align="left" style="width: 25%;">
|
| 233 |
+
|
| 234 |
+
```python
|
| 235 |
+
bool
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
</td>
|
| 239 |
+
<td align="left"><code>True</code></td>
|
| 240 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 241 |
+
</tr>
|
| 242 |
+
|
| 243 |
+
<tr>
|
| 244 |
+
<td align="left"><code>scale</code></td>
|
| 245 |
+
<td align="left" style="width: 25%;">
|
| 246 |
+
|
| 247 |
+
```python
|
| 248 |
+
int | None
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
</td>
|
| 252 |
+
<td align="left"><code>None</code></td>
|
| 253 |
+
<td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
|
| 254 |
+
</tr>
|
| 255 |
+
|
| 256 |
+
<tr>
|
| 257 |
+
<td align="left"><code>min_width</code></td>
|
| 258 |
+
<td align="left" style="width: 25%;">
|
| 259 |
+
|
| 260 |
+
```python
|
| 261 |
+
int
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
</td>
|
| 265 |
+
<td align="left"><code>160</code></td>
|
| 266 |
+
<td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
|
| 267 |
+
</tr>
|
| 268 |
+
|
| 269 |
+
<tr>
|
| 270 |
+
<td align="left"><code>visible</code></td>
|
| 271 |
+
<td align="left" style="width: 25%;">
|
| 272 |
+
|
| 273 |
+
```python
|
| 274 |
+
bool | Literal["hidden"]
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
</td>
|
| 278 |
+
<td align="left"><code>True</code></td>
|
| 279 |
+
<td align="left">If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM</td>
|
| 280 |
+
</tr>
|
| 281 |
+
|
| 282 |
+
<tr>
|
| 283 |
+
<td align="left"><code>elem_id</code></td>
|
| 284 |
+
<td align="left" style="width: 25%;">
|
| 285 |
+
|
| 286 |
+
```python
|
| 287 |
+
str | None
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
</td>
|
| 291 |
+
<td align="left"><code>None</code></td>
|
| 292 |
+
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 293 |
+
</tr>
|
| 294 |
+
|
| 295 |
+
<tr>
|
| 296 |
+
<td align="left"><code>elem_classes</code></td>
|
| 297 |
+
<td align="left" style="width: 25%;">
|
| 298 |
+
|
| 299 |
+
```python
|
| 300 |
+
list[str] | str | None
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
</td>
|
| 304 |
+
<td align="left"><code>None</code></td>
|
| 305 |
+
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 306 |
+
</tr>
|
| 307 |
+
|
| 308 |
+
<tr>
|
| 309 |
+
<td align="left"><code>render</code></td>
|
| 310 |
+
<td align="left" style="width: 25%;">
|
| 311 |
+
|
| 312 |
+
```python
|
| 313 |
+
bool
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
</td>
|
| 317 |
+
<td align="left"><code>True</code></td>
|
| 318 |
+
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
|
| 319 |
+
</tr>
|
| 320 |
+
|
| 321 |
+
<tr>
|
| 322 |
+
<td align="left"><code>key</code></td>
|
| 323 |
+
<td align="left" style="width: 25%;">
|
| 324 |
+
|
| 325 |
+
```python
|
| 326 |
+
int | str | tuple[int | str, ...] | None
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
</td>
|
| 330 |
+
<td align="left"><code>None</code></td>
|
| 331 |
+
<td align="left">in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
|
| 332 |
+
</tr>
|
| 333 |
+
|
| 334 |
+
<tr>
|
| 335 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 336 |
+
<td align="left" style="width: 25%;">
|
| 337 |
+
|
| 338 |
+
```python
|
| 339 |
+
list[str] | str | None
|
| 340 |
+
```
|
| 341 |
+
|
| 342 |
+
</td>
|
| 343 |
+
<td align="left"><code>"value"</code></td>
|
| 344 |
+
<td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
|
| 345 |
+
</tr>
|
| 346 |
+
|
| 347 |
+
<tr>
|
| 348 |
+
<td align="left"><code>columns</code></td>
|
| 349 |
+
<td align="left" style="width: 25%;">
|
| 350 |
+
|
| 351 |
+
```python
|
| 352 |
+
int | None
|
| 353 |
+
```
|
| 354 |
+
|
| 355 |
+
</td>
|
| 356 |
+
<td align="left"><code>2</code></td>
|
| 357 |
+
<td align="left">Represents the number of images that should be shown in one row.</td>
|
| 358 |
+
</tr>
|
| 359 |
+
|
| 360 |
+
<tr>
|
| 361 |
+
<td align="left"><code>rows</code></td>
|
| 362 |
+
<td align="left" style="width: 25%;">
|
| 363 |
+
|
| 364 |
+
```python
|
| 365 |
+
int | None
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
</td>
|
| 369 |
+
<td align="left"><code>None</code></td>
|
| 370 |
+
<td align="left">Represents the number of rows in the image grid.</td>
|
| 371 |
+
</tr>
|
| 372 |
+
|
| 373 |
+
<tr>
|
| 374 |
+
<td align="left"><code>height</code></td>
|
| 375 |
+
<td align="left" style="width: 25%;">
|
| 376 |
+
|
| 377 |
+
```python
|
| 378 |
+
int | float | str | None
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
</td>
|
| 382 |
+
<td align="left"><code>None</code></td>
|
| 383 |
+
<td align="left">The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.</td>
|
| 384 |
+
</tr>
|
| 385 |
+
|
| 386 |
+
<tr>
|
| 387 |
+
<td align="left"><code>allow_preview</code></td>
|
| 388 |
+
<td align="left" style="width: 25%;">
|
| 389 |
+
|
| 390 |
+
```python
|
| 391 |
+
bool
|
| 392 |
+
```
|
| 393 |
+
|
| 394 |
+
</td>
|
| 395 |
+
<td align="left"><code>True</code></td>
|
| 396 |
+
<td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
|
| 397 |
+
</tr>
|
| 398 |
+
|
| 399 |
+
<tr>
|
| 400 |
+
<td align="left"><code>preview</code></td>
|
| 401 |
+
<td align="left" style="width: 25%;">
|
| 402 |
+
|
| 403 |
+
```python
|
| 404 |
+
bool | None
|
| 405 |
+
```
|
| 406 |
+
|
| 407 |
+
</td>
|
| 408 |
+
<td align="left"><code>None</code></td>
|
| 409 |
+
<td align="left">If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.</td>
|
| 410 |
+
</tr>
|
| 411 |
+
|
| 412 |
+
<tr>
|
| 413 |
+
<td align="left"><code>selected_index</code></td>
|
| 414 |
+
<td align="left" style="width: 25%;">
|
| 415 |
+
|
| 416 |
+
```python
|
| 417 |
+
int | None
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
</td>
|
| 421 |
+
<td align="left"><code>None</code></td>
|
| 422 |
+
<td align="left">The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.</td>
|
| 423 |
+
</tr>
|
| 424 |
+
|
| 425 |
+
<tr>
|
| 426 |
+
<td align="left"><code>object_fit</code></td>
|
| 427 |
+
<td align="left" style="width: 25%;">
|
| 428 |
+
|
| 429 |
+
```python
|
| 430 |
+
Literal[
|
| 431 |
+
"contain", "cover", "fill", "none", "scale-down"
|
| 432 |
+
]
|
| 433 |
+
| None
|
| 434 |
+
```
|
| 435 |
+
|
| 436 |
+
</td>
|
| 437 |
+
<td align="left"><code>None</code></td>
|
| 438 |
+
<td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
|
| 439 |
+
</tr>
|
| 440 |
+
|
| 441 |
+
<tr>
|
| 442 |
+
<td align="left"><code>show_share_button</code></td>
|
| 443 |
+
<td align="left" style="width: 25%;">
|
| 444 |
+
|
| 445 |
+
```python
|
| 446 |
+
bool | None
|
| 447 |
+
```
|
| 448 |
+
|
| 449 |
+
</td>
|
| 450 |
+
<td align="left"><code>None</code></td>
|
| 451 |
+
<td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 452 |
+
</tr>
|
| 453 |
+
|
| 454 |
+
<tr>
|
| 455 |
+
<td align="left"><code>show_download_button</code></td>
|
| 456 |
+
<td align="left" style="width: 25%;">
|
| 457 |
+
|
| 458 |
+
```python
|
| 459 |
+
bool | None
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
</td>
|
| 463 |
+
<td align="left"><code>True</code></td>
|
| 464 |
+
<td align="left">If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.</td>
|
| 465 |
+
</tr>
|
| 466 |
+
|
| 467 |
+
<tr>
|
| 468 |
+
<td align="left"><code>interactive</code></td>
|
| 469 |
+
<td align="left" style="width: 25%;">
|
| 470 |
+
|
| 471 |
+
```python
|
| 472 |
+
bool | None
|
| 473 |
+
```
|
| 474 |
+
|
| 475 |
+
</td>
|
| 476 |
+
<td align="left"><code>None</code></td>
|
| 477 |
+
<td align="left">If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.</td>
|
| 478 |
+
</tr>
|
| 479 |
+
|
| 480 |
+
<tr>
|
| 481 |
+
<td align="left"><code>type</code></td>
|
| 482 |
+
<td align="left" style="width: 25%;">
|
| 483 |
+
|
| 484 |
+
```python
|
| 485 |
+
Literal["numpy", "pil", "filepath"]
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
</td>
|
| 489 |
+
<td align="left"><code>"filepath"</code></td>
|
| 490 |
+
<td align="left">The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.</td>
|
| 491 |
+
</tr>
|
| 492 |
+
|
| 493 |
+
<tr>
|
| 494 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 495 |
+
<td align="left" style="width: 25%;">
|
| 496 |
+
|
| 497 |
+
```python
|
| 498 |
+
bool
|
| 499 |
+
```
|
| 500 |
+
|
| 501 |
+
</td>
|
| 502 |
+
<td align="left"><code>True</code></td>
|
| 503 |
+
<td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 504 |
+
</tr>
|
| 505 |
+
|
| 506 |
+
<tr>
|
| 507 |
+
<td align="left"><code>only_custom_metadata</code></td>
|
| 508 |
+
<td align="left" style="width: 25%;">
|
| 509 |
+
|
| 510 |
+
```python
|
| 511 |
+
bool
|
| 512 |
+
```
|
| 513 |
+
|
| 514 |
+
</td>
|
| 515 |
+
<td align="left"><code>True</code></td>
|
| 516 |
+
<td align="left">If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.</td>
|
| 517 |
+
</tr>
|
| 518 |
+
|
| 519 |
+
<tr>
|
| 520 |
+
<td align="left"><code>popup_metadata_width</code></td>
|
| 521 |
+
<td align="left" style="width: 25%;">
|
| 522 |
+
|
| 523 |
+
```python
|
| 524 |
+
int | str
|
| 525 |
+
```
|
| 526 |
+
|
| 527 |
+
</td>
|
| 528 |
+
<td align="left"><code>500</code></td>
|
| 529 |
+
<td align="left">The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").</td>
|
| 530 |
+
</tr>
|
| 531 |
+
</tbody></table>
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
### Events
|
| 535 |
+
|
| 536 |
+
| name | description |
|
| 537 |
+
|:-----|:------------|
|
| 538 |
+
| `select` | Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data |
|
| 539 |
+
| `change` | Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
|
| 540 |
+
| `delete` | This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data |
|
| 541 |
+
| `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
|
| 542 |
+
| `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
|
| 543 |
+
| `load_metadata` | Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata. |
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
### User function
|
| 548 |
+
|
| 549 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 550 |
+
|
| 551 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 552 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 553 |
+
|
| 554 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 555 |
+
|
| 556 |
+
- **As output:** Is passed, the preprocessed input data sent to the user's function in the backend.
|
| 557 |
+
- **As input:** Should return, the output data received by the component from the user's function in the backend.
|
| 558 |
+
|
| 559 |
+
```python
|
| 560 |
+
def predict(
|
| 561 |
+
value: Any
|
| 562 |
+
) -> list | None:
|
| 563 |
+
return value
|
| 564 |
+
```
|
| 565 |
+
|
src/backend/gradio_mediagallery/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from .mediagallery import MediaGallery
|
| 3 |
+
|
| 4 |
+
__all__ = ['MediaGallery']
|
src/backend/gradio_mediagallery/helpers.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import fields, is_dataclass
|
| 2 |
+
import os
|
| 3 |
+
import numpy as np
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, Dict, List, Optional, Type
|
| 6 |
+
from PIL import Image, PngImagePlugin, ExifTags
|
| 7 |
+
|
| 8 |
+
def infer_type(s: str):
|
| 9 |
+
"""
|
| 10 |
+
Infers and converts a string to the most likely data type.
|
| 11 |
+
|
| 12 |
+
It attempts conversions in the following order:
|
| 13 |
+
1. Integer
|
| 14 |
+
2. Float
|
| 15 |
+
3. Boolean (case-insensitive 'true' or 'false')
|
| 16 |
+
If all conversions fail, it returns the original string.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
s: The input string to be converted.
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
The converted value (int, float, bool) or the original string.
|
| 23 |
+
"""
|
| 24 |
+
if not isinstance(s, str):
|
| 25 |
+
# If the input is not a string, return it as is.
|
| 26 |
+
return s
|
| 27 |
+
|
| 28 |
+
# 1. Try to convert to an integer
|
| 29 |
+
try:
|
| 30 |
+
return int(s)
|
| 31 |
+
except ValueError:
|
| 32 |
+
# Not an integer, continue...
|
| 33 |
+
pass
|
| 34 |
+
|
| 35 |
+
# 2. Try to convert to a float
|
| 36 |
+
try:
|
| 37 |
+
return float(s)
|
| 38 |
+
except ValueError:
|
| 39 |
+
# Not a float, continue...
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
# 3. Check for a boolean value
|
| 43 |
+
# This explicit check is important because bool('False') evaluates to True.
|
| 44 |
+
s_lower = s.lower()
|
| 45 |
+
if s_lower == 'true':
|
| 46 |
+
return True
|
| 47 |
+
if s_lower == 'false':
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
# 4. If nothing else worked, return the original string
|
| 51 |
+
return s
|
| 52 |
+
|
| 53 |
+
def build_path_to_metadata_key_map(cls: Type, prefix_list: List[str]) -> Dict[str, str]:
|
| 54 |
+
"""
|
| 55 |
+
Builds a map from a dataclass field path (e.g., 'image_settings.model') to the
|
| 56 |
+
expected key in the metadata dictionary (e.g., 'Image Settings - Model').
|
| 57 |
+
"""
|
| 58 |
+
path_map = {}
|
| 59 |
+
if not is_dataclass(cls):
|
| 60 |
+
return {}
|
| 61 |
+
|
| 62 |
+
for f in fields(cls):
|
| 63 |
+
current_path = f.name
|
| 64 |
+
|
| 65 |
+
if is_dataclass(f.type):
|
| 66 |
+
parent_label = f.metadata.get("label", f.name.replace("_", " ").title())
|
| 67 |
+
new_prefix_list = prefix_list + [parent_label]
|
| 68 |
+
nested_map = build_path_to_metadata_key_map(f.type, new_prefix_list)
|
| 69 |
+
for nested_path, metadata_key in nested_map.items():
|
| 70 |
+
path_map[f"{current_path}.{nested_path}"] = metadata_key
|
| 71 |
+
else:
|
| 72 |
+
label = f.metadata.get("label", f.name.replace("_", " ").title())
|
| 73 |
+
full_prefix = " - ".join(prefix_list)
|
| 74 |
+
metadata_key = f"{full_prefix} - {label}" if full_prefix else label
|
| 75 |
+
path_map[current_path] = metadata_key
|
| 76 |
+
|
| 77 |
+
return path_map
|
| 78 |
+
|
| 79 |
+
def extract_metadata(image_data: str | Path | Image.Image | np.ndarray | None, only_custom_metadata: bool = True) -> Dict[str, Any]:
|
| 80 |
+
"""
|
| 81 |
+
Extracts metadata from an image.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
image_data: Image data as a filepath, Path, PIL Image, NumPy array, or None.
|
| 85 |
+
only_custom_metadata: If True, excludes technical metadata (e.g., ImageWidth, ImageHeight). Defaults to True.
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
Dictionary of extracted metadata. Returns empty dictionary if no metadata is available or extraction fails.
|
| 89 |
+
"""
|
| 90 |
+
if not image_data:
|
| 91 |
+
return {}
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
# Convert image_data to PIL.Image
|
| 95 |
+
if isinstance(image_data, (str, Path)):
|
| 96 |
+
image = Image.open(image_data)
|
| 97 |
+
elif isinstance(image_data, np.ndarray):
|
| 98 |
+
image = Image.fromarray(image_data)
|
| 99 |
+
elif isinstance(image_data, Image.Image):
|
| 100 |
+
image = image_data
|
| 101 |
+
elif hasattr(image_data, 'path'): # For ImageMetaData
|
| 102 |
+
image = Image.open(image_data.path)
|
| 103 |
+
else:
|
| 104 |
+
return {}
|
| 105 |
+
|
| 106 |
+
decoded_meta = {}
|
| 107 |
+
if image.format == "PNG":
|
| 108 |
+
if not only_custom_metadata:
|
| 109 |
+
decoded_meta["ImageWidth"] = image.width
|
| 110 |
+
decoded_meta["ImageHeight"] = image.height
|
| 111 |
+
metadata = image.info
|
| 112 |
+
if metadata:
|
| 113 |
+
for key, value in metadata.items():
|
| 114 |
+
if isinstance(value, bytes):
|
| 115 |
+
value = value.decode(errors='ignore')
|
| 116 |
+
decoded_meta[str(key)] = value
|
| 117 |
+
else:
|
| 118 |
+
exif_data = image.getexif()
|
| 119 |
+
if exif_data:
|
| 120 |
+
for tag_id, value in exif_data.items():
|
| 121 |
+
tag = ExifTags.TAGS.get(tag_id, tag_id)
|
| 122 |
+
if isinstance(value, bytes):
|
| 123 |
+
value = value.decode(errors='ignore')
|
| 124 |
+
decoded_meta[str(tag)] = value
|
| 125 |
+
if not only_custom_metadata:
|
| 126 |
+
decoded_meta["ImageWidth"] = image.width
|
| 127 |
+
decoded_meta["ImageHeight"] = image.height
|
| 128 |
+
|
| 129 |
+
return decoded_meta
|
| 130 |
+
except Exception:
|
| 131 |
+
return {}
|
| 132 |
+
|
| 133 |
+
def add_metadata(image_data: str | Path | Image.Image | np.ndarray, save_path: str, metadata: Dict[str, Any]) -> bool:
|
| 134 |
+
"""
|
| 135 |
+
Adds metadata to an image and saves it to the specified path.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
image_data: Image data as a filepath, Path, PIL Image, or NumPy array.
|
| 139 |
+
save_path: Filepath where the modified image will be saved.
|
| 140 |
+
metadata: Dictionary of metadata to add to the image.
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
True if metadata was added and image was saved successfully, False otherwise.
|
| 144 |
+
"""
|
| 145 |
+
try:
|
| 146 |
+
if not bool(save_path):
|
| 147 |
+
return False
|
| 148 |
+
|
| 149 |
+
# Convert image_data to PIL.Image
|
| 150 |
+
if isinstance(image_data, (str, Path)):
|
| 151 |
+
image = Image.open(image_data)
|
| 152 |
+
elif isinstance(image_data, np.ndarray):
|
| 153 |
+
image = Image.fromarray(image_data)
|
| 154 |
+
elif isinstance(image_data, Image.Image):
|
| 155 |
+
image = image_data
|
| 156 |
+
elif hasattr(image_data, 'path'): # For ImageMetaData
|
| 157 |
+
image = Image.open(image_data.path)
|
| 158 |
+
else:
|
| 159 |
+
return False
|
| 160 |
+
|
| 161 |
+
_, ext = os.path.splitext(save_path)
|
| 162 |
+
image_copy = image.copy()
|
| 163 |
+
|
| 164 |
+
if (image.format if image.format is not None else ext.replace('.','').upper()) == "PNG":
|
| 165 |
+
meta = None
|
| 166 |
+
if metadata:
|
| 167 |
+
meta = PngImagePlugin.PngInfo()
|
| 168 |
+
for key, value in metadata.items():
|
| 169 |
+
meta.add_text(str(key), str(value))
|
| 170 |
+
image_copy.info.update(metadata) # For reference, but requires pnginfo when saving
|
| 171 |
+
image_copy.save(save_path, pnginfo=meta)
|
| 172 |
+
else:
|
| 173 |
+
if metadata:
|
| 174 |
+
exif = image_copy.getexif() or Image.Exif()
|
| 175 |
+
for key, value in metadata.items():
|
| 176 |
+
tag_id = next((k for k, v in ExifTags.TAGS.items() if v == key), None)
|
| 177 |
+
if tag_id:
|
| 178 |
+
exif[tag_id] = value
|
| 179 |
+
image_copy.exif = exif
|
| 180 |
+
image_copy.save(save_path)
|
| 181 |
+
return True
|
| 182 |
+
except Exception:
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
def transfer_metadata(
|
| 186 |
+
output_fields: List[Any],
|
| 187 |
+
metadata: Dict[str, Any],
|
| 188 |
+
propertysheet_map: Optional[Dict[int, Dict[str, Any]]] = None,
|
| 189 |
+
gradio_component_map: Optional[Dict[int, str]] = None,
|
| 190 |
+
remove_prefix_from_keys: bool = False
|
| 191 |
+
) -> List[Any]:
|
| 192 |
+
"""
|
| 193 |
+
Maps a flat metadata dictionary to a list of Gradio UI components, with
|
| 194 |
+
flexible and powerful matching logic for all component types.
|
| 195 |
+
|
| 196 |
+
This function is UI-agnostic. For PropertySheets, it uses the `propertysheet_map`
|
| 197 |
+
to reconstruct nested dataclass instances. It intelligently builds the expected
|
| 198 |
+
metadata keys by interpreting the `prefixes` list: strings found as keys in the
|
| 199 |
+
`metadata` are replaced by their values, while other strings are used literally.
|
| 200 |
+
|
| 201 |
+
For standard Gradio components, it uses a three-tiered priority system:
|
| 202 |
+
1. **Explicit Mapping (Highest Priority):** If `gradio_component_map` is provided,
|
| 203 |
+
it uses the specified metadata key for a given component ID.
|
| 204 |
+
2. **Base Label Matching:** If `remove_prefix_from_keys=True` and no explicit map
|
| 205 |
+
is found, it matches the component's `.label` against "base" (prefix-stripped)
|
| 206 |
+
metadata keys.
|
| 207 |
+
3. **Exact Label Matching (Default):** Otherwise, it attempts an exact match
|
| 208 |
+
between the component's `.label` and a metadata key.
|
| 209 |
+
|
| 210 |
+
Args:
|
| 211 |
+
output_fields (List[Any]): The list of Gradio components to be updated.
|
| 212 |
+
metadata (Dict[str, Any]): The flat dictionary of metadata from an image.
|
| 213 |
+
propertysheet_map (Optional[Dict[int, Dict[str, Any]]]):
|
| 214 |
+
A map from a PropertySheet's `id()` to its configuration dict, which
|
| 215 |
+
should contain its `type` and a list of `prefixes`.
|
| 216 |
+
gradio_component_map (Optional[Dict[int, str]]):
|
| 217 |
+
An optional map from a standard component's `id()` to the exact
|
| 218 |
+
metadata key that should populate it.
|
| 219 |
+
remove_prefix_from_keys (bool): If True, enables fallback matching
|
| 220 |
+
by stripping prefixes from metadata keys for standard components.
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
List[Any]: A list of `gr.update` or `gr.skip()` objects.
|
| 224 |
+
"""
|
| 225 |
+
if propertysheet_map is None: propertysheet_map = {}
|
| 226 |
+
if gradio_component_map is None: gradio_component_map = {}
|
| 227 |
+
|
| 228 |
+
output_values = [None] * len(output_fields)
|
| 229 |
+
component_to_index = {id(comp): i for i, comp in enumerate(output_fields)}
|
| 230 |
+
|
| 231 |
+
# Pre-process metadata to create a map of base labels if needed.
|
| 232 |
+
base_label_map = {}
|
| 233 |
+
if remove_prefix_from_keys:
|
| 234 |
+
base_label_map = {key.rsplit(' - ', 1)[-1]: value for key, value in metadata.items()}
|
| 235 |
+
|
| 236 |
+
for component in output_fields:
|
| 237 |
+
comp_id = id(component)
|
| 238 |
+
output_index = component_to_index.get(comp_id)
|
| 239 |
+
if output_index is None:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
# --- Logic for PropertySheets ---
|
| 243 |
+
if comp_id in propertysheet_map:
|
| 244 |
+
sheet_info = propertysheet_map[comp_id]
|
| 245 |
+
dc_type = sheet_info.get("type")
|
| 246 |
+
prefix_definitions = sheet_info.get("prefixes", [])
|
| 247 |
+
|
| 248 |
+
# Build the list of actual prefix strings by interpreting the definitions.
|
| 249 |
+
prefix_values = []
|
| 250 |
+
for p_def in prefix_definitions:
|
| 251 |
+
# If the prefix definition is a key in the metadata, use its value.
|
| 252 |
+
if p_def in metadata:
|
| 253 |
+
prefix_values.append(str(metadata[p_def]))
|
| 254 |
+
# Otherwise, treat it as a literal string.
|
| 255 |
+
else:
|
| 256 |
+
prefix_values.append(p_def)
|
| 257 |
+
|
| 258 |
+
prefix_values = [p for p in prefix_values if p]
|
| 259 |
+
|
| 260 |
+
if not dc_type or not is_dataclass(dc_type):
|
| 261 |
+
continue
|
| 262 |
+
|
| 263 |
+
# Build the map from the dataclass structure to the expected full metadata keys.
|
| 264 |
+
path_to_key_map = build_path_to_metadata_key_map(dc_type, prefix_values)
|
| 265 |
+
|
| 266 |
+
# Get the base instance to start populating.
|
| 267 |
+
instance_to_populate = getattr(component, '_dataclass_value', dc_type())
|
| 268 |
+
if not is_dataclass(instance_to_populate):
|
| 269 |
+
instance_to_populate = dc_type()
|
| 270 |
+
|
| 271 |
+
# Populate the instance by iterating through the path map.
|
| 272 |
+
for path, metadata_key in path_to_key_map.items():
|
| 273 |
+
if metadata_key in metadata:
|
| 274 |
+
value_from_meta = metadata[metadata_key]
|
| 275 |
+
|
| 276 |
+
parts = path.split('.')
|
| 277 |
+
obj_to_set = instance_to_populate
|
| 278 |
+
try:
|
| 279 |
+
for part in parts[:-1]:
|
| 280 |
+
obj_to_set = getattr(obj_to_set, part)
|
| 281 |
+
|
| 282 |
+
final_field_name = parts[-1]
|
| 283 |
+
converted_value = infer_type(value_from_meta)
|
| 284 |
+
setattr(obj_to_set, final_field_name, converted_value)
|
| 285 |
+
except (AttributeError, KeyError, ValueError, TypeError) as e:
|
| 286 |
+
print(f"Warning (transfer_metadata): Could not set value for path '{path}'. Error: {e}")
|
| 287 |
+
|
| 288 |
+
output_values[output_index] = instance_to_populate
|
| 289 |
+
|
| 290 |
+
# --- Unified Logic for Standard Gradio Components ---
|
| 291 |
+
else:
|
| 292 |
+
value_to_set = None
|
| 293 |
+
|
| 294 |
+
# Priority 1: Check for an explicit mapping via gradio_component_map.
|
| 295 |
+
if comp_id in gradio_component_map:
|
| 296 |
+
metadata_key = gradio_component_map[comp_id]
|
| 297 |
+
if metadata_key in metadata:
|
| 298 |
+
value_to_set = metadata[metadata_key]
|
| 299 |
+
|
| 300 |
+
# If no explicit mapping was found, proceed to label-based matching.
|
| 301 |
+
if value_to_set is None:
|
| 302 |
+
label = getattr(component, 'label', None)
|
| 303 |
+
if label:
|
| 304 |
+
# Priority 2: Check for a base label match if the flag is set.
|
| 305 |
+
if remove_prefix_from_keys and label in base_label_map:
|
| 306 |
+
value_to_set = base_label_map[label]
|
| 307 |
+
# Priority 3: Fallback to an exact label match.
|
| 308 |
+
elif label in metadata:
|
| 309 |
+
value_to_set = metadata[label]
|
| 310 |
+
|
| 311 |
+
# If a value was found by any method, create the update object.
|
| 312 |
+
if value_to_set is not None:
|
| 313 |
+
output_values[output_index] = infer_type(value_to_set)
|
| 314 |
+
|
| 315 |
+
return output_values
|
src/backend/gradio_mediagallery/mediagallery.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""gr.Gallery() component."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
from collections.abc import Sequence
|
| 5 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import (
|
| 8 |
+
TYPE_CHECKING,
|
| 9 |
+
Any,
|
| 10 |
+
Callable,
|
| 11 |
+
Literal,
|
| 12 |
+
Union,
|
| 13 |
+
)
|
| 14 |
+
from urllib.parse import urlparse
|
| 15 |
+
import PIL.Image
|
| 16 |
+
import numpy as np
|
| 17 |
+
from gradio_client import handle_file, utils as client_utils
|
| 18 |
+
from gradio_client.utils import is_http_url_like
|
| 19 |
+
from gradio import processing_utils, utils
|
| 20 |
+
from gradio.components.base import Component
|
| 21 |
+
from gradio.data_classes import FileData, GradioModel, GradioRootModel, ImageData
|
| 22 |
+
from gradio.events import EventListener, Events # Remova EventListener se não for usado
|
| 23 |
+
# Remova todas as importações e classes relacionadas à árvore (TreeNode, JsonData, etc.)
|
| 24 |
+
|
| 25 |
+
from gradio.i18n import I18nData
|
| 26 |
+
|
| 27 |
+
if TYPE_CHECKING:
|
| 28 |
+
from gradio.components import Timer
|
| 29 |
+
# --- DEFINIÇÕES DE MODELO SIMPLIFICADAS ---
|
| 30 |
+
|
| 31 |
+
class GalleryImage(GradioModel):
|
| 32 |
+
image: ImageData
|
| 33 |
+
caption: str | None = None
|
| 34 |
+
|
| 35 |
+
class GalleryVideo(GradioModel):
|
| 36 |
+
video: FileData
|
| 37 |
+
caption: str | None = None
|
| 38 |
+
|
| 39 |
+
class GalleryData(GradioRootModel): # Voltamos a usar GradioRootModel
|
| 40 |
+
root: list[Union[GalleryImage, GalleryVideo]]
|
| 41 |
+
|
| 42 |
+
class MediaGallery(Component):
|
| 43 |
+
"""
|
| 44 |
+
Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.
|
| 45 |
+
If used as an output, the user can click on individual images or videos to view them at a higher resolution.
|
| 46 |
+
|
| 47 |
+
Demos: fake_gan, gif_maker
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
EVENTS = [
|
| 51 |
+
Events.select,
|
| 52 |
+
Events.change,
|
| 53 |
+
Events.delete,
|
| 54 |
+
EventListener(
|
| 55 |
+
"preview_close",
|
| 56 |
+
doc="This event is triggered when the MediaGallery preview is closed by the user",
|
| 57 |
+
),
|
| 58 |
+
EventListener(
|
| 59 |
+
"preview_open",
|
| 60 |
+
doc="This event is triggered when the MediaGallery preview is opened by the user",
|
| 61 |
+
),
|
| 62 |
+
EventListener(
|
| 63 |
+
"load_metadata",
|
| 64 |
+
doc="Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata.",
|
| 65 |
+
),
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
data_model = GalleryData
|
| 69 |
+
|
| 70 |
+
def __init__(
|
| 71 |
+
self,
|
| 72 |
+
value: (
|
| 73 |
+
Sequence[np.ndarray | PIL.Image.Image | str | Path | tuple]
|
| 74 |
+
| Callable
|
| 75 |
+
| None
|
| 76 |
+
) = None,
|
| 77 |
+
*,
|
| 78 |
+
file_types: list[str] | None = None,
|
| 79 |
+
label: str | I18nData | None = None,
|
| 80 |
+
every: Timer | float | None = None,
|
| 81 |
+
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
| 82 |
+
show_label: bool | None = None,
|
| 83 |
+
container: bool = True,
|
| 84 |
+
scale: int | None = None,
|
| 85 |
+
min_width: int = 160,
|
| 86 |
+
visible: bool | Literal["hidden"] = True,
|
| 87 |
+
elem_id: str | None = None,
|
| 88 |
+
elem_classes: list[str] | str | None = None,
|
| 89 |
+
render: bool = True,
|
| 90 |
+
key: int | str | tuple[int | str, ...] | None = None,
|
| 91 |
+
preserved_by_key: list[str] | str | None = "value",
|
| 92 |
+
columns: int | None = 2,
|
| 93 |
+
rows: int | None = None,
|
| 94 |
+
height: int | float | str | None = None,
|
| 95 |
+
allow_preview: bool = True,
|
| 96 |
+
preview: bool | None = None,
|
| 97 |
+
selected_index: int | None = None,
|
| 98 |
+
object_fit: (
|
| 99 |
+
Literal["contain", "cover", "fill", "none", "scale-down"] | None
|
| 100 |
+
) = None,
|
| 101 |
+
show_share_button: bool | None = None,
|
| 102 |
+
show_download_button: bool | None = True,
|
| 103 |
+
interactive: bool | None = None,
|
| 104 |
+
type: Literal["numpy", "pil", "filepath"] = "filepath",
|
| 105 |
+
show_fullscreen_button: bool = True,
|
| 106 |
+
only_custom_metadata: bool = True,
|
| 107 |
+
popup_metadata_width: int | str = 500
|
| 108 |
+
):
|
| 109 |
+
"""
|
| 110 |
+
Parameters:
|
| 111 |
+
value: List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.
|
| 112 |
+
format: Format to save images before they are returned to the frontend, such as 'jpeg' or 'png'. This parameter only applies to images that are returned from the prediction function as numpy arrays or PIL Images. The format should be supported by the PIL library.
|
| 113 |
+
file_types: List of file extensions or types of files to be uploaded (e.g. ['image', '.mp4']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.
|
| 114 |
+
label: the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
|
| 115 |
+
every: Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.
|
| 116 |
+
inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.
|
| 117 |
+
show_label: if True, will display label.
|
| 118 |
+
container: If True, will place the component in a container - providing some extra padding around the border.
|
| 119 |
+
scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.
|
| 120 |
+
min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
|
| 121 |
+
visible: If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM
|
| 122 |
+
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.
|
| 123 |
+
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
|
| 124 |
+
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
|
| 125 |
+
key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.
|
| 126 |
+
preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.
|
| 127 |
+
columns: Represents the number of images that should be shown in one row.
|
| 128 |
+
rows: Represents the number of rows in the image grid.
|
| 129 |
+
height: The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.
|
| 130 |
+
allow_preview: If True, images in the gallery will be enlarged when they are clicked. Default is True.
|
| 131 |
+
preview: If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.
|
| 132 |
+
selected_index: The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.
|
| 133 |
+
object_fit: CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".
|
| 134 |
+
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
|
| 135 |
+
show_download_button: If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.
|
| 136 |
+
interactive: If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.
|
| 137 |
+
type: The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.
|
| 138 |
+
show_fullscreen_button: If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
|
| 139 |
+
only_custom_metadata: If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.
|
| 140 |
+
popup_metadata_width: The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").
|
| 141 |
+
"""
|
| 142 |
+
self.columns = columns
|
| 143 |
+
self.rows = rows
|
| 144 |
+
self.height = height
|
| 145 |
+
self.preview = preview
|
| 146 |
+
self.object_fit = object_fit
|
| 147 |
+
self.allow_preview = allow_preview
|
| 148 |
+
self.show_download_button = (
|
| 149 |
+
(utils.get_space() is not None)
|
| 150 |
+
if show_download_button is None
|
| 151 |
+
else show_download_button
|
| 152 |
+
)
|
| 153 |
+
self.selected_index = selected_index
|
| 154 |
+
if type not in ["numpy", "pil", "filepath"]:
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f"Invalid type: {type}. Must be one of ['numpy', 'pil', 'filepath']"
|
| 157 |
+
)
|
| 158 |
+
self.type = type
|
| 159 |
+
self.show_fullscreen_button = show_fullscreen_button
|
| 160 |
+
self.file_types = file_types
|
| 161 |
+
|
| 162 |
+
self.only_custom_metadata = only_custom_metadata
|
| 163 |
+
self.popup_metadata_width = popup_metadata_width
|
| 164 |
+
|
| 165 |
+
self.show_share_button = (
|
| 166 |
+
(utils.get_space() is not None)
|
| 167 |
+
if show_share_button is None
|
| 168 |
+
else show_share_button
|
| 169 |
+
)
|
| 170 |
+
super().__init__(
|
| 171 |
+
label=label,
|
| 172 |
+
every=every,
|
| 173 |
+
inputs=inputs,
|
| 174 |
+
show_label=show_label,
|
| 175 |
+
container=container,
|
| 176 |
+
scale=scale,
|
| 177 |
+
min_width=min_width,
|
| 178 |
+
visible=visible,
|
| 179 |
+
elem_id=elem_id,
|
| 180 |
+
elem_classes=elem_classes,
|
| 181 |
+
render=render,
|
| 182 |
+
key=key,
|
| 183 |
+
preserved_by_key=preserved_by_key,
|
| 184 |
+
value=value,
|
| 185 |
+
interactive=interactive,
|
| 186 |
+
)
|
| 187 |
+
self._value_description = f"a list of {'string filepaths' if type == 'filepath' else 'numpy arrays' if type == 'numpy' else 'PIL images'}"
|
| 188 |
+
|
| 189 |
+
def preprocess(self, payload: GalleryData | None) -> Any:
|
| 190 |
+
if payload is None:
|
| 191 |
+
return None
|
| 192 |
+
return [
|
| 193 |
+
(item.video.path if isinstance(item, GalleryVideo) else item.image.path, item.caption)
|
| 194 |
+
for item in payload.root
|
| 195 |
+
] # Extrai os dados do wrapper
|
| 196 |
+
|
| 197 |
+
def postprocess(
|
| 198 |
+
self,
|
| 199 |
+
value: list | None,
|
| 200 |
+
) -> GalleryData: # Assinatura está correta: retorna um dicionário
|
| 201 |
+
|
| 202 |
+
if value is None:
|
| 203 |
+
return GalleryData(root=[])
|
| 204 |
+
|
| 205 |
+
if isinstance(value, str):
|
| 206 |
+
raise ValueError(
|
| 207 |
+
"The `value` passed into `gr.Gallery` must be a list of images or videos, or list of (media, caption) tuples."
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
output_items = []
|
| 211 |
+
|
| 212 |
+
def _save_item(item):
|
| 213 |
+
img, caption = (item, None) if not isinstance(item, (tuple, list)) else item
|
| 214 |
+
|
| 215 |
+
# Lógica para extrair nome original (importante para nossa UI)
|
| 216 |
+
orig_name = None
|
| 217 |
+
if hasattr(img, 'filename') and img.filename:
|
| 218 |
+
orig_name = Path(img.filename).name
|
| 219 |
+
|
| 220 |
+
# Lógica de salvar no cache e preparar dados
|
| 221 |
+
if isinstance(img, np.ndarray):
|
| 222 |
+
file = processing_utils.save_img_array_to_cache(img, cache_dir=self.GRADIO_CACHE, format="png")
|
| 223 |
+
file_path = str(utils.abspath(file))
|
| 224 |
+
url = None
|
| 225 |
+
elif isinstance(img, PIL.Image.Image):
|
| 226 |
+
file = processing_utils.save_pil_to_cache(img, cache_dir=self.GRADIO_CACHE, format=str(img.format).lower())
|
| 227 |
+
file_path = str(utils.abspath(file))
|
| 228 |
+
url = None
|
| 229 |
+
elif isinstance(img, (str, Path)):
|
| 230 |
+
file_path = str(img)
|
| 231 |
+
if not orig_name:
|
| 232 |
+
orig_name = Path(file_path).name
|
| 233 |
+
url = file_path if is_http_url_like(file_path) else None
|
| 234 |
+
else:
|
| 235 |
+
raise ValueError(f"Cannot process type: {type(img)}")
|
| 236 |
+
|
| 237 |
+
mime_type = client_utils.get_mimetype(file_path)
|
| 238 |
+
|
| 239 |
+
if mime_type and "video" in mime_type:
|
| 240 |
+
file = processing_utils.save_file_to_cache(img, cache_dir=self.GRADIO_CACHE)
|
| 241 |
+
file_path = str(utils.abspath(file))
|
| 242 |
+
return GalleryVideo(
|
| 243 |
+
video=FileData(path=file_path, url=url, orig_name=orig_name, mime_type=mime_type),
|
| 244 |
+
caption=caption
|
| 245 |
+
)
|
| 246 |
+
else:
|
| 247 |
+
return GalleryImage(
|
| 248 |
+
image=ImageData(path=file_path, url=url, orig_name=orig_name, mime_type=mime_type),
|
| 249 |
+
caption=caption
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
with ThreadPoolExecutor() as executor:
|
| 253 |
+
for item in executor.map(_save_item, value):
|
| 254 |
+
if item:
|
| 255 |
+
output_items.append(item)
|
| 256 |
+
|
| 257 |
+
return GalleryData(root=output_items)
|
| 258 |
+
|
| 259 |
+
@staticmethod
|
| 260 |
+
def convert_to_type(img: str, type: Literal["filepath", "numpy", "pil"]):
|
| 261 |
+
if type == "filepath":
|
| 262 |
+
return img
|
| 263 |
+
else:
|
| 264 |
+
converted_image = PIL.Image.open(img)
|
| 265 |
+
if type == "numpy":
|
| 266 |
+
converted_image = np.array(converted_image)
|
| 267 |
+
return converted_image
|
| 268 |
+
|
| 269 |
+
def example_payload(self) -> Any:
|
| 270 |
+
return [
|
| 271 |
+
{
|
| 272 |
+
"image": handle_file(
|
| 273 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 274 |
+
)
|
| 275 |
+
},
|
| 276 |
+
]
|
| 277 |
+
|
| 278 |
+
def example_value(self) -> Any:
|
| 279 |
+
return [
|
| 280 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 281 |
+
]
|
src/backend/gradio_mediagallery/templates/component/assets/worker-BAOIWoxA.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
(function(){"use strict";const i="https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js";var E;(function(t){t.LOAD="LOAD",t.EXEC="EXEC",t.FFPROBE="FFPROBE",t.WRITE_FILE="WRITE_FILE",t.READ_FILE="READ_FILE",t.DELETE_FILE="DELETE_FILE",t.RENAME="RENAME",t.CREATE_DIR="CREATE_DIR",t.LIST_DIR="LIST_DIR",t.DELETE_DIR="DELETE_DIR",t.ERROR="ERROR",t.DOWNLOAD="DOWNLOAD",t.PROGRESS="PROGRESS",t.LOG="LOG",t.MOUNT="MOUNT",t.UNMOUNT="UNMOUNT"})(E||(E={}));const f=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),u=new Error("failed to import ffmpeg-core.js");let r;const O=async({coreURL:t,wasmURL:n,workerURL:e})=>{const o=!r;try{t||(t=i),importScripts(t)}catch{if((!t||t===i)&&(t=i.replace("/umd/","/esm/")),self.createFFmpegCore=(await import(t)).default,!self.createFFmpegCore)throw u}const s=t,c=n||t.replace(/.js$/g,".wasm"),p=e||t.replace(/.js$/g,".worker.js");return r=await self.createFFmpegCore({mainScriptUrlOrBlob:`${s}#${btoa(JSON.stringify({wasmURL:c,workerURL:p}))}`}),r.setLogger(R=>self.postMessage({type:E.LOG,data:R})),r.setProgress(R=>self.postMessage({type:E.PROGRESS,data:R})),o},m=({args:t,timeout:n=-1})=>{r.setTimeout(n),r.exec(...t);const e=r.ret;return r.reset(),e},l=({args:t,timeout:n=-1})=>{r.setTimeout(n),r.ffprobe(...t);const e=r.ret;return r.reset(),e},D=({path:t,data:n})=>(r.FS.writeFile(t,n),!0),S=({path:t,encoding:n})=>r.FS.readFile(t,{encoding:n}),I=({path:t})=>(r.FS.unlink(t),!0),L=({oldPath:t,newPath:n})=>(r.FS.rename(t,n),!0),N=({path:t})=>(r.FS.mkdir(t),!0),A=({path:t})=>{const n=r.FS.readdir(t),e=[];for(const o of n){const s=r.FS.stat(`${t}/${o}`),c=r.FS.isDir(s.mode);e.push({name:o,isDir:c})}return e},k=({path:t})=>(r.FS.rmdir(t),!0),w=({fsType:t,options:n,mountPoint:e})=>{const o=t,s=r.FS.filesystems[o];return s?(r.FS.mount(s,n,e),!0):!1},b=({mountPoint:t})=>(r.FS.unmount(t),!0);self.onmessage=async({data:{id:t,type:n,data:e}})=>{const o=[];let s;try{if(n!==E.LOAD&&!r)throw a;switch(n){case E.LOAD:s=await O(e);break;case E.EXEC:s=m(e);break;case E.FFPROBE:s=l(e);break;case E.WRITE_FILE:s=D(e);break;case E.READ_FILE:s=S(e);break;case E.DELETE_FILE:s=I(e);break;case E.RENAME:s=L(e);break;case E.CREATE_DIR:s=N(e);break;case E.LIST_DIR:s=A(e);break;case E.DELETE_DIR:s=k(e);break;case E.MOUNT:s=w(e);break;case E.UNMOUNT:s=b(e);break;default:throw f}}catch(c){self.postMessage({id:t,type:E.ERROR,data:c.toString()});return}s instanceof Uint8Array&&o.push(s.buffer),self.postMessage({id:t,type:n,data:s},o)}})();
|
src/backend/gradio_mediagallery/templates/component/index.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/backend/gradio_mediagallery/templates/component/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.block.svelte-239wnu{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.fullscreen.svelte-239wnu{border-radius:0}.auto-margin.svelte-239wnu{margin-left:auto;margin-right:auto}.block.border_focus.svelte-239wnu{border-color:var(--color-accent)}.block.border_contrast.svelte-239wnu{border-color:var(--body-text-color)}.padded.svelte-239wnu{padding:var(--block-padding)}.hidden.svelte-239wnu{display:none}.flex.svelte-239wnu{display:flex;flex-direction:column}.hide-container.svelte-239wnu:not(.fullscreen){margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}.resize-handle.svelte-239wnu{position:absolute;bottom:0;right:0;width:10px;height:10px;fill:var(--block-border-color);cursor:nwse-resize}.fullscreen.svelte-239wnu{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;overflow:auto}.animating.svelte-239wnu{animation:svelte-239wnu-pop-out .1s ease-out forwards}@keyframes svelte-239wnu-pop-out{0%{position:fixed;top:var(--start-top);left:var(--start-left);width:var(--start-width);height:var(--start-height);z-index:100}to{position:fixed;top:0vh;left:0vw;width:100vw;height:100vh;z-index:1000}}.placeholder.svelte-239wnu{border-radius:var(--block-radius);border-width:var(--block-border-width);border-color:var(--block-border-color);border-style:dashed}Tables */ table,tr,td,th{margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);padding:var(--spacing-xl)}.md code,.md pre{background:none;font-family:var(--font-mono);font-size:var(--text-sm);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:2;tab-size:2;-webkit-hyphens:none;hyphens:none}.md pre[class*=language-]::selection,.md pre[class*=language-] ::selection,.md code[class*=language-]::selection,.md code[class*=language-] ::selection{text-shadow:none;background:#b3d4fc}.md pre{padding:1em;margin:.5em 0;overflow:auto;position:relative;margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);box-shadow:none;border:none;border-radius:var(--radius-md);background:var(--code-background-fill);padding:var(--spacing-xxl);font-family:var(--font-mono);text-shadow:none;border-radius:var(--radius-sm);white-space:nowrap;display:block;white-space:pre}.md :not(pre)>code{padding:.1em;border-radius:var(--radius-xs);white-space:normal;background:var(--code-background-fill);border:1px solid var(--panel-border-color);padding:var(--spacing-xxs) var(--spacing-xs)}.md .token.comment,.md .token.prolog,.md .token.doctype,.md .token.cdata{color:#708090}.md .token.punctuation{color:#999}.md .token.namespace{opacity:.7}.md .token.property,.md .token.tag,.md .token.boolean,.md .token.number,.md .token.constant,.md .token.symbol,.md .token.deleted{color:#905}.md .token.selector,.md .token.attr-name,.md .token.string,.md .token.char,.md .token.builtin,.md .token.inserted{color:#690}.md .token.atrule,.md .token.attr-value,.md .token.keyword{color:#07a}.md .token.function,.md .token.class-name{color:#dd4a68}.md .token.regex,.md .token.important,.md .token.variable{color:#e90}.md .token.important,.md .token.bold{font-weight:700}.md .token.italic{font-style:italic}.md .token.entity{cursor:help}.dark .md .token.comment,.dark .md .token.prolog,.dark .md .token.cdata{color:#5c6370}.dark .md .token.doctype,.dark .md .token.punctuation,.dark .md .token.entity{color:#abb2bf}.dark .md .token.attr-name,.dark .md .token.class-name,.dark .md .token.boolean,.dark .md .token.constant,.dark .md .token.number,.dark .md .token.atrule{color:#d19a66}.dark .md .token.keyword{color:#c678dd}.dark .md .token.property,.dark .md .token.tag,.dark .md .token.symbol,.dark .md .token.deleted,.dark .md .token.important{color:#e06c75}.dark .md .token.selector,.dark .md .token.string,.dark .md .token.char,.dark .md .token.builtin,.dark .md .token.inserted,.dark .md .token.regex,.dark .md .token.attr-value,.dark .md .token.attr-value>.token.punctuation{color:#98c379}.dark .md .token.variable,.dark .md .token.operator,.dark .md .token.function{color:#61afef}.dark .md .token.url{color:#56b6c2}span.svelte-1m32c2s div[class*=code_wrap]{position:relative}span.svelte-1m32c2s span.katex{font-size:var(--text-lg);direction:ltr}span.svelte-1m32c2s div[class*=code_wrap]>button{z-index:1;cursor:pointer;border-bottom-left-radius:var(--radius-sm);padding:var(--spacing-md);width:25px;height:25px;position:absolute;right:0}span.svelte-1m32c2s .check{opacity:0;z-index:var(--layer-top);transition:opacity .2s;background:var(--code-background-fill);color:var(--body-text-color);position:absolute;top:var(--size-1-5);left:var(--size-1-5)}span.svelte-1m32c2s p:not(:first-child){margin-top:var(--spacing-xxl)}span.svelte-1m32c2s .md-header-anchor{margin-left:-25px;padding-right:8px;line-height:1;color:var(--body-text-color-subdued);opacity:0}span.svelte-1m32c2s h1:hover .md-header-anchor,span.svelte-1m32c2s h2:hover .md-header-anchor,span.svelte-1m32c2s h3:hover .md-header-anchor,span.svelte-1m32c2s h4:hover .md-header-anchor,span.svelte-1m32c2s h5:hover .md-header-anchor,span.svelte-1m32c2s h6:hover .md-header-anchor{opacity:1}span.md.svelte-1m32c2s .md-header-anchor>svg{color:var(--body-text-color-subdued)}span.svelte-1m32c2s table{word-break:break-word}div.svelte-17qq50w>.md.prose{font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}div.svelte-17qq50w>.md.prose *{color:var(--block-info-text-color)}div.svelte-17qq50w{margin-bottom:var(--spacing-md)}span.has-info.svelte-zgrq3{margin-bottom:var(--spacing-xs)}span.svelte-zgrq3:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-zgrq3{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}span[dir=rtl].svelte-zgrq3{display:block}.hide.svelte-zgrq3{margin:0;height:0}label.svelte-igqdol.svelte-igqdol{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-igqdol.svelte-igqdol{border-top-left-radius:0}label.float.svelte-igqdol.svelte-igqdol{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-igqdol.svelte-igqdol:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-igqdol.svelte-igqdol{display:none}span.svelte-igqdol.svelte-igqdol{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-igqdol.svelte-igqdol{box-shadow:none;border-width:0;background:transparent;overflow:visible}label[dir=rtl].svelte-igqdol.svelte-igqdol{border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-right:none;border-bottom-left-radius:var(--block-radius);border-bottom-right-radius:var(--block-label-radius);border-top-left-radius:var(--block-label-radius)}label[dir=rtl].svelte-igqdol span.svelte-igqdol{margin-left:var(--size-2);margin-right:0}.unstyled-link.svelte-151nsdd{all:unset;cursor:pointer}button.svelte-y0enk4{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-xs);color:var(--block-label-text-color);border:1px solid var(--border-color);padding:var(--spacing-xxs)}button.svelte-y0enk4:hover{background-color:var(--background-fill-secondary)}button[disabled].svelte-y0enk4{opacity:.5;box-shadow:none}button[disabled].svelte-y0enk4:hover{cursor:not-allowed}.padded.svelte-y0enk4{background:var(--bg-color)}button.svelte-y0enk4:hover,button.highlight.svelte-y0enk4{cursor:pointer;color:var(--color-accent)}.padded.svelte-y0enk4:hover{color:var(--block-label-text-color)}span.svelte-y0enk4{padding:0 1px;font-size:10px}div.svelte-y0enk4{display:flex;align-items:center;justify-content:center;transition:filter .2s ease-in-out}.x-small.svelte-y0enk4{width:10px;height:10px}.small.svelte-y0enk4{width:14px;height:14px}.medium.svelte-y0enk4{width:20px;height:20px}.large.svelte-y0enk4{width:22px;height:22px}.pending.svelte-y0enk4{animation:svelte-y0enk4-flash .5s infinite}@keyframes svelte-y0enk4-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-y0enk4{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6,.dropdown-arrow.svelte-ihhdbf{fill:currentColor}.circle.svelte-ihhdbf{fill:currentColor;opacity:.1}svg.svelte-pb9pol{animation:svelte-pb9pol-spin 1.5s linear infinite}@keyframes svelte-pb9pol-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}h2.svelte-1xg7h5n{font-size:var(--text-xl)!important}p.svelte-1xg7h5n,h2.svelte-1xg7h5n{white-space:pre-line}.wrap.svelte-1xg7h5n{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3);text-align:center;margin:auto var(--spacing-lg)}.or.svelte-1xg7h5n{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-1xg7h5n{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-1xg7h5n{font-size:var(--text-lg)}}.hovered.svelte-1xg7h5n{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-15ls1gu{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:100%;margin-left:auto;margin-right:auto;height:var(--size-10)}.icon.svelte-15ls1gu{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-15ls1gu{color:var(--color-accent)}.icon.svelte-15ls1gu:hover,.icon.svelte-15ls1gu:focus{color:var(--color-accent)}.icon-button-wrapper.svelte-1h0hs6p{display:flex;flex-direction:row;align-items:center;justify-content:center;z-index:var(--layer-2);gap:var(--spacing-sm);box-shadow:var(--shadow-drop);border:1px solid var(--border-color-primary);background:var(--block-background-fill);padding:var(--spacing-xxs)}.icon-button-wrapper.hide-top-corner.svelte-1h0hs6p{border-top:none;border-right:none;border-radius:var(--block-label-right-radius)}.icon-button-wrapper.display-top-corner.svelte-1h0hs6p{border-radius:var(--radius-sm) 0 0 var(--radius-sm);top:var(--spacing-sm);right:-1px}.icon-button-wrapper.svelte-1h0hs6p:not(.top-panel){border:1px solid var(--border-color-primary);border-radius:var(--radius-sm)}.top-panel.svelte-1h0hs6p{position:absolute;top:var(--block-label-margin);right:var(--block-label-margin);margin:0}.icon-button-wrapper.svelte-1h0hs6p button{margin:var(--spacing-xxs);border-radius:var(--radius-xs);position:relative}.icon-button-wrapper.svelte-1h0hs6p a.download-link:not(:last-child),.icon-button-wrapper.svelte-1h0hs6p button:not(:last-child){margin-right:var(--spacing-xxs)}.icon-button-wrapper.svelte-1h0hs6p a.download-link:not(:last-child):not(.no-border *):after,.icon-button-wrapper.svelte-1h0hs6p button:not(:last-child):not(.no-border *):after{content:"";position:absolute;right:-4.5px;top:15%;height:70%;width:1px;background-color:var(--border-color-primary)}.icon-button-wrapper.svelte-1h0hs6p>*{height:100%}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-x2tujq.svelte-x2tujq{height:100%;position:relative;min-width:var(--size-20)}.image-container.svelte-x2tujq button.svelte-x2tujq{width:var(--size-full);height:var(--size-full);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.image-frame.svelte-x2tujq img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.selectable.svelte-x2tujq.svelte-x2tujq{cursor:crosshair}.fullscreen-controls svg{position:relative;top:0}.image-container:fullscreen{background-color:#000;display:flex;justify-content:center;align-items:center}.image-container:fullscreen img{max-width:90vw;max-height:90vh;object-fit:scale-down}.image-frame.svelte-x2tujq.svelte-x2tujq{width:auto;height:100%;display:flex;align-items:center;justify-content:center}.overlay.svelte-1pwzuub{position:absolute;background-color:#0006;width:100%;height:100%}.hidden.svelte-1pwzuub{display:none}.load-wrap.svelte-1pwzuub{display:flex;justify-content:center;align-items:center;height:100%}.loader.svelte-1pwzuub{display:flex;position:relative;background-color:var(--border-color-accent-subdued);animation:svelte-1pwzuub-shadowPulse 2s linear infinite;box-shadow:-24px 0 var(--border-color-accent-subdued),24px 0 var(--border-color-accent-subdued);margin:var(--spacing-md);border-radius:50%;width:10px;height:10px;scale:.5}@keyframes svelte-1pwzuub-shadowPulse{33%{box-shadow:-24px 0 var(--border-color-accent-subdued),24px 0 #fff;background:#fff}66%{box-shadow:-24px 0 #fff,24px 0 #fff;background:var(--border-color-accent-subdued)}to{box-shadow:-24px 0 #fff,24px 0 var(--border-color-accent-subdued);background:#fff}}.gallery-container.svelte-1crbckc.svelte-1crbckc{position:relative;width:100%;height:100%}button.svelte-1crbckc.svelte-1crbckc{width:var(--size-full);height:var(--size-full);display:block;border-radius:var(--radius-lg)}.preview.svelte-1crbckc.svelte-1crbckc{display:flex;position:absolute;flex-direction:column;z-index:var(--layer-2);border-radius:calc(var(--block-radius) - var(--block-border-width));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);width:var(--size-full);height:var(--size-full)}.preview.svelte-1crbckc.svelte-1crbckc:focus-visible{outline:none}.preview.minimal.svelte-1crbckc.svelte-1crbckc{width:fit-content;height:fit-content}.preview.svelte-1crbckc.svelte-1crbckc:before{content:"";position:absolute;z-index:var(--layer-below);background:var(--background-fill-primary);opacity:.9;width:var(--size-full);height:var(--size-full)}.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:var(--size-80);max-height:65vh}@media (--screen-xl){.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:450px}}.media-container.svelte-1crbckc.svelte-1crbckc{height:calc(100% - var(--size-14));width:100%;display:flex;align-items:center;justify-content:center;cursor:pointer}.media-container.svelte-1crbckc img,.media-container.svelte-1crbckc video{max-width:100%;max-height:100%;object-fit:contain}.thumbnails.svelte-1crbckc img{object-fit:cover;width:var(--size-full);height:var(--size-full)}.thumbnails.svelte-1crbckc svg{position:absolute;top:var(--size-2);left:var(--size-2);width:50%;height:50%;opacity:50%}.caption.svelte-1crbckc.svelte-1crbckc{padding:var(--size-2) var(--size-3);overflow:hidden;color:var(--block-label-text-color);font-weight:var(--weight-semibold);text-align:center;text-overflow:ellipsis;white-space:nowrap;align-self:center}.thumbnails.svelte-1crbckc.svelte-1crbckc{display:flex;position:absolute;bottom:0;justify-content:flex-start;align-items:center;gap:var(--spacing-lg);width:var(--size-full);height:var(--size-14);overflow-x:scroll}.thumbnail-item.svelte-1crbckc.svelte-1crbckc{--ring-color:transparent;position:relative;box-shadow:inset 0 0 0 1px var(--ring-color),var(--shadow-drop);border:1px solid var(--border-color-primary);border-radius:var(--button-small-radius);background:var(--background-fill-secondary);aspect-ratio:var(--ratio-square);width:var(--size-full);height:var(--size-full);overflow:clip}.thumbnail-item.svelte-1crbckc.svelte-1crbckc:hover{--ring-color:var(--color-accent);border-color:var(--color-accent);filter:brightness(1.1)}.thumbnail-item.selected.svelte-1crbckc.svelte-1crbckc{--ring-color:var(--color-accent);border-color:var(--color-accent)}.thumbnail-item.svelte-1crbckc svg{position:absolute;top:50%;left:50%;width:50%;height:50%;opacity:50%;transform:translate(-50%,-50%)}.thumbnail-item.svelte-1crbckc video{width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:cover}.thumbnail-small.svelte-1crbckc.svelte-1crbckc{flex:none;transform:scale(.9);transition:75ms;width:var(--size-9);height:var(--size-9)}.thumbnail-small.selected.svelte-1crbckc.svelte-1crbckc{--ring-color:var(--color-accent);transform:scale(1);border-color:var(--color-accent)}.grid-wrap.svelte-1crbckc.svelte-1crbckc{position:relative;padding:var(--size-2);overflow-y:auto}.grid-wrap.fixed-height.svelte-1crbckc.svelte-1crbckc{min-height:var(--size-80);max-height:65vh}.grid-container.svelte-1crbckc.svelte-1crbckc{display:grid;position:relative;grid-template-rows:repeat(var(--grid-rows),minmax(100px,1fr));grid-template-columns:repeat(var(--grid-cols),minmax(100px,1fr));grid-auto-rows:minmax(100px,1fr);gap:var(--spacing-lg)}.single-item-wrapper.svelte-1crbckc.svelte-1crbckc{display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:var(--spacing-xxl);box-sizing:border-box}.single-item-wrapper.svelte-1crbckc .gallery-item-with-name.svelte-1crbckc{width:100%;height:100%;max-width:min(300px,80vw);max-height:min(320px,calc(80vh - var(--size-4)));display:flex;flex-direction:column;align-items:center}.single-item-wrapper.svelte-1crbckc .gallery-item.svelte-1crbckc{width:100%;height:100%;max-width:100%;max-height:100%}.single-item-wrapper.svelte-1crbckc .thumbnail-item.thumbnail-lg.svelte-1crbckc{display:flex!important;align-items:center!important;justify-content:center!important}.single-item-wrapper.svelte-1crbckc .thumbnail-filename.svelte-1crbckc{height:var(--size-4);line-height:var(--size-4)}.single-item-wrapper.svelte-1crbckc .thumbnail-lg.svelte-1crbckc>img,.single-item-wrapper.svelte-1crbckc .thumbnail-lg.svelte-1crbckc>video{object-fit:var(--object-fit)!important}.thumbnail-lg.svelte-1crbckc>img,.thumbnail-lg.svelte-1crbckc>video{width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:var(--object-fit)}.thumbnail-lg.svelte-1crbckc:hover .caption-label.svelte-1crbckc{opacity:.5}.caption-label.svelte-1crbckc.svelte-1crbckc{position:absolute;right:var(--block-label-margin);bottom:var(--block-label-margin);z-index:var(--layer-1);border-top:1px solid var(--border-color-primary);border-left:1px solid var(--border-color-primary);border-radius:var(--block-label-radius);background:var(--background-fill-secondary);padding:var(--block-label-padding);max-width:80%;overflow:hidden;font-size:var(--block-label-text-size);text-align:left;text-overflow:ellipsis;white-space:nowrap}.grid-wrap.minimal.svelte-1crbckc.svelte-1crbckc{padding:0}.gallery-item-with-name.svelte-1crbckc.svelte-1crbckc{display:flex;flex-direction:column;gap:var(--size-1);width:100%;height:100%}.thumbnail-filename.svelte-1crbckc.svelte-1crbckc{font-size:var(--text-xs);color:var(--body-text-color);text-align:center;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 var(--size-1)}.gallery-item.svelte-1crbckc.svelte-1crbckc{position:relative;width:100%;height:100%}.metadata-popup.svelte-1crbckc.svelte-1crbckc{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;max-width:min(90%,600px);max-height:min(50vh,calc(100% - 2rem));min-height:200px;display:flex;flex-direction:column;pointer-events:auto}.popup-content.svelte-1crbckc.svelte-1crbckc{padding:1rem;display:flex;flex-direction:column;width:100%;box-sizing:border-box;overflow-y:auto;position:relative}.close-button.svelte-1crbckc.svelte-1crbckc{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1.25rem;cursor:pointer;z-index:20;color:var(--body-text-color);padding:.25rem;line-height:1;width:24px;height:24px;text-align:center}.popup-title.svelte-1crbckc.svelte-1crbckc{font-weight:700;margin:0 0 1rem;flex-shrink:0;padding-right:2.5rem}.metadata-table-container.svelte-1crbckc.svelte-1crbckc{flex-grow:1;overflow-y:auto;max-height:calc(100% - 5rem);min-height:0;margin-bottom:1rem}.metadata-table.svelte-1crbckc.svelte-1crbckc{width:100%;border-collapse:collapse;table-layout:auto}.metadata-label.svelte-1crbckc.svelte-1crbckc{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:35%}.metadata-value.svelte-1crbckc.svelte-1crbckc{text-align:left;padding:.5rem;white-space:pre-wrap;word-break:break-all;vertical-align:top}.load-metadata-button.svelte-1crbckc.svelte-1crbckc{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center;flex-shrink:0}.load-metadata-button.svelte-1crbckc.svelte-1crbckc:hover{background-color:var(--button-primary-background-fill-hover)}.no-metadata-message.svelte-1crbckc.svelte-1crbckc{flex-grow:1;display:flex;align-items:center;justify-content:center;color:var(--body-text-color-subdued)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-vusapu.svelte-vusapu{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-3);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);overflow:hidden;pointer-events:none}.wrap.center.svelte-vusapu.svelte-vusapu{top:0;right:0;left:0}.wrap.default.svelte-vusapu.svelte-vusapu{top:0;right:0;bottom:0;left:0}.hide.svelte-vusapu.svelte-vusapu{opacity:0;pointer-events:none}.generating.svelte-vusapu.svelte-vusapu{animation:svelte-vusapu-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-vusapu-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-vusapu.svelte-vusapu{background:none}@keyframes svelte-vusapu-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-vusapu-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-vusapu.svelte-vusapu{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-vusapu.svelte-vusapu{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-vusapu.svelte-vusapu{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-vusapu.svelte-vusapu{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-vusapu.svelte-vusapu{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-vusapu.svelte-vusapu{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-vusapu.svelte-vusapu{position:absolute;bottom:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-vusapu.svelte-vusapu{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-vusapu.svelte-vusapu{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.validation-error.svelte-vusapu.svelte-vusapu{pointer-events:auto;color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font);position:absolute;background:var(--error-background-fill);top:0;right:0;z-index:var(--layer-3);padding:var(--size-1) var(--size-2);font-size:var(--text-md);text-align:center;border-bottom-left-radius:var(--radius-sm);border-bottom:1px solid var(--error-border-color);border-left:1px solid var(--error-border-color);display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-xl)}.minimal.svelte-vusapu.svelte-vusapu{pointer-events:none}.minimal.svelte-vusapu .progress-text.svelte-vusapu{background:var(--block-background-fill)}.border.svelte-vusapu.svelte-vusapu{border:1px solid var(--border-color-primary)}.clear-status.svelte-vusapu.svelte-vusapu{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.toast-body.svelte-syezpc{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-syezpc{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-syezpc{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-body.success.svelte-syezpc{border:1px solid var(--color-green-700);background:var(--color-green-50)}.dark .toast-body.success.svelte-syezpc{border:1px solid var(--color-green-500);background-color:var(--color-grey-950)}.toast-title.svelte-syezpc{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm)}.toast-title.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-title.error.svelte-syezpc{color:var(--color-red-50)}.toast-title.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-title.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-title.info.svelte-syezpc{color:var(--color-grey-50)}.toast-title.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-title.success.svelte-syezpc{color:var(--color-green-50)}.toast-close.svelte-syezpc{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-close.error.svelte-syezpc{color:var(--color-red-500)}.toast-close.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-close.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-close.info.svelte-syezpc{color:var(--color-grey-500)}.toast-close.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-close.success.svelte-syezpc{color:var(--color-green-500)}.toast-text.svelte-syezpc{font-size:var(--text-lg);word-wrap:break-word;overflow-wrap:break-word;word-break:break-word}.toast-text.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-text.error.svelte-syezpc{color:var(--color-red-50)}.toast-text.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-text.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-text.info.svelte-syezpc{color:var(--color-grey-50)}.toast-text.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-text.success.svelte-syezpc{color:var(--color-green-50)}.toast-details.svelte-syezpc{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-syezpc{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-icon.error.svelte-syezpc{color:var(--color-red-500)}.toast-icon.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-icon.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-syezpc{color:var(--color-grey-500)}.toast-icon.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-icon.success.svelte-syezpc{color:var(--color-green-500)}@keyframes svelte-syezpc-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-syezpc{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-syezpc-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-syezpc{background:var(--color-red-700)}.dark .timer.error.svelte-syezpc{background:var(--color-red-500)}.timer.warning.svelte-syezpc{background:var(--color-yellow-700)}.dark .timer.warning.svelte-syezpc{background:var(--color-yellow-500)}.timer.info.svelte-syezpc{background:var(--color-grey-700)}.dark .timer.info.svelte-syezpc{background:var(--color-grey-500)}.timer.success.svelte-syezpc{background:var(--color-green-700)}.dark .timer.success.svelte-syezpc{background:var(--color-green-500)}.hidden.svelte-syezpc{display:none}.toast-text.svelte-syezpc a{text-decoration:underline}.toast-wrap.svelte-je2isz{--toast-top:var(--size-4);display:flex;position:fixed;top:calc(var(--toast-top) + var(--size-4));right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-je2isz{width:calc(var(--size-96) + var(--size-10))}}.streaming-bar.svelte-ga0jj6{position:absolute;bottom:0;left:0;right:0;height:4px;background-color:var(--primary-600);animation:svelte-ga0jj6-countdown linear forwards;z-index:1}@keyframes svelte-ga0jj6-countdown{0%{transform:translate(0)}to{transform:translate(-100%)}}:global(*::-webkit-scrollbar){width:8px;height:8px;background-color:transparent}:global(*::-webkit-scrollbar-track){background:transparent;border-radius:10px}:global(*::-webkit-scrollbar-thumb){background-color:#8886;border-radius:10px;border:2px solid transparent;background-clip:content-box}:global(*::-webkit-scrollbar-thumb:hover){background-color:#888888b3}:global(html){scrollbar-width:thin;scrollbar-color:rgba(136,136,136,.7) transparent}.gallery-container{position:relative;width:100%;height:100%}button{width:var(--size-full);height:var(--size-full);display:block;border-radius:var(--radius-lg)}.preview{display:flex;position:absolute;flex-direction:column;z-index:var(--layer-2);border-radius:calc(var(--block-radius) - var(--block-border-width));-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);width:var(--size-full);height:var(--size-full)}.preview:focus-visible{outline:none}.preview.minimal{width:fit-content;height:fit-content}.preview:before{content:"";position:absolute;z-index:var(--layer-below);background:var(--background-fill-primary);opacity:.9;width:var(--size-full);height:var(--size-full)}.fixed-height{min-height:var(--size-80);max-height:65vh}@media (--screen-xl){.fixed-height{min-height:450px}}.media-container{height:calc(100% - var(--size-14));width:100%;display:flex;align-items:center;justify-content:center;cursor:pointer}.media-container :global(img),.media-container :global(video){max-width:100%;max-height:100%;object-fit:contain}.thumbnails :global(img){object-fit:cover;width:var(--size-full);height:var(--size-full)}.thumbnails :global(svg){position:absolute;top:var(--size-2);left:var(--size-2);width:50%;height:50%;opacity:50%}.caption{padding:var(--size-2) var(--size-3);overflow:hidden;color:var(--block-label-text-color);font-weight:var(--weight-semibold);text-align:center;text-overflow:ellipsis;white-space:nowrap;align-self:center}.thumbnails{display:flex;position:absolute;bottom:0;justify-content:flex-start;align-items:center;gap:var(--spacing-lg);width:var(--size-full);height:var(--size-14);overflow-x:scroll}.thumbnail-item{--ring-color: transparent;position:relative;box-shadow:inset 0 0 0 1px var(--ring-color),var(--shadow-drop);border:1px solid var(--border-color-primary);border-radius:var(--button-small-radius);background:var(--background-fill-secondary);aspect-ratio:var(--ratio-square);width:var(--size-full);height:var(--size-full);overflow:clip}.thumbnail-item:hover{--ring-color: var(--color-accent);border-color:var(--color-accent);filter:brightness(1.1)}.thumbnail-item.selected{--ring-color: var(--color-accent);border-color:var(--color-accent)}.thumbnail-item :global(svg){position:absolute;top:50%;left:50%;width:50%;height:50%;opacity:50%;transform:translate(-50%,-50%)}.thumbnail-item :global(video){width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:cover}.thumbnail-small{flex:none;transform:scale(.9);transition:75ms;width:var(--size-9);height:var(--size-9)}.thumbnail-small.selected{--ring-color: var(--color-accent);transform:scale(1);border-color:var(--color-accent)}.grid-wrap{position:relative;padding:var(--size-2);overflow-y:auto}.grid-wrap.fixed-height{min-height:var(--size-80);max-height:65vh}.grid-container{display:grid;position:relative;grid-template-rows:repeat(var(--grid-rows),minmax(100px,1fr));grid-template-columns:repeat(var(--grid-cols),minmax(100px,1fr));grid-auto-rows:minmax(100px,1fr);gap:var(--spacing-lg)}.single-item-wrapper{display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:var(--spacing-xxl);box-sizing:border-box}.single-item-wrapper .gallery-item-with-name{width:100%;height:100%;max-width:min(300px,80vw);max-height:min(320px,calc(80vh - var(--size-4)));display:flex;flex-direction:column;align-items:center}.single-item-wrapper .gallery-item{width:100%;height:100%;max-width:100%;max-height:100%}.single-item-wrapper .thumbnail-item.thumbnail-lg{display:flex!important;align-items:center!important;justify-content:center!important}.single-item-wrapper .thumbnail-filename{height:var(--size-4);line-height:var(--size-4)}.single-item-wrapper .thumbnail-lg>:global(img),.single-item-wrapper .thumbnail-lg>:global(video){object-fit:var(--object-fit)!important}.thumbnail-lg>:global(img),.thumbnail-lg>:global(video){width:var(--size-full);height:var(--size-full);overflow:hidden;object-fit:var(--object-fit)}.thumbnail-lg:hover .caption-label{opacity:.5}.caption-label{position:absolute;right:var(--block-label-margin);bottom:var(--block-label-margin);z-index:var(--layer-1);border-top:1px solid var(--border-color-primary);border-left:1px solid var(--border-color-primary);border-radius:var(--block-label-radius);background:var(--background-fill-secondary);padding:var(--block-label-padding);max-width:80%;overflow:hidden;font-size:var(--block-label-text-size);text-align:left;text-overflow:ellipsis;white-space:nowrap}.grid-wrap.minimal{padding:0}.gallery-item-with-name{display:flex;flex-direction:column;gap:var(--size-1);width:100%;height:100%}.thumbnail-filename{font-size:var(--text-xs);color:var(--body-text-color);text-align:center;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 var(--size-1)}.gallery-item{position:relative;width:100%;height:100%}.metadata-popup{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;max-width:min(90%,600px);max-height:min(50vh,calc(100% - 2rem));min-height:200px;display:flex;flex-direction:column;pointer-events:auto}.popup-content{padding:1rem;display:flex;flex-direction:column;width:100%;box-sizing:border-box;overflow-y:auto;position:relative}.close-button{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1.25rem;cursor:pointer;z-index:20;color:var(--body-text-color);padding:.25rem;line-height:1;width:24px;height:24px;text-align:center}.popup-title{font-weight:700;margin:0 0 1rem;flex-shrink:0;padding-right:2.5rem}.metadata-table-container{flex-grow:1;overflow-y:auto;max-height:calc(100% - 5rem);min-height:0;margin-bottom:1rem}.metadata-table{width:100%;border-collapse:collapse;table-layout:auto}.metadata-label{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:35%}.metadata-value{text-align:left;padding:.5rem;white-space:pre-wrap;word-break:break-all;vertical-align:top}.load-metadata-button{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center;flex-shrink:0}.load-metadata-button:hover{background-color:var(--button-primary-background-fill-hover)}.no-metadata-message{flex-grow:1;display:flex;align-items:center;justify-content:center;color:var(--body-text-color-subdued)}.container.svelte-1onbytl.svelte-1onbytl{border-radius:var(--radius-lg);overflow:hidden}.container.selected.svelte-1onbytl.svelte-1onbytl{border:2px solid var(--border-color-accent)}.images-wrapper.svelte-1onbytl.svelte-1onbytl{display:flex;gap:var(--spacing-sm)}.container.table.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;align-items:center;padding:var(--spacing-sm);border:1px solid var(--border-color-primary);border-radius:var(--radius-lg);background:var(--background-fill-secondary)}.container.gallery.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;gap:0}.image-container.svelte-1onbytl.svelte-1onbytl{position:relative;flex-shrink:0}.container.table.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8))}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl:first-child{margin-left:0}.more-indicator.svelte-1onbytl.svelte-1onbytl{display:flex;align-items:center;justify-content:center;font-size:var(--text-lg);font-weight:700;color:var(--border-color-primary)}.container.table.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8));margin-right:calc(-1 * var(--size-6))}.image-container.svelte-1onbytl img.svelte-1onbytl,.image-container.svelte-1onbytl video.svelte-1onbytl{width:100%;height:100%;object-fit:cover;border-radius:var(--radius-md)}.caption.svelte-1onbytl.svelte-1onbytl{position:absolute;bottom:0;left:0;right:0;background:#000000b3;color:#fff;padding:var(--spacing-xs);font-size:var(--text-xs);text-align:center;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.container.table.svelte-1onbytl .caption.svelte-1onbytl{display:none}
|
src/backend/gradio_mediagallery/templates/example/index.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const {
|
| 2 |
+
SvelteComponent: j,
|
| 3 |
+
append_hydration: d,
|
| 4 |
+
attr: f,
|
| 5 |
+
children: v,
|
| 6 |
+
claim_element: u,
|
| 7 |
+
claim_space: g,
|
| 8 |
+
claim_text: q,
|
| 9 |
+
destroy_each: G,
|
| 10 |
+
detach: o,
|
| 11 |
+
element: _,
|
| 12 |
+
empty: k,
|
| 13 |
+
ensure_array_like: E,
|
| 14 |
+
get_svelte_dataset: M,
|
| 15 |
+
init: O,
|
| 16 |
+
insert_hydration: h,
|
| 17 |
+
noop: I,
|
| 18 |
+
safe_not_equal: z,
|
| 19 |
+
set_data: A,
|
| 20 |
+
space: b,
|
| 21 |
+
src_url_equal: p,
|
| 22 |
+
text: P,
|
| 23 |
+
toggle_class: m
|
| 24 |
+
} = window.__gradio__svelte__internal;
|
| 25 |
+
function D(a, e, i) {
|
| 26 |
+
const s = a.slice();
|
| 27 |
+
return s[3] = e[i], s;
|
| 28 |
+
}
|
| 29 |
+
function V(a) {
|
| 30 |
+
let e, i, s = E(
|
| 31 |
+
/*value*/
|
| 32 |
+
a[0].slice(0, 3)
|
| 33 |
+
), l = [];
|
| 34 |
+
for (let n = 0; n < s.length; n += 1)
|
| 35 |
+
l[n] = N(D(a, s, n));
|
| 36 |
+
let t = (
|
| 37 |
+
/*value*/
|
| 38 |
+
a[0].length > 3 && S()
|
| 39 |
+
);
|
| 40 |
+
return {
|
| 41 |
+
c() {
|
| 42 |
+
e = _("div");
|
| 43 |
+
for (let n = 0; n < l.length; n += 1)
|
| 44 |
+
l[n].c();
|
| 45 |
+
i = b(), t && t.c(), this.h();
|
| 46 |
+
},
|
| 47 |
+
l(n) {
|
| 48 |
+
e = u(n, "DIV", { class: !0 });
|
| 49 |
+
var r = v(e);
|
| 50 |
+
for (let c = 0; c < l.length; c += 1)
|
| 51 |
+
l[c].l(r);
|
| 52 |
+
i = g(r), t && t.l(r), r.forEach(o), this.h();
|
| 53 |
+
},
|
| 54 |
+
h() {
|
| 55 |
+
f(e, "class", "images-wrapper svelte-1onbytl");
|
| 56 |
+
},
|
| 57 |
+
m(n, r) {
|
| 58 |
+
h(n, e, r);
|
| 59 |
+
for (let c = 0; c < l.length; c += 1)
|
| 60 |
+
l[c] && l[c].m(e, null);
|
| 61 |
+
d(e, i), t && t.m(e, null);
|
| 62 |
+
},
|
| 63 |
+
p(n, r) {
|
| 64 |
+
if (r & /*value*/
|
| 65 |
+
1) {
|
| 66 |
+
s = E(
|
| 67 |
+
/*value*/
|
| 68 |
+
n[0].slice(0, 3)
|
| 69 |
+
);
|
| 70 |
+
let c;
|
| 71 |
+
for (c = 0; c < s.length; c += 1) {
|
| 72 |
+
const y = D(n, s, c);
|
| 73 |
+
l[c] ? l[c].p(y, r) : (l[c] = N(y), l[c].c(), l[c].m(e, i));
|
| 74 |
+
}
|
| 75 |
+
for (; c < l.length; c += 1)
|
| 76 |
+
l[c].d(1);
|
| 77 |
+
l.length = s.length;
|
| 78 |
+
}
|
| 79 |
+
/*value*/
|
| 80 |
+
n[0].length > 3 ? t || (t = S(), t.c(), t.m(e, null)) : t && (t.d(1), t = null);
|
| 81 |
+
},
|
| 82 |
+
d(n) {
|
| 83 |
+
n && o(e), G(l, n), t && t.d();
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
}
|
| 87 |
+
function B(a) {
|
| 88 |
+
let e, i, s, l, t = (
|
| 89 |
+
/*item*/
|
| 90 |
+
a[3].caption && C(a)
|
| 91 |
+
);
|
| 92 |
+
return {
|
| 93 |
+
c() {
|
| 94 |
+
e = _("div"), i = _("video"), l = b(), t && t.c(), this.h();
|
| 95 |
+
},
|
| 96 |
+
l(n) {
|
| 97 |
+
e = u(n, "DIV", { class: !0 });
|
| 98 |
+
var r = v(e);
|
| 99 |
+
i = u(r, "VIDEO", { src: !0, preload: !0, class: !0 }), v(i).forEach(o), l = g(r), t && t.l(r), r.forEach(o), this.h();
|
| 100 |
+
},
|
| 101 |
+
h() {
|
| 102 |
+
p(i.src, s = /*item*/
|
| 103 |
+
a[3].video.url) || f(i, "src", s), i.controls = !1, i.muted = !0, f(i, "preload", "metadata"), f(i, "class", "svelte-1onbytl"), f(e, "class", "image-container svelte-1onbytl");
|
| 104 |
+
},
|
| 105 |
+
m(n, r) {
|
| 106 |
+
h(n, e, r), d(e, i), d(e, l), t && t.m(e, null);
|
| 107 |
+
},
|
| 108 |
+
p(n, r) {
|
| 109 |
+
r & /*value*/
|
| 110 |
+
1 && !p(i.src, s = /*item*/
|
| 111 |
+
n[3].video.url) && f(i, "src", s), /*item*/
|
| 112 |
+
n[3].caption ? t ? t.p(n, r) : (t = C(n), t.c(), t.m(e, null)) : t && (t.d(1), t = null);
|
| 113 |
+
},
|
| 114 |
+
d(n) {
|
| 115 |
+
n && o(e), t && t.d();
|
| 116 |
+
}
|
| 117 |
+
};
|
| 118 |
+
}
|
| 119 |
+
function F(a) {
|
| 120 |
+
let e, i, s, l, t, n = (
|
| 121 |
+
/*item*/
|
| 122 |
+
a[3].caption && w(a)
|
| 123 |
+
);
|
| 124 |
+
return {
|
| 125 |
+
c() {
|
| 126 |
+
e = _("div"), i = _("img"), t = b(), n && n.c(), this.h();
|
| 127 |
+
},
|
| 128 |
+
l(r) {
|
| 129 |
+
e = u(r, "DIV", { class: !0 });
|
| 130 |
+
var c = v(e);
|
| 131 |
+
i = u(c, "IMG", { src: !0, alt: !0, class: !0 }), t = g(c), n && n.l(c), c.forEach(o), this.h();
|
| 132 |
+
},
|
| 133 |
+
h() {
|
| 134 |
+
p(i.src, s = /*item*/
|
| 135 |
+
a[3].image.url) || f(i, "src", s), f(i, "alt", l = /*item*/
|
| 136 |
+
a[3].caption || ""), f(i, "class", "svelte-1onbytl"), f(e, "class", "image-container svelte-1onbytl");
|
| 137 |
+
},
|
| 138 |
+
m(r, c) {
|
| 139 |
+
h(r, e, c), d(e, i), d(e, t), n && n.m(e, null);
|
| 140 |
+
},
|
| 141 |
+
p(r, c) {
|
| 142 |
+
c & /*value*/
|
| 143 |
+
1 && !p(i.src, s = /*item*/
|
| 144 |
+
r[3].image.url) && f(i, "src", s), c & /*value*/
|
| 145 |
+
1 && l !== (l = /*item*/
|
| 146 |
+
r[3].caption || "") && f(i, "alt", l), /*item*/
|
| 147 |
+
r[3].caption ? n ? n.p(r, c) : (n = w(r), n.c(), n.m(e, null)) : n && (n.d(1), n = null);
|
| 148 |
+
},
|
| 149 |
+
d(r) {
|
| 150 |
+
r && o(e), n && n.d();
|
| 151 |
+
}
|
| 152 |
+
};
|
| 153 |
+
}
|
| 154 |
+
function C(a) {
|
| 155 |
+
let e, i = (
|
| 156 |
+
/*item*/
|
| 157 |
+
a[3].caption + ""
|
| 158 |
+
), s;
|
| 159 |
+
return {
|
| 160 |
+
c() {
|
| 161 |
+
e = _("span"), s = P(i), this.h();
|
| 162 |
+
},
|
| 163 |
+
l(l) {
|
| 164 |
+
e = u(l, "SPAN", { class: !0 });
|
| 165 |
+
var t = v(e);
|
| 166 |
+
s = q(t, i), t.forEach(o), this.h();
|
| 167 |
+
},
|
| 168 |
+
h() {
|
| 169 |
+
f(e, "class", "caption svelte-1onbytl");
|
| 170 |
+
},
|
| 171 |
+
m(l, t) {
|
| 172 |
+
h(l, e, t), d(e, s);
|
| 173 |
+
},
|
| 174 |
+
p(l, t) {
|
| 175 |
+
t & /*value*/
|
| 176 |
+
1 && i !== (i = /*item*/
|
| 177 |
+
l[3].caption + "") && A(s, i);
|
| 178 |
+
},
|
| 179 |
+
d(l) {
|
| 180 |
+
l && o(e);
|
| 181 |
+
}
|
| 182 |
+
};
|
| 183 |
+
}
|
| 184 |
+
function w(a) {
|
| 185 |
+
let e, i = (
|
| 186 |
+
/*item*/
|
| 187 |
+
a[3].caption + ""
|
| 188 |
+
), s;
|
| 189 |
+
return {
|
| 190 |
+
c() {
|
| 191 |
+
e = _("span"), s = P(i), this.h();
|
| 192 |
+
},
|
| 193 |
+
l(l) {
|
| 194 |
+
e = u(l, "SPAN", { class: !0 });
|
| 195 |
+
var t = v(e);
|
| 196 |
+
s = q(t, i), t.forEach(o), this.h();
|
| 197 |
+
},
|
| 198 |
+
h() {
|
| 199 |
+
f(e, "class", "caption svelte-1onbytl");
|
| 200 |
+
},
|
| 201 |
+
m(l, t) {
|
| 202 |
+
h(l, e, t), d(e, s);
|
| 203 |
+
},
|
| 204 |
+
p(l, t) {
|
| 205 |
+
t & /*value*/
|
| 206 |
+
1 && i !== (i = /*item*/
|
| 207 |
+
l[3].caption + "") && A(s, i);
|
| 208 |
+
},
|
| 209 |
+
d(l) {
|
| 210 |
+
l && o(e);
|
| 211 |
+
}
|
| 212 |
+
};
|
| 213 |
+
}
|
| 214 |
+
function N(a) {
|
| 215 |
+
let e;
|
| 216 |
+
function i(t, n) {
|
| 217 |
+
if ("image" in /*item*/
|
| 218 |
+
t[3] && /*item*/
|
| 219 |
+
t[3].image) return F;
|
| 220 |
+
if ("video" in /*item*/
|
| 221 |
+
t[3] && /*item*/
|
| 222 |
+
t[3].video) return B;
|
| 223 |
+
}
|
| 224 |
+
let s = i(a), l = s && s(a);
|
| 225 |
+
return {
|
| 226 |
+
c() {
|
| 227 |
+
l && l.c(), e = k();
|
| 228 |
+
},
|
| 229 |
+
l(t) {
|
| 230 |
+
l && l.l(t), e = k();
|
| 231 |
+
},
|
| 232 |
+
m(t, n) {
|
| 233 |
+
l && l.m(t, n), h(t, e, n);
|
| 234 |
+
},
|
| 235 |
+
p(t, n) {
|
| 236 |
+
s === (s = i(t)) && l ? l.p(t, n) : (l && l.d(1), l = s && s(t), l && (l.c(), l.m(e.parentNode, e)));
|
| 237 |
+
},
|
| 238 |
+
d(t) {
|
| 239 |
+
t && o(e), l && l.d(t);
|
| 240 |
+
}
|
| 241 |
+
};
|
| 242 |
+
}
|
| 243 |
+
function S(a) {
|
| 244 |
+
let e, i = "…";
|
| 245 |
+
return {
|
| 246 |
+
c() {
|
| 247 |
+
e = _("div"), e.textContent = i, this.h();
|
| 248 |
+
},
|
| 249 |
+
l(s) {
|
| 250 |
+
e = u(s, "DIV", { class: !0, "data-svelte-h": !0 }), M(e) !== "svelte-1u6jrni" && (e.textContent = i), this.h();
|
| 251 |
+
},
|
| 252 |
+
h() {
|
| 253 |
+
f(e, "class", "more-indicator svelte-1onbytl");
|
| 254 |
+
},
|
| 255 |
+
m(s, l) {
|
| 256 |
+
h(s, e, l);
|
| 257 |
+
},
|
| 258 |
+
d(s) {
|
| 259 |
+
s && o(e);
|
| 260 |
+
}
|
| 261 |
+
};
|
| 262 |
+
}
|
| 263 |
+
function H(a) {
|
| 264 |
+
let e, i = (
|
| 265 |
+
/*value*/
|
| 266 |
+
a[0] && /*value*/
|
| 267 |
+
a[0].length > 0 && V(a)
|
| 268 |
+
);
|
| 269 |
+
return {
|
| 270 |
+
c() {
|
| 271 |
+
e = _("div"), i && i.c(), this.h();
|
| 272 |
+
},
|
| 273 |
+
l(s) {
|
| 274 |
+
e = u(s, "DIV", { class: !0 });
|
| 275 |
+
var l = v(e);
|
| 276 |
+
i && i.l(l), l.forEach(o), this.h();
|
| 277 |
+
},
|
| 278 |
+
h() {
|
| 279 |
+
f(e, "class", "container svelte-1onbytl"), m(
|
| 280 |
+
e,
|
| 281 |
+
"table",
|
| 282 |
+
/*type*/
|
| 283 |
+
a[1] === "table"
|
| 284 |
+
), m(
|
| 285 |
+
e,
|
| 286 |
+
"gallery",
|
| 287 |
+
/*type*/
|
| 288 |
+
a[1] === "gallery"
|
| 289 |
+
), m(
|
| 290 |
+
e,
|
| 291 |
+
"selected",
|
| 292 |
+
/*selected*/
|
| 293 |
+
a[2]
|
| 294 |
+
);
|
| 295 |
+
},
|
| 296 |
+
m(s, l) {
|
| 297 |
+
h(s, e, l), i && i.m(e, null);
|
| 298 |
+
},
|
| 299 |
+
p(s, [l]) {
|
| 300 |
+
/*value*/
|
| 301 |
+
s[0] && /*value*/
|
| 302 |
+
s[0].length > 0 ? i ? i.p(s, l) : (i = V(s), i.c(), i.m(e, null)) : i && (i.d(1), i = null), l & /*type*/
|
| 303 |
+
2 && m(
|
| 304 |
+
e,
|
| 305 |
+
"table",
|
| 306 |
+
/*type*/
|
| 307 |
+
s[1] === "table"
|
| 308 |
+
), l & /*type*/
|
| 309 |
+
2 && m(
|
| 310 |
+
e,
|
| 311 |
+
"gallery",
|
| 312 |
+
/*type*/
|
| 313 |
+
s[1] === "gallery"
|
| 314 |
+
), l & /*selected*/
|
| 315 |
+
4 && m(
|
| 316 |
+
e,
|
| 317 |
+
"selected",
|
| 318 |
+
/*selected*/
|
| 319 |
+
s[2]
|
| 320 |
+
);
|
| 321 |
+
},
|
| 322 |
+
i: I,
|
| 323 |
+
o: I,
|
| 324 |
+
d(s) {
|
| 325 |
+
s && o(e), i && i.d();
|
| 326 |
+
}
|
| 327 |
+
};
|
| 328 |
+
}
|
| 329 |
+
function J(a, e, i) {
|
| 330 |
+
let { value: s } = e, { type: l } = e, { selected: t = !1 } = e;
|
| 331 |
+
return a.$$set = (n) => {
|
| 332 |
+
"value" in n && i(0, s = n.value), "type" in n && i(1, l = n.type), "selected" in n && i(2, t = n.selected);
|
| 333 |
+
}, [s, l, t];
|
| 334 |
+
}
|
| 335 |
+
class K extends j {
|
| 336 |
+
constructor(e) {
|
| 337 |
+
super(), O(this, e, J, H, z, { value: 0, type: 1, selected: 2 });
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
export {
|
| 341 |
+
K as default
|
| 342 |
+
};
|
src/backend/gradio_mediagallery/templates/example/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.container.svelte-1onbytl.svelte-1onbytl{border-radius:var(--radius-lg);overflow:hidden}.container.selected.svelte-1onbytl.svelte-1onbytl{border:2px solid var(--border-color-accent)}.images-wrapper.svelte-1onbytl.svelte-1onbytl{display:flex;gap:var(--spacing-sm)}.container.table.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;align-items:center;padding:var(--spacing-sm);border:1px solid var(--border-color-primary);border-radius:var(--radius-lg);background:var(--background-fill-secondary)}.container.gallery.svelte-1onbytl .images-wrapper.svelte-1onbytl{flex-direction:row;gap:0}.image-container.svelte-1onbytl.svelte-1onbytl{position:relative;flex-shrink:0}.container.table.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8))}.container.gallery.svelte-1onbytl .image-container.svelte-1onbytl:first-child{margin-left:0}.more-indicator.svelte-1onbytl.svelte-1onbytl{display:flex;align-items:center;justify-content:center;font-size:var(--text-lg);font-weight:700;color:var(--border-color-primary)}.container.table.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-12);height:var(--size-12)}.container.gallery.svelte-1onbytl .more-indicator.svelte-1onbytl{width:var(--size-20);height:var(--size-20);margin-left:calc(-1 * var(--size-8));margin-right:calc(-1 * var(--size-6))}.image-container.svelte-1onbytl img.svelte-1onbytl,.image-container.svelte-1onbytl video.svelte-1onbytl{width:100%;height:100%;object-fit:cover;border-radius:var(--radius-md)}.caption.svelte-1onbytl.svelte-1onbytl{position:absolute;bottom:0;left:0;right:0;background:#000000b3;color:#fff;padding:var(--spacing-xs);font-size:var(--text-xs);text-align:center;border-radius:0 0 var(--radius-md) var(--radius-md);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.container.table.svelte-1onbytl .caption.svelte-1onbytl{display:none}
|
src/demo/__init__.py
ADDED
|
File without changes
|
src/demo/app.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, List
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from gradio_folderexplorer import FolderExplorer
|
| 4 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 5 |
+
from gradio_mediagallery import MediaGallery
|
| 6 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Configuration constant for the root directory containing media files
|
| 10 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 11 |
+
|
| 12 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 13 |
+
"""
|
| 14 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 21 |
+
"""
|
| 22 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 23 |
+
return [gr.skip()] * len(output_fields)
|
| 24 |
+
|
| 25 |
+
return transfer_metadata(
|
| 26 |
+
output_fields=output_fields,
|
| 27 |
+
metadata=image_data._data,
|
| 28 |
+
remove_prefix_from_keys=True
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# UI layout and logic
|
| 32 |
+
with gr.Blocks() as demo:
|
| 33 |
+
"""
|
| 34 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 35 |
+
"""
|
| 36 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 37 |
+
gr.Markdown(
|
| 38 |
+
"""
|
| 39 |
+
**To Test:**
|
| 40 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 41 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 42 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 43 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 44 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 45 |
+
"""
|
| 46 |
+
)
|
| 47 |
+
with gr.Row(equal_height=True):
|
| 48 |
+
with gr.Column(scale=1, min_width=300):
|
| 49 |
+
folder_explorer = FolderExplorer(
|
| 50 |
+
label="Select a Folder",
|
| 51 |
+
root_dir=ROOT_DIR_PATH,
|
| 52 |
+
value=ROOT_DIR_PATH
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
with gr.Column(scale=3):
|
| 56 |
+
gallery = MediaGallery(
|
| 57 |
+
label="Media in Folder",
|
| 58 |
+
columns=6,
|
| 59 |
+
height="auto",
|
| 60 |
+
preview=False,
|
| 61 |
+
show_download_button=False,
|
| 62 |
+
only_custom_metadata=False,
|
| 63 |
+
popup_metadata_width="40%",
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
gr.Markdown("## Metadata Viewer")
|
| 67 |
+
with gr.Row():
|
| 68 |
+
model_box = gr.Textbox(label="Model")
|
| 69 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 70 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 71 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 72 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 73 |
+
|
| 74 |
+
# Event handling
|
| 75 |
+
output_fields = [
|
| 76 |
+
model_box,
|
| 77 |
+
fnumber_box,
|
| 78 |
+
iso_box,
|
| 79 |
+
s_churn,
|
| 80 |
+
description_box
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
# Populate the gallery when the folder changes
|
| 84 |
+
folder_explorer.change(
|
| 85 |
+
fn=load_media_from_folder,
|
| 86 |
+
inputs=folder_explorer,
|
| 87 |
+
outputs=gallery
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Populate the gallery on initial load
|
| 91 |
+
demo.load(
|
| 92 |
+
fn=load_media_from_folder,
|
| 93 |
+
inputs=folder_explorer,
|
| 94 |
+
outputs=gallery
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Handle the load_metadata event from MediaGallery
|
| 98 |
+
gallery.load_metadata(
|
| 99 |
+
fn=handle_load_metadata,
|
| 100 |
+
inputs=None,
|
| 101 |
+
outputs=output_fields
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
"""
|
| 106 |
+
Launches the Gradio interface in debug mode.
|
| 107 |
+
"""
|
| 108 |
+
demo.launch(debug=True)
|
src/demo/css.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html {
|
| 2 |
+
font-family: Inter;
|
| 3 |
+
font-size: 16px;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
-webkit-text-size-adjust: 100%;
|
| 7 |
+
background: #fff;
|
| 8 |
+
color: #323232;
|
| 9 |
+
-webkit-font-smoothing: antialiased;
|
| 10 |
+
-moz-osx-font-smoothing: grayscale;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--space: 1;
|
| 16 |
+
--vspace: calc(var(--space) * 1rem);
|
| 17 |
+
--vspace-0: calc(3 * var(--space) * 1rem);
|
| 18 |
+
--vspace-1: calc(2 * var(--space) * 1rem);
|
| 19 |
+
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
| 20 |
+
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.app {
|
| 24 |
+
max-width: 748px !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prose p {
|
| 28 |
+
margin: var(--vspace) 0;
|
| 29 |
+
line-height: var(--vspace * 2);
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
code {
|
| 34 |
+
font-family: "Inconsolata", sans-serif;
|
| 35 |
+
font-size: 16px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1,
|
| 39 |
+
h1 code {
|
| 40 |
+
font-weight: 400;
|
| 41 |
+
line-height: calc(2.5 / var(--space) * var(--vspace));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 code {
|
| 45 |
+
background: none;
|
| 46 |
+
border: none;
|
| 47 |
+
letter-spacing: 0.05em;
|
| 48 |
+
padding-bottom: 5px;
|
| 49 |
+
position: relative;
|
| 50 |
+
padding: 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h2 {
|
| 54 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 55 |
+
line-height: 1em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h3,
|
| 59 |
+
h3 code {
|
| 60 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 61 |
+
line-height: 1em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h4,
|
| 65 |
+
h5,
|
| 66 |
+
h6 {
|
| 67 |
+
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
| 68 |
+
line-height: var(--vspace);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.bigtitle,
|
| 72 |
+
h1,
|
| 73 |
+
h1 code {
|
| 74 |
+
font-size: calc(8px * 4.5);
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.title,
|
| 79 |
+
h2,
|
| 80 |
+
h2 code {
|
| 81 |
+
font-size: calc(8px * 3.375);
|
| 82 |
+
font-weight: lighter;
|
| 83 |
+
word-break: break-word;
|
| 84 |
+
border: none;
|
| 85 |
+
background: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.subheading1,
|
| 89 |
+
h3,
|
| 90 |
+
h3 code {
|
| 91 |
+
font-size: calc(8px * 1.8);
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
border: none;
|
| 94 |
+
background: none;
|
| 95 |
+
letter-spacing: 0.1em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
h2 code {
|
| 100 |
+
padding: 0;
|
| 101 |
+
position: relative;
|
| 102 |
+
letter-spacing: 0.05em;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
blockquote {
|
| 106 |
+
font-size: calc(8px * 1.1667);
|
| 107 |
+
font-style: italic;
|
| 108 |
+
line-height: calc(1.1667 * var(--vspace));
|
| 109 |
+
margin: var(--vspace-2) var(--vspace-2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.subheading2,
|
| 113 |
+
h4 {
|
| 114 |
+
font-size: calc(8px * 1.4292);
|
| 115 |
+
text-transform: uppercase;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.subheading3,
|
| 120 |
+
h5 {
|
| 121 |
+
font-size: calc(8px * 1.2917);
|
| 122 |
+
line-height: calc(1.2917 * var(--vspace));
|
| 123 |
+
|
| 124 |
+
font-weight: lighter;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
letter-spacing: 0.15em;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h6 {
|
| 130 |
+
font-size: calc(8px * 1.1667);
|
| 131 |
+
font-size: 1.1667em;
|
| 132 |
+
font-weight: normal;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
font-family: "le-monde-livre-classic-byol", serif !important;
|
| 135 |
+
letter-spacing: 0px !important;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#start .md > *:first-child {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
h2 + h3 {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.md hr {
|
| 147 |
+
border: none;
|
| 148 |
+
border-top: 1px solid var(--block-border-color);
|
| 149 |
+
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
| 150 |
+
}
|
| 151 |
+
.prose ul {
|
| 152 |
+
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.gap {
|
| 156 |
+
gap: 0;
|
| 157 |
+
}
|
src/demo/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio_mediagallery
|
| 2 |
+
gradio_folderexplorer
|
src/demo/space.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from app import demo as app
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
_docs = {'MediaGallery': {'description': 'Creates a gallery component that allows displaying a grid of images or videos, and optionally captions. If used as an input, the user can upload images or videos to the gallery.\nIf used as an output, the user can click on individual images or videos to view them at a higher resolution.\n', 'members': {'__init__': {'value': {'type': 'Sequence[\n np.ndarray | PIL.Image.Image | str | Path | tuple\n ]\n | Callable\n | None', 'default': 'None', 'description': 'List of images or videos to display in the gallery by default. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'file_types': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of file extensions or types of files to be uploaded (e.g. [\'image\', \'.mp4\']), when this is used as an input component. "image" allows only image files to be uploaded, "video" allows only video files to be uploaded, ".mp4" allows only mp4 files to be uploaded, etc. If None, any image and video files types are allowed.'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'visible': {'type': 'bool | Literal["hidden"]', 'default': 'True', 'description': 'If False, component will be hidden. If "hidden", component will be visually hidden and not take up space in the layout but still exist in the DOM'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'columns': {'type': 'int | None', 'default': '2', 'description': 'Represents the number of images that should be shown in one row.'}, 'rows': {'type': 'int | None', 'default': 'None', 'description': 'Represents the number of rows in the image grid.'}, 'height': {'type': 'int | float | str | None', 'default': 'None', 'description': 'The height of the gallery component, specified in pixels if a number is passed, or in CSS units if a string is passed. If more images are displayed than can fit in the height, a scrollbar will appear.'}, 'allow_preview': {'type': 'bool', 'default': 'True', 'description': 'If True, images in the gallery will be enlarged when they are clicked. Default is True.'}, 'preview': {'type': 'bool | None', 'default': 'None', 'description': 'If True, MediaGallery will start in preview mode, which shows all of the images as thumbnails and allows the user to click on them to view them in full size. Only works if allow_preview is True.'}, 'selected_index': {'type': 'int | None', 'default': 'None', 'description': 'The index of the image that should be initially selected. If None, no image will be selected at start. If provided, will set MediaGallery to preview mode unless allow_preview is set to False.'}, 'object_fit': {'type': 'Literal[\n "contain", "cover", "fill", "none", "scale-down"\n ]\n | None', 'default': 'None', 'description': 'CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_download_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a download button in the corner of the selected image. If False, the icon does not appear. Default is True.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"filepath"', 'description': 'The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the gallery in fullscreen mode. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, the metadata popup will filter out common technical EXIF data (like ImageWidth, ColorType, etc.), showing only custom or descriptive metadata.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '500', 'description': 'The width of the metadata popup modal, specified in pixels (e.g., 500) or as a CSS string (e.g., "50%").'}}, 'postprocess': {'value': {'type': 'list | None', 'description': "The output data received by the component from the user's function in the backend."}}, 'preprocess': {'return': {'type': 'Any', 'description': "The preprocessed input data sent to the user's function in the backend."}, 'value': None}}, 'events': {'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the MediaGallery. Uses event data gradio.SelectData to carry `value` referring to the label of the MediaGallery, and `selected` to refer to state of the MediaGallery. See EventData documentation on how to use this event data'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the MediaGallery changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'delete': {'type': None, 'default': None, 'description': 'This listener is triggered when the user deletes and item from the MediaGallery. Uses event data gradio.DeletedFileData to carry `value` referring to the file that was deleted as an instance of FileData. See EventData documentation on how to use this event data'}, 'preview_close': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is closed by the user'}, 'preview_open': {'type': None, 'default': None, 'description': 'This event is triggered when the MediaGallery preview is opened by the user'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button in the metadata popup. The event data will be a dictionary containing the image metadata."}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'MediaGallery': []}}}
|
| 7 |
+
|
| 8 |
+
abs_path = os.path.join(os.path.dirname(__file__), "css.css")
|
| 9 |
+
|
| 10 |
+
with gr.Blocks(
|
| 11 |
+
css=abs_path,
|
| 12 |
+
theme=gr.themes.Ocean(
|
| 13 |
+
font_mono=[
|
| 14 |
+
gr.themes.GoogleFont("Inconsolata"),
|
| 15 |
+
"monospace",
|
| 16 |
+
],
|
| 17 |
+
),
|
| 18 |
+
) as demo:
|
| 19 |
+
gr.Markdown(
|
| 20 |
+
"""
|
| 21 |
+
# `gradio_mediagallery`
|
| 22 |
+
|
| 23 |
+
<div style="display: flex; gap: 7px;">
|
| 24 |
+
<a href="https://pypi.org/project/gradio_mediagallery/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_mediagallery"></a>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
Python library for easily interacting with trained machine learning models
|
| 28 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 29 |
+
app.render()
|
| 30 |
+
gr.Markdown(
|
| 31 |
+
"""
|
| 32 |
+
## Installation
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install gradio_mediagallery
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from typing import Any, List
|
| 42 |
+
import gradio as gr
|
| 43 |
+
from gradio_folderexplorer import FolderExplorer
|
| 44 |
+
from gradio_folderexplorer.helpers import load_media_from_folder
|
| 45 |
+
from gradio_mediagallery import MediaGallery
|
| 46 |
+
from gradio_mediagallery.helpers import extract_metadata, transfer_metadata
|
| 47 |
+
import os
|
| 48 |
+
|
| 49 |
+
# Configuration constant for the root directory containing media files
|
| 50 |
+
ROOT_DIR_PATH = "./src/examples"
|
| 51 |
+
|
| 52 |
+
def handle_load_metadata(image_data: gr.EventData) -> List[Any]:
|
| 53 |
+
\"\"\"
|
| 54 |
+
Processes image metadata by calling the `transfer_metadata` helper.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
image_data (gr.EventData): Event data containing metadata from the MediaGallery component.
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
List[Any]: A list of values to populate the output fields, or skipped updates if no data is provided.
|
| 61 |
+
\"\"\"
|
| 62 |
+
if not image_data or not hasattr(image_data, "_data"):
|
| 63 |
+
return [gr.skip()] * len(output_fields)
|
| 64 |
+
|
| 65 |
+
return transfer_metadata(
|
| 66 |
+
output_fields=output_fields,
|
| 67 |
+
metadata=image_data._data,
|
| 68 |
+
remove_prefix_from_keys=True
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# UI layout and logic
|
| 72 |
+
with gr.Blocks() as demo:
|
| 73 |
+
\"\"\"
|
| 74 |
+
A Gradio interface for browsing and displaying media files with metadata extraction.
|
| 75 |
+
\"\"\"
|
| 76 |
+
gr.Markdown("# MediaGallery with Metadata Extraction")
|
| 77 |
+
gr.Markdown(
|
| 78 |
+
\"\"\"
|
| 79 |
+
**To Test:**
|
| 80 |
+
1. Use the **FolderExplorer** on the left to select a folder containing images with metadata.
|
| 81 |
+
2. Click on an image in the **Media Gallery** to open the preview mode.
|
| 82 |
+
3. In the preview toolbar, click the 'Info' icon (ⓘ) to open the metadata popup.
|
| 83 |
+
4. Click the **'Load Metadata'** button inside the popup.
|
| 84 |
+
5. The fields in the **Metadata Viewer** below will be populated with the data from the image.
|
| 85 |
+
\"\"\"
|
| 86 |
+
)
|
| 87 |
+
with gr.Row(equal_height=True):
|
| 88 |
+
with gr.Column(scale=1, min_width=300):
|
| 89 |
+
folder_explorer = FolderExplorer(
|
| 90 |
+
label="Select a Folder",
|
| 91 |
+
root_dir=ROOT_DIR_PATH,
|
| 92 |
+
value=ROOT_DIR_PATH
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
with gr.Column(scale=3):
|
| 96 |
+
gallery = MediaGallery(
|
| 97 |
+
label="Media in Folder",
|
| 98 |
+
columns=6,
|
| 99 |
+
height="auto",
|
| 100 |
+
preview=False,
|
| 101 |
+
show_download_button=False,
|
| 102 |
+
only_custom_metadata=False,
|
| 103 |
+
popup_metadata_width="40%",
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
gr.Markdown("## Metadata Viewer")
|
| 107 |
+
with gr.Row():
|
| 108 |
+
model_box = gr.Textbox(label="Model")
|
| 109 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 110 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 111 |
+
s_churn = gr.Slider(label="Schurn", minimum=0.0, maximum=1.0, step=0.01)
|
| 112 |
+
description_box = gr.Textbox(label="Description", lines=2)
|
| 113 |
+
|
| 114 |
+
# Event handling
|
| 115 |
+
output_fields = [
|
| 116 |
+
model_box,
|
| 117 |
+
fnumber_box,
|
| 118 |
+
iso_box,
|
| 119 |
+
s_churn,
|
| 120 |
+
description_box
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
# Populate the gallery when the folder changes
|
| 124 |
+
folder_explorer.change(
|
| 125 |
+
fn=load_media_from_folder,
|
| 126 |
+
inputs=folder_explorer,
|
| 127 |
+
outputs=gallery
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Populate the gallery on initial load
|
| 131 |
+
demo.load(
|
| 132 |
+
fn=load_media_from_folder,
|
| 133 |
+
inputs=folder_explorer,
|
| 134 |
+
outputs=gallery
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# Handle the load_metadata event from MediaGallery
|
| 138 |
+
gallery.load_metadata(
|
| 139 |
+
fn=handle_load_metadata,
|
| 140 |
+
inputs=None,
|
| 141 |
+
outputs=output_fields
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
\"\"\"
|
| 146 |
+
Launches the Gradio interface in debug mode.
|
| 147 |
+
\"\"\"
|
| 148 |
+
demo.launch(debug=True)
|
| 149 |
+
```
|
| 150 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
gr.Markdown("""
|
| 154 |
+
## `MediaGallery`
|
| 155 |
+
|
| 156 |
+
### Initialization
|
| 157 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 158 |
+
|
| 159 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["members"]["__init__"], linkify=[])
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
gr.Markdown("### Events")
|
| 163 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["events"], linkify=['Event'])
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
gr.Markdown("""
|
| 169 |
+
|
| 170 |
+
### User function
|
| 171 |
+
|
| 172 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 173 |
+
|
| 174 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 175 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 176 |
+
|
| 177 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 178 |
+
|
| 179 |
+
- **As input:** Is passed, the preprocessed input data sent to the user's function in the backend.
|
| 180 |
+
- **As output:** Should return, the output data received by the component from the user's function in the backend.
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
def predict(
|
| 184 |
+
value: Any
|
| 185 |
+
) -> list | None:
|
| 186 |
+
return value
|
| 187 |
+
```
|
| 188 |
+
""", elem_classes=["md-custom", "MediaGallery-user-fn"], header_links=True)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
demo.load(None, js=r"""function() {
|
| 194 |
+
const refs = {};
|
| 195 |
+
const user_fn_refs = {
|
| 196 |
+
MediaGallery: [], };
|
| 197 |
+
requestAnimationFrame(() => {
|
| 198 |
+
|
| 199 |
+
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
| 200 |
+
if (refs.length > 0) {
|
| 201 |
+
const el = document.querySelector(`.${key}-user-fn`);
|
| 202 |
+
if (!el) return;
|
| 203 |
+
refs.forEach(ref => {
|
| 204 |
+
el.innerHTML = el.innerHTML.replace(
|
| 205 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 206 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 207 |
+
);
|
| 208 |
+
})
|
| 209 |
+
}
|
| 210 |
+
})
|
| 211 |
+
|
| 212 |
+
Object.entries(refs).forEach(([key, refs]) => {
|
| 213 |
+
if (refs.length > 0) {
|
| 214 |
+
const el = document.querySelector(`.${key}`);
|
| 215 |
+
if (!el) return;
|
| 216 |
+
refs.forEach(ref => {
|
| 217 |
+
el.innerHTML = el.innerHTML.replace(
|
| 218 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 219 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 220 |
+
);
|
| 221 |
+
})
|
| 222 |
+
}
|
| 223 |
+
})
|
| 224 |
+
})
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
""")
|
| 228 |
+
|
| 229 |
+
demo.launch()
|
src/examples/folder1/Jensen.jpeg
ADDED
|
Git LFS Details
|
src/examples/folder1/newton_0.jpg
ADDED
|
Git LFS Details
|
src/examples/folder1/newton_2.png
ADDED
|
Git LFS Details
|
src/examples/folder1/newton_3.jpg
ADDED
|
src/examples/folder1/sam1.jpg
ADDED
|
Git LFS Details
|
src/examples/folder2/SampleVideo 720x480.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:91a5373da22bc8f34aa5664588b4728f88eeddb42abbfff362c7eb822a2b94cb
|
| 3 |
+
size 186695
|
src/examples/folder2/butterfly_input.jpg
ADDED
|
Git LFS Details
|
src/examples/folder2/lemons_input.jpg
ADDED
|
Git LFS Details
|
src/examples/folder2/vermeer.jpg
ADDED
|
src/examples/image_with_meta.png
ADDED
|
Git LFS Details
|
src/frontend/Example.svelte
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { GalleryImage, GalleryVideo } from "./types";
|
| 3 |
+
|
| 4 |
+
export let value: (GalleryImage | GalleryVideo)[] | null;
|
| 5 |
+
export let type: "gallery" | "table";
|
| 6 |
+
export let selected = false;
|
| 7 |
+
</script>
|
| 8 |
+
|
| 9 |
+
<div
|
| 10 |
+
class="container"
|
| 11 |
+
class:table={type === "table"}
|
| 12 |
+
class:gallery={type === "gallery"}
|
| 13 |
+
class:selected
|
| 14 |
+
>
|
| 15 |
+
{#if value && value.length > 0}
|
| 16 |
+
<div class="images-wrapper">
|
| 17 |
+
{#each value.slice(0, 3) as item}
|
| 18 |
+
{#if "image" in item && item.image}
|
| 19 |
+
<div class="image-container">
|
| 20 |
+
<img src={item.image.url} alt={item.caption || ""} />
|
| 21 |
+
{#if item.caption}
|
| 22 |
+
<span class="caption">{item.caption}</span>
|
| 23 |
+
{/if}
|
| 24 |
+
</div>
|
| 25 |
+
{:else if "video" in item && item.video}
|
| 26 |
+
<div class="image-container">
|
| 27 |
+
<video
|
| 28 |
+
src={item.video.url}
|
| 29 |
+
controls={false}
|
| 30 |
+
muted
|
| 31 |
+
preload="metadata"
|
| 32 |
+
/>
|
| 33 |
+
{#if item.caption}
|
| 34 |
+
<span class="caption">{item.caption}</span>
|
| 35 |
+
{/if}
|
| 36 |
+
</div>
|
| 37 |
+
{/if}
|
| 38 |
+
{/each}
|
| 39 |
+
{#if value.length > 3}
|
| 40 |
+
<div class="more-indicator">…</div>
|
| 41 |
+
{/if}
|
| 42 |
+
</div>
|
| 43 |
+
{/if}
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<style>
|
| 47 |
+
.container {
|
| 48 |
+
border-radius: var(--radius-lg);
|
| 49 |
+
overflow: hidden;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.container.selected {
|
| 53 |
+
border: 2px solid var(--border-color-accent);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.images-wrapper {
|
| 57 |
+
display: flex;
|
| 58 |
+
gap: var(--spacing-sm);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.container.table .images-wrapper {
|
| 62 |
+
flex-direction: row;
|
| 63 |
+
align-items: center;
|
| 64 |
+
padding: var(--spacing-sm);
|
| 65 |
+
border: 1px solid var(--border-color-primary);
|
| 66 |
+
border-radius: var(--radius-lg);
|
| 67 |
+
background: var(--background-fill-secondary);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.container.gallery .images-wrapper {
|
| 71 |
+
flex-direction: row;
|
| 72 |
+
gap: 0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.image-container {
|
| 76 |
+
position: relative;
|
| 77 |
+
flex-shrink: 0;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.container.table .image-container {
|
| 81 |
+
width: var(--size-12);
|
| 82 |
+
height: var(--size-12);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.container.gallery .image-container {
|
| 86 |
+
width: var(--size-20);
|
| 87 |
+
height: var(--size-20);
|
| 88 |
+
margin-left: calc(-1 * var(--size-8));
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.container.gallery .image-container:first-child {
|
| 92 |
+
margin-left: 0;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.more-indicator {
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: center;
|
| 98 |
+
justify-content: center;
|
| 99 |
+
font-size: var(--text-lg);
|
| 100 |
+
font-weight: bold;
|
| 101 |
+
color: var(--border-color-primary);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.container.table .more-indicator {
|
| 105 |
+
width: var(--size-12);
|
| 106 |
+
height: var(--size-12);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.container.gallery .more-indicator {
|
| 110 |
+
width: var(--size-20);
|
| 111 |
+
height: var(--size-20);
|
| 112 |
+
margin-left: calc(-1 * var(--size-8));
|
| 113 |
+
margin-right: calc(-1 * var(--size-6));
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.image-container img,
|
| 117 |
+
.image-container video {
|
| 118 |
+
width: 100%;
|
| 119 |
+
height: 100%;
|
| 120 |
+
object-fit: cover;
|
| 121 |
+
border-radius: var(--radius-md);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.caption {
|
| 125 |
+
position: absolute;
|
| 126 |
+
bottom: 0;
|
| 127 |
+
left: 0;
|
| 128 |
+
right: 0;
|
| 129 |
+
background: rgba(0, 0, 0, 0.7);
|
| 130 |
+
color: white;
|
| 131 |
+
padding: var(--spacing-xs);
|
| 132 |
+
font-size: var(--text-xs);
|
| 133 |
+
text-align: center;
|
| 134 |
+
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
| 135 |
+
overflow: hidden;
|
| 136 |
+
text-overflow: ellipsis;
|
| 137 |
+
white-space: nowrap;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.container.table .caption {
|
| 141 |
+
display: none;
|
| 142 |
+
}
|
| 143 |
+
</style>
|
src/frontend/Gallery.css
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:global(*::-webkit-scrollbar) {
|
| 2 |
+
width: 8px;
|
| 3 |
+
height: 8px;
|
| 4 |
+
background-color: transparent;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
:global(*::-webkit-scrollbar-track) {
|
| 8 |
+
background: transparent;
|
| 9 |
+
border-radius: 10px;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
:global(*::-webkit-scrollbar-thumb) {
|
| 13 |
+
background-color: rgba(136, 136, 136, 0.4);
|
| 14 |
+
border-radius: 10px;
|
| 15 |
+
border: 2px solid transparent;
|
| 16 |
+
background-clip: content-box;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
:global(*::-webkit-scrollbar-thumb:hover) {
|
| 20 |
+
background-color: rgba(136, 136, 136, 0.7);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* Para Firefox */
|
| 24 |
+
:global(html) {
|
| 25 |
+
scrollbar-width: thin;
|
| 26 |
+
scrollbar-color: rgba(136, 136, 136, 0.7) transparent;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* Styles for the gallery container, which holds the entire component.
|
| 31 |
+
*/
|
| 32 |
+
.gallery-container {
|
| 33 |
+
position: relative;
|
| 34 |
+
width: 100%;
|
| 35 |
+
height: 100%;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Styles for buttons to ensure they fill their container.
|
| 40 |
+
*/
|
| 41 |
+
button {
|
| 42 |
+
width: var(--size-full);
|
| 43 |
+
height: var(--size-full);
|
| 44 |
+
display: block;
|
| 45 |
+
border-radius: var(--radius-lg);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Styles for the preview mode, displaying a selected media item.
|
| 50 |
+
*/
|
| 51 |
+
.preview {
|
| 52 |
+
display: flex;
|
| 53 |
+
position: absolute;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
z-index: var(--layer-2);
|
| 56 |
+
border-radius: calc(var(--block-radius) - var(--block-border-width));
|
| 57 |
+
-webkit-backdrop-filter: blur(8px);
|
| 58 |
+
backdrop-filter: blur(8px);
|
| 59 |
+
width: var(--size-full);
|
| 60 |
+
height: var(--size-full);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.preview:focus-visible {
|
| 64 |
+
outline: none;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.preview.minimal {
|
| 68 |
+
width: fit-content;
|
| 69 |
+
height: fit-content;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.preview::before {
|
| 73 |
+
content: "";
|
| 74 |
+
position: absolute;
|
| 75 |
+
z-index: var(--layer-below);
|
| 76 |
+
background: var(--background-fill-primary);
|
| 77 |
+
opacity: 0.9;
|
| 78 |
+
width: var(--size-full);
|
| 79 |
+
height: var(--size-full);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Styles for the grid wrapper with fixed height constraints.
|
| 84 |
+
*/
|
| 85 |
+
.fixed-height {
|
| 86 |
+
min-height: var(--size-80);
|
| 87 |
+
max-height: 65vh;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
@media (--screen-xl) {
|
| 91 |
+
.fixed-height {
|
| 92 |
+
min-height: 450px;
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Styles for the media container in preview mode.
|
| 98 |
+
*/
|
| 99 |
+
.media-container {
|
| 100 |
+
height: calc(100% - var(--size-14));
|
| 101 |
+
width: 100%;
|
| 102 |
+
display: flex;
|
| 103 |
+
align-items: center;
|
| 104 |
+
justify-content: center;
|
| 105 |
+
cursor: pointer;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.media-container :global(img),
|
| 109 |
+
.media-container :global(video) {
|
| 110 |
+
max-width: 100%;
|
| 111 |
+
max-height: 100%;
|
| 112 |
+
object-fit: contain;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/**
|
| 116 |
+
* Styles for thumbnails in the preview mode carousel.
|
| 117 |
+
*/
|
| 118 |
+
.thumbnails :global(img) {
|
| 119 |
+
object-fit: cover;
|
| 120 |
+
width: var(--size-full);
|
| 121 |
+
height: var(--size-full);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.thumbnails :global(svg) {
|
| 125 |
+
position: absolute;
|
| 126 |
+
top: var(--size-2);
|
| 127 |
+
left: var(--size-2);
|
| 128 |
+
width: 50%;
|
| 129 |
+
height: 50%;
|
| 130 |
+
opacity: 50%;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/**
|
| 134 |
+
* Styles for captions in preview mode.
|
| 135 |
+
*/
|
| 136 |
+
.caption {
|
| 137 |
+
padding: var(--size-2) var(--size-3);
|
| 138 |
+
overflow: hidden;
|
| 139 |
+
color: var(--block-label-text-color);
|
| 140 |
+
font-weight: var(--weight-semibold);
|
| 141 |
+
text-align: center;
|
| 142 |
+
text-overflow: ellipsis;
|
| 143 |
+
white-space: nowrap;
|
| 144 |
+
align-self: center;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Styles for the thumbnails carousel in preview mode.
|
| 149 |
+
*/
|
| 150 |
+
.thumbnails {
|
| 151 |
+
display: flex;
|
| 152 |
+
position: absolute;
|
| 153 |
+
bottom: 0;
|
| 154 |
+
justify-content: flex-start;
|
| 155 |
+
align-items: center;
|
| 156 |
+
gap: var(--spacing-lg);
|
| 157 |
+
width: var(--size-full);
|
| 158 |
+
height: var(--size-14);
|
| 159 |
+
overflow-x: scroll;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* Styles for individual thumbnail items.
|
| 164 |
+
*/
|
| 165 |
+
.thumbnail-item {
|
| 166 |
+
--ring-color: transparent;
|
| 167 |
+
position: relative;
|
| 168 |
+
box-shadow: inset 0 0 0 1px var(--ring-color), var(--shadow-drop);
|
| 169 |
+
border: 1px solid var(--border-color-primary);
|
| 170 |
+
border-radius: var(--button-small-radius);
|
| 171 |
+
background: var(--background-fill-secondary);
|
| 172 |
+
aspect-ratio: var(--ratio-square);
|
| 173 |
+
width: var(--size-full);
|
| 174 |
+
height: var(--size-full);
|
| 175 |
+
overflow: clip;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.thumbnail-item:hover {
|
| 179 |
+
--ring-color: var(--color-accent);
|
| 180 |
+
border-color: var(--color-accent);
|
| 181 |
+
filter: brightness(1.1);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.thumbnail-item.selected {
|
| 185 |
+
--ring-color: var(--color-accent);
|
| 186 |
+
border-color: var(--color-accent);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.thumbnail-item :global(svg) {
|
| 190 |
+
position: absolute;
|
| 191 |
+
top: 50%;
|
| 192 |
+
left: 50%;
|
| 193 |
+
width: 50%;
|
| 194 |
+
height: 50%;
|
| 195 |
+
opacity: 50%;
|
| 196 |
+
transform: translate(-50%, -50%);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.thumbnail-item :global(video) {
|
| 200 |
+
width: var(--size-full);
|
| 201 |
+
height: var(--size-full);
|
| 202 |
+
overflow: hidden;
|
| 203 |
+
object-fit: cover;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/**
|
| 207 |
+
* Styles for small thumbnails in the preview carousel.
|
| 208 |
+
*/
|
| 209 |
+
.thumbnail-small {
|
| 210 |
+
flex: none;
|
| 211 |
+
transform: scale(0.9);
|
| 212 |
+
transition: 0.075s;
|
| 213 |
+
width: var(--size-9);
|
| 214 |
+
height: var(--size-9);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.thumbnail-small.selected {
|
| 218 |
+
--ring-color: var(--color-accent);
|
| 219 |
+
transform: scale(1);
|
| 220 |
+
border-color: var(--color-accent);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/**
|
| 224 |
+
* Styles for the grid wrapper containing the gallery items.
|
| 225 |
+
*/
|
| 226 |
+
.grid-wrap {
|
| 227 |
+
position: relative;
|
| 228 |
+
padding: var(--size-2);
|
| 229 |
+
overflow-y: auto;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.grid-wrap.fixed-height {
|
| 233 |
+
min-height: var(--size-80);
|
| 234 |
+
max-height: 65vh;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/**
|
| 238 |
+
* Styles for the grid container for multiple items.
|
| 239 |
+
*/
|
| 240 |
+
.grid-container {
|
| 241 |
+
display: grid;
|
| 242 |
+
position: relative;
|
| 243 |
+
grid-template-rows: repeat(var(--grid-rows), minmax(100px, 1fr));
|
| 244 |
+
grid-template-columns: repeat(var(--grid-cols), minmax(100px, 1fr));
|
| 245 |
+
grid-auto-rows: minmax(100px, 1fr);
|
| 246 |
+
gap: var(--spacing-lg);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
/**
|
| 250 |
+
* Styles for single-item view wrapper.
|
| 251 |
+
*/
|
| 252 |
+
.single-item-wrapper {
|
| 253 |
+
display: flex;
|
| 254 |
+
align-items: center;
|
| 255 |
+
justify-content: center;
|
| 256 |
+
width: 100%;
|
| 257 |
+
height: 100%;
|
| 258 |
+
padding: var(--spacing-xxl);
|
| 259 |
+
box-sizing: border-box;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.single-item-wrapper .gallery-item-with-name {
|
| 263 |
+
width: 100%;
|
| 264 |
+
height: 100%;
|
| 265 |
+
max-width: min(300px, 80vw);
|
| 266 |
+
max-height: min(320px, calc(80vh - var(--size-4)));
|
| 267 |
+
display: flex;
|
| 268 |
+
flex-direction: column;
|
| 269 |
+
align-items: center;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.single-item-wrapper .gallery-item {
|
| 273 |
+
width: 100%;
|
| 274 |
+
height: 100%;
|
| 275 |
+
max-width: 100%;
|
| 276 |
+
max-height: 100%;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.single-item-wrapper .thumbnail-item.thumbnail-lg {
|
| 280 |
+
display: flex !important;
|
| 281 |
+
align-items: center !important;
|
| 282 |
+
justify-content: center !important;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.single-item-wrapper .thumbnail-filename {
|
| 286 |
+
height: var(--size-4);
|
| 287 |
+
line-height: var(--size-4);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.single-item-wrapper .thumbnail-lg > :global(img),
|
| 291 |
+
.single-item-wrapper .thumbnail-lg > :global(video) {
|
| 292 |
+
object-fit: var(--object-fit) !important;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/**
|
| 296 |
+
* Styles for large thumbnails in the grid or single-item view.
|
| 297 |
+
*/
|
| 298 |
+
.thumbnail-lg > :global(img),
|
| 299 |
+
.thumbnail-lg > :global(video) {
|
| 300 |
+
width: var(--size-full);
|
| 301 |
+
height: var(--size-full);
|
| 302 |
+
overflow: hidden;
|
| 303 |
+
object-fit: var(--object-fit);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.thumbnail-lg:hover .caption-label {
|
| 307 |
+
opacity: 0.5;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
/**
|
| 311 |
+
* Styles for captions in the grid or single-item view.
|
| 312 |
+
*/
|
| 313 |
+
.caption-label {
|
| 314 |
+
position: absolute;
|
| 315 |
+
right: var(--block-label-margin);
|
| 316 |
+
bottom: var(--block-label-margin);
|
| 317 |
+
z-index: var(--layer-1);
|
| 318 |
+
border-top: 1px solid var(--border-color-primary);
|
| 319 |
+
border-left: 1px solid var(--border-color-primary);
|
| 320 |
+
border-radius: var(--block-label-radius);
|
| 321 |
+
background: var(--background-fill-secondary);
|
| 322 |
+
padding: var(--block-label-padding);
|
| 323 |
+
max-width: 80%;
|
| 324 |
+
overflow: hidden;
|
| 325 |
+
font-size: var(--block-label-text-size);
|
| 326 |
+
text-align: left;
|
| 327 |
+
text-overflow: ellipsis;
|
| 328 |
+
white-space: nowrap;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.grid-wrap.minimal {
|
| 332 |
+
padding: 0;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
/**
|
| 336 |
+
* Styles for gallery items with associated filenames.
|
| 337 |
+
*/
|
| 338 |
+
.gallery-item-with-name {
|
| 339 |
+
display: flex;
|
| 340 |
+
flex-direction: column;
|
| 341 |
+
gap: var(--size-1);
|
| 342 |
+
width: 100%;
|
| 343 |
+
height: 100%;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.thumbnail-filename {
|
| 347 |
+
font-size: var(--text-xs);
|
| 348 |
+
color: var(--body-text-color);
|
| 349 |
+
text-align: center;
|
| 350 |
+
width: 100%;
|
| 351 |
+
overflow: hidden;
|
| 352 |
+
text-overflow: ellipsis;
|
| 353 |
+
white-space: nowrap;
|
| 354 |
+
padding: 0 var(--size-1);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.gallery-item {
|
| 358 |
+
position: relative;
|
| 359 |
+
width: 100%;
|
| 360 |
+
height: 100%;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/**
|
| 364 |
+
* Styles for the metadata popup displayed in preview mode.
|
| 365 |
+
*/
|
| 366 |
+
.metadata-popup {
|
| 367 |
+
position: fixed;
|
| 368 |
+
top: 50%;
|
| 369 |
+
left: 50%;
|
| 370 |
+
transform: translate(-50%, -50%);
|
| 371 |
+
background: var(--background-fill-primary, white);
|
| 372 |
+
border: 1px solid var(--border-color-primary);
|
| 373 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 374 |
+
z-index: 1000;
|
| 375 |
+
border-radius: 8px;
|
| 376 |
+
max-width: min(90%, 600px);
|
| 377 |
+
max-height: min(50vh, calc(100% - 2rem));
|
| 378 |
+
min-height: 200px;
|
| 379 |
+
display: flex;
|
| 380 |
+
flex-direction: column;
|
| 381 |
+
pointer-events: auto;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.popup-content {
|
| 385 |
+
padding: 1rem;
|
| 386 |
+
display: flex;
|
| 387 |
+
flex-direction: column;
|
| 388 |
+
width: 100%;
|
| 389 |
+
box-sizing: border-box;
|
| 390 |
+
overflow-y: auto;
|
| 391 |
+
position: relative;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.close-button {
|
| 395 |
+
position: absolute;
|
| 396 |
+
top: 0.5rem;
|
| 397 |
+
right: 0.5rem;
|
| 398 |
+
background: none;
|
| 399 |
+
border: none;
|
| 400 |
+
font-size: 1.25rem;
|
| 401 |
+
cursor: pointer;
|
| 402 |
+
z-index: 20;
|
| 403 |
+
color: var(--body-text-color);
|
| 404 |
+
padding: 0.25rem;
|
| 405 |
+
line-height: 1;
|
| 406 |
+
width: 24px;
|
| 407 |
+
height: 24px;
|
| 408 |
+
text-align: center;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.popup-title {
|
| 412 |
+
font-weight: bold;
|
| 413 |
+
margin: 0 0 1rem 0;
|
| 414 |
+
flex-shrink: 0;
|
| 415 |
+
padding-right: 2.5rem;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.metadata-table-container {
|
| 419 |
+
flex-grow: 1;
|
| 420 |
+
overflow-y: auto;
|
| 421 |
+
max-height: calc(100% - 5rem);
|
| 422 |
+
min-height: 0;
|
| 423 |
+
margin-bottom: 1rem;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.metadata-table {
|
| 427 |
+
width: 100%;
|
| 428 |
+
border-collapse: collapse;
|
| 429 |
+
table-layout: auto;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.metadata-label {
|
| 433 |
+
background: var(--background-fill-secondary, #f5f5f5);
|
| 434 |
+
padding: 0.5rem;
|
| 435 |
+
font-weight: bold;
|
| 436 |
+
text-align: left;
|
| 437 |
+
vertical-align: top;
|
| 438 |
+
width: 35%;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.metadata-value {
|
| 442 |
+
text-align: left;
|
| 443 |
+
padding: 0.5rem;
|
| 444 |
+
white-space: pre-wrap;
|
| 445 |
+
word-break: break-all;
|
| 446 |
+
vertical-align: top;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.load-metadata-button {
|
| 450 |
+
margin-top: 1rem;
|
| 451 |
+
padding: 0.5rem 1rem;
|
| 452 |
+
background-color: var(--button-primary-background-fill);
|
| 453 |
+
color: var(--button-primary-text-color);
|
| 454 |
+
border: none;
|
| 455 |
+
border-radius: 4px;
|
| 456 |
+
cursor: pointer;
|
| 457 |
+
align-self: center;
|
| 458 |
+
flex-shrink: 0;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.load-metadata-button:hover {
|
| 462 |
+
background-color: var(--button-primary-background-fill-hover);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.no-metadata-message {
|
| 466 |
+
flex-grow: 1;
|
| 467 |
+
display: flex;
|
| 468 |
+
align-items: center;
|
| 469 |
+
justify-content: center;
|
| 470 |
+
color: var(--body-text-color-subdued);
|
| 471 |
+
}
|
src/frontend/Index.svelte
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script context="module" lang="ts">
|
| 2 |
+
export { default as BaseGallery } from "./shared/Gallery.svelte";
|
| 3 |
+
export { default as BaseExample } from "./Example.svelte";
|
| 4 |
+
</script>
|
| 5 |
+
|
| 6 |
+
<script lang="ts">
|
| 7 |
+
import type { GalleryImage, GalleryVideo } from "./types";
|
| 8 |
+
import type { Gradio, SelectData } from "@gradio/utils";
|
| 9 |
+
import { Block, Empty } from "@gradio/atoms";
|
| 10 |
+
import Gallery from "./shared/Gallery.svelte";
|
| 11 |
+
import type { LoadingStatus } from "@gradio/statustracker";
|
| 12 |
+
import { StatusTracker } from "@gradio/statustracker";
|
| 13 |
+
import { Image } from "@gradio/icons";
|
| 14 |
+
|
| 15 |
+
type GalleryData = GalleryImage | GalleryVideo;
|
| 16 |
+
|
| 17 |
+
export let loading_status: LoadingStatus;
|
| 18 |
+
export let show_label: boolean;
|
| 19 |
+
export let label: string;
|
| 20 |
+
export let elem_id = "";
|
| 21 |
+
export let elem_classes: string[] = [];
|
| 22 |
+
export let visible: boolean | "hidden" = true;
|
| 23 |
+
// O `value` agora é uma lista simples de arquivos
|
| 24 |
+
export let value: GalleryData[] | null = null;
|
| 25 |
+
export let container = true;
|
| 26 |
+
export let scale: number | null = null;
|
| 27 |
+
export let min_width: number | undefined = undefined;
|
| 28 |
+
export let columns: number | number[] | undefined = [5];
|
| 29 |
+
export let height: number | "auto" = "auto";
|
| 30 |
+
export let preview: boolean = true;
|
| 31 |
+
export let allow_preview = true;
|
| 32 |
+
export let selected_index: number | null = null;
|
| 33 |
+
export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" = "cover";
|
| 34 |
+
export let gradio: Gradio<{
|
| 35 |
+
change: typeof value;
|
| 36 |
+
select: SelectData;
|
| 37 |
+
share: ShareData;
|
| 38 |
+
error: string;
|
| 39 |
+
prop_change: Record<string, any>;
|
| 40 |
+
clear_status: LoadingStatus;
|
| 41 |
+
preview_open: never;
|
| 42 |
+
preview_close: never;
|
| 43 |
+
load_metadata: Record<string, any>;
|
| 44 |
+
}>;
|
| 45 |
+
export let show_fullscreen_button = true;
|
| 46 |
+
export let show_download_button = false;
|
| 47 |
+
export let show_share_button = false;
|
| 48 |
+
export let fullscreen = false;
|
| 49 |
+
export let popup_metadata_width: number | string = "50%";
|
| 50 |
+
import "./Gallery.css";
|
| 51 |
+
</script>
|
| 52 |
+
|
| 53 |
+
<Block
|
| 54 |
+
{visible}
|
| 55 |
+
variant={value === null || value.length === 0 ? "dashed" : "solid"}
|
| 56 |
+
padding={false}
|
| 57 |
+
{elem_id}
|
| 58 |
+
{elem_classes}
|
| 59 |
+
{container}
|
| 60 |
+
{scale}
|
| 61 |
+
{min_width}
|
| 62 |
+
allow_overflow={false}
|
| 63 |
+
height={typeof height === "number" ? height : undefined}
|
| 64 |
+
bind:fullscreen
|
| 65 |
+
>
|
| 66 |
+
<StatusTracker
|
| 67 |
+
autoscroll={gradio.autoscroll}
|
| 68 |
+
i18n={gradio.i18n}
|
| 69 |
+
{...loading_status}
|
| 70 |
+
/>
|
| 71 |
+
{#if value === null || value.length === 0}
|
| 72 |
+
<Empty unpadded_box={true} size="large"><Image /></Empty>
|
| 73 |
+
{:else}
|
| 74 |
+
<Gallery
|
| 75 |
+
on:change={() => gradio.dispatch("change", value)}
|
| 76 |
+
on:select={(e) => gradio.dispatch("select", e.detail)}
|
| 77 |
+
on:share={(e) => gradio.dispatch("share", e.detail)}
|
| 78 |
+
on:error={(e) => gradio.dispatch("error", e.detail)}
|
| 79 |
+
on:preview_open={() => gradio.dispatch("preview_open")}
|
| 80 |
+
on:preview_close={() => gradio.dispatch("preview_close")}
|
| 81 |
+
on:fullscreen={({ detail }) => {
|
| 82 |
+
fullscreen = detail;
|
| 83 |
+
}}
|
| 84 |
+
on:load_metadata={(e) => gradio.dispatch("load_metadata", e.detail)}
|
| 85 |
+
{label}
|
| 86 |
+
{show_label}
|
| 87 |
+
{columns}
|
| 88 |
+
height={"auto"}
|
| 89 |
+
{preview}
|
| 90 |
+
{object_fit}
|
| 91 |
+
{allow_preview}
|
| 92 |
+
bind:selected_index
|
| 93 |
+
bind:value
|
| 94 |
+
i18n={gradio.i18n}
|
| 95 |
+
_fetch={(...args) => gradio.client.fetch(...args)}
|
| 96 |
+
{show_fullscreen_button}
|
| 97 |
+
{show_download_button}
|
| 98 |
+
{show_share_button}
|
| 99 |
+
{fullscreen}
|
| 100 |
+
{popup_metadata_width}
|
| 101 |
+
|
| 102 |
+
/>
|
| 103 |
+
{/if}
|
| 104 |
+
</Block>
|
| 105 |
+
|
src/frontend/gradio.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: [],
|
| 3 |
+
svelte: {
|
| 4 |
+
preprocess: [],
|
| 5 |
+
},
|
| 6 |
+
build: {
|
| 7 |
+
target: "modules",
|
| 8 |
+
},
|
| 9 |
+
};
|
src/frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/frontend/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "gradio_mediagallery",
|
| 3 |
+
"version": "0.15.34",
|
| 4 |
+
"description": "Gradio UI packages",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"author": "",
|
| 7 |
+
"license": "ISC",
|
| 8 |
+
"private": false,
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@gradio/atoms": "0.18.1",
|
| 11 |
+
"@gradio/client": "1.19.1",
|
| 12 |
+
"@gradio/file": "0.13.0",
|
| 13 |
+
"@gradio/icons": "0.14.0",
|
| 14 |
+
"@gradio/image": "0.23.1",
|
| 15 |
+
"@gradio/statustracker": "0.11.1",
|
| 16 |
+
"@gradio/upload": "0.17.1",
|
| 17 |
+
"@gradio/utils": "0.10.2",
|
| 18 |
+
"@gradio/video": "0.16.0",
|
| 19 |
+
"dequal": "^2.0.2",
|
| 20 |
+
"exifr": "^7.1.3",
|
| 21 |
+
"marked": "^16.3.0"
|
| 22 |
+
},
|
| 23 |
+
"devDependencies": {
|
| 24 |
+
"@gradio/preview": "0.14.0"
|
| 25 |
+
},
|
| 26 |
+
"main": "./Index.svelte",
|
| 27 |
+
"main_changeset": true,
|
| 28 |
+
"exports": {
|
| 29 |
+
".": {
|
| 30 |
+
"gradio": "./Index.svelte",
|
| 31 |
+
"svelte": "./dist/Index.svelte",
|
| 32 |
+
"types": "./dist/Index.svelte.d.ts"
|
| 33 |
+
},
|
| 34 |
+
"./package.json": "./package.json",
|
| 35 |
+
"./base": {
|
| 36 |
+
"gradio": "./shared/Gallery.svelte",
|
| 37 |
+
"svelte": "./dist/shared/Gallery.svelte",
|
| 38 |
+
"types": "./dist/shared/Gallery.svelte.d.ts"
|
| 39 |
+
},
|
| 40 |
+
"./example": {
|
| 41 |
+
"gradio": "./Example.svelte",
|
| 42 |
+
"svelte": "./dist/Example.svelte",
|
| 43 |
+
"types": "./dist/Example.svelte.d.ts"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"peerDependencies": {
|
| 47 |
+
"svelte": "^4.0.0"
|
| 48 |
+
},
|
| 49 |
+
"repository": {
|
| 50 |
+
"type": "git",
|
| 51 |
+
"url": "git+https://github.com/gradio-app/gradio.git",
|
| 52 |
+
"directory": "js/gallery"
|
| 53 |
+
}
|
| 54 |
+
}
|
src/frontend/shared/Gallery.svelte
ADDED
|
@@ -0,0 +1,1099 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import {
|
| 3 |
+
BlockLabel,
|
| 4 |
+
Empty,
|
| 5 |
+
ShareButton,
|
| 6 |
+
IconButton,
|
| 7 |
+
IconButtonWrapper,
|
| 8 |
+
FullscreenButton,
|
| 9 |
+
} from "@gradio/atoms";
|
| 10 |
+
import type { SelectData } from "@gradio/utils";
|
| 11 |
+
import { Image } from "@gradio/image/shared";
|
| 12 |
+
import { Video } from "@gradio/video/shared";
|
| 13 |
+
import { dequal } from "dequal";
|
| 14 |
+
import { createEventDispatcher, onMount } from "svelte";
|
| 15 |
+
import { tick } from "svelte";
|
| 16 |
+
import type { GalleryImage, GalleryVideo } from "../types";
|
| 17 |
+
import { Download, Image as ImageIcon, Clear, Play, Info } from "@gradio/icons";
|
| 18 |
+
import { FileData } from "@gradio/client";
|
| 19 |
+
import { format_gallery_for_sharing } from "./utils";
|
| 20 |
+
import type { I18nFormatter } from "@gradio/utils";
|
| 21 |
+
import * as exifr from "exifr";
|
| 22 |
+
|
| 23 |
+
type GalleryData = GalleryImage | GalleryVideo;
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* @component Gallery
|
| 27 |
+
* @description A Svelte component for displaying a gallery of images or videos with optional preview mode, fullscreen support, and metadata popup for images.
|
| 28 |
+
*/
|
| 29 |
+
|
| 30 |
+
// Component props
|
| 31 |
+
/** @prop {boolean} show_label - Whether to display the gallery label. Defaults to true. */
|
| 32 |
+
export let show_label = true;
|
| 33 |
+
/** @prop {string} label - The label text for the gallery. */
|
| 34 |
+
export let label: string;
|
| 35 |
+
/** @prop {GalleryData[] | null} value - Array of gallery items (images or videos). */
|
| 36 |
+
export let value: GalleryData[] | null = null;
|
| 37 |
+
/** @prop {number | number[] | undefined} columns - Number of grid columns or array of column counts per breakpoint. Defaults to [2]. */
|
| 38 |
+
export let columns: number | number[] | undefined = [2];
|
| 39 |
+
/** @prop {number | number[] | undefined} rows - Number of grid rows or array of row counts per breakpoint. */
|
| 40 |
+
export let rows: number | number[] | undefined = undefined;
|
| 41 |
+
/** @prop {number | "auto"} height - Gallery height in pixels or "auto". Defaults to "auto". */
|
| 42 |
+
export let height: number | "auto" = "auto";
|
| 43 |
+
/** @prop {boolean} preview - Whether to start in preview mode if a value is provided. */
|
| 44 |
+
export let preview: boolean;
|
| 45 |
+
/** @prop {boolean} allow_preview - Whether preview mode is enabled. Defaults to true. */
|
| 46 |
+
export let allow_preview = true;
|
| 47 |
+
/** @prop {"contain" | "cover" | "fill" | "none" | "scale-down"} object_fit - CSS object-fit for media. Defaults to "cover". */
|
| 48 |
+
export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" = "cover";
|
| 49 |
+
/** @prop {boolean} show_share_button - Whether to show the share button. Defaults to false. */
|
| 50 |
+
export let show_share_button = false;
|
| 51 |
+
/** @prop {boolean} show_download_button - Whether to show the download button. Defaults to false. */
|
| 52 |
+
export let show_download_button = false;
|
| 53 |
+
/** @prop {I18nFormatter} i18n - Internationalization formatter for labels. */
|
| 54 |
+
export let i18n: I18nFormatter;
|
| 55 |
+
/** @prop {number | null} selected_index - Index of the selected media item. Defaults to null. */
|
| 56 |
+
export let selected_index: number | null = null;
|
| 57 |
+
/** @prop {boolean} interactive - Whether the gallery is interactive (not used). Defaults to false. */
|
| 58 |
+
export const interactive = false;
|
| 59 |
+
/** @prop {typeof fetch} _fetch - Fetch function for downloading files. */
|
| 60 |
+
export let _fetch: typeof fetch;
|
| 61 |
+
/** @prop {"normal" | "minimal"} mode - Display mode for the gallery. Defaults to "normal". */
|
| 62 |
+
export let mode: "normal" | "minimal" = "normal";
|
| 63 |
+
/** @prop {boolean} show_fullscreen_button - Whether to show the fullscreen button. Defaults to true. */
|
| 64 |
+
export let show_fullscreen_button = true;
|
| 65 |
+
/** @prop {boolean} display_icon_button_wrapper_top_corner - Whether to position icon buttons in the top corner. Defaults to false. */
|
| 66 |
+
export let display_icon_button_wrapper_top_corner = false;
|
| 67 |
+
/** @prop {boolean} fullscreen - Whether the gallery is in fullscreen mode. Defaults to false. */
|
| 68 |
+
export let fullscreen = false;
|
| 69 |
+
/** @prop {boolean} only_custom_metadata - Whether to show only custom metadata in the popup. Defaults to true. */
|
| 70 |
+
export let only_custom_metadata: boolean = true;
|
| 71 |
+
/** @prop {number | string} popup_metadata_width - Width of the metadata popup. Defaults to "50%". */
|
| 72 |
+
export let popup_metadata_width: number | string = "50%";
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
const dispatch = createEventDispatcher<{
|
| 76 |
+
change: undefined;
|
| 77 |
+
select: SelectData;
|
| 78 |
+
preview_open: undefined;
|
| 79 |
+
preview_close: undefined;
|
| 80 |
+
fullscreen: boolean;
|
| 81 |
+
error: string;
|
| 82 |
+
load_metadata: Record<string, any>;
|
| 83 |
+
}>();
|
| 84 |
+
|
| 85 |
+
// Gallery state
|
| 86 |
+
let is_full_screen = false;
|
| 87 |
+
let image_container: HTMLElement;
|
| 88 |
+
let was_reset = true;
|
| 89 |
+
let resolved_value: GalleryData[] | null = null;
|
| 90 |
+
let effective_columns: number | number[] | undefined = columns;
|
| 91 |
+
let prev_value: GalleryData[] | null = value;
|
| 92 |
+
let old_selected_index: number | null = selected_index;
|
| 93 |
+
let el: HTMLButtonElement[] = [];
|
| 94 |
+
let container_element: HTMLDivElement;
|
| 95 |
+
let thumbnails_overflow = false;
|
| 96 |
+
let preview_element: HTMLDivElement | null = null;
|
| 97 |
+
|
| 98 |
+
// Metadata state
|
| 99 |
+
let metadata: Record<string, any> | null = null;
|
| 100 |
+
let showMetadataPopup: boolean = false;
|
| 101 |
+
let is_extracting_metadata = false;
|
| 102 |
+
const technicalMetadata: string[] = [
|
| 103 |
+
"ImageWidth",
|
| 104 |
+
"ImageHeight",
|
| 105 |
+
"BitDepth",
|
| 106 |
+
"ColorType",
|
| 107 |
+
"Compression",
|
| 108 |
+
"Filter",
|
| 109 |
+
"Interlace",
|
| 110 |
+
];
|
| 111 |
+
|
| 112 |
+
$: filteredMetadata = only_custom_metadata && metadata
|
| 113 |
+
? Object.fromEntries(
|
| 114 |
+
Object.entries(metadata).filter(([key]) => !technicalMetadata.includes(key))
|
| 115 |
+
)
|
| 116 |
+
: metadata;
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Extracts metadata from an image file using the exifr library.
|
| 120 |
+
* @param fileData - The file data containing the image URL.
|
| 121 |
+
* @returns A promise resolving to the extracted metadata or null if extraction fails or is not applicable.
|
| 122 |
+
*/
|
| 123 |
+
async function extractMetadata(fileData: FileData): Promise<Record<string, any> | null> {
|
| 124 |
+
if (!fileData?.url) return null;
|
| 125 |
+
if (
|
| 126 |
+
fileData.url.toLowerCase().endsWith(".png") ||
|
| 127 |
+
fileData.url.toLowerCase().endsWith(".jpg") ||
|
| 128 |
+
fileData.url.toLowerCase().endsWith(".jpeg")
|
| 129 |
+
) {
|
| 130 |
+
try {
|
| 131 |
+
const data = await exifr.parse(fileData.url, true);
|
| 132 |
+
let parsed_meta: Record<string, any> = {};
|
| 133 |
+
if (data) {
|
| 134 |
+
for (const [key, value] of Object.entries(data)) {
|
| 135 |
+
if (
|
| 136 |
+
typeof value === "string" ||
|
| 137 |
+
typeof value === "number" ||
|
| 138 |
+
typeof value === "boolean"
|
| 139 |
+
) {
|
| 140 |
+
parsed_meta[key] = value;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
return parsed_meta;
|
| 145 |
+
} catch (error) {
|
| 146 |
+
return {};
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
return null;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Toggles the metadata popup visibility and extracts metadata if needed.
|
| 154 |
+
*/
|
| 155 |
+
async function toggleMetadataPopup(): Promise<void> {
|
| 156 |
+
if (showMetadataPopup) {
|
| 157 |
+
showMetadataPopup = false;
|
| 158 |
+
return;
|
| 159 |
+
}
|
| 160 |
+
if (!selected_media) return;
|
| 161 |
+
const media_file = "image" in selected_media ? selected_media.image : null;
|
| 162 |
+
if (!media_file) return;
|
| 163 |
+
is_extracting_metadata = true;
|
| 164 |
+
metadata = await extractMetadata(media_file);
|
| 165 |
+
is_extracting_metadata = false;
|
| 166 |
+
showMetadataPopup = true;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Dispatches the load_metadata event with filtered metadata and closes the popup.
|
| 171 |
+
*/
|
| 172 |
+
function dispatchLoadMetadata(): void {
|
| 173 |
+
if (filteredMetadata !== null) {
|
| 174 |
+
dispatch("load_metadata", filteredMetadata);
|
| 175 |
+
closePopup();
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/**
|
| 180 |
+
* Closes the metadata popup.
|
| 181 |
+
*/
|
| 182 |
+
function closePopup(): void {
|
| 183 |
+
showMetadataPopup = false;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
$: was_reset = value == null || value.length === 0;
|
| 187 |
+
$: resolved_value = value
|
| 188 |
+
? (value.map((data) =>
|
| 189 |
+
"video" in data
|
| 190 |
+
? { video: data.video as FileData, caption: data.caption }
|
| 191 |
+
: { image: data.image as FileData, caption: data.caption }
|
| 192 |
+
) as GalleryData[])
|
| 193 |
+
: null;
|
| 194 |
+
|
| 195 |
+
$: {
|
| 196 |
+
if (resolved_value && columns) {
|
| 197 |
+
const item_count = resolved_value.length;
|
| 198 |
+
if (Array.isArray(columns)) {
|
| 199 |
+
effective_columns = columns.map((col) => Math.min(col, item_count));
|
| 200 |
+
} else {
|
| 201 |
+
effective_columns = Math.min(columns, item_count);
|
| 202 |
+
}
|
| 203 |
+
} else {
|
| 204 |
+
effective_columns = columns;
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
$: if (!dequal(prev_value, value)) {
|
| 209 |
+
selected_index = null;
|
| 210 |
+
if (preview && value && value.length > 0) {
|
| 211 |
+
selected_index = 0;
|
| 212 |
+
}
|
| 213 |
+
dispatch("change");
|
| 214 |
+
prev_value = value;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
$: selected_media =
|
| 218 |
+
selected_index != null && resolved_value != null
|
| 219 |
+
? resolved_value[selected_index]
|
| 220 |
+
: null;
|
| 221 |
+
|
| 222 |
+
$: has_extractable_metadata =
|
| 223 |
+
selected_media &&
|
| 224 |
+
"image" in selected_media &&
|
| 225 |
+
(selected_media.image.url?.toLowerCase().endsWith(".png") ||
|
| 226 |
+
selected_media.image.url?.toLowerCase().endsWith(".jpg") ||
|
| 227 |
+
selected_media.image.url?.toLowerCase().endsWith(".jpeg"));
|
| 228 |
+
|
| 229 |
+
$: previous = ((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) % (resolved_value?.length ?? 0);
|
| 230 |
+
$: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Handles click events on the preview image to navigate to the previous or next item.
|
| 234 |
+
* @param event - The mouse click event.
|
| 235 |
+
*/
|
| 236 |
+
function handle_preview_click(event: MouseEvent): void {
|
| 237 |
+
const element = event.target as HTMLElement;
|
| 238 |
+
const x = event.offsetX;
|
| 239 |
+
const centerX = element.offsetWidth / 2;
|
| 240 |
+
selected_index = x < centerX ? previous : next;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/**
|
| 244 |
+
* Handles keyboard navigation in preview mode.
|
| 245 |
+
* @param e - The keyboard event.
|
| 246 |
+
*/
|
| 247 |
+
function on_keydown(e: KeyboardEvent): void {
|
| 248 |
+
switch (e.code) {
|
| 249 |
+
case "Escape":
|
| 250 |
+
e.preventDefault();
|
| 251 |
+
selected_index = null;
|
| 252 |
+
dispatch("preview_close");
|
| 253 |
+
break;
|
| 254 |
+
case "ArrowLeft":
|
| 255 |
+
e.preventDefault();
|
| 256 |
+
selected_index = previous;
|
| 257 |
+
break;
|
| 258 |
+
case "ArrowRight":
|
| 259 |
+
e.preventDefault();
|
| 260 |
+
selected_index = next;
|
| 261 |
+
break;
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
$: {
|
| 266 |
+
if (selected_index !== old_selected_index) {
|
| 267 |
+
showMetadataPopup = false;
|
| 268 |
+
metadata = null;
|
| 269 |
+
old_selected_index = selected_index;
|
| 270 |
+
if (selected_index !== null) {
|
| 271 |
+
if (resolved_value != null) {
|
| 272 |
+
selected_index = Math.max(0, Math.min(selected_index, resolved_value.length - 1));
|
| 273 |
+
}
|
| 274 |
+
dispatch("select", {
|
| 275 |
+
index: selected_index,
|
| 276 |
+
value: resolved_value?.[selected_index],
|
| 277 |
+
});
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
$: if (allow_preview) {
|
| 283 |
+
scroll_to_img(selected_index);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
$: if (selected_index !== null && preview_element) {
|
| 287 |
+
tick().then(() => {
|
| 288 |
+
preview_element?.focus();
|
| 289 |
+
});
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
/**
|
| 293 |
+
* Scrolls to the selected thumbnail in the thumbnails container.
|
| 294 |
+
* @param index - The index of the thumbnail to scroll to.
|
| 295 |
+
*/
|
| 296 |
+
async function scroll_to_img(index: number | null): Promise<void> {
|
| 297 |
+
if (typeof index !== "number" || !container_element) return;
|
| 298 |
+
await tick();
|
| 299 |
+
if (!el[index]) return;
|
| 300 |
+
el[index].focus();
|
| 301 |
+
const { left: container_left, width: container_width } = container_element.getBoundingClientRect();
|
| 302 |
+
const { left, width } = el[index].getBoundingClientRect();
|
| 303 |
+
const pos = left - container_left + width / 2 - container_width / 2 + container_element.scrollLeft;
|
| 304 |
+
container_element.scrollTo({ left: pos < 0 ? 0 : pos, behavior: "smooth" });
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/**
|
| 308 |
+
* Downloads a file from the provided URL.
|
| 309 |
+
* @param file_url - The URL of the file to download.
|
| 310 |
+
* @param name - The name to use for the downloaded file.
|
| 311 |
+
*/
|
| 312 |
+
async function download(file_url: string, name: string): Promise<void> {
|
| 313 |
+
try {
|
| 314 |
+
const response = await _fetch(file_url);
|
| 315 |
+
const blob = await response.blob();
|
| 316 |
+
const url = URL.createObjectURL(blob);
|
| 317 |
+
const link = document.createElement("a");
|
| 318 |
+
link.href = url;
|
| 319 |
+
link.download = name;
|
| 320 |
+
link.click();
|
| 321 |
+
URL.revokeObjectURL(url);
|
| 322 |
+
} catch (error) {
|
| 323 |
+
if (error instanceof TypeError) {
|
| 324 |
+
window.open(file_url, "_blank", "noreferrer");
|
| 325 |
+
return;
|
| 326 |
+
}
|
| 327 |
+
throw error;
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/**
|
| 332 |
+
* Checks if the thumbnails container overflows horizontally.
|
| 333 |
+
*/
|
| 334 |
+
function check_thumbnails_overflow(): void {
|
| 335 |
+
if (container_element) {
|
| 336 |
+
thumbnails_overflow = container_element.scrollWidth > container_element.clientWidth;
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/**
|
| 341 |
+
* Initializes the component, setting up event listeners for fullscreen and resize events.
|
| 342 |
+
*/
|
| 343 |
+
onMount(() => {
|
| 344 |
+
check_thumbnails_overflow();
|
| 345 |
+
document.addEventListener("fullscreenchange", () => {
|
| 346 |
+
is_full_screen = !!document.fullscreenElement;
|
| 347 |
+
fullscreen = is_full_screen;
|
| 348 |
+
});
|
| 349 |
+
window.addEventListener("resize", check_thumbnails_overflow);
|
| 350 |
+
return () => {
|
| 351 |
+
window.removeEventListener("resize", check_thumbnails_overflow);
|
| 352 |
+
document.removeEventListener("fullscreenchange", () => {
|
| 353 |
+
is_full_screen = !!document.fullscreenElement;
|
| 354 |
+
fullscreen = is_full_screen;
|
| 355 |
+
});
|
| 356 |
+
};
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
$: resolved_value, check_thumbnails_overflow();
|
| 360 |
+
$: if (container_element) check_thumbnails_overflow();
|
| 361 |
+
</script>
|
| 362 |
+
|
| 363 |
+
<svelte:window />
|
| 364 |
+
|
| 365 |
+
{#if show_label}
|
| 366 |
+
<BlockLabel {show_label} Icon={ImageIcon} label={label || "Gallery"} />
|
| 367 |
+
{/if}
|
| 368 |
+
|
| 369 |
+
{#if value == null || resolved_value == null || resolved_value.length === 0}
|
| 370 |
+
<Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
|
| 371 |
+
{:else}
|
| 372 |
+
<div class="gallery-container" bind:this={image_container}>
|
| 373 |
+
<!-- Preview Mode -->
|
| 374 |
+
{#if selected_media && allow_preview}
|
| 375 |
+
<button
|
| 376 |
+
class="preview"
|
| 377 |
+
bind:this={preview_element}
|
| 378 |
+
class:minimal={mode === "minimal"}
|
| 379 |
+
aria-label="Image Preview"
|
| 380 |
+
tabindex="-1"
|
| 381 |
+
on:keydown={on_keydown}
|
| 382 |
+
>
|
| 383 |
+
<IconButtonWrapper {display_icon_button_wrapper_top_corner}>
|
| 384 |
+
{#if show_download_button}
|
| 385 |
+
<IconButton
|
| 386 |
+
Icon={Download}
|
| 387 |
+
label={i18n("common.download")}
|
| 388 |
+
on:click={() => {
|
| 389 |
+
const media =
|
| 390 |
+
"image" in selected_media
|
| 391 |
+
? selected_media.image
|
| 392 |
+
: selected_media.video;
|
| 393 |
+
if (media?.url) download(media.url, media.orig_name ?? "media");
|
| 394 |
+
}}
|
| 395 |
+
/>
|
| 396 |
+
{/if}
|
| 397 |
+
{#if show_fullscreen_button}
|
| 398 |
+
<FullscreenButton
|
| 399 |
+
{fullscreen}
|
| 400 |
+
on:fullscreen={() => {
|
| 401 |
+
fullscreen = !fullscreen;
|
| 402 |
+
if (fullscreen) {
|
| 403 |
+
preview_element?.requestFullscreen();
|
| 404 |
+
} else if (document.fullscreenElement) {
|
| 405 |
+
document.exitFullscreen();
|
| 406 |
+
}
|
| 407 |
+
}}
|
| 408 |
+
/>
|
| 409 |
+
{/if}
|
| 410 |
+
{#if has_extractable_metadata}
|
| 411 |
+
<IconButton
|
| 412 |
+
Icon={Info}
|
| 413 |
+
label="View Metadata"
|
| 414 |
+
pending={is_extracting_metadata}
|
| 415 |
+
on:click={(event) => {
|
| 416 |
+
event.stopPropagation();
|
| 417 |
+
toggleMetadataPopup();
|
| 418 |
+
}}
|
| 419 |
+
/>
|
| 420 |
+
{/if}
|
| 421 |
+
{#if show_share_button}
|
| 422 |
+
<div class="icon-button">
|
| 423 |
+
<ShareButton
|
| 424 |
+
{i18n}
|
| 425 |
+
on:share
|
| 426 |
+
on:error
|
| 427 |
+
{value}
|
| 428 |
+
formatter={format_gallery_for_sharing}
|
| 429 |
+
/>
|
| 430 |
+
</div>
|
| 431 |
+
{/if}
|
| 432 |
+
{#if !is_full_screen}
|
| 433 |
+
<IconButton
|
| 434 |
+
Icon={Clear}
|
| 435 |
+
label="Close"
|
| 436 |
+
on:click={() => {
|
| 437 |
+
selected_index = null;
|
| 438 |
+
dispatch("preview_close");
|
| 439 |
+
}}
|
| 440 |
+
/>
|
| 441 |
+
{/if}
|
| 442 |
+
</IconButtonWrapper>
|
| 443 |
+
|
| 444 |
+
<button
|
| 445 |
+
class="media-container"
|
| 446 |
+
on:click={"image" in selected_media ? handle_preview_click : null}
|
| 447 |
+
>
|
| 448 |
+
{#if "image" in selected_media}
|
| 449 |
+
<Image
|
| 450 |
+
src={selected_media.image.url}
|
| 451 |
+
alt={selected_media.caption || ""}
|
| 452 |
+
loading="lazy"
|
| 453 |
+
/>
|
| 454 |
+
{:else}
|
| 455 |
+
<Video
|
| 456 |
+
src={selected_media.video.url}
|
| 457 |
+
alt={selected_media.caption || ""}
|
| 458 |
+
loading="lazy"
|
| 459 |
+
controls={true}
|
| 460 |
+
/>
|
| 461 |
+
{/if}
|
| 462 |
+
</button>
|
| 463 |
+
|
| 464 |
+
{#if selected_media?.caption}
|
| 465 |
+
<caption class="caption">{selected_media.caption}</caption>
|
| 466 |
+
{/if}
|
| 467 |
+
|
| 468 |
+
<div
|
| 469 |
+
bind:this={container_element}
|
| 470 |
+
class="thumbnails scroll-hide"
|
| 471 |
+
style="justify-content: {thumbnails_overflow ? 'flex-start' : 'center'};"
|
| 472 |
+
>
|
| 473 |
+
{#each resolved_value as media, i}
|
| 474 |
+
<button
|
| 475 |
+
bind:this={el[i]}
|
| 476 |
+
on:click={() => (selected_index = i)}
|
| 477 |
+
class="thumbnail-item thumbnail-small"
|
| 478 |
+
class:selected={selected_index === i && mode !== "minimal"}
|
| 479 |
+
aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
|
| 480 |
+
>
|
| 481 |
+
{#if "image" in media}
|
| 482 |
+
<Image
|
| 483 |
+
src={media.image.url}
|
| 484 |
+
title={media.caption || null}
|
| 485 |
+
alt=""
|
| 486 |
+
loading="lazy"
|
| 487 |
+
/>
|
| 488 |
+
{:else}
|
| 489 |
+
<Play />
|
| 490 |
+
<Video
|
| 491 |
+
src={media.video.url}
|
| 492 |
+
title={media.caption || null}
|
| 493 |
+
is_stream={false}
|
| 494 |
+
alt=""
|
| 495 |
+
loading="lazy"
|
| 496 |
+
loop={false}
|
| 497 |
+
/>
|
| 498 |
+
{/if}
|
| 499 |
+
</button>
|
| 500 |
+
{/each}
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
{#if showMetadataPopup && filteredMetadata !== null}
|
| 504 |
+
<div
|
| 505 |
+
class="metadata-popup"
|
| 506 |
+
on:click|stopPropagation
|
| 507 |
+
role="presentation"
|
| 508 |
+
style:width={typeof popup_metadata_width === "number" ? `${popup_metadata_width}px` : popup_metadata_width}
|
| 509 |
+
>
|
| 510 |
+
<div class="popup-content">
|
| 511 |
+
<button class="close-button" on:click={closePopup}>X</button>
|
| 512 |
+
<h3 class="popup-title">Image Metadata</h3>
|
| 513 |
+
{#if Object.keys(filteredMetadata).length > 0}
|
| 514 |
+
<div class="metadata-table-container">
|
| 515 |
+
<table class="metadata-table">
|
| 516 |
+
<tbody>
|
| 517 |
+
{#each Object.entries(filteredMetadata) as [key, val]}
|
| 518 |
+
{#if val}
|
| 519 |
+
<tr>
|
| 520 |
+
<td class="metadata-label">{key}</td>
|
| 521 |
+
<td class="metadata-value">{val}</td>
|
| 522 |
+
</tr>
|
| 523 |
+
{/if}
|
| 524 |
+
{/each}
|
| 525 |
+
</tbody>
|
| 526 |
+
</table>
|
| 527 |
+
</div>
|
| 528 |
+
<button
|
| 529 |
+
class="load-metadata-button"
|
| 530 |
+
on:click={dispatchLoadMetadata}
|
| 531 |
+
>Load Metadata</button>
|
| 532 |
+
{:else}
|
| 533 |
+
<p class="no-metadata-message">No custom metadata found.</p>
|
| 534 |
+
{/if}
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
{/if}
|
| 538 |
+
</button>
|
| 539 |
+
{/if}
|
| 540 |
+
|
| 541 |
+
<!-- Main Grid / Single Item View -->
|
| 542 |
+
<div
|
| 543 |
+
class="grid-wrap"
|
| 544 |
+
class:minimal={mode === "minimal"}
|
| 545 |
+
class:fixed-height={!height || height == "auto"}
|
| 546 |
+
class:hidden={is_full_screen || (selected_media && allow_preview)}
|
| 547 |
+
style:height={height !== "auto" ? `${height}px` : null}
|
| 548 |
+
>
|
| 549 |
+
<!-- Multi-item grid -->
|
| 550 |
+
{#if resolved_value && resolved_value.length > 1}
|
| 551 |
+
<div
|
| 552 |
+
class="grid-container"
|
| 553 |
+
style:--grid-cols={effective_columns}
|
| 554 |
+
style:--grid-rows={rows}
|
| 555 |
+
style:--object-fit={object_fit}
|
| 556 |
+
class:pt-6={show_label}
|
| 557 |
+
>
|
| 558 |
+
{#each resolved_value as entry, i}
|
| 559 |
+
{@const file_name = "image" in entry ? entry.image.orig_name : entry.video.orig_name}
|
| 560 |
+
<div class="gallery-item-with-name">
|
| 561 |
+
<div class="gallery-item">
|
| 562 |
+
<button
|
| 563 |
+
class="thumbnail-item thumbnail-lg"
|
| 564 |
+
class:selected={selected_index === i}
|
| 565 |
+
on:click={() => {
|
| 566 |
+
if (selected_index === null && allow_preview)
|
| 567 |
+
dispatch("preview_open");
|
| 568 |
+
selected_index = i;
|
| 569 |
+
}}
|
| 570 |
+
aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
|
| 571 |
+
>
|
| 572 |
+
{#if "image" in entry}
|
| 573 |
+
<Image
|
| 574 |
+
alt={entry.caption || ""}
|
| 575 |
+
src={entry.image.url}
|
| 576 |
+
loading="lazy"
|
| 577 |
+
/>
|
| 578 |
+
{:else}
|
| 579 |
+
<Play />
|
| 580 |
+
<Video
|
| 581 |
+
src={entry.video.url}
|
| 582 |
+
title={entry.caption || null}
|
| 583 |
+
is_stream={false}
|
| 584 |
+
alt=""
|
| 585 |
+
loading="lazy"
|
| 586 |
+
loop={false}
|
| 587 |
+
/>
|
| 588 |
+
{/if}
|
| 589 |
+
{#if entry.caption}
|
| 590 |
+
<div class="caption-label">{entry.caption}</div>
|
| 591 |
+
{/if}
|
| 592 |
+
</button>
|
| 593 |
+
</div>
|
| 594 |
+
{#if file_name}
|
| 595 |
+
<div class="thumbnail-filename" title={file_name}>
|
| 596 |
+
{file_name}
|
| 597 |
+
</div>
|
| 598 |
+
{/if}
|
| 599 |
+
</div>
|
| 600 |
+
{/each}
|
| 601 |
+
</div>
|
| 602 |
+
<!-- Single-item view -->
|
| 603 |
+
{:else if resolved_value && resolved_value.length === 1}
|
| 604 |
+
{@const entry = resolved_value[0]}
|
| 605 |
+
{@const file_name = "image" in entry ? entry.image.orig_name : entry.video.orig_name}
|
| 606 |
+
<div class="single-item-wrapper" style:--object-fit={object_fit}>
|
| 607 |
+
<div class="gallery-item-with-name">
|
| 608 |
+
<div class="gallery-item">
|
| 609 |
+
<button
|
| 610 |
+
class="thumbnail-item thumbnail-lg"
|
| 611 |
+
on:click={() => {
|
| 612 |
+
if (allow_preview) {
|
| 613 |
+
dispatch("preview_open");
|
| 614 |
+
selected_index = 0;
|
| 615 |
+
}
|
| 616 |
+
}}
|
| 617 |
+
aria-label="View single item in preview mode"
|
| 618 |
+
>
|
| 619 |
+
{#if "image" in entry}
|
| 620 |
+
<Image
|
| 621 |
+
alt={entry.caption || ""}
|
| 622 |
+
src={entry.image.url}
|
| 623 |
+
loading="lazy"
|
| 624 |
+
/>
|
| 625 |
+
{:else}
|
| 626 |
+
<Play />
|
| 627 |
+
<Video
|
| 628 |
+
src={entry.video.url}
|
| 629 |
+
title={entry.caption || null}
|
| 630 |
+
is_stream={false}
|
| 631 |
+
alt=""
|
| 632 |
+
loading="lazy"
|
| 633 |
+
loop={false}
|
| 634 |
+
/>
|
| 635 |
+
{/if}
|
| 636 |
+
{#if entry.caption}
|
| 637 |
+
<div class="caption-label">{entry.caption}</div>
|
| 638 |
+
{/if}
|
| 639 |
+
</button>
|
| 640 |
+
</div>
|
| 641 |
+
{#if file_name}
|
| 642 |
+
<div class="thumbnail-filename" title={file_name}>
|
| 643 |
+
{file_name}
|
| 644 |
+
</div>
|
| 645 |
+
{/if}
|
| 646 |
+
</div>
|
| 647 |
+
</div>
|
| 648 |
+
{/if}
|
| 649 |
+
</div>
|
| 650 |
+
</div>
|
| 651 |
+
{/if}
|
| 652 |
+
|
| 653 |
+
<style lang="postcss">
|
| 654 |
+
/**
|
| 655 |
+
* Styles for the gallery container, which holds the entire component.
|
| 656 |
+
*/
|
| 657 |
+
.gallery-container {
|
| 658 |
+
position: relative;
|
| 659 |
+
width: 100%;
|
| 660 |
+
height: 100%;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
/**
|
| 664 |
+
* Styles for buttons to ensure they fill their container.
|
| 665 |
+
*/
|
| 666 |
+
button {
|
| 667 |
+
width: var(--size-full);
|
| 668 |
+
height: var(--size-full);
|
| 669 |
+
display: block;
|
| 670 |
+
border-radius: var(--radius-lg);
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
/**
|
| 674 |
+
* Styles for the preview mode, displaying a selected media item.
|
| 675 |
+
*/
|
| 676 |
+
.preview {
|
| 677 |
+
display: flex;
|
| 678 |
+
position: absolute;
|
| 679 |
+
flex-direction: column;
|
| 680 |
+
z-index: var(--layer-2);
|
| 681 |
+
border-radius: calc(var(--block-radius) - var(--block-border-width));
|
| 682 |
+
-webkit-backdrop-filter: blur(8px);
|
| 683 |
+
backdrop-filter: blur(8px);
|
| 684 |
+
width: var(--size-full);
|
| 685 |
+
height: var(--size-full);
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.preview:focus-visible {
|
| 689 |
+
outline: none;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
.preview.minimal {
|
| 693 |
+
width: fit-content;
|
| 694 |
+
height: fit-content;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.preview::before {
|
| 698 |
+
content: "";
|
| 699 |
+
position: absolute;
|
| 700 |
+
z-index: var(--layer-below);
|
| 701 |
+
background: var(--background-fill-primary);
|
| 702 |
+
opacity: 0.9;
|
| 703 |
+
width: var(--size-full);
|
| 704 |
+
height: var(--size-full);
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
/**
|
| 708 |
+
* Styles for the grid wrapper with fixed height constraints.
|
| 709 |
+
*/
|
| 710 |
+
.fixed-height {
|
| 711 |
+
min-height: var(--size-80);
|
| 712 |
+
max-height: 65vh;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
@media (--screen-xl) {
|
| 716 |
+
.fixed-height {
|
| 717 |
+
min-height: 450px;
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
/**
|
| 722 |
+
* Styles for the media container in preview mode.
|
| 723 |
+
*/
|
| 724 |
+
.media-container {
|
| 725 |
+
height: calc(100% - var(--size-14));
|
| 726 |
+
width: 100%;
|
| 727 |
+
display: flex;
|
| 728 |
+
align-items: center;
|
| 729 |
+
justify-content: center;
|
| 730 |
+
cursor: pointer;
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.media-container :global(img),
|
| 734 |
+
.media-container :global(video) {
|
| 735 |
+
max-width: 100%;
|
| 736 |
+
max-height: 100%;
|
| 737 |
+
object-fit: contain;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
/**
|
| 741 |
+
* Styles for thumbnails in the preview mode carousel.
|
| 742 |
+
*/
|
| 743 |
+
.thumbnails :global(img) {
|
| 744 |
+
object-fit: cover;
|
| 745 |
+
width: var(--size-full);
|
| 746 |
+
height: var(--size-full);
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.thumbnails :global(svg) {
|
| 750 |
+
position: absolute;
|
| 751 |
+
top: var(--size-2);
|
| 752 |
+
left: var(--size-2);
|
| 753 |
+
width: 50%;
|
| 754 |
+
height: 50%;
|
| 755 |
+
opacity: 50%;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
/**
|
| 759 |
+
* Styles for captions in preview mode.
|
| 760 |
+
*/
|
| 761 |
+
.caption {
|
| 762 |
+
padding: var(--size-2) var(--size-3);
|
| 763 |
+
overflow: hidden;
|
| 764 |
+
color: var(--block-label-text-color);
|
| 765 |
+
font-weight: var(--weight-semibold);
|
| 766 |
+
text-align: center;
|
| 767 |
+
text-overflow: ellipsis;
|
| 768 |
+
white-space: nowrap;
|
| 769 |
+
align-self: center;
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
/**
|
| 773 |
+
* Styles for the thumbnails carousel in preview mode.
|
| 774 |
+
*/
|
| 775 |
+
.thumbnails {
|
| 776 |
+
display: flex;
|
| 777 |
+
position: absolute;
|
| 778 |
+
bottom: 0;
|
| 779 |
+
justify-content: flex-start;
|
| 780 |
+
align-items: center;
|
| 781 |
+
gap: var(--spacing-lg);
|
| 782 |
+
width: var(--size-full);
|
| 783 |
+
height: var(--size-14);
|
| 784 |
+
overflow-x: scroll;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
/**
|
| 788 |
+
* Styles for individual thumbnail items.
|
| 789 |
+
*/
|
| 790 |
+
.thumbnail-item {
|
| 791 |
+
--ring-color: transparent;
|
| 792 |
+
position: relative;
|
| 793 |
+
box-shadow: inset 0 0 0 1px var(--ring-color), var(--shadow-drop);
|
| 794 |
+
border: 1px solid var(--border-color-primary);
|
| 795 |
+
border-radius: var(--button-small-radius);
|
| 796 |
+
background: var(--background-fill-secondary);
|
| 797 |
+
aspect-ratio: var(--ratio-square);
|
| 798 |
+
width: var(--size-full);
|
| 799 |
+
height: var(--size-full);
|
| 800 |
+
overflow: clip;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
.thumbnail-item:hover {
|
| 804 |
+
--ring-color: var(--color-accent);
|
| 805 |
+
border-color: var(--color-accent);
|
| 806 |
+
filter: brightness(1.1);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.thumbnail-item.selected {
|
| 810 |
+
--ring-color: var(--color-accent);
|
| 811 |
+
border-color: var(--color-accent);
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.thumbnail-item :global(svg) {
|
| 815 |
+
position: absolute;
|
| 816 |
+
top: 50%;
|
| 817 |
+
left: 50%;
|
| 818 |
+
width: 50%;
|
| 819 |
+
height: 50%;
|
| 820 |
+
opacity: 50%;
|
| 821 |
+
transform: translate(-50%, -50%);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.thumbnail-item :global(video) {
|
| 825 |
+
width: var(--size-full);
|
| 826 |
+
height: var(--size-full);
|
| 827 |
+
overflow: hidden;
|
| 828 |
+
object-fit: cover;
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
/**
|
| 832 |
+
* Styles for small thumbnails in the preview carousel.
|
| 833 |
+
*/
|
| 834 |
+
.thumbnail-small {
|
| 835 |
+
flex: none;
|
| 836 |
+
transform: scale(0.9);
|
| 837 |
+
transition: 0.075s;
|
| 838 |
+
width: var(--size-9);
|
| 839 |
+
height: var(--size-9);
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
.thumbnail-small.selected {
|
| 843 |
+
--ring-color: var(--color-accent);
|
| 844 |
+
transform: scale(1);
|
| 845 |
+
border-color: var(--color-accent);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
/**
|
| 849 |
+
* Styles for the grid wrapper containing the gallery items.
|
| 850 |
+
*/
|
| 851 |
+
.grid-wrap {
|
| 852 |
+
position: relative;
|
| 853 |
+
padding: var(--size-2);
|
| 854 |
+
overflow-y: auto;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
.grid-wrap.fixed-height {
|
| 858 |
+
min-height: var(--size-80);
|
| 859 |
+
max-height: 65vh;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
/**
|
| 863 |
+
* Styles for the grid container for multiple items.
|
| 864 |
+
*/
|
| 865 |
+
.grid-container {
|
| 866 |
+
display: grid;
|
| 867 |
+
position: relative;
|
| 868 |
+
grid-template-rows: repeat(var(--grid-rows), minmax(100px, 1fr));
|
| 869 |
+
grid-template-columns: repeat(var(--grid-cols), minmax(100px, 1fr));
|
| 870 |
+
grid-auto-rows: minmax(100px, 1fr);
|
| 871 |
+
gap: var(--spacing-lg);
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
/**
|
| 875 |
+
* Styles for single-item view wrapper.
|
| 876 |
+
*/
|
| 877 |
+
.single-item-wrapper {
|
| 878 |
+
display: flex;
|
| 879 |
+
align-items: center;
|
| 880 |
+
justify-content: center;
|
| 881 |
+
width: 100%;
|
| 882 |
+
height: 100%;
|
| 883 |
+
padding: var(--spacing-xxl);
|
| 884 |
+
box-sizing: border-box;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
.single-item-wrapper .gallery-item-with-name {
|
| 888 |
+
width: 100%;
|
| 889 |
+
height: 100%;
|
| 890 |
+
max-width: min(300px, 80vw);
|
| 891 |
+
max-height: min(320px, calc(80vh - var(--size-4)));
|
| 892 |
+
display: flex;
|
| 893 |
+
flex-direction: column;
|
| 894 |
+
align-items: center;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
.single-item-wrapper .gallery-item {
|
| 898 |
+
width: 100%;
|
| 899 |
+
height: 100%;
|
| 900 |
+
max-width: 100%;
|
| 901 |
+
max-height: 100%;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
.single-item-wrapper .thumbnail-item.thumbnail-lg {
|
| 905 |
+
display: flex !important;
|
| 906 |
+
align-items: center !important;
|
| 907 |
+
justify-content: center !important;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.single-item-wrapper .thumbnail-filename {
|
| 911 |
+
height: var(--size-4);
|
| 912 |
+
line-height: var(--size-4);
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.single-item-wrapper .thumbnail-lg > :global(img),
|
| 916 |
+
.single-item-wrapper .thumbnail-lg > :global(video) {
|
| 917 |
+
object-fit: var(--object-fit) !important;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
/**
|
| 921 |
+
* Styles for large thumbnails in the grid or single-item view.
|
| 922 |
+
*/
|
| 923 |
+
.thumbnail-lg > :global(img),
|
| 924 |
+
.thumbnail-lg > :global(video) {
|
| 925 |
+
width: var(--size-full);
|
| 926 |
+
height: var(--size-full);
|
| 927 |
+
overflow: hidden;
|
| 928 |
+
object-fit: var(--object-fit);
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
.thumbnail-lg:hover .caption-label {
|
| 932 |
+
opacity: 0.5;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
/**
|
| 936 |
+
* Styles for captions in the grid or single-item view.
|
| 937 |
+
*/
|
| 938 |
+
.caption-label {
|
| 939 |
+
position: absolute;
|
| 940 |
+
right: var(--block-label-margin);
|
| 941 |
+
bottom: var(--block-label-margin);
|
| 942 |
+
z-index: var(--layer-1);
|
| 943 |
+
border-top: 1px solid var(--border-color-primary);
|
| 944 |
+
border-left: 1px solid var(--border-color-primary);
|
| 945 |
+
border-radius: var(--block-label-radius);
|
| 946 |
+
background: var(--background-fill-secondary);
|
| 947 |
+
padding: var(--block-label-padding);
|
| 948 |
+
max-width: 80%;
|
| 949 |
+
overflow: hidden;
|
| 950 |
+
font-size: var(--block-label-text-size);
|
| 951 |
+
text-align: left;
|
| 952 |
+
text-overflow: ellipsis;
|
| 953 |
+
white-space: nowrap;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
.grid-wrap.minimal {
|
| 957 |
+
padding: 0;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
/**
|
| 961 |
+
* Styles for gallery items with associated filenames.
|
| 962 |
+
*/
|
| 963 |
+
.gallery-item-with-name {
|
| 964 |
+
display: flex;
|
| 965 |
+
flex-direction: column;
|
| 966 |
+
gap: var(--size-1);
|
| 967 |
+
width: 100%;
|
| 968 |
+
height: 100%;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.thumbnail-filename {
|
| 972 |
+
font-size: var(--text-xs);
|
| 973 |
+
color: var(--body-text-color);
|
| 974 |
+
text-align: center;
|
| 975 |
+
width: 100%;
|
| 976 |
+
overflow: hidden;
|
| 977 |
+
text-overflow: ellipsis;
|
| 978 |
+
white-space: nowrap;
|
| 979 |
+
padding: 0 var(--size-1);
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
.gallery-item {
|
| 983 |
+
position: relative;
|
| 984 |
+
width: 100%;
|
| 985 |
+
height: 100%;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
/**
|
| 989 |
+
* Styles for the metadata popup displayed in preview mode.
|
| 990 |
+
*/
|
| 991 |
+
.metadata-popup {
|
| 992 |
+
position: fixed;
|
| 993 |
+
top: 50%;
|
| 994 |
+
left: 50%;
|
| 995 |
+
transform: translate(-50%, -50%);
|
| 996 |
+
background: var(--background-fill-primary, white);
|
| 997 |
+
border: 1px solid var(--border-color-primary);
|
| 998 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 999 |
+
z-index: 1000;
|
| 1000 |
+
border-radius: 8px;
|
| 1001 |
+
max-width: min(90%, 600px);
|
| 1002 |
+
max-height: min(50vh, calc(100% - 2rem));
|
| 1003 |
+
min-height: 200px;
|
| 1004 |
+
display: flex;
|
| 1005 |
+
flex-direction: column;
|
| 1006 |
+
pointer-events: auto;
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
.popup-content {
|
| 1010 |
+
padding: 1rem;
|
| 1011 |
+
display: flex;
|
| 1012 |
+
flex-direction: column;
|
| 1013 |
+
width: 100%;
|
| 1014 |
+
box-sizing: border-box;
|
| 1015 |
+
overflow-y: auto;
|
| 1016 |
+
position: relative;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
.close-button {
|
| 1020 |
+
position: absolute;
|
| 1021 |
+
top: 0.5rem;
|
| 1022 |
+
right: 0.5rem;
|
| 1023 |
+
background: none;
|
| 1024 |
+
border: none;
|
| 1025 |
+
font-size: 1.25rem;
|
| 1026 |
+
cursor: pointer;
|
| 1027 |
+
z-index: 20;
|
| 1028 |
+
color: var(--body-text-color);
|
| 1029 |
+
padding: 0.25rem;
|
| 1030 |
+
line-height: 1;
|
| 1031 |
+
width: 24px;
|
| 1032 |
+
height: 24px;
|
| 1033 |
+
text-align: center;
|
| 1034 |
+
}
|
| 1035 |
+
|
| 1036 |
+
.popup-title {
|
| 1037 |
+
font-weight: bold;
|
| 1038 |
+
margin: 0 0 1rem 0;
|
| 1039 |
+
flex-shrink: 0;
|
| 1040 |
+
padding-right: 2.5rem;
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
.metadata-table-container {
|
| 1044 |
+
flex-grow: 1;
|
| 1045 |
+
overflow-y: auto;
|
| 1046 |
+
max-height: calc(100% - 5rem);
|
| 1047 |
+
min-height: 0;
|
| 1048 |
+
margin-bottom: 1rem;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.metadata-table {
|
| 1052 |
+
width: 100%;
|
| 1053 |
+
border-collapse: collapse;
|
| 1054 |
+
table-layout: auto;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
.metadata-label {
|
| 1058 |
+
background: var(--background-fill-secondary, #f5f5f5);
|
| 1059 |
+
padding: 0.5rem;
|
| 1060 |
+
font-weight: bold;
|
| 1061 |
+
text-align: left;
|
| 1062 |
+
vertical-align: top;
|
| 1063 |
+
width: 35%;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
.metadata-value {
|
| 1067 |
+
text-align: left;
|
| 1068 |
+
padding: 0.5rem;
|
| 1069 |
+
white-space: pre-wrap;
|
| 1070 |
+
word-break: break-all;
|
| 1071 |
+
vertical-align: top;
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
.load-metadata-button {
|
| 1075 |
+
margin-top: 1rem;
|
| 1076 |
+
padding: 0.5rem 1rem;
|
| 1077 |
+
background-color: var(--button-primary-background-fill);
|
| 1078 |
+
color: var(--button-primary-text-color);
|
| 1079 |
+
border: none;
|
| 1080 |
+
border-radius: 4px;
|
| 1081 |
+
cursor: pointer;
|
| 1082 |
+
align-self: center;
|
| 1083 |
+
flex-shrink: 0;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
.load-metadata-button:hover {
|
| 1087 |
+
background-color: var(--button-primary-background-fill-hover);
|
| 1088 |
+
}
|
| 1089 |
+
|
| 1090 |
+
.no-metadata-message {
|
| 1091 |
+
flex-grow: 1;
|
| 1092 |
+
display: flex;
|
| 1093 |
+
align-items: center;
|
| 1094 |
+
justify-content: center;
|
| 1095 |
+
color: var(--body-text-color-subdued);
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
|
| 1099 |
+
</style>
|
src/frontend/shared/utils.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { uploadToHuggingFace } from "@gradio/utils";
|
| 2 |
+
import type { FileData } from "@gradio/client";
|
| 3 |
+
|
| 4 |
+
export async function format_gallery_for_sharing(
|
| 5 |
+
value: [FileData, string | null][] | null
|
| 6 |
+
): Promise<string> {
|
| 7 |
+
if (!value) return "";
|
| 8 |
+
let urls = await Promise.all(
|
| 9 |
+
value.map(async ([image, _]) => {
|
| 10 |
+
if (image === null || !image.url) return "";
|
| 11 |
+
return await uploadToHuggingFace(image.url, "url");
|
| 12 |
+
})
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
return `<div style="display: flex; flex-wrap: wrap; gap: 16px">${urls
|
| 16 |
+
.map((url) => `<img src="${url}" style="height: 400px" />`)
|
| 17 |
+
.join("")}</div>`;
|
| 18 |
+
}
|
src/frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"allowJs": true,
|
| 4 |
+
"checkJs": true,
|
| 5 |
+
"esModuleInterop": true,
|
| 6 |
+
"forceConsistentCasingInFileNames": true,
|
| 7 |
+
"resolveJsonModule": true,
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
"sourceMap": true,
|
| 10 |
+
"strict": true,
|
| 11 |
+
"verbatimModuleSyntax": true
|
| 12 |
+
},
|
| 13 |
+
"exclude": ["node_modules", "dist", "./gradio.config.js"]
|
| 14 |
+
}
|
src/frontend/types.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { FileData } from "@gradio/client";
|
| 2 |
+
|
| 3 |
+
export interface GalleryImage {
|
| 4 |
+
image: FileData;
|
| 5 |
+
caption: string | null;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export interface GalleryVideo {
|
| 9 |
+
video: FileData;
|
| 10 |
+
caption: string | null;
|
| 11 |
+
}
|
src/log.txt
ADDED
|
File without changes
|
src/pyproject.toml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = [
|
| 3 |
+
"hatchling",
|
| 4 |
+
"hatch-requirements-txt",
|
| 5 |
+
"hatch-fancy-pypi-readme>=22.5.0",
|
| 6 |
+
]
|
| 7 |
+
build-backend = "hatchling.build"
|
| 8 |
+
|
| 9 |
+
[project]
|
| 10 |
+
name = "gradio_mediagallery"
|
| 11 |
+
version = "0.0.1"
|
| 12 |
+
description = "Python library for easily interacting with trained machine learning models"
|
| 13 |
+
readme = "README.md"
|
| 14 |
+
license = "Apache-2.0"
|
| 15 |
+
requires-python = ">=3.8"
|
| 16 |
+
authors = [{ name = "Eliseu Silva", email = "elismasilva@gmail.com" }]
|
| 17 |
+
keywords = [
|
| 18 |
+
"gradio-custom-component",
|
| 19 |
+
"gradio-template-Gallery"
|
| 20 |
+
]
|
| 21 |
+
# Add dependencies here
|
| 22 |
+
dependencies = ["gradio>=4.0,<6.0"]
|
| 23 |
+
classifiers = [
|
| 24 |
+
'Development Status :: 3 - Alpha',
|
| 25 |
+
'Operating System :: OS Independent',
|
| 26 |
+
'Programming Language :: Python :: 3',
|
| 27 |
+
'Programming Language :: Python :: 3 :: Only',
|
| 28 |
+
'Programming Language :: Python :: 3.8',
|
| 29 |
+
'Programming Language :: Python :: 3.9',
|
| 30 |
+
'Programming Language :: Python :: 3.10',
|
| 31 |
+
'Programming Language :: Python :: 3.11',
|
| 32 |
+
'Topic :: Scientific/Engineering',
|
| 33 |
+
'Topic :: Scientific/Engineering :: Artificial Intelligence',
|
| 34 |
+
'Topic :: Scientific/Engineering :: Visualization',
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
# The repository and space URLs are optional, but recommended.
|
| 38 |
+
# Adding a repository URL will create a badge in the auto-generated README that links to the repository.
|
| 39 |
+
# Adding a space URL will create a badge in the auto-generated README that links to the space.
|
| 40 |
+
# This will make it easy for people to find your deployed demo or source code when they
|
| 41 |
+
# encounter your project in the wild.
|
| 42 |
+
|
| 43 |
+
# [project.urls]
|
| 44 |
+
# repository = "your github repository"
|
| 45 |
+
# space = "your space url"
|
| 46 |
+
|
| 47 |
+
[project.optional-dependencies]
|
| 48 |
+
dev = ["build", "twine"]
|
| 49 |
+
|
| 50 |
+
[tool.hatch.build]
|
| 51 |
+
artifacts = ["/backend/gradio_mediagallery/templates", "*.pyi", "/\\backend\\gradio_mediagallery\\templates"]
|
| 52 |
+
|
| 53 |
+
[tool.hatch.build.targets.wheel]
|
| 54 |
+
packages = ["/backend/gradio_mediagallery"]
|