Spaces:
Sleeping
Sleeping
small improvements in veo handling
Browse files- app.py +63 -11
- providers.py +38 -0
app.py
CHANGED
|
@@ -100,6 +100,23 @@ def format_metadata(provider, status, error=None, duration=None):
|
|
| 100 |
return "\n".join(lines)
|
| 101 |
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def format_result_metadata(result: RunResult) -> str:
|
| 104 |
lines = [
|
| 105 |
f"**{result.provider_name}**",
|
|
@@ -178,15 +195,18 @@ def on_submit(image_path, prompt, duration_seconds, resolution, history, *dynami
|
|
| 178 |
selected_names = {provider.name for provider in selected_runtime_providers}
|
| 179 |
|
| 180 |
metadata_values = []
|
|
|
|
| 181 |
for provider in PROVIDERS:
|
| 182 |
if provider.name in selected_names:
|
| 183 |
metadata_values.append(
|
| 184 |
format_metadata(apply_global_settings(provider, prompt, duration_seconds, resolution), "running")
|
| 185 |
)
|
|
|
|
| 186 |
else:
|
| 187 |
metadata_values.append(
|
| 188 |
format_metadata(apply_global_settings(provider, prompt, duration_seconds, resolution), "skipped")
|
| 189 |
)
|
|
|
|
| 190 |
results_by_name[provider.name] = RunResult(
|
| 191 |
provider_name=provider.name,
|
| 192 |
output_path=None,
|
|
@@ -196,7 +216,7 @@ def on_submit(image_path, prompt, duration_seconds, resolution, history, *dynami
|
|
| 196 |
params_used=provider.params,
|
| 197 |
)
|
| 198 |
video_values = [None] * len(PROVIDERS)
|
| 199 |
-
yield metadata_values + video_values + [history]
|
| 200 |
|
| 201 |
for provider, video_bytes, error, duration in run_providers(
|
| 202 |
selected_runtime_providers, image_path, prompt, duration_seconds, resolution
|
|
@@ -214,6 +234,7 @@ def on_submit(image_path, prompt, duration_seconds, resolution, history, *dynami
|
|
| 214 |
params_used=provider.params,
|
| 215 |
)
|
| 216 |
metadata_values[index] = format_metadata(provider, "ok", duration=duration)
|
|
|
|
| 217 |
video_values[index] = output_path
|
| 218 |
else:
|
| 219 |
results_by_name[provider.name] = RunResult(
|
|
@@ -225,9 +246,10 @@ def on_submit(image_path, prompt, duration_seconds, resolution, history, *dynami
|
|
| 225 |
params_used=provider.params,
|
| 226 |
)
|
| 227 |
metadata_values[index] = format_metadata(provider, "error", error=str(error), duration=duration)
|
|
|
|
| 228 |
video_values[index] = None
|
| 229 |
|
| 230 |
-
yield metadata_values + video_values + [history]
|
| 231 |
|
| 232 |
run = Run(
|
| 233 |
run_id=run_id,
|
|
@@ -239,8 +261,26 @@ def on_submit(image_path, prompt, duration_seconds, resolution, history, *dynami
|
|
| 239 |
save_run(run)
|
| 240 |
logger.info("Run %s: saved", run_id)
|
| 241 |
|
| 242 |
-
yield metadata_values + video_values + [history + [run]]
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
with gr.Blocks(title="Kaleidoscope") as demo:
|
| 246 |
gr.Markdown(
|
|
@@ -297,7 +337,8 @@ with gr.Blocks(title="Kaleidoscope") as demo:
|
|
| 297 |
)
|
| 298 |
)
|
| 299 |
|
| 300 |
-
|
|
|
|
| 301 |
prompt_input = gr.Textbox(
|
| 302 |
label="Prompt (optional)",
|
| 303 |
placeholder="Used by providers that support a prompt; ignored by the rest.",
|
|
@@ -307,17 +348,20 @@ with gr.Blocks(title="Kaleidoscope") as demo:
|
|
| 307 |
gr.Markdown("## Results")
|
| 308 |
check_all_btn = gr.Button("Check all models")
|
| 309 |
enabled_components = []
|
|
|
|
| 310 |
metadata_components = []
|
| 311 |
video_components = []
|
| 312 |
for provider in PROVIDERS:
|
| 313 |
with gr.Row():
|
| 314 |
-
|
|
|
|
|
|
|
| 315 |
with gr.Accordion(label=provider.name, open=False):
|
| 316 |
with gr.Row():
|
| 317 |
with gr.Column(scale=1):
|
| 318 |
metadata_components.append(gr.Markdown(format_metadata(provider, "idle")))
|
| 319 |
with gr.Column(scale=2):
|
| 320 |
-
video_components.append(gr.Video(label=provider.name))
|
| 321 |
|
| 322 |
gr.Markdown("## Past runs")
|
| 323 |
# Start empty and populate via demo.load() below, so every new page
|
|
@@ -382,15 +426,23 @@ with gr.Blocks(title="Kaleidoscope") as demo:
|
|
| 382 |
with gr.Column(scale=1):
|
| 383 |
gr.Markdown(format_result_metadata(result))
|
| 384 |
with gr.Column(scale=2):
|
| 385 |
-
gr.Video(value=result.output_path)
|
| 386 |
|
| 387 |
submit_btn.click(
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
inputs=[image_input, prompt_input, duration_input, resolution_input, history_state]
|
| 390 |
+ token_inputs
|
| 391 |
+ extra_config_inputs
|
| 392 |
+ enabled_components,
|
| 393 |
-
outputs=metadata_components + video_components + [history_state],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
)
|
| 395 |
|
| 396 |
demo.queue()
|
|
@@ -400,4 +452,4 @@ if __name__ == "__main__":
|
|
| 400 |
# /data (on a Hugging Face Space) lives outside the cwd and the system
|
| 401 |
# temp dir, so Gradio refuses to serve run files from it unless the
|
| 402 |
# directory is explicitly allow-listed here.
|
| 403 |
-
demo.launch(allowed_paths=[get_data_dir()])
|
|
|
|
| 100 |
return "\n".join(lines)
|
| 101 |
|
| 102 |
|
| 103 |
+
# Small per-provider status indicator shown next to its checkbox (visible
|
| 104 |
+
# without expanding that provider's accordion) - a CSS spinner while a
|
| 105 |
+
# request is in flight, then a static icon once it settles. The spinner's
|
| 106 |
+
# look is defined by STATUS_SPINNER_CSS, injected once via gr.Blocks(css=...).
|
| 107 |
+
_STATUS_ICONS = {
|
| 108 |
+
"idle": "",
|
| 109 |
+
"running": '<span class="ks-spinner" title="Running"></span>',
|
| 110 |
+
"ok": '<span title="Done" style="font-size:1.1em;">\u2705</span>',
|
| 111 |
+
"error": '<span title="Error" style="font-size:1.1em;">\u274c</span>',
|
| 112 |
+
"skipped": '<span title="Skipped" style="font-size:1.1em;">\u23ed\ufe0f</span>',
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def format_status_icon(status: str) -> str:
|
| 117 |
+
return _STATUS_ICONS.get(status, "")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
def format_result_metadata(result: RunResult) -> str:
|
| 121 |
lines = [
|
| 122 |
f"**{result.provider_name}**",
|
|
|
|
| 195 |
selected_names = {provider.name for provider in selected_runtime_providers}
|
| 196 |
|
| 197 |
metadata_values = []
|
| 198 |
+
status_values = []
|
| 199 |
for provider in PROVIDERS:
|
| 200 |
if provider.name in selected_names:
|
| 201 |
metadata_values.append(
|
| 202 |
format_metadata(apply_global_settings(provider, prompt, duration_seconds, resolution), "running")
|
| 203 |
)
|
| 204 |
+
status_values.append(format_status_icon("running"))
|
| 205 |
else:
|
| 206 |
metadata_values.append(
|
| 207 |
format_metadata(apply_global_settings(provider, prompt, duration_seconds, resolution), "skipped")
|
| 208 |
)
|
| 209 |
+
status_values.append(format_status_icon("skipped"))
|
| 210 |
results_by_name[provider.name] = RunResult(
|
| 211 |
provider_name=provider.name,
|
| 212 |
output_path=None,
|
|
|
|
| 216 |
params_used=provider.params,
|
| 217 |
)
|
| 218 |
video_values = [None] * len(PROVIDERS)
|
| 219 |
+
yield metadata_values + status_values + video_values + [history]
|
| 220 |
|
| 221 |
for provider, video_bytes, error, duration in run_providers(
|
| 222 |
selected_runtime_providers, image_path, prompt, duration_seconds, resolution
|
|
|
|
| 234 |
params_used=provider.params,
|
| 235 |
)
|
| 236 |
metadata_values[index] = format_metadata(provider, "ok", duration=duration)
|
| 237 |
+
status_values[index] = format_status_icon("ok")
|
| 238 |
video_values[index] = output_path
|
| 239 |
else:
|
| 240 |
results_by_name[provider.name] = RunResult(
|
|
|
|
| 246 |
params_used=provider.params,
|
| 247 |
)
|
| 248 |
metadata_values[index] = format_metadata(provider, "error", error=str(error), duration=duration)
|
| 249 |
+
status_values[index] = format_status_icon("error")
|
| 250 |
video_values[index] = None
|
| 251 |
|
| 252 |
+
yield metadata_values + status_values + video_values + [history]
|
| 253 |
|
| 254 |
run = Run(
|
| 255 |
run_id=run_id,
|
|
|
|
| 261 |
save_run(run)
|
| 262 |
logger.info("Run %s: saved", run_id)
|
| 263 |
|
| 264 |
+
yield metadata_values + status_values + video_values + [history + [run]]
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# CSS for the small per-provider running spinner (see format_status_icon) -
|
| 268 |
+
# a plain rotating-border circle so no extra asset/dependency is needed.
|
| 269 |
+
STATUS_SPINNER_CSS = """
|
| 270 |
+
.ks-spinner {
|
| 271 |
+
display: inline-block;
|
| 272 |
+
width: 14px;
|
| 273 |
+
height: 14px;
|
| 274 |
+
border: 2px solid var(--border-color-primary, #999);
|
| 275 |
+
border-top-color: var(--color-accent, #555);
|
| 276 |
+
border-radius: 50%;
|
| 277 |
+
animation: ks-spin 0.8s linear infinite;
|
| 278 |
+
vertical-align: middle;
|
| 279 |
+
}
|
| 280 |
+
@keyframes ks-spin {
|
| 281 |
+
to { transform: rotate(360deg); }
|
| 282 |
+
}
|
| 283 |
+
"""
|
| 284 |
|
| 285 |
with gr.Blocks(title="Kaleidoscope") as demo:
|
| 286 |
gr.Markdown(
|
|
|
|
| 337 |
)
|
| 338 |
)
|
| 339 |
|
| 340 |
+
with gr.Accordion(label="Input image", open=True) as image_accordion:
|
| 341 |
+
image_input = gr.Image(type="filepath", label="Input image")
|
| 342 |
prompt_input = gr.Textbox(
|
| 343 |
label="Prompt (optional)",
|
| 344 |
placeholder="Used by providers that support a prompt; ignored by the rest.",
|
|
|
|
| 348 |
gr.Markdown("## Results")
|
| 349 |
check_all_btn = gr.Button("Check all models")
|
| 350 |
enabled_components = []
|
| 351 |
+
status_components = []
|
| 352 |
metadata_components = []
|
| 353 |
video_components = []
|
| 354 |
for provider in PROVIDERS:
|
| 355 |
with gr.Row():
|
| 356 |
+
with gr.Column(scale=0, min_width=90):
|
| 357 |
+
enabled_components.append(gr.Checkbox(label="Use", value=True))
|
| 358 |
+
status_components.append(gr.HTML(value=format_status_icon("idle")))
|
| 359 |
with gr.Accordion(label=provider.name, open=False):
|
| 360 |
with gr.Row():
|
| 361 |
with gr.Column(scale=1):
|
| 362 |
metadata_components.append(gr.Markdown(format_metadata(provider, "idle")))
|
| 363 |
with gr.Column(scale=2):
|
| 364 |
+
video_components.append(gr.Video(label=provider.name, interactive=False))
|
| 365 |
|
| 366 |
gr.Markdown("## Past runs")
|
| 367 |
# Start empty and populate via demo.load() below, so every new page
|
|
|
|
| 426 |
with gr.Column(scale=1):
|
| 427 |
gr.Markdown(format_result_metadata(result))
|
| 428 |
with gr.Column(scale=2):
|
| 429 |
+
gr.Video(value=result.output_path, interactive=False)
|
| 430 |
|
| 431 |
submit_btn.click(
|
| 432 |
+
fn=lambda: (gr.update(interactive=False), gr.update(open=False)),
|
| 433 |
+
inputs=None,
|
| 434 |
+
outputs=[submit_btn, image_accordion],
|
| 435 |
+
).then(
|
| 436 |
+
fn=on_submit,
|
| 437 |
inputs=[image_input, prompt_input, duration_input, resolution_input, history_state]
|
| 438 |
+ token_inputs
|
| 439 |
+ extra_config_inputs
|
| 440 |
+ enabled_components,
|
| 441 |
+
outputs=metadata_components + status_components + video_components + [history_state],
|
| 442 |
+
).then(
|
| 443 |
+
fn=lambda: gr.update(interactive=True),
|
| 444 |
+
inputs=None,
|
| 445 |
+
outputs=submit_btn,
|
| 446 |
)
|
| 447 |
|
| 448 |
demo.queue()
|
|
|
|
| 452 |
# /data (on a Hugging Face Space) lives outside the cwd and the system
|
| 453 |
# temp dir, so Gradio refuses to serve run files from it unless the
|
| 454 |
# directory is explicitly allow-listed here.
|
| 455 |
+
demo.launch(allowed_paths=[get_data_dir()], css=STATUS_SPINNER_CSS)
|
providers.py
CHANGED
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
| 12 |
|
| 13 |
import base64
|
| 14 |
import io
|
|
|
|
| 15 |
import logging
|
| 16 |
import mimetypes
|
| 17 |
import time
|
|
@@ -501,6 +502,28 @@ def _extract_veo_video_uri(node) -> str | None:
|
|
| 501 |
return None
|
| 502 |
|
| 503 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
def veo_call(
|
| 505 |
provider: ModelProvider,
|
| 506 |
image_path: str,
|
|
@@ -565,6 +588,21 @@ def veo_call(
|
|
| 565 |
|
| 566 |
video_uri = _extract_veo_video_uri(operation.get("response"))
|
| 567 |
if not video_uri:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
raise RuntimeError(f"veo operation for '{provider.name}' succeeded but returned no video URI")
|
| 569 |
|
| 570 |
video_response = requests.get(video_uri, headers=headers, timeout=180)
|
|
|
|
| 12 |
|
| 13 |
import base64
|
| 14 |
import io
|
| 15 |
+
import json
|
| 16 |
import logging
|
| 17 |
import mimetypes
|
| 18 |
import time
|
|
|
|
| 502 |
return None
|
| 503 |
|
| 504 |
|
| 505 |
+
def _extract_veo_filter_reason(node) -> str | None:
|
| 506 |
+
"""Recursively searches a Gemini API operation response for a
|
| 507 |
+
safety-filter rejection reason (e.g. `raiMediaFilteredReasons`), which
|
| 508 |
+
is the most common real-world cause of a "done" operation with no
|
| 509 |
+
video URI - the prompt/image got silently filtered rather than the
|
| 510 |
+
response shape being unexpected."""
|
| 511 |
+
if isinstance(node, dict):
|
| 512 |
+
reasons = node.get("raiMediaFilteredReasons")
|
| 513 |
+
if reasons:
|
| 514 |
+
return "; ".join(str(reason) for reason in reasons)
|
| 515 |
+
for value in node.values():
|
| 516 |
+
found = _extract_veo_filter_reason(value)
|
| 517 |
+
if found:
|
| 518 |
+
return found
|
| 519 |
+
elif isinstance(node, list):
|
| 520 |
+
for item in node:
|
| 521 |
+
found = _extract_veo_filter_reason(item)
|
| 522 |
+
if found:
|
| 523 |
+
return found
|
| 524 |
+
return None
|
| 525 |
+
|
| 526 |
+
|
| 527 |
def veo_call(
|
| 528 |
provider: ModelProvider,
|
| 529 |
image_path: str,
|
|
|
|
| 588 |
|
| 589 |
video_uri = _extract_veo_video_uri(operation.get("response"))
|
| 590 |
if not video_uri:
|
| 591 |
+
# Log the full response so this is actually debuggable next time -
|
| 592 |
+
# the shape of a "done" operation with no video can vary (safety
|
| 593 |
+
# filtering, quota/partial failures, an undocumented response
|
| 594 |
+
# nesting, etc.) and a bare exception message throws that
|
| 595 |
+
# information away.
|
| 596 |
+
logger.error(
|
| 597 |
+
"veo %s: operation done but no video URI found; full response=%s",
|
| 598 |
+
provider.name,
|
| 599 |
+
json.dumps(operation, indent=2)[:4000],
|
| 600 |
+
)
|
| 601 |
+
filter_reason = _extract_veo_filter_reason(operation.get("response"))
|
| 602 |
+
if filter_reason:
|
| 603 |
+
raise RuntimeError(
|
| 604 |
+
f"veo operation for '{provider.name}' was rejected by Google's safety filters: {filter_reason}"
|
| 605 |
+
)
|
| 606 |
raise RuntimeError(f"veo operation for '{provider.name}' succeeded but returned no video URI")
|
| 607 |
|
| 608 |
video_response = requests.get(video_uri, headers=headers, timeout=180)
|