Spaces:
Running
on
Zero
Running
on
Zero
| 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() | |