ismdrobiul489 commited on
Commit
d4f61bc
·
1 Parent(s): 36f63dd

feat: Multiple updates - HTML optimization, dual API search, text story tuning, Messenger UI

Browse files
modules/fact_image/services/text_overlay.py CHANGED
@@ -34,9 +34,9 @@ class TextOverlay:
34
  LINE_SPACING = 1.3
35
 
36
  # Layout settings
37
- HEADING_TOP_PERCENT = 0.40 # Heading starts at 40% from top
38
- TEXT_TOP_PERCENT = 0.50 # Fact text starts at 50% from top
39
- GAP_HEADING_TEXT = 80 # Gap between heading and fact text (fallback)
40
 
41
  def __init__(self, font_path: Optional[str] = None):
42
  """
@@ -195,8 +195,8 @@ class TextOverlay:
195
  # Calculate heading dimensions if present
196
  heading_height = 0
197
  heading_width = 0
198
- heading_bg_padding = 22
199
- heading_bg_radius = 28
200
 
201
  if heading:
202
  heading_bbox = draw.textbbox((0, 0), heading, font=heading_font)
@@ -228,7 +228,7 @@ class TextOverlay:
228
 
229
  # Draw background if enabled
230
  if heading_background and heading_background.get('enabled', True):
231
- bg_color = self._parse_rgba(heading_background.get('color', 'rgba(0, 0, 0, 0.45)'))
232
 
233
  bg_x1 = heading_x - heading_bg_padding
234
  bg_y1 = current_y - heading_bg_padding // 2
@@ -252,11 +252,36 @@ class TextOverlay:
252
 
253
  current_y += heading_height + heading_bg_padding + gap_between
254
 
255
- # Draw fact text with shadow/outline
256
  # Use fixed position at 50% from top
257
  fact_text_y = text_y if heading else int(self.TARGET_HEIGHT * 0.40)
258
  text_x = (self.TARGET_WIDTH - text_width) // 2
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  # Draw outline (multiple directions for thickness)
261
  for dx in range(-outline_width, outline_width + 1):
262
  for dy in range(-outline_width, outline_width + 1):
 
34
  LINE_SPACING = 1.3
35
 
36
  # Layout settings
37
+ HEADING_TOP_PERCENT = 0.30 # Heading starts at 30% from top (more visible)
38
+ TEXT_TOP_PERCENT = 0.45 # Fact text starts at 45% from top
39
+ GAP_HEADING_TEXT = 60 # Gap between heading and fact text (fallback)
40
 
41
  def __init__(self, font_path: Optional[str] = None):
42
  """
 
195
  # Calculate heading dimensions if present
196
  heading_height = 0
197
  heading_width = 0
198
+ heading_bg_padding = 12 # Tight padding - close to text
199
+ heading_bg_radius = 15 # Smaller radius for tighter look
200
 
201
  if heading:
202
  heading_bbox = draw.textbbox((0, 0), heading, font=heading_font)
 
228
 
229
  # Draw background if enabled
230
  if heading_background and heading_background.get('enabled', True):
231
+ bg_color = self._parse_rgba(heading_background.get('color', 'rgba(255, 109, 128, 0.95)'))
232
 
233
  bg_x1 = heading_x - heading_bg_padding
234
  bg_y1 = current_y - heading_bg_padding // 2
 
252
 
253
  current_y += heading_height + heading_bg_padding + gap_between
254
 
255
+ # Draw fact text with glass background
256
  # Use fixed position at 50% from top
257
  fact_text_y = text_y if heading else int(self.TARGET_HEIGHT * 0.40)
258
  text_x = (self.TARGET_WIDTH - text_width) // 2
259
 
260
+ # Draw glass background behind fact text
261
+ glass_padding = 35
262
+ glass_radius = 25
263
+ glass_x1 = text_x - glass_padding
264
+ glass_y1 = fact_text_y - glass_padding
265
+ glass_x2 = text_x + text_width + glass_padding
266
+ glass_y2 = fact_text_y + text_height + glass_padding
267
+
268
+ # Glassmorphism effect - semi-transparent dark with slight blur effect
269
+ glass_color = (0, 0, 0, 100) # Dark glass with 40% opacity
270
+ self._draw_rounded_rect(
271
+ draw,
272
+ (glass_x1, glass_y1, glass_x2, glass_y2),
273
+ glass_radius,
274
+ glass_color
275
+ )
276
+
277
+ # Draw subtle border for glass effect
278
+ draw.rounded_rectangle(
279
+ (glass_x1, glass_y1, glass_x2, glass_y2),
280
+ radius=glass_radius,
281
+ outline=(255, 255, 255, 40), # Subtle white border
282
+ width=2
283
+ )
284
+
285
  # Draw outline (multiple directions for thickness)
286
  for dx in range(-outline_width, outline_width + 1):
287
  for dy in range(-outline_width, outline_width + 1):
modules/text_story/services/renderer.py CHANGED
@@ -33,22 +33,23 @@ LAYOUT = {
33
  "max_chat_height": 1100, # Maximum height of entire chat box
34
  }
35
 
36
- # Colors (iMessage dark style from reference)
37
  COLORS = {
38
- # Header (dark)
39
- "header_bg": (28, 28, 30), # #1C1C1E - Dark header
40
 
41
- # Bubbles (exactly like reference)
42
- "bubble_other": (58, 58, 60), # #3A3A3C - Gray for left (other person)
43
- "bubble_user": (0, 122, 255), # #007AFF - Blue for right (user)
44
 
45
  # Text
46
- "text_white": (255, 255, 255), # White text
47
- "text_gray": (142, 142, 147), # #8E8E93 - Secondary text
48
- "text_blue": (0, 122, 255), # Blue accent
 
49
 
50
  # Background
51
- "chat_bg": (0, 0, 0), # Black chat area background
52
  }
53
 
54
  # UI Measurements
@@ -234,13 +235,14 @@ class ChatRenderer:
234
  fill=color
235
  )
236
 
237
- # Draw text
 
238
  text_x = x + UI["bubble_padding_h"]
239
  text_y = y + UI["bubble_padding_v"]
240
  line_height = self.font.getbbox("Ay")[3] + 4
241
 
242
  for line in lines:
243
- draw.text((text_x, text_y), line, fill=COLORS["text_white"], font=self.font)
244
  text_y += line_height
245
 
246
  return y + height
 
33
  "max_chat_height": 1100, # Maximum height of entire chat box
34
  }
35
 
36
+ # Colors (Facebook Messenger style - white/light theme)
37
  COLORS = {
38
+ # Header (white/light gray)
39
+ "header_bg": (255, 255, 255), # White header
40
 
41
+ # Bubbles (Messenger style)
42
+ "bubble_other": (228, 230, 235), # Light gray for left (other person)
43
+ "bubble_user": (0, 132, 255), # Messenger blue for right (user)
44
 
45
  # Text
46
+ "text_white": (255, 255, 255), # White text (for blue bubble)
47
+ "text_black": (5, 5, 5), # Black text (for gray bubble)
48
+ "text_gray": (101, 103, 107), # Secondary text
49
+ "text_blue": (0, 132, 255), # Blue accent
50
 
51
  # Background
52
+ "chat_bg": (255, 255, 255), # White chat area background
53
  }
54
 
55
  # UI Measurements
 
235
  fill=color
236
  )
237
 
238
+ # Draw text (white on blue, black on gray)
239
+ text_color = COLORS["text_white"] if is_user else COLORS.get("text_black", (5, 5, 5))
240
  text_x = x + UI["bubble_padding_h"]
241
  text_y = y + UI["bubble_padding_v"]
242
  line_height = self.font.getbbox("Ay")[3] + 4
243
 
244
  for line in lines:
245
+ draw.text((text_x, text_y), line, fill=text_color, font=self.font)
246
  text_y += line_height
247
 
248
  return y + height
modules/text_story/services/tts_handler.py CHANGED
@@ -53,7 +53,7 @@ class TTSHandler:
53
  "model": "kokoro",
54
  "input": text,
55
  "voice": voice,
56
- "speed": 1.3 # Faster voice for engaging content
57
  }
58
 
59
  async with session.post(
 
53
  "model": "kokoro",
54
  "input": text,
55
  "voice": voice,
56
+ "speed": 1.4 # Faster voice for engaging content
57
  }
58
 
59
  async with session.post(
modules/text_story/services/video_composer.py CHANGED
@@ -21,18 +21,18 @@ from .tts_handler import TTSHandler
21
 
22
  logger = logging.getLogger(__name__)
23
 
24
- # Timing configurations (realistic chat behavior)
25
  TIMING = {
26
- "typing_base": 0.5, # Base typing indicator duration
27
- "typing_per_char": 0.008, # Additional time per character
28
- "typing_max": 1.2, # Max typing duration
29
- "human_pause_min": 0.3, # Minimum pause before message
30
- "human_pause_max": 0.8, # Maximum pause
31
- "voice_delay": 0.15, # Gap between text appear and voice
32
- "micro_silence": 0.3, # Silence between messages
33
- "last_msg_pause": 1.5, # Pause after last message
34
- "ending_duration": 3.0, # Ending text duration
35
- "ending_fade": 0.5, # Ending fade in/out
36
  }
37
 
38
 
 
21
 
22
  logger = logging.getLogger(__name__)
23
 
24
+ # Timing configurations (fast-paced for engaging content)
25
  TIMING = {
26
+ "typing_base": 0.25, # Base typing indicator duration (reduced)
27
+ "typing_per_char": 0.004, # Additional time per character (halved)
28
+ "typing_max": 0.6, # Max typing duration (halved)
29
+ "human_pause_min": 0.1, # Minimum pause before message (reduced)
30
+ "human_pause_max": 0.3, # Maximum pause (reduced)
31
+ "voice_delay": 0.08, # Gap between text appear and voice (reduced)
32
+ "micro_silence": 0.12, # Silence between messages (reduced from 0.3)
33
+ "last_msg_pause": 0.8, # Pause after last message (reduced)
34
+ "ending_duration": 2.5, # Ending text duration
35
+ "ending_fade": 0.3, # Ending fade in/out
36
  }
37
 
38
 
modules/video_creator/services/short_creator.py CHANGED
@@ -197,35 +197,62 @@ class ShortCreator:
197
  # Ensure keywords is a list for find_video
198
  search_keywords = keywords if isinstance(keywords, list) else [keywords]
199
 
200
- # Try Pexels first (if configured)
201
  pexels_video = None
202
  pixabay_video = None
203
  selected_video = None
 
204
 
 
205
  if self.pexels:
206
- logger.debug(f"Trying Pexels for: {search_keywords}")
207
- pexels_video = self.pexels.find_video(
208
- search_keywords,
209
- search_duration,
210
- exclude_video_ids,
211
- orientation
212
- )
213
- if pexels_video:
214
- selected_video = pexels_video
215
- logger.info(f"Found video on Pexels for '{keyword}'")
216
 
217
- # Try Pixabay if Pexels didn't find anything (and Pixabay is configured)
218
- if not selected_video and self.pixabay:
219
- logger.debug(f"Trying Pixabay for: {search_keywords}")
220
- pixabay_video = self.pixabay.find_video(
221
- search_keywords,
222
- search_duration,
223
- exclude_video_ids,
224
- orientation
225
- )
226
- if pixabay_video:
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  selected_video = pixabay_video
228
- logger.info(f"Found video on Pixabay for '{keyword}'")
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  # If no video found on either platform
231
  if not selected_video:
@@ -235,7 +262,7 @@ class ShortCreator:
235
  temp_files.append(video_path)
236
 
237
  # Download video
238
- logger.debug(f"Downloading video for '{keyword}' (Target: {audio_duration:.2f}s)")
239
  response = requests.get(selected_video["url"], stream=True, timeout=30)
240
  response.raise_for_status()
241
 
@@ -255,7 +282,8 @@ class ShortCreator:
255
  norm_path.rename(video_path)
256
 
257
  video_found = True
258
- exclude_video_ids.append(pexels_video["id"])
 
259
 
260
  except Exception as e:
261
  logger.warning(f"Video search/download failed for '{keyword}': {e}. Trying photo fallback.")
 
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:
 
262
  temp_files.append(video_path)
263
 
264
  # Download video
265
+ logger.debug(f"Downloading {video_source} video for '{keyword}' (Target: {audio_duration:.2f}s)")
266
  response = requests.get(selected_video["url"], stream=True, timeout=30)
267
  response.raise_for_status()
268
 
 
282
  norm_path.rename(video_path)
283
 
284
  video_found = True
285
+ exclude_video_ids.append(selected_video["id"]) # Fixed: use selected_video
286
+
287
 
288
  except Exception as e:
289
  logger.warning(f"Video search/download failed for '{keyword}': {e}. Trying photo fallback.")