GLAkavya commited on
Commit
c980c4a
ยท
verified ยท
1 Parent(s): cbcf1e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +441 -312
app.py CHANGED
@@ -1,333 +1,462 @@
1
- import os
2
- import json
3
- import tempfile
4
- import io
5
- import math
6
  import numpy as np
7
  import cv2
8
  import gradio as gr
9
- from google import genai
10
- from google.genai import types
11
- from PIL import Image
12
-
13
- # โ”€โ”€ ENV SETUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
14
- gemini_key = (
15
- os.environ.get("GEMINI_API_KEY", "")
16
- or os.environ.get("GOOGLE_API_KEY", "")
17
- ).strip()
18
- if gemini_key:
19
- os.environ["GOOGLE_API_KEY"] = gemini_key
20
- print(f"โœ… Gemini key loaded (len={len(gemini_key)})")
21
- else:
22
- print("โŒ No Gemini key found!")
23
-
24
- hf_token = (
25
- os.environ.get("HF_TOKEN", "")
26
- or os.environ.get("HF_KEY", "")
27
- ).strip()
28
  if hf_token:
29
  try:
30
- from huggingface_hub import login
31
- login(token=hf_token)
32
- print("โœ… HF login OK")
33
- except Exception as e:
34
- print(f"โš ๏ธ HF login skipped: {e}")
35
-
36
- print("โœ… App ready โ€” using fast OpenCV video generation (no heavy models!)")
37
-
38
-
39
- # โ”€โ”€ GEMINI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
40
- def call_gemini(pil_image: Image.Image, user_desc: str, language: str, style: str) -> dict:
41
- client = genai.Client()
42
-
43
- lang_map = {
44
- "English": "Write everything in English.",
45
- "Hindi": "เคธเคฌ เค•เฅเค› เคนเคฟเค‚เคฆเฅ€ เคฎเฅ‡เค‚ เคฒเคฟเค–เฅ‡เค‚เฅค",
46
- "Hinglish": "Write in Hinglish (mix of Hindi and English).",
47
- }
48
- style_map = {
49
- "Fun": "tone: playful, witty, youthful",
50
- "Premium": "tone: luxurious, sophisticated, aspirational",
51
- "Energetic": "tone: high-energy, bold, action-packed",
52
- }
53
-
54
- prompt = f"""You are an expert ad copywriter. Analyze this product image and create a compelling social-media video ad.
55
-
56
- {f'Product description: {user_desc}' if user_desc.strip() else ''}
57
- Language rule : {lang_map.get(language, lang_map['English'])}
58
- Style rule : {style_map.get(style, style_map['Fun'])}
59
-
60
- CRITICAL: Return ONLY raw JSON. No markdown. No ```json. No explanation. Pure JSON only.
61
- {{
62
- "hook": "attention-grabbing opening line (1-2 sentences)",
63
- "script": "full 15-20 second voiceover script",
64
- "cta": "call-to-action phrase",
65
- "video_prompt": "detailed cinematic advertising scene description"
66
- }}"""
67
-
68
- buf = io.BytesIO()
69
- pil_image.save(buf, format="JPEG")
70
- image_bytes = buf.getvalue()
71
-
72
- response = client.models.generate_content(
73
- model="gemini-2.5-flash",
74
- contents=[
75
- types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg"),
76
- types.Part.from_text(text=prompt),
77
- ],
78
- )
79
-
80
- raw = response.text.strip()
81
- if "```" in raw:
82
- raw = raw.split("```")[1]
83
- if raw.lower().startswith("json"):
84
- raw = raw[4:]
85
- raw = raw.strip()
86
-
87
- return json.loads(raw)
88
-
89
-
90
- # โ”€โ”€ FAST VIDEO: Ken Burns effect (zoom + pan) โ€” NO heavy model needed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
91
- def ease_in_out(t):
92
- """Smooth easing โ€” no jerky motion."""
93
- return t * t * (3 - 2 * t)
94
-
95
- def ease_out_bounce(t):
96
- """Bouncy pop effect."""
97
- if t < 1/2.75:
98
- return 7.5625 * t * t
99
- elif t < 2/2.75:
100
- t -= 1.5/2.75
101
- return 7.5625 * t * t + 0.75
102
- elif t < 2.5/2.75:
103
- t -= 2.25/2.75
104
- return 7.5625 * t * t + 0.9375
105
- else:
106
- t -= 2.625/2.75
107
- return 7.5625 * t * t + 0.984375
108
-
109
- def apply_vignette(frame, strength=0.6):
110
- """Dark edges โ€” cinematic look."""
111
- h, w = frame.shape[:2]
112
- Y, X = np.ogrid[:h, :w]
113
- cx, cy = w / 2, h / 2
114
- dist = np.sqrt(((X - cx) / cx) ** 2 + ((Y - cy) / cy) ** 2)
115
- mask = np.clip(1.0 - strength * (dist ** 1.5), 0, 1)
116
- return (frame * mask[:, :, np.newaxis]).astype(np.uint8)
117
-
118
- def apply_color_grade(frame, style="premium"):
119
- """Color grading per style."""
120
- f = frame.astype(np.float32)
121
- if style == "premium":
122
- # Teal-orange grade: boost blues in shadows, warm highlights
123
- f[:,:,0] = np.clip(f[:,:,0] * 1.05, 0, 255) # R boost
124
- f[:,:,2] = np.clip(f[:,:,2] * 1.08, 0, 255) # B boost
125
- f = np.clip(f * 1.05, 0, 255) # slight brightness
126
- elif style == "energetic":
127
- # Saturated vivid
128
- gray = np.mean(f, axis=2, keepdims=True)
129
- f = np.clip(gray + 1.4 * (f - gray), 0, 255)
130
- f = np.clip(f * 1.1, 0, 255)
131
- elif style == "fun":
132
- # Warm, bright, punchy
133
- f[:,:,0] = np.clip(f[:,:,0] * 1.1, 0, 255) # R
134
- f[:,:,1] = np.clip(f[:,:,1] * 1.05, 0, 255) # G
135
- return f.astype(np.uint8)
136
-
137
- def generate_video(pil_image: Image.Image, duration_sec: int = 5, fps: int = 24, style: str = "premium") -> str:
138
- """
139
- Cinematic 5-second video with:
140
- - Segment 1 (0-1.5s): ZOOM IN burst + bounce pop
141
- - Segment 2 (1.5-3s): Slow upward pan + subtle shake
142
- - Segment 3 (3-4.2s): ZOOM OUT pull-back
143
- - Segment 4 (4.2-5s): Fade out with color flash
144
- - Vignette overlay
145
- - Color grading
146
- - Fade in/out
147
- """
148
- total_frames = duration_sec * fps # 120 frames
149
-
150
- img = pil_image.convert("RGB")
151
- target_w, target_h = 720, 1280
152
- img = img.resize((target_w, target_h), Image.LANCZOS)
153
-
154
- tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
155
- fourcc = cv2.VideoWriter_fourcc(*"mp4v")
156
- out = cv2.VideoWriter(tmp.name, fourcc, fps, (target_w, target_h))
157
-
158
- # Large canvas to allow all movements without black borders
159
- pad = 160
160
- big_h, big_w = target_h + pad * 2, target_w + pad * 2
161
- big_img = np.array(img.resize((big_w, big_h), Image.LANCZOS))
162
-
163
- # Segment boundaries (in frames)
164
- s1_end = int(fps * 1.5) # 36
165
- s2_end = int(fps * 3.0) # 72
166
- s3_end = int(fps * 4.2) # 100
167
- s4_end = total_frames # 120
168
-
169
- for i in range(total_frames):
170
- t_global = i / (total_frames - 1)
171
-
172
- # โ”€โ”€ SEGMENT 1: Zoom-in bounce pop (0 โ†’ 1.5s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
173
- if i < s1_end:
174
- t = i / s1_end
175
- te = ease_out_bounce(min(t * 1.1, 1.0))
176
- zoom = 1.35 - 0.25 * te # 1.35 โ†’ 1.10 with bounce
177
- pan_x = int(pad * 0.1 * t)
178
- pan_y = int(-pad * 0.15 * t) # slight upward
179
-
180
- # โ”€โ”€ SEGMENT 2: Slow pan upward + micro shake (1.5s โ†’ 3s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
181
- elif i < s2_end:
182
- t = (i - s1_end) / (s2_end - s1_end)
183
- te = ease_in_out(t)
184
- zoom = 1.10 - 0.05 * te # gentle zoom out
185
- shake_x = int(3 * math.sin(i * 0.8)) # micro horizontal shake
186
- shake_y = int(2 * math.cos(i * 1.1))
187
- pan_x = int(pad * 0.1 + shake_x)
188
- pan_y = int(-pad * 0.15 - pad * 0.20 * te + shake_y)
189
-
190
- # โ”€โ”€ SEGMENT 3: Zoom out pull-back (3s โ†’ 4.2s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
191
- elif i < s3_end:
192
- t = (i - s2_end) / (s3_end - s2_end)
193
- te = ease_in_out(t)
194
- zoom = 1.05 - 0.04 * te # zoom out to near 1.0
195
- pan_x = int(pad * 0.1 * (1 - te))
196
- pan_y = int(-pad * 0.35 * (1 - te))
197
-
198
- # โ”€โ”€ SEGMENT 4: Final fade out (4.2s โ†’ 5s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
199
- else:
200
- t = (i - s3_end) / (s4_end - s3_end)
201
- te = ease_in_out(t)
202
- zoom = 1.01 + 0.03 * te # subtle zoom in at end
203
- pan_x = 0
204
- pan_y = 0
205
-
206
- # Crop from big canvas
207
- crop_w = int(target_w / zoom)
208
- crop_h = int(target_h / zoom)
209
- cx = big_w // 2 + pan_x
210
- cy = big_h // 2 + pan_y
211
-
212
- x1 = max(0, cx - crop_w // 2)
213
- y1 = max(0, cy - crop_h // 2)
214
- x2 = min(big_w, x1 + crop_w)
215
- y2 = min(big_h, y1 + crop_h)
216
-
217
- if x2 - x1 < 10 or y2 - y1 < 10:
218
- x1, y1, x2, y2 = 0, 0, target_w, target_h
219
-
220
- cropped = big_img[y1:y2, x1:x2]
221
- frame = cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
222
-
223
- # โ”€โ”€ COLOR GRADE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
224
- frame = apply_color_grade(frame, style)
225
-
226
- # โ”€โ”€ VIGNETTE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
227
- frame = apply_vignette(frame, strength=0.55)
228
-
229
- # โ”€โ”€ FADE IN (first 0.4s) + FADE OUT (last 0.6s) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
230
- fade_in_end = int(fps * 0.4)
231
- fade_out_sta = int(fps * 4.4)
232
- if i < fade_in_end:
233
- alpha = ease_in_out(i / fade_in_end)
234
- elif i >= fade_out_sta:
235
- alpha = ease_in_out(1.0 - (i - fade_out_sta) / (total_frames - fade_out_sta))
236
- else:
237
- alpha = 1.0
238
-
239
- # โ”€โ”€ WHITE FLASH at segment transitions (frame 36, 72) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
240
- flash_frames = {s1_end, s1_end+1, s2_end, s2_end+1}
241
- if i in flash_frames:
242
- flash_strength = 0.35 if i in {s1_end, s2_end} else 0.15
243
- white = np.ones_like(frame) * 255
244
- frame = cv2.addWeighted(frame, 1 - flash_strength, white.astype(np.uint8), flash_strength, 0)
245
-
246
- frame = np.clip(frame.astype(np.float32) * alpha, 0, 255).astype(np.uint8)
247
-
248
- # RGB โ†’ BGR for OpenCV
249
- frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
250
- out.write(frame_bgr)
251
-
252
- out.release()
253
- return tmp.name
254
-
255
-
256
-
257
- # โ”€โ”€ MAIN PIPELINE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
258
- def generate_ad(image, user_desc, language, style):
259
- if image is None:
260
- return None, "โš ๏ธ Please upload a product image.", "", ""
261
-
262
- pil_image = image if isinstance(image, Image.Image) else Image.fromarray(image)
263
-
264
- # STEP 1 โ€” Gemini ad copy
265
  try:
266
- ad_data = call_gemini(pil_image, user_desc or "", language, style)
267
- except Exception as e:
268
- return None, f"โŒ Gemini error: {e}", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- hook = ad_data.get("hook", "")
271
- script = ad_data.get("script", "")
272
- cta = ad_data.get("cta", "")
273
 
274
- # STEP 2 โ€” Fast video (2-3 sec)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  try:
276
- video_path = generate_video(pil_image, duration_sec=5, fps=24, style=style.lower())
277
- except Exception as e:
278
- return None, hook, f"โŒ Video error: {e}\n\n{script}", cta
279
-
280
- return video_path, hook, script, cta
281
-
282
-
283
- # โ”€โ”€ GRADIO UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
284
- css = """
285
- #title { text-align:center; font-size:2.2rem; font-weight:800; margin-bottom:.2rem; }
286
- #sub { text-align:center; color:#888; margin-bottom:1.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  """
288
-
289
- with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="violet")) as demo:
290
-
291
- gr.Markdown("# ๐ŸŽฌ AI Reel Generator", elem_id="title")
292
- gr.Markdown("Upload a product image โ†’ cinematic 5-sec ad reel + copy in seconds.", elem_id="sub")
293
 
294
  with gr.Row():
 
295
  with gr.Column(scale=1):
296
- image_input = gr.Image(label="๐Ÿ“ธ Upload Product Image", type="pil", height=300)
297
- desc_input = gr.Textbox(
298
- label="๐Ÿ“ Describe your product (optional)",
299
- placeholder="e.g. Premium sneakers with star design โ€ฆ",
300
- lines=3,
301
- )
302
  with gr.Row():
303
- lang_dropdown = gr.Dropdown(
304
- choices=["English", "Hindi", "Hinglish"],
305
- value="English", label="๐ŸŒ Language",
306
- )
307
- style_dropdown = gr.Dropdown(
308
- choices=["Fun", "Premium", "Energetic"],
309
- value="Fun", label="๐ŸŽจ Style",
310
- )
311
- gen_btn = gr.Button("๐Ÿš€ Generate Ad", variant="primary", size="lg")
312
 
 
313
  with gr.Column(scale=1):
314
- video_out = gr.Video(label="๐ŸŽฅ 5-Second Ad Reel", height=400)
315
- hook_out = gr.Textbox(label="โšก Hook", lines=2, interactive=False)
316
- script_out = gr.Textbox(label="๐Ÿ“„ Script", lines=5, interactive=False)
317
- cta_out = gr.Textbox(label="๐ŸŽฏ CTA", lines=1, interactive=False)
318
 
319
  gen_btn.click(
320
- fn=generate_ad,
321
- inputs=[image_input, desc_input, lang_dropdown, style_dropdown],
322
- outputs=[video_out, hook_out, script_out, cta_out],
323
- )
324
-
325
- gr.Markdown(
326
- "---\n**How it works:** "
327
- "1๏ธโƒฃ Gemini 2.5 Flash โ†’ hook, script, CTA. "
328
- "2๏ธโƒฃ Ken Burns cinematic effect โ†’ smooth 5-sec reel (no heavy AI model!). "
329
- "โšก Total time: ~5-10 seconds!"
330
  )
331
 
332
- if __name__ == "__main__":
333
  demo.launch()
 
1
+ import os, tempfile, io, math, time, threading
 
 
 
 
2
  import numpy as np
3
  import cv2
4
  import gradio as gr
5
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
6
+
7
+ # โ”€โ”€ TOKENS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
8
+ hf_token = (os.environ.get("HF_TOKEN","") or os.environ.get("HF_KEY","")).strip()
9
+ hf_client = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  if hf_token:
11
  try:
12
+ from huggingface_hub import login, InferenceClient
13
+ login(token=hf_token); hf_client = InferenceClient(token=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.00โ†’1.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))
114
+
115
+ for i in range(total):
116
+ tg=i/max(total-1,1)
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
127
+ x1,y1=max(0,ox-cw//2),max(0,oy-ch//2)
128
+ x2,y2=min(BW,x1+cw),min(BH,y1+ch)
129
+ if (x2-x1)<10 or (y2-y1)<10: x1,y1,x2,y2=0,0,TW,TH
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
+
161
+ writer.write(cv2.cvtColor(frame,cv2.COLOR_RGB2BGR))
162
+ writer.release()
163
+ return tmp.name
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())
264
+
265
+ 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()