Simzy commited on
Commit
720aa07
·
verified ·
1 Parent(s): 49ca4f1

Add Extend prompt + custom start image controls (app.py)

Browse files
Files changed (1) hide show
  1. app.py +97 -12
app.py CHANGED
@@ -33,6 +33,9 @@ DEFAULT_NEGATIVE = (
33
  "静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
34
  )
35
 
 
 
 
36
  WORK = Path(tempfile.gettempdir()) / "wan22_extend"
37
  WORK.mkdir(parents=True, exist_ok=True)
38
 
@@ -261,6 +264,41 @@ def estimate_total(seg_duration: float, num_segments: int) -> float:
261
  return float(seg_duration) * int(num_segments)
262
 
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  def do_generate(
265
  image,
266
  prompt,
@@ -325,6 +363,9 @@ def do_generate(
325
 
326
  def do_extend(
327
  prompt,
 
 
 
328
  seg_duration,
329
  steps,
330
  negative,
@@ -336,22 +377,20 @@ def do_extend(
336
  state: dict,
337
  progress=gr.Progress(track_tqdm=False),
338
  ):
339
- if not state or not state.get("video") or not state.get("last_frame"):
340
  raise gr.Error("Generate a clip first, then use Extend.")
341
  if len(state.get("segments", [])) >= 6:
342
  raise gr.Error("Segment cap reached (6). Reset and start a new chain.")
343
 
344
  sess = Path(state["dir"])
345
- last = Path(state["last_frame"])
346
- if not last.exists():
347
- last = extract_last_frame(state["video"], sess / "last_frame")
348
- state["last_frame"] = str(last)
349
 
350
- use_prompt = prompt or state.get("prompt") or DEFAULT_PROMPT
351
- progress(0.05, desc="Extending from last frame…")
352
  try:
353
  clip = call_wan_i2v(
354
- last,
355
  use_prompt,
356
  float(seg_duration),
357
  int(steps),
@@ -373,6 +412,8 @@ def do_extend(
373
  except Exception as e:
374
  raise gr.Error(f"Concat failed (ffmpeg): {e}") from e
375
 
 
 
376
  last = extract_last_frame(out, sess / "last_frame")
377
  dur = video_duration_seconds(out)
378
  state["segments"] = [str(p) for p in segs]
@@ -392,6 +433,9 @@ def do_extend(
392
  def do_auto_extend(
393
  target_seconds,
394
  prompt,
 
 
 
395
  seg_duration,
396
  steps,
397
  negative,
@@ -414,6 +458,7 @@ def do_auto_extend(
414
  state.get("last_frame"),
415
  state,
416
  )
 
417
  while len(state.get("segments", [])) < max_segs:
418
  dur = video_duration_seconds(state["video"])
419
  if dur >= target:
@@ -422,8 +467,15 @@ def do_auto_extend(
422
  len(state["segments"]) / max_segs,
423
  desc=f"Auto-extend: {dur:.1f}s / {target:.0f}s…",
424
  )
 
 
 
 
425
  video, download, status, last_preview, state = do_extend(
426
  prompt,
 
 
 
427
  seg_duration,
428
  steps,
429
  negative,
@@ -435,6 +487,7 @@ def do_auto_extend(
435
  state,
436
  progress,
437
  )
 
438
  time.sleep(0.3)
439
  return video, download, status, last_preview, state
440
 
@@ -453,6 +506,11 @@ def update_estimate(seg_duration, num_planned):
453
  return f"Planned length ≈ **{total:.1f}s** ({num_planned} × {seg_duration:.1f}s segments). Cap: 6 segments."
454
 
455
 
 
 
 
 
 
456
  CSS = """
457
  .big-btn button { font-size: 1.15rem !important; min-height: 3rem !important; }
458
  #status-md { font-size: 1rem; }
@@ -465,7 +523,7 @@ def build_ui():
465
  gr.Markdown(
466
  """
467
  # Wan 2.2 Image → Video + Extend
468
- Upload a still, generate a short Wan 2.2 clip on **free ZeroGPU**, then **Extend** from the last frame to chain ~10–20s videos.
469
 
470
  ZeroGPU is free but **queued and quota-limited** — each segment often takes 1–3 minutes when busy.
471
  """
@@ -476,11 +534,34 @@ ZeroGPU is free but **queued and quota-limited** — each segment often takes 1
476
  with gr.Column(scale=1):
477
  image = gr.Image(label="Still image", type="pil", height=320)
478
  prompt = gr.Textbox(
479
- label="Prompt",
480
  value=DEFAULT_PROMPT,
481
  lines=3,
482
  placeholder="Describe the motion you want…",
483
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  with gr.Row():
485
  seg_duration = gr.Slider(
486
  2.0,
@@ -543,6 +624,7 @@ ZeroGPU is free but **queued and quota-limited** — each segment often takes 1
543
 
544
  seg_duration.change(update_estimate, [seg_duration, num_planned], estimate)
545
  num_planned.change(update_estimate, [seg_duration, num_planned], estimate)
 
546
 
547
  gen_inputs = [
548
  image,
@@ -559,6 +641,9 @@ ZeroGPU is free but **queued and quota-limited** — each segment often takes 1
559
  ]
560
  ext_inputs = [
561
  prompt,
 
 
 
562
  seg_duration,
563
  steps,
564
  negative,
@@ -583,8 +668,8 @@ ZeroGPU is free but **queued and quota-limited** — each segment often takes 1
583
  gr.Markdown(
584
  f"""
585
  ### How Extend works
586
- 1. **Generate** — still image + prompt → short Wan 2.2 I2V clip (~{3.5}s default).
587
- 2. **Extend** — take the **last frame**, run I2V again, **concatenate** onto the previous video.
588
  3. Repeat until you hit ~10–20s (or use **Auto-extend**). Soft cap: **6 segments**.
589
 
590
  ### Limits
 
33
  "静止不动的画面, 杂乱的背景, 三条腿, 背景人很多, 倒着走"
34
  )
35
 
36
+ EXTEND_START_LAST_FRAME = "Last frame from video"
37
+ EXTEND_START_UPLOAD = "Upload custom image"
38
+
39
  WORK = Path(tempfile.gettempdir()) / "wan22_extend"
40
  WORK.mkdir(parents=True, exist_ok=True)
41
 
 
264
  return float(seg_duration) * int(num_segments)
265
 
266
 
267
+ def resolve_extend_prompt(extend_prompt: str | None, generate_prompt: str | None, state: dict) -> str:
268
+ """Prefer dedicated Extend prompt; else Generate prompt / saved state / default."""
269
+ if extend_prompt and str(extend_prompt).strip():
270
+ return str(extend_prompt).strip()
271
+ if generate_prompt and str(generate_prompt).strip():
272
+ return str(generate_prompt).strip()
273
+ return state.get("prompt") or DEFAULT_PROMPT
274
+
275
+
276
+ def resolve_extend_start_image(
277
+ extend_start: str,
278
+ custom_image,
279
+ state: dict,
280
+ sess: Path,
281
+ ) -> Path:
282
+ """Pick I2V start image for an Extend segment."""
283
+ mode = (extend_start or EXTEND_START_LAST_FRAME).strip()
284
+ if mode == EXTEND_START_UPLOAD:
285
+ if custom_image is None:
286
+ raise gr.Error("Upload a custom extend start image, or switch to “Last frame from video”.")
287
+ saved = save_image(custom_image, sess / f"extend_custom_{uuid.uuid4().hex[:8]}")
288
+ if saved is None:
289
+ raise gr.Error("Could not read the custom extend start image.")
290
+ return saved
291
+
292
+ # Default: last frame from current video
293
+ last = Path(state["last_frame"]) if state.get("last_frame") else None
294
+ if last is None or not last.exists():
295
+ if not state.get("video"):
296
+ raise gr.Error("Generate a clip first, then use Extend.")
297
+ last = extract_last_frame(state["video"], sess / "last_frame")
298
+ state["last_frame"] = str(last)
299
+ return last
300
+
301
+
302
  def do_generate(
303
  image,
304
  prompt,
 
363
 
364
  def do_extend(
365
  prompt,
366
+ extend_prompt,
367
+ extend_start,
368
+ custom_extend_image,
369
  seg_duration,
370
  steps,
371
  negative,
 
377
  state: dict,
378
  progress=gr.Progress(track_tqdm=False),
379
  ):
380
+ if not state or not state.get("video"):
381
  raise gr.Error("Generate a clip first, then use Extend.")
382
  if len(state.get("segments", [])) >= 6:
383
  raise gr.Error("Segment cap reached (6). Reset and start a new chain.")
384
 
385
  sess = Path(state["dir"])
386
+ start_img = resolve_extend_start_image(extend_start, custom_extend_image, state, sess)
387
+ use_prompt = resolve_extend_prompt(extend_prompt, prompt, state)
 
 
388
 
389
+ mode_label = (extend_start or EXTEND_START_LAST_FRAME).strip()
390
+ progress(0.05, desc=f"Extending ({mode_label})…")
391
  try:
392
  clip = call_wan_i2v(
393
+ start_img,
394
  use_prompt,
395
  float(seg_duration),
396
  int(steps),
 
412
  except Exception as e:
413
  raise gr.Error(f"Concat failed (ffmpeg): {e}") from e
414
 
415
+ # Always refresh last_frame from the new concatenated video so later
416
+ # “Last frame from video” extends stay correct.
417
  last = extract_last_frame(out, sess / "last_frame")
418
  dur = video_duration_seconds(out)
419
  state["segments"] = [str(p) for p in segs]
 
433
  def do_auto_extend(
434
  target_seconds,
435
  prompt,
436
+ extend_prompt,
437
+ extend_start,
438
+ custom_extend_image,
439
  seg_duration,
440
  steps,
441
  negative,
 
458
  state.get("last_frame"),
459
  state,
460
  )
461
+ first = True
462
  while len(state.get("segments", [])) < max_segs:
463
  dur = video_duration_seconds(state["video"])
464
  if dur >= target:
 
467
  len(state["segments"]) / max_segs,
468
  desc=f"Auto-extend: {dur:.1f}s / {target:.0f}s…",
469
  )
470
+ # Custom image only applies to the first auto-extend segment;
471
+ # later segments chain from the updated video last frame.
472
+ this_start = extend_start if first else EXTEND_START_LAST_FRAME
473
+ this_custom = custom_extend_image if first else None
474
  video, download, status, last_preview, state = do_extend(
475
  prompt,
476
+ extend_prompt,
477
+ this_start,
478
+ this_custom,
479
  seg_duration,
480
  steps,
481
  negative,
 
487
  state,
488
  progress,
489
  )
490
+ first = False
491
  time.sleep(0.3)
492
  return video, download, status, last_preview, state
493
 
 
506
  return f"Planned length ≈ **{total:.1f}s** ({num_planned} × {seg_duration:.1f}s segments). Cap: 6 segments."
507
 
508
 
509
+ def toggle_custom_extend_image(mode: str):
510
+ visible = (mode or "").strip() == EXTEND_START_UPLOAD
511
+ return gr.update(visible=visible)
512
+
513
+
514
  CSS = """
515
  .big-btn button { font-size: 1.15rem !important; min-height: 3rem !important; }
516
  #status-md { font-size: 1rem; }
 
523
  gr.Markdown(
524
  """
525
  # Wan 2.2 Image → Video + Extend
526
+ Upload a still, generate a short Wan 2.2 clip on **free ZeroGPU**, then **Extend** from the last frame (or a custom start image) to chain ~10–20s videos.
527
 
528
  ZeroGPU is free but **queued and quota-limited** — each segment often takes 1–3 minutes when busy.
529
  """
 
534
  with gr.Column(scale=1):
535
  image = gr.Image(label="Still image", type="pil", height=320)
536
  prompt = gr.Textbox(
537
+ label="Generate prompt",
538
  value=DEFAULT_PROMPT,
539
  lines=3,
540
  placeholder="Describe the motion you want…",
541
  )
542
+
543
+ with gr.Accordion("Extend options", open=True):
544
+ extend_prompt = gr.Textbox(
545
+ label="Extend prompt",
546
+ value="",
547
+ lines=3,
548
+ placeholder="Leave blank to reuse Generate prompt",
549
+ info="Optional. Used only for Extend / Auto-extend.",
550
+ )
551
+ extend_start = gr.Radio(
552
+ choices=[EXTEND_START_LAST_FRAME, EXTEND_START_UPLOAD],
553
+ value=EXTEND_START_LAST_FRAME,
554
+ label="Extend start",
555
+ info="Default keeps current behavior (auto last frame).",
556
+ )
557
+ custom_extend_image = gr.Image(
558
+ label="Custom extend start image",
559
+ type="pil",
560
+ height=220,
561
+ visible=False,
562
+ info="Replaces the auto last frame as the I2V start for the next segment.",
563
+ )
564
+
565
  with gr.Row():
566
  seg_duration = gr.Slider(
567
  2.0,
 
624
 
625
  seg_duration.change(update_estimate, [seg_duration, num_planned], estimate)
626
  num_planned.change(update_estimate, [seg_duration, num_planned], estimate)
627
+ extend_start.change(toggle_custom_extend_image, [extend_start], [custom_extend_image])
628
 
629
  gen_inputs = [
630
  image,
 
641
  ]
642
  ext_inputs = [
643
  prompt,
644
+ extend_prompt,
645
+ extend_start,
646
+ custom_extend_image,
647
  seg_duration,
648
  steps,
649
  negative,
 
668
  gr.Markdown(
669
  f"""
670
  ### How Extend works
671
+ 1. **Generate** — still image + Generate prompt → short Wan 2.2 I2V clip (~{3.5}s default).
672
+ 2. **Extend** — start from the **last frame** (or a **custom uploaded image**), optional separate **Extend prompt**, run I2V again, **concatenate**.
673
  3. Repeat until you hit ~10–20s (or use **Auto-extend**). Soft cap: **6 segments**.
674
 
675
  ### Limits