Spaces:
Running
on
Zero
Running
on
Zero
aknapitsch user
commited on
Commit
·
478d9e0
1
Parent(s):
19d7794
common video and image upload field
Browse files
app.py
CHANGED
|
@@ -19,8 +19,8 @@ import gradio as gr
|
|
| 19 |
import numpy as np
|
| 20 |
import spaces
|
| 21 |
import torch
|
| 22 |
-
from pillow_heif import register_heif_opener
|
| 23 |
from PIL import Image
|
|
|
|
| 24 |
|
| 25 |
register_heif_opener()
|
| 26 |
|
|
@@ -368,7 +368,7 @@ def populate_visualization_tabs(processed_data):
|
|
| 368 |
# -------------------------------------------------------------------------
|
| 369 |
# 2) Handle uploaded video/images --> produce target_dir + images
|
| 370 |
# -------------------------------------------------------------------------
|
| 371 |
-
def handle_uploads(
|
| 372 |
"""
|
| 373 |
Create a new 'target_dir' + 'images' subfolder, and place user-uploaded
|
| 374 |
images or extracted frames from video into it. Return (target_dir, image_paths).
|
|
@@ -390,76 +390,100 @@ def handle_uploads(input_video, input_images, s_time_interval=1.0):
|
|
| 390 |
|
| 391 |
image_paths = []
|
| 392 |
|
| 393 |
-
# --- Handle images ---
|
| 394 |
-
if
|
| 395 |
-
for file_data in
|
| 396 |
if isinstance(file_data, dict) and "name" in file_data:
|
| 397 |
file_path = file_data["name"]
|
| 398 |
else:
|
| 399 |
-
file_path = file_data
|
| 400 |
-
|
| 401 |
-
# Check if the file is a HEIC image
|
| 402 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
image_paths.append(dst_path)
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
shutil.copy(file_path, dst_path)
|
| 424 |
image_paths.append(dst_path)
|
| 425 |
-
else:
|
| 426 |
-
# Regular image files - copy as is
|
| 427 |
-
dst_path = os.path.join(target_dir_images, os.path.basename(file_path))
|
| 428 |
-
shutil.copy(file_path, dst_path)
|
| 429 |
-
image_paths.append(dst_path)
|
| 430 |
-
|
| 431 |
-
# --- Handle video ---
|
| 432 |
-
if input_video is not None:
|
| 433 |
-
if isinstance(input_video, dict) and "name" in input_video:
|
| 434 |
-
video_path = input_video["name"]
|
| 435 |
-
else:
|
| 436 |
-
video_path = input_video
|
| 437 |
-
|
| 438 |
-
vs = cv2.VideoCapture(video_path)
|
| 439 |
-
fps = vs.get(cv2.CAP_PROP_FPS)
|
| 440 |
-
frame_interval = int(fps * s_time_interval) # 1 frame/sec
|
| 441 |
-
|
| 442 |
-
count = 0
|
| 443 |
-
video_frame_num = 0
|
| 444 |
-
while True:
|
| 445 |
-
gotit, frame = vs.read()
|
| 446 |
-
if not gotit:
|
| 447 |
-
break
|
| 448 |
-
count += 1
|
| 449 |
-
if count % frame_interval == 0:
|
| 450 |
-
image_path = os.path.join(
|
| 451 |
-
target_dir_images, f"{video_frame_num:06}.png"
|
| 452 |
-
)
|
| 453 |
-
cv2.imwrite(image_path, frame)
|
| 454 |
-
image_paths.append(image_path)
|
| 455 |
-
video_frame_num += 1
|
| 456 |
|
| 457 |
# Sort final images for gallery
|
| 458 |
image_paths = sorted(image_paths)
|
| 459 |
|
| 460 |
end_time = time.time()
|
| 461 |
print(
|
| 462 |
-
f"Files
|
| 463 |
)
|
| 464 |
return target_dir, image_paths
|
| 465 |
|
|
@@ -1058,8 +1082,14 @@ def load_example_scene(scene_name, examples_dir="examples"):
|
|
| 1058 |
if selected_scene is None:
|
| 1059 |
return None, None, None, "Scene not found"
|
| 1060 |
|
| 1061 |
-
# Create
|
| 1062 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
|
| 1064 |
return (
|
| 1065 |
None, # Clear reconstruction output
|
|
@@ -1089,19 +1119,30 @@ with gr.Blocks(theme=theme, css=GRADIO_CSS) as demo:
|
|
| 1089 |
|
| 1090 |
with gr.Row():
|
| 1091 |
with gr.Column(scale=2):
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
value=1.0,
|
| 1097 |
-
step=0.1,
|
| 1098 |
-
label="Sample time interval (take a sample every x sec.)",
|
| 1099 |
interactive=True,
|
| 1100 |
-
|
| 1101 |
-
)
|
| 1102 |
-
input_images = gr.File(
|
| 1103 |
-
file_count="multiple", label="Upload Images", interactive=True
|
| 1104 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1105 |
|
| 1106 |
image_gallery = gr.Gallery(
|
| 1107 |
label="Preview",
|
|
@@ -1112,6 +1153,13 @@ with gr.Blocks(theme=theme, css=GRADIO_CSS) as demo:
|
|
| 1112 |
preview=True,
|
| 1113 |
)
|
| 1114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
with gr.Column(scale=4):
|
| 1116 |
with gr.Column():
|
| 1117 |
gr.Markdown(
|
|
@@ -1202,8 +1250,7 @@ with gr.Blocks(theme=theme, css=GRADIO_CSS) as demo:
|
|
| 1202 |
submit_btn = gr.Button("Reconstruct", scale=1, variant="primary")
|
| 1203 |
clear_btn = gr.ClearButton(
|
| 1204 |
[
|
| 1205 |
-
|
| 1206 |
-
input_images,
|
| 1207 |
reconstruction_output,
|
| 1208 |
log_output,
|
| 1209 |
target_dir_output,
|
|
@@ -1422,15 +1469,130 @@ with gr.Blocks(theme=theme, css=GRADIO_CSS) as demo:
|
|
| 1422 |
# -------------------------------------------------------------------------
|
| 1423 |
# Auto-update gallery whenever user uploads or changes their files
|
| 1424 |
# -------------------------------------------------------------------------
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1429 |
)
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1434 |
)
|
| 1435 |
|
| 1436 |
# -------------------------------------------------------------------------
|
|
|
|
| 19 |
import numpy as np
|
| 20 |
import spaces
|
| 21 |
import torch
|
|
|
|
| 22 |
from PIL import Image
|
| 23 |
+
from pillow_heif import register_heif_opener
|
| 24 |
|
| 25 |
register_heif_opener()
|
| 26 |
|
|
|
|
| 368 |
# -------------------------------------------------------------------------
|
| 369 |
# 2) Handle uploaded video/images --> produce target_dir + images
|
| 370 |
# -------------------------------------------------------------------------
|
| 371 |
+
def handle_uploads(unified_upload, s_time_interval=1.0):
|
| 372 |
"""
|
| 373 |
Create a new 'target_dir' + 'images' subfolder, and place user-uploaded
|
| 374 |
images or extracted frames from video into it. Return (target_dir, image_paths).
|
|
|
|
| 390 |
|
| 391 |
image_paths = []
|
| 392 |
|
| 393 |
+
# --- Handle uploaded files (both images and videos) ---
|
| 394 |
+
if unified_upload is not None:
|
| 395 |
+
for file_data in unified_upload:
|
| 396 |
if isinstance(file_data, dict) and "name" in file_data:
|
| 397 |
file_path = file_data["name"]
|
| 398 |
else:
|
| 399 |
+
file_path = str(file_data)
|
| 400 |
+
|
|
|
|
| 401 |
file_ext = os.path.splitext(file_path)[1].lower()
|
| 402 |
+
|
| 403 |
+
# Check if it's a video file
|
| 404 |
+
video_extensions = [
|
| 405 |
+
".mp4",
|
| 406 |
+
".avi",
|
| 407 |
+
".mov",
|
| 408 |
+
".mkv",
|
| 409 |
+
".wmv",
|
| 410 |
+
".flv",
|
| 411 |
+
".webm",
|
| 412 |
+
".m4v",
|
| 413 |
+
".3gp",
|
| 414 |
+
]
|
| 415 |
+
if file_ext in video_extensions:
|
| 416 |
+
# Handle as video
|
| 417 |
+
vs = cv2.VideoCapture(file_path)
|
| 418 |
+
fps = vs.get(cv2.CAP_PROP_FPS)
|
| 419 |
+
frame_interval = int(fps * s_time_interval) # frames per interval
|
| 420 |
+
|
| 421 |
+
count = 0
|
| 422 |
+
video_frame_num = 0
|
| 423 |
+
while True:
|
| 424 |
+
gotit, frame = vs.read()
|
| 425 |
+
if not gotit:
|
| 426 |
+
break
|
| 427 |
+
count += 1
|
| 428 |
+
if count % frame_interval == 0:
|
| 429 |
+
# Use original filename as prefix for frames
|
| 430 |
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
| 431 |
+
image_path = os.path.join(
|
| 432 |
+
target_dir_images, f"{base_name}_{video_frame_num:06}.png"
|
| 433 |
+
)
|
| 434 |
+
cv2.imwrite(image_path, frame)
|
| 435 |
+
image_paths.append(image_path)
|
| 436 |
+
video_frame_num += 1
|
| 437 |
+
vs.release()
|
| 438 |
+
print(
|
| 439 |
+
f"Extracted {video_frame_num} frames from video: {os.path.basename(file_path)}"
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
else:
|
| 443 |
+
# Handle as image
|
| 444 |
+
# Check if the file is a HEIC image
|
| 445 |
+
if file_ext in [".heic", ".heif"]:
|
| 446 |
+
# Convert HEIC to JPEG for better gallery compatibility
|
| 447 |
+
try:
|
| 448 |
+
with Image.open(file_path) as img:
|
| 449 |
+
# Convert to RGB if necessary (HEIC can have different color modes)
|
| 450 |
+
if img.mode not in ("RGB", "L"):
|
| 451 |
+
img = img.convert("RGB")
|
| 452 |
+
|
| 453 |
+
# Create JPEG filename
|
| 454 |
+
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
| 455 |
+
dst_path = os.path.join(
|
| 456 |
+
target_dir_images, f"{base_name}.jpg"
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
# Save as JPEG with high quality
|
| 460 |
+
img.save(dst_path, "JPEG", quality=95)
|
| 461 |
+
image_paths.append(dst_path)
|
| 462 |
+
print(
|
| 463 |
+
f"Converted HEIC to JPEG: {os.path.basename(file_path)} -> {os.path.basename(dst_path)}"
|
| 464 |
+
)
|
| 465 |
+
except Exception as e:
|
| 466 |
+
print(f"Error converting HEIC file {file_path}: {e}")
|
| 467 |
+
# Fall back to copying as is
|
| 468 |
+
dst_path = os.path.join(
|
| 469 |
+
target_dir_images, os.path.basename(file_path)
|
| 470 |
+
)
|
| 471 |
+
shutil.copy(file_path, dst_path)
|
| 472 |
image_paths.append(dst_path)
|
| 473 |
+
else:
|
| 474 |
+
# Regular image files - copy as is
|
| 475 |
+
dst_path = os.path.join(
|
| 476 |
+
target_dir_images, os.path.basename(file_path)
|
| 477 |
+
)
|
| 478 |
shutil.copy(file_path, dst_path)
|
| 479 |
image_paths.append(dst_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
|
| 481 |
# Sort final images for gallery
|
| 482 |
image_paths = sorted(image_paths)
|
| 483 |
|
| 484 |
end_time = time.time()
|
| 485 |
print(
|
| 486 |
+
f"Files processed to {target_dir_images}; took {end_time - start_time:.3f} seconds"
|
| 487 |
)
|
| 488 |
return target_dir, image_paths
|
| 489 |
|
|
|
|
| 1082 |
if selected_scene is None:
|
| 1083 |
return None, None, None, "Scene not found"
|
| 1084 |
|
| 1085 |
+
# Create file-like objects for the unified upload system
|
| 1086 |
+
# Convert image file paths to the format expected by unified_upload
|
| 1087 |
+
file_objects = []
|
| 1088 |
+
for image_path in selected_scene["image_files"]:
|
| 1089 |
+
file_objects.append(image_path)
|
| 1090 |
+
|
| 1091 |
+
# Create target directory and copy images using the unified upload system
|
| 1092 |
+
target_dir, image_paths = handle_uploads(file_objects, 1.0)
|
| 1093 |
|
| 1094 |
return (
|
| 1095 |
None, # Clear reconstruction output
|
|
|
|
| 1119 |
|
| 1120 |
with gr.Row():
|
| 1121 |
with gr.Column(scale=2):
|
| 1122 |
+
# Unified upload component for both videos and images
|
| 1123 |
+
unified_upload = gr.File(
|
| 1124 |
+
file_count="multiple",
|
| 1125 |
+
label="Upload Video or Images",
|
|
|
|
|
|
|
|
|
|
| 1126 |
interactive=True,
|
| 1127 |
+
file_types=["image", "video"],
|
|
|
|
|
|
|
|
|
|
| 1128 |
)
|
| 1129 |
+
with gr.Row():
|
| 1130 |
+
s_time_interval = gr.Slider(
|
| 1131 |
+
minimum=0.1,
|
| 1132 |
+
maximum=5.0,
|
| 1133 |
+
value=1.0,
|
| 1134 |
+
step=0.1,
|
| 1135 |
+
label="Video sample time interval (take a sample every x sec.)",
|
| 1136 |
+
interactive=True,
|
| 1137 |
+
visible=True,
|
| 1138 |
+
scale=3,
|
| 1139 |
+
)
|
| 1140 |
+
resample_btn = gr.Button(
|
| 1141 |
+
"Resample Video",
|
| 1142 |
+
visible=False,
|
| 1143 |
+
variant="secondary",
|
| 1144 |
+
scale=1,
|
| 1145 |
+
)
|
| 1146 |
|
| 1147 |
image_gallery = gr.Gallery(
|
| 1148 |
label="Preview",
|
|
|
|
| 1153 |
preview=True,
|
| 1154 |
)
|
| 1155 |
|
| 1156 |
+
clear_uploads_btn = gr.ClearButton(
|
| 1157 |
+
[unified_upload, image_gallery],
|
| 1158 |
+
value="Clear Uploads",
|
| 1159 |
+
variant="secondary",
|
| 1160 |
+
size="sm",
|
| 1161 |
+
)
|
| 1162 |
+
|
| 1163 |
with gr.Column(scale=4):
|
| 1164 |
with gr.Column():
|
| 1165 |
gr.Markdown(
|
|
|
|
| 1250 |
submit_btn = gr.Button("Reconstruct", scale=1, variant="primary")
|
| 1251 |
clear_btn = gr.ClearButton(
|
| 1252 |
[
|
| 1253 |
+
unified_upload,
|
|
|
|
| 1254 |
reconstruction_output,
|
| 1255 |
log_output,
|
| 1256 |
target_dir_output,
|
|
|
|
| 1469 |
# -------------------------------------------------------------------------
|
| 1470 |
# Auto-update gallery whenever user uploads or changes their files
|
| 1471 |
# -------------------------------------------------------------------------
|
| 1472 |
+
def update_gallery_on_unified_upload(files, interval):
|
| 1473 |
+
if not files:
|
| 1474 |
+
return None, None, None
|
| 1475 |
+
target_dir, image_paths = handle_uploads(files, interval)
|
| 1476 |
+
return (
|
| 1477 |
+
target_dir,
|
| 1478 |
+
image_paths,
|
| 1479 |
+
"Upload complete. Click 'Reconstruct' to begin 3D processing.",
|
| 1480 |
+
)
|
| 1481 |
+
|
| 1482 |
+
def show_resample_button(files):
|
| 1483 |
+
"""Show the resample button only if there are uploaded files containing videos"""
|
| 1484 |
+
if not files:
|
| 1485 |
+
return gr.update(visible=False)
|
| 1486 |
+
|
| 1487 |
+
# Check if any uploaded files are videos
|
| 1488 |
+
video_extensions = [
|
| 1489 |
+
".mp4",
|
| 1490 |
+
".avi",
|
| 1491 |
+
".mov",
|
| 1492 |
+
".mkv",
|
| 1493 |
+
".wmv",
|
| 1494 |
+
".flv",
|
| 1495 |
+
".webm",
|
| 1496 |
+
".m4v",
|
| 1497 |
+
".3gp",
|
| 1498 |
+
]
|
| 1499 |
+
has_video = False
|
| 1500 |
+
|
| 1501 |
+
for file_data in files:
|
| 1502 |
+
if isinstance(file_data, dict) and "name" in file_data:
|
| 1503 |
+
file_path = file_data["name"]
|
| 1504 |
+
else:
|
| 1505 |
+
file_path = str(file_data)
|
| 1506 |
+
|
| 1507 |
+
file_ext = os.path.splitext(file_path)[1].lower()
|
| 1508 |
+
if file_ext in video_extensions:
|
| 1509 |
+
has_video = True
|
| 1510 |
+
break
|
| 1511 |
+
|
| 1512 |
+
return gr.update(visible=has_video)
|
| 1513 |
+
|
| 1514 |
+
def hide_resample_button():
|
| 1515 |
+
"""Hide the resample button after use"""
|
| 1516 |
+
return gr.update(visible=False)
|
| 1517 |
+
|
| 1518 |
+
def resample_video_with_new_interval(files, new_interval, current_target_dir):
|
| 1519 |
+
"""Resample video with new slider value"""
|
| 1520 |
+
if not files:
|
| 1521 |
+
return (
|
| 1522 |
+
current_target_dir,
|
| 1523 |
+
None,
|
| 1524 |
+
"No files to resample.",
|
| 1525 |
+
gr.update(visible=False),
|
| 1526 |
+
)
|
| 1527 |
+
|
| 1528 |
+
# Check if we have videos to resample
|
| 1529 |
+
video_extensions = [
|
| 1530 |
+
".mp4",
|
| 1531 |
+
".avi",
|
| 1532 |
+
".mov",
|
| 1533 |
+
".mkv",
|
| 1534 |
+
".wmv",
|
| 1535 |
+
".flv",
|
| 1536 |
+
".webm",
|
| 1537 |
+
".m4v",
|
| 1538 |
+
".3gp",
|
| 1539 |
+
]
|
| 1540 |
+
has_video = any(
|
| 1541 |
+
os.path.splitext(
|
| 1542 |
+
str(file_data["name"] if isinstance(file_data, dict) else file_data)
|
| 1543 |
+
)[1].lower()
|
| 1544 |
+
in video_extensions
|
| 1545 |
+
for file_data in files
|
| 1546 |
+
)
|
| 1547 |
+
|
| 1548 |
+
if not has_video:
|
| 1549 |
+
return (
|
| 1550 |
+
current_target_dir,
|
| 1551 |
+
None,
|
| 1552 |
+
"No videos found to resample.",
|
| 1553 |
+
gr.update(visible=False),
|
| 1554 |
+
)
|
| 1555 |
+
|
| 1556 |
+
# Clean up old target directory if it exists
|
| 1557 |
+
if (
|
| 1558 |
+
current_target_dir
|
| 1559 |
+
and current_target_dir != "None"
|
| 1560 |
+
and os.path.exists(current_target_dir)
|
| 1561 |
+
):
|
| 1562 |
+
shutil.rmtree(current_target_dir)
|
| 1563 |
+
|
| 1564 |
+
# Process files with new interval
|
| 1565 |
+
target_dir, image_paths = handle_uploads(files, new_interval)
|
| 1566 |
+
|
| 1567 |
+
return (
|
| 1568 |
+
target_dir,
|
| 1569 |
+
image_paths,
|
| 1570 |
+
f"Video resampled with {new_interval}s interval. Click 'Reconstruct' to begin 3D processing.",
|
| 1571 |
+
gr.update(visible=False),
|
| 1572 |
+
)
|
| 1573 |
+
|
| 1574 |
+
unified_upload.change(
|
| 1575 |
+
fn=update_gallery_on_unified_upload,
|
| 1576 |
+
inputs=[unified_upload, s_time_interval],
|
| 1577 |
+
outputs=[target_dir_output, image_gallery, log_output],
|
| 1578 |
+
).then(
|
| 1579 |
+
fn=show_resample_button,
|
| 1580 |
+
inputs=[unified_upload],
|
| 1581 |
+
outputs=[resample_btn],
|
| 1582 |
)
|
| 1583 |
+
|
| 1584 |
+
# Show resample button when slider changes (only if files are uploaded)
|
| 1585 |
+
s_time_interval.change(
|
| 1586 |
+
fn=show_resample_button,
|
| 1587 |
+
inputs=[unified_upload],
|
| 1588 |
+
outputs=[resample_btn],
|
| 1589 |
+
)
|
| 1590 |
+
|
| 1591 |
+
# Handle resample button click
|
| 1592 |
+
resample_btn.click(
|
| 1593 |
+
fn=resample_video_with_new_interval,
|
| 1594 |
+
inputs=[unified_upload, s_time_interval, target_dir_output],
|
| 1595 |
+
outputs=[target_dir_output, image_gallery, log_output, resample_btn],
|
| 1596 |
)
|
| 1597 |
|
| 1598 |
# -------------------------------------------------------------------------
|