Adnan commited on
Commit
b7b43b1
·
verified ·
1 Parent(s): 294de92

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +259 -214
app.py CHANGED
@@ -1,131 +1,124 @@
1
  """
2
- ⚡ TimeLapseForge — JSON to Timelapse Video in One Click
3
- Main Gradio Application for Hugging Face Spaces.
4
  """
5
 
6
  import os
7
  import json
8
- import tempfile
9
  import gradio as gr
10
  import numpy as np
11
  from PIL import Image
12
  from typing import List, Optional
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()
24
 
25
  # ─── Constants ───────────────────────────────────────────────
26
- MODELS = {
27
- "SDXL Turbo (Fast, 512px)": "stabilityai/sdxl-turbo",
28
- "SDXL Base (HQ, 1024px)": "stabilityai/stable-diffusion-xl-base-1.0",
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
55
- ]
56
- return (
57
- summary,
58
- table_data,
59
- json.dumps(data, indent=2),
60
- gr.update(visible=True),
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...")
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
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 = []
@@ -133,39 +126,30 @@ def generate_panels(
133
  label = prompts[i]["panel_title"] if i < len(prompts) else f"Panel {i+1}"
134
  gallery_items.append((img, label))
135
 
136
- status = f"✅ Generated {len(images)} panels successfully!"
137
- return gallery_items, images, status
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...")
154
  if interp_multiplier > 1:
155
- def interp_cb(current, total):
156
- p = 0.1 + (current / total) * 0.3
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:
@@ -173,57 +157,48 @@ def interpolate_and_assemble(
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]
182
  except Exception:
183
- labels = None
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,
197
- add_progress=add_progress_bar,
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:
226
- return images_state, [], "❌ No images or JSON found."
227
 
228
  try:
229
  data = json.loads(parsed_json)
@@ -233,172 +208,239 @@ def regenerate_single(
233
 
234
  idx = int(panel_num) - 1
235
  if idx < 0 or idx >= len(images_state):
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!"
248
 
249
 
250
- # ─── Gradio UI ───────────────────────────────────────────────
 
 
251
 
252
- HEADER_MD = """
253
- # ⚡ TimeLapseForge
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",
303
- visible=False,
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(
312
- list(MODELS.keys()),
313
- value="SDXL Turbo (Fast, 512px)",
314
- label="🤖 Model",
 
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):
335
  generate_btn = gr.Button(
336
- "🚀 Generate All Panels", variant="primary", size="lg"
337
- )
338
  gen_status = gr.Markdown("")
339
  gallery = gr.Gallery(
340
  label="🖼️ Generated Panels",
341
- columns=5,
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],
@@ -408,8 +450,10 @@ with gr.Blocks(
408
  generate_btn.click(
409
  fn=generate_panels,
410
  inputs=[
411
- parsed_json_state, model_choice, strength, base_seed,
412
- steps, guidance, ref_image,
 
 
413
  ],
414
  outputs=[gallery, images_state, gen_status],
415
  )
@@ -417,8 +461,10 @@ with gr.Blocks(
417
  regen_btn.click(
418
  fn=regenerate_single,
419
  inputs=[
420
- regen_panel_num, parsed_json_state, images_state,
421
- model_choice, strength, base_seed,
 
 
422
  ],
423
  outputs=[images_state, gallery, regen_status],
424
  )
@@ -434,6 +480,5 @@ with gr.Blocks(
434
  )
435
 
436
 
437
- # ─── Launch ──────────────────────────────────────────────────
438
  if __name__ == "__main__":
439
  app.launch()
 
1
  """
2
+ ⚡ TimeLapseForge v2.0 — Universal API Edition
3
+ Supports 13+ image generation APIs + local models.
4
  """
5
 
6
  import os
7
  import json
 
8
  import gradio as gr
9
  import numpy as np
10
  from PIL import Image
11
  from typing import List, Optional
12
 
 
13
  from prompt_parser import PromptParser, QuickGenerator
14
  from frame_interpolator import FrameInterpolator
15
  from video_assembler import VideoAssembler
16
+ from api_providers import (
17
+ PROVIDERS, PROVIDER_DISPLAY_NAMES,
18
+ get_models_for_provider, get_provider_info,
19
+ )
20
 
21
+ # ─── Initialize ──────────────────────────────────────────────
22
  prompt_parser = PromptParser()
23
  quick_gen = QuickGenerator()
24
  interpolator = FrameInterpolator()
25
  assembler = VideoAssembler()
26
 
27
  # ─── Constants ───────────────────────────────────────────────
28
+ LOCAL_MODELS = {
29
+ "SDXL Turbo (Fast)": "stabilityai/sdxl-turbo",
30
+ "SDXL Base (HQ)": "stabilityai/stable-diffusion-xl-base-1.0",
31
+ "SD 1.5 (Light)": "runwayml/stable-diffusion-v1-5",
32
  }
33
 
34
+ PROVIDER_CHOICES = [p["display_name"] for p in get_provider_info()]
 
35
 
 
 
 
 
 
 
36
 
37
+ # ─── Helper: Get provider key from display name ─────────────
38
+ def display_to_key(display_name):
39
+ return PROVIDER_DISPLAY_NAMES.get(display_name, "openai")
40
 
41
+
42
+ # ─── Dynamic Model List ─────────────────────────────────────
43
+ def update_model_list(provider_display_name):
44
+ key = display_to_key(provider_display_name)
45
+ models = get_models_for_provider(key)
46
+ if models:
47
+ return gr.update(choices=models, value=models[0])
48
+ return gr.update(choices=["custom"], value="custom")
49
+
50
+
51
+ # ─── Parse Input ─────────────────────────────────────────────
52
  def parse_input(json_text, quick_text, num_panels, mode):
 
53
  if json_text and json_text.strip():
54
  result = prompt_parser.parse(json_text)
55
  if result["success"]:
56
  data = result["data"]
57
  prompts = prompt_parser.extract_prompts(data)
58
  summary = prompt_parser.get_summary(data)
59
+ table = [[p["panel_id"], p["phase"], p["panel_title"],
60
+ p["main_prompt"][:100] + "..."] for p in prompts]
61
+ return summary, table, json.dumps(data, indent=2), gr.update(visible=True)
62
+ return f"❌ Parse Error: {result['error']}", [], "", gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  elif quick_text and quick_text.strip():
64
+ data = quick_gen.generate(quick_text, int(num_panels), mode)
65
  prompts = prompt_parser.extract_prompts(data)
66
  summary = prompt_parser.get_summary(data)
67
+ table = [[p["panel_id"], p["phase"], p["panel_title"],
68
+ p["main_prompt"][:100] + "..."] for p in prompts]
69
+ return (summary + "\n\n⚠️ *Quick text — use GPT JSON for better results*",
70
+ table, json.dumps(data, indent=2), gr.update(visible=True))
71
+ return "⚠️ Paste JSON or enter quick text.", [], "", gr.update(visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
 
74
+ # ─── Generate Panels ─────────────────────────────────────────
75
  def generate_panels(
76
+ parsed_json, gen_mode, local_model, provider_name, api_key,
77
+ api_model, custom_base_url, custom_endpoint_url,
78
+ strength, base_seed, steps, guidance, width, height,
79
+ ref_image, progress=gr.Progress(),
80
  ):
 
81
  if not parsed_json:
82
+ return [], [], "❌ No JSON. Parse input first."
83
 
84
  try:
85
  data = json.loads(parsed_json)
86
  except json.JSONDecodeError:
87
+ return [], [], "❌ Invalid JSON."
88
 
89
  prompts = prompt_parser.extract_prompts(data)
90
  if not prompts:
91
+ return [], [], "❌ No panels found."
92
 
93
+ from image_generator import ImageGenerator
 
 
94
 
95
+ if gen_mode == "🖥️ Local (Free GPU)":
96
+ model_id = LOCAL_MODELS.get(local_model, "stabilityai/sdxl-turbo")
97
+ gen = ImageGenerator(mode="local", local_model_id=model_id)
98
+ else:
99
+ pkey = display_to_key(provider_name)
100
+ if not api_key or not api_key.strip():
101
+ return [], [], f"❌ API key required for {provider_name}"
102
+ gen = ImageGenerator(
103
+ mode="api", provider_name=pkey, api_key=api_key.strip(),
104
+ api_model=api_model, custom_base_url=custom_base_url,
105
+ custom_endpoint_url=custom_endpoint_url,
106
+ )
107
+
108
+ progress(0, desc="Starting generation...")
109
 
110
+ def cb(current, total):
111
+ progress(current / total, desc=f"Panel {current}/{total}")
112
 
113
  gen_steps = int(steps) if steps and int(steps) > 0 else None
114
  gen_guidance = float(guidance) if guidance is not None and float(guidance) >= 0 else None
115
+ w = int(width) if width and int(width) > 0 else None
116
+ h = int(height) if height and int(height) > 0 else None
117
 
118
  images = gen.generate_all_panels(
119
+ prompts=prompts, strength=float(strength), base_seed=int(base_seed),
120
+ steps=gen_steps, guidance=gen_guidance, width=w, height=h,
121
+ reference_image=ref_image, progress_callback=cb,
 
 
 
 
122
  )
123
 
124
  gallery_items = []
 
126
  label = prompts[i]["panel_title"] if i < len(prompts) else f"Panel {i+1}"
127
  gallery_items.append((img, label))
128
 
129
+ return gallery_items, images, f"✅ Generated {len(images)} panels!"
 
130
 
131
 
132
+ # ─── Interpolate & Assemble ──────────────────────────────────
133
  def interpolate_and_assemble(
134
  images_state, parsed_json, interp_multiplier, interp_method,
135
  fps, hold_seconds, add_labels, add_progress_bar, add_bookend,
136
  music_file, export_gif, progress=gr.Progress(),
137
  ):
 
138
  if not images_state:
139
+ return None, None, None, "❌ No images. Generate first."
140
 
141
  images = images_state
142
  interp_multiplier = int(interp_multiplier)
143
 
144
+ progress(0.1, desc="Interpolating...")
 
145
  if interp_multiplier > 1:
146
+ def icb(c, t):
147
+ progress(0.1 + (c/t)*0.3, desc=f"Interpolating {c}/{t}")
148
+ smooth = interpolator.interpolate_sequence(
149
+ images, interp_multiplier, interp_method, icb)
 
 
 
 
 
 
150
  else:
151
+ smooth = list(images)
152
 
 
153
  labels = None
154
  if add_labels and parsed_json:
155
  try:
 
157
  prompts = prompt_parser.extract_prompts(data)
158
  if interp_multiplier > 1:
159
  labels = []
160
+ for p in prompts:
161
  labels.append(p.get("timestamp_label", ""))
162
  labels.extend([""] * interp_multiplier)
163
+ labels = labels[:len(smooth)]
164
  else:
165
  labels = [p.get("timestamp_label", "") for p in prompts]
166
  except Exception:
167
+ pass
168
 
 
169
  progress(0.5, desc="Assembling video...")
170
+ adj_hold = float(hold_seconds) / max(interp_multiplier, 1) if interp_multiplier > 1 else float(hold_seconds)
 
 
171
 
172
  video_path = assembler.create_video(
173
+ smooth, fps=int(fps), hold_seconds=adj_hold,
174
+ add_labels=add_labels, labels=labels,
175
+ add_progress=add_progress_bar, add_bookend_labels=add_bookend,
 
 
 
 
176
  )
177
 
 
178
  if music_file:
179
  progress(0.8, desc="Adding audio...")
180
  video_path = assembler.add_audio_to_video(video_path, music_file)
181
 
182
+ progress(0.9, desc="Comparison...")
 
183
  comparison = assembler.create_comparison_image(images[0], images[-1])
184
 
 
185
  gif_path = None
186
  if export_gif:
187
+ progress(0.95, desc="GIF...")
188
+ gif_path = assembler.create_gif(images)
189
 
190
+ return video_path, comparison, gif_path, f"✅ Done! {len(smooth)} frames at {int(fps)}fps"
 
 
191
 
192
 
193
+ # ─── Regenerate Single ───────────────────────────────────────
194
  def regenerate_single(
195
+ panel_num, parsed_json, images_state,
196
+ gen_mode, local_model, provider_name, api_key,
197
+ api_model, custom_base_url, custom_endpoint_url,
198
+ strength, base_seed,
199
  ):
 
200
  if not images_state or not parsed_json:
201
+ return images_state, [], "❌ No data."
202
 
203
  try:
204
  data = json.loads(parsed_json)
 
208
 
209
  idx = int(panel_num) - 1
210
  if idx < 0 or idx >= len(images_state):
211
+ return images_state, [], f"❌ Panel must be 1-{len(images_state)}"
212
 
213
+ from image_generator import ImageGenerator
214
+
215
+ if gen_mode == "🖥️ Local (Free GPU)":
216
+ model_id = LOCAL_MODELS.get(local_model, "stabilityai/sdxl-turbo")
217
+ gen = ImageGenerator(mode="local", local_model_id=model_id)
218
+ else:
219
+ pkey = display_to_key(provider_name)
220
+ gen = ImageGenerator(
221
+ mode="api", provider_name=pkey, api_key=api_key.strip(),
222
+ api_model=api_model, custom_base_url=custom_base_url,
223
+ custom_endpoint_url=custom_endpoint_url,
224
+ )
225
 
226
  _, updated = gen.regenerate_single_panel(
227
+ idx, prompts, images_state, float(strength), int(base_seed))
 
228
 
229
+ gallery = [(img, f"Panel {i+1}") for i, img in enumerate(updated)]
230
+ return updated, gallery, f"✅ Panel {int(panel_num)} regenerated!"
231
 
232
 
233
+ # ═══════════════════════════════════════════
234
+ # GRADIO UI
235
+ # ════════════════════��══════════════════════
236
 
237
+ HEADER = """
238
+ # ⚡ TimeLapseForge v2.0 — Universal API Edition
239
+ ### Supports **13+ AI Image APIs** — Enter YOUR API key, select model, generate!
240
 
241
+ **OpenAI** · **Stability AI** · **Replicate** · **Together AI** · **Fal.ai** · **Google Gemini**
242
+ · **HuggingFace** · **xAI Grok** · **Fireworks** · **Ideogram** · **Leonardo** · **Custom API** · **Local GPU**
243
  """
244
 
245
+ API_HELP = """
246
+ ### 🔑 Where to Get API Keys
247
+
248
+ | Provider | Get Key | Free Tier? |
249
+ |---|---|---|
250
+ | OpenAI | [platform.openai.com](https://platform.openai.com/api-keys) | Pay-per-use |
251
+ | Stability AI | [platform.stability.ai](https://platform.stability.ai/account/keys) | 25 free credits |
252
+ | Replicate | [replicate.com](https://replicate.com/account/api-tokens) | Free tier |
253
+ | Together AI | [api.together.xyz](https://api.together.xyz/settings/api-keys) | $5 free |
254
+ | Fal.ai | [fal.ai/dashboard](https://fal.ai/dashboard/keys) | $10 free |
255
+ | Google Gemini | [aistudio.google.com](https://aistudio.google.com/apikey) | Free! |
256
+ | HuggingFace | [huggingface.co](https://huggingface.co/settings/tokens) | Free! |
257
+ | xAI Grok | [console.x.ai](https://console.x.ai/team/default/api-keys) | $25 free/mo |
258
+ | Fireworks | [fireworks.ai](https://fireworks.ai/account/api-keys) | $1 free |
259
+ | Ideogram | [ideogram.ai](https://ideogram.ai/manage-api) | Pay-per-use |
260
+ | Leonardo | [leonardo.ai](https://app.leonardo.ai/api-access) | 150 free/day |
261
+ """
262
+
263
+
264
  with gr.Blocks(
265
+ title="⚡ TimeLapseForge v2.0",
266
  theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
267
  ) as app:
268
 
 
269
  images_state = gr.State(value=[])
270
  parsed_json_state = gr.State(value="")
271
 
272
+ gr.Markdown(HEADER)
273
 
274
  with gr.Tabs():
275
 
276
+ # ════════════════════════════════════════
277
+ # TAB 1: INPUT
278
+ # ════════════════════════════════════════
279
  with gr.Tab("📥 Input & Parse"):
280
  with gr.Row():
281
  with gr.Column(scale=2):
282
  json_input = gr.Textbox(
283
+ label="📋 Paste GPT JSON",
284
+ placeholder='{\n "project_metadata": {...},\n "panels": [...]\n}',
285
  lines=15,
286
  )
287
  with gr.Column(scale=1):
288
  gr.Markdown("**— OR — Quick Text:**")
289
  quick_text = gr.Textbox(
290
+ label="⚡ Quick Command",
291
+ placeholder="Restore a rusty 1969 Camaro SS",
292
  lines=3,
293
  )
294
+ quick_panels = gr.Slider(8, 40, value=20, step=1, label="Panels")
295
+ quick_mode = gr.Dropdown(
296
+ ["restoration", "creation"], value="restoration", label="Mode")
 
 
 
 
 
 
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="📊 Panels", visible=False, wrap=True)
 
 
 
303
 
304
+ # ════════════════════════════════════════
305
+ # TAB 2: GENERATE
306
+ # ════════════════════════════════════════
307
  with gr.Tab("🎨 Generate Panels"):
308
  with gr.Row():
309
  with gr.Column(scale=1):
310
+ # --- Generation Mode ---
311
+ gen_mode = gr.Radio(
312
+ ["🖥️ Local (Free GPU)", "🌐 API (Your Key)"],
313
+ value="🌐 API (Your Key)",
314
+ label="Generation Mode",
315
  )
316
+
317
+ # --- Local Settings ---
318
+ with gr.Group(visible=False) as local_group:
319
+ gr.Markdown("### 🖥️ Local Model")
320
+ local_model = gr.Dropdown(
321
+ list(LOCAL_MODELS.keys()),
322
+ value="SDXL Turbo (Fast)",
323
+ label="Model",
324
+ )
325
+
326
+ # --- API Settings ---
327
+ with gr.Group(visible=True) as api_group:
328
+ gr.Markdown("### 🌐 API Provider")
329
+ provider_dropdown = gr.Dropdown(
330
+ PROVIDER_CHOICES,
331
+ value=PROVIDER_CHOICES[0],
332
+ label="Provider",
333
+ )
334
+ api_key_input = gr.Textbox(
335
+ label="🔑 API Key",
336
+ placeholder="Paste your API key here...",
337
+ type="password",
338
+ )
339
+ api_model_dropdown = gr.Dropdown(
340
+ label="🤖 Model",
341
+ choices=get_models_for_provider("openai"),
342
+ value="dall-e-3",
343
+ allow_custom_value=True,
344
+ )
345
+ with gr.Accordion("🔧 Custom API Settings", open=False):
346
+ custom_base_url = gr.Textbox(
347
+ label="Custom Base URL (OpenAI-compatible)",
348
+ placeholder="https://your-api.com/v1",
349
+ )
350
+ custom_endpoint_url = gr.Textbox(
351
+ label="Direct Endpoint URL",
352
+ placeholder="https://your-api.com/generate",
353
+ )
354
+
355
+ # --- Common Settings ---
356
+ gr.Markdown("### ⚙️ Generation Settings")
357
  strength = gr.Slider(
358
  0.15, 0.70, value=0.38, step=0.01,
359
  label="🔄 Change Strength",
360
+ info="Lower = more consistent",
 
 
 
 
 
 
 
 
 
 
 
 
361
  )
362
+ base_seed = gr.Number(value=42, label="🌱 Seed", precision=0)
363
+ with gr.Row():
364
+ steps = gr.Slider(0, 50, value=0, step=1, label="Steps (0=auto)")
365
+ guidance = gr.Slider(-1, 15, value=-1, step=0.5, label="CFG (-1=auto)")
366
+ with gr.Row():
367
+ img_width = gr.Number(value=1024, label="Width", precision=0)
368
+ img_height = gr.Number(value=1024, label="Height", precision=0)
369
+
370
+ ref_image = gr.Image(label="🖼️ Reference Image", type="pil")
371
 
372
  with gr.Column(scale=3):
373
  generate_btn = gr.Button(
374
+ "🚀 Generate All Panels", variant="primary", size="lg")
 
375
  gen_status = gr.Markdown("")
376
  gallery = gr.Gallery(
377
  label="🖼️ Generated Panels",
378
+ columns=5, rows=3, height=450, object_fit="contain")
 
 
 
 
379
 
380
  with gr.Accordion("🔄 Regenerate Single Panel", open=False):
381
  with gr.Row():
382
+ regen_num = gr.Number(value=1, label="Panel #", precision=0)
 
 
383
  regen_btn = gr.Button("🔄 Regenerate", variant="secondary")
384
  regen_status = gr.Markdown("")
385
 
386
+ # ═════════════════════════���══════════════
387
+ # TAB 3: VIDEO
388
+ # ════════════════════════════════════════
389
  with gr.Tab("🎬 Create Video"):
390
  with gr.Row():
391
  with gr.Column(scale=1):
392
  gr.Markdown("### 🎞️ Interpolation")
393
+ interp_mult = gr.Slider(1, 8, value=3, step=1, label="Frame Multiplier")
394
+ interp_method = gr.Dropdown(["blend", "flow"], value="blend", label="Method")
 
 
 
 
 
 
 
395
 
396
  gr.Markdown("### 🎬 Video")
397
  vid_fps = gr.Slider(12, 60, value=24, step=1, label="FPS")
398
+ frame_hold = gr.Slider(0.3, 5.0, value=1.5, step=0.1, label="Sec/Panel")
 
 
 
399
 
400
  gr.Markdown("### 🏷️ Overlays")
401
+ add_labels = gr.Checkbox(value=True, label="Timestamps")
402
  add_progress_bar = gr.Checkbox(value=True, label="Progress bar")
403
+ add_bookend = gr.Checkbox(value=True, label="BEFORE/AFTER")
404
 
405
  gr.Markdown("### 🎵 Audio")
406
+ music_file = gr.Audio(label="Background Music", type="filepath")
407
+ export_gif = gr.Checkbox(value=False, label="Export GIF too")
 
 
 
 
408
 
409
  with gr.Column(scale=2):
410
  assemble_btn = gr.Button(
411
+ "🎬 Create Video", variant="primary", size="lg")
 
 
 
412
  vid_status = gr.Markdown("")
413
+ video_output = gr.Video(label="🎥 Timelapse")
414
+ comparison_output = gr.Image(label="📊 Before/After")
415
+ gif_output = gr.File(label="📦 GIF")
416
+
417
+ # ════════════════════════════════════════
418
+ # TAB 4: API KEYS HELP
419
+ # ════════════════════════════════════════
420
+ with gr.Tab("🔑 API Keys Guide"):
421
+ gr.Markdown(API_HELP)
422
+
423
+ # ── Dynamic UI Updates ──
424
+
425
+ def toggle_mode(mode):
426
+ if mode == "🖥️ Local (Free GPU)":
427
+ return gr.update(visible=True), gr.update(visible=False)
428
+ return gr.update(visible=False), gr.update(visible=True)
429
+
430
+ gen_mode.change(
431
+ fn=toggle_mode,
432
+ inputs=[gen_mode],
433
+ outputs=[local_group, api_group],
434
+ )
435
+
436
+ provider_dropdown.change(
437
+ fn=update_model_list,
438
+ inputs=[provider_dropdown],
439
+ outputs=[api_model_dropdown],
440
+ )
441
+
442
+ # ── Core Event Handlers ──
443
 
 
444
  parse_btn.click(
445
  fn=parse_input,
446
  inputs=[json_input, quick_text, quick_panels, quick_mode],
 
450
  generate_btn.click(
451
  fn=generate_panels,
452
  inputs=[
453
+ parsed_json_state, gen_mode, local_model, provider_dropdown,
454
+ api_key_input, api_model_dropdown, custom_base_url,
455
+ custom_endpoint_url, strength, base_seed, steps, guidance,
456
+ img_width, img_height, ref_image,
457
  ],
458
  outputs=[gallery, images_state, gen_status],
459
  )
 
461
  regen_btn.click(
462
  fn=regenerate_single,
463
  inputs=[
464
+ regen_num, parsed_json_state, images_state,
465
+ gen_mode, local_model, provider_dropdown, api_key_input,
466
+ api_model_dropdown, custom_base_url, custom_endpoint_url,
467
+ strength, base_seed,
468
  ],
469
  outputs=[images_state, gallery, regen_status],
470
  )
 
480
  )
481
 
482
 
 
483
  if __name__ == "__main__":
484
  app.launch()