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()