dima0987654321 commited on
Commit
12e6b02
·
verified ·
1 Parent(s): 8f0dfaf

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -23
app.py CHANGED
@@ -708,21 +708,11 @@ def _cover_crop(image_path: str, canvas_label: str) -> str:
708
  return image_path
709
 
710
  def _fit_keyframe(image_path, current_canvas):
711
- """Snap the canvas to a keyframe's aspect ratio, then cover-crop the keyframe to that canvas."""
712
- from PIL import Image as _Image
713
-
714
- with _Image.open(image_path) as img:
715
- aspect = img.width / img.height
716
- label = _snap_canvas(aspect, current_canvas)
717
- return _cover_crop(image_path, label), label
718
 
719
  def _fit_keyframe_ui(image_path, current_canvas):
720
- """``image.upload`` handler — the reference Space's behaviour, ported.
721
-
722
- Doing this on upload (rather than only server-side inside ``generate``) is the whole point: the
723
- user immediately sees the keyframe cropped to the ratio the video will be rendered at, and the
724
- Canvas dropdown moves to the snapped resolution instead of sitting on the 16:9 default.
725
- """
726
  path = _as_path(image_path)
727
  if not path:
728
  return gr.update(), gr.update()
@@ -838,16 +828,13 @@ def generate(
838
 
839
  from diffusers.utils import encode_video
840
 
841
- # The UI already snapped + cropped on upload, so this is a no-op there; it still runs for API
842
- # callers (and for a canvas the user changed afterwards) so the output geometry always follows the
843
- # I2V image. The *first* frame owns the canvas — a last frame is only cover-cropped to match it.
844
  first, last = _as_path(image_path), _as_path(last_image_path)
845
  if first:
846
- first, canvas = _fit_keyframe(first, canvas)
847
- if last:
848
- last = _cover_crop(last, canvas)
849
- elif last:
850
- last, canvas = _fit_keyframe(last, canvas)
851
 
852
  num_frames = snap_frames(duration)
853
 
@@ -869,7 +856,9 @@ def generate(
869
  rewrite_prompt=bool(enhance_prompt),
870
  )
871
  condition_seconds = time.time() - conditioned
872
- height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
 
 
873
  # `None` whenever no rewrite happened — either none was asked for, or the conditioner declined one.
874
  enhanced = (plan or {}).get("refined_prompt") or ""
875
 
@@ -893,10 +882,55 @@ def generate(
893
  path = os.path.join(OUTPUT_DIR, f"h3-flashgen-{int(time.time() * 1000)}.mp4")
894
  encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896
  # Reported, not assumed: `steps_taken` is the number of Euler updates the scheduler actually took.
897
  schedule = " → ".join(f"{s:g}" for s in sigmas_used)
898
  report = (
899
- f"{width}x{height} · {num_frames} frames ({num_frames / FPS:.3f}s) · "
 
900
  f"{steps_taken} steps (FlashGen σ {schedule}) · "
901
  f"conditioner {condition_seconds:.0f}s{' (prompt enhanced)' if enhanced else ''} · "
902
  f"denoise + decode {generate_seconds:.0f}s · seed {int(seed)}"
 
708
  return image_path
709
 
710
  def _fit_keyframe(image_path, current_canvas):
711
+ """Crop a keyframe to the selected canvas ratio without changing the selected resolution."""
712
+ return _cover_crop(image_path, current_canvas), current_canvas
 
 
 
 
 
713
 
714
  def _fit_keyframe_ui(image_path, current_canvas):
715
+ """Crop the uploaded keyframe to the selected canvas ratio; preserve the Canvas dropdown."""
 
 
 
 
 
716
  path = _as_path(image_path)
717
  if not path:
718
  return gr.update(), gr.update()
 
828
 
829
  from diffusers.utils import encode_video
830
 
831
+ # The user's Canvas selection is authoritative. Crop keyframes to its aspect ratio,
832
+ # but never replace the selected resolution with a preset chosen from the source image.
 
833
  first, last = _as_path(image_path), _as_path(last_image_path)
834
  if first:
835
+ first = _cover_crop(first, canvas)
836
+ if last:
837
+ last = _cover_crop(last, canvas)
 
 
838
 
839
  num_frames = snap_frames(duration)
840
 
 
856
  rewrite_prompt=bool(enhance_prompt),
857
  )
858
  condition_seconds = time.time() - conditioned
859
+ # Keep the requested Canvas dimensions even if the remote conditioner reports a fallback size.
860
+ height, width = CANVASES[canvas]
861
+ num_frames = int(metadata["num_frames"])
862
  # `None` whenever no rewrite happened — either none was asked for, or the conditioner declined one.
863
  enhanced = (plan or {}).get("refined_prompt") or ""
864
 
 
882
  path = os.path.join(OUTPUT_DIR, f"h3-flashgen-{int(time.time() * 1000)}.mp4")
883
  encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
884
 
885
+ # Verify the encoded MP4 dimensions. If the pipeline/encoder emitted another size, normalize
886
+ # the final video to the selected Canvas while preserving its audio track.
887
+ import json
888
+ import shutil
889
+ import subprocess
890
+
891
+ ffprobe = shutil.which("ffprobe")
892
+ ffmpeg = shutil.which("ffmpeg")
893
+ actual_width, actual_height = width, height
894
+ if ffprobe:
895
+ probe = subprocess.run(
896
+ [ffprobe, "-v", "error", "-select_streams", "v:0",
897
+ "-show_entries", "stream=width,height", "-of", "json", path],
898
+ capture_output=True, text=True, check=True,
899
+ )
900
+ streams = json.loads(probe.stdout).get("streams", [])
901
+ if streams:
902
+ actual_width = int(streams[0]["width"])
903
+ actual_height = int(streams[0]["height"])
904
+
905
+ resized = False
906
+ if (actual_width, actual_height) != (width, height):
907
+ if not ffmpeg:
908
+ raise gr.Error(
909
+ f"Выбрано {width}x{height}, но модель создала {actual_width}x{actual_height}; "
910
+ "FFmpeg недоступен для исправления размера."
911
+ )
912
+ corrected = path[:-4] + "-canvas.mp4"
913
+ subprocess.run(
914
+ [ffmpeg, "-y", "-i", path,
915
+ "-vf", f"scale={width}:{height}:force_original_aspect_ratio=increase:flags=lanczos,"
916
+ f"crop={width}:{height}",
917
+ "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
918
+ "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", corrected],
919
+ capture_output=True, text=True, check=True,
920
+ )
921
+ os.replace(corrected, path)
922
+ resized = True
923
+ print(
924
+ f"[canvas] Model/encoder output {actual_width}x{actual_height}; "
925
+ f"final MP4 normalized to selected Canvas {width}x{height}.",
926
+ flush=True,
927
+ )
928
+
929
  # Reported, not assumed: `steps_taken` is the number of Euler updates the scheduler actually took.
930
  schedule = " → ".join(f"{s:g}" for s in sigmas_used)
931
  report = (
932
+ f"{width}x{height}{' (final size corrected with FFmpeg)' if resized else ''} · "
933
+ f"{num_frames} frames ({num_frames / FPS:.3f}s) · "
934
  f"{steps_taken} steps (FlashGen σ {schedule}) · "
935
  f"conditioner {condition_seconds:.0f}s{' (prompt enhanced)' if enhanced else ''} · "
936
  f"denoise + decode {generate_seconds:.0f}s · seed {int(seed)}"