Shalmoni commited on
Commit
c84843c
·
verified ·
1 Parent(s): 75c12be

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -42
app.py CHANGED
@@ -11,7 +11,6 @@ INFERENCE_URL = "https://moonmath-ai-dev--moonmath-i2v-backend-moonmathinference
11
 
12
  # -------- settings --------
13
  MAX_SLOTS = 12 # max image slots user can reveal
14
- MAX_TIMELINE = 20 # max clips in the timeline
15
 
16
  # -------- small helpers --------
17
  def _save_video_bytes(data: bytes, tag: str) -> str:
@@ -32,6 +31,9 @@ def _download_to_bytes(url: str) -> bytes:
32
  return r.content
33
 
34
  def stitch_call(start_img: Image.Image, end_img: Image.Image, prompt: str, seed: Optional[int]) -> Optional[str]:
 
 
 
35
  if start_img is None or end_img is None:
36
  return None
37
 
@@ -56,11 +58,11 @@ def stitch_call(start_img: Image.Image, end_img: Image.Image, prompt: str, seed:
56
 
57
  # JSON with url or base64
58
  data = resp.json()
59
- video_url = data.get("video_url") or data.get("url") or data.get("result")
60
- if isinstance(video_url, str) and video_url.startswith("http"):
61
  return _save_video_bytes(_download_to_bytes(video_url), "stitch")
62
 
63
- video_b64 = data.get("video_b64")
64
  if isinstance(video_b64, str):
65
  pad = (-len(video_b64)) % 4
66
  if pad:
@@ -93,21 +95,44 @@ def concat_many(videos: List[str]) -> Optional[str]:
93
  print("concat_many error:", e)
94
  return None
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  # =========================
97
  # Gradio callbacks / state ops
98
  # =========================
99
  def add_image_slot(visible_slots: int):
100
  """Reveal one more upload slot (up to MAX_SLOTS)."""
101
- new_count = min(MAX_SLOTS, visible_slots + 1)
102
- return new_count
 
 
 
 
 
 
 
103
 
104
  def collect_choices(*imgs):
105
- """Build dropdown choices of available indices (1-based labels)."""
106
  choices = []
107
  for i, img in enumerate(imgs, start=1):
108
  if img is not None:
109
  choices.append(str(i))
110
- # Return same list for both start/end dropdowns
111
  return gr.update(choices=choices), gr.update(choices=choices)
112
 
113
  def stitch_selected(prompt, seed, start_idx_str, end_idx_str, *imgs):
@@ -125,6 +150,7 @@ def stitch_selected(prompt, seed, start_idx_str, end_idx_str, *imgs):
125
  if s < 0 or e < 0 or s >= len(imgs) or e >= len(imgs):
126
  gr.Warning("Start/End out of range.")
127
  return None
 
128
  start_img = imgs[s]
129
  end_img = imgs[e]
130
  if start_img is None or end_img is None:
@@ -138,24 +164,13 @@ def stitch_selected(prompt, seed, start_idx_str, end_idx_str, *imgs):
138
  return vid # path for preview
139
 
140
  def add_to_timeline(preview_path, timeline_paths: List[str]):
141
- """Append preview_path to timeline state; return updated per-slot outputs."""
 
142
  if not preview_path:
143
  gr.Warning("Generate a clip first.")
144
- return timeline_paths, *([gr.update(value=None)] * MAX_TIMELINE)
145
-
146
- # append if room
147
- tl = list(timeline_paths or [])
148
- if len(tl) >= MAX_TIMELINE:
149
- gr.Warning("Timeline full.")
150
- return tl, *([gr.update(value=None)] * MAX_TIMELINE)
151
-
152
  tl.append(preview_path)
153
-
154
- # map into video components
155
- outputs = []
156
- for i in range(MAX_TIMELINE):
157
- outputs.append(gr.update(value=tl[i] if i < len(tl) else None))
158
- return tl, *outputs
159
 
160
  def stitch_all_from_timeline(timeline_paths: List[str]):
161
  vids = list(timeline_paths or [])
@@ -176,17 +191,35 @@ CSS = """
176
  .rounded textarea { border-radius: 16px !important; }
177
  .gallery-row { display:flex; gap:16px; overflow-x:auto; padding:8px 4px; }
178
  .gallery-row .gradio-image { min-width: 220px; }
179
- .timeline-row { display:flex; gap:16px; overflow-x:auto; padding:8px 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  """
181
 
182
  with gr.Blocks(css=CSS, title="StitchMaster") as demo:
183
- gr.Markdown("## StitchMaster — Upload images, stitch between frames, build a timeline, and export a single video.")
184
 
185
  # --- State ---
186
- visible_slots = gr.State(value=3) # how many image slots are visible
187
  timeline_state = gr.State(value=[]) # list[str] of video file paths (timeline)
188
 
189
- # --- Image gallery (growing) ---
190
  with gr.Row(elem_classes=["gallery-row"]):
191
  img_comps = []
192
  for i in range(MAX_SLOTS):
@@ -202,20 +235,13 @@ with gr.Blocks(css=CSS, title="StitchMaster") as demo:
202
  )
203
 
204
  # reflect visibility changes whenever visible_slots changes
205
- # (we re-render all image components with correct visibility)
206
- def _reveal_slots(n, *imgs):
207
- updates = []
208
- for i in range(MAX_SLOTS):
209
- updates.append(gr.update(visible=(i < int(n))))
210
- return updates
211
-
212
  visible_slots.change(
213
  fn=_reveal_slots,
214
  inputs=[visible_slots] + img_comps,
215
  outputs=img_comps
216
  )
217
 
218
- # --- Stitch controls ---
219
  seed = gr.Number(value=0, precision=0, label="Seed (0 = random)")
220
 
221
  with gr.Row():
@@ -226,12 +252,12 @@ with gr.Blocks(css=CSS, title="StitchMaster") as demo:
226
  placeholder="Describe the transition between the selected start and end frames…",
227
  lines=3, label="Prompt", elem_classes=["rounded"]
228
  )
229
- run_btn = gr.Button("Stitch", elem_classes=["pill"])
230
  add_tl_btn = gr.Button("Add to timeline", elem_classes=["pill"])
231
  with gr.Column(scale=1, min_width=420):
232
  preview = gr.Video(label="Video output", interactive=False)
233
 
234
- # keep start/end dropdowns up to date based on which slots actually have images
235
  for comp in img_comps:
236
  comp.change(
237
  fn=collect_choices,
@@ -246,15 +272,14 @@ with gr.Blocks(css=CSS, title="StitchMaster") as demo:
246
  outputs=[preview]
247
  )
248
 
249
- # add to timeline action update state and visible clips
250
- # Prepare timeline video components (scroll row)
251
- with gr.Row(elem_classes=["timeline-row"]):
252
- timeline_videos = [gr.Video(label=f"Clip {i+1}", interactive=False) for i in range(MAX_TIMELINE)]
253
 
254
  add_tl_btn.click(
255
  fn=add_to_timeline,
256
  inputs=[preview, timeline_state],
257
- outputs=[timeline_state] + timeline_videos
258
  )
259
 
260
  # final stitch all (concatenate in order)
 
11
 
12
  # -------- settings --------
13
  MAX_SLOTS = 12 # max image slots user can reveal
 
14
 
15
  # -------- small helpers --------
16
  def _save_video_bytes(data: bytes, tag: str) -> str:
 
31
  return r.content
32
 
33
  def stitch_call(start_img: Image.Image, end_img: Image.Image, prompt: str, seed: Optional[int]) -> Optional[str]:
34
+ """
35
+ Calls your Modal backend with two images + prompt + seed and returns a local /tmp video path.
36
+ """
37
  if start_img is None or end_img is None:
38
  return None
39
 
 
58
 
59
  # JSON with url or base64
60
  data = resp.json()
61
+ video_url = data.get("video_url") or data.get("url") or data.get("result") or data.get("output")
62
+ if isinstance(video_url, str) and video_url.startswith(("http://", "https://")):
63
  return _save_video_bytes(_download_to_bytes(video_url), "stitch")
64
 
65
+ video_b64 = data.get("video_b64") or data.get("videoBase64")
66
  if isinstance(video_b64, str):
67
  pad = (-len(video_b64)) % 4
68
  if pad:
 
95
  print("concat_many error:", e)
96
  return None
97
 
98
+ # -------- Timeline HTML renderer --------
99
+ def render_timeline_html(paths: List[str]):
100
+ vids = [p for p in (paths or []) if p]
101
+ if not vids:
102
+ return "<div class='tl-grid tl-empty'>No clips yet. Generate and click ‘Add to timeline’.</div>"
103
+ items = []
104
+ for i, p in enumerate(vids, 1):
105
+ items.append(
106
+ f"""
107
+ <div class="tl-item">
108
+ <video src="{p}" controls playsinline></video>
109
+ <div class="tl-label">Clip {i}</div>
110
+ </div>
111
+ """
112
+ )
113
+ return f"<div class='tl-grid'>{''.join(items)}</div>"
114
+
115
  # =========================
116
  # Gradio callbacks / state ops
117
  # =========================
118
  def add_image_slot(visible_slots: int):
119
  """Reveal one more upload slot (up to MAX_SLOTS)."""
120
+ return min(MAX_SLOTS, int(visible_slots) + 1)
121
+
122
+ def _reveal_slots(n, *imgs):
123
+ """Update visibility of image upload components based on visible_slots state."""
124
+ n = int(n)
125
+ updates = []
126
+ for i in range(MAX_SLOTS):
127
+ updates.append(gr.update(visible=(i < n)))
128
+ return updates
129
 
130
  def collect_choices(*imgs):
131
+ """Build dropdown choices of available indices (1-based labels) based on non-empty slots."""
132
  choices = []
133
  for i, img in enumerate(imgs, start=1):
134
  if img is not None:
135
  choices.append(str(i))
 
136
  return gr.update(choices=choices), gr.update(choices=choices)
137
 
138
  def stitch_selected(prompt, seed, start_idx_str, end_idx_str, *imgs):
 
150
  if s < 0 or e < 0 or s >= len(imgs) or e >= len(imgs):
151
  gr.Warning("Start/End out of range.")
152
  return None
153
+
154
  start_img = imgs[s]
155
  end_img = imgs[e]
156
  if start_img is None or end_img is None:
 
164
  return vid # path for preview
165
 
166
  def add_to_timeline(preview_path, timeline_paths: List[str]):
167
+ """Append preview to timeline; return updated state and HTML."""
168
+ tl = list(timeline_paths or [])
169
  if not preview_path:
170
  gr.Warning("Generate a clip first.")
171
+ return tl, gr.update(value=render_timeline_html(tl))
 
 
 
 
 
 
 
172
  tl.append(preview_path)
173
+ return tl, gr.update(value=render_timeline_html(tl))
 
 
 
 
 
174
 
175
  def stitch_all_from_timeline(timeline_paths: List[str]):
176
  vids = list(timeline_paths or [])
 
191
  .rounded textarea { border-radius: 16px !important; }
192
  .gallery-row { display:flex; gap:16px; overflow-x:auto; padding:8px 4px; }
193
  .gallery-row .gradio-image { min-width: 220px; }
194
+ .tl-grid {
195
+ display: grid;
196
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
197
+ gap: 12px;
198
+ }
199
+ .tl-grid video {
200
+ width: 100%;
201
+ height: 120px;
202
+ object-fit: cover;
203
+ border-radius: 12px;
204
+ display: block;
205
+ }
206
+ .tl-label {
207
+ font-size: 12px;
208
+ color: #9aa0a6;
209
+ margin-top: 4px;
210
+ text-align: center;
211
+ }
212
+ .tl-empty { color: #9aa0a6; padding: 8px 4px; }
213
  """
214
 
215
  with gr.Blocks(css=CSS, title="StitchMaster") as demo:
216
+ gr.Markdown("## StitchMaster")
217
 
218
  # --- State ---
219
+ visible_slots = gr.State(value=3) # number of visible image slots
220
  timeline_state = gr.State(value=[]) # list[str] of video file paths (timeline)
221
 
222
+ # --- Image gallery (horizontal, grows on demand) ---
223
  with gr.Row(elem_classes=["gallery-row"]):
224
  img_comps = []
225
  for i in range(MAX_SLOTS):
 
235
  )
236
 
237
  # reflect visibility changes whenever visible_slots changes
 
 
 
 
 
 
 
238
  visible_slots.change(
239
  fn=_reveal_slots,
240
  inputs=[visible_slots] + img_comps,
241
  outputs=img_comps
242
  )
243
 
244
+ # Seed + Start/End selection + Prompt + Stitch + Preview
245
  seed = gr.Number(value=0, precision=0, label="Seed (0 = random)")
246
 
247
  with gr.Row():
 
252
  placeholder="Describe the transition between the selected start and end frames…",
253
  lines=3, label="Prompt", elem_classes=["rounded"]
254
  )
255
+ run_btn = gr.Button("Generate", elem_classes=["pill"])
256
  add_tl_btn = gr.Button("Add to timeline", elem_classes=["pill"])
257
  with gr.Column(scale=1, min_width=420):
258
  preview = gr.Video(label="Video output", interactive=False)
259
 
260
+ # keep start/end dropdowns up to date based on which slots have images
261
  for comp in img_comps:
262
  comp.change(
263
  fn=collect_choices,
 
272
  outputs=[preview]
273
  )
274
 
275
+ # --- Dynamic timeline (no placeholders) ---
276
+ with gr.Row():
277
+ timeline_html = gr.HTML(value=render_timeline_html([]))
 
278
 
279
  add_tl_btn.click(
280
  fn=add_to_timeline,
281
  inputs=[preview, timeline_state],
282
+ outputs=[timeline_state, timeline_html]
283
  )
284
 
285
  # final stitch all (concatenate in order)