feat: Add MediaGallery custom component for mixed media assets
Browse files- Custom Gradio component supporting images, videos, and audio in unified grid
- Responsive grid layout (2-5 columns based on screen size)
- Square thumbnails with media type badges
- Add Media button for uploading additional files
- Audio preview with gradient background
- File upload handling with proper media type detection
- Examples display up to 5 previews without hover effects
- app.py +70 -32
- mediagallery/.gitignore +12 -0
- mediagallery/README.md +490 -0
- mediagallery/backend/gradio_mediagallery/__init__.py +4 -0
- mediagallery/backend/gradio_mediagallery/mediagallery.py +374 -0
- mediagallery/demo/__init__.py +0 -0
- mediagallery/demo/app.py +15 -0
- mediagallery/demo/css.css +157 -0
- mediagallery/demo/space.py +176 -0
- mediagallery/frontend/Example.svelte +153 -0
- mediagallery/frontend/Index.svelte +193 -0
- mediagallery/frontend/gradio.config.js +9 -0
- mediagallery/frontend/package-lock.json +0 -0
- mediagallery/frontend/package.json +52 -0
- mediagallery/frontend/shared/Gallery.svelte +1030 -0
- mediagallery/frontend/shared/utils.ts +21 -0
- mediagallery/frontend/tsconfig.json +14 -0
- mediagallery/frontend/types.ts +33 -0
- mediagallery/pyproject.toml +54 -0
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import spaces
|
|
|
|
| 3 |
|
| 4 |
from PIL import Image
|
| 5 |
from moviepy.editor import VideoFileClip, AudioFileClip
|
|
@@ -56,7 +57,37 @@ allowed_medias = [
|
|
| 56 |
]
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
def get_files_infos(files):
|
|
|
|
| 60 |
results = []
|
| 61 |
for file in files:
|
| 62 |
file_path = Path(file.name)
|
|
@@ -236,19 +267,22 @@ Remember: Simpler is better. Only use advanced ffmpeg features if absolutely nec
|
|
| 236 |
else:
|
| 237 |
# Use existing conversation history
|
| 238 |
messages = conversation_history[:]
|
| 239 |
-
|
| 240 |
# If there's a previous error, add it as a separate message exchange
|
| 241 |
if previous_error and previous_command:
|
| 242 |
# Add the failed command as assistant response
|
| 243 |
-
messages.append(
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
# Add the error as user feedback
|
| 249 |
-
messages.append(
|
| 250 |
-
|
| 251 |
-
|
|
|
|
| 252 |
|
| 253 |
ERROR MESSAGE:
|
| 254 |
{previous_error}
|
|
@@ -266,14 +300,17 @@ FORMAT DETECTION KEYWORDS:
|
|
| 266 |
- "horizontal", "landscape", "16:9", "YouTube", "TV" → Use 1920x1080 (default)
|
| 267 |
- "square", "1:1", "Instagram post" → Use 1080x1080
|
| 268 |
|
| 269 |
-
Please provide a corrected FFmpeg command."""
|
| 270 |
-
|
|
|
|
| 271 |
else:
|
| 272 |
# Add new user request to existing conversation
|
| 273 |
-
messages.append(
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
| 277 |
try:
|
| 278 |
# Print the complete prompt
|
| 279 |
print("\n=== COMPLETE PROMPT ===")
|
|
@@ -299,18 +336,19 @@ Please provide a corrected FFmpeg command."""
|
|
| 299 |
)
|
| 300 |
content = completion.choices[0].message.content
|
| 301 |
print(f"\n=== RAW API RESPONSE ===\n{content}\n========================\n")
|
| 302 |
-
|
| 303 |
# Extract command from code block if present
|
| 304 |
import re
|
|
|
|
| 305 |
command = None
|
| 306 |
-
|
| 307 |
# Try multiple code block patterns
|
| 308 |
code_patterns = [
|
| 309 |
r"```(?:bash|sh|shell)?\n(.*?)\n```", # Standard code blocks
|
| 310 |
r"```\n(.*?)\n```", # Plain code blocks
|
| 311 |
r"`([^`]*ffmpeg[^`]*)`", # Inline code with ffmpeg
|
| 312 |
]
|
| 313 |
-
|
| 314 |
for pattern in code_patterns:
|
| 315 |
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
| 316 |
for match in matches:
|
|
@@ -319,7 +357,7 @@ Please provide a corrected FFmpeg command."""
|
|
| 319 |
break
|
| 320 |
if command:
|
| 321 |
break
|
| 322 |
-
|
| 323 |
# If no code block found, try to find ffmpeg lines directly
|
| 324 |
if not command:
|
| 325 |
ffmpeg_lines = [
|
|
@@ -329,7 +367,7 @@ Please provide a corrected FFmpeg command."""
|
|
| 329 |
]
|
| 330 |
if ffmpeg_lines:
|
| 331 |
command = ffmpeg_lines[0]
|
| 332 |
-
|
| 333 |
# Last resort: look for any line containing ffmpeg
|
| 334 |
if not command:
|
| 335 |
for line in content.split("\n"):
|
|
@@ -337,21 +375,18 @@ Please provide a corrected FFmpeg command."""
|
|
| 337 |
if "ffmpeg" in line.lower() and len(line) > 10:
|
| 338 |
command = line
|
| 339 |
break
|
| 340 |
-
|
| 341 |
if not command:
|
| 342 |
print(f"ERROR: No ffmpeg command found in response")
|
| 343 |
command = content.replace("\n", " ").strip()
|
| 344 |
-
|
| 345 |
print(f"=== EXTRACTED COMMAND ===\n{command}\n========================\n")
|
| 346 |
-
|
| 347 |
# remove output.mp4 with the actual output file path
|
| 348 |
command = command.replace("output.mp4", "")
|
| 349 |
|
| 350 |
# Add the assistant's response to conversation history
|
| 351 |
-
messages.append({
|
| 352 |
-
"role": "assistant",
|
| 353 |
-
"content": content
|
| 354 |
-
})
|
| 355 |
|
| 356 |
return command, messages
|
| 357 |
except Exception as e:
|
|
@@ -377,8 +412,8 @@ def compose_video(
|
|
| 377 |
"""
|
| 378 |
Compose videos from existing media assets using natural language instructions.
|
| 379 |
|
| 380 |
-
This tool is NOT for AI video generation. Instead, it uses AI to generate FFmpeg
|
| 381 |
-
commands that combine, edit, and transform your uploaded images, videos, and audio
|
| 382 |
files based on natural language descriptions.
|
| 383 |
|
| 384 |
Args:
|
|
@@ -407,6 +442,8 @@ def update(
|
|
| 407 |
if prompt == "":
|
| 408 |
raise gr.Error("Please enter a prompt.")
|
| 409 |
|
|
|
|
|
|
|
| 410 |
files_info = get_files_infos(files)
|
| 411 |
# disable this if you're running the app locally or on your own server
|
| 412 |
for file_info in files_info:
|
|
@@ -564,10 +601,11 @@ with gr.Blocks() as demo:
|
|
| 564 |
)
|
| 565 |
with gr.Row():
|
| 566 |
with gr.Column():
|
| 567 |
-
user_files =
|
| 568 |
-
file_count="multiple",
|
| 569 |
-
label="Media files",
|
| 570 |
file_types=allowed_medias,
|
|
|
|
|
|
|
|
|
|
| 571 |
)
|
| 572 |
user_prompt = gr.Textbox(
|
| 573 |
placeholder="eg: Remove the 3 first seconds of the video",
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import spaces
|
| 3 |
+
from gradio_mediagallery import MediaGallery
|
| 4 |
|
| 5 |
from PIL import Image
|
| 6 |
from moviepy.editor import VideoFileClip, AudioFileClip
|
|
|
|
| 57 |
]
|
| 58 |
|
| 59 |
|
| 60 |
+
class FileWrapper:
|
| 61 |
+
"""Wrapper to provide .name attribute for MediaGallery output tuples."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, path):
|
| 64 |
+
self.name = path if isinstance(path, str) else str(path)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def normalize_files(files):
|
| 68 |
+
"""Convert MediaGallery output or gr.File output to list of file-like objects."""
|
| 69 |
+
if not files:
|
| 70 |
+
return []
|
| 71 |
+
|
| 72 |
+
result = []
|
| 73 |
+
for item in files:
|
| 74 |
+
if isinstance(item, tuple):
|
| 75 |
+
# MediaGallery returns (path, caption) tuples
|
| 76 |
+
path = item[0]
|
| 77 |
+
result.append(FileWrapper(path))
|
| 78 |
+
elif hasattr(item, "name"):
|
| 79 |
+
# gr.File returns objects with .name attribute
|
| 80 |
+
result.append(item)
|
| 81 |
+
elif isinstance(item, str):
|
| 82 |
+
# Direct file path
|
| 83 |
+
result.append(FileWrapper(item))
|
| 84 |
+
else:
|
| 85 |
+
result.append(FileWrapper(str(item)))
|
| 86 |
+
return result
|
| 87 |
+
|
| 88 |
+
|
| 89 |
def get_files_infos(files):
|
| 90 |
+
files = normalize_files(files)
|
| 91 |
results = []
|
| 92 |
for file in files:
|
| 93 |
file_path = Path(file.name)
|
|
|
|
| 267 |
else:
|
| 268 |
# Use existing conversation history
|
| 269 |
messages = conversation_history[:]
|
| 270 |
+
|
| 271 |
# If there's a previous error, add it as a separate message exchange
|
| 272 |
if previous_error and previous_command:
|
| 273 |
# Add the failed command as assistant response
|
| 274 |
+
messages.append(
|
| 275 |
+
{
|
| 276 |
+
"role": "assistant",
|
| 277 |
+
"content": f"I'll execute this FFmpeg command:\n\n```bash\n{previous_command}\n```",
|
| 278 |
+
}
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
# Add the error as user feedback
|
| 282 |
+
messages.append(
|
| 283 |
+
{
|
| 284 |
+
"role": "user",
|
| 285 |
+
"content": f"""The command failed with the following error:
|
| 286 |
|
| 287 |
ERROR MESSAGE:
|
| 288 |
{previous_error}
|
|
|
|
| 300 |
- "horizontal", "landscape", "16:9", "YouTube", "TV" → Use 1920x1080 (default)
|
| 301 |
- "square", "1:1", "Instagram post" → Use 1080x1080
|
| 302 |
|
| 303 |
+
Please provide a corrected FFmpeg command.""",
|
| 304 |
+
}
|
| 305 |
+
)
|
| 306 |
else:
|
| 307 |
# Add new user request to existing conversation
|
| 308 |
+
messages.append(
|
| 309 |
+
{
|
| 310 |
+
"role": "user",
|
| 311 |
+
"content": user_content,
|
| 312 |
+
}
|
| 313 |
+
)
|
| 314 |
try:
|
| 315 |
# Print the complete prompt
|
| 316 |
print("\n=== COMPLETE PROMPT ===")
|
|
|
|
| 336 |
)
|
| 337 |
content = completion.choices[0].message.content
|
| 338 |
print(f"\n=== RAW API RESPONSE ===\n{content}\n========================\n")
|
| 339 |
+
|
| 340 |
# Extract command from code block if present
|
| 341 |
import re
|
| 342 |
+
|
| 343 |
command = None
|
| 344 |
+
|
| 345 |
# Try multiple code block patterns
|
| 346 |
code_patterns = [
|
| 347 |
r"```(?:bash|sh|shell)?\n(.*?)\n```", # Standard code blocks
|
| 348 |
r"```\n(.*?)\n```", # Plain code blocks
|
| 349 |
r"`([^`]*ffmpeg[^`]*)`", # Inline code with ffmpeg
|
| 350 |
]
|
| 351 |
+
|
| 352 |
for pattern in code_patterns:
|
| 353 |
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
|
| 354 |
for match in matches:
|
|
|
|
| 357 |
break
|
| 358 |
if command:
|
| 359 |
break
|
| 360 |
+
|
| 361 |
# If no code block found, try to find ffmpeg lines directly
|
| 362 |
if not command:
|
| 363 |
ffmpeg_lines = [
|
|
|
|
| 367 |
]
|
| 368 |
if ffmpeg_lines:
|
| 369 |
command = ffmpeg_lines[0]
|
| 370 |
+
|
| 371 |
# Last resort: look for any line containing ffmpeg
|
| 372 |
if not command:
|
| 373 |
for line in content.split("\n"):
|
|
|
|
| 375 |
if "ffmpeg" in line.lower() and len(line) > 10:
|
| 376 |
command = line
|
| 377 |
break
|
| 378 |
+
|
| 379 |
if not command:
|
| 380 |
print(f"ERROR: No ffmpeg command found in response")
|
| 381 |
command = content.replace("\n", " ").strip()
|
| 382 |
+
|
| 383 |
print(f"=== EXTRACTED COMMAND ===\n{command}\n========================\n")
|
| 384 |
+
|
| 385 |
# remove output.mp4 with the actual output file path
|
| 386 |
command = command.replace("output.mp4", "")
|
| 387 |
|
| 388 |
# Add the assistant's response to conversation history
|
| 389 |
+
messages.append({"role": "assistant", "content": content})
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
return command, messages
|
| 392 |
except Exception as e:
|
|
|
|
| 412 |
"""
|
| 413 |
Compose videos from existing media assets using natural language instructions.
|
| 414 |
|
| 415 |
+
This tool is NOT for AI video generation. Instead, it uses AI to generate FFmpeg
|
| 416 |
+
commands that combine, edit, and transform your uploaded images, videos, and audio
|
| 417 |
files based on natural language descriptions.
|
| 418 |
|
| 419 |
Args:
|
|
|
|
| 442 |
if prompt == "":
|
| 443 |
raise gr.Error("Please enter a prompt.")
|
| 444 |
|
| 445 |
+
# Normalize files from MediaGallery or gr.File format
|
| 446 |
+
files = normalize_files(files)
|
| 447 |
files_info = get_files_infos(files)
|
| 448 |
# disable this if you're running the app locally or on your own server
|
| 449 |
for file_info in files_info:
|
|
|
|
| 601 |
)
|
| 602 |
with gr.Row():
|
| 603 |
with gr.Column():
|
| 604 |
+
user_files = MediaGallery(
|
|
|
|
|
|
|
| 605 |
file_types=allowed_medias,
|
| 606 |
+
label="Media Assets",
|
| 607 |
+
columns=3,
|
| 608 |
+
interactive=True,
|
| 609 |
)
|
| 610 |
user_prompt = gr.Textbox(
|
| 611 |
placeholder="eg: Remove the 3 first seconds of the video",
|
mediagallery/.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.eggs/
|
| 2 |
+
dist/
|
| 3 |
+
*.pyc
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.py[cod]
|
| 6 |
+
*$py.class
|
| 7 |
+
__tmp/*
|
| 8 |
+
*.pyi
|
| 9 |
+
.mypycache
|
| 10 |
+
.ruff_cache
|
| 11 |
+
node_modules
|
| 12 |
+
backend/**/templates/
|
mediagallery/README.md
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# `gradio_mediagallery`
|
| 3 |
+
<a href="https://pypi.org/project/gradio_mediagallery/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_mediagallery"></a>
|
| 4 |
+
|
| 5 |
+
Python library for easily interacting with trained machine learning models
|
| 6 |
+
|
| 7 |
+
## Installation
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install gradio_mediagallery
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Usage
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
|
| 17 |
+
import gradio as gr
|
| 18 |
+
from gradio_mediagallery import MediaGallery
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
example = MediaGallery().example_value()
|
| 22 |
+
|
| 23 |
+
with gr.Blocks() as demo:
|
| 24 |
+
with gr.Row():
|
| 25 |
+
MediaGallery(label="Blank"), # blank component
|
| 26 |
+
MediaGallery(value=example, label="Populated"), # populated component
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
demo.launch()
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## `MediaGallery`
|
| 35 |
+
|
| 36 |
+
### Initialization
|
| 37 |
+
|
| 38 |
+
<table>
|
| 39 |
+
<thead>
|
| 40 |
+
<tr>
|
| 41 |
+
<th align="left">name</th>
|
| 42 |
+
<th align="left" style="width: 25%;">type</th>
|
| 43 |
+
<th align="left">default</th>
|
| 44 |
+
<th align="left">description</th>
|
| 45 |
+
</tr>
|
| 46 |
+
</thead>
|
| 47 |
+
<tbody>
|
| 48 |
+
<tr>
|
| 49 |
+
<td align="left"><code>value</code></td>
|
| 50 |
+
<td align="left" style="width: 25%;">
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
Sequence[
|
| 54 |
+
np.ndarray | PIL.Image.Image | str | Path | tuple
|
| 55 |
+
]
|
| 56 |
+
| Callable
|
| 57 |
+
| None
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
</td>
|
| 61 |
+
<td align="left"><code>None</code></td>
|
| 62 |
+
<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>
|
| 63 |
+
</tr>
|
| 64 |
+
|
| 65 |
+
<tr>
|
| 66 |
+
<td align="left"><code>format</code></td>
|
| 67 |
+
<td align="left" style="width: 25%;">
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
str
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
</td>
|
| 74 |
+
<td align="left"><code>"webp"</code></td>
|
| 75 |
+
<td align="left">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.</td>
|
| 76 |
+
</tr>
|
| 77 |
+
|
| 78 |
+
<tr>
|
| 79 |
+
<td align="left"><code>file_types</code></td>
|
| 80 |
+
<td align="left" style="width: 25%;">
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
list[str] | None
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
</td>
|
| 87 |
+
<td align="left"><code>None</code></td>
|
| 88 |
+
<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>
|
| 89 |
+
</tr>
|
| 90 |
+
|
| 91 |
+
<tr>
|
| 92 |
+
<td align="left"><code>label</code></td>
|
| 93 |
+
<td align="left" style="width: 25%;">
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
str | I18nData | None
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
</td>
|
| 100 |
+
<td align="left"><code>None</code></td>
|
| 101 |
+
<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>
|
| 102 |
+
</tr>
|
| 103 |
+
|
| 104 |
+
<tr>
|
| 105 |
+
<td align="left"><code>every</code></td>
|
| 106 |
+
<td align="left" style="width: 25%;">
|
| 107 |
+
|
| 108 |
+
```python
|
| 109 |
+
Timer | float | None
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
</td>
|
| 113 |
+
<td align="left"><code>None</code></td>
|
| 114 |
+
<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>
|
| 115 |
+
</tr>
|
| 116 |
+
|
| 117 |
+
<tr>
|
| 118 |
+
<td align="left"><code>inputs</code></td>
|
| 119 |
+
<td align="left" style="width: 25%;">
|
| 120 |
+
|
| 121 |
+
```python
|
| 122 |
+
Component | Sequence[Component] | set[Component] | None
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
</td>
|
| 126 |
+
<td align="left"><code>None</code></td>
|
| 127 |
+
<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>
|
| 128 |
+
</tr>
|
| 129 |
+
|
| 130 |
+
<tr>
|
| 131 |
+
<td align="left"><code>show_label</code></td>
|
| 132 |
+
<td align="left" style="width: 25%;">
|
| 133 |
+
|
| 134 |
+
```python
|
| 135 |
+
bool | None
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
</td>
|
| 139 |
+
<td align="left"><code>None</code></td>
|
| 140 |
+
<td align="left">if True, will display label.</td>
|
| 141 |
+
</tr>
|
| 142 |
+
|
| 143 |
+
<tr>
|
| 144 |
+
<td align="left"><code>container</code></td>
|
| 145 |
+
<td align="left" style="width: 25%;">
|
| 146 |
+
|
| 147 |
+
```python
|
| 148 |
+
bool
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
</td>
|
| 152 |
+
<td align="left"><code>True</code></td>
|
| 153 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 154 |
+
</tr>
|
| 155 |
+
|
| 156 |
+
<tr>
|
| 157 |
+
<td align="left"><code>scale</code></td>
|
| 158 |
+
<td align="left" style="width: 25%;">
|
| 159 |
+
|
| 160 |
+
```python
|
| 161 |
+
int | None
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
</td>
|
| 165 |
+
<td align="left"><code>None</code></td>
|
| 166 |
+
<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>
|
| 167 |
+
</tr>
|
| 168 |
+
|
| 169 |
+
<tr>
|
| 170 |
+
<td align="left"><code>min_width</code></td>
|
| 171 |
+
<td align="left" style="width: 25%;">
|
| 172 |
+
|
| 173 |
+
```python
|
| 174 |
+
int
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
</td>
|
| 178 |
+
<td align="left"><code>160</code></td>
|
| 179 |
+
<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>
|
| 180 |
+
</tr>
|
| 181 |
+
|
| 182 |
+
<tr>
|
| 183 |
+
<td align="left"><code>visible</code></td>
|
| 184 |
+
<td align="left" style="width: 25%;">
|
| 185 |
+
|
| 186 |
+
```python
|
| 187 |
+
bool
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
</td>
|
| 191 |
+
<td align="left"><code>True</code></td>
|
| 192 |
+
<td align="left">If False, component will be hidden.</td>
|
| 193 |
+
</tr>
|
| 194 |
+
|
| 195 |
+
<tr>
|
| 196 |
+
<td align="left"><code>elem_id</code></td>
|
| 197 |
+
<td align="left" style="width: 25%;">
|
| 198 |
+
|
| 199 |
+
```python
|
| 200 |
+
str | None
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
</td>
|
| 204 |
+
<td align="left"><code>None</code></td>
|
| 205 |
+
<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>
|
| 206 |
+
</tr>
|
| 207 |
+
|
| 208 |
+
<tr>
|
| 209 |
+
<td align="left"><code>elem_classes</code></td>
|
| 210 |
+
<td align="left" style="width: 25%;">
|
| 211 |
+
|
| 212 |
+
```python
|
| 213 |
+
list[str] | str | None
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
</td>
|
| 217 |
+
<td align="left"><code>None</code></td>
|
| 218 |
+
<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>
|
| 219 |
+
</tr>
|
| 220 |
+
|
| 221 |
+
<tr>
|
| 222 |
+
<td align="left"><code>render</code></td>
|
| 223 |
+
<td align="left" style="width: 25%;">
|
| 224 |
+
|
| 225 |
+
```python
|
| 226 |
+
bool
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
</td>
|
| 230 |
+
<td align="left"><code>True</code></td>
|
| 231 |
+
<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>
|
| 232 |
+
</tr>
|
| 233 |
+
|
| 234 |
+
<tr>
|
| 235 |
+
<td align="left"><code>key</code></td>
|
| 236 |
+
<td align="left" style="width: 25%;">
|
| 237 |
+
|
| 238 |
+
```python
|
| 239 |
+
int | str | tuple[int | str, ...] | None
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
</td>
|
| 243 |
+
<td align="left"><code>None</code></td>
|
| 244 |
+
<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>
|
| 245 |
+
</tr>
|
| 246 |
+
|
| 247 |
+
<tr>
|
| 248 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 249 |
+
<td align="left" style="width: 25%;">
|
| 250 |
+
|
| 251 |
+
```python
|
| 252 |
+
list[str] | str | None
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
</td>
|
| 256 |
+
<td align="left"><code>"value"</code></td>
|
| 257 |
+
<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>
|
| 258 |
+
</tr>
|
| 259 |
+
|
| 260 |
+
<tr>
|
| 261 |
+
<td align="left"><code>columns</code></td>
|
| 262 |
+
<td align="left" style="width: 25%;">
|
| 263 |
+
|
| 264 |
+
```python
|
| 265 |
+
int | None
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
</td>
|
| 269 |
+
<td align="left"><code>2</code></td>
|
| 270 |
+
<td align="left">Represents the number of images that should be shown in one row.</td>
|
| 271 |
+
</tr>
|
| 272 |
+
|
| 273 |
+
<tr>
|
| 274 |
+
<td align="left"><code>rows</code></td>
|
| 275 |
+
<td align="left" style="width: 25%;">
|
| 276 |
+
|
| 277 |
+
```python
|
| 278 |
+
int | None
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
</td>
|
| 282 |
+
<td align="left"><code>None</code></td>
|
| 283 |
+
<td align="left">Represents the number of rows in the image grid.</td>
|
| 284 |
+
</tr>
|
| 285 |
+
|
| 286 |
+
<tr>
|
| 287 |
+
<td align="left"><code>height</code></td>
|
| 288 |
+
<td align="left" style="width: 25%;">
|
| 289 |
+
|
| 290 |
+
```python
|
| 291 |
+
int | float | str | None
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
</td>
|
| 295 |
+
<td align="left"><code>None</code></td>
|
| 296 |
+
<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>
|
| 297 |
+
</tr>
|
| 298 |
+
|
| 299 |
+
<tr>
|
| 300 |
+
<td align="left"><code>allow_preview</code></td>
|
| 301 |
+
<td align="left" style="width: 25%;">
|
| 302 |
+
|
| 303 |
+
```python
|
| 304 |
+
bool
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
</td>
|
| 308 |
+
<td align="left"><code>True</code></td>
|
| 309 |
+
<td align="left">If True, images in the gallery will be enlarged when they are clicked. Default is True.</td>
|
| 310 |
+
</tr>
|
| 311 |
+
|
| 312 |
+
<tr>
|
| 313 |
+
<td align="left"><code>preview</code></td>
|
| 314 |
+
<td align="left" style="width: 25%;">
|
| 315 |
+
|
| 316 |
+
```python
|
| 317 |
+
bool | None
|
| 318 |
+
```
|
| 319 |
+
|
| 320 |
+
</td>
|
| 321 |
+
<td align="left"><code>None</code></td>
|
| 322 |
+
<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>
|
| 323 |
+
</tr>
|
| 324 |
+
|
| 325 |
+
<tr>
|
| 326 |
+
<td align="left"><code>selected_index</code></td>
|
| 327 |
+
<td align="left" style="width: 25%;">
|
| 328 |
+
|
| 329 |
+
```python
|
| 330 |
+
int | None
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
</td>
|
| 334 |
+
<td align="left"><code>None</code></td>
|
| 335 |
+
<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>
|
| 336 |
+
</tr>
|
| 337 |
+
|
| 338 |
+
<tr>
|
| 339 |
+
<td align="left"><code>object_fit</code></td>
|
| 340 |
+
<td align="left" style="width: 25%;">
|
| 341 |
+
|
| 342 |
+
```python
|
| 343 |
+
Literal[
|
| 344 |
+
"contain", "cover", "fill", "none", "scale-down"
|
| 345 |
+
]
|
| 346 |
+
| None
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
</td>
|
| 350 |
+
<td align="left"><code>None</code></td>
|
| 351 |
+
<td align="left">CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".</td>
|
| 352 |
+
</tr>
|
| 353 |
+
|
| 354 |
+
<tr>
|
| 355 |
+
<td align="left"><code>show_share_button</code></td>
|
| 356 |
+
<td align="left" style="width: 25%;">
|
| 357 |
+
|
| 358 |
+
```python
|
| 359 |
+
bool | None
|
| 360 |
+
```
|
| 361 |
+
|
| 362 |
+
</td>
|
| 363 |
+
<td align="left"><code>None</code></td>
|
| 364 |
+
<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>
|
| 365 |
+
</tr>
|
| 366 |
+
|
| 367 |
+
<tr>
|
| 368 |
+
<td align="left"><code>show_download_button</code></td>
|
| 369 |
+
<td align="left" style="width: 25%;">
|
| 370 |
+
|
| 371 |
+
```python
|
| 372 |
+
bool | None
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
</td>
|
| 376 |
+
<td align="left"><code>True</code></td>
|
| 377 |
+
<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>
|
| 378 |
+
</tr>
|
| 379 |
+
|
| 380 |
+
<tr>
|
| 381 |
+
<td align="left"><code>interactive</code></td>
|
| 382 |
+
<td align="left" style="width: 25%;">
|
| 383 |
+
|
| 384 |
+
```python
|
| 385 |
+
bool | None
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
</td>
|
| 389 |
+
<td align="left"><code>None</code></td>
|
| 390 |
+
<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>
|
| 391 |
+
</tr>
|
| 392 |
+
|
| 393 |
+
<tr>
|
| 394 |
+
<td align="left"><code>type</code></td>
|
| 395 |
+
<td align="left" style="width: 25%;">
|
| 396 |
+
|
| 397 |
+
```python
|
| 398 |
+
Literal["numpy", "pil", "filepath"]
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
</td>
|
| 402 |
+
<td align="left"><code>"filepath"</code></td>
|
| 403 |
+
<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>
|
| 404 |
+
</tr>
|
| 405 |
+
|
| 406 |
+
<tr>
|
| 407 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 408 |
+
<td align="left" style="width: 25%;">
|
| 409 |
+
|
| 410 |
+
```python
|
| 411 |
+
bool
|
| 412 |
+
```
|
| 413 |
+
|
| 414 |
+
</td>
|
| 415 |
+
<td align="left"><code>True</code></td>
|
| 416 |
+
<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>
|
| 417 |
+
</tr>
|
| 418 |
+
</tbody></table>
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
### Events
|
| 422 |
+
|
| 423 |
+
| name | description |
|
| 424 |
+
|:-----|:------------|
|
| 425 |
+
| `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 |
|
| 426 |
+
| `upload` | This listener is triggered when the user uploads a file into the MediaGallery. |
|
| 427 |
+
| `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. |
|
| 428 |
+
| `preview_close` | This event is triggered when the MediaGallery preview is closed by the user |
|
| 429 |
+
| `preview_open` | This event is triggered when the MediaGallery preview is opened by the user |
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
### User function
|
| 434 |
+
|
| 435 |
+
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).
|
| 436 |
+
|
| 437 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 438 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 439 |
+
|
| 440 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 441 |
+
|
| 442 |
+
- **As output:** Is passed, passes the list of images or videos as a list of (media, caption) tuples, or a list of (media, None) tuples if no captions are provided (which is usually the case). Images can be a `str` file path, a `numpy` array, or a `PIL.Image` object depending on `type`. Videos are always `str` file path.
|
| 443 |
+
- **As input:** Should return, expects the function to return a `list` of images or videos, or `list` of (media, `str` caption) tuples. Each image can be a `str` file path, a `numpy` array, or a `PIL.Image` object. Each video can be a `str` file path.
|
| 444 |
+
|
| 445 |
+
```python
|
| 446 |
+
def predict(
|
| 447 |
+
value: list[tuple[str, str | None]]
|
| 448 |
+
| list[tuple[PIL.Image.Image, str | None]]
|
| 449 |
+
| list[tuple[numpy.ndarray, str | None]]
|
| 450 |
+
| None
|
| 451 |
+
) -> list[
|
| 452 |
+
typing.Union[
|
| 453 |
+
numpy.ndarray,
|
| 454 |
+
PIL.Image.Image,
|
| 455 |
+
pathlib.Path,
|
| 456 |
+
str,
|
| 457 |
+
tuple[
|
| 458 |
+
typing.Union[
|
| 459 |
+
numpy.ndarray,
|
| 460 |
+
PIL.Image.Image,
|
| 461 |
+
pathlib.Path,
|
| 462 |
+
str,
|
| 463 |
+
],
|
| 464 |
+
str,
|
| 465 |
+
],
|
| 466 |
+
][
|
| 467 |
+
numpy.ndarray,
|
| 468 |
+
PIL.Image.Image,
|
| 469 |
+
pathlib.Path,
|
| 470 |
+
str,
|
| 471 |
+
tuple[
|
| 472 |
+
typing.Union[
|
| 473 |
+
numpy.ndarray,
|
| 474 |
+
PIL.Image.Image,
|
| 475 |
+
pathlib.Path,
|
| 476 |
+
str,
|
| 477 |
+
][
|
| 478 |
+
numpy.ndarray,
|
| 479 |
+
PIL.Image.Image,
|
| 480 |
+
pathlib.Path,
|
| 481 |
+
str,
|
| 482 |
+
],
|
| 483 |
+
str,
|
| 484 |
+
],
|
| 485 |
+
]
|
| 486 |
+
]
|
| 487 |
+
| None:
|
| 488 |
+
return value
|
| 489 |
+
```
|
| 490 |
+
|
mediagallery/backend/gradio_mediagallery/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from .mediagallery import MediaGallery
|
| 3 |
+
|
| 4 |
+
__all__ = ['MediaGallery']
|
mediagallery/backend/gradio_mediagallery/mediagallery.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""gr.Gallery() component."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Callable, Sequence
|
| 6 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import (
|
| 9 |
+
TYPE_CHECKING,
|
| 10 |
+
Any,
|
| 11 |
+
Literal,
|
| 12 |
+
Optional,
|
| 13 |
+
Union,
|
| 14 |
+
)
|
| 15 |
+
from urllib.parse import quote, urlparse
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import PIL.Image
|
| 19 |
+
from gradio_client import handle_file
|
| 20 |
+
from gradio_client import utils as client_utils
|
| 21 |
+
from gradio_client.documentation import document
|
| 22 |
+
from gradio_client.utils import is_http_url_like
|
| 23 |
+
|
| 24 |
+
from gradio import image_utils, processing_utils, utils
|
| 25 |
+
try:
|
| 26 |
+
from gradio import wasm_utils
|
| 27 |
+
IS_WASM = wasm_utils.IS_WASM
|
| 28 |
+
except ImportError:
|
| 29 |
+
IS_WASM = False
|
| 30 |
+
from gradio.components.base import Component
|
| 31 |
+
from gradio.data_classes import FileData, GradioModel, GradioRootModel, ImageData
|
| 32 |
+
from gradio.events import EventListener, Events
|
| 33 |
+
from gradio.exceptions import Error
|
| 34 |
+
from gradio.i18n import I18nData
|
| 35 |
+
|
| 36 |
+
if TYPE_CHECKING:
|
| 37 |
+
from gradio.components import Timer
|
| 38 |
+
|
| 39 |
+
GalleryMediaType = Union[np.ndarray, PIL.Image.Image, Path, str]
|
| 40 |
+
CaptionedGalleryMediaType = tuple[GalleryMediaType, str]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class GalleryImage(GradioModel):
|
| 44 |
+
image: ImageData
|
| 45 |
+
caption: Optional[str] = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class GalleryVideo(GradioModel):
|
| 49 |
+
video: FileData
|
| 50 |
+
caption: Optional[str] = None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class GalleryAudio(GradioModel):
|
| 54 |
+
audio: FileData
|
| 55 |
+
caption: Optional[str] = None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class GalleryData(GradioRootModel):
|
| 59 |
+
root: list[Union[GalleryImage, GalleryVideo, GalleryAudio]]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# File extension mappings for media type detection
|
| 63 |
+
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff', '.svg'}
|
| 64 |
+
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm', '.mpg', '.mpeg', '.m4v', '.3gp', '.3g2', '.3gpp'}
|
| 65 |
+
AUDIO_EXTENSIONS = {'.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class MediaGallery(Component):
|
| 69 |
+
"""
|
| 70 |
+
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.
|
| 71 |
+
If used as an output, the user can click on individual images or videos to view them at a higher resolution.
|
| 72 |
+
|
| 73 |
+
Demos: fake_gan
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
EVENTS = [
|
| 77 |
+
Events.select,
|
| 78 |
+
Events.upload,
|
| 79 |
+
Events.change,
|
| 80 |
+
EventListener(
|
| 81 |
+
"preview_close",
|
| 82 |
+
doc="This event is triggered when the MediaGallery preview is closed by the user",
|
| 83 |
+
),
|
| 84 |
+
EventListener(
|
| 85 |
+
"preview_open",
|
| 86 |
+
doc="This event is triggered when the MediaGallery preview is opened by the user",
|
| 87 |
+
),
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
data_model = GalleryData
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
value: (
|
| 95 |
+
Sequence[np.ndarray | PIL.Image.Image | str | Path | tuple]
|
| 96 |
+
| Callable
|
| 97 |
+
| None
|
| 98 |
+
) = None,
|
| 99 |
+
*,
|
| 100 |
+
format: str = "webp",
|
| 101 |
+
file_types: list[str] | None = None,
|
| 102 |
+
label: str | I18nData | None = None,
|
| 103 |
+
every: Timer | float | None = None,
|
| 104 |
+
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
| 105 |
+
show_label: bool | None = None,
|
| 106 |
+
container: bool = True,
|
| 107 |
+
scale: int | None = None,
|
| 108 |
+
min_width: int = 160,
|
| 109 |
+
visible: bool = True,
|
| 110 |
+
elem_id: str | None = None,
|
| 111 |
+
elem_classes: list[str] | str | None = None,
|
| 112 |
+
render: bool = True,
|
| 113 |
+
key: int | str | tuple[int | str, ...] | None = None,
|
| 114 |
+
preserved_by_key: list[str] | str | None = "value",
|
| 115 |
+
columns: int | None = 2,
|
| 116 |
+
rows: int | None = None,
|
| 117 |
+
height: int | float | str | None = None,
|
| 118 |
+
allow_preview: bool = True,
|
| 119 |
+
preview: bool | None = None,
|
| 120 |
+
selected_index: int | None = None,
|
| 121 |
+
object_fit: (
|
| 122 |
+
Literal["contain", "cover", "fill", "none", "scale-down"] | None
|
| 123 |
+
) = None,
|
| 124 |
+
show_share_button: bool | None = None,
|
| 125 |
+
show_download_button: bool | None = True,
|
| 126 |
+
interactive: bool | None = None,
|
| 127 |
+
type: Literal["numpy", "pil", "filepath"] = "filepath",
|
| 128 |
+
show_fullscreen_button: bool = True,
|
| 129 |
+
):
|
| 130 |
+
"""
|
| 131 |
+
Parameters:
|
| 132 |
+
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.
|
| 133 |
+
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.
|
| 134 |
+
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.
|
| 135 |
+
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.
|
| 136 |
+
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.
|
| 137 |
+
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.
|
| 138 |
+
show_label: if True, will display label.
|
| 139 |
+
container: If True, will place the component in a container - providing some extra padding around the border.
|
| 140 |
+
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.
|
| 141 |
+
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.
|
| 142 |
+
visible: If False, component will be hidden.
|
| 143 |
+
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.
|
| 144 |
+
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.
|
| 145 |
+
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.
|
| 146 |
+
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.
|
| 147 |
+
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.
|
| 148 |
+
columns: Represents the number of images that should be shown in one row.
|
| 149 |
+
rows: Represents the number of rows in the image grid.
|
| 150 |
+
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.
|
| 151 |
+
allow_preview: If True, images in the gallery will be enlarged when they are clicked. Default is True.
|
| 152 |
+
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.
|
| 153 |
+
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.
|
| 154 |
+
object_fit: CSS object-fit property for the thumbnail images in the gallery. Can be "contain", "cover", "fill", "none", or "scale-down".
|
| 155 |
+
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.
|
| 156 |
+
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.
|
| 157 |
+
interactive: If True, the gallery will be interactive, allowing the user to upload images. If False, the gallery will be static. Default is True.
|
| 158 |
+
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.
|
| 159 |
+
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.
|
| 160 |
+
"""
|
| 161 |
+
self.format = format
|
| 162 |
+
self.columns = columns
|
| 163 |
+
self.rows = rows
|
| 164 |
+
self.height = height
|
| 165 |
+
self.preview = preview
|
| 166 |
+
self.object_fit = object_fit
|
| 167 |
+
self.allow_preview = allow_preview
|
| 168 |
+
self.show_download_button = (
|
| 169 |
+
(utils.get_space() is not None)
|
| 170 |
+
if show_download_button is None
|
| 171 |
+
else show_download_button
|
| 172 |
+
)
|
| 173 |
+
self.selected_index = selected_index
|
| 174 |
+
self.type = type
|
| 175 |
+
self.show_fullscreen_button = show_fullscreen_button
|
| 176 |
+
self.file_types = file_types
|
| 177 |
+
|
| 178 |
+
self.show_share_button = (
|
| 179 |
+
(utils.get_space() is not None)
|
| 180 |
+
if show_share_button is None
|
| 181 |
+
else show_share_button
|
| 182 |
+
)
|
| 183 |
+
super().__init__(
|
| 184 |
+
label=label,
|
| 185 |
+
every=every,
|
| 186 |
+
inputs=inputs,
|
| 187 |
+
show_label=show_label,
|
| 188 |
+
container=container,
|
| 189 |
+
scale=scale,
|
| 190 |
+
min_width=min_width,
|
| 191 |
+
visible=visible,
|
| 192 |
+
elem_id=elem_id,
|
| 193 |
+
elem_classes=elem_classes,
|
| 194 |
+
render=render,
|
| 195 |
+
key=key,
|
| 196 |
+
preserved_by_key=preserved_by_key,
|
| 197 |
+
value=value,
|
| 198 |
+
interactive=interactive,
|
| 199 |
+
)
|
| 200 |
+
self._value_description = f"a list of {'string filepaths' if type == 'filepath' else 'numpy arrays' if type == 'numpy' else 'PIL images'}"
|
| 201 |
+
|
| 202 |
+
def preprocess(
|
| 203 |
+
self, payload: GalleryData | None
|
| 204 |
+
) -> (
|
| 205 |
+
list[tuple[str, str | None]]
|
| 206 |
+
| list[tuple[PIL.Image.Image, str | None]]
|
| 207 |
+
| list[tuple[np.ndarray, str | None]]
|
| 208 |
+
| None
|
| 209 |
+
):
|
| 210 |
+
"""
|
| 211 |
+
Parameters:
|
| 212 |
+
payload: a list of images or videos, or list of (media, caption) tuples
|
| 213 |
+
Returns:
|
| 214 |
+
Passes the list of images or videos as a list of (media, caption) tuples, or a list of (media, None) tuples if no captions are provided (which is usually the case). Images can be a `str` file path, a `numpy` array, or a `PIL.Image` object depending on `type`. Videos are always `str` file path.
|
| 215 |
+
"""
|
| 216 |
+
if payload is None or not payload.root:
|
| 217 |
+
return None
|
| 218 |
+
data = []
|
| 219 |
+
for gallery_element in payload.root:
|
| 220 |
+
if isinstance(gallery_element, GalleryVideo):
|
| 221 |
+
file_path = gallery_element.video.path
|
| 222 |
+
elif isinstance(gallery_element, GalleryAudio):
|
| 223 |
+
file_path = gallery_element.audio.path
|
| 224 |
+
else:
|
| 225 |
+
file_path = gallery_element.image.path or ""
|
| 226 |
+
if self.file_types and not client_utils.is_valid_file(
|
| 227 |
+
file_path, self.file_types
|
| 228 |
+
):
|
| 229 |
+
raise Error(
|
| 230 |
+
f"Invalid file type. Please upload a file that is one of these formats: {self.file_types}"
|
| 231 |
+
)
|
| 232 |
+
else:
|
| 233 |
+
# Return file path for video and audio, convert images based on type
|
| 234 |
+
if isinstance(gallery_element, GalleryVideo):
|
| 235 |
+
media = gallery_element.video.path
|
| 236 |
+
elif isinstance(gallery_element, GalleryAudio):
|
| 237 |
+
media = gallery_element.audio.path
|
| 238 |
+
else:
|
| 239 |
+
media = self.convert_to_type(gallery_element.image.path, self.type) # type: ignore
|
| 240 |
+
data.append((media, gallery_element.caption))
|
| 241 |
+
return data
|
| 242 |
+
|
| 243 |
+
def postprocess(
|
| 244 |
+
self,
|
| 245 |
+
value: list[GalleryMediaType | CaptionedGalleryMediaType] | None,
|
| 246 |
+
) -> GalleryData:
|
| 247 |
+
"""
|
| 248 |
+
Parameters:
|
| 249 |
+
value: Expects the function to return a `list` of images or videos, or `list` of (media, `str` caption) tuples. Each image can be a `str` file path, a `numpy` array, or a `PIL.Image` object. Each video can be a `str` file path.
|
| 250 |
+
Returns:
|
| 251 |
+
a list of images or videos, or list of (media, caption) tuples
|
| 252 |
+
"""
|
| 253 |
+
if value is None:
|
| 254 |
+
return GalleryData(root=[])
|
| 255 |
+
if isinstance(value, str):
|
| 256 |
+
raise ValueError(
|
| 257 |
+
"The `value` passed into `gr.Gallery` must be a list of images or videos, or list of (media, caption) tuples."
|
| 258 |
+
)
|
| 259 |
+
output = []
|
| 260 |
+
|
| 261 |
+
def _save(img):
|
| 262 |
+
url = None
|
| 263 |
+
caption = None
|
| 264 |
+
orig_name = None
|
| 265 |
+
mime_type = None
|
| 266 |
+
if isinstance(img, (tuple, list)):
|
| 267 |
+
img, caption = img
|
| 268 |
+
if isinstance(img, np.ndarray):
|
| 269 |
+
file = processing_utils.save_img_array_to_cache(
|
| 270 |
+
img, cache_dir=self.GRADIO_CACHE, format=self.format
|
| 271 |
+
)
|
| 272 |
+
file_path = str(utils.abspath(file))
|
| 273 |
+
elif isinstance(img, PIL.Image.Image):
|
| 274 |
+
file = processing_utils.save_pil_to_cache(
|
| 275 |
+
img, cache_dir=self.GRADIO_CACHE, format=self.format
|
| 276 |
+
)
|
| 277 |
+
file_path = str(utils.abspath(file))
|
| 278 |
+
elif isinstance(img, str):
|
| 279 |
+
mime_type = client_utils.get_mimetype(img)
|
| 280 |
+
if img.lower().endswith(".svg"):
|
| 281 |
+
svg_content = image_utils.extract_svg_content(img)
|
| 282 |
+
orig_name = Path(img).name
|
| 283 |
+
url = f"data:image/svg+xml,{quote(svg_content)}"
|
| 284 |
+
file_path = None
|
| 285 |
+
elif is_http_url_like(img):
|
| 286 |
+
url = img
|
| 287 |
+
orig_name = Path(urlparse(img).path).name
|
| 288 |
+
file_path = img
|
| 289 |
+
else:
|
| 290 |
+
url = None
|
| 291 |
+
orig_name = Path(img).name
|
| 292 |
+
file_path = img
|
| 293 |
+
elif isinstance(img, Path):
|
| 294 |
+
file_path = str(img)
|
| 295 |
+
orig_name = img.name
|
| 296 |
+
mime_type = client_utils.get_mimetype(file_path)
|
| 297 |
+
else:
|
| 298 |
+
raise ValueError(f"Cannot process type as image: {type(img)}")
|
| 299 |
+
# Determine media type from mime_type or file extension
|
| 300 |
+
if mime_type is not None and "video" in mime_type:
|
| 301 |
+
return GalleryVideo(
|
| 302 |
+
video=FileData(
|
| 303 |
+
path=file_path, # type: ignore
|
| 304 |
+
url=url,
|
| 305 |
+
orig_name=orig_name,
|
| 306 |
+
mime_type=mime_type,
|
| 307 |
+
),
|
| 308 |
+
caption=caption,
|
| 309 |
+
)
|
| 310 |
+
elif mime_type is not None and "audio" in mime_type:
|
| 311 |
+
return GalleryAudio(
|
| 312 |
+
audio=FileData(
|
| 313 |
+
path=file_path, # type: ignore
|
| 314 |
+
url=url,
|
| 315 |
+
orig_name=orig_name,
|
| 316 |
+
mime_type=mime_type,
|
| 317 |
+
),
|
| 318 |
+
caption=caption,
|
| 319 |
+
)
|
| 320 |
+
else:
|
| 321 |
+
# Check file extension for audio files (fallback)
|
| 322 |
+
ext = Path(orig_name or file_path or "").suffix.lower() if (orig_name or file_path) else ""
|
| 323 |
+
if ext in AUDIO_EXTENSIONS:
|
| 324 |
+
return GalleryAudio(
|
| 325 |
+
audio=FileData(
|
| 326 |
+
path=file_path, # type: ignore
|
| 327 |
+
url=url,
|
| 328 |
+
orig_name=orig_name,
|
| 329 |
+
mime_type=mime_type or "audio/mpeg",
|
| 330 |
+
),
|
| 331 |
+
caption=caption,
|
| 332 |
+
)
|
| 333 |
+
return GalleryImage(
|
| 334 |
+
image=ImageData(
|
| 335 |
+
path=file_path,
|
| 336 |
+
url=url,
|
| 337 |
+
orig_name=orig_name,
|
| 338 |
+
mime_type=mime_type,
|
| 339 |
+
),
|
| 340 |
+
caption=caption,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
if IS_WASM:
|
| 344 |
+
for img in value:
|
| 345 |
+
output.append(_save(img))
|
| 346 |
+
else:
|
| 347 |
+
with ThreadPoolExecutor() as executor:
|
| 348 |
+
for o in executor.map(_save, value):
|
| 349 |
+
output.append(o)
|
| 350 |
+
return GalleryData(root=output)
|
| 351 |
+
|
| 352 |
+
@staticmethod
|
| 353 |
+
def convert_to_type(img: str, type: Literal["filepath", "numpy", "pil"]):
|
| 354 |
+
if type == "filepath":
|
| 355 |
+
return img
|
| 356 |
+
else:
|
| 357 |
+
converted_image = PIL.Image.open(img)
|
| 358 |
+
if type == "numpy":
|
| 359 |
+
converted_image = np.array(converted_image)
|
| 360 |
+
return converted_image
|
| 361 |
+
|
| 362 |
+
def example_payload(self) -> Any:
|
| 363 |
+
return [
|
| 364 |
+
{
|
| 365 |
+
"image": handle_file(
|
| 366 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 367 |
+
)
|
| 368 |
+
},
|
| 369 |
+
]
|
| 370 |
+
|
| 371 |
+
def example_value(self) -> Any:
|
| 372 |
+
return [
|
| 373 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 374 |
+
]
|
mediagallery/demo/__init__.py
ADDED
|
File without changes
|
mediagallery/demo/app.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from gradio_mediagallery import MediaGallery
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
example = MediaGallery().example_value()
|
| 7 |
+
|
| 8 |
+
with gr.Blocks() as demo:
|
| 9 |
+
with gr.Row():
|
| 10 |
+
MediaGallery(label="Blank"), # blank component
|
| 11 |
+
MediaGallery(value=example, label="Populated"), # populated component
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
if __name__ == "__main__":
|
| 15 |
+
demo.launch()
|
mediagallery/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 |
+
}
|
mediagallery/demo/space.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.'}, 'format': {'type': 'str', 'default': '"webp"', 'description': "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."}, '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', 'default': 'True', 'description': 'If False, component will be hidden.'}, '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.'}}, 'postprocess': {'value': {'type': 'list[\n typing.Union[\n numpy.ndarray,\n PIL.Image.Image,\n pathlib.Path,\n str,\n tuple[\n typing.Union[\n numpy.ndarray,\n PIL.Image.Image,\n pathlib.Path,\n str,\n ],\n str,\n ],\n ][\n numpy.ndarray,\n PIL.Image.Image,\n pathlib.Path,\n str,\n tuple[\n typing.Union[\n numpy.ndarray,\n PIL.Image.Image,\n pathlib.Path,\n str,\n ][\n numpy.ndarray,\n PIL.Image.Image,\n pathlib.Path,\n str,\n ],\n str,\n ],\n ]\n ]\n | None', 'description': 'Expects the function to return a `list` of images or videos, or `list` of (media, `str` caption) tuples. Each image can be a `str` file path, a `numpy` array, or a `PIL.Image` object. Each video can be a `str` file path.'}}, 'preprocess': {'return': {'type': 'list[tuple[str, str | None]]\n | list[tuple[PIL.Image.Image, str | None]]\n | list[tuple[numpy.ndarray, str | None]]\n | None', 'description': 'Passes the list of images or videos as a list of (media, caption) tuples, or a list of (media, None) tuples if no captions are provided (which is usually the case). Images can be a `str` file path, a `numpy` array, or a `PIL.Image` object depending on `type`. Videos are always `str` file path.'}, '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'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the MediaGallery.'}, '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.'}, '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'}}}, '__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.Default(
|
| 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 |
+
|
| 42 |
+
import gradio as gr
|
| 43 |
+
from gradio_mediagallery import MediaGallery
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
example = MediaGallery().example_value()
|
| 47 |
+
|
| 48 |
+
with gr.Blocks() as demo:
|
| 49 |
+
with gr.Row():
|
| 50 |
+
MediaGallery(label="Blank"), # blank component
|
| 51 |
+
MediaGallery(value=example, label="Populated"), # populated component
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
demo.launch()
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
gr.Markdown("""
|
| 62 |
+
## `MediaGallery`
|
| 63 |
+
|
| 64 |
+
### Initialization
|
| 65 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 66 |
+
|
| 67 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["members"]["__init__"], linkify=[])
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
gr.Markdown("### Events")
|
| 71 |
+
gr.ParamViewer(value=_docs["MediaGallery"]["events"], linkify=['Event'])
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
gr.Markdown("""
|
| 77 |
+
|
| 78 |
+
### User function
|
| 79 |
+
|
| 80 |
+
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).
|
| 81 |
+
|
| 82 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 83 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 84 |
+
|
| 85 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 86 |
+
|
| 87 |
+
- **As input:** Is passed, passes the list of images or videos as a list of (media, caption) tuples, or a list of (media, None) tuples if no captions are provided (which is usually the case). Images can be a `str` file path, a `numpy` array, or a `PIL.Image` object depending on `type`. Videos are always `str` file path.
|
| 88 |
+
- **As output:** Should return, expects the function to return a `list` of images or videos, or `list` of (media, `str` caption) tuples. Each image can be a `str` file path, a `numpy` array, or a `PIL.Image` object. Each video can be a `str` file path.
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
def predict(
|
| 92 |
+
value: list[tuple[str, str | None]]
|
| 93 |
+
| list[tuple[PIL.Image.Image, str | None]]
|
| 94 |
+
| list[tuple[numpy.ndarray, str | None]]
|
| 95 |
+
| None
|
| 96 |
+
) -> list[
|
| 97 |
+
typing.Union[
|
| 98 |
+
numpy.ndarray,
|
| 99 |
+
PIL.Image.Image,
|
| 100 |
+
pathlib.Path,
|
| 101 |
+
str,
|
| 102 |
+
tuple[
|
| 103 |
+
typing.Union[
|
| 104 |
+
numpy.ndarray,
|
| 105 |
+
PIL.Image.Image,
|
| 106 |
+
pathlib.Path,
|
| 107 |
+
str,
|
| 108 |
+
],
|
| 109 |
+
str,
|
| 110 |
+
],
|
| 111 |
+
][
|
| 112 |
+
numpy.ndarray,
|
| 113 |
+
PIL.Image.Image,
|
| 114 |
+
pathlib.Path,
|
| 115 |
+
str,
|
| 116 |
+
tuple[
|
| 117 |
+
typing.Union[
|
| 118 |
+
numpy.ndarray,
|
| 119 |
+
PIL.Image.Image,
|
| 120 |
+
pathlib.Path,
|
| 121 |
+
str,
|
| 122 |
+
][
|
| 123 |
+
numpy.ndarray,
|
| 124 |
+
PIL.Image.Image,
|
| 125 |
+
pathlib.Path,
|
| 126 |
+
str,
|
| 127 |
+
],
|
| 128 |
+
str,
|
| 129 |
+
],
|
| 130 |
+
]
|
| 131 |
+
]
|
| 132 |
+
| None:
|
| 133 |
+
return value
|
| 134 |
+
```
|
| 135 |
+
""", elem_classes=["md-custom", "MediaGallery-user-fn"], header_links=True)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
demo.load(None, js=r"""function() {
|
| 141 |
+
const refs = {};
|
| 142 |
+
const user_fn_refs = {
|
| 143 |
+
MediaGallery: [], };
|
| 144 |
+
requestAnimationFrame(() => {
|
| 145 |
+
|
| 146 |
+
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
| 147 |
+
if (refs.length > 0) {
|
| 148 |
+
const el = document.querySelector(`.${key}-user-fn`);
|
| 149 |
+
if (!el) return;
|
| 150 |
+
refs.forEach(ref => {
|
| 151 |
+
el.innerHTML = el.innerHTML.replace(
|
| 152 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 153 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 154 |
+
);
|
| 155 |
+
})
|
| 156 |
+
}
|
| 157 |
+
})
|
| 158 |
+
|
| 159 |
+
Object.entries(refs).forEach(([key, refs]) => {
|
| 160 |
+
if (refs.length > 0) {
|
| 161 |
+
const el = document.querySelector(`.${key}`);
|
| 162 |
+
if (!el) return;
|
| 163 |
+
refs.forEach(ref => {
|
| 164 |
+
el.innerHTML = el.innerHTML.replace(
|
| 165 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 166 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 167 |
+
);
|
| 168 |
+
})
|
| 169 |
+
}
|
| 170 |
+
})
|
| 171 |
+
})
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
""")
|
| 175 |
+
|
| 176 |
+
demo.launch()
|
mediagallery/frontend/Example.svelte
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { GalleryImage, GalleryVideo, GalleryAudio, GalleryData } from "./types";
|
| 3 |
+
|
| 4 |
+
export let value: GalleryData[] | 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, 5) as item}
|
| 18 |
+
{#if "image" in item && item.image}
|
| 19 |
+
<div class="image-container">
|
| 20 |
+
<img src={item.image.url} alt={item.caption || ""} />
|
| 21 |
+
</div>
|
| 22 |
+
{:else if "video" in item && item.video}
|
| 23 |
+
<div class="image-container">
|
| 24 |
+
<video
|
| 25 |
+
src={item.video.url}
|
| 26 |
+
controls={false}
|
| 27 |
+
muted
|
| 28 |
+
preload="metadata"
|
| 29 |
+
/>
|
| 30 |
+
</div>
|
| 31 |
+
{:else if "audio" in item && item.audio}
|
| 32 |
+
<div class="image-container audio">
|
| 33 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 34 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 35 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 36 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 37 |
+
</svg>
|
| 38 |
+
</div>
|
| 39 |
+
{/if}
|
| 40 |
+
{/each}
|
| 41 |
+
{#if value.length > 5}
|
| 42 |
+
<div class="more-indicator">+{value.length - 5}</div>
|
| 43 |
+
{/if}
|
| 44 |
+
</div>
|
| 45 |
+
{/if}
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<style>
|
| 49 |
+
.container {
|
| 50 |
+
border-radius: var(--radius-lg);
|
| 51 |
+
overflow: hidden;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.container.selected {
|
| 55 |
+
border: 2px solid var(--border-color-accent);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.images-wrapper {
|
| 59 |
+
display: flex;
|
| 60 |
+
gap: var(--spacing-sm);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.container.table .images-wrapper {
|
| 64 |
+
flex-direction: row;
|
| 65 |
+
align-items: center;
|
| 66 |
+
padding: var(--spacing-sm);
|
| 67 |
+
border: 1px solid var(--border-color-primary);
|
| 68 |
+
border-radius: var(--radius-lg);
|
| 69 |
+
background: var(--background-fill-secondary);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.container.gallery .images-wrapper {
|
| 73 |
+
flex-direction: row;
|
| 74 |
+
gap: 0;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.image-container {
|
| 78 |
+
position: relative;
|
| 79 |
+
flex-shrink: 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.container.table .image-container {
|
| 83 |
+
width: var(--size-12);
|
| 84 |
+
height: var(--size-12);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.container.gallery .image-container {
|
| 88 |
+
width: var(--size-20);
|
| 89 |
+
height: var(--size-20);
|
| 90 |
+
margin-left: calc(-1 * var(--size-8));
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.container.gallery .image-container:first-child {
|
| 94 |
+
margin-left: 0;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.more-indicator {
|
| 98 |
+
display: flex;
|
| 99 |
+
align-items: center;
|
| 100 |
+
justify-content: center;
|
| 101 |
+
font-size: var(--text-sm);
|
| 102 |
+
font-weight: bold;
|
| 103 |
+
color: var(--body-text-color-subdued);
|
| 104 |
+
background: var(--background-fill-secondary);
|
| 105 |
+
border-radius: var(--radius-md);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.container.table .more-indicator {
|
| 109 |
+
width: var(--size-12);
|
| 110 |
+
height: var(--size-12);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.container.gallery .more-indicator {
|
| 114 |
+
width: var(--size-20);
|
| 115 |
+
height: var(--size-20);
|
| 116 |
+
margin-left: calc(-1 * var(--size-8));
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.image-container img,
|
| 120 |
+
.image-container video {
|
| 121 |
+
width: 100%;
|
| 122 |
+
height: 100%;
|
| 123 |
+
object-fit: cover;
|
| 124 |
+
border-radius: var(--radius-md);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.image-container.audio {
|
| 128 |
+
display: flex;
|
| 129 |
+
align-items: center;
|
| 130 |
+
justify-content: center;
|
| 131 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 132 |
+
border-radius: var(--radius-md);
|
| 133 |
+
color: white;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* Remove hover effects */
|
| 137 |
+
.container,
|
| 138 |
+
.container *,
|
| 139 |
+
.image-container,
|
| 140 |
+
.image-container img,
|
| 141 |
+
.image-container video {
|
| 142 |
+
transition: none !important;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.container:hover,
|
| 146 |
+
.image-container:hover,
|
| 147 |
+
.image-container:hover img,
|
| 148 |
+
.image-container:hover video {
|
| 149 |
+
transform: none !important;
|
| 150 |
+
filter: none !important;
|
| 151 |
+
opacity: 1 !important;
|
| 152 |
+
}
|
| 153 |
+
</style>
|
mediagallery/frontend/Index.svelte
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, GalleryAudio, GalleryData } from "./types";
|
| 8 |
+
import type { FileData } from "@gradio/client";
|
| 9 |
+
import type { Gradio, ShareData, SelectData } from "@gradio/utils";
|
| 10 |
+
import { Block, UploadText } from "@gradio/atoms";
|
| 11 |
+
import Gallery from "./shared/Gallery.svelte";
|
| 12 |
+
import type { LoadingStatus } from "@gradio/statustracker";
|
| 13 |
+
import { StatusTracker } from "@gradio/statustracker";
|
| 14 |
+
import { createEventDispatcher } from "svelte";
|
| 15 |
+
import { BaseFileUpload } from "@gradio/file";
|
| 16 |
+
|
| 17 |
+
export let loading_status: LoadingStatus;
|
| 18 |
+
export let show_label: boolean;
|
| 19 |
+
export let label: string;
|
| 20 |
+
export let root: string;
|
| 21 |
+
export let elem_id = "";
|
| 22 |
+
export let elem_classes: string[] = [];
|
| 23 |
+
export let visible = true;
|
| 24 |
+
export let value: GalleryData[] | null = null;
|
| 25 |
+
export let file_types: string[] | null = ["image", "video", "audio"];
|
| 26 |
+
export let container = true;
|
| 27 |
+
export let scale: number | null = null;
|
| 28 |
+
export let min_width: number | undefined = undefined;
|
| 29 |
+
export let columns: number | number[] | undefined = [2];
|
| 30 |
+
export let rows: number | number[] | undefined = undefined;
|
| 31 |
+
export let height: number | "auto" = "auto";
|
| 32 |
+
export let preview: boolean;
|
| 33 |
+
export let allow_preview = true;
|
| 34 |
+
export let selected_index: number | null = null;
|
| 35 |
+
export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" =
|
| 36 |
+
"cover";
|
| 37 |
+
export let show_share_button = false;
|
| 38 |
+
export let interactive: boolean;
|
| 39 |
+
export let show_download_button = false;
|
| 40 |
+
export let gradio: Gradio<{
|
| 41 |
+
change: typeof value;
|
| 42 |
+
upload: typeof value;
|
| 43 |
+
select: SelectData;
|
| 44 |
+
share: ShareData;
|
| 45 |
+
error: string;
|
| 46 |
+
prop_change: Record<string, any>;
|
| 47 |
+
clear_status: LoadingStatus;
|
| 48 |
+
preview_open: never;
|
| 49 |
+
preview_close: never;
|
| 50 |
+
}>;
|
| 51 |
+
export let show_fullscreen_button = true;
|
| 52 |
+
export let fullscreen = false;
|
| 53 |
+
|
| 54 |
+
const dispatch = createEventDispatcher();
|
| 55 |
+
|
| 56 |
+
$: no_value = value === null ? true : value.length === 0;
|
| 57 |
+
$: selected_index, dispatch("prop_change", { selected_index });
|
| 58 |
+
|
| 59 |
+
// Audio file extensions for detection
|
| 60 |
+
const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'];
|
| 61 |
+
|
| 62 |
+
function isAudioFile(file: FileData): boolean {
|
| 63 |
+
if (file.mime_type?.includes("audio")) return true;
|
| 64 |
+
const ext = (file.orig_name || file.path || "").toLowerCase();
|
| 65 |
+
return AUDIO_EXTENSIONS.some(e => ext.endsWith(e));
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async function process_upload_files(
|
| 69 |
+
files: FileData[]
|
| 70 |
+
): Promise<GalleryData[]> {
|
| 71 |
+
const processed_files = await Promise.all(
|
| 72 |
+
files.map(async (x) => {
|
| 73 |
+
if (x.path?.toLowerCase().endsWith(".svg") && x.url) {
|
| 74 |
+
const response = await fetch(x.url);
|
| 75 |
+
const svgContent = await response.text();
|
| 76 |
+
return {
|
| 77 |
+
...x,
|
| 78 |
+
url: `data:image/svg+xml,${encodeURIComponent(svgContent)}`
|
| 79 |
+
};
|
| 80 |
+
}
|
| 81 |
+
return x;
|
| 82 |
+
})
|
| 83 |
+
);
|
| 84 |
+
|
| 85 |
+
return processed_files.map((x): GalleryData => {
|
| 86 |
+
if (x.mime_type?.includes("video")) {
|
| 87 |
+
return { video: x, caption: null };
|
| 88 |
+
} else if (isAudioFile(x)) {
|
| 89 |
+
return { audio: x, caption: null };
|
| 90 |
+
} else {
|
| 91 |
+
return { image: x, caption: null };
|
| 92 |
+
}
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Handle adding more files to existing gallery
|
| 97 |
+
async function handle_upload(e: CustomEvent<FileData | FileData[]>) {
|
| 98 |
+
const files = Array.isArray(e.detail) ? e.detail : [e.detail];
|
| 99 |
+
const new_items = await process_upload_files(files);
|
| 100 |
+
|
| 101 |
+
// Append to existing items instead of replacing
|
| 102 |
+
if (value && value.length > 0) {
|
| 103 |
+
value = [...value, ...new_items];
|
| 104 |
+
} else {
|
| 105 |
+
value = new_items;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
gradio.dispatch("upload", value);
|
| 109 |
+
gradio.dispatch("change", value);
|
| 110 |
+
}
|
| 111 |
+
</script>
|
| 112 |
+
|
| 113 |
+
<Block
|
| 114 |
+
{visible}
|
| 115 |
+
variant="solid"
|
| 116 |
+
padding={false}
|
| 117 |
+
{elem_id}
|
| 118 |
+
{elem_classes}
|
| 119 |
+
{container}
|
| 120 |
+
{scale}
|
| 121 |
+
{min_width}
|
| 122 |
+
allow_overflow={false}
|
| 123 |
+
height={typeof height === "number" ? height : undefined}
|
| 124 |
+
bind:fullscreen
|
| 125 |
+
>
|
| 126 |
+
<StatusTracker
|
| 127 |
+
autoscroll={gradio.autoscroll}
|
| 128 |
+
i18n={gradio.i18n}
|
| 129 |
+
{...loading_status}
|
| 130 |
+
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
|
| 131 |
+
/>
|
| 132 |
+
{#if interactive && no_value}
|
| 133 |
+
<!-- Initial upload area when gallery is empty -->
|
| 134 |
+
<BaseFileUpload
|
| 135 |
+
value={null}
|
| 136 |
+
{root}
|
| 137 |
+
{label}
|
| 138 |
+
{file_types}
|
| 139 |
+
max_file_size={gradio.max_file_size}
|
| 140 |
+
file_count={"multiple"}
|
| 141 |
+
i18n={gradio.i18n}
|
| 142 |
+
upload={(...args) => gradio.client.upload(...args)}
|
| 143 |
+
stream_handler={(...args) => gradio.client.stream(...args)}
|
| 144 |
+
on:upload={handle_upload}
|
| 145 |
+
on:error={({ detail }) => {
|
| 146 |
+
loading_status = loading_status || {};
|
| 147 |
+
loading_status.status = "error";
|
| 148 |
+
gradio.dispatch("error", detail);
|
| 149 |
+
}}
|
| 150 |
+
>
|
| 151 |
+
<UploadText i18n={gradio.i18n} type="gallery" />
|
| 152 |
+
</BaseFileUpload>
|
| 153 |
+
{:else}
|
| 154 |
+
<Gallery
|
| 155 |
+
on:change={() => gradio.dispatch("change", value)}
|
| 156 |
+
on:select={(e) => gradio.dispatch("select", e.detail)}
|
| 157 |
+
on:share={(e) => gradio.dispatch("share", e.detail)}
|
| 158 |
+
on:error={(e) => gradio.dispatch("error", e.detail)}
|
| 159 |
+
on:preview_open={() => gradio.dispatch("preview_open")}
|
| 160 |
+
on:preview_close={() => gradio.dispatch("preview_close")}
|
| 161 |
+
on:fullscreen={({ detail }) => {
|
| 162 |
+
fullscreen = detail;
|
| 163 |
+
}}
|
| 164 |
+
on:upload={handle_upload}
|
| 165 |
+
{label}
|
| 166 |
+
{show_label}
|
| 167 |
+
{columns}
|
| 168 |
+
{rows}
|
| 169 |
+
{height}
|
| 170 |
+
{preview}
|
| 171 |
+
{object_fit}
|
| 172 |
+
{interactive}
|
| 173 |
+
{allow_preview}
|
| 174 |
+
bind:selected_index
|
| 175 |
+
bind:value
|
| 176 |
+
{show_share_button}
|
| 177 |
+
{show_download_button}
|
| 178 |
+
i18n={gradio.i18n}
|
| 179 |
+
_fetch={(...args) => gradio.client.fetch(...args)}
|
| 180 |
+
{show_fullscreen_button}
|
| 181 |
+
{fullscreen}
|
| 182 |
+
{root}
|
| 183 |
+
{file_types}
|
| 184 |
+
max_file_size={gradio.max_file_size}
|
| 185 |
+
upload={(...args) => gradio.client.upload(...args)}
|
| 186 |
+
stream_handler={(...args) => gradio.client.stream(...args)}
|
| 187 |
+
/>
|
| 188 |
+
{/if}
|
| 189 |
+
</Block>
|
| 190 |
+
|
| 191 |
+
<style>
|
| 192 |
+
/* Component styles are in Gallery.svelte */
|
| 193 |
+
</style>
|
mediagallery/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 |
+
};
|
mediagallery/frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
mediagallery/frontend/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "gradio_mediagallery",
|
| 3 |
+
"version": "0.15.24",
|
| 4 |
+
"description": "Gradio UI packages",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"author": "",
|
| 7 |
+
"license": "ISC",
|
| 8 |
+
"private": false,
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@gradio/atoms": "0.16.2",
|
| 11 |
+
"@gradio/client": "1.15.3",
|
| 12 |
+
"@gradio/icons": "0.12.0",
|
| 13 |
+
"@gradio/image": "0.22.10",
|
| 14 |
+
"@gradio/statustracker": "0.10.13",
|
| 15 |
+
"@gradio/upload": "0.16.8",
|
| 16 |
+
"@gradio/utils": "0.10.2",
|
| 17 |
+
"@gradio/video": "0.14.18",
|
| 18 |
+
"@gradio/file": "0.12.21",
|
| 19 |
+
"dequal": "^2.0.2"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"@gradio/preview": "0.13.2"
|
| 23 |
+
},
|
| 24 |
+
"main": "./Index.svelte",
|
| 25 |
+
"main_changeset": true,
|
| 26 |
+
"exports": {
|
| 27 |
+
".": {
|
| 28 |
+
"gradio": "./Index.svelte",
|
| 29 |
+
"svelte": "./dist/Index.svelte",
|
| 30 |
+
"types": "./dist/Index.svelte.d.ts"
|
| 31 |
+
},
|
| 32 |
+
"./package.json": "./package.json",
|
| 33 |
+
"./base": {
|
| 34 |
+
"gradio": "./shared/Gallery.svelte",
|
| 35 |
+
"svelte": "./dist/shared/Gallery.svelte",
|
| 36 |
+
"types": "./dist/shared/Gallery.svelte.d.ts"
|
| 37 |
+
},
|
| 38 |
+
"./example": {
|
| 39 |
+
"gradio": "./Example.svelte",
|
| 40 |
+
"svelte": "./dist/Example.svelte",
|
| 41 |
+
"types": "./dist/Example.svelte.d.ts"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"peerDependencies": {
|
| 45 |
+
"svelte": "^4.0.0"
|
| 46 |
+
},
|
| 47 |
+
"repository": {
|
| 48 |
+
"type": "git",
|
| 49 |
+
"url": "git+https://github.com/gradio-app/gradio.git",
|
| 50 |
+
"directory": "js/gallery"
|
| 51 |
+
}
|
| 52 |
+
}
|
mediagallery/frontend/shared/Gallery.svelte
ADDED
|
@@ -0,0 +1,1030 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import {
|
| 3 |
+
BlockLabel,
|
| 4 |
+
Empty,
|
| 5 |
+
ShareButton,
|
| 6 |
+
IconButton,
|
| 7 |
+
IconButtonWrapper,
|
| 8 |
+
FullscreenButton
|
| 9 |
+
} from "@gradio/atoms";
|
| 10 |
+
import { Upload } from "@gradio/upload";
|
| 11 |
+
import type { SelectData, I18nFormatter } from "@gradio/utils";
|
| 12 |
+
import { Image } from "@gradio/image/shared";
|
| 13 |
+
import { Video } from "@gradio/video/shared";
|
| 14 |
+
import { dequal } from "dequal";
|
| 15 |
+
import { createEventDispatcher, onMount } from "svelte";
|
| 16 |
+
import { tick } from "svelte";
|
| 17 |
+
import type { GalleryImage, GalleryVideo, GalleryAudio, GalleryData } from "../types";
|
| 18 |
+
import { getMediaType, getMediaFile } from "../types";
|
| 19 |
+
|
| 20 |
+
import { Download, Image as ImageIcon, Clear, Play } from "@gradio/icons";
|
| 21 |
+
import { FileData } from "@gradio/client";
|
| 22 |
+
import type { Client } from "@gradio/client";
|
| 23 |
+
import { format_gallery_for_sharing } from "./utils";
|
| 24 |
+
|
| 25 |
+
export let show_label = true;
|
| 26 |
+
export let label: string;
|
| 27 |
+
export let value: GalleryData[] | null = null;
|
| 28 |
+
export let columns: number | number[] | undefined = [2];
|
| 29 |
+
export let rows: number | number[] | undefined = undefined;
|
| 30 |
+
export let height: number | "auto" = "auto";
|
| 31 |
+
export let preview: boolean;
|
| 32 |
+
export let allow_preview = true;
|
| 33 |
+
export let object_fit: "contain" | "cover" | "fill" | "none" | "scale-down" =
|
| 34 |
+
"cover";
|
| 35 |
+
export let show_share_button = false;
|
| 36 |
+
export let show_download_button = false;
|
| 37 |
+
export let i18n: I18nFormatter;
|
| 38 |
+
export let selected_index: number | null = null;
|
| 39 |
+
export let interactive: boolean;
|
| 40 |
+
export let _fetch: typeof fetch;
|
| 41 |
+
export let mode: "normal" | "minimal" = "normal";
|
| 42 |
+
export let show_fullscreen_button = true;
|
| 43 |
+
export let display_icon_button_wrapper_top_corner = false;
|
| 44 |
+
export let fullscreen = false;
|
| 45 |
+
export let root = "";
|
| 46 |
+
export let file_types: string[] | null = ["image", "video", "audio"];
|
| 47 |
+
export let max_file_size: number | null = null;
|
| 48 |
+
export let upload: Client["upload"] | undefined = undefined;
|
| 49 |
+
export let stream_handler: Client["stream"] | undefined = undefined;
|
| 50 |
+
|
| 51 |
+
let is_full_screen = false;
|
| 52 |
+
let image_container: HTMLElement;
|
| 53 |
+
|
| 54 |
+
const dispatch = createEventDispatcher<{
|
| 55 |
+
change: undefined;
|
| 56 |
+
select: SelectData;
|
| 57 |
+
preview_open: undefined;
|
| 58 |
+
preview_close: undefined;
|
| 59 |
+
fullscreen: boolean;
|
| 60 |
+
upload: FileData | FileData[];
|
| 61 |
+
error: string;
|
| 62 |
+
}>();
|
| 63 |
+
|
| 64 |
+
// tracks whether the value of the gallery was reset
|
| 65 |
+
let was_reset = true;
|
| 66 |
+
|
| 67 |
+
$: was_reset = value == null || value.length === 0 ? true : was_reset;
|
| 68 |
+
|
| 69 |
+
let resolved_value: GalleryData[] | null = null;
|
| 70 |
+
|
| 71 |
+
$: resolved_value =
|
| 72 |
+
value == null
|
| 73 |
+
? null
|
| 74 |
+
: (value.map((data) => {
|
| 75 |
+
if ("video" in data) {
|
| 76 |
+
return {
|
| 77 |
+
video: data.video as FileData,
|
| 78 |
+
caption: data.caption
|
| 79 |
+
};
|
| 80 |
+
} else if ("audio" in data) {
|
| 81 |
+
return {
|
| 82 |
+
audio: data.audio as FileData,
|
| 83 |
+
caption: data.caption
|
| 84 |
+
};
|
| 85 |
+
} else if ("image" in data) {
|
| 86 |
+
return { image: data.image as FileData, caption: data.caption };
|
| 87 |
+
}
|
| 88 |
+
return {};
|
| 89 |
+
}) as GalleryData[]);
|
| 90 |
+
|
| 91 |
+
let prev_value: GalleryData[] | null = value;
|
| 92 |
+
if (selected_index == null && preview && value?.length) {
|
| 93 |
+
selected_index = 0;
|
| 94 |
+
}
|
| 95 |
+
let old_selected_index: number | null = selected_index;
|
| 96 |
+
|
| 97 |
+
$: if (!dequal(prev_value, value)) {
|
| 98 |
+
// When value is falsy (clear button or first load),
|
| 99 |
+
// preview determines the selected image
|
| 100 |
+
if (was_reset) {
|
| 101 |
+
selected_index = preview && value?.length ? 0 : null;
|
| 102 |
+
was_reset = false;
|
| 103 |
+
// Otherwise we keep the selected_index the same if the
|
| 104 |
+
// gallery has at least as many elements as it did before
|
| 105 |
+
} else {
|
| 106 |
+
if (selected_index !== null && value !== null) {
|
| 107 |
+
selected_index = Math.max(
|
| 108 |
+
0,
|
| 109 |
+
Math.min(selected_index, value.length - 1)
|
| 110 |
+
);
|
| 111 |
+
} else {
|
| 112 |
+
selected_index = null;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
dispatch("change");
|
| 116 |
+
prev_value = value;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
$: previous =
|
| 120 |
+
((selected_index ?? 0) + (resolved_value?.length ?? 0) - 1) %
|
| 121 |
+
(resolved_value?.length ?? 0);
|
| 122 |
+
$: next = ((selected_index ?? 0) + 1) % (resolved_value?.length ?? 0);
|
| 123 |
+
|
| 124 |
+
function handle_preview_click(event: MouseEvent): void {
|
| 125 |
+
const element = event.target as HTMLElement;
|
| 126 |
+
const x = event.offsetX;
|
| 127 |
+
const width = element.offsetWidth;
|
| 128 |
+
const centerX = width / 2;
|
| 129 |
+
|
| 130 |
+
if (x < centerX) {
|
| 131 |
+
selected_index = previous;
|
| 132 |
+
} else {
|
| 133 |
+
selected_index = next;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function on_keydown(e: KeyboardEvent): void {
|
| 138 |
+
switch (e.code) {
|
| 139 |
+
case "Escape":
|
| 140 |
+
e.preventDefault();
|
| 141 |
+
selected_index = null;
|
| 142 |
+
break;
|
| 143 |
+
case "ArrowLeft":
|
| 144 |
+
e.preventDefault();
|
| 145 |
+
selected_index = previous;
|
| 146 |
+
break;
|
| 147 |
+
case "ArrowRight":
|
| 148 |
+
e.preventDefault();
|
| 149 |
+
selected_index = next;
|
| 150 |
+
break;
|
| 151 |
+
default:
|
| 152 |
+
break;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
$: {
|
| 157 |
+
if (selected_index !== old_selected_index) {
|
| 158 |
+
old_selected_index = selected_index;
|
| 159 |
+
if (selected_index !== null) {
|
| 160 |
+
if (resolved_value != null) {
|
| 161 |
+
selected_index = Math.max(
|
| 162 |
+
0,
|
| 163 |
+
Math.min(selected_index, resolved_value.length - 1)
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
dispatch("select", {
|
| 167 |
+
index: selected_index,
|
| 168 |
+
value: resolved_value?.[selected_index]
|
| 169 |
+
});
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
$: if (allow_preview) {
|
| 175 |
+
scroll_to_img(selected_index);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
let el: HTMLButtonElement[] = [];
|
| 179 |
+
let container_element: HTMLDivElement;
|
| 180 |
+
|
| 181 |
+
async function scroll_to_img(index: number | null): Promise<void> {
|
| 182 |
+
if (typeof index !== "number") return;
|
| 183 |
+
await tick();
|
| 184 |
+
|
| 185 |
+
if (el[index] === undefined) return;
|
| 186 |
+
|
| 187 |
+
el[index]?.focus();
|
| 188 |
+
|
| 189 |
+
const { left: container_left, width: container_width } =
|
| 190 |
+
container_element.getBoundingClientRect();
|
| 191 |
+
const { left, width } = el[index].getBoundingClientRect();
|
| 192 |
+
|
| 193 |
+
const relative_left = left - container_left;
|
| 194 |
+
|
| 195 |
+
const pos =
|
| 196 |
+
relative_left +
|
| 197 |
+
width / 2 -
|
| 198 |
+
container_width / 2 +
|
| 199 |
+
container_element.scrollLeft;
|
| 200 |
+
|
| 201 |
+
if (container_element && typeof container_element.scrollTo === "function") {
|
| 202 |
+
container_element.scrollTo({
|
| 203 |
+
left: pos < 0 ? 0 : pos,
|
| 204 |
+
behavior: "smooth"
|
| 205 |
+
});
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
let window_height = 0;
|
| 210 |
+
|
| 211 |
+
// Unlike `gr.Image()`, images specified via remote URLs are not cached in the server
|
| 212 |
+
// and their remote URLs are directly passed to the client as `value[].image.url`.
|
| 213 |
+
// The `download` attribute of the <a> tag doesn't work for remote URLs (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download),
|
| 214 |
+
// so we need to download the image via JS as below.
|
| 215 |
+
async function download(file_url: string, name: string): Promise<void> {
|
| 216 |
+
let response;
|
| 217 |
+
try {
|
| 218 |
+
response = await _fetch(file_url);
|
| 219 |
+
} catch (error) {
|
| 220 |
+
if (error instanceof TypeError) {
|
| 221 |
+
// If CORS is not allowed (https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#checking_that_the_fetch_was_successful),
|
| 222 |
+
// open the link in a new tab instead, mimicing the behavior of the `download` attribute for remote URLs,
|
| 223 |
+
// which is not ideal, but a reasonable fallback.
|
| 224 |
+
window.open(file_url, "_blank", "noreferrer");
|
| 225 |
+
return;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
throw error;
|
| 229 |
+
}
|
| 230 |
+
const blob = await response.blob();
|
| 231 |
+
const url = URL.createObjectURL(blob);
|
| 232 |
+
const link = document.createElement("a");
|
| 233 |
+
link.href = url;
|
| 234 |
+
link.download = name;
|
| 235 |
+
link.click();
|
| 236 |
+
URL.revokeObjectURL(url);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
$: selected_media =
|
| 240 |
+
selected_index != null && resolved_value != null
|
| 241 |
+
? resolved_value[selected_index]
|
| 242 |
+
: null;
|
| 243 |
+
|
| 244 |
+
let thumbnails_overflow = false;
|
| 245 |
+
|
| 246 |
+
function check_thumbnails_overflow(): void {
|
| 247 |
+
if (container_element) {
|
| 248 |
+
thumbnails_overflow =
|
| 249 |
+
container_element.scrollWidth > container_element.clientWidth;
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
onMount(() => {
|
| 254 |
+
check_thumbnails_overflow();
|
| 255 |
+
document.addEventListener("fullscreenchange", () => {
|
| 256 |
+
is_full_screen = !!document.fullscreenElement;
|
| 257 |
+
});
|
| 258 |
+
window.addEventListener("resize", check_thumbnails_overflow);
|
| 259 |
+
return () =>
|
| 260 |
+
window.removeEventListener("resize", check_thumbnails_overflow);
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
$: resolved_value, check_thumbnails_overflow();
|
| 264 |
+
$: if (container_element) {
|
| 265 |
+
check_thumbnails_overflow();
|
| 266 |
+
}
|
| 267 |
+
</script>
|
| 268 |
+
|
| 269 |
+
<svelte:window bind:innerHeight={window_height} />
|
| 270 |
+
|
| 271 |
+
{#if show_label}
|
| 272 |
+
<BlockLabel {show_label} Icon={ImageIcon} label={label || "Gallery"} />
|
| 273 |
+
{/if}
|
| 274 |
+
{#if value == null || resolved_value == null || resolved_value.length === 0}
|
| 275 |
+
<Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
|
| 276 |
+
{:else}
|
| 277 |
+
<div class="gallery-container" bind:this={image_container}>
|
| 278 |
+
{#if selected_media && allow_preview}
|
| 279 |
+
<button
|
| 280 |
+
on:keydown={on_keydown}
|
| 281 |
+
class="preview"
|
| 282 |
+
class:minimal={mode === "minimal"}
|
| 283 |
+
>
|
| 284 |
+
<IconButtonWrapper
|
| 285 |
+
display_top_corner={display_icon_button_wrapper_top_corner}
|
| 286 |
+
>
|
| 287 |
+
{#if show_download_button}
|
| 288 |
+
<IconButton
|
| 289 |
+
Icon={Download}
|
| 290 |
+
label={i18n("common.download")}
|
| 291 |
+
on:click={() => {
|
| 292 |
+
const file = getMediaFile(selected_media);
|
| 293 |
+
if (file == null) {
|
| 294 |
+
return;
|
| 295 |
+
}
|
| 296 |
+
const { url, orig_name } = file;
|
| 297 |
+
if (url) {
|
| 298 |
+
download(url, orig_name ?? "media");
|
| 299 |
+
}
|
| 300 |
+
}}
|
| 301 |
+
/>
|
| 302 |
+
{/if}
|
| 303 |
+
|
| 304 |
+
{#if show_fullscreen_button}
|
| 305 |
+
<FullscreenButton {fullscreen} on:fullscreen />
|
| 306 |
+
{/if}
|
| 307 |
+
|
| 308 |
+
{#if show_share_button}
|
| 309 |
+
<div class="icon-button">
|
| 310 |
+
<ShareButton
|
| 311 |
+
{i18n}
|
| 312 |
+
on:share
|
| 313 |
+
on:error
|
| 314 |
+
value={resolved_value}
|
| 315 |
+
formatter={format_gallery_for_sharing}
|
| 316 |
+
/>
|
| 317 |
+
</div>
|
| 318 |
+
{/if}
|
| 319 |
+
{#if !is_full_screen}
|
| 320 |
+
<IconButton
|
| 321 |
+
Icon={Clear}
|
| 322 |
+
label="Close"
|
| 323 |
+
on:click={() => {
|
| 324 |
+
selected_index = null;
|
| 325 |
+
dispatch("preview_close");
|
| 326 |
+
}}
|
| 327 |
+
/>
|
| 328 |
+
{/if}
|
| 329 |
+
</IconButtonWrapper>
|
| 330 |
+
<button
|
| 331 |
+
class="media-button"
|
| 332 |
+
on:click={"image" in selected_media
|
| 333 |
+
? (event) => handle_preview_click(event)
|
| 334 |
+
: null}
|
| 335 |
+
style="height: calc(100% - {selected_media.caption
|
| 336 |
+
? '80px'
|
| 337 |
+
: '60px'})"
|
| 338 |
+
aria-label="detailed view of selected media"
|
| 339 |
+
>
|
| 340 |
+
{#if "image" in selected_media}
|
| 341 |
+
<Image
|
| 342 |
+
data-testid="detailed-image"
|
| 343 |
+
src={selected_media.image.url}
|
| 344 |
+
alt={selected_media.caption || ""}
|
| 345 |
+
title={selected_media.caption || null}
|
| 346 |
+
class={selected_media.caption && "with-caption"}
|
| 347 |
+
loading="lazy"
|
| 348 |
+
/>
|
| 349 |
+
{:else if "video" in selected_media}
|
| 350 |
+
<Video
|
| 351 |
+
src={selected_media.video.url}
|
| 352 |
+
data-testid={"detailed-video"}
|
| 353 |
+
alt={selected_media.caption || ""}
|
| 354 |
+
loading="lazy"
|
| 355 |
+
loop={false}
|
| 356 |
+
is_stream={false}
|
| 357 |
+
muted={false}
|
| 358 |
+
controls={true}
|
| 359 |
+
/>
|
| 360 |
+
{:else if "audio" in selected_media}
|
| 361 |
+
<div class="audio-preview">
|
| 362 |
+
<div class="audio-icon-large">
|
| 363 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
| 364 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 365 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 366 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 367 |
+
</svg>
|
| 368 |
+
</div>
|
| 369 |
+
<div class="audio-filename">{selected_media.audio.orig_name || "Audio"}</div>
|
| 370 |
+
<audio
|
| 371 |
+
src={selected_media.audio.url}
|
| 372 |
+
controls
|
| 373 |
+
class="audio-player"
|
| 374 |
+
data-testid="detailed-audio"
|
| 375 |
+
/>
|
| 376 |
+
</div>
|
| 377 |
+
{/if}
|
| 378 |
+
</button>
|
| 379 |
+
{#if selected_media?.caption}
|
| 380 |
+
<caption class="caption">
|
| 381 |
+
{selected_media.caption}
|
| 382 |
+
</caption>
|
| 383 |
+
{/if}
|
| 384 |
+
<div
|
| 385 |
+
bind:this={container_element}
|
| 386 |
+
class="thumbnails scroll-hide"
|
| 387 |
+
data-testid="container_el"
|
| 388 |
+
style="justify-content: {thumbnails_overflow
|
| 389 |
+
? 'flex-start'
|
| 390 |
+
: 'center'};"
|
| 391 |
+
>
|
| 392 |
+
{#each resolved_value as media, i}
|
| 393 |
+
<button
|
| 394 |
+
bind:this={el[i]}
|
| 395 |
+
on:click={() => (selected_index = i)}
|
| 396 |
+
class="thumbnail-item thumbnail-small"
|
| 397 |
+
class:selected={selected_index === i && mode !== "minimal"}
|
| 398 |
+
aria-label={"Thumbnail " +
|
| 399 |
+
(i + 1) +
|
| 400 |
+
" of " +
|
| 401 |
+
resolved_value.length}
|
| 402 |
+
>
|
| 403 |
+
{#if "image" in media}
|
| 404 |
+
<Image
|
| 405 |
+
src={media.image.url}
|
| 406 |
+
title={media.caption || null}
|
| 407 |
+
data-testid={"thumbnail " + (i + 1)}
|
| 408 |
+
alt=""
|
| 409 |
+
loading="lazy"
|
| 410 |
+
/>
|
| 411 |
+
{:else if "video" in media}
|
| 412 |
+
<Play />
|
| 413 |
+
<Video
|
| 414 |
+
src={media.video.url}
|
| 415 |
+
title={media.caption || null}
|
| 416 |
+
is_stream={false}
|
| 417 |
+
data-testid={"thumbnail " + (i + 1)}
|
| 418 |
+
alt=""
|
| 419 |
+
loading="lazy"
|
| 420 |
+
loop={false}
|
| 421 |
+
/>
|
| 422 |
+
{:else if "audio" in media}
|
| 423 |
+
<div class="audio-thumbnail">
|
| 424 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 425 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 426 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 427 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 428 |
+
</svg>
|
| 429 |
+
</div>
|
| 430 |
+
{/if}
|
| 431 |
+
</button>
|
| 432 |
+
{/each}
|
| 433 |
+
</div>
|
| 434 |
+
</button>
|
| 435 |
+
{/if}
|
| 436 |
+
|
| 437 |
+
<div
|
| 438 |
+
class="grid-wrap"
|
| 439 |
+
class:minimal={mode === "minimal"}
|
| 440 |
+
class:fixed-height={mode !== "minimal" && (!height || height == "auto")}
|
| 441 |
+
class:hidden={is_full_screen}
|
| 442 |
+
>
|
| 443 |
+
<div
|
| 444 |
+
class="grid-container"
|
| 445 |
+
style="--grid-cols:{columns}; --grid-rows:{rows}; --object-fit: {object_fit}; height: {height};"
|
| 446 |
+
class:pt-6={show_label}
|
| 447 |
+
>
|
| 448 |
+
{#each resolved_value as entry, i}
|
| 449 |
+
<div class="thumbnail-wrapper">
|
| 450 |
+
<button
|
| 451 |
+
class="thumbnail-item thumbnail-lg"
|
| 452 |
+
class:selected={selected_index === i}
|
| 453 |
+
on:click={() => {
|
| 454 |
+
if (selected_index === null && allow_preview) {
|
| 455 |
+
dispatch("preview_open");
|
| 456 |
+
}
|
| 457 |
+
selected_index = i;
|
| 458 |
+
}}
|
| 459 |
+
aria-label={"Thumbnail " + (i + 1) + " of " + resolved_value.length}
|
| 460 |
+
>
|
| 461 |
+
{#if "image" in entry}
|
| 462 |
+
<Image
|
| 463 |
+
alt={entry.caption || ""}
|
| 464 |
+
src={typeof entry.image === "string"
|
| 465 |
+
? entry.image
|
| 466 |
+
: entry.image.url}
|
| 467 |
+
loading="lazy"
|
| 468 |
+
/>
|
| 469 |
+
<div class="media-type-badge image">
|
| 470 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 471 |
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
| 472 |
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
| 473 |
+
<polyline points="21 15 16 10 5 21"></polyline>
|
| 474 |
+
</svg>
|
| 475 |
+
</div>
|
| 476 |
+
{:else if "video" in entry}
|
| 477 |
+
<Play />
|
| 478 |
+
<Video
|
| 479 |
+
src={entry.video.url}
|
| 480 |
+
title={entry.caption || null}
|
| 481 |
+
is_stream={false}
|
| 482 |
+
data-testid={"thumbnail " + (i + 1)}
|
| 483 |
+
alt=""
|
| 484 |
+
loading="lazy"
|
| 485 |
+
loop={false}
|
| 486 |
+
/>
|
| 487 |
+
<div class="media-type-badge video">
|
| 488 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 489 |
+
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
| 490 |
+
</svg>
|
| 491 |
+
</div>
|
| 492 |
+
{:else if "audio" in entry}
|
| 493 |
+
<div class="audio-thumbnail-lg">
|
| 494 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
| 495 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 496 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 497 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 498 |
+
</svg>
|
| 499 |
+
</div>
|
| 500 |
+
<div class="media-type-badge audio">
|
| 501 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 502 |
+
<path d="M9 18V5l12-2v13"></path>
|
| 503 |
+
<circle cx="6" cy="18" r="3"></circle>
|
| 504 |
+
<circle cx="18" cy="16" r="3"></circle>
|
| 505 |
+
</svg>
|
| 506 |
+
</div>
|
| 507 |
+
{/if}
|
| 508 |
+
</button>
|
| 509 |
+
<!-- Remove button -->
|
| 510 |
+
{#if interactive}
|
| 511 |
+
<button
|
| 512 |
+
class="remove-btn"
|
| 513 |
+
on:click|stopPropagation={() => {
|
| 514 |
+
if (value) {
|
| 515 |
+
value = value.filter((_, idx) => idx !== i);
|
| 516 |
+
if (selected_index !== null && selected_index >= i) {
|
| 517 |
+
selected_index = selected_index > 0 ? selected_index - 1 : null;
|
| 518 |
+
}
|
| 519 |
+
dispatch("change");
|
| 520 |
+
}
|
| 521 |
+
}}
|
| 522 |
+
aria-label="Remove item"
|
| 523 |
+
>
|
| 524 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 525 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 526 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 527 |
+
</svg>
|
| 528 |
+
</button>
|
| 529 |
+
{/if}
|
| 530 |
+
<!-- Filename label -->
|
| 531 |
+
<div class="filename-label">
|
| 532 |
+
{#if "image" in entry}
|
| 533 |
+
{entry.image.orig_name || "Image"}
|
| 534 |
+
{:else if "video" in entry}
|
| 535 |
+
{entry.video.orig_name || "Video"}
|
| 536 |
+
{:else if "audio" in entry}
|
| 537 |
+
{entry.audio.orig_name || "Audio"}
|
| 538 |
+
{/if}
|
| 539 |
+
</div>
|
| 540 |
+
</div>
|
| 541 |
+
{/each}
|
| 542 |
+
</div>
|
| 543 |
+
</div>
|
| 544 |
+
<!-- Add Media button below grid -->
|
| 545 |
+
{#if interactive && upload && stream_handler}
|
| 546 |
+
<div class="add-media-bar">
|
| 547 |
+
<Upload
|
| 548 |
+
filetype={file_types}
|
| 549 |
+
file_count="multiple"
|
| 550 |
+
{max_file_size}
|
| 551 |
+
{root}
|
| 552 |
+
{upload}
|
| 553 |
+
{stream_handler}
|
| 554 |
+
on:load={(e) => dispatch("upload", e.detail)}
|
| 555 |
+
on:error={(e) => dispatch("error", e.detail)}
|
| 556 |
+
>
|
| 557 |
+
<div class="add-media-btn">
|
| 558 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 559 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 560 |
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
| 561 |
+
</svg>
|
| 562 |
+
<span>Add Media</span>
|
| 563 |
+
</div>
|
| 564 |
+
</Upload>
|
| 565 |
+
</div>
|
| 566 |
+
{/if}
|
| 567 |
+
</div>
|
| 568 |
+
{/if}
|
| 569 |
+
|
| 570 |
+
<style lang="postcss">
|
| 571 |
+
.gallery-container {
|
| 572 |
+
position: relative;
|
| 573 |
+
display: flex;
|
| 574 |
+
flex-direction: column;
|
| 575 |
+
width: 100%;
|
| 576 |
+
height: 100%;
|
| 577 |
+
min-height: 0;
|
| 578 |
+
overflow: hidden;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.image-container {
|
| 582 |
+
height: 100%;
|
| 583 |
+
position: relative;
|
| 584 |
+
}
|
| 585 |
+
.image-container :global(img),
|
| 586 |
+
button {
|
| 587 |
+
width: var(--size-full);
|
| 588 |
+
height: var(--size-full);
|
| 589 |
+
object-fit: contain;
|
| 590 |
+
display: block;
|
| 591 |
+
border-radius: var(--radius-lg);
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.preview {
|
| 595 |
+
display: flex;
|
| 596 |
+
position: absolute;
|
| 597 |
+
flex-direction: column;
|
| 598 |
+
z-index: var(--layer-2);
|
| 599 |
+
border-radius: calc(var(--block-radius) - var(--block-border-width));
|
| 600 |
+
-webkit-backdrop-filter: blur(8px);
|
| 601 |
+
backdrop-filter: blur(8px);
|
| 602 |
+
width: var(--size-full);
|
| 603 |
+
height: var(--size-full);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.preview.minimal {
|
| 607 |
+
width: fit-content;
|
| 608 |
+
height: fit-content;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.preview::before {
|
| 612 |
+
content: "";
|
| 613 |
+
position: absolute;
|
| 614 |
+
z-index: var(--layer-below);
|
| 615 |
+
background: var(--background-fill-primary);
|
| 616 |
+
opacity: 0.9;
|
| 617 |
+
width: var(--size-full);
|
| 618 |
+
height: var(--size-full);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.fixed-height {
|
| 622 |
+
min-height: var(--size-80);
|
| 623 |
+
max-height: 55vh;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
@media (--screen-xl) {
|
| 627 |
+
.fixed-height {
|
| 628 |
+
min-height: 450px;
|
| 629 |
+
}
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.media-button {
|
| 633 |
+
height: calc(100% - 60px);
|
| 634 |
+
width: 100%;
|
| 635 |
+
display: flex;
|
| 636 |
+
}
|
| 637 |
+
.media-button :global(img),
|
| 638 |
+
.media-button :global(video) {
|
| 639 |
+
width: var(--size-full);
|
| 640 |
+
height: var(--size-full);
|
| 641 |
+
object-fit: contain;
|
| 642 |
+
}
|
| 643 |
+
.thumbnails :global(img) {
|
| 644 |
+
object-fit: cover;
|
| 645 |
+
width: var(--size-full);
|
| 646 |
+
height: var(--size-full);
|
| 647 |
+
}
|
| 648 |
+
.thumbnails :global(svg) {
|
| 649 |
+
position: absolute;
|
| 650 |
+
top: var(--size-2);
|
| 651 |
+
left: var(--size-2);
|
| 652 |
+
width: 50%;
|
| 653 |
+
height: 50%;
|
| 654 |
+
opacity: 50%;
|
| 655 |
+
}
|
| 656 |
+
.preview :global(img.with-caption) {
|
| 657 |
+
height: var(--size-full);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.preview.minimal :global(img.with-caption) {
|
| 661 |
+
height: auto;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
.selectable {
|
| 665 |
+
cursor: crosshair;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.caption {
|
| 669 |
+
padding: var(--size-2) var(--size-3);
|
| 670 |
+
overflow: hidden;
|
| 671 |
+
color: var(--block-label-text-color);
|
| 672 |
+
font-weight: var(--weight-semibold);
|
| 673 |
+
text-align: center;
|
| 674 |
+
text-overflow: ellipsis;
|
| 675 |
+
white-space: nowrap;
|
| 676 |
+
align-self: center;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.thumbnails {
|
| 680 |
+
display: flex;
|
| 681 |
+
position: absolute;
|
| 682 |
+
bottom: 0;
|
| 683 |
+
justify-content: flex-start;
|
| 684 |
+
align-items: center;
|
| 685 |
+
gap: var(--spacing-lg);
|
| 686 |
+
width: var(--size-full);
|
| 687 |
+
height: var(--size-14);
|
| 688 |
+
overflow-x: scroll;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
.thumbnail-item {
|
| 692 |
+
--ring-color: transparent;
|
| 693 |
+
position: relative;
|
| 694 |
+
box-shadow:
|
| 695 |
+
inset 0 0 0 1px var(--ring-color),
|
| 696 |
+
var(--shadow-drop);
|
| 697 |
+
border: 1px solid var(--border-color-primary);
|
| 698 |
+
border-radius: var(--button-small-radius);
|
| 699 |
+
background: var(--background-fill-secondary);
|
| 700 |
+
aspect-ratio: var(--ratio-square);
|
| 701 |
+
width: var(--size-full);
|
| 702 |
+
height: var(--size-full);
|
| 703 |
+
overflow: clip;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.thumbnail-item:hover {
|
| 707 |
+
--ring-color: var(--color-accent);
|
| 708 |
+
border-color: var(--color-accent);
|
| 709 |
+
filter: brightness(1.1);
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
.thumbnail-item.selected {
|
| 713 |
+
--ring-color: var(--color-accent);
|
| 714 |
+
border-color: var(--color-accent);
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.thumbnail-item :global(svg) {
|
| 718 |
+
position: absolute;
|
| 719 |
+
top: 50%;
|
| 720 |
+
left: 50%;
|
| 721 |
+
width: 50%;
|
| 722 |
+
height: 50%;
|
| 723 |
+
opacity: 50%;
|
| 724 |
+
transform: translate(-50%, -50%);
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.thumbnail-item :global(video) {
|
| 728 |
+
width: var(--size-full);
|
| 729 |
+
height: var(--size-full);
|
| 730 |
+
overflow: hidden;
|
| 731 |
+
object-fit: cover;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
.thumbnail-small {
|
| 735 |
+
flex: none;
|
| 736 |
+
transform: scale(0.9);
|
| 737 |
+
transition: 0.075s;
|
| 738 |
+
width: var(--size-9);
|
| 739 |
+
height: var(--size-9);
|
| 740 |
+
}
|
| 741 |
+
.thumbnail-small.selected {
|
| 742 |
+
--ring-color: var(--color-accent);
|
| 743 |
+
transform: scale(1);
|
| 744 |
+
border-color: var(--color-accent);
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.thumbnail-small > img {
|
| 748 |
+
width: var(--size-full);
|
| 749 |
+
height: var(--size-full);
|
| 750 |
+
overflow: hidden;
|
| 751 |
+
object-fit: var(--object-fit);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.grid-wrap {
|
| 755 |
+
position: relative;
|
| 756 |
+
padding: var(--size-2);
|
| 757 |
+
max-height: 100%;
|
| 758 |
+
overflow-y: auto;
|
| 759 |
+
overflow-x: hidden;
|
| 760 |
+
flex: 1;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
.grid-container {
|
| 764 |
+
display: grid;
|
| 765 |
+
position: relative;
|
| 766 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 767 |
+
gap: var(--spacing-md);
|
| 768 |
+
width: 100%;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
/* Responsive columns */
|
| 772 |
+
@media (min-width: 768px) {
|
| 773 |
+
.grid-container {
|
| 774 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
@media (min-width: 1280px) {
|
| 779 |
+
.grid-container {
|
| 780 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 781 |
+
}
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
@media (min-width: 1536px) {
|
| 785 |
+
.grid-container {
|
| 786 |
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
| 787 |
+
}
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.thumbnail-wrapper {
|
| 791 |
+
position: relative;
|
| 792 |
+
width: 100%;
|
| 793 |
+
padding-bottom: 100%; /* Square aspect ratio using padding trick */
|
| 794 |
+
min-width: 0;
|
| 795 |
+
overflow: hidden;
|
| 796 |
+
border-radius: var(--button-small-radius);
|
| 797 |
+
background: var(--background-fill-secondary);
|
| 798 |
+
aspect-ratio: 1 / 1;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
.thumbnail-lg {
|
| 802 |
+
position: absolute;
|
| 803 |
+
top: 0;
|
| 804 |
+
left: 0;
|
| 805 |
+
width: 100%;
|
| 806 |
+
height: 100%;
|
| 807 |
+
overflow: hidden;
|
| 808 |
+
padding: 0;
|
| 809 |
+
margin: 0;
|
| 810 |
+
border: none;
|
| 811 |
+
background: transparent;
|
| 812 |
+
cursor: pointer;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
/* Force all images and videos to be square cropped */
|
| 816 |
+
.thumbnail-lg :global(img),
|
| 817 |
+
.thumbnail-lg :global(video) {
|
| 818 |
+
position: absolute !important;
|
| 819 |
+
top: 0 !important;
|
| 820 |
+
left: 0 !important;
|
| 821 |
+
width: 100% !important;
|
| 822 |
+
height: 100% !important;
|
| 823 |
+
object-fit: cover !important;
|
| 824 |
+
max-width: none !important;
|
| 825 |
+
max-height: none !important;
|
| 826 |
+
min-width: 100% !important;
|
| 827 |
+
min-height: 100% !important;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
/* Picture elements from Image component */
|
| 831 |
+
.thumbnail-lg :global(picture) {
|
| 832 |
+
position: absolute !important;
|
| 833 |
+
top: 0 !important;
|
| 834 |
+
left: 0 !important;
|
| 835 |
+
width: 100% !important;
|
| 836 |
+
height: 100% !important;
|
| 837 |
+
overflow: hidden !important;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.grid-wrap.minimal {
|
| 841 |
+
padding: 0;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
/* Audio thumbnail styles */
|
| 845 |
+
.audio-thumbnail {
|
| 846 |
+
display: flex;
|
| 847 |
+
align-items: center;
|
| 848 |
+
justify-content: center;
|
| 849 |
+
width: 100%;
|
| 850 |
+
height: 100%;
|
| 851 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 852 |
+
color: white;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.audio-thumbnail-lg {
|
| 856 |
+
position: absolute;
|
| 857 |
+
top: 0;
|
| 858 |
+
left: 0;
|
| 859 |
+
display: flex;
|
| 860 |
+
flex-direction: column;
|
| 861 |
+
align-items: center;
|
| 862 |
+
justify-content: center;
|
| 863 |
+
width: 100%;
|
| 864 |
+
height: 100%;
|
| 865 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 866 |
+
color: white;
|
| 867 |
+
gap: var(--size-2);
|
| 868 |
+
padding: var(--size-2);
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
.audio-thumbnail-lg .audio-name {
|
| 872 |
+
font-size: var(--text-sm);
|
| 873 |
+
text-overflow: ellipsis;
|
| 874 |
+
overflow: hidden;
|
| 875 |
+
white-space: nowrap;
|
| 876 |
+
max-width: 100%;
|
| 877 |
+
text-align: center;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
/* Audio preview styles */
|
| 881 |
+
.audio-preview {
|
| 882 |
+
display: flex;
|
| 883 |
+
flex-direction: column;
|
| 884 |
+
align-items: center;
|
| 885 |
+
justify-content: center;
|
| 886 |
+
width: 100%;
|
| 887 |
+
height: 100%;
|
| 888 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 889 |
+
color: white;
|
| 890 |
+
gap: var(--size-4);
|
| 891 |
+
padding: var(--size-8);
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
.audio-icon-large {
|
| 895 |
+
opacity: 0.8;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.audio-filename {
|
| 899 |
+
font-size: var(--text-lg);
|
| 900 |
+
font-weight: var(--weight-semibold);
|
| 901 |
+
text-align: center;
|
| 902 |
+
max-width: 80%;
|
| 903 |
+
overflow: hidden;
|
| 904 |
+
text-overflow: ellipsis;
|
| 905 |
+
white-space: nowrap;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.audio-player {
|
| 909 |
+
width: 80%;
|
| 910 |
+
max-width: 400px;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
/* Media type badge styles */
|
| 914 |
+
.media-type-badge {
|
| 915 |
+
position: absolute;
|
| 916 |
+
bottom: var(--size-1);
|
| 917 |
+
left: var(--size-1);
|
| 918 |
+
display: flex;
|
| 919 |
+
align-items: center;
|
| 920 |
+
justify-content: center;
|
| 921 |
+
padding: var(--size-1);
|
| 922 |
+
border-radius: var(--radius-sm);
|
| 923 |
+
background: rgba(0, 0, 0, 0.6);
|
| 924 |
+
color: white;
|
| 925 |
+
z-index: var(--layer-1);
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.media-type-badge.image {
|
| 929 |
+
background: rgba(59, 130, 246, 0.8);
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.media-type-badge.video {
|
| 933 |
+
background: rgba(239, 68, 68, 0.8);
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
.media-type-badge.audio {
|
| 937 |
+
background: rgba(139, 92, 246, 0.8);
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.media-type-badge svg {
|
| 941 |
+
width: 12px;
|
| 942 |
+
height: 12px;
|
| 943 |
+
}
|
| 944 |
+
|
| 945 |
+
/* Remove button */
|
| 946 |
+
.remove-btn {
|
| 947 |
+
position: absolute;
|
| 948 |
+
top: var(--size-1);
|
| 949 |
+
right: var(--size-1);
|
| 950 |
+
display: flex;
|
| 951 |
+
align-items: center;
|
| 952 |
+
justify-content: center;
|
| 953 |
+
width: 24px;
|
| 954 |
+
height: 24px;
|
| 955 |
+
border-radius: 50%;
|
| 956 |
+
background: rgba(0, 0, 0, 0.7);
|
| 957 |
+
color: white;
|
| 958 |
+
border: none;
|
| 959 |
+
cursor: pointer;
|
| 960 |
+
opacity: 0;
|
| 961 |
+
transition: opacity 0.2s ease, background 0.2s ease;
|
| 962 |
+
z-index: var(--layer-2);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.thumbnail-wrapper:hover .remove-btn {
|
| 966 |
+
opacity: 1;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.remove-btn:hover {
|
| 970 |
+
background: rgba(239, 68, 68, 0.9);
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
.remove-btn svg {
|
| 974 |
+
width: 14px;
|
| 975 |
+
height: 14px;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
/* Filename label */
|
| 979 |
+
.filename-label {
|
| 980 |
+
position: absolute;
|
| 981 |
+
bottom: 0;
|
| 982 |
+
left: 0;
|
| 983 |
+
right: 0;
|
| 984 |
+
padding: var(--size-1) var(--size-2);
|
| 985 |
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
| 986 |
+
color: white;
|
| 987 |
+
font-size: var(--text-xs);
|
| 988 |
+
text-overflow: ellipsis;
|
| 989 |
+
overflow: hidden;
|
| 990 |
+
white-space: nowrap;
|
| 991 |
+
pointer-events: none;
|
| 992 |
+
border-radius: 0 0 var(--button-small-radius) var(--button-small-radius);
|
| 993 |
+
z-index: var(--layer-1);
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
/* Add Media button bar */
|
| 997 |
+
.add-media-bar {
|
| 998 |
+
padding: var(--size-2);
|
| 999 |
+
border-top: 1px solid var(--border-color-primary);
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.add-media-btn {
|
| 1003 |
+
display: flex;
|
| 1004 |
+
align-items: center;
|
| 1005 |
+
justify-content: center;
|
| 1006 |
+
gap: var(--size-2);
|
| 1007 |
+
padding: var(--size-2) var(--size-4);
|
| 1008 |
+
background: var(--background-fill-secondary);
|
| 1009 |
+
border: 1px dashed var(--border-color-primary);
|
| 1010 |
+
border-radius: var(--radius-lg);
|
| 1011 |
+
color: var(--body-text-color-subdued);
|
| 1012 |
+
cursor: pointer;
|
| 1013 |
+
transition: all 0.2s ease;
|
| 1014 |
+
width: 100%;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
.add-media-btn:hover {
|
| 1018 |
+
background: var(--background-fill-primary);
|
| 1019 |
+
border-color: var(--color-accent);
|
| 1020 |
+
color: var(--color-accent);
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
.add-media-btn input {
|
| 1024 |
+
display: none;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
.add-media-btn svg {
|
| 1028 |
+
flex-shrink: 0;
|
| 1029 |
+
}
|
| 1030 |
+
</style>
|
mediagallery/frontend/shared/utils.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { uploadToHuggingFace } from "@gradio/utils";
|
| 2 |
+
import type { GalleryData } from "../types";
|
| 3 |
+
import { getMediaFile } from "../types";
|
| 4 |
+
|
| 5 |
+
export async function format_gallery_for_sharing(
|
| 6 |
+
value: GalleryData[] | null
|
| 7 |
+
): Promise<string> {
|
| 8 |
+
if (!value) return "";
|
| 9 |
+
let urls = await Promise.all(
|
| 10 |
+
value.map(async (item) => {
|
| 11 |
+
const file = getMediaFile(item);
|
| 12 |
+
if (!file || !file.url) return "";
|
| 13 |
+
return await uploadToHuggingFace(file.url, "url");
|
| 14 |
+
})
|
| 15 |
+
);
|
| 16 |
+
|
| 17 |
+
return `<div style="display: flex; flex-wrap: wrap; gap: 16px">${urls
|
| 18 |
+
.filter(url => url)
|
| 19 |
+
.map((url) => `<img src="${url}" style="height: 400px" />`)
|
| 20 |
+
.join("")}</div>`;
|
| 21 |
+
}
|
mediagallery/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 |
+
}
|
mediagallery/frontend/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface GalleryAudio {
|
| 14 |
+
audio: FileData;
|
| 15 |
+
caption: string | null;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export type GalleryData = GalleryImage | GalleryVideo | GalleryAudio;
|
| 19 |
+
|
| 20 |
+
// Helper to detect media type
|
| 21 |
+
export type MediaType = "image" | "video" | "audio";
|
| 22 |
+
|
| 23 |
+
export function getMediaType(item: GalleryData): MediaType {
|
| 24 |
+
if ("video" in item) return "video";
|
| 25 |
+
if ("audio" in item) return "audio";
|
| 26 |
+
return "image";
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export function getMediaFile(item: GalleryData): FileData {
|
| 30 |
+
if ("video" in item) return item.video;
|
| 31 |
+
if ("audio" in item) return item.audio;
|
| 32 |
+
return item.image;
|
| 33 |
+
}
|
mediagallery/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 = "YOUR NAME", email = "YOUREMAIL@domain.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"]
|
| 52 |
+
|
| 53 |
+
[tool.hatch.build.targets.wheel]
|
| 54 |
+
packages = ["/backend/gradio_mediagallery"]
|