Shalmoni commited on
Commit
ff07852
·
verified ·
1 Parent(s): aaab32d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -33
app.py CHANGED
@@ -1,5 +1,5 @@
1
- # app.py — FLUX-only with temporal chaining + Aggressive follow + Video stitching (backend + ffmpeg)
2
- import os, json, uuid, re, tempfile, subprocess, shlex
3
  from datetime import datetime
4
 
5
  import gradio as gr
@@ -403,7 +403,7 @@ def generate_keyframe_image(
403
  return saved_path
404
 
405
  # =========================
406
- # Video stitching helpers (backend per pair + ffmpeg concat)
407
  # =========================
408
  def _pair_clip_path(pid: str, i: int, j: int) -> str:
409
  return os.path.join(project_dir(pid), "clips", f"pair_{i:02d}_to_{j:02d}.mp4")
@@ -411,7 +411,7 @@ def _pair_clip_path(pid: str, i: int, j: int) -> str:
411
  def _final_stitched_path(pid: str) -> str:
412
  return os.path.join(project_dir(pid), "clips", "final_stitched.mp4")
413
 
414
- def _call_i2v_backend(img_a_path: str, img_b_path: str, prompt: str, seed: int | None, endpoint: str) -> bytes:
415
  """
416
  Calls Modal backend with two images to get a transition clip (mp4 bytes).
417
  """
@@ -419,7 +419,10 @@ def _call_i2v_backend(img_a_path: str, img_b_path: str, prompt: str, seed: int |
419
  if prompt:
420
  params["prompt"] = prompt
421
  if seed is not None:
422
- params["seed"] = str(int(seed))
 
 
 
423
 
424
  with open(img_a_path, "rb") as fa, open(img_b_path, "rb") as fb:
425
  files = {
@@ -431,7 +434,7 @@ def _call_i2v_backend(img_a_path: str, img_b_path: str, prompt: str, seed: int |
431
  raise gr.Error(f"I2V backend error {r.status_code}: {r.text[:400]}")
432
  return r.content
433
 
434
- def _build_all_pair_videos_backend(pid: str, shots: list, endpoint: str, prompt: str, seed: int | None) -> list[str]:
435
  out_paths = []
436
  for k in range(len(shots) - 1):
437
  a = shots[k].get("image_path")
@@ -445,35 +448,138 @@ def _build_all_pair_videos_backend(pid: str, shots: list, endpoint: str, prompt:
445
  out_paths.append(outp)
446
  return out_paths
447
 
448
- def _ffmpeg_concat_videos(mp4_paths: list[str], out_path: str) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  if not mp4_paths:
450
- raise gr.Error("No clips to concatenate.")
451
 
452
- # Create a concat list file
453
- list_txt = tempfile.NamedTemporaryFile("w", delete=False, suffix=".txt")
454
- try:
 
 
 
 
 
 
 
455
  for p in mp4_paths:
456
- if not os.path.exists(p):
457
- raise gr.Error(f"Missing clip: {p}")
458
- list_txt.write(f"file '{p}'\n")
459
- list_txt.flush(); list_txt.close()
460
-
461
- ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
462
-
463
- # Try stream copy (fast)
464
- cmd_copy = f"{shlex.quote(ffmpeg)} -y -f concat -safe 0 -i {shlex.quote(list_txt.name)} -c copy {shlex.quote(out_path)}"
465
- rc = subprocess.call(cmd_copy, shell=True)
466
- if rc == 0 and os.path.exists(out_path) and os.path.getsize(out_path) > 0:
467
- return
468
-
469
- # Fallback re-encode
470
- cmd_reenc = f"{shlex.quote(ffmpeg)} -y -f concat -safe 0 -i {shlex.quote(list_txt.name)} -c:v libx264 -pix_fmt yuv420p -preset medium -crf 18 -an {shlex.quote(out_path)}"
471
- rc2 = subprocess.call(cmd_reenc, shell=True)
472
- if rc2 != 0 or not os.path.exists(out_path) or os.path.getsize(out_path) == 0:
473
- raise gr.Error("ffmpeg concat failed (copy and re-encode).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  finally:
475
- try: os.unlink(list_txt.name)
476
- except: pass
 
 
477
 
478
  # =========================
479
  # Shots <-> DataFrame utils
@@ -723,7 +829,7 @@ with gr.Blocks() as demo:
723
 
724
  approve_next_btn.click(on_approve_next, inputs=[project, current_idx, prompt_box, out_img], outputs=[project, current_idx, shot_info_md, prompt_box, prev_img, out_img, kf_status])
725
 
726
- # ---- Videos tab handlers (backend + ffmpeg)
727
  def on_build_pairs(p, fps, hold, xfade):
728
  if p is None:
729
  raise gr.Error("No project.")
@@ -764,7 +870,7 @@ with gr.Blocks() as demo:
764
  if not pair_paths:
765
  raise gr.Error("No pair clips found. Click 'Build pair clips' first.")
766
  outp = _final_stitched_path(pid)
767
- _ffmpeg_concat_videos(pair_paths, outp)
768
  return {"pair_clips": pair_paths, "final": outp}
769
 
770
  build_final_btn.click(
 
1
+ # app.py — FLUX-only with temporal chaining + Aggressive follow + Video stitching (backend + robust ffmpeg concat)
2
+ import os, json, uuid, re, tempfile, subprocess, shlex, shutil, json as _json
3
  from datetime import datetime
4
 
5
  import gradio as gr
 
403
  return saved_path
404
 
405
  # =========================
406
+ # Video stitching helpers (backend per pair + robust ffmpeg concat)
407
  # =========================
408
  def _pair_clip_path(pid: str, i: int, j: int) -> str:
409
  return os.path.join(project_dir(pid), "clips", f"pair_{i:02d}_to_{j:02d}.mp4")
 
411
  def _final_stitched_path(pid: str) -> str:
412
  return os.path.join(project_dir(pid), "clips", "final_stitched.mp4")
413
 
414
+ def _call_i2v_backend(img_a_path: str, img_b_path: str, prompt: str, seed, endpoint: str) -> bytes:
415
  """
416
  Calls Modal backend with two images to get a transition clip (mp4 bytes).
417
  """
 
419
  if prompt:
420
  params["prompt"] = prompt
421
  if seed is not None:
422
+ try:
423
+ params["seed"] = str(int(seed))
424
+ except Exception:
425
+ pass
426
 
427
  with open(img_a_path, "rb") as fa, open(img_b_path, "rb") as fb:
428
  files = {
 
434
  raise gr.Error(f"I2V backend error {r.status_code}: {r.text[:400]}")
435
  return r.content
436
 
437
+ def _build_all_pair_videos_backend(pid: str, shots: list, endpoint: str, prompt: str, seed) -> list[str]:
438
  out_paths = []
439
  for k in range(len(shots) - 1):
440
  a = shots[k].get("image_path")
 
448
  out_paths.append(outp)
449
  return out_paths
450
 
451
+ # ---------- robust concat (normalize + concat demuxer; fallback re-encode once)
452
+ def _ffprobe_stream(path):
453
+ ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
454
+ ffprobe = ffmpeg.replace("ffmpeg", "ffprobe")
455
+ cmd = [
456
+ ffprobe,
457
+ "-v", "error",
458
+ "-select_streams", "v:0",
459
+ "-show_entries", "stream=width,height,avg_frame_rate,pix_fmt,codec_name",
460
+ "-of", "json",
461
+ path,
462
+ ]
463
+ try:
464
+ out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
465
+ data = _json.loads(out.decode("utf-8"))
466
+ return (data.get("streams") or [{}])[0]
467
+ except Exception:
468
+ return {}
469
+
470
+ def _ffmpeg_safe_concat(mp4_paths: list[str], out_path: str, fps: int = 24, size=None):
471
  if not mp4_paths:
472
+ raise gr.Error("No clips to stitch.")
473
 
474
+ clips_dir = os.path.dirname(out_path)
475
+ os.makedirs(clips_dir, exist_ok=True)
476
+ log_path = os.path.join(clips_dir, "ffmpeg_concat.log")
477
+ ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
478
+
479
+ # Determine reference WxH
480
+ ref_w, ref_h = None, None
481
+ if size and isinstance(size, (tuple, list)) and len(size) == 2:
482
+ ref_w, ref_h = int(size[0]), int(size[1])
483
+ else:
484
  for p in mp4_paths:
485
+ st = _ffprobe_stream(p)
486
+ if st.get("width") and st.get("height"):
487
+ ref_w, ref_h = int(st["width"]), int(st["height"])
488
+ break
489
+ if ref_w and ref_h:
490
+ if ref_w % 2: ref_w += 1
491
+ if ref_h % 2: ref_h += 1
492
+
493
+ norm_paths = []
494
+ tmpdir = tempfile.mkdtemp(prefix="norm_")
495
+ try:
496
+ # Normalize each clip to consistent fps/size/codec/pixfmt
497
+ for i, inp in enumerate(mp4_paths):
498
+ if not os.path.exists(inp):
499
+ continue
500
+ norm = os.path.join(tmpdir, f"norm_{i:03d}.mp4")
501
+
502
+ vf = []
503
+ if ref_w and ref_h:
504
+ vf.append(f"scale=w={ref_w}:h={ref_h}:force_original_aspect_ratio=decrease")
505
+ vf.append(f"pad={ref_w}:{ref_h}:(ow-iw)/2:(oh-ih)/2:color=black")
506
+ vf.append(f"fps={int(fps)}")
507
+ vf_arg = ",".join(vf)
508
+
509
+ cmd = [
510
+ ffmpeg, "-y", "-i", inp,
511
+ "-vf", vf_arg,
512
+ "-an",
513
+ "-c:v", "libx264",
514
+ "-pix_fmt", "yuv420p",
515
+ "-profile:v", "main",
516
+ "-preset", "veryfast",
517
+ "-crf", "18",
518
+ "-movflags", "+faststart",
519
+ norm
520
+ ]
521
+ with open(log_path, "ab") as lg:
522
+ lg.write(("NORMALIZE: " + " ".join(cmd) + "\n").encode())
523
+ subprocess.check_call(cmd, stdout=lg, stderr=lg)
524
+ norm_paths.append(norm)
525
+
526
+ if not norm_paths:
527
+ raise gr.Error("No inputs could be normalized for concat.")
528
+
529
+ # concat demuxer (stream copy)
530
+ listfile = os.path.join(tmpdir, "list.txt")
531
+ with open(listfile, "w") as f:
532
+ for p in norm_paths:
533
+ f.write(f"file '{p}'\n")
534
+
535
+ cmd_concat_copy = [
536
+ ffmpeg, "-y",
537
+ "-f", "concat", "-safe", "0", "-i", listfile,
538
+ "-c", "copy",
539
+ out_path
540
+ ]
541
+ try:
542
+ with open(log_path, "ab") as lg:
543
+ lg.write(("CONCAT_COPY: " + " ".join(cmd_concat_copy) + "\n").encode())
544
+ subprocess.check_call(cmd_concat_copy, stdout=lg, stderr=lg)
545
+ return out_path
546
+ except subprocess.CalledProcessError:
547
+ pass # fallback
548
+
549
+ # filter_complex concat (re-encode once)
550
+ cmd = [ffmpeg, "-y"]
551
+ for p in norm_paths:
552
+ cmd += ["-i", p]
553
+ n = len(norm_paths)
554
+ inputs = "".join([f"[{i}:v]" for i in range(n)])
555
+ filtergraph = f"{inputs}concat=n={n}:v=1:a=0[outv]"
556
+ cmd += [
557
+ "-filter_complex", filtergraph,
558
+ "-map", "[outv]",
559
+ "-an",
560
+ "-c:v", "libx264",
561
+ "-pix_fmt", "yuv420p",
562
+ "-profile:v", "main",
563
+ "-preset", "veryfast",
564
+ "-crf", "18",
565
+ "-r", str(int(fps)),
566
+ "-movflags", "+faststart",
567
+ out_path
568
+ ]
569
+ with open(log_path, "ab") as lg:
570
+ lg.write(("CONCAT_REENC: " + " ".join(cmd) + "\n").encode())
571
+ subprocess.check_call(cmd, stdout=lg, stderr=lg)
572
+ return out_path
573
+
574
+ except subprocess.CalledProcessError:
575
+ raise gr.Error("ffmpeg concat failed (copy and re-encode). See logs at: " + log_path)
576
+ except Exception as e:
577
+ raise gr.Error(f"Concat error: {e}")
578
  finally:
579
+ try:
580
+ shutil.rmtree(tmpdir)
581
+ except Exception:
582
+ pass
583
 
584
  # =========================
585
  # Shots <-> DataFrame utils
 
829
 
830
  approve_next_btn.click(on_approve_next, inputs=[project, current_idx, prompt_box, out_img], outputs=[project, current_idx, shot_info_md, prompt_box, prev_img, out_img, kf_status])
831
 
832
+ # ---- Videos tab handlers (backend + robust ffmpeg)
833
  def on_build_pairs(p, fps, hold, xfade):
834
  if p is None:
835
  raise gr.Error("No project.")
 
870
  if not pair_paths:
871
  raise gr.Error("No pair clips found. Click 'Build pair clips' first.")
872
  outp = _final_stitched_path(pid)
873
+ _ffmpeg_safe_concat(pair_paths, outp, fps=int(fps), size=None) # set size=(640,640) to force letterbox
874
  return {"pair_clips": pair_paths, "final": outp}
875
 
876
  build_final_btn.click(