TrueAl commited on
Commit
91bfaf0
·
1 Parent(s): 86024d9

small improvements in veo handling

Browse files
Files changed (2) hide show
  1. app.py +63 -11
  2. 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
- image_input = gr.Image(type="filepath", label="Input image")
 
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
- enabled_components.append(gr.Checkbox(label="Use", value=True))
 
 
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
- on_submit,
 
 
 
 
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)