Spaces:
Running
Running
Update app.py
Browse files
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
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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
|
| 33 |
-
except Exception as e:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
-
hook = ad_data.get("hook", "")
|
| 271 |
-
script = ad_data.get("script", "")
|
| 272 |
-
cta = ad_data.get("cta", "")
|
| 273 |
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
try:
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
"""
|
| 288 |
-
|
| 289 |
-
|
| 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 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
with gr.Row():
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 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 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
|
| 319 |
gen_btn.click(
|
| 320 |
-
fn=
|
| 321 |
-
inputs=[
|
| 322 |
-
outputs=[
|
| 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__
|
| 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()
|