GLAkavya commited on
Commit
af9a17b
·
verified ·
1 Parent(s): c980c4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +367 -304
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, tempfile, io, math, time, threading
2
  import numpy as np
3
  import cv2
4
  import gradio as gr
@@ -14,100 +14,236 @@ if hf_token:
14
  print("✅ HF ready")
15
  except Exception as e: print(f"⚠️ HF: {e}")
16
 
17
- # ── HF MODELS ─────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  HF_MODELS = [
19
- {"id": "Lightricks/LTX-2", "name": "LTX-2 ⚡"},
20
- {"id": "Wan-AI/Wan2.2-I2V-A14B", "name": "Wan 2.2"},
21
- {"id": "stabilityai/stable-video-diffusion-img2vid-xt", "name": "SVD-XT"},
22
- {"id": "KlingTeam/LivePortrait", "name": "Kling LivePortrait"},
23
- {"id": "Lightricks/LTX-Video", "name": "LTX-Video"},
24
- {"id": "__local__", "name": "Ken Burns ✅"},
25
  ]
26
 
27
- def pil_to_bytes(img):
28
- b=io.BytesIO(); img.save(b,format="JPEG",quality=92); return b.getvalue()
29
-
30
  def run_timeout(fn, sec, *a, **kw):
31
  box=[None]; err=[None]
32
  def r():
33
  try: box[0]=fn(*a,**kw)
34
  except Exception as e: err[0]=str(e)
35
  t=threading.Thread(target=r,daemon=True); t.start(); t.join(timeout=sec)
36
- if t.is_alive(): print(f" ⏱ timeout"); return None
37
- if err[0]: print(f" ❌ {err[0][:80]}")
38
  return box[0]
39
 
40
  def try_hf(model_id, pil, prompt):
41
  if not hf_client: return None
42
  try:
43
- r=hf_client.image_to_video(image=pil_to_bytes(pil),model=model_id,prompt=prompt)
 
44
  return r.read() if hasattr(r,"read") else r
45
  except Exception as e: print(f" ❌ {model_id}: {e}"); return None
46
 
47
- def get_video(pil, prompt, cb=None):
48
  for m in HF_MODELS:
49
  mid,mname=m["id"],m["name"]
50
  if cb: cb(f"⏳ Trying: {mname}")
51
  if mid=="__local__":
52
- return ken_burns(pil), mname
53
  data=run_timeout(try_hf,50,mid,pil,prompt)
54
  if data:
55
  t=tempfile.NamedTemporaryFile(suffix=".mp4",delete=False)
56
  t.write(data); t.flush()
57
  return t.name, mname
58
- time.sleep(1)
59
- return ken_burns(pil), "Ken Burns"
60
 
61
 
62
  # ══════════════════════════════════════════════════════════════════
63
- # KEN BURNS (working, image always shows)
64
  # ══════════════════════════════════════════════════════════════════
65
- def ease(t): t=max(0.,min(1.,t)); return t*t*(3-2*t)
66
- def ease_cubic(t): t=max(0.,min(1.,t)); return 4*t*t*t if t<.5 else 1-math.pow(-2*t+2,3)/2
67
- def ease_expo(t): return 1-math.pow(2,-10*t) if t<1 else 1.
68
- def ease_bounce(t):
69
- if t<1/2.75: return 7.5625*t*t
70
- elif t<2/2.75: t-=1.5/2.75; return 7.5625*t*t+.75
71
- elif t<2.5/2.75: t-=2.25/2.75; return 7.5625*t*t+.9375
72
- else: t-=2.625/2.75; return 7.5625*t*t+.984375
73
 
74
  def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
75
- TW,TH=720,1280
76
- # Small pad — just enough for gentle movement, no aggressive zoom
77
- pad=60; BW,BH=TW+pad*2,TH+pad*2
78
- total=duration_sec*fps
79
 
80
- # Prepare image — fit full image, letterbox if needed
81
  img=pil.convert("RGB"); sw,sh=img.size
82
- # Fit entire image inside TH height, pad sides with blurred bg
83
- scale=TH/sh; nw=int(sw*scale); nh=TH
84
- if nw>TW: scale=TW/sw; nw=TW; nh=int(sh*scale)
85
- img_resized=img.resize((nw,nh),Image.LANCZOS)
86
- # Blurred background fill
87
- bg=img.resize((TW,TH),Image.LANCZOS)
88
- bg=bg.filter(ImageFilter.GaussianBlur(radius=20))
89
- bg_arr=np.array(ImageEnhance.Brightness(bg).enhance(0.5))
90
- canvas=Image.fromarray(bg_arr)
91
- # Paste sharp image centered
92
- px=(TW-nw)//2; py=(TH-nh)//2
93
- canvas.paste(img_resized,(px,py))
94
- canvas=canvas.filter(ImageFilter.UnsharpMask(radius=0.8,percent=110,threshold=2))
95
- canvas=ImageEnhance.Contrast(canvas).enhance(1.05)
96
- canvas=ImageEnhance.Color(canvas).enhance(1.08)
97
  base=np.array(canvas.resize((BW,BH),Image.LANCZOS))
98
 
99
- # Pre-baked vignette mask (very subtle)
100
  Y,X=np.ogrid[:TH,:TW]
101
  dist=np.sqrt(((X-TW/2)/(TW/2))**2+((Y-TH/2)/(TH/2))**2)
102
  vmask=np.clip(1.-0.22*np.maximum(dist-0.85,0)**2,0,1).astype(np.float32)
103
 
104
- # GENTLE zoom: 1.001.06 max — full image always visible
105
- SEG=[
106
- (0.00,0.30, 1.00,1.04, 0, -int(pad*.40), 0, -int(pad*.40)),
107
- (0.30,0.60, 1.04,1.06, -int(pad*.30), int(pad*.30), -int(pad*.40),-int(pad*.70)),
108
- (0.60,0.80, 1.06,1.04, int(pad*.30), int(pad*.50), -int(pad*.70),-int(pad*.40)),
109
- (0.80,1.00, 1.04,1.00, int(pad*.50), 0, -int(pad*.40), 0),
110
- ]
111
 
112
  tmp=tempfile.NamedTemporaryFile(suffix=".mp4",delete=False)
113
  writer=cv2.VideoWriter(tmp.name,cv2.VideoWriter_fourcc(*"mp4v"),fps,(TW,TH))
@@ -117,10 +253,9 @@ def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
117
  zoom=pan_x=pan_y=None
118
  for t0,t1,z0,z1,px0,px1,py0,py1 in SEG:
119
  if t0<=tg<=t1:
120
- te=ease_cubic((tg-t0)/(t1-t0))
121
  zoom=z0+(z1-z0)*te; pan_x=int(px0+(px1-px0)*te); pan_y=int(py0+(py1-py0)*te); break
122
  if zoom is None: zoom,pan_x,pan_y=1.,0,0
123
- # No shake — keeps image stable and well-framed
124
 
125
  cw,ch=int(TW/zoom),int(TH/zoom)
126
  ox,oy=BW//2+pan_x,BH//2+pan_y
@@ -130,31 +265,21 @@ def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
130
 
131
  frame=cv2.resize(base[y1:y2,x1:x2],(TW,TH),interpolation=cv2.INTER_LINEAR)
132
 
133
- # Very subtle color grade
134
  f=frame.astype(np.float32)/255.
135
  if style=="premium":
136
- f[:,:,0]=np.clip(f[:,:,0]*1.03+.01,0,1)
137
- f[:,:,2]=np.clip(f[:,:,2]*1.02,0,1)
138
  elif style=="energetic":
139
- gray=0.299*f[:,:,0:1]+0.587*f[:,:,1:2]+0.114*f[:,:,2:3]
140
- f=np.clip(gray+1.2*(f-gray),0,1); f=np.clip(f*1.04,0,1)
141
  elif style=="fun":
142
- f[:,:,0]=np.clip(f[:,:,0]*1.05,0,1)
143
- f[:,:,1]=np.clip(f[:,:,1]*1.03,0,1)
144
  frame=np.clip(f*255,0,255).astype(np.uint8)
145
-
146
- # Vignette
147
  frame=np.clip(frame.astype(np.float32)*vmask[:,:,None],0,255).astype(np.uint8)
148
-
149
- # Grain
150
- frame=np.clip(frame.astype(np.float32)+np.random.normal(0,3,frame.shape),0,255).astype(np.uint8)
151
-
152
- # Bars
153
  frame[:36,:]=0; frame[-36:,:]=0
154
 
155
- # Fade in (2%) / out (5%)
156
- if tg<0.02: alpha=ease_expo(tg/0.02)
157
- elif tg>0.95: alpha=ease(1-(tg-0.95)/0.05)
158
  else: alpha=1.
159
  if alpha<1.: frame=np.clip(frame.astype(np.float32)*alpha,0,255).astype(np.uint8)
160
 
@@ -164,100 +289,86 @@ def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
164
 
165
 
166
  # ══════════════════════════════════════════════════════════════════
167
- # CAPTIONS — burn into existing video via ffmpeg
168
  # ══════════════════════════════════════════════════════════════════
169
- def add_captions_ffmpeg(video_path, caption, duration_sec, style):
170
- """Burn animated captions + hashtag tag + shop-now CTA using ffmpeg drawtext."""
171
- import re
172
- def clean(t): return re.sub(r"[^A-Za-z0-9 !.,-]","",t).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- words=caption.strip().split()
175
- mid=max(1,len(words)//2)
176
- line1=clean(" ".join(words[:mid]))
177
- line2=clean(" ".join(words[mid:])) if len(words)>1 else line1
178
 
179
- colors={"premium":"FFD232","energetic":"3CC8FF","fun":"FF78C8"}
180
- col=colors.get(style,"FFFFFF")
 
 
 
 
 
 
 
181
  out=video_path.replace(".mp4","_cap.mp4")
182
-
183
- font_paths=[
184
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
185
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
186
- "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
187
- ]
188
- font=""; font_reg=""
189
- for p in font_paths:
190
- if os.path.exists(p): font=f":fontfile='{p}'"; font_reg=font; break
191
-
192
- def dt(text, start, end, y, size=42, color=None, box_alpha="0.60"):
193
- c = color or col
194
- fd=0.4
195
- return (
196
- f"drawtext=text='{text}'{font}"
197
- f":fontsize={size}:fontcolor=#{c}"
198
- f":x=(w-text_w)/2:y={y}"
199
- f":box=1:boxcolor=black@{box_alpha}:boxborderw=14"
200
- f":enable='between(t,{start},{end})'"
201
- f":alpha='if(lt(t,{start+fd}),(t-{start})/{fd},if(gt(t,{end-fd}),({end}-t)/{fd},1))'"
202
- )
203
-
204
- end2 = min(duration_sec-0.2, 6.5)
205
-
206
- # 1. Main captions — inside frame, above bars
207
- cap1 = dt(line1, 1.0, 3.5, "h-190")
208
- cap2 = dt(line2, 3.8, end2, "h-190")
209
-
210
- # 2. "Shop Now" CTA — appears at 4.5s, small, bottom center
211
- cta_colors={"premium":"FF9900","energetic":"FF4444","fun":"AA44FF"}
212
- cta = dt("Shop Now >", 4.5, end2, "h-130", size=32, color=cta_colors.get(style,"FF9900"), box_alpha="0.70")
213
-
214
- # 3. Hashtag top-left — appears early
215
- tag = dt("#NewCollection", 0.5, 3.0, "60", size=28, color="FFFFFF", box_alpha="0.40")
216
-
217
- vf = ",".join([cap1, cap2, cta, tag])
218
-
219
  ret=os.system(f'ffmpeg -y -i "{video_path}" -vf "{vf}" -c:a copy "{out}" -loglevel error')
220
  return out if (ret==0 and os.path.exists(out)) else video_path
221
 
222
 
223
  # ══════════════════════════════════════════════════════════════════
224
- # AUDIO — BGM + optional TTS
225
  # ══════════════════════════════════════════════════════════════════
226
  def make_bgm(duration_sec, out_path, style="premium"):
227
  import wave
228
- sr=44100; n=int(sr*duration_sec)
229
- t=np.linspace(0,duration_sec,n,endpoint=False)
230
- bpm={"premium":88,"energetic":126,"fun":104}.get(style,88)
231
- beat=60./bpm
232
-
233
  kick=np.zeros(n,np.float32)
234
  for i in range(int(duration_sec/beat)+2):
235
  s=int(i*beat*sr)
236
  if s>=n: break
237
- l=min(int(sr*.10),n-s)
238
- env=np.exp(-20*np.arange(l)/sr)
239
  kick[s:s+l]+=env*np.sin(2*math.pi*55*np.exp(-25*np.arange(l)/sr)*np.arange(l)/sr)*0.55
240
-
241
- bass_f={"premium":55,"energetic":80,"fun":65}.get(style,55)
242
- bass=np.sin(2*math.pi*bass_f*t)*0.10*(0.5+0.5*np.sin(2*math.pi*(bpm/60/4)*t))
243
-
244
  mf={"premium":[261,329,392],"energetic":[330,415,494],"fun":[392,494,587]}.get(style,[261,329,392])
245
  mel=np.zeros(n,np.float32)
246
  for j,f in enumerate(mf):
247
- env=np.clip(0.5+0.5*np.sin(2*math.pi*1.5*t-j*2.1),0,1)
248
- mel+=np.sin(2*math.pi*f*t)*env*0.045
249
-
250
  hat=np.zeros(n,np.float32)
251
- hs=beat/2
252
- for i in range(int(duration_sec/hs)+2):
253
- s=int(i*hs*sr)
254
  if s>=n: break
255
- l=min(int(sr*.03),n-s)
256
- hat[s:s+l]+=np.random.randn(l)*np.exp(-80*np.arange(l)/sr)*0.06
257
-
258
  mix=np.clip((kick+bass+mel+hat)*0.18,-1,1)
259
  fade=int(sr*.5); mix[:fade]*=np.linspace(0,1,fade); mix[-fade:]*=np.linspace(1,0,fade)
260
-
261
  with wave.open(out_path,"w") as wf:
262
  wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(sr)
263
  wf.writeframes((mix*32767).astype(np.int16).tobytes())
@@ -266,197 +377,149 @@ def add_audio(video_path, caption, duration_sec, style):
266
  bgm=video_path.replace(".mp4","_bgm.wav")
267
  final=video_path.replace(".mp4","_final.mp4")
268
  make_bgm(duration_sec, bgm, style)
269
-
270
- # Try TTS voiceover
271
  audio=bgm
272
  try:
273
  from gtts import gTTS
274
- tts_mp3=video_path.replace(".mp4","_tts.mp3")
275
- tts_wav=video_path.replace(".mp4","_tts.wav")
276
- gTTS(text=caption[:200],lang="en",slow=False).save(tts_mp3)
277
  mixed=video_path.replace(".mp4","_mix.wav")
278
- os.system(f'ffmpeg -y -i "{bgm}" -i "{tts_mp3}" '
279
- f'-filter_complex "[0]volume=0.20[a];[1]volume=0.95[b];[a][b]amix=inputs=2:duration=first" '
280
  f'-t {duration_sec} "{mixed}" -loglevel error')
281
  if os.path.exists(mixed): audio=mixed
282
- except Exception as e: print(f" TTS skip: {e}")
283
-
284
- os.system(f'ffmpeg -y -i "{video_path}" -i "{audio}" '
285
- f'-c:v copy -c:a aac -b:a 128k -shortest "{final}" -loglevel error')
286
  return final if os.path.exists(final) else video_path
287
 
288
 
289
  # ══════════════════════════════════════════════════════════════════
290
- # AI BRAIN — Captions, Posting Time, Target Audience
291
- # ══════════════════════════════════════════════════════════════════
292
-
293
- POSTING_TIMES = {
294
- "Fashion": {"slots":["7:00 AM","12:00 PM","6:00 PM","9:00 PM"],"best":"9:00 PM","days":"Tue, Thu, Fri"},
295
- "Food": {"slots":["11:00 AM","1:00 PM","7:00 PM"],"best":"12:00 PM","days":"Mon, Wed, Sat"},
296
- "Tech": {"slots":["8:00 AM","12:00 PM","5:00 PM"],"best":"8:00 AM","days":"Mon, Tue, Wed"},
297
- "Beauty": {"slots":["8:00 AM","1:00 PM","8:00 PM"],"best":"8:00 PM","days":"Wed, Fri, Sun"},
298
- "Fitness": {"slots":["6:00 AM","12:00 PM","7:00 PM"],"best":"6:00 AM","days":"Mon, Wed, Fri"},
299
- "Lifestyle": {"slots":["9:00 AM","2:00 PM","7:00 PM"],"best":"7:00 PM","days":"Thu, Fri, Sat"},
300
- "Product/Other":{"slots":["10:00 AM","3:00 PM","8:00 PM"],"best":"8:00 PM","days":"Tue, Thu, Sat"},
301
- }
302
-
303
- AUDIENCES = {
304
- "Fashion": "👗 18-35 yo females, fashion lovers, Instagram scrollers, trend followers",
305
- "Food": "🍕 18-45 yo foodies, home cooks, restaurant goers, food bloggers",
306
- "Tech": "💻 20-40 yo tech enthusiasts, early adopters, gadget buyers, professionals",
307
- "Beauty": "💄 16-35 yo beauty lovers, skincare fans, makeup artists, self-care community",
308
- "Fitness": "💪 18-40 yo gym goers, health-conscious buyers, athletes, wellness seekers",
309
- "Lifestyle": "🌿 22-40 yo aspirational buyers, aesthetic lovers, home decor fans",
310
- "Product/Other":"🛍️ 18-45 yo online shoppers, deal hunters, value-conscious buyers",
311
- }
312
-
313
- CAPTION_TEMPLATES = {
314
- "English": {
315
- "Premium": ["{cap} ✨ Quality that speaks for itself. 🛒 Shop Now → Link in bio",
316
- "Elevate your style. {cap} 💫 DM us to order!"],
317
- "Energetic": ["🔥 {cap} Hit different. Grab yours NOW 👆 Limited stock!",
318
- "⚡ Game changer alert! {cap} Don't sleep on this 🚀"],
319
- "Fun": ["Obsessed with this!! 😍 {cap} Tag someone who needs it 👇",
320
- "POV: You just found your new fav 🎉 {cap} Link in bio!"],
321
- },
322
- "Hindi": {
323
- "Premium": ["{cap} ✨ क्वालिटी जो बोलती है। 🛒 अभी खरीदें → Bio में link",
324
- "अपना स्टाइल बढ़ाएं। {cap} 💫 Order के लिए DM करें!"],
325
- "Energetic": ["🔥 {cap} एकदम अलग है! अभी grab करो 👆 Limited stock!",
326
- "⚡ Game changer! {cap} मत सोचो, order करो 🚀"],
327
- "Fun": ["इसके साथ तो दीवाने हो जाओगे!! 😍 {cap} किसी को tag करो 👇",
328
- "POV: नया favourite मिल गया 🎉 {cap} Bio में link है!"],
329
- },
330
- "Hinglish": {
331
- "Premium": ["{cap} ✨ Quality toh dekho yaar! 🛒 Shop karo → Bio mein link",
332
- "Style upgrade time! {cap} 💫 DM karo order ke liye!"],
333
- "Energetic": ["🔥 {cap} Bilkul alag hai bhai! Abhi lo 👆 Limited stock!",
334
- "⚡ Ek dum fire hai! {cap} Mat ruko, order karo 🚀"],
335
- "Fun": ["Yaar yeh toh kamaal hai!! 😍 {cap} Kisi ko tag karo 👇",
336
- "POV: Naya fav mil gaya 🎉 {cap} Bio mein link hai!"],
337
- },
338
- }
339
-
340
- def detect_category(caption):
341
- cap_low = caption.lower()
342
- if any(w in cap_low for w in ["shoe","sneaker","dress","outfit","wear","fashion","style","cloth","jeans","kurta"]):
343
- return "Fashion"
344
- if any(w in cap_low for w in ["food","eat","recipe","cook","restaurant","cafe","pizza","biryani"]):
345
- return "Food"
346
- if any(w in cap_low for w in ["phone","laptop","tech","gadget","device","app","software","camera"]):
347
- return "Tech"
348
- if any(w in cap_low for w in ["skin","beauty","makeup","lipstick","cream","hair","glow","face"]):
349
- return "Beauty"
350
- if any(w in cap_low for w in ["gym","fit","workout","protein","yoga","health","run","sport"]):
351
- return "Fitness"
352
- if any(w in cap_low for w in ["home","decor","interior","lifestyle","aesthetic","plant","candle"]):
353
- return "Lifestyle"
354
- return "Product/Other"
355
-
356
- def get_smart_insights(caption, style, language):
357
- import random, re
358
- category = detect_category(caption)
359
- pt = POSTING_TIMES[category]
360
- audience = AUDIENCES[category]
361
-
362
- # Generate caption in selected language
363
- templates = CAPTION_TEMPLATES.get(language, CAPTION_TEMPLATES["English"])
364
- style_templates = templates.get(style, templates["Premium"])
365
- clean_cap = re.sub(r"[^A-Za-z0-9 !.,'-ऀ-ॿ]","",caption).strip()
366
- generated_cap = random.choice(style_templates).replace("{cap}", clean_cap)
367
-
368
- # Build insight card
369
- insight = f"""📊 SMART INSIGHTS
370
- ━━━━━━━━━━━━━━━━━━━━━━
371
- 🎯 Category Detected: {category}
372
-
373
- 👥 Target Audience:
374
- {audience}
375
-
376
- ⏰ Best Time to Post:
377
- 🏆 Prime Slot: {pt['best']}
378
- 📅 Best Days: {pt['days']}
379
- 🕐 All Good Times: {', '.join(pt['slots'])}
380
-
381
- 💬 AI Caption ({language}):
382
- {generated_cap}
383
-
384
- #️⃣ Suggested Hashtags:
385
- #{category.replace('/','').replace(' ','')} #Trending #NewCollection #MustHave #ShopNow #Viral #Reels #ForYou
386
- ━━━━━━━━━━━━━━━━━━━━━━"""
387
- return insight, generated_cap
388
-
389
-
390
- # ══════════════════════════════════════════════════════════════════
391
- # MAIN
392
  # ══════════════════════════════════════════════════════════════════
393
- def generate(image, caption, style, language, add_aud, add_cap, progress=gr.Progress()):
394
- if image is None: return None,"⚠️ Upload an image!","Upload image first!"
395
- pil=image if isinstance(image,Image.Image) else Image.fromarray(image)
396
- cap=caption.strip() or "Premium Quality. Shop Now."
397
- prompt=f"cinematic product ad, {cap}, smooth motion, dramatic lighting"
398
- lines=[]
399
- def log(msg): lines.append(msg); progress(min(.1+len(lines)*.10,.80),desc=msg)
400
-
401
- # Get smart insights first (instant)
402
- insight, ai_caption = get_smart_insights(cap, style, language)
403
-
404
- progress(.05,desc="🚀 Generating video...")
405
- video_path, model_used = get_video(pil, prompt, cb=log)
406
- dur=6
407
-
408
- # Use AI caption for video if captions enabled
409
- video_caption = ai_caption if language != "English" else cap
410
-
411
- if add_cap:
412
- log("💬 Adding captions...")
413
- video_path=add_captions_ffmpeg(video_path, video_caption, dur, style.lower())
414
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  if add_aud:
416
  log("🎵 Adding music + voice...")
417
- video_path=add_audio(video_path, cap, dur, style.lower())
418
 
419
- progress(1.0,desc="✅ Done!")
420
- return video_path, "\n".join(lines)+f"\n\n✅ Used: {model_used}", insight
421
 
422
 
423
- # ── UI ────────────────────────────────────────────────────────────
 
 
424
  css="""
425
  #title{text-align:center;font-size:2.3rem;font-weight:900}
426
- #sub{text-align:center;color:#888;margin-bottom:1.5rem}
427
- .insight{font-family:monospace;font-size:.88rem;line-height:1.7}
428
  """
429
- with gr.Blocks(css=css,theme=gr.themes.Soft(primary_hue="violet")) as demo:
430
- gr.Markdown("# 🎬 AI Reel Generator",elem_id="title")
431
- gr.Markdown("Image → AI video + smart captions + posting strategy",elem_id="sub")
432
 
433
  with gr.Row():
434
  # ── LEFT ──────────────────────────────────────────────────
435
  with gr.Column(scale=1):
436
- img_in = gr.Image(label="📸 Upload Image",type="pil",height=280)
437
- cap_in = gr.Textbox(label="✏️ Your Caption / Product Description",
438
- value="Step into style. Own the moment.",lines=2)
 
 
 
 
 
 
 
 
 
439
  with gr.Row():
440
- sty_dd = gr.Dropdown(["Premium","Energetic","Fun"],value="Premium",label="🎨 Style")
441
- lang_dd = gr.Dropdown(["English","Hindi","Hinglish"],value="English",label="🌐 Language")
 
 
 
442
  with gr.Row():
443
- aud_cb = gr.Checkbox(label="🎵 Music + Voice",value=True)
444
- cap_cb = gr.Checkbox(label="💬 Captions", value=True)
445
- gen_btn = gr.Button("🚀 Generate Reel + Insights",variant="primary",size="lg")
446
- gr.Markdown("**🔗 Chain:** LTX-2 Wan 2.2 → SVD-XT → Kling → LTX-Video → Ken Burns ✅")
 
 
 
 
447
 
448
  # ── RIGHT ─────────────────────────────────────────────────
449
  with gr.Column(scale=1):
450
- vid_out = gr.Video(label="🎥 Reel",height=420)
451
- insight_out = gr.Textbox(label="📊 Smart Insights — Audience + Posting Time + AI Caption",
452
- lines=18, interactive=False, elem_classes="insight")
453
- log_out = gr.Textbox(label="🔧 Log",lines=3,interactive=False)
 
 
454
 
455
  gen_btn.click(
456
  fn=generate,
457
- inputs=[img_in,cap_in,sty_dd,lang_dd,aud_cb,cap_cb],
458
- outputs=[vid_out,log_out,insight_out],
459
  )
460
 
461
- if __name__=="__main__":
462
  demo.launch()
 
1
+ import os, tempfile, io, math, time, threading, re, random
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
+ # ══════════════════════════════════════════════════════════════════
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"
66
+ elif any(w in cap_low for w in ["food","eat","recipe","cook","restaurant","cafe","pizza","biryani"]): category="Food"
67
+ elif any(w in cap_low for w in ["phone","laptop","tech","gadget","device","app","camera"]): category="Tech"
68
+ elif any(w in cap_low for w in ["skin","beauty","makeup","lipstick","cream","hair","glow"]): category="Beauty"
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",
76
+ "Tech": "cinematic tech product reveal, sleek background, blue accent lighting, smooth rotation, premium feel",
77
+ "Beauty": "cinematic beauty product shot, soft pink bokeh, gentle sparkle, slow zoom, elegant lighting",
78
+ "Fitness": "cinematic fitness product shot, energetic motion blur, bold lighting, dynamic angle, powerful",
79
+ "Lifestyle": "cinematic lifestyle shot, warm ambient light, cozy aesthetic, slow pan, aspirational feel",
80
+ "Product/Other": "cinematic product advertisement, dramatic lighting, smooth zoom, professional commercial look",
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
+ # ════════════════════════════════════════════════��═════════════════
91
+ POSTING_TIMES = {
92
+ "Fashion": {"best":"9:00 PM", "days":"Tue, Thu, Fri", "slots":["7AM","12PM","6PM","9PM"]},
93
+ "Food": {"best":"12:00 PM", "days":"Mon, Wed, Sat", "slots":["11AM","1PM","7PM"]},
94
+ "Tech": {"best":"8:00 AM", "days":"Mon, Tue, Wed", "slots":["8AM","12PM","5PM"]},
95
+ "Beauty": {"best":"8:00 PM", "days":"Wed, Fri, Sun", "slots":["8AM","1PM","8PM"]},
96
+ "Fitness": {"best":"6:00 AM", "days":"Mon, Wed, Fri", "slots":["6AM","12PM","7PM"]},
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",
104
+ "Tech": "💻 20-40 · Tech enthusiasts · Early adopters · Gadget buyers",
105
+ "Beauty": "💄 16-35 yo · Beauty lovers · Skincare fans · Self-care community",
106
+ "Fitness": "💪 18-40 · Gym goers · Health-conscious · Athletes · Wellness seekers",
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",
114
+ "Elevate your game. {cap} 💫 DM to order!"],
115
+ "Energetic": ["🔥 {cap} Hit different. Grab yours NOW 👆 Limited stock!",
116
+ "⚡ Game changer! {cap} Don't sleep on this 🚀"],
117
+ "Fun": ["Obsessed!! 😍 {cap} Tag someone who needs this 👇",
118
+ "POV: You just found your new fav 🎉 {cap} Link in bio!"],
119
+ },
120
+ "Hindi": {
121
+ "Premium": ["✨ {cap} क्वालिटी जो बोलती है। 🛒 अभी खरीदें → Bio में link",
122
+ "अपना स्टाइल बढ़ाएं। {cap} 💫 Order के लिए DM करें!"],
123
+ "Energetic": ["🔥 {cap} एकदम अलग! अभी grab करो 👆 Limited stock!",
124
+ "⚡ Game changer! {cap} मत सोचो, order करो 🚀"],
125
+ "Fun": ["दीवाने हो जाओगे!! 😍 {cap} किसी को tag करो 👇",
126
+ "POV: नया favourite मिल गया 🎉 {cap} Bio में link!"],
127
+ },
128
+ "Hinglish": {
129
+ "Premium": ["✨ {cap} Quality toh dekho yaar! 🛒 Shop karo → Bio mein link",
130
+ "Style upgrade time! {cap} 💫 DM karo order ke liye!"],
131
+ "Energetic": ["🔥 {cap} Bilkul alag hai bhai! Abhi lo 👆 Limited stock!",
132
+ "⚡ Ek dum fire! {cap} Mat ruko, order karo 🚀"],
133
+ "Fun": ["Yaar yeh toh kamaal hai!! 😍 {cap} Kisi ko tag karo 👇",
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",
141
+ "Tech": "#TechReview #Gadgets #TechLovers #Innovation #NewTech #MustHave",
142
+ "Beauty": "#BeautyTips #Skincare #MakeupLovers #GlowUp #BeautyReels #GRWM",
143
+ "Fitness": "#FitnessMotivation #GymLife #HealthyLifestyle #FitFam #WorkoutReels",
144
+ "Lifestyle": "#Lifestyle #Aesthetic #HomeDecor #VibeCheck #DailyInspo #Reels",
145
+ "Product/Other":"#NewProduct #MustHave #ShopNow #Trending #Viral #Reels #ForYou",
146
+ }
147
+
148
+ def get_insights(category, style, language, cap):
149
+ pt = POSTING_TIMES[category]
150
+ clean_cap = re.sub(r"[^\w\s!.,'-]","",cap).strip()[:60]
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"
158
+ f"🎯 Category: {category}\n\n"
159
+ f"👥 Target Audience:\n{AUDIENCES[category]}\n\n"
160
+ f"⏰ Best Time to Post:\n"
161
+ f"🏆 Prime: {pt['best']} | 📅 Days: {pt['days']}\n"
162
+ f"🕐 All slots: {', '.join(pt['slots'])}\n\n"
163
+ f"💬 AI Caption ({language} · {style}):\n{ai_cap}\n\n"
164
+ f"#️⃣ Hashtags:\n{tags}\n"
165
+ f"{'━'*38}"
166
+ )
167
+ return insight, ai_cap
168
+
169
+
170
+ # ══════════════════════════════════════════════════════════════════
171
+ # HF VIDEO CHAIN
172
+ # ══════════════════════════════════════════════════════════════════
173
  HF_MODELS = [
174
+ {"id":"Lightricks/LTX-2", "name":"LTX-2 ⚡"},
175
+ {"id":"Wan-AI/Wan2.2-I2V-A14B", "name":"Wan 2.2"},
176
+ {"id":"stabilityai/stable-video-diffusion-img2vid-xt", "name":"SVD-XT"},
177
+ {"id":"KlingTeam/LivePortrait", "name":"Kling"},
178
+ {"id":"Lightricks/LTX-Video", "name":"LTX-Video"},
179
+ {"id":"__local__", "name":"Ken Burns ✅"},
180
  ]
181
 
 
 
 
182
  def run_timeout(fn, sec, *a, **kw):
183
  box=[None]; err=[None]
184
  def r():
185
  try: box[0]=fn(*a,**kw)
186
  except Exception as e: err[0]=str(e)
187
  t=threading.Thread(target=r,daemon=True); t.start(); t.join(timeout=sec)
188
+ if t.is_alive(): return None
 
189
  return box[0]
190
 
191
  def try_hf(model_id, pil, prompt):
192
  if not hf_client: return None
193
  try:
194
+ b=io.BytesIO(); pil.save(b,format="JPEG",quality=92)
195
+ r=hf_client.image_to_video(image=b.getvalue(),model=model_id,prompt=prompt)
196
  return r.read() if hasattr(r,"read") else r
197
  except Exception as e: print(f" ❌ {model_id}: {e}"); return None
198
 
199
+ def get_video(pil, prompt, dur, cb=None):
200
  for m in HF_MODELS:
201
  mid,mname=m["id"],m["name"]
202
  if cb: cb(f"⏳ Trying: {mname}")
203
  if mid=="__local__":
204
+ return ken_burns(pil, duration_sec=dur), mname
205
  data=run_timeout(try_hf,50,mid,pil,prompt)
206
  if data:
207
  t=tempfile.NamedTemporaryFile(suffix=".mp4",delete=False)
208
  t.write(data); t.flush()
209
  return t.name, mname
210
+ time.sleep(0.5)
211
+ return ken_burns(pil, duration_sec=dur), "Ken Burns"
212
 
213
 
214
  # ══════════════════════════════════════════════════════════════════
215
+ # KEN BURNS
216
  # ══════════════════════════════════════════════════════════════════
217
+ def ease_c(t): t=max(0.,min(1.,t)); return 4*t*t*t if t<.5 else 1-math.pow(-2*t+2,3)/2
218
+ def ease_e(t): return 1-math.pow(2,-10*t) if t<1 else 1.
219
+ def ease_s(t): t=max(0.,min(1.,t)); return t*t*(3-2*t)
 
 
 
 
 
220
 
221
  def ken_burns(pil, duration_sec=6, fps=30, style="premium"):
222
+ TW,TH=720,1280; pad=60; BW,BH=TW+pad*2,TH+pad*2
223
+ total=int(duration_sec*fps)
 
 
224
 
 
225
  img=pil.convert("RGB"); sw,sh=img.size
226
+ scale=min(TH/sh, TW/sw)
227
+ nw,nh=int(sw*scale),int(sh*scale)
228
+ img_r=img.resize((nw,nh),Image.LANCZOS)
229
+ img_r=img_r.filter(ImageFilter.UnsharpMask(radius=0.8,percent=110,threshold=2))
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))
 
 
 
 
237
  base=np.array(canvas.resize((BW,BH),Image.LANCZOS))
238
 
 
239
  Y,X=np.ogrid[:TH,:TW]
240
  dist=np.sqrt(((X-TW/2)/(TW/2))**2+((Y-TH/2)/(TH/2))**2)
241
  vmask=np.clip(1.-0.22*np.maximum(dist-0.85,0)**2,0,1).astype(np.float32)
242
 
243
+ SEG=[(0.00,0.30,1.00,1.04,0,-int(pad*.4),0,-int(pad*.4)),
244
+ (0.30,0.60,1.04,1.06,-int(pad*.3),int(pad*.3),-int(pad*.4),-int(pad*.7)),
245
+ (0.60,0.80,1.06,1.04,int(pad*.3),int(pad*.5),-int(pad*.7),-int(pad*.4)),
246
+ (0.80,1.00,1.04,1.00,int(pad*.5),0,-int(pad*.4),0)]
 
 
 
247
 
248
  tmp=tempfile.NamedTemporaryFile(suffix=".mp4",delete=False)
249
  writer=cv2.VideoWriter(tmp.name,cv2.VideoWriter_fourcc(*"mp4v"),fps,(TW,TH))
 
253
  zoom=pan_x=pan_y=None
254
  for t0,t1,z0,z1,px0,px1,py0,py1 in SEG:
255
  if t0<=tg<=t1:
256
+ te=ease_c((tg-t0)/(t1-t0))
257
  zoom=z0+(z1-z0)*te; pan_x=int(px0+(px1-px0)*te); pan_y=int(py0+(py1-py0)*te); break
258
  if zoom is None: zoom,pan_x,pan_y=1.,0,0
 
259
 
260
  cw,ch=int(TW/zoom),int(TH/zoom)
261
  ox,oy=BW//2+pan_x,BH//2+pan_y
 
265
 
266
  frame=cv2.resize(base[y1:y2,x1:x2],(TW,TH),interpolation=cv2.INTER_LINEAR)
267
 
 
268
  f=frame.astype(np.float32)/255.
269
  if style=="premium":
270
+ f[:,:,0]=np.clip(f[:,:,0]*1.03+.01,0,1); f[:,:,2]=np.clip(f[:,:,2]*1.02,0,1)
 
271
  elif style=="energetic":
272
+ g=0.299*f[:,:,0:1]+0.587*f[:,:,1:2]+0.114*f[:,:,2:3]
273
+ f=np.clip(g+1.2*(f-g),0,1); f=np.clip(f*1.04,0,1)
274
  elif style=="fun":
275
+ f[:,:,0]=np.clip(f[:,:,0]*1.05,0,1); f[:,:,1]=np.clip(f[:,:,1]*1.03,0,1)
 
276
  frame=np.clip(f*255,0,255).astype(np.uint8)
 
 
277
  frame=np.clip(frame.astype(np.float32)*vmask[:,:,None],0,255).astype(np.uint8)
278
+ frame=np.clip(frame.astype(np.float32)+np.random.normal(0,2.5,frame.shape),0,255).astype(np.uint8)
 
 
 
 
279
  frame[:36,:]=0; frame[-36:,:]=0
280
 
281
+ if tg<0.02: alpha=ease_e(tg/0.02)
282
+ elif tg>0.95: alpha=ease_s(1-(tg-0.95)/0.05)
 
283
  else: alpha=1.
284
  if alpha<1.: frame=np.clip(frame.astype(np.float32)*alpha,0,255).astype(np.uint8)
285
 
 
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 '
308
+ f'"{out}" -loglevel error'
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()
318
+ words=caption.strip().split(); mid=max(1,len(words)//2)
319
+ line1=clean(" ".join(words[:mid])); line2=clean(" ".join(words[mid:])) if len(words)>1 else line1
320
+ col={"premium":"FFD232","energetic":"3CC8FF","fun":"FF78C8"}.get(style,"FFFFFF")
321
+ cta_col={"premium":"FF9900","energetic":"FF4444","fun":"AA44FF"}.get(style,"FF9900")
322
  out=video_path.replace(".mp4","_cap.mp4")
323
+ font=""
324
+ for p in ["/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
325
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"]:
326
+ if os.path.exists(p): font=f":fontfile='{p}'"; break
327
+
328
+ def dt(text,start,end,y,size=42,color=None,box_a="0.60"):
329
+ c=color or col; fd=0.4
330
+ return (f"drawtext=text='{text}'{font}:fontsize={size}:fontcolor=#{c}"
331
+ f":x=(w-text_w)/2:y={y}:box=1:boxcolor=black@{box_a}:boxborderw=14"
332
+ f":enable='between(t,{start},{end})'"
333
+ f":alpha='if(lt(t,{start+fd}),(t-{start})/{fd},if(gt(t,{end-fd}),({end}-t)/{fd},1))'")
334
+
335
+ e2=min(duration_sec-0.2,6.5)
336
+ vf=",".join([
337
+ dt(line1, 1.0, 3.5, "h-190"),
338
+ dt(line2, 3.8, e2, "h-190"),
339
+ dt("Shop Now >", min(4.5,e2-0.5), e2, "h-130", size=32, color=cta_col, box_a="0.70"),
340
+ dt("#NewCollection", 0.5, 3.0, "60", size=28, color="FFFFFF", box_a="0.40"),
341
+ ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  # ══════════════════════════════════════════════════════════════════
349
  def make_bgm(duration_sec, out_path, style="premium"):
350
  import wave
351
+ sr=44100; n=int(sr*duration_sec); t=np.linspace(0,duration_sec,n,endpoint=False)
352
+ bpm={"premium":88,"energetic":126,"fun":104}.get(style,88); beat=60./bpm
 
 
 
353
  kick=np.zeros(n,np.float32)
354
  for i in range(int(duration_sec/beat)+2):
355
  s=int(i*beat*sr)
356
  if s>=n: break
357
+ l=min(int(sr*.10),n-s); env=np.exp(-20*np.arange(l)/sr)
 
358
  kick[s:s+l]+=env*np.sin(2*math.pi*55*np.exp(-25*np.arange(l)/sr)*np.arange(l)/sr)*0.55
359
+ bf={"premium":55,"energetic":80,"fun":65}.get(style,55)
360
+ bass=np.sin(2*math.pi*bf*t)*0.10*(0.5+0.5*np.sin(2*math.pi*(bpm/60/4)*t))
 
 
361
  mf={"premium":[261,329,392],"energetic":[330,415,494],"fun":[392,494,587]}.get(style,[261,329,392])
362
  mel=np.zeros(n,np.float32)
363
  for j,f in enumerate(mf):
364
+ mel+=np.sin(2*math.pi*f*t)*np.clip(0.5+0.5*np.sin(2*math.pi*1.5*t-j*2.1),0,1)*0.045
 
 
365
  hat=np.zeros(n,np.float32)
366
+ for i in range(int(duration_sec/(beat/2))+2):
367
+ s=int(i*(beat/2)*sr)
 
368
  if s>=n: break
369
+ l=min(int(sr*.03),n-s); hat[s:s+l]+=np.random.randn(l)*np.exp(-80*np.arange(l)/sr)*0.06
 
 
370
  mix=np.clip((kick+bass+mel+hat)*0.18,-1,1)
371
  fade=int(sr*.5); mix[:fade]*=np.linspace(0,1,fade); mix[-fade:]*=np.linspace(1,0,fade)
 
372
  with wave.open(out_path,"w") as wf:
373
  wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(sr)
374
  wf.writeframes((mix*32767).astype(np.int16).tobytes())
 
377
  bgm=video_path.replace(".mp4","_bgm.wav")
378
  final=video_path.replace(".mp4","_final.mp4")
379
  make_bgm(duration_sec, bgm, style)
 
 
380
  audio=bgm
381
  try:
382
  from gtts import gTTS
383
+ tts=video_path.replace(".mp4","_tts.mp3"); gTTS(text=caption[:200],lang="en",slow=False).save(tts)
 
 
384
  mixed=video_path.replace(".mp4","_mix.wav")
385
+ os.system(f'ffmpeg -y -i "{bgm}" -i "{tts}" -filter_complex '
386
+ f'"[0]volume=0.20[a];[1]volume=0.95[b];[a][b]amix=inputs=2:duration=first" '
387
  f'-t {duration_sec} "{mixed}" -loglevel error')
388
  if os.path.exists(mixed): audio=mixed
389
+ except: pass
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.",
417
+ "Food":"Every bite tells a story.",
418
+ "Tech":"The future is here.",
419
+ "Beauty":"Glow different.",
420
+ "Fitness":"Push your limits.",
421
+ "Lifestyle":"Live the aesthetic.",
422
+ "Product/Other":"Quality that speaks for itself.",
423
+ }
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:
443
+ log(f"💬 Captions {idx+1}...")
444
+ video_caption = ai_cap if language != "English" else cap
445
+ vpath = add_captions_ffmpeg(vpath, video_caption, clip_dur, style.lower())
446
+
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())
461
 
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()