understanding commited on
Commit
b0363b8
·
verified ·
1 Parent(s): e0b27eb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -128
app.py CHANGED
@@ -8,7 +8,11 @@ import asyncio
8
  from concurrent.futures import ThreadPoolExecutor
9
  from typing import List, Optional, Tuple
10
 
 
 
11
  from dotenv import load_dotenv
 
 
12
  from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter, ImageEnhance, UnidentifiedImageError
13
  import numpy as np
14
  from telegram import Update, InputFile
@@ -18,12 +22,12 @@ from telegram.error import TelegramError
18
 
19
  # --- Configuration ---
20
  class Config:
21
- # Load environment variables - essential for Hugging Face secrets
22
- load_dotenv()
23
  TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')
24
  if not TELEGRAM_TOKEN:
25
- # Use logger here if already configured, otherwise print/raise
26
- # logger.critical("TELEGRAM_TOKEN environment variable not set!")
 
27
  raise ValueError("TELEGRAM_TOKEN environment variable not set! Please set it in Hugging Face Space secrets.")
28
 
29
  # Directories (relative paths for container compatibility)
@@ -33,7 +37,6 @@ class Config:
33
  FONT_PATH = "arial.ttf"
34
 
35
  # Predefined Template Settings
36
- # Assuming templates are e.g., 1200x900 and have a space for the user image
37
  TEMPLATE_SIZE = (1200, 900) # Expected size of predefined template canvas
38
  PLACEHOLDER_SIZE = (700, 500) # Size to fit user image into within the template
39
  PLACEHOLDER_POSITION = (50, 50) # Top-left corner to paste user image in template
@@ -56,24 +59,29 @@ logging.basicConfig(
56
  level=logging.INFO,
57
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
58
  )
59
- # Reduce verbosity of httpx logger used by python-telegram-bot
60
  logging.getLogger("httpx").setLevel(logging.WARNING)
61
  logger = logging.getLogger(__name__)
62
 
63
  # --- Setup ---
64
  # Ensure necessary directories exist within the container's filesystem
 
65
  try:
 
66
  os.makedirs(Config.PREDEFINED_TEMPLATES_DIR, exist_ok=True)
67
  os.makedirs(Config.OUTPUT_DIR, exist_ok=True)
68
  logger.info(f"Ensured directories exist: {Config.PREDEFINED_TEMPLATES_DIR}, {Config.OUTPUT_DIR}")
69
  except OSError as e:
70
- logger.error(f"Error creating directories: {e}", exc_info=True)
71
- # Depending on the error, you might want to exit or handle differently
 
72
 
73
- # Check for font file availability
 
74
  if not os.path.exists(Config.FONT_PATH):
75
- logger.warning(f"Font file not found at '{Config.FONT_PATH}'. Text rendering might use default system font or fail if Pillow cannot find a fallback.")
76
- # Pillow might find system fonts installed by Dockerfile's `apt-get install fontconfig ttf-mscorefonts-installer`
 
77
 
78
  # --- Helper Functions ---
79
  def add_noise_to_image(img: Image.Image, intensity: float = 0.02) -> Image.Image:
@@ -108,13 +116,15 @@ def apply_template(user_image_path: str, caption: str, template_path: str) -> Op
108
  Returns:
109
  Path to the generated image (in OUTPUT_DIR), or None if an error occurred.
110
  """
111
- # Generate a unique-ish output filename
112
  base_name = os.path.basename(template_path).split('.')[0]
113
- output_filename = f"result_{base_name}_{random.randint(1000, 9999)}.jpg"
 
114
  output_path = os.path.join(Config.OUTPUT_DIR, output_filename)
 
115
 
116
  try:
117
- # Use 'with' statement for automatic resource cleanup
118
  with Image.open(template_path).convert("RGBA") as template, \
119
  Image.open(user_image_path).convert("RGBA") as user_image_orig:
120
 
@@ -123,6 +133,7 @@ def apply_template(user_image_path: str, caption: str, template_path: str) -> Op
123
  logger.warning(f"Template {os.path.basename(template_path)} size {template.size} differs from expected {Config.TEMPLATE_SIZE}. Results may vary.")
124
 
125
  # Resize user image to fit the placeholder area using Lanczos resampling for quality
 
126
  user_image_resized = ImageOps.fit(user_image_orig, Config.PLACEHOLDER_SIZE, Image.Resampling.LANCZOS)
127
 
128
  # Create a working copy of the template to paste onto
@@ -130,29 +141,35 @@ def apply_template(user_image_path: str, caption: str, template_path: str) -> Op
130
 
131
  # Paste the resized user image into the placeholder position
132
  # The third argument (mask) uses the alpha channel of the user image for smooth edges if it has transparency
 
133
  combined.paste(user_image_resized, Config.PLACEHOLDER_POSITION, user_image_resized if user_image_resized.mode == 'RGBA' else None)
134
 
135
  # --- Add Caption ---
 
136
  draw = ImageDraw.Draw(combined)
137
  try:
138
  # Use a medium font size relative to config
139
  font_size = Config.MAX_FONT_SIZE // 2
 
140
  font = ImageFont.truetype(Config.FONT_PATH, font_size)
141
  except IOError:
142
- logger.warning(f"Failed to load font: {Config.FONT_PATH}. Using Pillow's default.")
143
  font = ImageFont.load_default()
144
 
145
  # Wrap text according to configured width
146
  wrapped_text = textwrap.fill(caption, width=Config.MAX_CAPTION_WIDTH)
 
147
 
148
  # Calculate text position (e.g., centered below the placeholder area)
 
149
  text_bbox = draw.textbbox((0, 0), wrapped_text, font=font, align="center")
150
  text_width = text_bbox[2] - text_bbox[0]
151
  text_height = text_bbox[3] - text_bbox[1]
152
  # Center horizontally
153
  text_x = (combined.width - text_width) // 2
154
  # Position below placeholder, add some padding
155
- text_y = Config.PLACEHOLDER_POSITION[1] + Config.PLACEHOLDER_SIZE[1] + 20
 
156
 
157
  # Draw text with a simple shadow/stroke for better visibility
158
  # Draw shadow slightly offset
@@ -162,16 +179,19 @@ def apply_template(user_image_path: str, caption: str, template_path: str) -> Op
162
  draw.text((text_x, text_y), wrapped_text, font=font, fill="white", align="center")
163
 
164
  # Convert to RGB before saving as JPG (removes alpha channel)
 
165
  combined_rgb = combined.convert("RGB")
 
166
  combined_rgb.save(output_path, "JPEG", quality=Config.JPEG_QUALITY)
167
  logger.info(f"Generated image using template '{os.path.basename(template_path)}': {output_path}")
168
  return output_path
169
 
170
  except FileNotFoundError:
171
- logger.error(f"Template or user image not found. Template: {template_path}, User Image: {user_image_path}")
172
  except UnidentifiedImageError:
173
- logger.error(f"Could not identify image file (corrupted or unsupported format?). Template: {template_path}, User Image: {user_image_path}")
174
  except Exception as e:
 
175
  logger.error(f"Error applying template '{os.path.basename(template_path)}': {e}", exc_info=True)
176
 
177
  # Explicitly return None on any error during the process
@@ -190,13 +210,15 @@ def create_auto_template(user_image_path: str, caption: str, variant: int) -> Op
190
  Returns:
191
  Path to the generated image (in OUTPUT_DIR), or None if an error occurred.
192
  """
193
- output_filename = f"auto_template_{variant}_{random.randint(1000, 9999)}.jpg"
194
  output_path = os.path.join(Config.OUTPUT_DIR, output_filename)
 
195
 
196
  try:
197
  # --- Create Background ---
198
  # Generate a random somewhat dark color for the background
199
  bg_color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150))
 
200
  bg = Image.new('RGB', Config.AUTO_TEMPLATE_SIZE, color=bg_color)
201
 
202
  # --- Process User Image ---
@@ -204,47 +226,65 @@ def create_auto_template(user_image_path: str, caption: str, variant: int) -> Op
204
  # Work on a copy
205
  user_img = user_img_orig.copy()
206
  # Resize to fit the designated area within the auto-template
 
207
  user_img = ImageOps.fit(user_img, Config.AUTO_USER_IMAGE_SIZE, Image.Resampling.LANCZOS)
208
 
209
- # Apply random effects based on the variant index
210
- effect_choice = random.choice(['blur', 'noise', 'color', 'contrast', 'none']) # Add more variety
211
- logger.debug(f"Auto template variant {variant}: Applying effect '{effect_choice}'")
212
 
213
  if effect_choice == 'blur':
214
- user_img = user_img.filter(ImageFilter.GaussianBlur(random.uniform(0.5, 1.5)))
 
 
215
  elif effect_choice == 'noise':
 
216
  user_img = add_noise_to_image(user_img, Config.NOISE_INTENSITY)
217
  elif effect_choice == 'color': # Enhance or reduce color saturation
 
 
218
  enhancer = ImageEnhance.Color(user_img)
219
- user_img = enhancer.enhance(random.uniform(0.3, 1.7))
220
  elif effect_choice == 'contrast': # Enhance or reduce contrast
 
 
221
  enhancer = ImageEnhance.Contrast(user_img)
222
- user_img = enhancer.enhance(random.uniform(0.7, 1.3))
 
 
 
 
 
223
  # 'none' applies no extra filter
224
 
225
  # Add a decorative border with a random light color
226
  border_color = (random.randint(180, 255), random.randint(180, 255), random.randint(180, 255))
227
  border_width = random.randint(8, 20)
 
228
  user_img = ImageOps.expand(user_img, border=border_width, fill=border_color)
229
 
230
  # --- Paste User Image onto Background ---
231
  # Calculate position to center the (bordered) user image horizontally, and place it in the upper part vertically
232
  paste_x = (bg.width - user_img.width) // 2
233
  paste_y = (bg.height - user_img.height) // 3 # Position slightly above vertical center
 
234
  bg.paste(user_img, (paste_x, paste_y))
235
 
236
  # --- Add Styled Text ---
 
237
  draw = ImageDraw.Draw(bg)
238
  try:
239
  # Random font size within configured range
240
  font_size = random.randint(Config.MIN_FONT_SIZE, Config.MAX_FONT_SIZE)
 
241
  font = ImageFont.truetype(Config.FONT_PATH, font_size)
242
  except IOError:
243
- logger.warning(f"Failed to load font: {Config.FONT_PATH}. Using Pillow's default.")
244
  font = ImageFont.load_default() # Fallback font
245
 
246
  # Wrap text
247
  wrapped_text = textwrap.fill(caption, width=Config.MAX_CAPTION_WIDTH)
 
248
 
249
  # Calculate text position (centered horizontally, below the pasted image)
250
  text_bbox = draw.textbbox((0, 0), wrapped_text, font=font, align="center")
@@ -253,24 +293,28 @@ def create_auto_template(user_image_path: str, caption: str, variant: int) -> Op
253
  text_x = (bg.width - text_width) // 2
254
  # Position below the image + border, add padding
255
  text_y = paste_y + user_img.height + 30
 
 
256
 
257
  # Random bright text color and dark stroke color
258
  text_color = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255))
259
  stroke_color = (random.randint(0, 50), random.randint(0, 50), random.randint(0, 50))
 
260
 
261
  # Draw text with stroke
262
  draw.text((text_x, text_y), wrapped_text, font=font, fill=text_color,
263
  stroke_width=Config.TEXT_STROKE_WIDTH, stroke_fill=stroke_color, align="center")
264
 
265
  # Save the final image
 
266
  bg.save(output_path, "JPEG", quality=Config.JPEG_QUALITY)
267
  logger.info(f"Generated auto-template image (variant {variant}): {output_path}")
268
  return output_path
269
 
270
  except FileNotFoundError:
271
- logger.error(f"User image not found during auto-template creation: {user_image_path}")
272
  except UnidentifiedImageError:
273
- logger.error(f"Could not identify user image file during auto-template creation: {user_image_path}")
274
  except Exception as e:
275
  logger.error(f"Error creating auto-template variant {variant}: {e}", exc_info=True)
276
 
@@ -280,19 +324,32 @@ def create_auto_template(user_image_path: str, caption: str, variant: int) -> Op
280
  def load_predefined_templates() -> List[str]:
281
  """Loads paths of all valid template images from the predefined directory."""
282
  templates = []
283
- logger.debug(f"Searching for templates in: {os.path.abspath(Config.PREDEFINED_TEMPLATES_DIR)}")
 
284
  try:
285
- if not os.path.isdir(Config.PREDEFINED_TEMPLATES_DIR):
286
- logger.warning(f"Predefined templates directory not found: {Config.PREDEFINED_TEMPLATES_DIR}")
287
- return []
288
- for file in os.listdir(Config.PREDEFINED_TEMPLATES_DIR):
 
 
 
289
  # Check for common image extensions
290
  if file.lower().endswith(('.png', '.jpg', '.jpeg')):
291
- full_path = os.path.join(Config.PREDEFINED_TEMPLATES_DIR, file)
292
- templates.append(full_path)
293
- logger.info(f"Loaded {len(templates)} predefined templates.")
 
 
 
 
 
 
 
 
 
294
  except Exception as e:
295
- logger.error(f"Error loading predefined templates from {Config.PREDEFINED_TEMPLATES_DIR}: {e}", exc_info=True)
296
  return templates
297
 
298
  # This function orchestrates the image processing. It contains blocking Pillow calls,
@@ -309,6 +366,7 @@ def process_images(user_image_path: str, caption: str) -> List[str]:
309
  Returns:
310
  A list of paths to the generated images.
311
  """
 
312
  generated_image_paths: List[str] = []
313
  predefined_templates = load_predefined_templates()
314
 
@@ -319,8 +377,10 @@ def process_images(user_image_path: str, caption: str) -> List[str]:
319
  result_path = apply_template(user_image_path, caption, template_path)
320
  if result_path:
321
  generated_image_paths.append(result_path)
 
 
322
  else:
323
- logger.info("No predefined templates found or loaded.")
324
 
325
  # 2. Generate auto templates
326
  logger.info(f"Generating {Config.AUTO_TEMPLATES_COUNT} auto-templates...")
@@ -328,8 +388,11 @@ def process_images(user_image_path: str, caption: str) -> List[str]:
328
  result_path = create_auto_template(user_image_path, caption, i)
329
  if result_path:
330
  generated_image_paths.append(result_path)
 
 
331
 
332
- logger.info(f"Finished processing. Generated {len(generated_image_paths)} images in total.")
 
333
  return generated_image_paths
334
 
335
 
@@ -339,56 +402,65 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
339
  """Handles incoming messages with photos and captions."""
340
  # Basic check for essential message components
341
  if not update.message or not update.message.photo or not update.message.caption:
342
- # This case should ideally be filtered out by MessageHandler filters, but check just in case
343
- logger.warning("Handler invoked for message without photo or caption.")
344
- # Optionally reply, but filter should prevent this
345
- # await update.message.reply_text("⚠️ Please send an image with a caption text!")
346
  return
347
 
348
  user = update.message.from_user
 
349
  user_id = user.id if user else "UnknownUser"
 
350
  caption = update.message.caption
351
  message_id = update.message.message_id
352
  chat_id = update.message.chat_id
353
 
354
- logger.info(f"Received photo with caption from user {user_id} in chat {chat_id}.")
355
 
356
  # --- Download User Image ---
357
- # Create a unique temporary path for the downloaded image
358
  temp_user_image_path = os.path.join(Config.OUTPUT_DIR, f"user_{user_id}_{message_id}.jpg")
359
  file_downloaded = False
 
360
  try:
361
  photo = update.message.photo[-1] # Get the highest resolution photo available
362
- logger.info(f"Downloading photo (file_id: {photo.file_id}, size: {photo.width}x{photo.height})...")
363
  photo_file = await photo.get_file()
364
  await photo_file.download_to_drive(temp_user_image_path)
365
- logger.info(f"Photo downloaded successfully to {temp_user_image_path}")
 
366
  file_downloaded = True
367
  except TelegramError as e:
368
- logger.error(f"Telegram error downloading photo: {e}", exc_info=True)
369
- await update.message.reply_text("❌ Sorry, there was a Telegram error downloading the image. Please try again.")
370
- return
371
  except Exception as e:
372
- logger.error(f"Unexpected error downloading photo: {e}", exc_info=True)
373
  await update.message.reply_text("❌ Sorry, I couldn't download the image due to an unexpected error.")
374
- return
375
 
376
- if not file_downloaded: # Should not happen if exceptions are caught, but as safety
 
 
 
377
  return
378
 
379
  # --- Process Images in Executor ---
380
  # Notify user that processing has started
 
381
  try:
 
382
  processing_message = await update.message.reply_text("⏳ Processing your image with different styles...", quote=True)
383
- message_to_delete = processing_message.message_id
384
  except TelegramError as e:
385
- logger.warning(f"Could not send 'Processing...' message: {e}")
386
- message_to_delete = None # Cannot delete if sending failed
 
 
387
 
388
  loop = asyncio.get_running_loop()
389
  generated_images = []
390
- start_time = loop.time()
 
391
  try:
 
392
  # Run the blocking image processing function in the default thread pool executor
393
  generated_images = await loop.run_in_executor(
394
  None, # Use default ThreadPoolExecutor
@@ -396,83 +468,94 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
396
  temp_user_image_path, # Argument 1 for process_images
397
  caption # Argument 2 for process_images
398
  )
399
- processing_time = loop.time() - start_time
400
- logger.info(f"Image processing completed in {processing_time:.2f} seconds.")
401
 
402
  except Exception as e:
403
- logger.error(f"Error during image processing executor call: {e}", exc_info=True)
 
 
404
  # Try to edit the "Processing..." message to show error
405
  if message_to_delete:
406
  try:
407
  await context.bot.edit_message_text(
408
  chat_id=chat_id,
409
  message_id=message_to_delete,
410
- text="❌ An unexpected error occurred during processing."
411
  )
412
- message_to_delete = None # Don't try to delete it again later
413
  except TelegramError as edit_err:
414
- logger.warning(f"Could not edit processing message to show error: {edit_err}")
415
  # Fallback reply if editing failed
416
- await update.message.reply_text("❌ An unexpected error occurred during processing.")
417
- else:
418
- await update.message.reply_text("❌ An unexpected error occurred during processing.")
419
 
420
- finally:
421
- # Delete the "Processing..." message if it was sent and not edited to show error
422
- if message_to_delete:
423
- try:
424
- await context.bot.delete_message(
425
- chat_id=chat_id,
426
- message_id=message_to_delete
427
- )
428
- except TelegramError as del_err:
429
- logger.warning(f"Could not delete 'Processing...' message ({message_to_delete}): {del_err}")
 
430
 
431
 
432
  # --- Send Results ---
433
- if not generated_images:
434
- logger.warning("No images were generated successfully.")
435
- # Avoid sending redundant message if error was already reported
436
- if 'e' not in locals(): # Check if processing failed with exception 'e'
437
- await update.message.reply_text("😕 Sorry, I couldn't generate any styled images this time. Please check the templates or try again.")
438
- else:
439
- logger.info(f"Sending {len(generated_images)} generated images back to user {user_id}.")
440
- sent_count = 0
441
- for i, img_path in enumerate(generated_images):
442
- caption_text = f"Style variant {i+1}" if len(generated_images) > 1 else "Here's your styled image!"
443
- try:
444
- # Send the photo from the generated path
445
- with open(img_path, 'rb') as photo_data:
 
 
 
 
 
 
 
 
446
  await update.message.reply_photo(
447
- photo=InputFile(photo_data, filename=os.path.basename(img_path)),
448
- caption=caption_text
 
 
449
  )
450
- sent_count += 1
451
- logger.debug(f"Sent photo {os.path.basename(img_path)}")
452
-
453
- except FileNotFoundError:
454
- logger.error(f"Generated image file not found for sending: {img_path}")
455
- # Try to inform user about partial failure if multiple images were expected
456
- if len(generated_images) > 1:
457
- await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} (file missing).")
458
- except TelegramError as e:
459
- logger.error(f"Telegram error sending photo {os.path.basename(img_path)}: {e}", exc_info=True)
460
- if len(generated_images) > 1:
461
- await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} due to a Telegram error.")
462
- except Exception as e:
463
- logger.error(f"Unexpected error sending photo {os.path.basename(img_path)}: {e}", exc_info=True)
464
- if len(generated_images) > 1:
465
- await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} due to an unexpected error.")
466
- finally:
467
- # Clean up the generated image file regardless of sending success
468
- try:
469
- if os.path.exists(img_path):
470
- os.remove(img_path)
471
- logger.debug(f"Cleaned up generated image: {os.path.basename(img_path)}")
472
- except OSError as e:
473
- logger.error(f"Error deleting generated image file {img_path}: {e}")
474
-
475
- logger.info(f"Finished sending {sent_count}/{len(generated_images)} images.")
476
 
477
  # --- Final Cleanup ---
478
  # Clean up the originally downloaded user image
@@ -481,30 +564,30 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
481
  os.remove(temp_user_image_path)
482
  logger.info(f"Cleaned up temporary user image: {os.path.basename(temp_user_image_path)}")
483
  except OSError as e:
484
- logger.error(f"Error cleaning up user image {temp_user_image_path}: {e}", exc_info=True)
 
485
 
486
 
487
  # --- Main Execution ---
488
  if __name__ == "__main__":
489
- logger.info("Starting Telegram Bot...")
490
 
491
- if not Config.TELEGRAM_TOKEN:
492
- logger.critical("TELEGRAM_TOKEN is not set in environment variables or .env file. Bot cannot start.")
493
- exit(1) # Exit if token is missing
494
 
495
  try:
496
  # Build the application instance
497
  app = Application.builder().token(Config.TELEGRAM_TOKEN).build()
498
 
499
  # Add the handler for messages containing both a photo and a caption
500
- # Filters.PHOTO checks for `message.photo` being non-empty
501
- # Filters.CAPTION checks for `message.caption` being non-empty
502
  app.add_handler(MessageHandler(filters.PHOTO & filters.CAPTION, handle_message))
503
 
504
- logger.info("Bot application built. Starting polling...")
505
- # Start the bot polling for updates
506
- app.run_polling(allowed_updates=Update.ALL_TYPES) # Specify allowed updates if needed
507
 
508
  except Exception as e:
509
- logger.critical(f"Fatal error initializing or running the bot application: {e}", exc_info=True)
 
 
510
  exit(1)
 
8
  from concurrent.futures import ThreadPoolExecutor
9
  from typing import List, Optional, Tuple
10
 
11
+ # Attempt to load environment variables from .env file for local testing
12
+ # In Hugging Face, secrets are injected directly as environment variables
13
  from dotenv import load_dotenv
14
+ load_dotenv() # Load .env file if it exists
15
+
16
  from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageFilter, ImageEnhance, UnidentifiedImageError
17
  import numpy as np
18
  from telegram import Update, InputFile
 
22
 
23
  # --- Configuration ---
24
  class Config:
25
+ # Read Telegram token from environment variable (set in HF Secrets)
 
26
  TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN')
27
  if not TELEGRAM_TOKEN:
28
+ # Log critical error if token is missing
29
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
30
+ logging.critical("TELEGRAM_TOKEN environment variable not set! Please set it in Hugging Face Space secrets.")
31
  raise ValueError("TELEGRAM_TOKEN environment variable not set! Please set it in Hugging Face Space secrets.")
32
 
33
  # Directories (relative paths for container compatibility)
 
37
  FONT_PATH = "arial.ttf"
38
 
39
  # Predefined Template Settings
 
40
  TEMPLATE_SIZE = (1200, 900) # Expected size of predefined template canvas
41
  PLACEHOLDER_SIZE = (700, 500) # Size to fit user image into within the template
42
  PLACEHOLDER_POSITION = (50, 50) # Top-left corner to paste user image in template
 
59
  level=logging.INFO,
60
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
61
  )
62
+ # Reduce verbosity of underlying HTTP library logger used by python-telegram-bot
63
  logging.getLogger("httpx").setLevel(logging.WARNING)
64
  logger = logging.getLogger(__name__)
65
 
66
  # --- Setup ---
67
  # Ensure necessary directories exist within the container's filesystem
68
+ # This runs when the script starts inside the container
69
  try:
70
+ # These paths are relative to the WORKDIR /app set in the Dockerfile
71
  os.makedirs(Config.PREDEFINED_TEMPLATES_DIR, exist_ok=True)
72
  os.makedirs(Config.OUTPUT_DIR, exist_ok=True)
73
  logger.info(f"Ensured directories exist: {Config.PREDEFINED_TEMPLATES_DIR}, {Config.OUTPUT_DIR}")
74
  except OSError as e:
75
+ logger.error(f"FATAL: Error creating essential directories: {e}", exc_info=True)
76
+ # If directories cannot be created, the app likely cannot function
77
+ raise SystemExit(f"FATAL: Cannot create directories {Config.PREDEFINED_TEMPLATES_DIR} or {Config.OUTPUT_DIR}") from e
78
 
79
+ # Check for font file availability after setup
80
+ # FONT_PATH is relative to WORKDIR /app
81
  if not os.path.exists(Config.FONT_PATH):
82
+ logger.warning(f"Font file specified in Config ('{Config.FONT_PATH}') not found in the container's /app directory. Text rendering might use a system default font if available via fontconfig (installed via Dockerfile), or fail if Pillow finds no fallback.")
83
+ else:
84
+ logger.info(f"Font file '{Config.FONT_PATH}' found.")
85
 
86
  # --- Helper Functions ---
87
  def add_noise_to_image(img: Image.Image, intensity: float = 0.02) -> Image.Image:
 
116
  Returns:
117
  Path to the generated image (in OUTPUT_DIR), or None if an error occurred.
118
  """
119
+ # Generate a unique-ish output filename to avoid conflicts
120
  base_name = os.path.basename(template_path).split('.')[0]
121
+ # Include a random element to prevent overwriting if called rapidly
122
+ output_filename = f"result_{base_name}_{random.randint(10000, 99999)}.jpg"
123
  output_path = os.path.join(Config.OUTPUT_DIR, output_filename)
124
+ logger.debug(f"Applying template '{os.path.basename(template_path)}' to user image '{os.path.basename(user_image_path)}'. Output: {output_path}")
125
 
126
  try:
127
+ # Use 'with' statement for automatic resource cleanup (file closing)
128
  with Image.open(template_path).convert("RGBA") as template, \
129
  Image.open(user_image_path).convert("RGBA") as user_image_orig:
130
 
 
133
  logger.warning(f"Template {os.path.basename(template_path)} size {template.size} differs from expected {Config.TEMPLATE_SIZE}. Results may vary.")
134
 
135
  # Resize user image to fit the placeholder area using Lanczos resampling for quality
136
+ logger.debug(f"Resizing user image to placeholder size {Config.PLACEHOLDER_SIZE}")
137
  user_image_resized = ImageOps.fit(user_image_orig, Config.PLACEHOLDER_SIZE, Image.Resampling.LANCZOS)
138
 
139
  # Create a working copy of the template to paste onto
 
141
 
142
  # Paste the resized user image into the placeholder position
143
  # The third argument (mask) uses the alpha channel of the user image for smooth edges if it has transparency
144
+ logger.debug(f"Pasting resized user image at {Config.PLACEHOLDER_POSITION}")
145
  combined.paste(user_image_resized, Config.PLACEHOLDER_POSITION, user_image_resized if user_image_resized.mode == 'RGBA' else None)
146
 
147
  # --- Add Caption ---
148
+ logger.debug("Adding caption to template image")
149
  draw = ImageDraw.Draw(combined)
150
  try:
151
  # Use a medium font size relative to config
152
  font_size = Config.MAX_FONT_SIZE // 2
153
+ logger.debug(f"Loading font '{Config.FONT_PATH}' with size {font_size}")
154
  font = ImageFont.truetype(Config.FONT_PATH, font_size)
155
  except IOError:
156
+ logger.warning(f"Failed to load font '{Config.FONT_PATH}'. Using Pillow's default.")
157
  font = ImageFont.load_default()
158
 
159
  # Wrap text according to configured width
160
  wrapped_text = textwrap.fill(caption, width=Config.MAX_CAPTION_WIDTH)
161
+ logger.debug(f"Wrapped caption text: \"{wrapped_text[:50]}...\"")
162
 
163
  # Calculate text position (e.g., centered below the placeholder area)
164
+ # Use textbbox for more accurate positioning
165
  text_bbox = draw.textbbox((0, 0), wrapped_text, font=font, align="center")
166
  text_width = text_bbox[2] - text_bbox[0]
167
  text_height = text_bbox[3] - text_bbox[1]
168
  # Center horizontally
169
  text_x = (combined.width - text_width) // 2
170
  # Position below placeholder, add some padding
171
+ text_y = Config.PLACEHOLDER_POSITION[1] + Config.PLACEHOLDER_SIZE[1] + 20 # Adjust padding as needed
172
+ logger.debug(f"Calculated text position: ({text_x}, {text_y})")
173
 
174
  # Draw text with a simple shadow/stroke for better visibility
175
  # Draw shadow slightly offset
 
179
  draw.text((text_x, text_y), wrapped_text, font=font, fill="white", align="center")
180
 
181
  # Convert to RGB before saving as JPG (removes alpha channel)
182
+ logger.debug("Converting final image to RGB")
183
  combined_rgb = combined.convert("RGB")
184
+ logger.debug(f"Saving final image to {output_path} with quality {Config.JPEG_QUALITY}")
185
  combined_rgb.save(output_path, "JPEG", quality=Config.JPEG_QUALITY)
186
  logger.info(f"Generated image using template '{os.path.basename(template_path)}': {output_path}")
187
  return output_path
188
 
189
  except FileNotFoundError:
190
+ logger.error(f"Template or user image not found. Template: '{template_path}', User Image: '{user_image_path}'")
191
  except UnidentifiedImageError:
192
+ logger.error(f"Could not identify image file (corrupted or unsupported format?). Template: '{template_path}', User Image: '{user_image_path}'")
193
  except Exception as e:
194
+ # Log detailed error including traceback
195
  logger.error(f"Error applying template '{os.path.basename(template_path)}': {e}", exc_info=True)
196
 
197
  # Explicitly return None on any error during the process
 
210
  Returns:
211
  Path to the generated image (in OUTPUT_DIR), or None if an error occurred.
212
  """
213
+ output_filename = f"auto_template_{variant}_{random.randint(10000, 99999)}.jpg"
214
  output_path = os.path.join(Config.OUTPUT_DIR, output_filename)
215
+ logger.debug(f"Creating auto template variant {variant}. Output: {output_path}")
216
 
217
  try:
218
  # --- Create Background ---
219
  # Generate a random somewhat dark color for the background
220
  bg_color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150))
221
+ logger.debug(f"Auto template {variant}: Background color {bg_color}")
222
  bg = Image.new('RGB', Config.AUTO_TEMPLATE_SIZE, color=bg_color)
223
 
224
  # --- Process User Image ---
 
226
  # Work on a copy
227
  user_img = user_img_orig.copy()
228
  # Resize to fit the designated area within the auto-template
229
+ logger.debug(f"Auto template {variant}: Resizing user image to {Config.AUTO_USER_IMAGE_SIZE}")
230
  user_img = ImageOps.fit(user_img, Config.AUTO_USER_IMAGE_SIZE, Image.Resampling.LANCZOS)
231
 
232
+ # Apply random effects based on a random choice (not just variant index)
233
+ effect_choice = random.choice(['blur', 'noise', 'color', 'contrast', 'sharpness', 'none'])
234
+ logger.debug(f"Auto template {variant}: Applying effect '{effect_choice}'")
235
 
236
  if effect_choice == 'blur':
237
+ blur_radius = random.uniform(0.5, 1.8)
238
+ logger.debug(f"Applying GaussianBlur with radius {blur_radius:.2f}")
239
+ user_img = user_img.filter(ImageFilter.GaussianBlur(blur_radius))
240
  elif effect_choice == 'noise':
241
+ logger.debug(f"Applying noise with intensity {Config.NOISE_INTENSITY:.2f}")
242
  user_img = add_noise_to_image(user_img, Config.NOISE_INTENSITY)
243
  elif effect_choice == 'color': # Enhance or reduce color saturation
244
+ color_factor = random.uniform(0.3, 1.7)
245
+ logger.debug(f"Enhancing color with factor {color_factor:.2f}")
246
  enhancer = ImageEnhance.Color(user_img)
247
+ user_img = enhancer.enhance(color_factor)
248
  elif effect_choice == 'contrast': # Enhance or reduce contrast
249
+ contrast_factor = random.uniform(0.7, 1.4)
250
+ logger.debug(f"Enhancing contrast with factor {contrast_factor:.2f}")
251
  enhancer = ImageEnhance.Contrast(user_img)
252
+ user_img = enhancer.enhance(contrast_factor)
253
+ elif effect_choice == 'sharpness': # Enhance sharpness
254
+ sharpness_factor = random.uniform(1.1, 2.0)
255
+ logger.debug(f"Enhancing sharpness with factor {sharpness_factor:.2f}")
256
+ enhancer = ImageEnhance.Sharpness(user_img)
257
+ user_img = enhancer.enhance(sharpness_factor)
258
  # 'none' applies no extra filter
259
 
260
  # Add a decorative border with a random light color
261
  border_color = (random.randint(180, 255), random.randint(180, 255), random.randint(180, 255))
262
  border_width = random.randint(8, 20)
263
+ logger.debug(f"Adding border width {border_width} color {border_color}")
264
  user_img = ImageOps.expand(user_img, border=border_width, fill=border_color)
265
 
266
  # --- Paste User Image onto Background ---
267
  # Calculate position to center the (bordered) user image horizontally, and place it in the upper part vertically
268
  paste_x = (bg.width - user_img.width) // 2
269
  paste_y = (bg.height - user_img.height) // 3 # Position slightly above vertical center
270
+ logger.debug(f"Pasting processed user image at ({paste_x}, {paste_y})")
271
  bg.paste(user_img, (paste_x, paste_y))
272
 
273
  # --- Add Styled Text ---
274
+ logger.debug("Adding caption to auto template")
275
  draw = ImageDraw.Draw(bg)
276
  try:
277
  # Random font size within configured range
278
  font_size = random.randint(Config.MIN_FONT_SIZE, Config.MAX_FONT_SIZE)
279
+ logger.debug(f"Loading font '{Config.FONT_PATH}' with size {font_size}")
280
  font = ImageFont.truetype(Config.FONT_PATH, font_size)
281
  except IOError:
282
+ logger.warning(f"Failed to load font '{Config.FONT_PATH}'. Using Pillow's default.")
283
  font = ImageFont.load_default() # Fallback font
284
 
285
  # Wrap text
286
  wrapped_text = textwrap.fill(caption, width=Config.MAX_CAPTION_WIDTH)
287
+ logger.debug(f"Wrapped caption text: \"{wrapped_text[:50]}...\"")
288
 
289
  # Calculate text position (centered horizontally, below the pasted image)
290
  text_bbox = draw.textbbox((0, 0), wrapped_text, font=font, align="center")
 
293
  text_x = (bg.width - text_width) // 2
294
  # Position below the image + border, add padding
295
  text_y = paste_y + user_img.height + 30
296
+ logger.debug(f"Calculated text position: ({text_x}, {text_y})")
297
+
298
 
299
  # Random bright text color and dark stroke color
300
  text_color = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255))
301
  stroke_color = (random.randint(0, 50), random.randint(0, 50), random.randint(0, 50))
302
+ logger.debug(f"Text color {text_color}, stroke color {stroke_color}")
303
 
304
  # Draw text with stroke
305
  draw.text((text_x, text_y), wrapped_text, font=font, fill=text_color,
306
  stroke_width=Config.TEXT_STROKE_WIDTH, stroke_fill=stroke_color, align="center")
307
 
308
  # Save the final image
309
+ logger.debug(f"Saving final auto template image to {output_path} with quality {Config.JPEG_QUALITY}")
310
  bg.save(output_path, "JPEG", quality=Config.JPEG_QUALITY)
311
  logger.info(f"Generated auto-template image (variant {variant}): {output_path}")
312
  return output_path
313
 
314
  except FileNotFoundError:
315
+ logger.error(f"User image not found during auto-template creation: '{user_image_path}'")
316
  except UnidentifiedImageError:
317
+ logger.error(f"Could not identify user image file during auto-template creation: '{user_image_path}'")
318
  except Exception as e:
319
  logger.error(f"Error creating auto-template variant {variant}: {e}", exc_info=True)
320
 
 
324
  def load_predefined_templates() -> List[str]:
325
  """Loads paths of all valid template images from the predefined directory."""
326
  templates = []
327
+ template_dir = Config.PREDEFINED_TEMPLATES_DIR
328
+ logger.debug(f"Searching for templates in directory: {os.path.abspath(template_dir)}")
329
  try:
330
+ if not os.path.isdir(template_dir):
331
+ logger.warning(f"Predefined templates directory not found: '{template_dir}'")
332
+ return [] # Return empty list if directory doesn't exist
333
+
334
+ files = os.listdir(template_dir)
335
+ logger.debug(f"Found {len(files)} files/dirs in template directory.")
336
+ for file in files:
337
  # Check for common image extensions
338
  if file.lower().endswith(('.png', '.jpg', '.jpeg')):
339
+ full_path = os.path.join(template_dir, file)
340
+ if os.path.isfile(full_path): # Ensure it's actually a file
341
+ templates.append(full_path)
342
+ logger.debug(f"Found template: {full_path}")
343
+ else:
344
+ logger.warning(f"Found item with image extension but is not a file: {full_path}")
345
+
346
+ if not templates:
347
+ logger.warning(f"No valid template image files found in '{template_dir}'.")
348
+ else:
349
+ logger.info(f"Loaded {len(templates)} predefined templates.")
350
+
351
  except Exception as e:
352
+ logger.error(f"Error loading predefined templates from '{template_dir}': {e}", exc_info=True)
353
  return templates
354
 
355
  # This function orchestrates the image processing. It contains blocking Pillow calls,
 
366
  Returns:
367
  A list of paths to the generated images.
368
  """
369
+ logger.info("Starting image processing task...")
370
  generated_image_paths: List[str] = []
371
  predefined_templates = load_predefined_templates()
372
 
 
377
  result_path = apply_template(user_image_path, caption, template_path)
378
  if result_path:
379
  generated_image_paths.append(result_path)
380
+ else:
381
+ logger.warning(f"Failed to generate image for template: {os.path.basename(template_path)}")
382
  else:
383
+ logger.info("Skipping predefined templates (none found or loaded).")
384
 
385
  # 2. Generate auto templates
386
  logger.info(f"Generating {Config.AUTO_TEMPLATES_COUNT} auto-templates...")
 
388
  result_path = create_auto_template(user_image_path, caption, i)
389
  if result_path:
390
  generated_image_paths.append(result_path)
391
+ else:
392
+ logger.warning(f"Failed to generate auto-template variant {i}")
393
 
394
+
395
+ logger.info(f"Image processing task finished. Generated {len(generated_image_paths)} images in total.")
396
  return generated_image_paths
397
 
398
 
 
402
  """Handles incoming messages with photos and captions."""
403
  # Basic check for essential message components
404
  if not update.message or not update.message.photo or not update.message.caption:
405
+ logger.warning("Handler invoked for message missing photo or caption. This shouldn't happen with the current filters.")
 
 
 
406
  return
407
 
408
  user = update.message.from_user
409
+ # Use 'UnknownUser' or similar if user info isn't available
410
  user_id = user.id if user else "UnknownUser"
411
+ user_info = f"user_id={user_id}" + (f", username={user.username}" if user and user.username else "")
412
  caption = update.message.caption
413
  message_id = update.message.message_id
414
  chat_id = update.message.chat_id
415
 
416
+ logger.info(f"Received photo with caption from {user_info} in chat {chat_id} (message_id={message_id}).")
417
 
418
  # --- Download User Image ---
419
+ # Create a unique temporary path for the downloaded image within the OUTPUT_DIR
420
  temp_user_image_path = os.path.join(Config.OUTPUT_DIR, f"user_{user_id}_{message_id}.jpg")
421
  file_downloaded = False
422
+ download_start_time = asyncio.get_running_loop().time()
423
  try:
424
  photo = update.message.photo[-1] # Get the highest resolution photo available
425
+ logger.info(f"Attempting download photo (file_id: {photo.file_id}, size: {photo.width}x{photo.height})...")
426
  photo_file = await photo.get_file()
427
  await photo_file.download_to_drive(temp_user_image_path)
428
+ download_time = asyncio.get_running_loop().time() - download_start_time
429
+ logger.info(f"Photo downloaded successfully to '{temp_user_image_path}' in {download_time:.2f} seconds.")
430
  file_downloaded = True
431
  except TelegramError as e:
432
+ logger.error(f"Telegram error downloading photo for message {message_id}: {e}", exc_info=True)
433
+ await update.message.reply_text("❌ Sorry, there was a Telegram error downloading the image. Please try sending it again.")
434
+ return # Stop processing if download fails
435
  except Exception as e:
436
+ logger.error(f"Unexpected error downloading photo for message {message_id}: {e}", exc_info=True)
437
  await update.message.reply_text("❌ Sorry, I couldn't download the image due to an unexpected error.")
438
+ return # Stop processing if download fails
439
 
440
+ # Safety check, though exceptions should prevent reaching here if download failed
441
+ if not file_downloaded or not os.path.exists(temp_user_image_path):
442
+ logger.error(f"Download reported success but file '{temp_user_image_path}' does not exist.")
443
+ await update.message.reply_text("❌ An internal error occurred after downloading the image.")
444
  return
445
 
446
  # --- Process Images in Executor ---
447
  # Notify user that processing has started
448
+ processing_message = None
449
  try:
450
+ # Quote the original message for context
451
  processing_message = await update.message.reply_text("⏳ Processing your image with different styles...", quote=True)
 
452
  except TelegramError as e:
453
+ logger.warning(f"Could not send 'Processing...' message to chat {chat_id}: {e}")
454
+ # Continue processing even if status message fails
455
+
456
+ message_to_delete = processing_message.message_id if processing_message else None
457
 
458
  loop = asyncio.get_running_loop()
459
  generated_images = []
460
+ processing_failed = False
461
+ processing_start_time = loop.time()
462
  try:
463
+ logger.info(f"Submitting image processing task to executor for user image '{os.path.basename(temp_user_image_path)}'")
464
  # Run the blocking image processing function in the default thread pool executor
465
  generated_images = await loop.run_in_executor(
466
  None, # Use default ThreadPoolExecutor
 
468
  temp_user_image_path, # Argument 1 for process_images
469
  caption # Argument 2 for process_images
470
  )
471
+ processing_time = loop.time() - processing_start_time
472
+ logger.info(f"Image processing task completed in {processing_time:.2f} seconds.")
473
 
474
  except Exception as e:
475
+ processing_failed = True
476
+ logger.error(f"Error during image processing executor call for message {message_id}: {e}", exc_info=True)
477
+ error_message = "❌ An unexpected error occurred during processing. Please try again later."
478
  # Try to edit the "Processing..." message to show error
479
  if message_to_delete:
480
  try:
481
  await context.bot.edit_message_text(
482
  chat_id=chat_id,
483
  message_id=message_to_delete,
484
+ text=error_message
485
  )
486
+ message_to_delete = None # Mark as handled, don't delete later
487
  except TelegramError as edit_err:
488
+ logger.warning(f"Could not edit processing message {message_to_delete} to show error: {edit_err}")
489
  # Fallback reply if editing failed
490
+ await update.message.reply_text(error_message)
491
+ else: # If sending initial status failed, just send error as new message
492
+ await update.message.reply_text(error_message)
493
 
494
+ # Delete the "Processing..." message if it was sent and processing didn't fail or error wasn't edited in
495
+ if message_to_delete:
496
+ try:
497
+ await context.bot.delete_message(
498
+ chat_id=chat_id,
499
+ message_id=message_to_delete
500
+ )
501
+ logger.debug(f"Deleted 'Processing...' message {message_to_delete}")
502
+ except TelegramError as del_err:
503
+ # Log warning, but don't bother user if deleting status message fails
504
+ logger.warning(f"Could not delete 'Processing...' message ({message_to_delete}): {del_err}")
505
 
506
 
507
  # --- Send Results ---
508
+ # Only proceed if processing didn't explicitly fail with an exception
509
+ if not processing_failed:
510
+ if not generated_images:
511
+ logger.warning(f"Image processing finished but generated 0 images for message {message_id}.")
512
+ # Inform user if no images were created (e.g., no templates found, or all failed internally)
513
+ await update.message.reply_text("😕 Sorry, I couldn't generate any styled images this time. There might be an issue with the templates.")
514
+ else:
515
+ send_start_time = loop.time()
516
+ logger.info(f"Sending {len(generated_images)} generated images back to user {user_id} for message {message_id}.")
517
+ sent_count = 0
518
+ for i, img_path in enumerate(generated_images):
519
+ # Check if file exists before attempting to send
520
+ if not os.path.exists(img_path):
521
+ logger.error(f"Generated image file not found before sending: '{img_path}'")
522
+ if len(generated_images) > 1:
523
+ await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} (internal file missing).")
524
+ continue # Skip to next image
525
+
526
+ caption_text = f"Style variant {i+1}" if len(generated_images) > 1 else "🖼️ Here's your styled image!"
527
+ try:
528
+ # Send the photo from the generated path using InputFile
529
  await update.message.reply_photo(
530
+ photo=InputFile(img_path), # PTB handles opening/closing when path is given
531
+ caption=caption_text,
532
+ # Consider adding reply_to_message_id=message_id for clearer context in group chats
533
+ # reply_to_message_id=message_id
534
  )
535
+ sent_count += 1
536
+ logger.debug(f"Sent photo {os.path.basename(img_path)}")
537
+
538
+ except TelegramError as e:
539
+ # Log specific Telegram errors (e.g., file too large, chat not found, blocked by user)
540
+ logger.error(f"Telegram error sending photo {os.path.basename(img_path)}: {e}", exc_info=True)
541
+ if len(generated_images) > 1:
542
+ await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} due to a Telegram error.")
543
+ except Exception as e:
544
+ # Catch other potential errors during sending
545
+ logger.error(f"Unexpected error sending photo {os.path.basename(img_path)}: {e}", exc_info=True)
546
+ if len(generated_images) > 1:
547
+ await update.message.reply_text(f"⚠️ Couldn't send style variant {i+1} due to an unexpected error.")
548
+ finally:
549
+ # Clean up the generated image file after attempting to send it
550
+ try:
551
+ if os.path.exists(img_path):
552
+ os.remove(img_path)
553
+ logger.debug(f"Cleaned up generated image: {os.path.basename(img_path)}")
554
+ except OSError as e:
555
+ logger.error(f"Error deleting generated image file '{img_path}': {e}")
556
+
557
+ send_time = loop.time() - send_start_time
558
+ logger.info(f"Finished sending results for message {message_id}. Sent {sent_count}/{len(generated_images)} images in {send_time:.2f} seconds.")
 
 
559
 
560
  # --- Final Cleanup ---
561
  # Clean up the originally downloaded user image
 
564
  os.remove(temp_user_image_path)
565
  logger.info(f"Cleaned up temporary user image: {os.path.basename(temp_user_image_path)}")
566
  except OSError as e:
567
+ # Log error but don't bother user if temporary file cleanup fails
568
+ logger.error(f"Error cleaning up user image '{temp_user_image_path}': {e}", exc_info=True)
569
 
570
 
571
  # --- Main Execution ---
572
  if __name__ == "__main__":
573
+ logger.info("Initializing Telegram Bot Application...")
574
 
575
+ # Token check is done in Config class now, exiting if it fails there.
 
 
576
 
577
  try:
578
  # Build the application instance
579
  app = Application.builder().token(Config.TELEGRAM_TOKEN).build()
580
 
581
  # Add the handler for messages containing both a photo and a caption
582
+ # This ensures the 'handle_message' function only receives relevant updates
 
583
  app.add_handler(MessageHandler(filters.PHOTO & filters.CAPTION, handle_message))
584
 
585
+ logger.info("Bot application built successfully. Starting polling for updates...")
586
+ # Start the bot polling for updates indefinitely
587
+ app.run_polling(allowed_updates=Update.ALL_TYPES) # Consider specifying only needed updates
588
 
589
  except Exception as e:
590
+ # Catch potential errors during application build or startup
591
+ logger.critical(f"FATAL error initializing or running the bot application: {e}", exc_info=True)
592
+ # Exit with a non-zero code to indicate failure
593
  exit(1)