Spaces:
Sleeping
Sleeping
File size: 8,200 Bytes
0c80388 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
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()
|