Adnan commited on
Commit
2f96b53
·
verified ·
1 Parent(s): cf5507a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +191 -151
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- ⚡ TimeLapseForge v2.0 — Universal API Edition
3
  Supports 13+ image generation APIs + local models.
4
  """
5
 
@@ -18,13 +18,13 @@ from api_providers import (
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",
@@ -34,12 +34,10 @@ LOCAL_MODELS = {
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)
@@ -48,7 +46,6 @@ def update_model_list(provider_display_name):
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)
@@ -56,22 +53,41 @@ def parse_input(json_text, quick_text, num_panels, mode):
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,
@@ -79,26 +95,26 @@ def generate_panels(
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,
@@ -108,7 +124,7 @@ def generate_panels(
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
@@ -123,20 +139,19 @@ def generate_panels(
123
 
124
  gallery_items = []
125
  for i, img in enumerate(images):
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)
@@ -144,7 +159,7 @@ def interpolate_and_assemble(
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:
@@ -167,7 +182,9 @@ def interpolate_and_assemble(
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,
@@ -179,18 +196,21 @@ def interpolate_and_assemble(
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,
@@ -198,21 +218,21 @@ def regenerate_single(
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)
205
  prompts = prompt_parser.extract_prompts(data)
206
  except Exception:
207
- return images_state, [], "❌ Invalid 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:
@@ -226,58 +246,49 @@ def regenerate_single(
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
- # ════════════════════════════════════════
247
- # TAB 4: API KEYS HELP
248
- # ════════════════════════════════════════
249
- with gr.Tab("🔑 API Keys Guide"):
250
- gr.Markdown("""
251
- ### 🔑 Where to Get API Keys
252
-
253
- | Provider | Get Key | Needs Extra Package? |
254
- |---|---|---|
255
- | **Stability AI** | [platform.stability.ai](https://platform.stability.ai/account/keys) | ❌ No (uses requests) |
256
- | **HuggingFace** | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) | ❌ No (uses requests) |
257
- | **Fireworks AI** | [fireworks.ai](https://fireworks.ai/account/api-keys) | ❌ No (uses requests) |
258
- | **Ideogram** | [ideogram.ai](https://ideogram.ai/manage-api) | ❌ No (uses requests) |
259
- | **Leonardo AI** | [leonardo.ai](https://app.leonardo.ai/api-access) | ❌ No (uses requests) |
260
- | **Direct URL** | Any REST API | ❌ No (uses requests) |
261
- | **OpenAI** | [platform.openai.com](https://platform.openai.com/api-keys) | ✅ `pip install openai` |
262
- | **xAI Grok** | [console.x.ai](https://console.x.ai) | ✅ `pip install openai` |
263
- | **Replicate** | [replicate.com](https://replicate.com/account/api-tokens) | ✅ `pip install replicate` |
264
- | **Together AI** | [api.together.xyz](https://api.together.xyz/settings/api-keys) | ✅ `pip install together` |
265
- | **Fal.ai** | [fal.ai/dashboard](https://fal.ai/dashboard/keys) | ✅ `pip install fal-client` |
266
- | **Google Gemini** | [aistudio.google.com](https://aistudio.google.com/apikey) | ✅ `pip install google-generativeai` |
267
-
268
- > **💡 Providers marked ❌ work out of the box!** No extra packages needed.
269
- > Providers marked ✅ will show a friendly error if the package isn't installed.
270
-
271
- ### 🏆 Recommended (Zero Setup)
272
- 1. **Stability AI** — Best quality, supports img2img (consistency!)
273
- 2. **HuggingFace** — Free tier, many models
274
- 3. **Fireworks AI** — Fast Flux generation
275
- 4. **Ideogram** — Great text-in-image
276
- """)
277
 
278
 
279
  with gr.Blocks(
280
- title="⚡ TimeLapseForge v2.0",
281
  theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
282
  ) as app:
283
 
@@ -288,76 +299,74 @@ with gr.Blocks(
288
 
289
  with gr.Tabs():
290
 
291
- # ════════════════════════════════════════
292
- # TAB 1: INPUT
293
- # ════════════════════════════════════════
294
- with gr.Tab("📥 Input & Parse"):
295
  with gr.Row():
296
  with gr.Column(scale=2):
297
  json_input = gr.Textbox(
298
- label="📋 Paste GPT JSON",
299
  placeholder='{\n "project_metadata": {...},\n "panels": [...]\n}',
300
  lines=15,
301
  )
302
  with gr.Column(scale=1):
303
- gr.Markdown("**— OR — Quick Text:**")
304
  quick_text = gr.Textbox(
305
- label="⚡ Quick Command",
306
  placeholder="Restore a rusty 1969 Camaro SS",
307
  lines=3,
308
  )
309
- quick_panels = gr.Slider(8, 40, value=20, step=1, label="Panels")
 
 
310
  quick_mode = gr.Dropdown(
311
- ["restoration", "creation"], value="restoration", label="Mode")
 
 
312
 
313
- parse_btn = gr.Button("🔍 Parse & Preview", variant="primary", size="lg")
314
  parse_status = gr.Markdown("")
315
  prompt_table = gr.Dataframe(
316
  headers=["#", "Phase", "Title", "Prompt Preview"],
317
- label="📊 Panels", visible=False, wrap=True)
 
318
 
319
- # ════════════════════════════════════════
320
- # TAB 2: GENERATE
321
- # ════════════════════════════════════════
322
- with gr.Tab("🎨 Generate Panels"):
323
  with gr.Row():
324
  with gr.Column(scale=1):
325
- # --- Generation Mode ---
326
  gen_mode = gr.Radio(
327
- ["🖥️ Local (Free GPU)", "🌐 API (Your Key)"],
328
- value="🌐 API (Your Key)",
329
  label="Generation Mode",
330
  )
331
 
332
- # --- Local Settings ---
333
  with gr.Group(visible=False) as local_group:
334
- gr.Markdown("### 🖥️ Local Model")
335
  local_model = gr.Dropdown(
336
  list(LOCAL_MODELS.keys()),
337
  value="SDXL Turbo (Fast)",
338
  label="Model",
339
  )
340
 
341
- # --- API Settings ---
342
  with gr.Group(visible=True) as api_group:
343
- gr.Markdown("### 🌐 API Provider")
344
  provider_dropdown = gr.Dropdown(
345
  PROVIDER_CHOICES,
346
  value=PROVIDER_CHOICES[0],
347
  label="Provider",
348
  )
349
  api_key_input = gr.Textbox(
350
- label="🔑 API Key",
351
  placeholder="Paste your API key here...",
352
  type="password",
353
  )
354
  api_model_dropdown = gr.Dropdown(
355
- label="🤖 Model",
356
  choices=get_models_for_provider("openai"),
357
  value="dall-e-3",
358
  allow_custom_value=True,
359
  )
360
- with gr.Accordion("🔧 Custom API Settings", open=False):
361
  custom_base_url = gr.Textbox(
362
  label="Custom Base URL (OpenAI-compatible)",
363
  placeholder="https://your-api.com/v1",
@@ -367,78 +376,109 @@ with gr.Blocks(
367
  placeholder="https://your-api.com/generate",
368
  )
369
 
370
- # --- Common Settings ---
371
- gr.Markdown("### ⚙️ Generation Settings")
372
  strength = gr.Slider(
373
  0.15, 0.70, value=0.38, step=0.01,
374
- label="🔄 Change Strength",
375
- info="Lower = more consistent",
376
  )
377
- base_seed = gr.Number(value=42, label="🌱 Seed", precision=0)
378
  with gr.Row():
379
- steps = gr.Slider(0, 50, value=0, step=1, label="Steps (0=auto)")
380
- guidance = gr.Slider(-1, 15, value=-1, step=0.5, label="CFG (-1=auto)")
 
 
 
 
381
  with gr.Row():
382
- img_width = gr.Number(value=1024, label="Width", precision=0)
383
- img_height = gr.Number(value=1024, label="Height", precision=0)
 
 
 
 
384
 
385
- ref_image = gr.Image(label="🖼️ Reference Image", type="pil")
 
 
386
 
387
  with gr.Column(scale=3):
388
  generate_btn = gr.Button(
389
- "🚀 Generate All Panels", variant="primary", size="lg")
 
390
  gen_status = gr.Markdown("")
391
  gallery = gr.Gallery(
392
- label="🖼️ Generated Panels",
393
- columns=5, rows=3, height=450, object_fit="contain")
 
 
394
 
395
- with gr.Accordion("🔄 Regenerate Single Panel", open=False):
396
  with gr.Row():
397
- regen_num = gr.Number(value=1, label="Panel #", precision=0)
398
- regen_btn = gr.Button("🔄 Regenerate", variant="secondary")
 
 
 
 
399
  regen_status = gr.Markdown("")
400
 
401
- # ════════════════════════════════════════
402
- # TAB 3: VIDEO
403
- # ════════════════════════════════════════
404
- with gr.Tab("🎬 Create Video"):
405
  with gr.Row():
406
  with gr.Column(scale=1):
407
- gr.Markdown("### 🎞️ Interpolation")
408
- interp_mult = gr.Slider(1, 8, value=3, step=1, label="Frame Multiplier")
409
- interp_method = gr.Dropdown(["blend", "flow"], value="blend", label="Method")
 
 
 
 
410
 
411
- gr.Markdown("### 🎬 Video")
412
- vid_fps = gr.Slider(12, 60, value=24, step=1, label="FPS")
413
- frame_hold = gr.Slider(0.3, 5.0, value=1.5, step=0.1, label="Sec/Panel")
 
 
 
 
414
 
415
- gr.Markdown("### 🏷️ Overlays")
416
- add_labels = gr.Checkbox(value=True, label="Timestamps")
417
- add_progress_bar = gr.Checkbox(value=True, label="Progress bar")
418
- add_bookend = gr.Checkbox(value=True, label="BEFORE/AFTER")
 
 
 
 
 
 
419
 
420
- gr.Markdown("### 🎵 Audio")
421
- music_file = gr.Audio(label="Background Music", type="filepath")
422
- export_gif = gr.Checkbox(value=False, label="Export GIF too")
 
 
 
 
423
 
424
  with gr.Column(scale=2):
425
  assemble_btn = gr.Button(
426
- "🎬 Create Video", variant="primary", size="lg")
 
427
  vid_status = gr.Markdown("")
428
- video_output = gr.Video(label="🎥 Timelapse")
429
- comparison_output = gr.Image(label="📊 Before/After")
430
- gif_output = gr.File(label="📦 GIF")
431
-
432
- # ════════════════════════════════════════
433
- # TAB 4: API KEYS HELP
434
- # ════════════════════════════════════════
435
- with gr.Tab("🔑 API Keys Guide"):
436
  gr.Markdown(API_HELP)
437
 
438
- # ── Dynamic UI Updates ──
439
 
440
  def toggle_mode(mode):
441
- if mode == "🖥️ Local (Free GPU)":
442
  return gr.update(visible=True), gr.update(visible=False)
443
  return gr.update(visible=False), gr.update(visible=True)
444
 
@@ -454,7 +494,7 @@ with gr.Blocks(
454
  outputs=[api_model_dropdown],
455
  )
456
 
457
- # ── Core Event Handlers ──
458
 
459
  parse_btn.click(
460
  fn=parse_input,
 
1
  """
2
+ TimeLapseForge v2.0 - Universal API Edition
3
  Supports 13+ image generation APIs + local models.
4
  """
5
 
 
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",
 
34
  PROVIDER_CHOICES = [p["display_name"] for p in get_provider_info()]
35
 
36
 
 
37
  def display_to_key(display_name):
38
  return PROVIDER_DISPLAY_NAMES.get(display_name, "openai")
39
 
40
 
 
41
  def update_model_list(provider_display_name):
42
  key = display_to_key(provider_display_name)
43
  models = get_models_for_provider(key)
 
46
  return gr.update(choices=["custom"], value="custom")
47
 
48
 
 
49
  def parse_input(json_text, quick_text, num_panels, mode):
50
  if json_text and json_text.strip():
51
  result = prompt_parser.parse(json_text)
 
53
  data = result["data"]
54
  prompts = prompt_parser.extract_prompts(data)
55
  summary = prompt_parser.get_summary(data)
56
+ table = [
57
+ [p["panel_id"], p["phase"], p["panel_title"],
58
+ p["main_prompt"][:100] + "..."]
59
+ for p in prompts
60
+ ]
61
+ return (
62
+ summary, table,
63
+ json.dumps(data, indent=2),
64
+ gr.update(visible=True),
65
+ )
66
+ return (
67
+ "** Parse Error:** " + str(result["error"]),
68
+ [], "", gr.update(visible=False),
69
+ )
70
  elif quick_text and quick_text.strip():
71
  data = quick_gen.generate(quick_text, int(num_panels), mode)
72
  prompts = prompt_parser.extract_prompts(data)
73
  summary = prompt_parser.get_summary(data)
74
+ table = [
75
+ [p["panel_id"], p["phase"], p["panel_title"],
76
+ p["main_prompt"][:100] + "..."]
77
+ for p in prompts
78
+ ]
79
+ return (
80
+ summary + "\n\n*Quick text mode -- use GPT JSON for better results.*",
81
+ table,
82
+ json.dumps(data, indent=2),
83
+ gr.update(visible=True),
84
+ )
85
+ return (
86
+ "Please paste JSON or enter quick text.",
87
+ [], "", gr.update(visible=False),
88
+ )
89
 
90
 
 
91
  def generate_panels(
92
  parsed_json, gen_mode, local_model, provider_name, api_key,
93
  api_model, custom_base_url, custom_endpoint_url,
 
95
  ref_image, progress=gr.Progress(),
96
  ):
97
  if not parsed_json:
98
+ return [], [], "No JSON found. Parse input first."
99
 
100
  try:
101
  data = json.loads(parsed_json)
102
  except json.JSONDecodeError:
103
+ return [], [], "Invalid JSON."
104
 
105
  prompts = prompt_parser.extract_prompts(data)
106
  if not prompts:
107
+ return [], [], "No panels found in JSON."
108
 
109
  from image_generator import ImageGenerator
110
 
111
+ if gen_mode == "Local (Free GPU)":
112
  model_id = LOCAL_MODELS.get(local_model, "stabilityai/sdxl-turbo")
113
  gen = ImageGenerator(mode="local", local_model_id=model_id)
114
  else:
115
  pkey = display_to_key(provider_name)
116
  if not api_key or not api_key.strip():
117
+ return [], [], "API key required for " + str(provider_name)
118
  gen = ImageGenerator(
119
  mode="api", provider_name=pkey, api_key=api_key.strip(),
120
  api_model=api_model, custom_base_url=custom_base_url,
 
124
  progress(0, desc="Starting generation...")
125
 
126
  def cb(current, total):
127
+ progress(current / total, desc="Panel " + str(current) + "/" + str(total))
128
 
129
  gen_steps = int(steps) if steps and int(steps) > 0 else None
130
  gen_guidance = float(guidance) if guidance is not None and float(guidance) >= 0 else None
 
139
 
140
  gallery_items = []
141
  for i, img in enumerate(images):
142
+ label = prompts[i]["panel_title"] if i < len(prompts) else "Panel " + str(i + 1)
143
  gallery_items.append((img, label))
144
 
145
+ return gallery_items, images, "Generated " + str(len(images)) + " panels successfully!"
146
 
147
 
 
148
  def interpolate_and_assemble(
149
  images_state, parsed_json, interp_multiplier, interp_method,
150
  fps, hold_seconds, add_labels, add_progress_bar, add_bookend,
151
  music_file, export_gif, progress=gr.Progress(),
152
  ):
153
  if not images_state:
154
+ return None, None, None, "No images found. Generate panels first."
155
 
156
  images = images_state
157
  interp_multiplier = int(interp_multiplier)
 
159
  progress(0.1, desc="Interpolating...")
160
  if interp_multiplier > 1:
161
  def icb(c, t):
162
+ progress(0.1 + (c / t) * 0.3, desc="Interpolating " + str(c) + "/" + str(t))
163
  smooth = interpolator.interpolate_sequence(
164
  images, interp_multiplier, interp_method, icb)
165
  else:
 
182
  pass
183
 
184
  progress(0.5, desc="Assembling video...")
185
+ adj_hold = float(hold_seconds)
186
+ if interp_multiplier > 1:
187
+ adj_hold = adj_hold / max(interp_multiplier, 1)
188
 
189
  video_path = assembler.create_video(
190
  smooth, fps=int(fps), hold_seconds=adj_hold,
 
196
  progress(0.8, desc="Adding audio...")
197
  video_path = assembler.add_audio_to_video(video_path, music_file)
198
 
199
+ progress(0.9, desc="Creating comparison...")
200
  comparison = assembler.create_comparison_image(images[0], images[-1])
201
 
202
  gif_path = None
203
  if export_gif:
204
+ progress(0.95, desc="Creating GIF...")
205
  gif_path = assembler.create_gif(images)
206
 
207
+ progress(1.0, desc="Done!")
208
+ return (
209
+ video_path, comparison, gif_path,
210
+ "Video created! " + str(len(smooth)) + " frames at " + str(int(fps)) + "fps",
211
+ )
212
 
213
 
 
214
  def regenerate_single(
215
  panel_num, parsed_json, images_state,
216
  gen_mode, local_model, provider_name, api_key,
 
218
  strength, base_seed,
219
  ):
220
  if not images_state or not parsed_json:
221
+ return images_state, [], "No data available."
222
 
223
  try:
224
  data = json.loads(parsed_json)
225
  prompts = prompt_parser.extract_prompts(data)
226
  except Exception:
227
+ return images_state, [], "Invalid JSON."
228
 
229
  idx = int(panel_num) - 1
230
  if idx < 0 or idx >= len(images_state):
231
+ return images_state, [], "Panel must be 1-" + str(len(images_state))
232
 
233
  from image_generator import ImageGenerator
234
 
235
+ if gen_mode == "Local (Free GPU)":
236
  model_id = LOCAL_MODELS.get(local_model, "stabilityai/sdxl-turbo")
237
  gen = ImageGenerator(mode="local", local_model_id=model_id)
238
  else:
 
246
  _, updated = gen.regenerate_single_panel(
247
  idx, prompts, images_state, float(strength), int(base_seed))
248
 
249
+ gallery = [(img, "Panel " + str(i + 1)) for i, img in enumerate(updated)]
250
+ return updated, gallery, "Panel " + str(int(panel_num)) + " regenerated!"
251
 
252
 
253
+ # ===============================================
254
  # GRADIO UI
255
+ # ===============================================
256
+
257
+ HEADER = (
258
+ "# TimeLapseForge v2.0 -- Universal API Edition\n"
259
+ "### Supports 13+ AI Image APIs -- Enter YOUR API key, select model, generate!\n\n"
260
+ "**OpenAI** | **Stability AI** | **Replicate** | **Together AI** | **Fal.ai** | "
261
+ "**Google Gemini** | **HuggingFace** | **xAI Grok** | **Fireworks** | "
262
+ "**Ideogram** | **Leonardo** | **Custom API** | **Local GPU**"
263
+ )
264
 
265
+ API_HELP = (
266
+ "### Where to Get API Keys\n\n"
267
+ "| Provider | Get Key | Needs Extra Package? |\n"
268
+ "|---|---|---|\n"
269
+ "| **Stability AI** | [platform.stability.ai](https://platform.stability.ai/account/keys) | No (uses requests) |\n"
270
+ "| **HuggingFace** | [huggingface.co](https://huggingface.co/settings/tokens) | No (uses requests) |\n"
271
+ "| **Fireworks AI** | [fireworks.ai](https://fireworks.ai/account/api-keys) | No (uses requests) |\n"
272
+ "| **Ideogram** | [ideogram.ai](https://ideogram.ai/manage-api) | No (uses requests) |\n"
273
+ "| **Leonardo AI** | [leonardo.ai](https://app.leonardo.ai/api-access) | No (uses requests) |\n"
274
+ "| **Direct URL** | Any REST API | No (uses requests) |\n"
275
+ "| **OpenAI** | [platform.openai.com](https://platform.openai.com/api-keys) | Yes: pip install openai |\n"
276
+ "| **xAI Grok** | [console.x.ai](https://console.x.ai) | Yes: pip install openai |\n"
277
+ "| **Replicate** | [replicate.com](https://replicate.com/account/api-tokens) | Yes: pip install replicate |\n"
278
+ "| **Together AI** | [api.together.xyz](https://api.together.xyz/settings/api-keys) | Yes: pip install together |\n"
279
+ "| **Fal.ai** | [fal.ai](https://fal.ai/dashboard/keys) | Yes: pip install fal-client |\n"
280
+ "| **Google Gemini** | [aistudio.google.com](https://aistudio.google.com/apikey) | Yes: pip install google-generativeai |\n"
281
+ "\n"
282
+ "**Recommended (Zero Setup):**\n\n"
283
+ "1. **Stability AI** -- Best quality, supports img2img for consistency\n"
284
+ "2. **HuggingFace** -- Free tier, many models\n"
285
+ "3. **Fireworks AI** -- Fast Flux generation\n"
286
+ "4. **Ideogram** -- Great text-in-image\n"
287
+ )
 
 
 
 
 
 
 
 
 
288
 
289
 
290
  with gr.Blocks(
291
+ title="TimeLapseForge v2.0",
292
  theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
293
  ) as app:
294
 
 
299
 
300
  with gr.Tabs():
301
 
302
+ # === TAB 1: INPUT ===
303
+ with gr.Tab("Input and Parse"):
 
 
304
  with gr.Row():
305
  with gr.Column(scale=2):
306
  json_input = gr.Textbox(
307
+ label="Paste GPT JSON",
308
  placeholder='{\n "project_metadata": {...},\n "panels": [...]\n}',
309
  lines=15,
310
  )
311
  with gr.Column(scale=1):
312
+ gr.Markdown("**-- OR -- Quick Text:**")
313
  quick_text = gr.Textbox(
314
+ label="Quick Command",
315
  placeholder="Restore a rusty 1969 Camaro SS",
316
  lines=3,
317
  )
318
+ quick_panels = gr.Slider(
319
+ 8, 40, value=20, step=1, label="Panels"
320
+ )
321
  quick_mode = gr.Dropdown(
322
+ ["restoration", "creation"],
323
+ value="restoration", label="Mode",
324
+ )
325
 
326
+ parse_btn = gr.Button("Parse and Preview", variant="primary", size="lg")
327
  parse_status = gr.Markdown("")
328
  prompt_table = gr.Dataframe(
329
  headers=["#", "Phase", "Title", "Prompt Preview"],
330
+ label="Extracted Panels", visible=False, wrap=True,
331
+ )
332
 
333
+ # === TAB 2: GENERATE ===
334
+ with gr.Tab("Generate Panels"):
 
 
335
  with gr.Row():
336
  with gr.Column(scale=1):
 
337
  gen_mode = gr.Radio(
338
+ ["Local (Free GPU)", "API (Your Key)"],
339
+ value="API (Your Key)",
340
  label="Generation Mode",
341
  )
342
 
 
343
  with gr.Group(visible=False) as local_group:
344
+ gr.Markdown("### Local Model")
345
  local_model = gr.Dropdown(
346
  list(LOCAL_MODELS.keys()),
347
  value="SDXL Turbo (Fast)",
348
  label="Model",
349
  )
350
 
 
351
  with gr.Group(visible=True) as api_group:
352
+ gr.Markdown("### API Provider")
353
  provider_dropdown = gr.Dropdown(
354
  PROVIDER_CHOICES,
355
  value=PROVIDER_CHOICES[0],
356
  label="Provider",
357
  )
358
  api_key_input = gr.Textbox(
359
+ label="API Key",
360
  placeholder="Paste your API key here...",
361
  type="password",
362
  )
363
  api_model_dropdown = gr.Dropdown(
364
+ label="Model",
365
  choices=get_models_for_provider("openai"),
366
  value="dall-e-3",
367
  allow_custom_value=True,
368
  )
369
+ with gr.Accordion("Custom API Settings", open=False):
370
  custom_base_url = gr.Textbox(
371
  label="Custom Base URL (OpenAI-compatible)",
372
  placeholder="https://your-api.com/v1",
 
376
  placeholder="https://your-api.com/generate",
377
  )
378
 
379
+ gr.Markdown("### Generation Settings")
 
380
  strength = gr.Slider(
381
  0.15, 0.70, value=0.38, step=0.01,
382
+ label="Change Strength",
383
+ info="Lower = more consistent between panels",
384
  )
385
+ base_seed = gr.Number(value=42, label="Seed", precision=0)
386
  with gr.Row():
387
+ steps = gr.Slider(
388
+ 0, 50, value=0, step=1, label="Steps (0=auto)"
389
+ )
390
+ guidance = gr.Slider(
391
+ -1, 15, value=-1, step=0.5, label="CFG (-1=auto)"
392
+ )
393
  with gr.Row():
394
+ img_width = gr.Number(
395
+ value=1024, label="Width", precision=0
396
+ )
397
+ img_height = gr.Number(
398
+ value=1024, label="Height", precision=0
399
+ )
400
 
401
+ ref_image = gr.Image(
402
+ label="Reference Image (optional)", type="pil"
403
+ )
404
 
405
  with gr.Column(scale=3):
406
  generate_btn = gr.Button(
407
+ "Generate All Panels", variant="primary", size="lg"
408
+ )
409
  gen_status = gr.Markdown("")
410
  gallery = gr.Gallery(
411
+ label="Generated Panels",
412
+ columns=5, rows=3, height=450,
413
+ object_fit="contain",
414
+ )
415
 
416
+ with gr.Accordion("Regenerate Single Panel", open=False):
417
  with gr.Row():
418
+ regen_num = gr.Number(
419
+ value=1, label="Panel Number", precision=0
420
+ )
421
+ regen_btn = gr.Button(
422
+ "Regenerate", variant="secondary"
423
+ )
424
  regen_status = gr.Markdown("")
425
 
426
+ # === TAB 3: VIDEO ===
427
+ with gr.Tab("Create Video"):
 
 
428
  with gr.Row():
429
  with gr.Column(scale=1):
430
+ gr.Markdown("### Interpolation")
431
+ interp_mult = gr.Slider(
432
+ 1, 8, value=3, step=1, label="Frame Multiplier"
433
+ )
434
+ interp_method = gr.Dropdown(
435
+ ["blend", "flow"], value="blend", label="Method"
436
+ )
437
 
438
+ gr.Markdown("### Video Settings")
439
+ vid_fps = gr.Slider(
440
+ 12, 60, value=24, step=1, label="FPS"
441
+ )
442
+ frame_hold = gr.Slider(
443
+ 0.3, 5.0, value=1.5, step=0.1, label="Seconds per Panel"
444
+ )
445
 
446
+ gr.Markdown("### Overlays")
447
+ add_labels = gr.Checkbox(
448
+ value=True, label="Timestamp labels"
449
+ )
450
+ add_progress_bar = gr.Checkbox(
451
+ value=True, label="Progress bar"
452
+ )
453
+ add_bookend = gr.Checkbox(
454
+ value=True, label="BEFORE / AFTER labels"
455
+ )
456
 
457
+ gr.Markdown("### Audio")
458
+ music_file = gr.Audio(
459
+ label="Background Music", type="filepath"
460
+ )
461
+ export_gif = gr.Checkbox(
462
+ value=False, label="Also export GIF"
463
+ )
464
 
465
  with gr.Column(scale=2):
466
  assemble_btn = gr.Button(
467
+ "Create Video", variant="primary", size="lg"
468
+ )
469
  vid_status = gr.Markdown("")
470
+ video_output = gr.Video(label="Timelapse Video")
471
+ comparison_output = gr.Image(label="Before / After")
472
+ gif_output = gr.File(label="GIF Download")
473
+
474
+ # === TAB 4: API HELP ===
475
+ with gr.Tab("API Keys Guide"):
 
 
476
  gr.Markdown(API_HELP)
477
 
478
+ # === Dynamic UI Updates ===
479
 
480
  def toggle_mode(mode):
481
+ if mode == "Local (Free GPU)":
482
  return gr.update(visible=True), gr.update(visible=False)
483
  return gr.update(visible=False), gr.update(visible=True)
484
 
 
494
  outputs=[api_model_dropdown],
495
  )
496
 
497
+ # === Core Event Handlers ===
498
 
499
  parse_btn.click(
500
  fn=parse_input,