import os import shutil import uuid from datetime import datetime import numpy as np from PIL import Image, ImageDraw, ImageFont import gradio as gr from moviepy.editor import ( ImageClip, ColorClip, TextClip, CompositeVideoClip, concatenate_videoclips, VideoFileClip, ) # ---------- Simple "video generator" stub ---------- # Replace `generate_video_from_prompt` with your real model call. # It creates a short MP4 with a first frame (optionally from a previous video), # then overlays prompt text for a few seconds. OUT_DIR = "outputs" USED_DIR = os.path.join(OUT_DIR, "used") TMP_DIR = "tmp" os.makedirs(OUT_DIR, exist_ok=True) os.makedirs(USED_DIR, exist_ok=True) os.makedirs(TMP_DIR, exist_ok=True) FPS = 24 W, H = 768, 432 # 16:9 HD-ish to keep files light DURATION = 3.0 # seconds per generated clip for demo def _solid_bg(color=(18, 18, 18)): return ColorClip(size=(W, H), color=color, duration=DURATION) def _text_overlay(txt: str): # Use TextClip if ImageMagick is available; otherwise fallback to PIL. try: return TextClip( txt, fontsize=48, color="white", font="Arial-Bold", size=(W - 80, None), method="caption", ).set_position(("center", "center")).set_duration(DURATION) except Exception: # PIL fallback img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Try to load a font; fallback to default if not available on HF try: font = ImageFont.truetype("DejaVuSans-Bold.ttf", 48) except Exception: font = ImageFont.load_default() # simple multiline center lines = [] words = txt.split() line = "" for w in words: test = (line + " " + w).strip() if draw.textlength(test, font=font) > (W - 80): lines.append(line) line = w else: line = test lines.append(line) total_h = sum(font.getbbox(l)[3] for l in lines) + (len(lines)-1)*8 y = (H - total_h)//2 for l in lines: w_px = draw.textlength(l, font=font) x = (W - w_px)//2 draw.text((x, y), l, fill=(255,255,255,255), font=font) y += font.getbbox(l)[3] + 8 pil_path = os.path.join(TMP_DIR, f"txt_{uuid.uuid4().hex}.png") img.save(pil_path) return ImageClip(pil_path, duration=DURATION).set_position(("center","center")) def extract_last_frame_as_image(video_path: str) -> str: """Save last frame of video to an image file and return its path.""" with VideoFileClip(video_path) as v: frame = v.get_frame(v.duration - 1.0 / max(1, v.fps)) img = Image.fromarray(frame) frame_path = os.path.join(TMP_DIR, f"seed_{uuid.uuid4().hex}.png") img.save(frame_path) return frame_path def generate_video_from_prompt(prompt: str, seed_frame_path: str | None) -> str: """ Make a short demo MP4 using: - If seed_frame_path: start 0.5s with that still frame - Then a solid background + prompt text """ # Clips to concatenate clips = [] if seed_frame_path and os.path.exists(seed_frame_path): seed = ImageClip(seed_frame_path, duration=0.5).set_fps(FPS) clips.append(seed) bg = _solid_bg().set_fps(FPS) txt = _text_overlay(prompt) comp = CompositeVideoClip([bg, txt]).set_duration(DURATION).set_fps(FPS) clips.append(comp) final = concatenate_videoclips(clips, method="compose") out_name = f"gen_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.mp4" out_path = os.path.join(OUT_DIR, out_name) final.write_videofile(out_path, fps=FPS, codec="libx264", audio=False, verbose=False, logger=None) final.close() return out_path def concat_used_videos(video_paths: list[str]) -> str: clips = [VideoFileClip(p) for p in video_paths] final = concatenate_videoclips(clips, method="compose") out_path = os.path.join(OUT_DIR, f"continuous_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.mp4") final.write_videofile(out_path, fps=FPS, codec="libx264", audio=False, verbose=False, logger=None) for c in clips: c.close() return out_path def zip_used_videos(video_paths: list[str]) -> str: # Copy into a temp folder to zip cleanly stamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S') pack_dir = os.path.join(TMP_DIR, f"used_{stamp}_{uuid.uuid4().hex[:6]}") os.makedirs(pack_dir, exist_ok=True) for p in video_paths: shutil.copy(p, pack_dir) zip_base = os.path.join(OUT_DIR, f"used_{stamp}") shutil.make_archive(zip_base, "zip", pack_dir) shutil.rmtree(pack_dir, ignore_errors=True) return f"{zip_base}.zip" # ---------- Gradio App ---------- with gr.Blocks(css=".grow {flex: 1}") as demo: gr.Markdown("# Continuous Video Prompt → Use → Chain → Download") # Session state state_used_paths = gr.State([]) # list[str] state_seed_frame = gr.State(None) # str | None state_current_path = gr.State(None) # str | None with gr.Row(): prompt = gr.Textbox( label="Prompt", placeholder="Describe your next shot…", lines=2, autofocus=True, ) with gr.Row(equal_height=True): video_out = gr.Video(label="Video Output", interactive=False).style(height=360) with gr.Row(): btn_generate = gr.Button("Generate", variant="primary") btn_use = gr.Button("Use (chain this)", variant="secondary") btn_download = gr.Button("Download (A+B+C & ZIP)", variant="secondary") btn_reset = gr.Button("Reset Session", variant="stop") files_out = gr.Files(label="Downloads (concatenated MP4 + ZIP of used clips)", height=100) # ---- Handlers ---- def do_generate(prompt_text, seed_frame_path): if not prompt_text or not prompt_text.strip(): return None, None out_path = generate_video_from_prompt(prompt_text.strip(), seed_frame_path) return out_path, out_path # gr.Video path AND state_current_path btn_generate.click( do_generate, inputs=[prompt, state_seed_frame], outputs=[video_out, state_current_path], ) def do_use(current_path, used_paths): """ Save current_path to used list, extract its last frame as the next seed. """ if not current_path or not os.path.exists(current_path): # no-op if nothing to use return used_paths, gr.update(interactive=True), None # Append to used list new_used = list(used_paths) if current_path not in new_used: new_used.append(current_path) # Extract last frame for next generation seed next_seed = extract_last_frame_as_image(current_path) return new_used, gr.update(interactive=True), next_seed btn_use.click( do_use, inputs=[state_current_path, state_used_paths], outputs=[state_used_paths, prompt, state_seed_frame], ) def do_download(used_paths): """ Build concatenated video (A+B+C) and a ZIP of used clips. Returns list of two files for the Files component. """ if not used_paths: return [] concat_path = concat_used_videos(used_paths) zip_path = zip_used_videos(used_paths) return [concat_path, zip_path] btn_download.click( do_download, inputs=[state_used_paths], outputs=[files_out], ) def do_reset(): # Clear session state and temp try: for f in os.listdir(TMP_DIR): fp = os.path.join(TMP_DIR, f) if os.path.isfile(fp): os.remove(fp) except Exception: pass return None, [], None, None, gr.update(value=None), gr.update(value=[]) btn_reset.click( do_reset, inputs=None, outputs=[state_seed_frame, state_used_paths, state_current_path, prompt, video_out, files_out], ) if __name__ == "__main__": demo.launch()