GLAkavya commited on
Commit
e8898e6
·
verified ·
1 Parent(s): 5793ccd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -233
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import os
2
- import json
3
  import tempfile
4
  import io
5
  import math
@@ -7,25 +6,15 @@ import time
7
  import numpy as np
8
  import cv2
9
  import gradio as gr
10
- from google import genai
11
- from google.genai import types
12
  from PIL import Image
13
 
14
- # ── ENV SETUP ────────────────────────────────────────────────────────────────
15
- gemini_key = (
16
- os.environ.get("GEMINI_API_KEY", "")
17
- or os.environ.get("GOOGLE_API_KEY", "")
18
- ).strip()
19
- if gemini_key:
20
- os.environ["GOOGLE_API_KEY"] = gemini_key
21
- print(f"✅ Gemini key loaded (len={len(gemini_key)})")
22
- else:
23
- print("❌ No Gemini key found!")
24
-
25
  hf_token = (
26
  os.environ.get("HF_TOKEN", "")
27
  or os.environ.get("HF_KEY", "")
28
  ).strip()
 
 
29
  if hf_token:
30
  try:
31
  from huggingface_hub import login, InferenceClient
@@ -33,105 +22,60 @@ if hf_token:
33
  hf_client = InferenceClient(token=hf_token)
34
  print("✅ HF login OK")
35
  except Exception as e:
36
- hf_client = None
37
  print(f"⚠️ HF login skipped: {e}")
38
  else:
39
- hf_client = None
40
- print("⚠️ No HF token — will use Ken Burns fallback")
41
 
42
  print("✅ App ready!")
43
 
44
 
45
- # ── HF MODEL FALLBACK CHAIN ──────────────────────────────────────────────────
46
- # Models tried in order — first success wins, last is Ken Burns (always works)
47
-
48
  HF_MODELS = [
49
- {
50
- "id": "Lightricks/LTX-2",
51
- "name": "LTX-2 (Lightricks)",
52
- "note": "Best quality, fastest inference available ⚡",
53
- },
54
- {
55
- "id": "Wan-AI/Wan2.2-I2V-A14B",
56
- "name": "Wan 2.2 14B",
57
- "note": "High quality, slightly slower",
58
- },
59
- {
60
- "id": "stabilityai/stable-video-diffusion-img2vid-xt",
61
- "name": "Stable Video Diffusion XT",
62
- "note": "136k downloads, reliable classic",
63
- },
64
- {
65
- "id": "KlingTeam/LivePortrait",
66
- "name": "KlingTeam LivePortrait",
67
- "note": "Great for portraits / faces",
68
- },
69
- {
70
- "id": "Lightricks/LTX-Video",
71
- "name": "LTX-Video (older)",
72
- "note": "248k downloads, solid fallback",
73
- },
74
- # Final fallback — pure OpenCV, always works
75
- {
76
- "id": "__ken_burns__",
77
- "name": "Ken Burns (local, no API)",
78
- "note": "Always works — cinematic zoom/pan effect",
79
- },
80
  ]
81
 
82
 
83
- def try_hf_model(model_id: str, pil_image: Image.Image, prompt: str) -> bytes | None:
84
- """Try one HuggingFace model. Returns video bytes or None on failure."""
85
  if hf_client is None:
86
  return None
87
  try:
88
  buf = io.BytesIO()
89
  pil_image.save(buf, format="JPEG")
90
  image_bytes = buf.getvalue()
91
-
92
  print(f" 🤖 Trying {model_id} ...")
93
  result = hf_client.image_to_video(
94
  image=image_bytes,
95
  model=model_id,
96
  prompt=prompt,
97
  )
98
-
99
  if isinstance(result, bytes):
100
  return result
101
  elif hasattr(result, "read"):
102
  return result.read()
103
- else:
104
- return None
105
-
106
  except Exception as e:
107
  print(f" ❌ {model_id} failed: {e}")
108
  return None
109
 
110
 
111
- def generate_video_with_fallback(
112
- pil_image: Image.Image,
113
- prompt: str,
114
- style: str,
115
- progress_callback=None,
116
- ) -> tuple[str, str]:
117
- """
118
- Tries HF models in order. Falls back to Ken Burns if all fail.
119
- Returns (video_path, model_used_name).
120
- """
121
  for model_info in HF_MODELS:
122
  model_id = model_info["id"]
123
  model_name = model_info["name"]
124
 
125
  if progress_callback:
126
- progress_callback(f"⏳ Trying: **{model_name}** — {model_info['note']}")
127
 
128
- # Ken Burns is always last and always works
129
  if model_id == "__ken_burns__":
130
  print(" 🎬 Using Ken Burns (local fallback)")
131
- path = generate_video_ken_burns(pil_image, duration_sec=5, fps=24, style=style.lower())
132
  return path, f"🎨 {model_name}"
133
 
134
- # Try HF model
135
  video_bytes = try_hf_model(model_id, pil_image, prompt)
136
  if video_bytes:
137
  tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
@@ -140,66 +84,13 @@ def generate_video_with_fallback(
140
  print(f" ✅ SUCCESS with {model_name}")
141
  return tmp.name, f"🤖 {model_name}"
142
 
143
- # Small wait between retries to avoid hammering API
144
  time.sleep(1)
145
 
146
- # Should never reach here (Ken Burns is last), but just in case
147
- path = generate_video_ken_burns(pil_image, duration_sec=5, fps=24, style=style.lower())
148
  return path, "🎨 Ken Burns (local)"
149
 
150
 
151
- # ── GEMINI ────────────────────────────────────────────────────────────────────
152
- def call_gemini(pil_image: Image.Image, user_desc: str, language: str, style: str) -> dict:
153
- client = genai.Client(api_key=gemini_key)
154
-
155
- lang_map = {
156
- "English": "Write everything in English.",
157
- "Hindi": "सब कुछ हिंदी में लिखें।",
158
- "Hinglish": "Write in Hinglish (mix of Hindi and English).",
159
- }
160
- style_map = {
161
- "Fun": "tone: playful, witty, youthful",
162
- "Premium": "tone: luxurious, sophisticated, aspirational",
163
- "Energetic": "tone: high-energy, bold, action-packed",
164
- }
165
-
166
- prompt = f"""You are an expert ad copywriter. Analyze this product image and create a compelling social-media video ad.
167
-
168
- {f'Product description: {user_desc}' if user_desc.strip() else ''}
169
- Language rule : {lang_map.get(language, lang_map['English'])}
170
- Style rule : {style_map.get(style, style_map['Fun'])}
171
-
172
- CRITICAL: Return ONLY raw JSON. No markdown. No ```json. No explanation. Pure JSON only.
173
- {{
174
- "hook": "attention-grabbing opening line (1-2 sentences)",
175
- "script": "full 15-20 second voiceover script",
176
- "cta": "call-to-action phrase",
177
- "video_prompt": "detailed cinematic advertising scene description for image-to-video AI"
178
- }}"""
179
-
180
- buf = io.BytesIO()
181
- pil_image.save(buf, format="JPEG")
182
- image_bytes = buf.getvalue()
183
-
184
- response = client.models.generate_content(
185
- model="gemini-2.5-flash-preview-05-20",
186
- contents=[
187
- types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
188
- types.Part.from_text(text=prompt),
189
- ],
190
- )
191
-
192
- raw = response.text.strip()
193
- if "```" in raw:
194
- raw = raw.split("```")[1]
195
- if raw.lower().startswith("json"):
196
- raw = raw[4:]
197
- raw = raw.strip()
198
-
199
- return json.loads(raw)
200
-
201
-
202
- # ── KEN BURNS VIDEO (local fallback) ─────────────────────────────────────────
203
  def ease_in_out(t):
204
  return t * t * (3 - 2 * t)
205
 
@@ -239,9 +130,8 @@ def apply_color_grade(frame, style="premium"):
239
  f[:,:,1] = np.clip(f[:,:,1] * 1.05, 0, 255)
240
  return f.astype(np.uint8)
241
 
242
- def generate_video_ken_burns(pil_image: Image.Image, duration_sec: int = 5, fps: int = 24, style: str = "premium") -> str:
243
  total_frames = duration_sec * fps
244
-
245
  img = pil_image.convert("RGB")
246
  target_w, target_h = 720, 1280
247
  img = img.resize((target_w, target_h), Image.LANCZOS)
@@ -261,29 +151,29 @@ def generate_video_ken_burns(pil_image: Image.Image, duration_sec: int = 5, fps:
261
 
262
  for i in range(total_frames):
263
  if i < s1_end:
264
- t = i / s1_end
265
  te = ease_out_bounce(min(t * 1.1, 1.0))
266
- zoom = 1.35 - 0.25 * te
267
  pan_x = int(pad * 0.1 * t)
268
  pan_y = int(-pad * 0.15 * t)
269
  elif i < s2_end:
270
- t = (i - s1_end) / (s2_end - s1_end)
271
  te = ease_in_out(t)
272
- zoom = 1.10 - 0.05 * te
273
  shake_x = int(3 * math.sin(i * 0.8))
274
  shake_y = int(2 * math.cos(i * 1.1))
275
- pan_x = int(pad * 0.1 + shake_x)
276
- pan_y = int(-pad * 0.15 - pad * 0.20 * te + shake_y)
277
  elif i < s3_end:
278
- t = (i - s2_end) / (s3_end - s2_end)
279
  te = ease_in_out(t)
280
- zoom = 1.05 - 0.04 * te
281
  pan_x = int(pad * 0.1 * (1 - te))
282
  pan_y = int(-pad * 0.35 * (1 - te))
283
  else:
284
- t = (i - s3_end) / (s4_end - s3_end)
285
  te = ease_in_out(t)
286
- zoom = 1.01 + 0.03 * te
287
  pan_x = 0
288
  pan_y = 0
289
 
@@ -300,9 +190,9 @@ def generate_video_ken_burns(pil_image: Image.Image, duration_sec: int = 5, fps:
300
  x1, y1, x2, y2 = 0, 0, target_w, target_h
301
 
302
  cropped = big_img[y1:y2, x1:x2]
303
- frame = cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
304
- frame = apply_color_grade(frame, style)
305
- frame = apply_vignette(frame, strength=0.55)
306
 
307
  fade_in_end = int(fps * 0.4)
308
  fade_out_sta = int(fps * 4.4)
@@ -315,11 +205,11 @@ def generate_video_ken_burns(pil_image: Image.Image, duration_sec: int = 5, fps:
315
 
316
  flash_frames = {s1_end, s1_end+1, s2_end, s2_end+1}
317
  if i in flash_frames:
318
- flash_strength = 0.35 if i in {s1_end, s2_end} else 0.15
319
  white = np.ones_like(frame) * 255
320
- frame = cv2.addWeighted(frame, 1 - flash_strength, white.astype(np.uint8), flash_strength, 0)
321
 
322
- frame = np.clip(frame.astype(np.float32) * alpha, 0, 255).astype(np.uint8)
323
  frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
324
  out.write(frame_bgr)
325
 
@@ -327,124 +217,77 @@ def generate_video_ken_burns(pil_image: Image.Image, duration_sec: int = 5, fps:
327
  return tmp.name
328
 
329
 
330
- # ── MAIN PIPELINE ─────────────────────────────────────────────────────────────
331
- _status_log = []
332
-
333
- def generate_ad(image, user_desc, language, style, progress=gr.Progress()):
334
- global _status_log
335
- _status_log = []
336
-
337
  if image is None:
338
- return None, "⚠️ Please upload a product image.", "", "", "❌ No image"
339
 
340
  pil_image = image if isinstance(image, Image.Image) else Image.fromarray(image)
341
-
342
- # STEP 1 — Gemini ad copy
343
- progress(0.1, desc="🧠 Gemini generating ad copy...")
344
- try:
345
- ad_data = call_gemini(pil_image, user_desc or "", language, style)
346
- except Exception as e:
347
- return None, f"❌ Gemini error: {e}", "", "", "❌ Gemini failed"
348
-
349
- hook = ad_data.get("hook", "")
350
- script = ad_data.get("script", "")
351
- cta = ad_data.get("cta", "")
352
- video_prompt = ad_data.get("video_prompt", hook)
353
-
354
- # STEP 2 — Video with fallback chain
355
- progress(0.3, desc="🎬 Generating video (trying AI models)...")
356
 
357
  status_lines = []
358
 
359
- def log_progress(msg):
360
  status_lines.append(msg)
361
- progress(0.3 + len(status_lines) * 0.1, desc=msg.replace("**", "").replace("*", ""))
362
 
363
- try:
364
- video_path, model_used = generate_video_with_fallback(
365
- pil_image,
366
- prompt=video_prompt,
367
- style=style,
368
- progress_callback=log_progress,
369
- )
370
- except Exception as e:
371
- return None, hook, f"❌ Video error: {e}\n\n{script}", cta, "❌ All models failed"
372
 
373
- progress(1.0, desc="✅ Done!")
 
 
 
 
 
374
 
375
- model_log = "\n".join(status_lines) + f"\n\n**Used:** {model_used}"
376
- return video_path, hook, script, cta, model_log
 
377
 
378
 
379
- # ── GRADIO UI ─────���───────────────────────────────────────────────────────────
380
  css = """
381
- #title { text-align:center; font-size:2.2rem; font-weight:800; margin-bottom:.2rem; }
382
- #sub { text-align:center; color:#888; margin-bottom:1.5rem; }
383
- .model-chain { font-size:.85rem; line-height:1.7; }
384
  """
385
 
386
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="violet")) as demo:
387
 
388
  gr.Markdown("# 🎬 AI Reel Generator", elem_id="title")
389
- gr.Markdown(
390
- "Upload a product image → Gemini writes ad copy → "
391
- "AI generates cinematic 5-sec reel (5-model fallback chain).",
392
- elem_id="sub",
393
- )
394
 
395
  with gr.Row():
396
- # ── LEFT COLUMN ──────────────────────────────────────────────────────
397
  with gr.Column(scale=1):
398
- image_input = gr.Image(label="📸 Upload Product Image", type="pil", height=300)
399
- desc_input = gr.Textbox(
400
- label="📝 Describe your product (optional)",
401
- placeholder="e.g. Premium sneakers with star design ",
402
  lines=3,
403
  )
404
- with gr.Row():
405
- lang_dropdown = gr.Dropdown(
406
- choices=["English", "Hindi", "Hinglish"],
407
- value="English", label="🌐 Language",
408
- )
409
- style_dropdown = gr.Dropdown(
410
- choices=["Fun", "Premium", "Energetic"],
411
- value="Fun", label="🎨 Style",
412
- )
413
- gen_btn = gr.Button("🚀 Generate Ad", variant="primary", size="lg")
414
-
415
- # Model chain info box
416
  gr.Markdown(
417
- "**🔗 Model Fallback Chain:**\n"
418
- "1. 🤖 Lightricks/LTX-2 ⚡\n"
419
- "2. 🤖 Wan 2.2 I2V-A14B\n"
420
- "3. 🤖 Stable Video Diffusion XT\n"
421
- "4. 🤖 KlingTeam/LivePortrait\n"
422
- "5. 🤖 Lightricks/LTX-Video\n"
423
- "6. 🎨 Ken Burns (local, always works)",
424
- elem_classes="model-chain",
425
  )
426
 
427
- # ── RIGHT COLUMN ─────────────────────────────────────────────────────
428
  with gr.Column(scale=1):
429
- video_out = gr.Video(label="🎥 5-Second Ad Reel", height=400)
430
- hook_out = gr.Textbox(label=" Hook", lines=2, interactive=False)
431
- script_out = gr.Textbox(label="📄 Script", lines=5, interactive=False)
432
- cta_out = gr.Textbox(label="🎯 CTA", lines=1, interactive=False)
433
- status_out = gr.Textbox(label="📊 Model Log", lines=6, interactive=False)
434
 
435
  gen_btn.click(
436
  fn=generate_ad,
437
- inputs=[image_input, desc_input, lang_dropdown, style_dropdown],
438
- outputs=[video_out, hook_out, script_out, cta_out, status_out],
439
- )
440
-
441
- gr.Markdown(
442
- "---\n**How it works:** "
443
- "1️⃣ Gemini 2.5 Flash → hook + script + CTA + video prompt. "
444
- "2️⃣ Tries 5 HuggingFace image-to-video models in order. "
445
- "3️⃣ First success wins → downloads video. "
446
- "4️⃣ If all API calls fail → Ken Burns cinematic effect (local, always works). "
447
- "⚡ With HF token + inference-available model: ~10-30 seconds total!"
448
  )
449
 
450
  if __name__ == "__main__":
 
1
  import os
 
2
  import tempfile
3
  import io
4
  import math
 
6
  import numpy as np
7
  import cv2
8
  import gradio as gr
 
 
9
  from PIL import Image
10
 
11
+ # ── HF SETUP ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
12
  hf_token = (
13
  os.environ.get("HF_TOKEN", "")
14
  or os.environ.get("HF_KEY", "")
15
  ).strip()
16
+
17
+ hf_client = None
18
  if hf_token:
19
  try:
20
  from huggingface_hub import login, InferenceClient
 
22
  hf_client = InferenceClient(token=hf_token)
23
  print("✅ HF login OK")
24
  except Exception as e:
 
25
  print(f"⚠️ HF login skipped: {e}")
26
  else:
27
+ print("⚠️ No HF token — will use Ken Burns fallback only")
 
28
 
29
  print("✅ App ready!")
30
 
31
 
32
+ # ── HF MODEL FALLBACK CHAIN ──────────────────────────────────────────────────
 
 
33
  HF_MODELS = [
34
+ {"id": "Lightricks/LTX-2", "name": "LTX-2 (Lightricks) ⚡"},
35
+ {"id": "Wan-AI/Wan2.2-I2V-A14B", "name": "Wan 2.2 I2V-A14B"},
36
+ {"id": "stabilityai/stable-video-diffusion-img2vid-xt", "name": "Stable Video Diffusion XT"},
37
+ {"id": "KlingTeam/LivePortrait", "name": "KlingTeam LivePortrait"},
38
+ {"id": "Lightricks/LTX-Video", "name": "LTX-Video"},
39
+ {"id": "__ken_burns__", "name": "Ken Burns (local fallback)"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  ]
41
 
42
 
43
+ def try_hf_model(model_id, pil_image, prompt):
 
44
  if hf_client is None:
45
  return None
46
  try:
47
  buf = io.BytesIO()
48
  pil_image.save(buf, format="JPEG")
49
  image_bytes = buf.getvalue()
 
50
  print(f" 🤖 Trying {model_id} ...")
51
  result = hf_client.image_to_video(
52
  image=image_bytes,
53
  model=model_id,
54
  prompt=prompt,
55
  )
 
56
  if isinstance(result, bytes):
57
  return result
58
  elif hasattr(result, "read"):
59
  return result.read()
60
+ return None
 
 
61
  except Exception as e:
62
  print(f" ❌ {model_id} failed: {e}")
63
  return None
64
 
65
 
66
+ def generate_video_with_fallback(pil_image, prompt, style, progress_callback=None):
 
 
 
 
 
 
 
 
 
67
  for model_info in HF_MODELS:
68
  model_id = model_info["id"]
69
  model_name = model_info["name"]
70
 
71
  if progress_callback:
72
+ progress_callback(f"⏳ Trying: {model_name}")
73
 
 
74
  if model_id == "__ken_burns__":
75
  print(" 🎬 Using Ken Burns (local fallback)")
76
+ path = generate_video_ken_burns(pil_image, style=style.lower())
77
  return path, f"🎨 {model_name}"
78
 
 
79
  video_bytes = try_hf_model(model_id, pil_image, prompt)
80
  if video_bytes:
81
  tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
 
84
  print(f" ✅ SUCCESS with {model_name}")
85
  return tmp.name, f"🤖 {model_name}"
86
 
 
87
  time.sleep(1)
88
 
89
+ path = generate_video_ken_burns(pil_image, style=style.lower())
 
90
  return path, "🎨 Ken Burns (local)"
91
 
92
 
93
+ # ── KEN BURNS VIDEO ───────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def ease_in_out(t):
95
  return t * t * (3 - 2 * t)
96
 
 
130
  f[:,:,1] = np.clip(f[:,:,1] * 1.05, 0, 255)
131
  return f.astype(np.uint8)
132
 
133
+ def generate_video_ken_burns(pil_image, duration_sec=5, fps=24, style="premium"):
134
  total_frames = duration_sec * fps
 
135
  img = pil_image.convert("RGB")
136
  target_w, target_h = 720, 1280
137
  img = img.resize((target_w, target_h), Image.LANCZOS)
 
151
 
152
  for i in range(total_frames):
153
  if i < s1_end:
154
+ t = i / s1_end
155
  te = ease_out_bounce(min(t * 1.1, 1.0))
156
+ zoom = 1.35 - 0.25 * te
157
  pan_x = int(pad * 0.1 * t)
158
  pan_y = int(-pad * 0.15 * t)
159
  elif i < s2_end:
160
+ t = (i - s1_end) / (s2_end - s1_end)
161
  te = ease_in_out(t)
162
+ zoom = 1.10 - 0.05 * te
163
  shake_x = int(3 * math.sin(i * 0.8))
164
  shake_y = int(2 * math.cos(i * 1.1))
165
+ pan_x = int(pad * 0.1 + shake_x)
166
+ pan_y = int(-pad * 0.15 - pad * 0.20 * te + shake_y)
167
  elif i < s3_end:
168
+ t = (i - s2_end) / (s3_end - s2_end)
169
  te = ease_in_out(t)
170
+ zoom = 1.05 - 0.04 * te
171
  pan_x = int(pad * 0.1 * (1 - te))
172
  pan_y = int(-pad * 0.35 * (1 - te))
173
  else:
174
+ t = (i - s3_end) / (s4_end - s3_end)
175
  te = ease_in_out(t)
176
+ zoom = 1.01 + 0.03 * te
177
  pan_x = 0
178
  pan_y = 0
179
 
 
190
  x1, y1, x2, y2 = 0, 0, target_w, target_h
191
 
192
  cropped = big_img[y1:y2, x1:x2]
193
+ frame = cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
194
+ frame = apply_color_grade(frame, style)
195
+ frame = apply_vignette(frame, strength=0.55)
196
 
197
  fade_in_end = int(fps * 0.4)
198
  fade_out_sta = int(fps * 4.4)
 
205
 
206
  flash_frames = {s1_end, s1_end+1, s2_end, s2_end+1}
207
  if i in flash_frames:
208
+ fs = 0.35 if i in {s1_end, s2_end} else 0.15
209
  white = np.ones_like(frame) * 255
210
+ frame = cv2.addWeighted(frame, 1 - fs, white.astype(np.uint8), fs, 0)
211
 
212
+ frame = np.clip(frame.astype(np.float32) * alpha, 0, 255).astype(np.uint8)
213
  frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
214
  out.write(frame_bgr)
215
 
 
217
  return tmp.name
218
 
219
 
220
+ # ── MAIN ──────────────────────────────────────────────────────────────────────
221
+ def generate_ad(image, prompt_text, style, progress=gr.Progress()):
 
 
 
 
 
222
  if image is None:
223
+ return None, "⚠️ Please upload an image first!"
224
 
225
  pil_image = image if isinstance(image, Image.Image) else Image.fromarray(image)
226
+ prompt = prompt_text.strip() if prompt_text.strip() else "cinematic product advertisement, smooth motion"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  status_lines = []
229
 
230
+ def log(msg):
231
  status_lines.append(msg)
232
+ progress(0.2 + len(status_lines) * 0.12, desc=msg)
233
 
234
+ progress(0.1, desc="🎬 Starting video generation...")
 
 
 
 
 
 
 
 
235
 
236
+ video_path, model_used = generate_video_with_fallback(
237
+ pil_image,
238
+ prompt=prompt,
239
+ style=style,
240
+ progress_callback=log,
241
+ )
242
 
243
+ progress(1.0, desc="✅ Done!")
244
+ log_text = "\n".join(status_lines) + f"\n\n✅ Used: {model_used}"
245
+ return video_path, log_text
246
 
247
 
248
+ # ── UI ────────────────────────────────────────────────────────────────────────
249
  css = """
250
+ #title { text-align:center; font-size:2.2rem; font-weight:800; margin-bottom:.2rem; }
251
+ #sub { text-align:center; color:#888; margin-bottom:1.5rem; }
 
252
  """
253
 
254
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="violet")) as demo:
255
 
256
  gr.Markdown("# 🎬 AI Reel Generator", elem_id="title")
257
+ gr.Markdown("Image upload karo → cinematic video ready!", elem_id="sub")
 
 
 
 
258
 
259
  with gr.Row():
 
260
  with gr.Column(scale=1):
261
+ image_input = gr.Image(label="📸 Upload Image", type="pil", height=320)
262
+ prompt_input = gr.Textbox(
263
+ label="✏️ Prompt (optional)",
264
+ placeholder="e.g. cinematic slow zoom, product floating in air ...",
265
  lines=3,
266
  )
267
+ style_dd = gr.Dropdown(
268
+ choices=["Fun", "Premium", "Energetic"],
269
+ value="Premium", label="🎨 Style",
270
+ )
271
+ gen_btn = gr.Button("🚀 Generate Video", variant="primary", size="lg")
272
+
 
 
 
 
 
 
273
  gr.Markdown(
274
+ "**🔗 Fallback Chain:**\n"
275
+ "1. Lightricks/LTX-2 ⚡\n"
276
+ "2. Wan 2.2 I2V-A14B\n"
277
+ "3. Stable Video Diffusion XT\n"
278
+ "4. KlingTeam/LivePortrait\n"
279
+ "5. Lightricks/LTX-Video\n"
280
+ "6. Ken Burns (always works)"
 
281
  )
282
 
 
283
  with gr.Column(scale=1):
284
+ video_out = gr.Video(label="🎥 Generated Video", height=450)
285
+ status_out = gr.Textbox(label="📊 Model Log", lines=8, interactive=False)
 
 
 
286
 
287
  gen_btn.click(
288
  fn=generate_ad,
289
+ inputs=[image_input, prompt_input, style_dd],
290
+ outputs=[video_out, status_out],
 
 
 
 
 
 
 
 
 
291
  )
292
 
293
  if __name__ == "__main__":