Shalmoni commited on
Commit
6b00576
·
verified ·
1 Parent(s): c17772e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -63
app.py CHANGED
@@ -1,15 +1,18 @@
1
  import os, json, uuid
2
  from datetime import datetime
3
  import gradio as gr
 
4
 
5
- # -------- storage helpers --------
 
 
6
  ROOT = "outputs"
7
  os.makedirs(ROOT, exist_ok=True)
8
 
9
- def now_iso(): return datetime.utcnow().replace(microsecond=0).isoformat()+"Z"
10
  def new_id(): return uuid.uuid4().hex[:8]
11
 
12
- def project_dir(pid):
13
  path = os.path.join(ROOT, pid)
14
  os.makedirs(path, exist_ok=True)
15
  os.makedirs(os.path.join(path, "keyframes"), exist_ok=True)
@@ -25,39 +28,111 @@ def save_project(proj):
25
  def load_project_file(file_obj):
26
  with open(file_obj.name, "r") as f:
27
  proj = json.load(f)
28
- # ensure directories exist
29
- project_dir(proj["meta"]["id"])
30
  return proj
31
 
32
- # -------- model-free stubs for Step 1 --------
33
- def stub_storyboard_from_prompt(prompt, target_shots, default_fps, default_len):
34
- """Return a minimal storyboard (no LLM yet)."""
35
- shots = []
36
- n = target_shots or 3
37
- for i in range(1, n+1):
38
- shots.append({
39
- "id": i,
40
- "title": f"Shot {i}",
41
- "description": f"Placeholder description for shot {i} derived from: {prompt[:80]}",
42
- "duration": default_len,
43
- "fps": default_fps,
44
- "video_length": default_len,
45
- "steps": 30,
46
- "seed": None,
47
- "negative": "",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  "keyframe_path": None
49
  })
50
- return shots
51
 
52
- # -------- Gradio app --------
 
 
53
  with gr.Blocks() as demo:
54
- gr.Markdown("# 🎬 Storyboard → Keyframes → Videos → Export (Scaffold)")
55
- 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.")
56
 
57
  # Global state
58
- project = gr.State(None) # dict with meta/shots/clips
59
  current_tab = gr.State("Storyboard")
60
 
 
61
  with gr.Row():
62
  with gr.Column(scale=2):
63
  proj_name = gr.Textbox(label="Project name", placeholder="e.g., Desert Chase")
@@ -69,69 +144,59 @@ with gr.Blocks() as demo:
69
  load_file = gr.File(label="Load Project (project.json)", file_count="single", type="filepath")
70
  load_btn = gr.Button("Load")
71
 
72
- # ----- Tabs -----
73
- with gr.Tabs() as tabs:
74
  with gr.Tab("Storyboard"):
75
  gr.Markdown("### 1) Storyboard")
76
- sb_prompt = gr.Textbox(label="High-level prompt", lines=3, placeholder="Describe the story you want to create…")
77
  with gr.Row():
78
  sb_target_shots = gr.Slider(1, 12, value=3, step=1, label="Target # of shots")
79
- sb_default_fps = gr.Slider(8, 60, value=24, step=1, label="Default FPS")
80
- sb_default_len = gr.Slider(1, 12, value=4, step=1, label="Default seconds per shot")
81
- propose_btn = gr.Button("Propose Storyboard (stub)")
82
- shots_json = gr.JSON(label="Storyboard (editable later)")
83
- confirm_btn = gr.Button("Confirm Storyboard ✓", variant="primary")
84
- sb_status = gr.Markdown("")
85
 
86
  with gr.Tab("Keyframes"):
87
- gr.Markdown("### 2) Keyframes")
88
- gr.Markdown("In Step 2–3 we’ll add img2img generation and approvals here.")
89
  kf_table = gr.JSON(label="Shots (read-only for now)")
90
  to_videos_btn = gr.Button("Continue to Videos →", interactive=False)
91
 
92
  with gr.Tab("Videos"):
93
- gr.Markdown("### 3) Videos (Transitions)")
94
- gr.Markdown("In Step 3–4 we’ll add your Modal start/end-frame video generation here.")
95
  vd_table = gr.JSON(label="Planned clip edges (read-only for now)")
96
  to_export_btn = gr.Button("Continue to Export →", interactive=False)
97
 
98
  with gr.Tab("Export"):
99
- gr.Markdown("### 4) Export")
100
- gr.Markdown("Final concatenation & asset packaging will land here in Step 4.")
101
  export_info = gr.Markdown("Nothing to export yet.")
102
 
103
- # ----- Handlers -----
104
  def on_new(name):
105
- name = name.strip() or f"Project-{new_id()}"
106
  pid = new_id()
107
  p = {
108
- "meta": {
109
- "id": pid,
110
- "name": name,
111
- "created": now_iso(),
112
- "updated": now_iso()
113
- },
114
  "shots": [],
115
  "clips": []
116
  }
117
  save_project(p)
118
  return p, gr.update(value=f"**New project created** `{name}` (id: `{pid}`)")
119
 
120
- new_btn.click(
121
- on_new,
122
- inputs=[proj_name],
123
- outputs=[project, sb_status]
124
- )
125
 
126
  def on_propose(p, prompt, target_shots, fps, vlen):
127
  if p is None:
128
  raise gr.Error("Create a project first (New Project).")
129
- shots = stub_storyboard_from_prompt(prompt or "", int(target_shots), int(fps), int(vlen))
 
 
130
  p = dict(p)
131
  p["shots"] = shots
132
  p["meta"]["updated"] = now_iso()
133
  save_project(p)
134
- return p, shots, gr.update(value="Storyboard proposed (stub). Edit will be available in Step 2.")
135
 
136
  propose_btn.click(
137
  on_propose,
@@ -142,10 +207,11 @@ with gr.Blocks() as demo:
142
  def on_confirm(p):
143
  if p is None or not p.get("shots"):
144
  raise gr.Error("No storyboard yet.")
145
- # Build planned clip edges (i->i+1)
146
  edges = []
147
  for i in range(len(p["shots"]) - 1):
148
- edges.append({"from": p["shots"][i]["id"], "to": p["shots"][i+1]["id"], "prompt": f"Transition from shot {i+1} to {i+2}"})
 
 
149
  p = dict(p)
150
  p["clips"] = edges
151
  p["meta"]["updated"] = now_iso()
@@ -170,11 +236,7 @@ with gr.Blocks() as demo:
170
  path = save_project(p)
171
  return gr.update(value=f"Saved to `{path}`")
172
 
173
- save_btn.click(
174
- on_save,
175
- inputs=[project],
176
- outputs=[sb_status]
177
- )
178
 
179
  def on_load(file_obj):
180
  p = load_project_file(file_obj)
@@ -193,4 +255,5 @@ with gr.Blocks() as demo:
193
  )
194
 
195
  if __name__ == "__main__":
 
196
  demo.launch()
 
1
  import os, json, uuid
2
  from datetime import datetime
3
  import gradio as gr
4
+ import spaces # <<< required for ZeroGPU
5
 
6
+ # =========================
7
+ # Storage helpers
8
+ # =========================
9
  ROOT = "outputs"
10
  os.makedirs(ROOT, exist_ok=True)
11
 
12
+ def now_iso(): return datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
13
  def new_id(): return uuid.uuid4().hex[:8]
14
 
15
+ def project_dir(pid):
16
  path = os.path.join(ROOT, pid)
17
  os.makedirs(path, exist_ok=True)
18
  os.makedirs(os.path.join(path, "keyframes"), exist_ok=True)
 
28
  def load_project_file(file_obj):
29
  with open(file_obj.name, "r") as f:
30
  proj = json.load(f)
31
+ project_dir(proj["meta"]["id"]) # ensure dirs
 
32
  return proj
33
 
34
+ # =========================
35
+ # LLM (ZeroGPU) Storyboard generator
36
+ # =========================
37
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
38
+
39
+ STORYBOARD_MODEL = os.getenv("STORYBOARD_MODEL", "Qwen/Qwen2.5-1.5B-Instruct")
40
+ HF_TASK_MAX_TOKENS = int(os.getenv("HF_TASK_MAX_TOKENS", "900")) # keep tidy for JSON
41
+ _pipe = None # lazy-loaded global
42
+
43
+ def _lazy_pipe():
44
+ global _pipe
45
+ if _pipe is not None:
46
+ return _pipe
47
+ tok = AutoTokenizer.from_pretrained(STORYBOARD_MODEL, trust_remote_code=True)
48
+ mdl = AutoModelForCausalLM.from_pretrained(
49
+ STORYBOARD_MODEL,
50
+ device_map="auto",
51
+ torch_dtype="auto",
52
+ trust_remote_code=True,
53
+ )
54
+ _pipe = pipeline(
55
+ "text-generation",
56
+ model=mdl,
57
+ tokenizer=tok,
58
+ max_new_tokens=HF_TASK_MAX_TOKENS,
59
+ do_sample=False, # deterministic JSON
60
+ temperature=0.0,
61
+ repetition_penalty=1.05,
62
+ )
63
+ return _pipe
64
+
65
+ def _storyboard_prompt(user_prompt: str, n_shots: int, default_fps: int, default_len: int) -> str:
66
+ return f"""
67
+ You are a film previsualization assistant. Return ONLY valid JSON (no explanations).
68
+ Create a storyboard of {n_shots} numbered shots for the following idea:
69
+
70
+ \"\"\"{user_prompt}\"\"\"
71
+
72
+ Return an array of objects with this exact schema and default values:
73
+ [
74
+ {{
75
+ "id": 1,
76
+ "title": "Short title",
77
+ "description": "A visual description suitable for keyframe generation",
78
+ "duration": {default_len},
79
+ "fps": {default_fps},
80
+ "video_length": {default_len},
81
+ "steps": 30,
82
+ "seed": null,
83
+ "negative": ""
84
+ }}
85
+ ]
86
+
87
+ Rules:
88
+ - IDs must start at 1 and increment by 1.
89
+ - Use simple ASCII only. No trailing commas.
90
+ - Output must be valid JSON parseable by Python's json.loads.
91
+ """.strip()
92
+
93
+ @spaces.GPU(duration=180) # <<< ZeroGPU entrypoint: triggers pooled GPU allocation
94
+ def generate_storyboard_with_llm(user_prompt: str, n_shots: int, default_fps: int, default_len: int):
95
+ pipe = _lazy_pipe()
96
+ prompt = _storyboard_prompt(user_prompt, n_shots, default_fps, default_len)
97
+ out = pipe(prompt)[0]["generated_text"]
98
+
99
+ # Extract the JSON array
100
+ start = out.find("[")
101
+ end = out.rfind("]")
102
+ if start == -1 or end == -1 or end <= start:
103
+ raise ValueError("LLM did not return valid JSON.")
104
+ text = out[start:end+1]
105
+ shots = json.loads(text)
106
+
107
+ # Normalize & enforce required fields
108
+ norm = []
109
+ for i, s in enumerate(shots, start=1):
110
+ norm.append({
111
+ "id": int(s.get("id", i)),
112
+ "title": s.get("title", f"Shot {i}"),
113
+ "description": s.get("description", ""),
114
+ "duration": int(s.get("duration", default_len)),
115
+ "fps": int(s.get("fps", default_fps)),
116
+ "video_length": int(s.get("video_length", default_len)),
117
+ "steps": int(s.get("steps", 30)),
118
+ "seed": s.get("seed", None),
119
+ "negative": s.get("negative", ""),
120
  "keyframe_path": None
121
  })
122
+ return norm
123
 
124
+ # =========================
125
+ # Gradio UI
126
+ # =========================
127
  with gr.Blocks() as demo:
128
+ gr.Markdown("# ��� Storyboard → Keyframes → Videos → Export")
129
+ gr.Markdown("**Step 2**: Real storyboard generation on **ZeroGPU**. Next steps will add keyframes (img2img) and your Modal videos.")
130
 
131
  # Global state
132
+ project = gr.State(None) # dict with meta/shots/clips
133
  current_tab = gr.State("Storyboard")
134
 
135
+ # Header row
136
  with gr.Row():
137
  with gr.Column(scale=2):
138
  proj_name = gr.Textbox(label="Project name", placeholder="e.g., Desert Chase")
 
144
  load_file = gr.File(label="Load Project (project.json)", file_count="single", type="filepath")
145
  load_btn = gr.Button("Load")
146
 
147
+ # Tabs
148
+ with gr.Tabs():
149
  with gr.Tab("Storyboard"):
150
  gr.Markdown("### 1) Storyboard")
151
+ sb_prompt = gr.Textbox(label="High-level prompt", lines=4, placeholder="Describe the story you want to create…")
152
  with gr.Row():
153
  sb_target_shots = gr.Slider(1, 12, value=3, step=1, label="Target # of shots")
154
+ sb_default_fps = gr.Slider(8, 60, value=24, step=1, label="Default FPS")
155
+ sb_default_len = gr.Slider(1, 12, value=4, step=1, label="Default seconds per shot")
156
+ propose_btn = gr.Button("Propose Storyboard (LLM on ZeroGPU)")
157
+ shots_json = gr.JSON(label="Storyboard JSON (editable in next step)")
158
+ confirm_btn = gr.Button("Confirm Storyboard ✓", variant="primary")
159
+ sb_status = gr.Markdown("")
160
 
161
  with gr.Tab("Keyframes"):
162
+ gr.Markdown("### 2) Keyframes (coming next)")
 
163
  kf_table = gr.JSON(label="Shots (read-only for now)")
164
  to_videos_btn = gr.Button("Continue to Videos →", interactive=False)
165
 
166
  with gr.Tab("Videos"):
167
+ gr.Markdown("### 3) Videos (coming next)")
 
168
  vd_table = gr.JSON(label="Planned clip edges (read-only for now)")
169
  to_export_btn = gr.Button("Continue to Export →", interactive=False)
170
 
171
  with gr.Tab("Export"):
172
+ gr.Markdown("### 4) Export (coming next)")
 
173
  export_info = gr.Markdown("Nothing to export yet.")
174
 
175
+ # -------- Handlers --------
176
  def on_new(name):
177
+ name = (name or "").strip() or f"Project-{new_id()}"
178
  pid = new_id()
179
  p = {
180
+ "meta": {"id": pid, "name": name, "created": now_iso(), "updated": now_iso()},
 
 
 
 
 
181
  "shots": [],
182
  "clips": []
183
  }
184
  save_project(p)
185
  return p, gr.update(value=f"**New project created** `{name}` (id: `{pid}`)")
186
 
187
+ new_btn.click(on_new, inputs=[proj_name], outputs=[project, sb_status])
 
 
 
 
188
 
189
  def on_propose(p, prompt, target_shots, fps, vlen):
190
  if p is None:
191
  raise gr.Error("Create a project first (New Project).")
192
+ if not prompt or not str(prompt).strip():
193
+ raise gr.Error("Please enter a high-level prompt.")
194
+ shots = generate_storyboard_with_llm(str(prompt).strip(), int(target_shots), int(fps), int(vlen))
195
  p = dict(p)
196
  p["shots"] = shots
197
  p["meta"]["updated"] = now_iso()
198
  save_project(p)
199
+ return p, shots, gr.update(value="Storyboard generated by LLM (ZeroGPU).")
200
 
201
  propose_btn.click(
202
  on_propose,
 
207
  def on_confirm(p):
208
  if p is None or not p.get("shots"):
209
  raise gr.Error("No storyboard yet.")
 
210
  edges = []
211
  for i in range(len(p["shots"]) - 1):
212
+ a = p["shots"][i]["id"]
213
+ b = p["shots"][i+1]["id"]
214
+ edges.append({"from": a, "to": b, "prompt": f"Transition from shot {a} to {b}"})
215
  p = dict(p)
216
  p["clips"] = edges
217
  p["meta"]["updated"] = now_iso()
 
236
  path = save_project(p)
237
  return gr.update(value=f"Saved to `{path}`")
238
 
239
+ save_btn.click(on_save, inputs=[project], outputs=[sb_status])
 
 
 
 
240
 
241
  def on_load(file_obj):
242
  p = load_project_file(file_obj)
 
255
  )
256
 
257
  if __name__ == "__main__":
258
+ # SSR is fine; you can set share=True if you want a public link automatically
259
  demo.launch()