ismdrobiul489 commited on
Commit
ee36c8e
·
1 Parent(s): 800faa1

feat: Major optimizations - Quiz dynamic fonts, TTS 1.2x speed, Video stream copy (10x faster), Single API call, Fact Image dynamic fonts, Text Story position fix

Browse files
modules/fact_image/services/text_overlay.py CHANGED
@@ -77,23 +77,33 @@ class TextOverlay:
77
 
78
  return self._font_cache[cache_key]
79
 
80
- def _wrap_text(self, text: str, max_words_per_line: int = 4) -> str:
81
  """
82
  Wrap text for optimal display.
83
 
84
  Rules:
85
- - Max 4-5 words per line
 
86
  - Natural line breaks
87
  """
88
  words = text.split()
89
  lines = []
90
  current_line = []
 
91
 
92
  for word in words:
93
- current_line.append(word)
94
- if len(current_line) >= max_words_per_line:
 
 
 
 
95
  lines.append(' '.join(current_line))
96
- current_line = []
 
 
 
 
97
 
98
  if current_line:
99
  lines.append(' '.join(current_line))
@@ -177,17 +187,59 @@ class TextOverlay:
177
  overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
178
  draw = ImageDraw.Draw(overlay)
179
 
180
- # Get fonts (both bold now)
181
- heading_font = self._get_font(self.HEADING_FONT_SIZE, bold=True)
182
- text_font = self._get_font(self.TEXT_FONT_SIZE, bold=True) # Fact text also bold
183
-
184
- # Calculate max width
185
  max_width = self.TARGET_WIDTH - (2 * self.PADDING_X)
186
 
187
- # Prepare wrapped text
188
- wrapped_text = self._wrap_text(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- # Calculate text dimensions
191
  text_bbox = draw.multiline_textbbox((0, 0), wrapped_text, font=text_font)
192
  text_width = text_bbox[2] - text_bbox[0]
193
  text_height = text_bbox[3] - text_bbox[1]
 
77
 
78
  return self._font_cache[cache_key]
79
 
80
+ def _wrap_text(self, text: str, max_words_per_line: int = 4, max_chars_per_line: int = 25) -> str:
81
  """
82
  Wrap text for optimal display.
83
 
84
  Rules:
85
+ - Max characters per line to prevent overflow
86
+ - Max words per line for readability
87
  - Natural line breaks
88
  """
89
  words = text.split()
90
  lines = []
91
  current_line = []
92
+ current_char_count = 0
93
 
94
  for word in words:
95
+ word_len = len(word)
96
+ # Check if adding this word would exceed limits
97
+ new_char_count = current_char_count + word_len + (1 if current_line else 0)
98
+
99
+ if (len(current_line) >= max_words_per_line or
100
+ new_char_count > max_chars_per_line) and current_line:
101
  lines.append(' '.join(current_line))
102
+ current_line = [word]
103
+ current_char_count = word_len
104
+ else:
105
+ current_line.append(word)
106
+ current_char_count = new_char_count
107
 
108
  if current_line:
109
  lines.append(' '.join(current_line))
 
187
  overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
188
  draw = ImageDraw.Draw(overlay)
189
 
190
+ # Calculate max width (with padding on sides)
 
 
 
 
191
  max_width = self.TARGET_WIDTH - (2 * self.PADDING_X)
192
 
193
+ # Dynamic font size for HEADING based on text length
194
+ heading_text_length = len(heading) if heading else 0
195
+ if heading_text_length <= 15:
196
+ heading_font_size = 80 # Large for short heading
197
+ elif heading_text_length <= 25:
198
+ heading_font_size = 70
199
+ elif heading_text_length <= 35:
200
+ heading_font_size = 60
201
+ elif heading_text_length <= 50:
202
+ heading_font_size = 50
203
+ else:
204
+ heading_font_size = 42 # Minimum for very long heading
205
+
206
+ # Dynamic font size for FACT TEXT based on text length
207
+ text_length = len(text)
208
+ if text_length <= 50:
209
+ text_font_size = 60 # Large for short text
210
+ elif text_length <= 100:
211
+ text_font_size = 52
212
+ elif text_length <= 150:
213
+ text_font_size = 46
214
+ elif text_length <= 200:
215
+ text_font_size = 40
216
+ elif text_length <= 300:
217
+ text_font_size = 36
218
+ else:
219
+ text_font_size = 32 # Minimum for very long text
220
+
221
+ # Get fonts with dynamic sizes (both bold)
222
+ heading_font = self._get_font(heading_font_size, bold=True)
223
+ text_font = self._get_font(text_font_size, bold=True)
224
+
225
+ # Word wrap text with dynamic font to fit max_width
226
+ words = text.split()
227
+ lines = []
228
+ current_line = ""
229
+ for word in words:
230
+ test_line = current_line + " " + word if current_line else word
231
+ bbox = draw.textbbox((0, 0), test_line, font=text_font)
232
+ if bbox[2] - bbox[0] <= max_width:
233
+ current_line = test_line
234
+ else:
235
+ if current_line:
236
+ lines.append(current_line)
237
+ current_line = word
238
+ if current_line:
239
+ lines.append(current_line)
240
+ wrapped_text = "\n".join(lines)
241
 
242
+ # Calculate text dimensions with wrapped text
243
  text_bbox = draw.multiline_textbbox((0, 0), wrapped_text, font=text_font)
244
  text_width = text_bbox[2] - text_bbox[0]
245
  text_height = text_bbox[3] - text_bbox[1]
modules/quiz_reel/router.py CHANGED
@@ -79,7 +79,7 @@ async def generate_quiz_video(job_id: str, quizzes: list, voice: str):
79
  question_audio_path = os.path.join(temp_dir, f"question_{i}.wav")
80
 
81
  logger.info(f"Generating TTS for question {i+1}: {question_text[:30]}...")
82
- audio_bytes, duration = await tts_client.generate(text=question_text, voice=voice)
83
  with open(question_audio_path, "wb") as f:
84
  f.write(audio_bytes)
85
 
@@ -90,7 +90,7 @@ async def generate_quiz_video(job_id: str, quizzes: list, voice: str):
90
  answer_audio_path = os.path.join(temp_dir, f"answer_{i}.wav")
91
 
92
  logger.info(f"Generating TTS for answer {i+1}: {answer_text[:30]}...")
93
- audio_bytes, duration = await tts_client.generate(text=answer_text, voice=voice)
94
  with open(answer_audio_path, "wb") as f:
95
  f.write(audio_bytes)
96
 
 
79
  question_audio_path = os.path.join(temp_dir, f"question_{i}.wav")
80
 
81
  logger.info(f"Generating TTS for question {i+1}: {question_text[:30]}...")
82
+ audio_bytes, duration = await tts_client.generate(text=question_text, voice=voice, speed=1.2)
83
  with open(question_audio_path, "wb") as f:
84
  f.write(audio_bytes)
85
 
 
90
  answer_audio_path = os.path.join(temp_dir, f"answer_{i}.wav")
91
 
92
  logger.info(f"Generating TTS for answer {i+1}: {answer_text[:30]}...")
93
+ audio_bytes, duration = await tts_client.generate(text=answer_text, voice=voice, speed=1.2)
94
  with open(answer_audio_path, "wb") as f:
95
  f.write(audio_bytes)
96
 
modules/quiz_reel/services/quiz_frame.py CHANGED
@@ -182,13 +182,37 @@ class QuizFrameGenerator:
182
  img = self._create_gradient_background()
183
  draw = ImageDraw.Draw(img)
184
 
185
- # Hook text with background box
186
  if hook:
187
  hook_text = hook.upper()
188
  hook_y = int(self.HEIGHT * self.HOOK_Y)
189
 
190
- # Calculate hook text dimensions
191
- bbox = draw.textbbox((0, 0), hook_text, font=self.font_hook)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  text_width = bbox[2] - bbox[0]
193
  text_height = bbox[3] - bbox[1]
194
 
@@ -204,9 +228,11 @@ class QuizFrameGenerator:
204
  self._draw_rounded_rect(draw, (box_x1, box_y1, box_x2, box_y2),
205
  self.HOOK_BG_COLOR, radius=25)
206
 
207
- # Draw hook text centered in box
208
- self._draw_text_centered(draw, hook_text, hook_y,
209
- self.font_hook, self.HOOK_COLOR)
 
 
210
 
211
  # Question card
212
  card_x1 = int(self.WIDTH * 0.05)
@@ -277,9 +303,10 @@ class QuizFrameGenerator:
277
  x = (self.WIDTH - text_width) // 2
278
  draw.text((x, text_start_y + i * line_height), line, fill=self.QUESTION_TEXT, font=question_font)
279
 
280
- # Options
281
  option_width = int(self.WIDTH * 0.85)
282
  option_x = (self.WIDTH - option_width) // 2
 
283
 
284
  for i, (key, value) in enumerate(options.items()):
285
  y_pos = int(self.HEIGHT * (self.OPTIONS_START + i * self.OPTIONS_GAP))
@@ -289,12 +316,37 @@ class QuizFrameGenerator:
289
  (option_x, y_pos, option_x + option_width, y_pos + self.OPTION_HEIGHT),
290
  self.OPTION_DEFAULT_BG, radius=50)
291
 
292
- # Option text
293
  option_text = f"{key}. {value}"
294
- bbox = draw.textbbox((0, 0), option_text, font=self.font_option)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  text_height = bbox[3] - bbox[1]
296
  text_y = y_pos + (self.OPTION_HEIGHT - text_height) // 2
297
- draw.text((option_x + 30, text_y), option_text, fill=self.OPTION_TEXT, font=self.font_option)
298
 
299
  # Bottom right removed (was sparkle icon)
300
 
@@ -492,7 +544,7 @@ class QuizFrameGenerator:
492
  checkmark_y = y_pos + self.OPTION_HEIGHT // 2
493
  self._draw_checkmark(draw, (checkmark_x, checkmark_y))
494
 
495
- # Explain section with heading
496
  if explain:
497
  # Options end at approximately 67% (OPTIONS_START + 3*OPTIONS_GAP + OPTION_HEIGHT)
498
  # Explain text at EXPLAIN_Y (72%)
@@ -502,7 +554,6 @@ class QuizFrameGenerator:
502
  heading_y = (options_end_y + explain_text_y) // 2 # Midpoint
503
 
504
  # "Explanation" heading (bold, bigger font, different color - cyan/teal)
505
- heading_color = (56, 189, 248) # Sky blue #38BDF8
506
  heading_text = "Explanation"
507
 
508
  # Load 52px font for heading
@@ -516,12 +567,76 @@ class QuizFrameGenerator:
516
  except:
517
  pass
518
 
519
- # Draw heading centered
520
- self._draw_text_centered(draw, heading_text, heading_y, heading_font, heading_color)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
 
522
- # Explain text at original position
523
- self._draw_text_centered(draw, explain, explain_text_y + 30, self.font_explain,
524
- self.EXPLAIN_COLOR, max_width=int(self.WIDTH * 0.85))
 
 
525
 
526
  # Bottom right removed (was sparkle icon)
527
 
 
182
  img = self._create_gradient_background()
183
  draw = ImageDraw.Draw(img)
184
 
185
+ # Hook text with background box and dynamic font sizing
186
  if hook:
187
  hook_text = hook.upper()
188
  hook_y = int(self.HEIGHT * self.HOOK_Y)
189
 
190
+ # Calculate dynamic font size based on text length
191
+ text_length = len(hook_text)
192
+ if text_length <= 10:
193
+ hook_font_size = 72 # Default large
194
+ elif text_length <= 15:
195
+ hook_font_size = 60
196
+ elif text_length <= 20:
197
+ hook_font_size = 50
198
+ elif text_length <= 30:
199
+ hook_font_size = 40
200
+ else:
201
+ hook_font_size = 32 # Minimum size
202
+
203
+ # Load hook font with calculated size
204
+ hook_font = self.font_hook
205
+ for path in ["C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/ArialBD.ttf",
206
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"]:
207
+ if os.path.exists(path):
208
+ try:
209
+ hook_font = ImageFont.truetype(path, hook_font_size)
210
+ break
211
+ except:
212
+ pass
213
+
214
+ # Calculate hook text dimensions with dynamic font
215
+ bbox = draw.textbbox((0, 0), hook_text, font=hook_font)
216
  text_width = bbox[2] - bbox[0]
217
  text_height = bbox[3] - bbox[1]
218
 
 
228
  self._draw_rounded_rect(draw, (box_x1, box_y1, box_x2, box_y2),
229
  self.HOOK_BG_COLOR, radius=25)
230
 
231
+ # Draw hook text centered in box with dynamic font
232
+ bbox = draw.textbbox((0, 0), hook_text, font=hook_font)
233
+ text_width = bbox[2] - bbox[0]
234
+ x = (self.WIDTH - text_width) // 2
235
+ draw.text((x, hook_y), hook_text, fill=self.HOOK_COLOR, font=hook_font)
236
 
237
  # Question card
238
  card_x1 = int(self.WIDTH * 0.05)
 
303
  x = (self.WIDTH - text_width) // 2
304
  draw.text((x, text_start_y + i * line_height), line, fill=self.QUESTION_TEXT, font=question_font)
305
 
306
+ # Options - with dynamic font sizing
307
  option_width = int(self.WIDTH * 0.85)
308
  option_x = (self.WIDTH - option_width) // 2
309
+ max_option_text_width = option_width - 60 # Leave padding
310
 
311
  for i, (key, value) in enumerate(options.items()):
312
  y_pos = int(self.HEIGHT * (self.OPTIONS_START + i * self.OPTIONS_GAP))
 
316
  (option_x, y_pos, option_x + option_width, y_pos + self.OPTION_HEIGHT),
317
  self.OPTION_DEFAULT_BG, radius=50)
318
 
319
+ # Option text with dynamic font size (like question)
320
  option_text = f"{key}. {value}"
321
+ text_length = len(option_text)
322
+
323
+ # Calculate optimal font size based on text length
324
+ if text_length <= 20:
325
+ font_size = 36 # Default size
326
+ elif text_length <= 35:
327
+ font_size = 32
328
+ elif text_length <= 50:
329
+ font_size = 28
330
+ elif text_length <= 70:
331
+ font_size = 24
332
+ else:
333
+ font_size = 20 # Minimum size
334
+
335
+ # Load font with calculated size
336
+ option_font = self.font_option
337
+ for path in ["C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/ArialBD.ttf",
338
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"]:
339
+ if os.path.exists(path):
340
+ try:
341
+ option_font = ImageFont.truetype(path, font_size)
342
+ break
343
+ except:
344
+ pass
345
+
346
+ bbox = draw.textbbox((0, 0), option_text, font=option_font)
347
  text_height = bbox[3] - bbox[1]
348
  text_y = y_pos + (self.OPTION_HEIGHT - text_height) // 2
349
+ draw.text((option_x + 30, text_y), option_text, fill=self.OPTION_TEXT, font=option_font)
350
 
351
  # Bottom right removed (was sparkle icon)
352
 
 
544
  checkmark_y = y_pos + self.OPTION_HEIGHT // 2
545
  self._draw_checkmark(draw, (checkmark_x, checkmark_y))
546
 
547
+ # Explain section with heading and white background box
548
  if explain:
549
  # Options end at approximately 67% (OPTIONS_START + 3*OPTIONS_GAP + OPTION_HEIGHT)
550
  # Explain text at EXPLAIN_Y (72%)
 
554
  heading_y = (options_end_y + explain_text_y) // 2 # Midpoint
555
 
556
  # "Explanation" heading (bold, bigger font, different color - cyan/teal)
 
557
  heading_text = "Explanation"
558
 
559
  # Load 52px font for heading
 
567
  except:
568
  pass
569
 
570
+ # Dynamic font size for explain text based on length
571
+ explain_length = len(explain)
572
+ if explain_length <= 30:
573
+ explain_font_size = 44 # Large for short text
574
+ elif explain_length <= 60:
575
+ explain_font_size = 38
576
+ elif explain_length <= 100:
577
+ explain_font_size = 32
578
+ elif explain_length <= 150:
579
+ explain_font_size = 28
580
+ else:
581
+ explain_font_size = 24 # Minimum for very long text
582
+
583
+ # Load explain font with calculated size
584
+ explain_font = self.font_explain
585
+ for path in ["C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/ArialBD.ttf",
586
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"]:
587
+ if os.path.exists(path):
588
+ try:
589
+ explain_font = ImageFont.truetype(path, explain_font_size)
590
+ break
591
+ except:
592
+ pass
593
+
594
+ # Word wrap explain text to fit in box
595
+ max_explain_width = int(self.WIDTH * 0.8)
596
+ words = explain.split()
597
+ lines = []
598
+ current_line = ""
599
+ for word in words:
600
+ test_line = current_line + " " + word if current_line else word
601
+ bbox = draw.textbbox((0, 0), test_line, font=explain_font)
602
+ if bbox[2] - bbox[0] <= max_explain_width:
603
+ current_line = test_line
604
+ else:
605
+ if current_line:
606
+ lines.append(current_line)
607
+ current_line = word
608
+ if current_line:
609
+ lines.append(current_line)
610
+
611
+ wrapped_explain = "\n".join(lines)
612
+
613
+ # Calculate wrapped text dimensions
614
+ explain_bbox = draw.multiline_textbbox((0, 0), wrapped_explain, font=explain_font)
615
+ explain_width = explain_bbox[2] - explain_bbox[0]
616
+ explain_height = explain_bbox[3] - explain_bbox[1]
617
+
618
+ # Fixed box size (relative to screen width)
619
+ box_width = int(self.WIDTH * 0.9)
620
+ box_padding_x = 40
621
+ box_padding_y = 25
622
+ box_x1 = (self.WIDTH - box_width) // 2
623
+ box_x2 = box_x1 + box_width
624
+ box_y1 = heading_y - 20 # Above heading
625
+ box_y2 = explain_text_y + 30 + explain_height + box_padding_y + 20 # Below explain text
626
+
627
+ # Draw white rounded rectangle background
628
+ self._draw_rounded_rect(draw, (box_x1, box_y1, box_x2, box_y2),
629
+ (255, 255, 255), radius=20)
630
+
631
+ # Draw heading centered (now on white bg, use darker color)
632
+ heading_color_on_white = (30, 100, 150) # Darker blue for white bg
633
+ self._draw_text_centered(draw, heading_text, heading_y, heading_font, heading_color_on_white)
634
 
635
+ # Draw wrapped explain text centered (use dark text on white bg)
636
+ explain_text_color = (50, 50, 50) # Dark gray for visibility
637
+ text_x = (self.WIDTH - explain_width) // 2
638
+ draw.multiline_text((text_x, explain_text_y + 30), wrapped_explain,
639
+ fill=explain_text_color, font=explain_font, align="center")
640
 
641
  # Bottom right removed (was sparkle icon)
642
 
modules/text_story/router.py CHANGED
@@ -310,7 +310,7 @@ OUTPUT FORMAT (strict JSON):
310
  {{"sender": "A", "text": "What's up?"}},
311
  ...
312
  ],
313
- "ending_text": "To be continued..."
314
  }}
315
 
316
  Only output valid JSON, nothing else."""
 
310
  {{"sender": "A", "text": "What's up?"}},
311
  ...
312
  ],
313
+ "ending_text": ""
314
  }}
315
 
316
  Only output valid JSON, nothing else."""
modules/text_story/services/renderer.py CHANGED
@@ -26,7 +26,7 @@ CANVAS_HEIGHT = 1920
26
  # - Small avatar next to other person's messages
27
 
28
  LAYOUT = {
29
- "top_margin": 670, # 35% from top (1920 * 0.35)
30
  "side_margin": 50, # More side margin (left/right)
31
  "chat_box_width": 980, # Narrower card (1080 - 50*2)
32
  "chat_box_radius": 35, # Rounded corners
 
26
  # - Small avatar next to other person's messages
27
 
28
  LAYOUT = {
29
+ "top_margin": 500, # 26% from top (moved up 8-10% from original 35%)
30
  "side_margin": 50, # More side margin (left/right)
31
  "chat_box_width": 980, # Narrower card (1080 - 50*2)
32
  "chat_box_radius": 35, # Rounded corners
modules/video_creator/services/libraries/ffmpeg_utils.py CHANGED
@@ -135,6 +135,8 @@ class FFmpegUtils:
135
  def cut_video(input_path: Path, output_path: Path, start_time: float, duration: float):
136
  """
137
  Cut a segment from a video file using FFmpeg.
 
 
138
 
139
  Args:
140
  input_path: Source video
@@ -145,12 +147,11 @@ class FFmpegUtils:
145
  try:
146
  cmd = [
147
  "ffmpeg",
148
- "-ss", str(start_time),
149
  "-i", str(input_path),
150
  "-t", str(duration),
151
- "-c:v", "libx264",
152
- "-preset", "fast",
153
- "-c:a", "aac",
154
  "-y",
155
  str(output_path)
156
  ]
 
135
  def cut_video(input_path: Path, output_path: Path, start_time: float, duration: float):
136
  """
137
  Cut a segment from a video file using FFmpeg.
138
+ Uses stream copy for 10x faster cutting (no re-encoding).
139
+ Audio is removed since TTS is used separately.
140
 
141
  Args:
142
  input_path: Source video
 
147
  try:
148
  cmd = [
149
  "ffmpeg",
150
+ "-ss", str(start_time), # Seek to start (before -i for fast seeking)
151
  "-i", str(input_path),
152
  "-t", str(duration),
153
+ "-c:v", "copy", # Stream copy - no re-encode (10x faster!)
154
+ "-an", # Remove audio (TTS is used)
 
155
  "-y",
156
  str(output_path)
157
  ]
modules/video_creator/services/libraries/tts_client.py CHANGED
@@ -19,20 +19,21 @@ class TTSClient:
19
  self.api_url = api_url.rstrip('/')
20
  logger.info(f"Using cloud TTS API at {self.api_url}")
21
 
22
- async def generate(self, text: str, voice: str) -> Tuple[bytes, float]:
23
  """
24
  Generate speech from text
25
 
26
  Args:
27
  text: Text to convert to speech
28
  voice: Voice identifier (e.g., 'af_heart', 'am_adam')
 
29
 
30
  Returns:
31
  Tuple of (audio_bytes, duration_seconds)
32
  """
33
  endpoint = f"{self.api_url}/v1/audio/speech"
34
 
35
- logger.debug(f"Generating audio with voice={voice}, text_length={len(text)}")
36
 
37
  async with aiohttp.ClientSession() as session:
38
  async with session.post(
@@ -40,7 +41,8 @@ class TTSClient:
40
  json={
41
  "model": "kokoro",
42
  "input": text,
43
- "voice": voice
 
44
  },
45
  headers={"Content-Type": "application/json"},
46
  timeout=aiohttp.ClientTimeout(total=120)
 
19
  self.api_url = api_url.rstrip('/')
20
  logger.info(f"Using cloud TTS API at {self.api_url}")
21
 
22
+ async def generate(self, text: str, voice: str, speed: float = 1.0) -> Tuple[bytes, float]:
23
  """
24
  Generate speech from text
25
 
26
  Args:
27
  text: Text to convert to speech
28
  voice: Voice identifier (e.g., 'af_heart', 'am_adam')
29
+ speed: Speech speed multiplier (0.5-2.0, default 1.0)
30
 
31
  Returns:
32
  Tuple of (audio_bytes, duration_seconds)
33
  """
34
  endpoint = f"{self.api_url}/v1/audio/speech"
35
 
36
+ logger.debug(f"Generating audio with voice={voice}, speed={speed}, text_length={len(text)}")
37
 
38
  async with aiohttp.ClientSession() as session:
39
  async with session.post(
 
41
  json={
42
  "model": "kokoro",
43
  "input": text,
44
+ "voice": voice,
45
+ "speed": speed
46
  },
47
  headers={"Content-Type": "application/json"},
48
  timeout=aiohttp.ClientTimeout(total=120)
modules/video_creator/services/short_creator.py CHANGED
@@ -197,66 +197,47 @@ class ShortCreator:
197
  # Ensure keywords is a list for find_video
198
  search_keywords = keywords if isinstance(keywords, list) else [keywords]
199
 
200
- # Search BOTH platforms (if configured) - pick best result
201
- pexels_video = None
202
- pixabay_video = None
203
  selected_video = None
204
- video_source = None # Track which platform we use
205
 
206
- # Try Pexels (if configured)
207
  if self.pexels:
208
  try:
209
  logger.debug(f"Searching Pexels for: {search_keywords}")
210
- pexels_video = self.pexels.find_video(
211
  search_keywords,
212
  search_duration,
213
  exclude_video_ids,
214
  orientation
215
  )
 
 
 
216
  except Exception as e:
217
  logger.warning(f"Pexels search failed: {e}")
218
 
219
- # Try Pixabay (if configured) - ALWAYS try both
220
- if self.pixabay:
221
  try:
222
- logger.debug(f"Searching Pixabay for: {search_keywords}")
223
- pixabay_video = self.pixabay.find_video(
224
  search_keywords,
225
  search_duration,
226
  exclude_video_ids,
227
  orientation
228
  )
 
 
 
229
  except Exception as e:
230
  logger.warning(f"Pixabay search failed: {e}")
231
 
232
- # Smart Selection: Pick best video based on duration match
233
- if pexels_video and pixabay_video:
234
- # Both found - pick the one closer to required duration
235
- pexels_dur_diff = abs(pexels_video.get("duration", 0) - search_duration)
236
- pixabay_dur_diff = abs(pixabay_video.get("duration", 0) - search_duration)
237
-
238
- if pexels_dur_diff <= pixabay_dur_diff:
239
- selected_video = pexels_video
240
- video_source = "Pexels"
241
- else:
242
- selected_video = pixabay_video
243
- video_source = "Pixabay"
244
-
245
- logger.info(f"Both APIs found videos - selected {video_source} (better duration match)")
246
-
247
- elif pexels_video:
248
- selected_video = pexels_video
249
- video_source = "Pexels"
250
- logger.info(f"Found video on Pexels for '{keyword}'")
251
-
252
- elif pixabay_video:
253
- selected_video = pixabay_video
254
- video_source = "Pixabay"
255
- logger.info(f"Found video on Pixabay for '{keyword}'")
256
-
257
  # If no video found on either platform
258
  if not selected_video:
259
  raise Exception(f"No video found for {search_keywords} on any platform")
 
260
 
261
  video_path = self.config.temp_dir_path / f"{temp_vid_id}.mp4"
262
  temp_files.append(video_path)
 
197
  # Ensure keywords is a list for find_video
198
  search_keywords = keywords if isinstance(keywords, list) else [keywords]
199
 
200
+ # PRIORITY: Try ONE platform only to save API quota
201
+ # Priority: Pexels first, Pixabay as fallback
 
202
  selected_video = None
203
+ video_source = None
204
 
205
+ # Try Pexels FIRST (if configured)
206
  if self.pexels:
207
  try:
208
  logger.debug(f"Searching Pexels for: {search_keywords}")
209
+ selected_video = self.pexels.find_video(
210
  search_keywords,
211
  search_duration,
212
  exclude_video_ids,
213
  orientation
214
  )
215
+ if selected_video:
216
+ video_source = "Pexels"
217
+ logger.info(f"Found video on Pexels for '{keyword}'")
218
  except Exception as e:
219
  logger.warning(f"Pexels search failed: {e}")
220
 
221
+ # FALLBACK: Try Pixabay ONLY if Pexels didn't find anything
222
+ if not selected_video and self.pixabay:
223
  try:
224
+ logger.debug(f"Searching Pixabay (fallback) for: {search_keywords}")
225
+ selected_video = self.pixabay.find_video(
226
  search_keywords,
227
  search_duration,
228
  exclude_video_ids,
229
  orientation
230
  )
231
+ if selected_video:
232
+ video_source = "Pixabay"
233
+ logger.info(f"Found video on Pixabay for '{keyword}'")
234
  except Exception as e:
235
  logger.warning(f"Pixabay search failed: {e}")
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  # If no video found on either platform
238
  if not selected_video:
239
  raise Exception(f"No video found for {search_keywords} on any platform")
240
+
241
 
242
  video_path = self.config.temp_dir_path / f"{temp_vid_id}.mp4"
243
  temp_files.append(video_path)