GLAkavya commited on
Commit
265814a
Β·
verified Β·
1 Parent(s): cd469c3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +398 -104
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, tempfile, io, math, time, threading, re, random
2
  import numpy as np
3
  import cv2
4
  import gradio as gr
@@ -14,52 +14,83 @@ if hf_token:
14
  print("βœ… HF ready")
15
  except Exception as e: print(f"⚠️ HF: {e}")
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  # ══════════════════════════════════════════════════════════════════
18
- # AUTO-DETECT from image (color + HF classifier)
19
  # ══════════════════════════════════════════════════════════════════
20
  def auto_detect(pil_image, user_caption=""):
21
- """
22
- 1. Try HF image classification
23
- 2. Fallback: dominant color + aspect ratio heuristics
24
- Returns (category, auto_prompt, auto_caption_hint)
25
- """
26
  category = "Product/Other"
27
  label = ""
28
-
29
- # Try HF zero-shot image classification
30
  if hf_client:
31
  try:
32
- buf = io.BytesIO(); pil_image.save(buf,format="JPEG",quality=85)
33
- result = hf_client.image_classification(
34
- image=buf.getvalue(),
35
- model="google/vit-base-patch16-224",
36
- )
37
  if result:
38
  label = result[0].get("label","").lower()
39
- print(f" πŸ” HF label: {label}")
40
  except Exception as e:
41
  print(f" ⚠️ classifier skip: {e}")
42
 
43
- # Map HF label β†’ our category
44
  label_map = {
45
- "shoe": "Fashion", "sneaker": "Fashion", "boot": "Fashion",
46
- "dress": "Fashion", "shirt": "Fashion", "jacket": "Fashion",
47
- "jean": "Fashion", "sandal": "Fashion", "bag": "Fashion",
48
- "pizza": "Food", "burger": "Food", "cake": "Food",
49
- "food": "Food", "coffee": "Food", "sushi": "Food",
50
- "laptop": "Tech", "phone": "Tech", "camera": "Tech",
51
- "keyboard":"Tech", "monitor": "Tech", "tablet": "Tech",
52
- "lipstick":"Beauty", "cream": "Beauty", "perfume": "Beauty",
53
- "cosmetic":"Beauty", "makeup": "Beauty",
54
- "dumbbell":"Fitness", "yoga": "Fitness", "bottle": "Fitness",
55
- "bicycle": "Fitness", "jersey": "Fitness",
56
- "plant": "Lifestyle","candle": "Lifestyle","chair": "Lifestyle",
57
- "sofa": "Lifestyle","lamp": "Lifestyle",
58
  }
59
  for k,v in label_map.items():
60
  if k in label: category=v; break
61
 
62
- # Also check user caption
63
  if category == "Product/Other" and user_caption:
64
  cap_low = user_caption.lower()
65
  if any(w in cap_low for w in ["shoe","sneaker","dress","outfit","wear","fashion","style","cloth","kurta"]): category="Fashion"
@@ -69,7 +100,6 @@ def auto_detect(pil_image, user_caption=""):
69
  elif any(w in cap_low for w in ["gym","fit","workout","protein","yoga","health","sport"]): category="Fitness"
70
  elif any(w in cap_low for w in ["home","decor","interior","lifestyle","aesthetic","candle"]): category="Lifestyle"
71
 
72
- # Build cinematic prompt from detected category
73
  prompts = {
74
  "Fashion": "cinematic fashion product shot, model wearing outfit, soft studio lighting, slow zoom, luxury feel",
75
  "Food": "cinematic food photography, steam rising, dramatic close-up, warm golden lighting, slow reveal",
@@ -81,10 +111,8 @@ def auto_detect(pil_image, user_caption=""):
81
  }
82
  auto_prompt = prompts.get(category, prompts["Product/Other"])
83
  if label: auto_prompt = f"{label} product, {auto_prompt}"
84
-
85
  return category, auto_prompt, label
86
 
87
-
88
  # ══════════════════════════════════════════════════════════════════
89
  # SMART INSIGHTS
90
  # ══════════════════════════════════════════════════════════════════
@@ -97,7 +125,6 @@ POSTING_TIMES = {
97
  "Lifestyle": {"best":"7:00 PM", "days":"Thu, Fri, Sat", "slots":["9AM","2PM","7PM"]},
98
  "Product/Other":{"best":"8:00 PM", "days":"Tue, Thu, Sat", "slots":["10AM","3PM","8PM"]},
99
  }
100
-
101
  AUDIENCES = {
102
  "Fashion": "πŸ‘— 18-35 yo females Β· Fashion lovers Β· Insta scrollers Β· Trend followers",
103
  "Food": "πŸ• 18-45 Β· Foodies Β· Home cooks Β· Restaurant goers Β· Food bloggers",
@@ -107,7 +134,6 @@ AUDIENCES = {
107
  "Lifestyle": "🌿 22-40 Β· Aspirational buyers Β· Aesthetic lovers Β· Home decor fans",
108
  "Product/Other":"πŸ›οΈ 18-45 Β· Online shoppers Β· Deal hunters Β· Value-conscious buyers",
109
  }
110
-
111
  CAPTIONS = {
112
  "English": {
113
  "Premium": ["✨ {cap} Quality that speaks for itself. πŸ›’ Shop Now β†’ Link in bio",
@@ -134,7 +160,6 @@ CAPTIONS = {
134
  "POV: Naya fav mil gaya πŸŽ‰ {cap} Bio mein link!"],
135
  },
136
  }
137
-
138
  HASHTAGS = {
139
  "Fashion": "#Fashion #OOTD #StyleInspo #NewCollection #Trending #ShopNow #Reels",
140
  "Food": "#FoodLovers #Foodie #FoodPhotography #Yummy #FoodReels #MustTry",
@@ -151,7 +176,6 @@ def get_insights(category, style, language, cap):
151
  tmpl = CAPTIONS.get(language, CAPTIONS["English"]).get(style, CAPTIONS["English"]["Premium"])
152
  ai_cap = random.choice(tmpl).replace("{cap}", clean_cap)
153
  tags = HASHTAGS.get(category, HASHTAGS["Product/Other"])
154
-
155
  insight = (
156
  f"πŸ“Š SMART INSIGHTS\n"
157
  f"{'━'*38}\n"
@@ -166,6 +190,79 @@ def get_insights(category, style, language, cap):
166
  )
167
  return insight, ai_cap
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  # ══════════════════════════════════════════════════════════════════
171
  # HF VIDEO CHAIN
@@ -210,7 +307,6 @@ def get_video(pil, prompt, dur, cb=None):
210
  time.sleep(0.5)
211
  return ken_burns(pil, duration_sec=dur), "Ken Burns"
212
 
213
-
214
  # ══════════════════════════════════════════════════════════════════
215
  # KEN BURNS
216
  # ══════════════════════════════════════════════════════════════════
@@ -230,7 +326,6 @@ def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
230
  img_r=ImageEnhance.Contrast(img_r).enhance(1.05)
231
  img_r=ImageEnhance.Color(img_r).enhance(1.08)
232
 
233
- # Blurred bg
234
  bg=img.resize((TW,TH),Image.LANCZOS).filter(ImageFilter.GaussianBlur(18))
235
  bg=ImageEnhance.Brightness(bg).enhance(0.55)
236
  canvas=bg.copy(); canvas.paste(img_r,((TW-nw)//2,(TH-nh)//2))
@@ -287,21 +382,15 @@ def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
287
  writer.release()
288
  return tmp.name
289
 
290
-
291
  # ══════════════════════════════════════════════════════════════════
292
  # MULTI-VIDEO MERGE
293
  # ══════════════════════════════════════════════════════════════════
294
  def merge_videos(paths):
295
- """Concatenate multiple mp4s with crossfade using ffmpeg."""
296
  if len(paths)==1: return paths[0]
297
  out=paths[0].replace(".mp4","_merged.mp4")
298
-
299
- # Write concat list
300
  lst=tempfile.NamedTemporaryFile(suffix=".txt",mode="w",delete=False)
301
  for p in paths: lst.write(f"file '{p}'\n")
302
  lst.flush()
303
-
304
- # Simple concat (re-encode for compatibility)
305
  ret=os.system(
306
  f'ffmpeg -y -f concat -safe 0 -i "{lst.name}" '
307
  f'-c:v libx264 -c:a aac -b:a 128k -movflags +faststart '
@@ -309,9 +398,8 @@ def merge_videos(paths):
309
  )
310
  return out if (ret==0 and os.path.exists(out)) else paths[-1]
311
 
312
-
313
  # ══════════════════════════════════════════════════════════════════
314
- # CAPTIONS (ffmpeg drawtext)
315
  # ══════════════════════════════════════════════════════════════════
316
  def add_captions_ffmpeg(video_path, caption, duration_sec, style):
317
  def clean(t): return re.sub(r"[^A-Za-z0-9 !.,\-\u0900-\u097F]","",t).strip()
@@ -342,7 +430,6 @@ def add_captions_ffmpeg(video_path, caption, duration_sec, style):
342
  ret=os.system(f'ffmpeg -y -i "{video_path}" -vf "{vf}" -c:a copy "{out}" -loglevel error')
343
  return out if (ret==0 and os.path.exists(out)) else video_path
344
 
345
-
346
  # ══════════════════════════════════════════════════════════════════
347
  # AUDIO
348
  # ══════════════════════════════════════════════════════════════════
@@ -390,27 +477,49 @@ def add_audio(video_path, caption, duration_sec, style):
390
  os.system(f'ffmpeg -y -i "{video_path}" -i "{audio}" -c:v copy -c:a aac -b:a 128k -shortest "{final}" -loglevel error')
391
  return final if os.path.exists(final) else video_path
392
 
393
-
394
  # ══════════════════════════════════════════════════════════════════
395
- # MAIN PIPELINE
396
  # ══════════════════════════════════════════════════════════════════
397
  def generate(images, caption, style, language, duration, add_aud, add_cap, progress=gr.Progress()):
398
- # Filter out None images
399
- pils = [img if isinstance(img,Image.Image) else Image.fromarray(img)
400
- for img in (images or []) if img is not None]
401
- if not pils: return None, "⚠️ Upload at least 1 image!", "No image provided."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
403
  cap = caption.strip() or ""
404
  dur = int(duration)
405
  lines = []
406
- def log(msg): lines.append(msg); progress(min(.05+len(lines)*.08,.80),desc=msg)
407
 
408
- # ── Auto-detect from FIRST image ──────────────────────────────
409
  progress(.02, desc="πŸ” Auto-detecting category...")
410
  category, auto_prompt, detected_label = auto_detect(pils[0], cap)
411
  log(f"πŸ” Detected: {detected_label or category}")
412
 
413
- # If caption empty, auto-generate one
414
  if not cap:
415
  cap_hints = {
416
  "Fashion":"Step into style. Own the moment.",
@@ -424,19 +533,15 @@ def generate(images, caption, style, language, duration, add_aud, add_cap, progr
424
  cap = cap_hints.get(category,"Premium quality. Shop now.")
425
  log(f"πŸ’‘ Auto caption: {cap}")
426
 
427
- # ── Get insights ───────────────────────────────────────────────
428
  insight, ai_cap = get_insights(category, style, language, cap)
429
 
430
- # ── Generate video per image ───────────────────────────────────
431
  video_paths = []
432
- clip_dur = max(4, dur // len(pils)) # split duration across images
433
 
434
  for idx, pil in enumerate(pils):
435
  log(f"🎬 Image {idx+1}/{len(pils)}...")
436
- # Re-detect for each image but use same prompt style
437
  _, img_prompt, _ = auto_detect(pil, cap)
438
  full_prompt = f"{img_prompt}, {cap[:60]}"
439
-
440
  vpath, model = get_video(pil, full_prompt, clip_dur, cb=log if idx==0 else None)
441
 
442
  if add_cap:
@@ -447,14 +552,12 @@ def generate(images, caption, style, language, duration, add_aud, add_cap, progr
447
  video_paths.append(vpath)
448
  log(f"βœ… Clip {idx+1} done ({model})")
449
 
450
- # ── Merge if multiple ─────────────────────────────────────────
451
  if len(video_paths) > 1:
452
  log("πŸ”— Merging clips...")
453
  final = merge_videos(video_paths)
454
  else:
455
  final = video_paths[0]
456
 
457
- # ── Audio on merged video ─────────────────────────────────────
458
  if add_aud:
459
  log("🎡 Adding music + voice...")
460
  final = add_audio(final, cap, dur, style.lower())
@@ -462,64 +565,255 @@ def generate(images, caption, style, language, duration, add_aud, add_cap, progr
462
  progress(1.0, desc="βœ… Done!")
463
  return final, "\n".join(lines), insight
464
 
465
-
466
  # ══════════════════════════════════════════════════════════════════
467
  # UI
468
  # ══════════════════════════════════════════════════════════════════
469
- css="""
470
- #title{text-align:center;font-size:2.3rem;font-weight:900}
471
- #sub{text-align:center;color:#999;margin-bottom:1.2rem;font-size:1rem}
472
  .insight{font-family:monospace;font-size:.86rem;line-height:1.75}
 
 
 
 
473
  """
 
474
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="violet")) as demo:
475
  gr.Markdown("# 🎬 AI Reel Generator", elem_id="title")
476
- gr.Markdown("Upload 1-5 images β†’ AI auto-detects category β†’ cinematic reel + smart posting strategy", elem_id="sub")
477
-
478
- with gr.Row():
479
- # ── LEFT ──────────────────────────────────────────────────
480
- with gr.Column(scale=1):
481
- img_in = gr.Gallery(
482
- label="πŸ“Έ Upload 1–5 Images (drag & drop)",
483
- type="pil",
484
- columns=5, rows=1,
485
- height=200,
486
- object_fit="contain",
487
- )
488
- cap_in = gr.Textbox(
489
- label="✏️ Caption / Description (leave blank = auto-detect)",
490
- placeholder="e.g. Premium sneakers with star design... or leave empty!",
491
- lines=2,
492
- )
493
- with gr.Row():
494
- sty_dd = gr.Dropdown(["Premium","Energetic","Fun"], value="Premium", label="🎨 Style")
495
- lang_dd = gr.Dropdown(["English","Hindi","Hinglish"], value="English", label="🌐 Language")
496
 
497
- dur_sl = gr.Slider(minimum=5, maximum=20, value=6, step=1,
498
- label="⏱️ Total Duration (seconds)")
 
499
  with gr.Row():
500
- aud_cb = gr.Checkbox(label="🎡 Music + Voice", value=True)
501
- cap_cb = gr.Checkbox(label="πŸ’¬ Captions", value=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
- gen_btn = gr.Button("πŸš€ Generate Reel + Smart Insights", variant="primary", size="lg")
504
- gr.Markdown(
505
- "**πŸ”— AI Chain:** LTX-2 ⚑ β†’ Wan 2.2 β†’ SVD-XT β†’ Kling β†’ LTX-Video β†’ Ken Burns βœ…\n\n"
506
- "πŸ’‘ Upload multiple images for a multi-clip reel!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  )
508
 
509
- # ── RIGHT ─────────────────────────────────────────────────
510
- with gr.Column(scale=1):
511
- vid_out = gr.Video(label="πŸŽ₯ Cinematic Reel", height=400)
512
- insight_out = gr.Textbox(
513
- label="πŸ“Š Smart Insights β€” Auto-Detected + Audience + Posting Time + AI Caption",
514
- lines=16, interactive=False, elem_classes="insight",
515
- )
516
- log_out = gr.Textbox(label="πŸ”§ Log", lines=4, interactive=False)
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  gen_btn.click(
519
  fn=generate,
520
  inputs=[img_in, cap_in, sty_dd, lang_dd, dur_sl, aud_cb, cap_cb],
521
  outputs=[vid_out, log_out, insight_out],
522
  )
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  if __name__ == "__main__":
525
  demo.launch()
 
1
+ import os, tempfile, io, math, time, threading, re, random, json
2
  import numpy as np
3
  import cv2
4
  import gradio as gr
 
14
  print("βœ… HF ready")
15
  except Exception as e: print(f"⚠️ HF: {e}")
16
 
17
+ # ── TEMPLATE STORAGE ──────────────────────────────────────────────
18
+ TEMPLATES_FILE = "saved_templates.json"
19
+
20
+ def load_templates():
21
+ if os.path.exists(TEMPLATES_FILE):
22
+ try:
23
+ with open(TEMPLATES_FILE, "r") as f:
24
+ return json.load(f)
25
+ except: pass
26
+ return {}
27
+
28
+ def save_template(name, style, language, duration, caption, add_aud, add_cap):
29
+ templates = load_templates()
30
+ templates[name] = {
31
+ "style": style, "language": language, "duration": duration,
32
+ "caption": caption, "add_audio": add_aud, "add_captions": add_cap,
33
+ "created": time.strftime("%Y-%m-%d %H:%M")
34
+ }
35
+ with open(TEMPLATES_FILE, "w") as f:
36
+ json.dump(templates, f, indent=2)
37
+ return f"βœ… Template '{name}' saved!", list(templates.keys())
38
+
39
+ def get_template_names():
40
+ return list(load_templates().keys())
41
+
42
+ def load_template(name):
43
+ templates = load_templates()
44
+ if name in templates:
45
+ t = templates[name]
46
+ return t["style"], t["language"], t["duration"], t["caption"], t["add_audio"], t["add_captions"]
47
+ return "Premium", "English", 6, "", True, True
48
+
49
+ def delete_template(name):
50
+ templates = load_templates()
51
+ if name in templates:
52
+ del templates[name]
53
+ with open(TEMPLATES_FILE, "w") as f:
54
+ json.dump(templates, f, indent=2)
55
+ return f"πŸ—‘οΈ Template '{name}' deleted!", list(templates.keys())
56
+ return "Template not found!", list(templates.keys())
57
+
58
+ def export_template(name):
59
+ templates = load_templates()
60
+ if name in templates:
61
+ out = f"template_{name.replace(' ','_')}.json"
62
+ with open(out, "w") as f:
63
+ json.dump({name: templates[name]}, f, indent=2)
64
+ return out
65
+ return None
66
+
67
  # ══════════════════════════════════════════════════════════════════
68
+ # AUTO-DETECT
69
  # ══════════════════════════════════════════════════════════════════
70
  def auto_detect(pil_image, user_caption=""):
 
 
 
 
 
71
  category = "Product/Other"
72
  label = ""
 
 
73
  if hf_client:
74
  try:
75
+ buf = io.BytesIO(); pil_image.save(buf, format="JPEG", quality=85)
76
+ result = hf_client.image_classification(image=buf.getvalue(), model="google/vit-base-patch16-224")
 
 
 
77
  if result:
78
  label = result[0].get("label","").lower()
 
79
  except Exception as e:
80
  print(f" ⚠️ classifier skip: {e}")
81
 
 
82
  label_map = {
83
+ "shoe":"Fashion","sneaker":"Fashion","boot":"Fashion","dress":"Fashion",
84
+ "shirt":"Fashion","jacket":"Fashion","jean":"Fashion","sandal":"Fashion","bag":"Fashion",
85
+ "pizza":"Food","burger":"Food","cake":"Food","food":"Food","coffee":"Food","sushi":"Food",
86
+ "laptop":"Tech","phone":"Tech","camera":"Tech","keyboard":"Tech","monitor":"Tech","tablet":"Tech",
87
+ "lipstick":"Beauty","cream":"Beauty","perfume":"Beauty","cosmetic":"Beauty","makeup":"Beauty",
88
+ "dumbbell":"Fitness","yoga":"Fitness","bottle":"Fitness","bicycle":"Fitness","jersey":"Fitness",
89
+ "plant":"Lifestyle","candle":"Lifestyle","chair":"Lifestyle","sofa":"Lifestyle","lamp":"Lifestyle",
 
 
 
 
 
 
90
  }
91
  for k,v in label_map.items():
92
  if k in label: category=v; break
93
 
 
94
  if category == "Product/Other" and user_caption:
95
  cap_low = user_caption.lower()
96
  if any(w in cap_low for w in ["shoe","sneaker","dress","outfit","wear","fashion","style","cloth","kurta"]): category="Fashion"
 
100
  elif any(w in cap_low for w in ["gym","fit","workout","protein","yoga","health","sport"]): category="Fitness"
101
  elif any(w in cap_low for w in ["home","decor","interior","lifestyle","aesthetic","candle"]): category="Lifestyle"
102
 
 
103
  prompts = {
104
  "Fashion": "cinematic fashion product shot, model wearing outfit, soft studio lighting, slow zoom, luxury feel",
105
  "Food": "cinematic food photography, steam rising, dramatic close-up, warm golden lighting, slow reveal",
 
111
  }
112
  auto_prompt = prompts.get(category, prompts["Product/Other"])
113
  if label: auto_prompt = f"{label} product, {auto_prompt}"
 
114
  return category, auto_prompt, label
115
 
 
116
  # ══════════════════════════════════════════════════════════════════
117
  # SMART INSIGHTS
118
  # ══════════════════════════════════════════════════════════════════
 
125
  "Lifestyle": {"best":"7:00 PM", "days":"Thu, Fri, Sat", "slots":["9AM","2PM","7PM"]},
126
  "Product/Other":{"best":"8:00 PM", "days":"Tue, Thu, Sat", "slots":["10AM","3PM","8PM"]},
127
  }
 
128
  AUDIENCES = {
129
  "Fashion": "πŸ‘— 18-35 yo females Β· Fashion lovers Β· Insta scrollers Β· Trend followers",
130
  "Food": "πŸ• 18-45 Β· Foodies Β· Home cooks Β· Restaurant goers Β· Food bloggers",
 
134
  "Lifestyle": "🌿 22-40 Β· Aspirational buyers Β· Aesthetic lovers Β· Home decor fans",
135
  "Product/Other":"πŸ›οΈ 18-45 Β· Online shoppers Β· Deal hunters Β· Value-conscious buyers",
136
  }
 
137
  CAPTIONS = {
138
  "English": {
139
  "Premium": ["✨ {cap} Quality that speaks for itself. πŸ›’ Shop Now β†’ Link in bio",
 
160
  "POV: Naya fav mil gaya πŸŽ‰ {cap} Bio mein link!"],
161
  },
162
  }
 
163
  HASHTAGS = {
164
  "Fashion": "#Fashion #OOTD #StyleInspo #NewCollection #Trending #ShopNow #Reels",
165
  "Food": "#FoodLovers #Foodie #FoodPhotography #Yummy #FoodReels #MustTry",
 
176
  tmpl = CAPTIONS.get(language, CAPTIONS["English"]).get(style, CAPTIONS["English"]["Premium"])
177
  ai_cap = random.choice(tmpl).replace("{cap}", clean_cap)
178
  tags = HASHTAGS.get(category, HASHTAGS["Product/Other"])
 
179
  insight = (
180
  f"πŸ“Š SMART INSIGHTS\n"
181
  f"{'━'*38}\n"
 
190
  )
191
  return insight, ai_cap
192
 
193
+ # ══════════════════════════════════════════════════════════════════
194
+ # AI EXPLAINER BOT
195
+ # ══════════════════════════════════════════════════════════════════
196
+ BOT_KB = {
197
+ # Tech questions
198
+ "how does this work": "πŸ€– **How it works:**\n1. You upload 1-5 product images\n2. AI auto-detects category (Fashion/Food/Tech etc.) using HuggingFace VIT model\n3. Ken Burns cinematic effect is applied (zoom + pan + color grading)\n4. AI generates captions, hashtags & best posting times\n5. Optional: BGM music + TTS voice overlay added via gTTS\n6. Final video exported as MP4 πŸ“±",
199
+
200
+ "ken burns": "🎬 **Ken Burns Effect** is a cinematic technique used here:\n- Smooth zoom in/out (scale 1.0β†’1.06)\n- Gentle pan left/right/up/down\n- Easing curves (cubic) for natural motion\n- Color grading per style (warm tones for Premium, saturated for Energetic)\n- Vignette overlay for cinematic look\n- Fade in/out at start and end\nThis runs 100% locally β€” no GPU needed! βœ…",
201
+
202
+ "hf models": "πŸ€— **HuggingFace AI Chain** (tried in order):\n1. **LTX-2 ⚑** β€” Lightricks, fastest video gen\n2. **Wan 2.2** β€” High quality image-to-video\n3. **SVD-XT** β€” Stable Video Diffusion by Stability AI\n4. **Kling** β€” KlingTeam's LivePortrait model\n5. **LTX-Video** β€” Fallback video model\n6. **Ken Burns βœ…** β€” Always works locally!\nEach model gets 50 seconds timeout before trying next.",
203
+
204
+ "auto detect": "πŸ” **Auto-Detection System:**\n- Uses Google ViT-base-patch16-224 model via HF API\n- Classifies image into 1000 ImageNet categories\n- Maps labels β†’ Fashion/Food/Tech/Beauty/Fitness/Lifestyle\n- Falls back to caption keyword matching if HF unavailable\n- Selects cinematic prompt style based on detected category",
205
+
206
+ "captions": "πŸ’¬ **Caption System:**\n- 3 languages: English / Hindi / Hinglish\n- 3 styles: Premium / Energetic / Fun\n- Uses ffmpeg drawtext filter for overlay\n- Animated fade-in/out effect\n- CTA button ('Shop Now') added automatically\n- Hashtag line auto-appended based on category",
207
+
208
+ "audio": "🎡 **Audio Pipeline:**\n- **BGM:** Generated programmatically using numpy (sine waves + kick drum + hi-hat)\n- BPM varies by style: Premium=88, Energetic=126, Fun=104\n- **Voice:** gTTS (Google TTS) narrates your caption\n- Both mixed using ffmpeg amix filter\n- BGM volume=20%, Voice=95%",
209
+
210
+ "error": "πŸ”§ **Common Errors & Fixes:**\n- **Gallery Error:** Fixed! Now handles PIL images + numpy arrays safely\n- **HF Timeout:** Models auto-fallback to Ken Burns (always works)\n- **ffmpeg missing:** Install with `apt install ffmpeg`\n- **No video output:** Check if images are valid PNG/JPG\n- **Template not loading:** Templates saved in `saved_templates.json`",
211
+
212
+ "template": "πŸ’Ύ **Template System:**\n- Save your settings (style, language, duration, caption) as named templates\n- Load them anytime with one click\n- Export as JSON to share with others\n- Delete templates you no longer need\n- Great for brand consistency across multiple reels!",
213
+
214
+ "unique features": "⭐ **Unique Features of this Project:**\n1. πŸ€– Multi-model AI chain with auto-fallback\n2. πŸ” Auto category detection from image\n3. πŸ’¬ Multilingual captions (Hindi/Hinglish/English)\n4. 🎡 Programmatic BGM generation (no external assets)\n5. πŸ’Ύ Template save/load/export system\n6. πŸ“Š Smart posting time analytics\n7. 🎬 Custom Ken Burns with style-specific color grading\n8. 🀝 This explainer bot!\n9. πŸ“Έ Multi-image β†’ multi-clip merge\n10. 🏷️ Auto-hashtag generation",
215
+
216
+ "styles": "🎨 **Style Modes:**\n- **Premium:** Warm tones, 88 BPM, slow elegant zoom, serif feel\n- **Energetic:** High saturation, 126 BPM, dynamic cuts, bold colors\n- **Fun:** Pastel tones, 104 BPM, bouncy motion, playful captions\nEach style affects: color grading, music BPM, caption tone, and CTA color",
217
+
218
+ "multi image": "πŸ“Έ **Multi-Image Reel:**\n- Upload up to 5 images\n- Each image gets its own video clip\n- Duration is split equally across clips\n- All clips merged using ffmpeg concat\n- Result: a smooth multi-product showcase reel!",
219
+
220
+ "languages": "🌐 **Language Support:**\n- **English:** Standard captions with emojis\n- **Hindi:** Full Devanagari script captions\n- **Hinglish:** Mixed Hindi-English (very popular on Indian reels)\nFont must support Unicode for Hindi β€” DejaVu or Liberation used automatically",
221
+ }
222
+
223
+ def bot_reply(user_msg, history):
224
+ if not user_msg.strip():
225
+ return history, ""
226
+
227
+ msg_low = user_msg.lower()
228
+ reply = None
229
+
230
+ # Match keywords
231
+ for key, answer in BOT_KB.items():
232
+ if any(word in msg_low for word in key.split()):
233
+ reply = answer
234
+ break
235
+
236
+ # Fuzzy fallbacks
237
+ if not reply:
238
+ if any(w in msg_low for w in ["model","ai","hf","huggingface"]): reply = BOT_KB["hf models"]
239
+ elif any(w in msg_low for w in ["video","cinematic","animation","zoom"]): reply = BOT_KB["ken burns"]
240
+ elif any(w in msg_low for w in ["detect","category","classify"]): reply = BOT_KB["auto detect"]
241
+ elif any(w in msg_low for w in ["music","sound","bgm","voice","audio"]): reply = BOT_KB["audio"]
242
+ elif any(w in msg_low for w in ["caption","text","overlay","subtitle"]): reply = BOT_KB["captions"]
243
+ elif any(w in msg_low for w in ["save","load","template","export"]): reply = BOT_KB["template"]
244
+ elif any(w in msg_low for w in ["fix","bug","problem","issue","not working","fail"]): reply = BOT_KB["error"]
245
+ elif any(w in msg_low for w in ["special","unique","different","best","feature"]): reply = BOT_KB["unique features"]
246
+ elif any(w in msg_low for w in ["style","premium","energetic","fun"]): reply = BOT_KB["styles"]
247
+ elif any(w in msg_low for w in ["hindi","hinglish","language","english"]): reply = BOT_KB["languages"]
248
+ elif any(w in msg_low for w in ["multiple","multi","images","clips","merge"]): reply = BOT_KB["multi image"]
249
+ elif any(w in msg_low for w in ["hello","hi","hey","namaste","hii"]): reply = "πŸ‘‹ Namaste! Main hun **ReelBot** β€” tumhara AI guide!\n\nMujhse poocho:\n- 'how does this work'\n- 'ken burns kya hai'\n- 'HF models kaunse hain'\n- 'unique features kya hain'\n- 'error fix kaise karein'\n- 'template kaise use karein'\n\nKoi bhi sawaal pucho! πŸš€"
250
+ else:
251
+ reply = ("πŸ€” Hmm, is topic par mujhe exact info nahi mili.\n\n"
252
+ "Try asking about:\n"
253
+ "β€’ `how does this work` β€” full pipeline\n"
254
+ "β€’ `ken burns` β€” video animation technique\n"
255
+ "β€’ `hf models` β€” AI model chain\n"
256
+ "β€’ `unique features` β€” what makes this special\n"
257
+ "β€’ `error` β€” troubleshooting\n"
258
+ "β€’ `template` β€” save/load settings\n"
259
+ "β€’ `audio` β€” music & voice system\n"
260
+ "β€’ `styles` β€” Premium/Energetic/Fun")
261
+
262
+ history = history or []
263
+ history.append({"role": "user", "content": user_msg})
264
+ history.append({"role": "assistant", "content": reply})
265
+ return history, ""
266
 
267
  # ══════════════════════════════════════════════════════════════════
268
  # HF VIDEO CHAIN
 
307
  time.sleep(0.5)
308
  return ken_burns(pil, duration_sec=dur), "Ken Burns"
309
 
 
310
  # ══════════════════════════════════════════════════════════════════
311
  # KEN BURNS
312
  # ══════════════════════════════════════════════════════════════════
 
326
  img_r=ImageEnhance.Contrast(img_r).enhance(1.05)
327
  img_r=ImageEnhance.Color(img_r).enhance(1.08)
328
 
 
329
  bg=img.resize((TW,TH),Image.LANCZOS).filter(ImageFilter.GaussianBlur(18))
330
  bg=ImageEnhance.Brightness(bg).enhance(0.55)
331
  canvas=bg.copy(); canvas.paste(img_r,((TW-nw)//2,(TH-nh)//2))
 
382
  writer.release()
383
  return tmp.name
384
 
 
385
  # ══════════════════════════════════════════════════════════════════
386
  # MULTI-VIDEO MERGE
387
  # ══════════════════════════════════════════════════════════════════
388
  def merge_videos(paths):
 
389
  if len(paths)==1: return paths[0]
390
  out=paths[0].replace(".mp4","_merged.mp4")
 
 
391
  lst=tempfile.NamedTemporaryFile(suffix=".txt",mode="w",delete=False)
392
  for p in paths: lst.write(f"file '{p}'\n")
393
  lst.flush()
 
 
394
  ret=os.system(
395
  f'ffmpeg -y -f concat -safe 0 -i "{lst.name}" '
396
  f'-c:v libx264 -c:a aac -b:a 128k -movflags +faststart '
 
398
  )
399
  return out if (ret==0 and os.path.exists(out)) else paths[-1]
400
 
 
401
  # ══════════════════════════════════════════════════════════════════
402
+ # CAPTIONS
403
  # ══════════════════════════════════════════════════════════════════
404
  def add_captions_ffmpeg(video_path, caption, duration_sec, style):
405
  def clean(t): return re.sub(r"[^A-Za-z0-9 !.,\-\u0900-\u097F]","",t).strip()
 
430
  ret=os.system(f'ffmpeg -y -i "{video_path}" -vf "{vf}" -c:a copy "{out}" -loglevel error')
431
  return out if (ret==0 and os.path.exists(out)) else video_path
432
 
 
433
  # ══════════════════════════════════════════════════════════════════
434
  # AUDIO
435
  # ══════════════════════════════════════════════════════════════════
 
477
  os.system(f'ffmpeg -y -i "{video_path}" -i "{audio}" -c:v copy -c:a aac -b:a 128k -shortest "{final}" -loglevel error')
478
  return final if os.path.exists(final) else video_path
479
 
 
480
  # ══════════════════════════════════════════════════════════════════
481
+ # MAIN PIPELINE (FIXED: safe image conversion)
482
  # ══════════════════════════════════════════════════════════════════
483
  def generate(images, caption, style, language, duration, add_aud, add_cap, progress=gr.Progress()):
484
+ # βœ… FIX: Safe multi-format image handling
485
+ pils = []
486
+ if images:
487
+ for img in images:
488
+ if img is None:
489
+ continue
490
+ try:
491
+ if isinstance(img, Image.Image):
492
+ pils.append(img.convert("RGB"))
493
+ elif isinstance(img, np.ndarray):
494
+ pils.append(Image.fromarray(img).convert("RGB"))
495
+ elif isinstance(img, dict):
496
+ # Gradio sometimes wraps as dict
497
+ raw = img.get("composite") or img.get("image") or img.get("path")
498
+ if raw is not None:
499
+ if isinstance(raw, np.ndarray):
500
+ pils.append(Image.fromarray(raw).convert("RGB"))
501
+ elif isinstance(raw, Image.Image):
502
+ pils.append(raw.convert("RGB"))
503
+ elif isinstance(raw, str) and os.path.exists(raw):
504
+ pils.append(Image.open(raw).convert("RGB"))
505
+ elif isinstance(img, str) and os.path.exists(img):
506
+ pils.append(Image.open(img).convert("RGB"))
507
+ except Exception as e:
508
+ print(f" ⚠️ Skipping image: {e}")
509
+ continue
510
+
511
+ if not pils:
512
+ return None, "⚠️ Upload at least 1 valid image!", "No image provided."
513
 
514
  cap = caption.strip() or ""
515
  dur = int(duration)
516
  lines = []
517
+ def log(msg): lines.append(msg); progress(min(.05+len(lines)*.08,.80), desc=msg)
518
 
 
519
  progress(.02, desc="πŸ” Auto-detecting category...")
520
  category, auto_prompt, detected_label = auto_detect(pils[0], cap)
521
  log(f"πŸ” Detected: {detected_label or category}")
522
 
 
523
  if not cap:
524
  cap_hints = {
525
  "Fashion":"Step into style. Own the moment.",
 
533
  cap = cap_hints.get(category,"Premium quality. Shop now.")
534
  log(f"πŸ’‘ Auto caption: {cap}")
535
 
 
536
  insight, ai_cap = get_insights(category, style, language, cap)
537
 
 
538
  video_paths = []
539
+ clip_dur = max(4, dur // len(pils))
540
 
541
  for idx, pil in enumerate(pils):
542
  log(f"🎬 Image {idx+1}/{len(pils)}...")
 
543
  _, img_prompt, _ = auto_detect(pil, cap)
544
  full_prompt = f"{img_prompt}, {cap[:60]}"
 
545
  vpath, model = get_video(pil, full_prompt, clip_dur, cb=log if idx==0 else None)
546
 
547
  if add_cap:
 
552
  video_paths.append(vpath)
553
  log(f"βœ… Clip {idx+1} done ({model})")
554
 
 
555
  if len(video_paths) > 1:
556
  log("πŸ”— Merging clips...")
557
  final = merge_videos(video_paths)
558
  else:
559
  final = video_paths[0]
560
 
 
561
  if add_aud:
562
  log("🎡 Adding music + voice...")
563
  final = add_audio(final, cap, dur, style.lower())
 
565
  progress(1.0, desc="βœ… Done!")
566
  return final, "\n".join(lines), insight
567
 
 
568
  # ══════════════════════════════════════════════════════════════════
569
  # UI
570
  # ══════════════════════════════════════════════════════════════════
571
+ css = """
572
+ #title{text-align:center;font-size:2.3rem;font-weight:900;background:linear-gradient(135deg,#a855f7,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
573
+ #sub{text-align:center;color:#aaa;margin-bottom:1.2rem;font-size:1rem}
574
  .insight{font-family:monospace;font-size:.86rem;line-height:1.75}
575
+ .bot-container{border:1px solid #3a3a5c;border-radius:12px;padding:0;overflow:hidden}
576
+ .save-row{gap:8px}
577
+ .feature-badge{display:inline-block;background:linear-gradient(135deg,#7c3aed,#db2777);color:white;padding:2px 10px;border-radius:99px;font-size:.75rem;margin:2px}
578
+ .tab-label{font-weight:700}
579
  """
580
+
581
  with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="violet")) as demo:
582
  gr.Markdown("# 🎬 AI Reel Generator", elem_id="title")
583
+ gr.Markdown(
584
+ "Upload 1-5 images β†’ AI auto-detects category β†’ cinematic reel + smart posting strategy\n\n"
585
+ '<span class="feature-badge">Multi-Image</span>'
586
+ '<span class="feature-badge">Multilingual</span>'
587
+ '<span class="feature-badge">AI Chain</span>'
588
+ '<span class="feature-badge">Template Save/Share</span>'
589
+ '<span class="feature-badge">ReelBot πŸ€–</span>',
590
+ elem_id="sub"
591
+ )
 
 
 
 
 
 
 
 
 
 
 
592
 
593
+ with gr.Tabs():
594
+ # ── TAB 1: GENERATOR ─────────────────────────────────────
595
+ with gr.Tab("🎬 Generator", elem_classes="tab-label"):
596
  with gr.Row():
597
+ # LEFT
598
+ with gr.Column(scale=1):
599
+ img_in = gr.Gallery(
600
+ label="πŸ“Έ Upload 1–5 Images (drag & drop)",
601
+ type="pil",
602
+ columns=5, rows=1,
603
+ height=200,
604
+ object_fit="contain",
605
+ )
606
+ cap_in = gr.Textbox(
607
+ label="✏️ Caption / Description (leave blank = auto-detect)",
608
+ placeholder="e.g. Premium sneakers with star design... or leave empty!",
609
+ lines=2,
610
+ )
611
+ with gr.Row():
612
+ sty_dd = gr.Dropdown(["Premium","Energetic","Fun"], value="Premium", label="🎨 Style")
613
+ lang_dd = gr.Dropdown(["English","Hindi","Hinglish"], value="English", label="🌐 Language")
614
+
615
+ dur_sl = gr.Slider(minimum=5, maximum=20, value=6, step=1,
616
+ label="⏱️ Total Duration (seconds)")
617
+ with gr.Row():
618
+ aud_cb = gr.Checkbox(label="🎡 Music + Voice", value=True)
619
+ cap_cb = gr.Checkbox(label="πŸ’¬ Captions", value=True)
620
+
621
+ gen_btn = gr.Button("πŸš€ Generate Reel + Smart Insights", variant="primary", size="lg")
622
+ gr.Markdown(
623
+ "**πŸ”— AI Chain:** LTX-2 ⚑ β†’ Wan 2.2 β†’ SVD-XT β†’ Kling β†’ LTX-Video β†’ Ken Burns βœ…\n\n"
624
+ "πŸ’‘ Upload multiple images for a multi-clip reel!"
625
+ )
626
+
627
+ # RIGHT
628
+ with gr.Column(scale=1):
629
+ vid_out = gr.Video(label="πŸŽ₯ Cinematic Reel", height=400)
630
+ insight_out = gr.Textbox(
631
+ label="πŸ“Š Smart Insights",
632
+ lines=16, interactive=False, elem_classes="insight",
633
+ )
634
+ log_out = gr.Textbox(label="πŸ”§ Log", lines=4, interactive=False)
635
+
636
+ # ── TAB 2: TEMPLATES ─────────────────────────────────────
637
+ with gr.Tab("πŸ’Ύ Templates", elem_classes="tab-label"):
638
+ gr.Markdown("### πŸ’Ύ Save, Load & Share Your Reel Settings")
639
+
640
+ with gr.Row(elem_classes="save-row"):
641
+ tpl_name_in = gr.Textbox(label="Template Name", placeholder="e.g. My Brand Style", scale=3)
642
+ save_btn = gr.Button("πŸ’Ύ Save Current Settings", variant="primary", scale=1)
643
+
644
+ tpl_status = gr.Textbox(label="Status", interactive=False, lines=1)
645
+ tpl_list = gr.Dropdown(label="πŸ“‚ Saved Templates", choices=get_template_names(), interactive=True)
646
 
647
+ with gr.Row():
648
+ load_btn = gr.Button("πŸ“‚ Load Template", variant="secondary")
649
+ del_btn = gr.Button("πŸ—‘οΈ Delete Template", variant="stop")
650
+ export_btn = gr.Button("πŸ“€ Export as JSON")
651
+
652
+ export_file = gr.File(label="⬇️ Download Template JSON", visible=True)
653
+
654
+ gr.Markdown("""
655
+ **How to use Templates:**
656
+ 1. Configure your settings in the Generator tab
657
+ 2. Give it a name and click **Save Current Settings**
658
+ 3. Next time, just pick from the dropdown and **Load Template**
659
+ 4. **Export** to share with teammates or save as backup
660
+ """)
661
+
662
+ # ── TAB 3: REELBOT ───────────────────────────────────────
663
+ with gr.Tab("πŸ€– ReelBot", elem_classes="tab-label"):
664
+ gr.Markdown("""
665
+ ### πŸ€– ReelBot β€” Your AI Project Guide
666
+ _Ask me anything about how this project works, the tech used, features, and more!_
667
+ """)
668
+
669
+ bot_chatbox = gr.Chatbot(
670
+ label="πŸ’¬ Chat with ReelBot",
671
+ height=420,
672
+ type="messages",
673
+ avatar_images=(None, "https://api.dicebear.com/7.x/bottts/svg?seed=reelbot"),
674
+ value=[{
675
+ "role": "assistant",
676
+ "content": (
677
+ "πŸ‘‹ **Namaste! Main hun ReelBot!** πŸ€–\n\n"
678
+ "Main is project ke baare mein sab kuch jaanta hun.\n\n"
679
+ "**Mujhse poocho:**\n"
680
+ "β€’ `how does this work` β€” Full pipeline samjho\n"
681
+ "β€’ `ken burns` β€” Animation technique\n"
682
+ "β€’ `hf models` β€” AI model chain\n"
683
+ "β€’ `unique features` β€” Kya khaas hai is project mein\n"
684
+ "β€’ `error` β€” Bug troubleshooting\n"
685
+ "β€’ `template` β€” Settings save/share\n"
686
+ "β€’ `audio` β€” Music generation\n"
687
+ "β€’ `styles` β€” Premium/Energetic/Fun\n\n"
688
+ "**Koi bhi sawaal pucho! πŸš€**"
689
+ )
690
+ }]
691
  )
692
 
693
+ with gr.Row():
694
+ bot_input = gr.Textbox(
695
+ placeholder="Ask: 'how does this work?' or 'ken burns kya hai?' or 'unique features kya hain?'",
696
+ label="Your Question",
697
+ scale=5,
698
+ )
699
+ bot_send = gr.Button("Send πŸ“¨", variant="primary", scale=1)
 
700
 
701
+ with gr.Row():
702
+ gr.Button("how does this work").click(
703
+ lambda h: bot_reply("how does this work", h),
704
+ inputs=[bot_chatbox], outputs=[bot_chatbox, bot_input]
705
+ )
706
+ gr.Button("ken burns kya hai").click(
707
+ lambda h: bot_reply("ken burns", h),
708
+ inputs=[bot_chatbox], outputs=[bot_chatbox, bot_input]
709
+ )
710
+ gr.Button("unique features").click(
711
+ lambda h: bot_reply("unique features", h),
712
+ inputs=[bot_chatbox], outputs=[bot_chatbox, bot_input]
713
+ )
714
+ gr.Button("error fix").click(
715
+ lambda h: bot_reply("error fix", h),
716
+ inputs=[bot_chatbox], outputs=[bot_chatbox, bot_input]
717
+ )
718
+
719
+ # ── TAB 4: TECH EXPLAINED ────────────────────────────────
720
+ with gr.Tab("πŸ“š Tech Stack", elem_classes="tab-label"):
721
+ gr.Markdown("""
722
+ ## πŸ› οΈ Technology Used β€” Full Breakdown
723
+
724
+ ### 🎬 Video Generation
725
+ | Component | Technology | Purpose |
726
+ |-----------|-----------|---------|
727
+ | **Ken Burns Effect** | OpenCV + NumPy | Cinematic zoom/pan animation |
728
+ | **Color Grading** | NumPy array ops | Style-based color correction |
729
+ | **Vignette** | NumPy distance map | Cinematic edge darkening |
730
+ | **Video Encoding** | OpenCV VideoWriter | MP4 output @ 30fps |
731
+ | **AI Video** | HuggingFace InferenceClient | Image-to-video (when available) |
732
+
733
+ ### πŸ€— AI Model Chain
734
+ | Priority | Model | Provider | Type |
735
+ |----------|-------|----------|------|
736
+ | 1 | LTX-2 ⚑ | Lightricks | Fast I2V |
737
+ | 2 | Wan 2.2 | Wan-AI | High quality I2V |
738
+ | 3 | SVD-XT | Stability AI | Stable Video Diffusion |
739
+ | 4 | Kling | KlingTeam | LivePortrait |
740
+ | 5 | LTX-Video | Lightricks | Fallback I2V |
741
+ | 6 βœ… | Ken Burns | Local | Always works! |
742
+
743
+ ### 🎡 Audio System
744
+ | Component | Technology | Details |
745
+ |-----------|-----------|---------|
746
+ | **BGM Generation** | NumPy + wave | Sine waves, kick drum, hi-hat |
747
+ | **TTS Voice** | gTTS (Google TTS) | Caption narration |
748
+ | **Audio Mixing** | ffmpeg amix | BGM 20% + Voice 95% |
749
+ | **BPM by Style** | Custom logic | Premium=88, Energetic=126, Fun=104 |
750
+
751
+ ### πŸ’¬ Caption System
752
+ | Feature | Technology |
753
+ |---------|-----------|
754
+ | Text Overlay | ffmpeg drawtext filter |
755
+ | Fade Animation | ffmpeg alpha expression |
756
+ | Font | DejaVu / Liberation Sans Bold |
757
+ | Languages | English / Hindi / Hinglish |
758
+
759
+ ### πŸ” Auto-Detection
760
+ | Step | Technology |
761
+ |------|-----------|
762
+ | Image Classification | google/vit-base-patch16-224 |
763
+ | Label Mapping | Custom Python dict |
764
+ | Caption Fallback | Keyword matching |
765
+
766
+ ### 🌟 Unique Points
767
+ > βœ… **No GPU required** β€” Ken Burns always as fallback
768
+ > βœ… **Multilingual** β€” Hindi captions with Devanagari support
769
+ > βœ… **Programmatic BGM** β€” No audio files needed
770
+ > βœ… **Template system** β€” Save/load/export settings as JSON
771
+ > βœ… **AI fallback chain** β€” 5 models tried before local fallback
772
+ > βœ… **ReelBot** β€” Built-in explainer chatbot
773
+ > βœ… **Multi-image merge** β€” Up to 5 clips concatenated
774
+ > βœ… **Auto posting strategy** β€” AI-driven best time recommendation
775
+ """)
776
+
777
+ # ── EVENTS ────────────────────────────────────────────────────
778
  gen_btn.click(
779
  fn=generate,
780
  inputs=[img_in, cap_in, sty_dd, lang_dd, dur_sl, aud_cb, cap_cb],
781
  outputs=[vid_out, log_out, insight_out],
782
  )
783
 
784
+ # Template events
785
+ save_btn.click(
786
+ fn=save_template,
787
+ inputs=[tpl_name_in, sty_dd, lang_dd, dur_sl, cap_in, aud_cb, cap_cb],
788
+ outputs=[tpl_status, tpl_list],
789
+ )
790
+ load_btn.click(
791
+ fn=load_template,
792
+ inputs=[tpl_list],
793
+ outputs=[sty_dd, lang_dd, dur_sl, cap_in, aud_cb, cap_cb],
794
+ )
795
+ del_btn.click(
796
+ fn=delete_template,
797
+ inputs=[tpl_list],
798
+ outputs=[tpl_status, tpl_list],
799
+ )
800
+ export_btn.click(
801
+ fn=export_template,
802
+ inputs=[tpl_list],
803
+ outputs=[export_file],
804
+ )
805
+
806
+ # Bot events
807
+ bot_send.click(
808
+ fn=bot_reply,
809
+ inputs=[bot_input, bot_chatbox],
810
+ outputs=[bot_chatbox, bot_input],
811
+ )
812
+ bot_input.submit(
813
+ fn=bot_reply,
814
+ inputs=[bot_input, bot_chatbox],
815
+ outputs=[bot_chatbox, bot_input],
816
+ )
817
+
818
  if __name__ == "__main__":
819
  demo.launch()