GLAkavya commited on
Commit
cbcf1e6
Β·
verified Β·
1 Parent(s): e67da39

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -30
app.py CHANGED
@@ -88,44 +88,124 @@ CRITICAL: Return ONLY raw JSON. No markdown. No ```json. No explanation. Pure JS
88
 
89
 
90
  # ── FAST VIDEO: Ken Burns effect (zoom + pan) β€” NO heavy model needed ─────────
91
- def generate_video(pil_image: Image.Image, duration_sec: int = 5, fps: int = 24) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  """
93
- Creates a cinematic 5-second video from a single image using:
94
- - Ken Burns zoom-in effect
95
- - Slow pan movement
96
- - Brightness fade-in at start, fade-out at end
97
- Pure OpenCV β€” generates in ~2-3 seconds, no GPU needed.
 
 
 
98
  """
99
- total_frames = duration_sec * fps # 5 * 24 = 120 frames
100
 
101
- # Resize image to 720p for good quality
102
  img = pil_image.convert("RGB")
103
- target_w, target_h = 720, 1280 # vertical/reel format
104
  img = img.resize((target_w, target_h), Image.LANCZOS)
105
- frame_base = np.array(img)
106
 
107
  tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
108
  fourcc = cv2.VideoWriter_fourcc(*"mp4v")
109
  out = cv2.VideoWriter(tmp.name, fourcc, fps, (target_w, target_h))
110
 
111
- # Work on a larger canvas so zoom doesn't crop into black
112
- pad = 80
113
  big_h, big_w = target_h + pad * 2, target_w + pad * 2
114
  big_img = np.array(img.resize((big_w, big_h), Image.LANCZOS))
115
 
116
- for i in range(total_frames):
117
- t = i / (total_frames - 1) # 0.0 β†’ 1.0
118
-
119
- # Zoom: start slightly zoomed in, zoom out slowly (or zoom in)
120
- zoom = 1.0 + 0.08 * (1.0 - t) # 1.08 β†’ 1.00
121
 
122
- # Pan: slow drift from center-left to center-right
123
- pan_x = int(pad * 0.5 * t)
124
- pan_y = int(pad * 0.3 * t)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
 
126
  crop_w = int(target_w / zoom)
127
  crop_h = int(target_h / zoom)
128
-
129
  cx = big_w // 2 + pan_x
130
  cy = big_h // 2 + pan_y
131
 
@@ -134,21 +214,38 @@ def generate_video(pil_image: Image.Image, duration_sec: int = 5, fps: int = 24)
134
  x2 = min(big_w, x1 + crop_w)
135
  y2 = min(big_h, y1 + crop_h)
136
 
 
 
 
137
  cropped = big_img[y1:y2, x1:x2]
138
  frame = cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
139
 
140
- # Fade-in first 0.5s, fade-out last 0.5s
141
- fade_frames = int(fps * 0.5)
142
- if i < fade_frames:
143
- alpha = i / fade_frames
144
- elif i > total_frames - fade_frames:
145
- alpha = (total_frames - i) / fade_frames
 
 
 
 
 
 
 
146
  else:
147
  alpha = 1.0
148
 
149
- frame = (frame * alpha).astype(np.uint8)
 
 
 
 
 
150
 
151
- # Convert RGB β†’ BGR for OpenCV
 
 
152
  frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
153
  out.write(frame_bgr)
154
 
@@ -156,6 +253,7 @@ def generate_video(pil_image: Image.Image, duration_sec: int = 5, fps: int = 24)
156
  return tmp.name
157
 
158
 
 
159
  # ── MAIN PIPELINE ─────────────────────────────────────────────────────────────
160
  def generate_ad(image, user_desc, language, style):
161
  if image is None:
@@ -175,7 +273,7 @@ def generate_ad(image, user_desc, language, style):
175
 
176
  # STEP 2 β€” Fast video (2-3 sec)
177
  try:
178
- video_path = generate_video(pil_image, duration_sec=5, fps=24)
179
  except Exception as e:
180
  return None, hook, f"❌ Video error: {e}\n\n{script}", cta
181
 
 
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
 
 
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
 
 
253
  return tmp.name
254
 
255
 
256
+
257
  # ── MAIN PIPELINE ─────────────────────────────────────────────────────────────
258
  def generate_ad(image, user_desc, language, style):
259
  if image is None:
 
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