Adnan commited on
Commit
7a30217
·
verified ·
1 Parent(s): 32d07c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -125
app.py CHANGED
@@ -1,8 +1,6 @@
1
  """
2
  ⚡ TimeLapseForge — JSON to Timelapse Video in One Click
3
  Main Gradio Application for Hugging Face Spaces.
4
-
5
- Paste your GPT JSON → Generate consistent panels → Smooth video output
6
  """
7
 
8
  import os
@@ -15,19 +13,11 @@ from typing import List, Optional
15
 
16
  # Import our modules
17
  from prompt_parser import PromptParser, QuickGenerator
18
- from image_generator import ImageGenerator
19
  from frame_interpolator import FrameInterpolator
20
  from video_assembler import VideoAssembler
21
 
22
- # Try importing spaces for ZeroGPU support
23
- try:
24
- import spaces
25
- ZERO_GPU = True
26
- except ImportError:
27
- ZERO_GPU = False
28
-
29
  # ─── Initialize Modules ─────────────────────────────────────
30
- parser = PromptParser()
31
  quick_gen = QuickGenerator()
32
  interpolator = FrameInterpolator()
33
  assembler = VideoAssembler()
@@ -39,17 +29,26 @@ MODELS = {
39
  "SD 1.5 (Lightweight, 512px)": "runwayml/stable-diffusion-v1-5",
40
  }
41
 
 
 
 
 
 
 
 
 
 
 
42
  # ─── Core Functions ──────────────────────────────────────────
43
 
44
- def parse_input(json_text: str, quick_text: str, num_panels: int, mode: str):
45
  """Parse JSON input or generate from quick text."""
46
  if json_text and json_text.strip():
47
- result = parser.parse(json_text)
48
  if result["success"]:
49
  data = result["data"]
50
- prompts = parser.extract_prompts(data)
51
- summary = parser.get_summary(data)
52
- # Create preview table
53
  table_data = [
54
  [p["panel_id"], p["phase"], p["panel_title"], p["main_prompt"][:100] + "..."]
55
  for p in prompts
@@ -62,53 +61,53 @@ def parse_input(json_text: str, quick_text: str, num_panels: int, mode: str):
62
  )
63
  else:
64
  return (
65
- f"❌ JSON Parse Error: {result['error']}\n\nPlease check your JSON format.",
66
  [],
67
  "",
68
  gr.update(visible=False),
69
  )
70
  elif quick_text and quick_text.strip():
71
- data = quick_gen.generate(quick_text, num_panels=num_panels, mode=mode)
72
- prompts = parser.extract_prompts(data)
73
- summary = parser.get_summary(data)
74
  table_data = [
75
  [p["panel_id"], p["phase"], p["panel_title"], p["main_prompt"][:100] + "..."]
76
  for p in prompts
77
  ]
78
  return (
79
- summary + "\n\n⚠️ *Generated from quick text. For better results, use the GPT to generate detailed JSON.*",
80
  table_data,
81
  json.dumps(data, indent=2),
82
  gr.update(visible=True),
83
  )
84
  else:
85
- return "⚠️ Please paste JSON or enter a quick text description.", [], "", gr.update(visible=False)
 
 
 
 
 
86
 
87
 
88
  def generate_panels(
89
- parsed_json: str,
90
- model_name: str,
91
- strength: float,
92
- base_seed: int,
93
- steps: int,
94
- guidance: float,
95
- ref_image: Optional[Image.Image],
96
- progress=gr.Progress(),
97
  ):
98
- """Generate all panel images using the selected model."""
99
  if not parsed_json:
100
- return [], [], "❌ No parsed JSON found. Please parse input first."
101
 
102
  try:
103
  data = json.loads(parsed_json)
104
  except json.JSONDecodeError:
105
  return [], [], "❌ Invalid parsed JSON."
106
 
107
- prompts = parser.extract_prompts(data)
108
  if not prompts:
109
  return [], [], "❌ No panels found in JSON."
110
 
111
  model_id = MODELS.get(model_name, "stabilityai/sdxl-turbo")
 
112
  gen = ImageGenerator(model_id)
113
 
114
  progress(0, desc="Loading model...")
@@ -116,21 +115,19 @@ def generate_panels(
116
  def progress_cb(current, total):
117
  progress(current / total, desc=f"Generating panel {current}/{total}")
118
 
119
- # Handle steps and guidance for SDXL Turbo
120
- gen_steps = steps if steps > 0 else None
121
- gen_guidance = guidance if guidance >= 0 else None
122
 
123
  images = gen.generate_all_panels(
124
  prompts=prompts,
125
- strength=strength,
126
- base_seed=base_seed,
127
  steps=gen_steps,
128
  guidance=gen_guidance,
129
  reference_image=ref_image,
130
  progress_callback=progress_cb,
131
  )
132
 
133
- # Create gallery data with labels
134
  gallery_items = []
135
  for i, img in enumerate(images):
136
  label = prompts[i]["panel_title"] if i < len(prompts) else f"Panel {i+1}"
@@ -141,24 +138,16 @@ def generate_panels(
141
 
142
 
143
  def interpolate_and_assemble(
144
- images_state: List[Image.Image],
145
- parsed_json: str,
146
- interp_multiplier: int,
147
- interp_method: str,
148
- fps: int,
149
- hold_seconds: float,
150
- add_labels: bool,
151
- add_progress_bar: bool,
152
- add_bookend: bool,
153
- music_file: Optional[str],
154
- export_gif: bool,
155
- progress=gr.Progress(),
156
  ):
157
- """Interpolate frames and assemble the final video."""
158
  if not images_state:
159
- return None, None, None, "❌ No images to assemble. Generate panels first."
160
 
161
  images = images_state
 
162
 
163
  # Step 1: Interpolate
164
  progress(0.1, desc="Interpolating frames...")
@@ -168,23 +157,25 @@ def interpolate_and_assemble(
168
  progress(p, desc=f"Interpolating {current}/{total} pairs")
169
 
170
  smooth_frames = interpolator.interpolate_sequence(
171
- images, multiplier=interp_multiplier, method=interp_method, progress_callback=interp_cb
 
 
 
172
  )
173
  else:
174
- smooth_frames = images
175
 
176
- # Step 2: Extract labels from JSON
177
  labels = None
178
  if add_labels and parsed_json:
179
  try:
180
  data = json.loads(parsed_json)
181
- prompts = parser.extract_prompts(data)
182
- # Map labels to interpolated frames
183
  if interp_multiplier > 1:
184
  labels = []
185
  for i, p in enumerate(prompts):
186
  labels.append(p.get("timestamp_label", ""))
187
- labels.extend([""] * interp_multiplier) # No labels for interpolated frames
188
  labels = labels[:len(smooth_frames)]
189
  else:
190
  labels = [p.get("timestamp_label", "") for p in prompts]
@@ -193,11 +184,13 @@ def interpolate_and_assemble(
193
 
194
  # Step 3: Assemble video
195
  progress(0.5, desc="Assembling video...")
196
- adj_hold = hold_seconds / max(interp_multiplier, 1) if interp_multiplier > 1 else hold_seconds
 
 
197
 
198
  video_path = assembler.create_video(
199
  images=smooth_frames,
200
- fps=fps,
201
  hold_seconds=adj_hold,
202
  add_labels=add_labels,
203
  labels=labels,
@@ -205,33 +198,28 @@ def interpolate_and_assemble(
205
  add_bookend_labels=add_bookend,
206
  )
207
 
208
- # Step 4: Add audio if provided
209
  if music_file:
210
  progress(0.8, desc="Adding audio...")
211
  video_path = assembler.add_audio_to_video(video_path, music_file)
212
 
213
- # Step 5: Create comparison image
214
  progress(0.9, desc="Creating comparison...")
215
  comparison = assembler.create_comparison_image(images[0], images[-1])
216
 
217
- # Step 6: Create GIF (optional)
218
  gif_path = None
219
  if export_gif:
220
  progress(0.95, desc="Creating GIF...")
221
  gif_path = assembler.create_gif(images, duration_per_frame=400)
222
 
223
  progress(1.0, desc="Done!")
224
- status = f"✅ Video created! {len(smooth_frames)} frames at {fps}fps"
225
  return video_path, comparison, gif_path, status
226
 
227
 
228
  def regenerate_single(
229
- panel_num: int,
230
- parsed_json: str,
231
- images_state: List[Image.Image],
232
- model_name: str,
233
- strength: float,
234
- base_seed: int,
235
  ):
236
  """Regenerate a single panel."""
237
  if not images_state or not parsed_json:
@@ -239,7 +227,7 @@ def regenerate_single(
239
 
240
  try:
241
  data = json.loads(parsed_json)
242
- prompts = parser.extract_prompts(data)
243
  except Exception:
244
  return images_state, [], "❌ Invalid JSON."
245
 
@@ -248,9 +236,12 @@ def regenerate_single(
248
  return images_state, [], f"❌ Invalid panel number. Must be 1-{len(images_state)}."
249
 
250
  model_id = MODELS.get(model_name, "stabilityai/sdxl-turbo")
 
251
  gen = ImageGenerator(model_id)
252
 
253
- _, updated = gen.regenerate_single_panel(idx, prompts, images_state, strength, base_seed)
 
 
254
 
255
  gallery_items = [(img, f"Panel {i+1}") for i, img in enumerate(updated)]
256
  return updated, gallery_items, f"✅ Panel {int(panel_num)} regenerated!"
@@ -263,54 +254,49 @@ HEADER_MD = """
263
  ### JSON to Timelapse Video — One Click Pipeline
264
 
265
  **Workflow:** Paste GPT JSON → Generate Consistent Panels → Smooth Interpolation → Export Video
266
-
267
- Built for use with **Timelapse Vision Master GPT** — or use Quick Text for simple projects.
268
  """
269
 
270
  with gr.Blocks(
271
  title="⚡ TimeLapseForge",
272
  theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
273
- css="""
274
- .panel-gallery { min-height: 300px; }
275
- .status-box { padding: 10px; border-radius: 8px; }
276
- """
277
  ) as app:
278
 
279
- # ── Shared State ──
280
  images_state = gr.State(value=[])
281
  parsed_json_state = gr.State(value="")
282
 
283
  gr.Markdown(HEADER_MD)
284
 
285
  with gr.Tabs():
286
- # ════════════════════════════════════════
287
- # TAB 1: INPUT
288
- # ════════════════════════════════════════
289
- with gr.Tab("📥 Input & Parse", id="input"):
290
  with gr.Row():
291
  with gr.Column(scale=2):
292
  json_input = gr.Textbox(
293
  label="📋 Paste GPT JSON Output",
294
- placeholder='Paste the complete JSON from your Timelapse GPT here...\n\n{\n "project_metadata": { ... },\n "panels": [ ... ]\n}',
295
  lines=15,
296
- max_lines=30,
297
  )
298
  with gr.Column(scale=1):
299
- gr.Markdown("**— OR —**\n\nUse Quick Text for simple projects:")
300
  quick_text = gr.Textbox(
301
  label="⚡ Quick Text Command",
302
- placeholder="e.g., Restore a rusty 1969 Camaro SS in a workshop",
303
  lines=3,
304
  )
305
  with gr.Row():
306
- quick_panels = gr.Slider(8, 40, value=20, step=1, label="Panels")
 
 
307
  quick_mode = gr.Dropdown(
308
- ["restoration", "creation"], value="restoration", label="Mode"
 
 
309
  )
310
 
311
  parse_btn = gr.Button("🔍 Parse & Preview", variant="primary", size="lg")
312
-
313
- parse_status = gr.Markdown("", elem_classes=["status-box"])
314
  prompt_table = gr.Dataframe(
315
  headers=["#", "Phase", "Title", "Prompt Preview"],
316
  label="📊 Extracted Panels",
@@ -318,10 +304,8 @@ with gr.Blocks(
318
  wrap=True,
319
  )
320
 
321
- # ════════════════════════════════════════
322
- # TAB 2: GENERATE
323
- # ════════════════════════════════════════
324
- with gr.Tab("🎨 Generate Panels", id="generate"):
325
  with gr.Row():
326
  with gr.Column(scale=1):
327
  model_choice = gr.Dropdown(
@@ -331,21 +315,20 @@ with gr.Blocks(
331
  )
332
  strength = gr.Slider(
333
  0.15, 0.70, value=0.38, step=0.01,
334
- label="🔄 Change Strength (lower = more consistent)",
335
- info="0.25=barely changes, 0.38=natural, 0.55=significant change",
336
  )
337
  base_seed = gr.Number(value=42, label="🌱 Base Seed", precision=0)
338
  steps = gr.Slider(
339
  0, 50, value=0, step=1,
340
- label="🔢 Steps (0 = auto for model)",
341
  )
342
  guidance = gr.Slider(
343
  -1, 15, value=-1, step=0.5,
344
- label="🎯 Guidance Scale (-1 = auto for model)",
345
  )
346
  ref_image = gr.Image(
347
- label="🖼️ Reference Image (optional)",
348
- type="pil",
349
  )
350
 
351
  with gr.Column(scale=3):
@@ -359,62 +342,63 @@ with gr.Blocks(
359
  rows=3,
360
  height=450,
361
  object_fit="contain",
362
- elem_classes=["panel-gallery"],
363
  )
364
 
365
  with gr.Accordion("🔄 Regenerate Single Panel", open=False):
366
  with gr.Row():
367
- regen_panel_num = gr.Number(value=1, label="Panel Number", precision=0)
368
- regen_btn = gr.Button("🔄 Regenerate This Panel", variant="secondary")
 
 
369
  regen_status = gr.Markdown("")
370
 
371
- # ════════════════════════════════════════
372
- # TAB 3: VIDEO
373
- # ════════════════════════════════════════
374
- with gr.Tab("🎬 Create Video", id="video"):
375
  with gr.Row():
376
  with gr.Column(scale=1):
377
- gr.Markdown("### 🎞️ Interpolation Settings")
378
  interp_mult = gr.Slider(
379
  1, 8, value=3, step=1,
380
  label="Frame Multiplier",
381
- info="1=no interpolation, 3=smooth, 6=ultra smooth",
382
  )
383
  interp_method = gr.Dropdown(
384
  ["blend", "flow"], value="blend",
385
- label="Interpolation Method",
386
- info="blend=fast & reliable, flow=better quality but slower",
387
  )
388
 
389
- gr.Markdown("### 🎬 Video Settings")
390
  vid_fps = gr.Slider(12, 60, value=24, step=1, label="FPS")
391
  frame_hold = gr.Slider(
392
- 0.3, 5.0, value=1.5, step=0.1, label="Seconds per Panel"
 
393
  )
394
 
395
  gr.Markdown("### 🏷️ Overlays")
396
- add_labels = gr.Checkbox(value=True, label="Add timestamp labels")
397
- add_progress_bar = gr.Checkbox(value=True, label="Add progress bar")
398
- add_bookend = gr.Checkbox(value=True, label="Add BEFORE / AFTER labels")
399
 
400
  gr.Markdown("### 🎵 Audio")
401
- music_file = gr.Audio(label="Upload Background Music", type="filepath")
 
 
402
 
403
  gr.Markdown("### 📦 Export")
404
- export_gif = gr.Checkbox(value=False, label="Also export as GIF")
405
 
406
  with gr.Column(scale=2):
407
  assemble_btn = gr.Button(
408
- "🎬 Interpolate & Create Video", variant="primary", size="lg"
 
 
409
  )
410
  vid_status = gr.Markdown("")
411
-
412
- video_output = gr.Video(label="🎥 Your Timelapse Video")
413
- comparison_output = gr.Image(label="📊 Before / After Comparison")
414
  gif_output = gr.File(label="📦 GIF Download")
415
 
416
  # ── Event Handlers ──
417
-
418
  parse_btn.click(
419
  fn=parse_input,
420
  inputs=[json_input, quick_text, quick_panels, quick_mode],
 
1
  """
2
  ⚡ TimeLapseForge — JSON to Timelapse Video in One Click
3
  Main Gradio Application for Hugging Face Spaces.
 
 
4
  """
5
 
6
  import os
 
13
 
14
  # Import our modules
15
  from prompt_parser import PromptParser, QuickGenerator
 
16
  from frame_interpolator import FrameInterpolator
17
  from video_assembler import VideoAssembler
18
 
 
 
 
 
 
 
 
19
  # ─── Initialize Modules ─────────────────────────────────────
20
+ prompt_parser = PromptParser()
21
  quick_gen = QuickGenerator()
22
  interpolator = FrameInterpolator()
23
  assembler = VideoAssembler()
 
29
  "SD 1.5 (Lightweight, 512px)": "runwayml/stable-diffusion-v1-5",
30
  }
31
 
32
+ # ─── Lazy import image generator (heavy deps) ───────────────
33
+ _image_gen_class = None
34
+
35
+ def get_image_generator_class():
36
+ global _image_gen_class
37
+ if _image_gen_class is None:
38
+ from image_generator import ImageGenerator
39
+ _image_gen_class = ImageGenerator
40
+ return _image_gen_class
41
+
42
  # ─── Core Functions ──────────────────────────────────────────
43
 
44
+ def parse_input(json_text, quick_text, num_panels, mode):
45
  """Parse JSON input or generate from quick text."""
46
  if json_text and json_text.strip():
47
+ result = prompt_parser.parse(json_text)
48
  if result["success"]:
49
  data = result["data"]
50
+ prompts = prompt_parser.extract_prompts(data)
51
+ summary = prompt_parser.get_summary(data)
 
52
  table_data = [
53
  [p["panel_id"], p["phase"], p["panel_title"], p["main_prompt"][:100] + "..."]
54
  for p in prompts
 
61
  )
62
  else:
63
  return (
64
+ f"❌ JSON Parse Error: {result['error']}\n\nCheck your JSON format.",
65
  [],
66
  "",
67
  gr.update(visible=False),
68
  )
69
  elif quick_text and quick_text.strip():
70
+ data = quick_gen.generate(quick_text, num_panels=int(num_panels), mode=mode)
71
+ prompts = prompt_parser.extract_prompts(data)
72
+ summary = prompt_parser.get_summary(data)
73
  table_data = [
74
  [p["panel_id"], p["phase"], p["panel_title"], p["main_prompt"][:100] + "..."]
75
  for p in prompts
76
  ]
77
  return (
78
+ summary + "\n\n⚠️ *Generated from quick text. Use GPT JSON for better results.*",
79
  table_data,
80
  json.dumps(data, indent=2),
81
  gr.update(visible=True),
82
  )
83
  else:
84
+ return (
85
+ "⚠️ Please paste JSON or enter a quick text description.",
86
+ [],
87
+ "",
88
+ gr.update(visible=False),
89
+ )
90
 
91
 
92
  def generate_panels(
93
+ parsed_json, model_name, strength, base_seed,
94
+ steps, guidance, ref_image, progress=gr.Progress(),
 
 
 
 
 
 
95
  ):
96
+ """Generate all panel images."""
97
  if not parsed_json:
98
+ return [], [], "❌ No parsed JSON found. Parse input first."
99
 
100
  try:
101
  data = json.loads(parsed_json)
102
  except json.JSONDecodeError:
103
  return [], [], "❌ Invalid parsed JSON."
104
 
105
+ prompts = prompt_parser.extract_prompts(data)
106
  if not prompts:
107
  return [], [], "❌ No panels found in JSON."
108
 
109
  model_id = MODELS.get(model_name, "stabilityai/sdxl-turbo")
110
+ ImageGenerator = get_image_generator_class()
111
  gen = ImageGenerator(model_id)
112
 
113
  progress(0, desc="Loading model...")
 
115
  def progress_cb(current, total):
116
  progress(current / total, desc=f"Generating panel {current}/{total}")
117
 
118
+ gen_steps = int(steps) if steps and int(steps) > 0 else None
119
+ gen_guidance = float(guidance) if guidance is not None and float(guidance) >= 0 else None
 
120
 
121
  images = gen.generate_all_panels(
122
  prompts=prompts,
123
+ strength=float(strength),
124
+ base_seed=int(base_seed),
125
  steps=gen_steps,
126
  guidance=gen_guidance,
127
  reference_image=ref_image,
128
  progress_callback=progress_cb,
129
  )
130
 
 
131
  gallery_items = []
132
  for i, img in enumerate(images):
133
  label = prompts[i]["panel_title"] if i < len(prompts) else f"Panel {i+1}"
 
138
 
139
 
140
  def interpolate_and_assemble(
141
+ images_state, parsed_json, interp_multiplier, interp_method,
142
+ fps, hold_seconds, add_labels, add_progress_bar, add_bookend,
143
+ music_file, export_gif, progress=gr.Progress(),
 
 
 
 
 
 
 
 
 
144
  ):
145
+ """Interpolate frames and assemble video."""
146
  if not images_state:
147
+ return None, None, None, "❌ No images. Generate panels first."
148
 
149
  images = images_state
150
+ interp_multiplier = int(interp_multiplier)
151
 
152
  # Step 1: Interpolate
153
  progress(0.1, desc="Interpolating frames...")
 
157
  progress(p, desc=f"Interpolating {current}/{total} pairs")
158
 
159
  smooth_frames = interpolator.interpolate_sequence(
160
+ images,
161
+ multiplier=interp_multiplier,
162
+ method=interp_method,
163
+ progress_callback=interp_cb,
164
  )
165
  else:
166
+ smooth_frames = list(images)
167
 
168
+ # Step 2: Extract labels
169
  labels = None
170
  if add_labels and parsed_json:
171
  try:
172
  data = json.loads(parsed_json)
173
+ prompts = prompt_parser.extract_prompts(data)
 
174
  if interp_multiplier > 1:
175
  labels = []
176
  for i, p in enumerate(prompts):
177
  labels.append(p.get("timestamp_label", ""))
178
+ labels.extend([""] * interp_multiplier)
179
  labels = labels[:len(smooth_frames)]
180
  else:
181
  labels = [p.get("timestamp_label", "") for p in prompts]
 
184
 
185
  # Step 3: Assemble video
186
  progress(0.5, desc="Assembling video...")
187
+ adj_hold = float(hold_seconds)
188
+ if interp_multiplier > 1:
189
+ adj_hold = adj_hold / max(interp_multiplier, 1)
190
 
191
  video_path = assembler.create_video(
192
  images=smooth_frames,
193
+ fps=int(fps),
194
  hold_seconds=adj_hold,
195
  add_labels=add_labels,
196
  labels=labels,
 
198
  add_bookend_labels=add_bookend,
199
  )
200
 
201
+ # Step 4: Add audio
202
  if music_file:
203
  progress(0.8, desc="Adding audio...")
204
  video_path = assembler.add_audio_to_video(video_path, music_file)
205
 
206
+ # Step 5: Comparison image
207
  progress(0.9, desc="Creating comparison...")
208
  comparison = assembler.create_comparison_image(images[0], images[-1])
209
 
210
+ # Step 6: GIF
211
  gif_path = None
212
  if export_gif:
213
  progress(0.95, desc="Creating GIF...")
214
  gif_path = assembler.create_gif(images, duration_per_frame=400)
215
 
216
  progress(1.0, desc="Done!")
217
+ status = f"✅ Video created! {len(smooth_frames)} frames at {int(fps)}fps"
218
  return video_path, comparison, gif_path, status
219
 
220
 
221
  def regenerate_single(
222
+ panel_num, parsed_json, images_state, model_name, strength, base_seed,
 
 
 
 
 
223
  ):
224
  """Regenerate a single panel."""
225
  if not images_state or not parsed_json:
 
227
 
228
  try:
229
  data = json.loads(parsed_json)
230
+ prompts = prompt_parser.extract_prompts(data)
231
  except Exception:
232
  return images_state, [], "❌ Invalid JSON."
233
 
 
236
  return images_state, [], f"❌ Invalid panel number. Must be 1-{len(images_state)}."
237
 
238
  model_id = MODELS.get(model_name, "stabilityai/sdxl-turbo")
239
+ ImageGenerator = get_image_generator_class()
240
  gen = ImageGenerator(model_id)
241
 
242
+ _, updated = gen.regenerate_single_panel(
243
+ idx, prompts, images_state, float(strength), int(base_seed)
244
+ )
245
 
246
  gallery_items = [(img, f"Panel {i+1}") for i, img in enumerate(updated)]
247
  return updated, gallery_items, f"✅ Panel {int(panel_num)} regenerated!"
 
254
  ### JSON to Timelapse Video — One Click Pipeline
255
 
256
  **Workflow:** Paste GPT JSON → Generate Consistent Panels → Smooth Interpolation → Export Video
 
 
257
  """
258
 
259
  with gr.Blocks(
260
  title="⚡ TimeLapseForge",
261
  theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
 
 
 
 
262
  ) as app:
263
 
264
+ # Shared State
265
  images_state = gr.State(value=[])
266
  parsed_json_state = gr.State(value="")
267
 
268
  gr.Markdown(HEADER_MD)
269
 
270
  with gr.Tabs():
271
+
272
+ # ── TAB 1: INPUT ──
273
+ with gr.Tab("📥 Input & Parse"):
 
274
  with gr.Row():
275
  with gr.Column(scale=2):
276
  json_input = gr.Textbox(
277
  label="📋 Paste GPT JSON Output",
278
+ placeholder='Paste the complete JSON here...\n{\n "project_metadata": {...},\n "panels": [...]\n}',
279
  lines=15,
 
280
  )
281
  with gr.Column(scale=1):
282
+ gr.Markdown("**— OR — Quick Text:**")
283
  quick_text = gr.Textbox(
284
  label="⚡ Quick Text Command",
285
+ placeholder="e.g., Restore a rusty 1969 Camaro SS",
286
  lines=3,
287
  )
288
  with gr.Row():
289
+ quick_panels = gr.Slider(
290
+ 8, 40, value=20, step=1, label="Panels"
291
+ )
292
  quick_mode = gr.Dropdown(
293
+ ["restoration", "creation"],
294
+ value="restoration",
295
+ label="Mode",
296
  )
297
 
298
  parse_btn = gr.Button("🔍 Parse & Preview", variant="primary", size="lg")
299
+ parse_status = gr.Markdown("")
 
300
  prompt_table = gr.Dataframe(
301
  headers=["#", "Phase", "Title", "Prompt Preview"],
302
  label="📊 Extracted Panels",
 
304
  wrap=True,
305
  )
306
 
307
+ # ── TAB 2: GENERATE ──
308
+ with gr.Tab("🎨 Generate Panels"):
 
 
309
  with gr.Row():
310
  with gr.Column(scale=1):
311
  model_choice = gr.Dropdown(
 
315
  )
316
  strength = gr.Slider(
317
  0.15, 0.70, value=0.38, step=0.01,
318
+ label="🔄 Change Strength",
319
+ info="Lower = more consistent between panels",
320
  )
321
  base_seed = gr.Number(value=42, label="🌱 Base Seed", precision=0)
322
  steps = gr.Slider(
323
  0, 50, value=0, step=1,
324
+ label="🔢 Steps (0=auto)",
325
  )
326
  guidance = gr.Slider(
327
  -1, 15, value=-1, step=0.5,
328
+ label="🎯 Guidance (-1=auto)",
329
  )
330
  ref_image = gr.Image(
331
+ label="🖼️ Reference Image (optional)", type="pil",
 
332
  )
333
 
334
  with gr.Column(scale=3):
 
342
  rows=3,
343
  height=450,
344
  object_fit="contain",
 
345
  )
346
 
347
  with gr.Accordion("🔄 Regenerate Single Panel", open=False):
348
  with gr.Row():
349
+ regen_panel_num = gr.Number(
350
+ value=1, label="Panel Number", precision=0
351
+ )
352
+ regen_btn = gr.Button("🔄 Regenerate", variant="secondary")
353
  regen_status = gr.Markdown("")
354
 
355
+ # ── TAB 3: VIDEO ──
356
+ with gr.Tab("🎬 Create Video"):
 
 
357
  with gr.Row():
358
  with gr.Column(scale=1):
359
+ gr.Markdown("### 🎞️ Interpolation")
360
  interp_mult = gr.Slider(
361
  1, 8, value=3, step=1,
362
  label="Frame Multiplier",
363
+ info="1=none, 3=smooth, 6=ultra",
364
  )
365
  interp_method = gr.Dropdown(
366
  ["blend", "flow"], value="blend",
367
+ label="Method",
 
368
  )
369
 
370
+ gr.Markdown("### 🎬 Video")
371
  vid_fps = gr.Slider(12, 60, value=24, step=1, label="FPS")
372
  frame_hold = gr.Slider(
373
+ 0.3, 5.0, value=1.5, step=0.1,
374
+ label="Seconds per Panel",
375
  )
376
 
377
  gr.Markdown("### 🏷️ Overlays")
378
+ add_labels = gr.Checkbox(value=True, label="Timestamp labels")
379
+ add_progress_bar = gr.Checkbox(value=True, label="Progress bar")
380
+ add_bookend = gr.Checkbox(value=True, label="BEFORE / AFTER")
381
 
382
  gr.Markdown("### 🎵 Audio")
383
+ music_file = gr.Audio(
384
+ label="Background Music", type="filepath"
385
+ )
386
 
387
  gr.Markdown("### 📦 Export")
388
+ export_gif = gr.Checkbox(value=False, label="Also export GIF")
389
 
390
  with gr.Column(scale=2):
391
  assemble_btn = gr.Button(
392
+ "🎬 Interpolate & Create Video",
393
+ variant="primary",
394
+ size="lg",
395
  )
396
  vid_status = gr.Markdown("")
397
+ video_output = gr.Video(label="🎥 Timelapse Video")
398
+ comparison_output = gr.Image(label="📊 Before / After")
 
399
  gif_output = gr.File(label="📦 GIF Download")
400
 
401
  # ── Event Handlers ──
 
402
  parse_btn.click(
403
  fn=parse_input,
404
  inputs=[json_input, quick_text, quick_panels, quick_mode],