import os, json, uuid from datetime import datetime import gradio as gr # -------- storage helpers -------- ROOT = "outputs" os.makedirs(ROOT, exist_ok=True) def now_iso(): return datetime.utcnow().replace(microsecond=0).isoformat()+"Z" def new_id(): return uuid.uuid4().hex[:8] def project_dir(pid): path = os.path.join(ROOT, pid) os.makedirs(path, exist_ok=True) os.makedirs(os.path.join(path, "keyframes"), exist_ok=True) os.makedirs(os.path.join(path, "clips"), exist_ok=True) return path def save_project(proj): pid = proj["meta"]["id"] path = os.path.join(project_dir(pid), "project.json") with open(path, "w") as f: json.dump(proj, f, indent=2) return path def load_project_file(file_obj): with open(file_obj.name, "r") as f: proj = json.load(f) # ensure directories exist project_dir(proj["meta"]["id"]) return proj # -------- model-free stubs for Step 1 -------- def stub_storyboard_from_prompt(prompt, target_shots, default_fps, default_len): """Return a minimal storyboard (no LLM yet).""" shots = [] n = target_shots or 3 for i in range(1, n+1): shots.append({ "id": i, "title": f"Shot {i}", "description": f"Placeholder description for shot {i} derived from: {prompt[:80]}", "duration": default_len, "fps": default_fps, "video_length": default_len, "steps": 30, "seed": None, "negative": "", "keyframe_path": None }) return shots # -------- Gradio app -------- with gr.Blocks() as demo: gr.Markdown("# 🎬 Storyboard → Keyframes → Videos → Export (Scaffold)") gr.Markdown("This is the skeleton UI. In the next steps we’ll plug in an open LLM, img2img keyframes, and your Modal video generator.") # Global state project = gr.State(None) # dict with meta/shots/clips current_tab = gr.State("Storyboard") with gr.Row(): with gr.Column(scale=2): proj_name = gr.Textbox(label="Project name", placeholder="e.g., Desert Chase") with gr.Column(scale=1): new_btn = gr.Button("New Project", variant="primary") with gr.Column(scale=1): save_btn = gr.Button("Save Project") with gr.Column(scale=1): load_file = gr.File(label="Load Project (project.json)", file_count="single", type="filepath") load_btn = gr.Button("Load") # ----- Tabs ----- with gr.Tabs() as tabs: with gr.Tab("Storyboard"): gr.Markdown("### 1) Storyboard") sb_prompt = gr.Textbox(label="High-level prompt", lines=3, placeholder="Describe the story you want to create…") with gr.Row(): sb_target_shots = gr.Slider(1, 12, value=3, step=1, label="Target # of shots") sb_default_fps = gr.Slider(8, 60, value=24, step=1, label="Default FPS") sb_default_len = gr.Slider(1, 12, value=4, step=1, label="Default seconds per shot") propose_btn = gr.Button("Propose Storyboard (stub)") shots_json = gr.JSON(label="Storyboard (editable later)") confirm_btn = gr.Button("Confirm Storyboard ✓", variant="primary") sb_status = gr.Markdown("") with gr.Tab("Keyframes"): gr.Markdown("### 2) Keyframes") gr.Markdown("In Step 2–3 we’ll add img2img generation and approvals here.") kf_table = gr.JSON(label="Shots (read-only for now)") to_videos_btn = gr.Button("Continue to Videos →", interactive=False) with gr.Tab("Videos"): gr.Markdown("### 3) Videos (Transitions)") gr.Markdown("In Step 3–4 we’ll add your Modal start/end-frame video generation here.") vd_table = gr.JSON(label="Planned clip edges (read-only for now)") to_export_btn = gr.Button("Continue to Export →", interactive=False) with gr.Tab("Export"): gr.Markdown("### 4) Export") gr.Markdown("Final concatenation & asset packaging will land here in Step 4.") export_info = gr.Markdown("Nothing to export yet.") # ----- Handlers ----- def on_new(name): name = name.strip() or f"Project-{new_id()}" pid = new_id() p = { "meta": { "id": pid, "name": name, "created": now_iso(), "updated": now_iso() }, "shots": [], "clips": [] } save_project(p) return p, gr.update(value=f"**New project created** `{name}` (id: `{pid}`)") new_btn.click( on_new, inputs=[proj_name], outputs=[project, sb_status] ) def on_propose(p, prompt, target_shots, fps, vlen): if p is None: raise gr.Error("Create a project first (New Project).") shots = stub_storyboard_from_prompt(prompt or "", int(target_shots), int(fps), int(vlen)) p = dict(p) p["shots"] = shots p["meta"]["updated"] = now_iso() save_project(p) return p, shots, gr.update(value="Storyboard proposed (stub). Edit will be available in Step 2.") propose_btn.click( on_propose, inputs=[project, sb_prompt, sb_target_shots, sb_default_fps, sb_default_len], outputs=[project, shots_json, sb_status] ) def on_confirm(p): if p is None or not p.get("shots"): raise gr.Error("No storyboard yet.") # Build planned clip edges (i->i+1) edges = [] for i in range(len(p["shots"]) - 1): edges.append({"from": p["shots"][i]["id"], "to": p["shots"][i+1]["id"], "prompt": f"Transition from shot {i+1} to {i+2}"}) p = dict(p) p["clips"] = edges p["meta"]["updated"] = now_iso() save_project(p) return ( p, gr.update(value=p["shots"]), gr.update(value=p["clips"]), gr.update(value="Storyboard confirmed. Proceed to Keyframes."), gr.update(interactive=True) ) confirm_btn.click( on_confirm, inputs=[project], outputs=[project, kf_table, vd_table, sb_status, to_videos_btn] ) def on_save(p): if p is None: raise gr.Error("No project in memory.") path = save_project(p) return gr.update(value=f"Saved to `{path}`") save_btn.click( on_save, inputs=[project], outputs=[sb_status] ) def on_load(file_obj): p = load_project_file(file_obj) return ( p, gr.update(value=f"Loaded project `{p['meta']['name']}` (id: `{p['meta']['id']}`)"), gr.update(value=p["shots"]), gr.update(value=p["clips"]), gr.update(interactive=bool(p.get("shots"))) ) load_btn.click( on_load, inputs=[load_file], outputs=[project, sb_status, kf_table, vd_table, to_videos_btn] ) if __name__ == "__main__": demo.launch()