ladybug11 commited on
Commit
4908797
Β·
1 Parent(s): 850d1aa
Files changed (1) hide show
  1. modal_video_processing.py +14 -96
modal_video_processing.py CHANGED
@@ -23,64 +23,40 @@ image = modal.Image.debian_slim(python_version="3.11").pip_install(
23
  cpu=2,
24
  memory=2048,
25
  timeout=180,
26
- keep_warm=1, # Keep 1 container warm to eliminate cold starts!
27
- container_idle_timeout=300, # Keep alive for 5 minutes
28
  )
29
  def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None) -> bytes:
30
  """
31
- Process quote video on Modal's fast infrastructure.
32
- Downloads video, adds text overlay, optionally adds audio, returns video bytes.
33
-
34
- Args:
35
- video_url: URL of background video
36
- quote_text: Quote to overlay
37
- audio_b64: Optional base64-encoded audio file
38
-
39
- Returns:
40
- bytes: Processed video file as bytes
41
  """
42
  import tempfile
43
  import requests
44
- from moviepy.editor import VideoFileClip, ImageClip, CompositeVideoClip, AudioFileClip
45
  from PIL import Image, ImageDraw, ImageFont
46
  import numpy as np
47
  import time
48
- import base64
49
 
50
  start_time = time.time()
51
- print(f"🎬 Starting video processing on Modal...")
52
 
53
- # Download video with streaming
54
- print("πŸ“₯ Downloading video...")
55
- download_start = time.time()
56
  response = requests.get(video_url, stream=True, timeout=30)
57
  response.raise_for_status()
58
 
59
  temp_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
60
  with open(temp_video.name, 'wb') as f:
61
- for chunk in response.iter_content(chunk_size=1024*1024): # 1MB chunks
62
  f.write(chunk)
63
 
64
- print(f"βœ… Video downloaded in {time.time() - download_start:.1f}s")
65
-
66
  # Load video
67
- print("πŸŽ₯ Loading video...")
68
- load_start = time.time()
69
  video = VideoFileClip(temp_video.name)
70
 
71
- # Limit video duration to 10 seconds max for faster processing
72
- # Instagram quote videos are typically short anyway
73
  if video.duration > 10:
74
  video = video.subclip(0, 10)
75
 
76
  w, h = video.size
77
- print(f" Dimensions: {w}x{h}, Duration: {video.duration:.1f}s")
78
- print(f"βœ… Video loaded in {time.time() - load_start:.1f}s")
79
-
80
- # Create text overlay using PIL
81
- print("✍️ Creating text overlay...")
82
- overlay_start = time.time()
83
 
 
84
  def make_text_frame(t):
85
  img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
86
  draw = ImageDraw.Draw(img)
@@ -90,10 +66,7 @@ def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None)
90
  try:
91
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
92
  except:
93
- try:
94
- font = ImageFont.truetype("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", font_size)
95
- except:
96
- font = ImageFont.load_default()
97
 
98
  max_width = int(w * 0.6)
99
 
@@ -139,54 +112,18 @@ def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None)
139
  return np.array(img)
140
 
141
  text_clip = ImageClip(make_text_frame(0), duration=video.duration)
142
- print(f"βœ… Text overlay created in {time.time() - overlay_start:.1f}s")
143
 
144
  # Composite
145
- print("🎨 Compositing video...")
146
- composite_start = time.time()
147
  final_video = CompositeVideoClip([video, text_clip])
148
- print(f"βœ… Composited in {time.time() - composite_start:.1f}s")
149
 
150
- # Add audio if provided
151
- if audio_b64:
152
- print("🎀 Adding voice commentary audio...")
153
- audio_start = time.time()
154
- try:
155
- # Decode base64 audio
156
- audio_bytes = base64.b64decode(audio_b64)
157
-
158
- # Save to temp file
159
- temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
160
- with open(temp_audio.name, 'wb') as f:
161
- f.write(audio_bytes)
162
-
163
- # Load audio clip
164
- audio_clip = AudioFileClip(temp_audio.name)
165
-
166
- # Use the shorter duration between video and audio
167
- audio_duration = min(audio_clip.duration, final_video.duration)
168
- audio_clip = audio_clip.subclip(0, audio_duration)
169
-
170
- # Set audio on video
171
- final_video = final_video.set_audio(audio_clip)
172
-
173
- print(f"βœ… Audio added in {time.time() - audio_start:.1f}s")
174
-
175
- # Cleanup audio temp file
176
- os.unlink(temp_audio.name)
177
- except Exception as e:
178
- print(f"⚠️ Audio failed: {e}, continuing without audio")
179
-
180
- # Export with optimized settings
181
- print("πŸ“¦ Exporting video...")
182
- export_start = time.time()
183
  output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
184
 
185
  final_video.write_videofile(
186
  output_file.name,
187
  codec='libx264',
188
  audio_codec='aac',
189
- fps=10, # Lower fps for speed
190
  preset='ultrafast',
191
  threads=2,
192
  verbose=False,
@@ -195,9 +132,7 @@ def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None)
195
  ffmpeg_params=['-crf', '30', '-g', '30']
196
  )
197
 
198
- print(f"βœ… Video exported in {time.time() - export_start:.1f}s")
199
-
200
- # Read video bytes
201
  with open(output_file.name, 'rb') as f:
202
  video_bytes = f.read()
203
 
@@ -208,23 +143,18 @@ def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None)
208
  os.unlink(output_file.name)
209
 
210
  total_time = time.time() - start_time
211
- print(f"πŸŽ‰ TOTAL PROCESSING TIME: {total_time:.1f}s")
212
- print(f" Video size: {len(video_bytes) / 1024 / 1024:.2f}MB")
213
 
214
  return video_bytes
215
 
216
 
217
- # Expose as web endpoint for easy calling from Gradio
218
  @app.function(image=image)
219
  @modal.web_endpoint(method="POST")
220
  def process_video_endpoint(data: dict):
221
- """
222
- Web endpoint to process videos with optional audio.
223
- Accepts JSON with video_url, quote_text, and optional audio_b64.
224
- """
225
  video_url = data.get("video_url")
226
  quote_text = data.get("quote_text")
227
- audio_b64 = data.get("audio_b64") # Changed from audio_url
228
 
229
  if not video_url or not quote_text:
230
  return {"error": "Missing video_url or quote_text"}, 400
@@ -232,7 +162,6 @@ def process_video_endpoint(data: dict):
232
  try:
233
  video_bytes = process_quote_video.remote(video_url, quote_text, audio_b64)
234
 
235
- # Return video bytes as base64
236
  import base64
237
  video_b64 = base64.b64encode(video_bytes).decode()
238
 
@@ -244,14 +173,3 @@ def process_video_endpoint(data: dict):
244
 
245
  except Exception as e:
246
  return {"error": str(e)}, 500
247
-
248
-
249
- if __name__ == "__main__":
250
- # Test locally
251
- with app.run():
252
- result = process_quote_video.remote(
253
- video_url="https://videos.pexels.com/video-files/3843433/3843433-uhd_2732_1440_25fps.mp4",
254
- quote_text="Test quote for local testing",
255
- audio_b64=None
256
- )
257
- print(f"Got video: {len(result)} bytes")
 
23
  cpu=2,
24
  memory=2048,
25
  timeout=180,
26
+ keep_warm=1, # Keep 1 container warm
27
+ container_idle_timeout=300,
28
  )
29
  def process_quote_video(video_url: str, quote_text: str, audio_b64: str = None) -> bytes:
30
  """
31
+ Process quote video on Modal - FAST version (no audio).
 
 
 
 
 
 
 
 
 
32
  """
33
  import tempfile
34
  import requests
35
+ from moviepy.editor import VideoFileClip, ImageClip, CompositeVideoClip
36
  from PIL import Image, ImageDraw, ImageFont
37
  import numpy as np
38
  import time
 
39
 
40
  start_time = time.time()
 
41
 
42
+ # Download video
 
 
43
  response = requests.get(video_url, stream=True, timeout=30)
44
  response.raise_for_status()
45
 
46
  temp_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
47
  with open(temp_video.name, 'wb') as f:
48
+ for chunk in response.iter_content(chunk_size=1024*1024):
49
  f.write(chunk)
50
 
 
 
51
  # Load video
 
 
52
  video = VideoFileClip(temp_video.name)
53
 
 
 
54
  if video.duration > 10:
55
  video = video.subclip(0, 10)
56
 
57
  w, h = video.size
 
 
 
 
 
 
58
 
59
+ # Create text overlay
60
  def make_text_frame(t):
61
  img = Image.new('RGBA', (w, h), (0, 0, 0, 0))
62
  draw = ImageDraw.Draw(img)
 
66
  try:
67
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
68
  except:
69
+ font = ImageFont.load_default()
 
 
 
70
 
71
  max_width = int(w * 0.6)
72
 
 
112
  return np.array(img)
113
 
114
  text_clip = ImageClip(make_text_frame(0), duration=video.duration)
 
115
 
116
  # Composite
 
 
117
  final_video = CompositeVideoClip([video, text_clip])
 
118
 
119
+ # Export - FAST settings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
121
 
122
  final_video.write_videofile(
123
  output_file.name,
124
  codec='libx264',
125
  audio_codec='aac',
126
+ fps=10,
127
  preset='ultrafast',
128
  threads=2,
129
  verbose=False,
 
132
  ffmpeg_params=['-crf', '30', '-g', '30']
133
  )
134
 
135
+ # Read bytes
 
 
136
  with open(output_file.name, 'rb') as f:
137
  video_bytes = f.read()
138
 
 
143
  os.unlink(output_file.name)
144
 
145
  total_time = time.time() - start_time
146
+ print(f"πŸŽ‰ Total: {total_time:.1f}s, Size: {len(video_bytes)/1024/1024:.2f}MB")
 
147
 
148
  return video_bytes
149
 
150
 
 
151
  @app.function(image=image)
152
  @modal.web_endpoint(method="POST")
153
  def process_video_endpoint(data: dict):
154
+ """Web endpoint"""
 
 
 
155
  video_url = data.get("video_url")
156
  quote_text = data.get("quote_text")
157
+ audio_b64 = data.get("audio_b64")
158
 
159
  if not video_url or not quote_text:
160
  return {"error": "Missing video_url or quote_text"}, 400
 
162
  try:
163
  video_bytes = process_quote_video.remote(video_url, quote_text, audio_b64)
164
 
 
165
  import base64
166
  video_b64 = base64.b64encode(video_bytes).decode()
167
 
 
173
 
174
  except Exception as e:
175
  return {"error": str(e)}, 500