Spaces:
Running
Running
Chandima Prabhath
commited on
Commit
Β·
3e7203b
1
Parent(s):
a30ed82
re edit images
Browse files
app.py
CHANGED
@@ -5,6 +5,7 @@ Description: A comprehensive WhatsApp bot with a professional, class-based struc
|
|
5 |
Features include image generation, image editing, voice replies,
|
6 |
and various utility functions, all handled by an asynchronous task queue.
|
7 |
Includes request logging and chat ID filtering.
|
|
|
8 |
"""
|
9 |
|
10 |
import os
|
@@ -299,9 +300,13 @@ class WhatsAppBot:
|
|
299 |
self.conv_manager = ConversationManager(config.MAX_HISTORY_SIZE)
|
300 |
self.intent_router = IntentRouter(self.conv_manager, self.logger)
|
301 |
self.task_queue = queue.Queue()
|
302 |
-
|
|
|
303 |
self.image_cache = defaultdict(lambda: deque(maxlen=50))
|
304 |
-
|
|
|
|
|
|
|
305 |
self._setup_routes()
|
306 |
self._start_workers()
|
307 |
|
@@ -314,10 +319,8 @@ class WhatsAppBot:
|
|
314 |
|
315 |
try:
|
316 |
payload = await request.json()
|
317 |
-
# Log the entire incoming request payload for debugging
|
318 |
self.logger.debug(f"Incoming webhook payload: {json.dumps(payload, indent=2)}")
|
319 |
|
320 |
-
# Process valid incoming messages in the background
|
321 |
if payload.get("typeWebhook") == "incomingMessageReceived":
|
322 |
executor.submit(self._process_incoming_message, payload)
|
323 |
else:
|
@@ -342,7 +345,6 @@ class WhatsAppBot:
|
|
342 |
while True:
|
343 |
task = self.task_queue.get()
|
344 |
try:
|
345 |
-
# Set context for the worker thread
|
346 |
ThreadContext.set_context(task["chat_id"], task["message_id"])
|
347 |
handler = getattr(self, f"_task_{task['type']}", None)
|
348 |
if handler:
|
@@ -361,7 +363,6 @@ class WhatsAppBot:
|
|
361 |
chat_id = payload["senderData"]["chatId"]
|
362 |
message_id = payload["idMessage"]
|
363 |
|
364 |
-
# ** CHAT RESTRICTION LOGIC **
|
365 |
if chat_id not in self.config.ALLOWED_CHATS:
|
366 |
self.logger.warning(f"Ignoring message from unauthorized chat ID: {chat_id}")
|
367 |
return
|
@@ -371,35 +372,26 @@ class WhatsAppBot:
|
|
371 |
message_data = payload.get("messageData", {})
|
372 |
type_message = message_data.get("typeMessage")
|
373 |
|
374 |
-
|
375 |
-
text = ""
|
376 |
-
download_url = None
|
377 |
-
is_image_file = False
|
378 |
|
379 |
-
# Case 1 & 4: Image or Document with potential caption
|
380 |
if type_message in ["imageMessage", "documentMessage"]:
|
381 |
file_data = message_data.get("fileMessageData", {})
|
382 |
mime_type = file_data.get("mimeType", "")
|
383 |
-
|
384 |
-
if type_message == "imageMessage" or (type_message == "documentMessage" and mime_type.startswith("image/")):
|
385 |
is_image_file = True
|
386 |
download_url = file_data.get("downloadUrl")
|
387 |
text = file_data.get("caption", "").strip()
|
388 |
if download_url:
|
389 |
-
self.logger.debug(f"Caching
|
390 |
self.image_cache[chat_id].append((message_id, download_url))
|
391 |
|
392 |
-
# Case 2 & 3: Reply to something (quoted message)
|
393 |
elif type_message == "quotedMessage":
|
394 |
text = message_data.get("extendedTextMessageData", {}).get("text", "").strip()
|
395 |
-
|
396 |
-
# Fallback for other text types
|
397 |
elif type_message == "textMessage":
|
398 |
text = message_data.get("textMessageData", {}).get("textMessage", "").strip()
|
399 |
elif type_message == "extendedTextMessage":
|
400 |
text = message_data.get("extendedTextMessageData", {}).get("text", "").strip()
|
401 |
|
402 |
-
# --- Action Dispatching ---
|
403 |
if not text:
|
404 |
self.logger.debug(f"Received message type '{type_message}' without actionable text from {chat_id}.")
|
405 |
return
|
@@ -407,24 +399,18 @@ class WhatsAppBot:
|
|
407 |
self.logger.info(f"Processing text from {chat_id}: '{text}'")
|
408 |
self.conv_manager.add_user_message(chat_id, text)
|
409 |
|
410 |
-
# If it's a file with a caption, check if the caption is an edit command
|
411 |
if is_image_file and text:
|
412 |
intent = self.intent_router.get_intent(text, chat_id)
|
413 |
if isinstance(intent, EditImageIntent):
|
414 |
self.logger.info("Detected direct edit command in image caption.")
|
415 |
self.task_queue.put({
|
416 |
-
"type": "edit_image",
|
417 |
-
"
|
418 |
-
"message_id": message_id,
|
419 |
-
"prompt": intent.prompt,
|
420 |
-
"download_url": download_url
|
421 |
})
|
422 |
-
return
|
423 |
|
424 |
-
# Handle slash commands
|
425 |
if text.startswith('/'):
|
426 |
self._handle_command(chat_id, message_id, text, payload)
|
427 |
-
# Handle natural language replies or regular chat
|
428 |
else:
|
429 |
self._handle_natural_language(chat_id, message_id, text, payload)
|
430 |
|
@@ -452,7 +438,7 @@ class WhatsAppBot:
|
|
452 |
)
|
453 |
self.api_client.send_message(chat_id, help_text, message_id)
|
454 |
elif command == "/gen":
|
455 |
-
self.task_queue.put({"type": "generate_image", "chat_id": chat_id, "message_id": message_id, "prompt": args})
|
456 |
elif command == "/edit":
|
457 |
self._dispatch_edit_image(chat_id, message_id, args, payload)
|
458 |
elif command == "/joke":
|
@@ -467,128 +453,149 @@ class WhatsAppBot:
|
|
467 |
def _handle_natural_language(self, chat_id, message_id, text, payload):
|
468 |
"""Processes natural language using the intent router."""
|
469 |
intent = self.intent_router.get_intent(text, chat_id)
|
470 |
-
|
471 |
-
task_data = {
|
472 |
-
"chat_id": chat_id,
|
473 |
-
"message_id": message_id,
|
474 |
-
**intent.model_dump()
|
475 |
-
}
|
476 |
|
477 |
if intent.action == "edit_image":
|
478 |
-
# This action needs the original payload to find the replied-to image
|
479 |
self._dispatch_edit_image(chat_id, message_id, intent.prompt, payload)
|
480 |
elif hasattr(self, f"_task_{intent.action}"):
|
481 |
-
# Enqueue the task with its type and all necessary data
|
482 |
self.task_queue.put({"type": intent.action, **task_data})
|
483 |
else:
|
484 |
self.logger.warning(f"No handler found for intent action: {intent.action}")
|
485 |
self.api_client.send_message(chat_id, "Sorry, I'm not sure how to handle that.", message_id)
|
486 |
|
487 |
def _dispatch_edit_image(self, chat_id, message_id, prompt, payload):
|
488 |
-
"""
|
489 |
message_data = payload.get("messageData", {})
|
490 |
-
|
491 |
-
# This function is only for replies, so the top-level type must be quotedMessage
|
492 |
if message_data.get("typeMessage") != "quotedMessage":
|
493 |
self.api_client.send_message(chat_id, "To edit an image, please reply to an image with your instructions.", message_id)
|
494 |
return
|
495 |
|
496 |
-
quoted_info = message_data.get("quotedMessage")
|
497 |
-
if not quoted_info:
|
498 |
-
self.api_client.send_message(chat_id, "Couldn't find the message you replied to.", message_id)
|
499 |
-
return
|
500 |
-
|
501 |
-
# Check if the replied-to message is an image OR a document
|
502 |
-
quoted_type = quoted_info.get("typeMessage")
|
503 |
-
if quoted_type not in ["imageMessage", "documentMessage"]:
|
504 |
-
self.api_client.send_message(chat_id, "You must reply to an image to edit it.", message_id)
|
505 |
-
return
|
506 |
-
|
507 |
-
# Get the ID of the message being replied to
|
508 |
quoted_message_id = quoted_info.get("stanzaId")
|
509 |
if not quoted_message_id:
|
510 |
self.logger.error(f"Could not find stanzaId in quoted message: {quoted_info}")
|
511 |
self.api_client.send_message(chat_id, "Sorry, I couldn't identify which image to edit.", message_id)
|
512 |
return
|
513 |
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
for msg_id,
|
519 |
if msg_id == quoted_message_id:
|
520 |
-
|
|
|
|
|
|
|
|
|
521 |
break
|
522 |
|
523 |
-
|
524 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
525 |
self.api_client.send_message(chat_id, "I couldn't find the original image. It might be too old. Please send it again before editing.", message_id)
|
526 |
return
|
527 |
|
528 |
-
self.logger.info(f"
|
529 |
self.task_queue.put({
|
530 |
-
"type": "edit_image",
|
531 |
-
"
|
532 |
-
"message_id": message_id,
|
533 |
-
"prompt": prompt,
|
534 |
-
"download_url": download_url
|
535 |
})
|
536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
537 |
# --- Task Handler Methods ---
|
538 |
|
539 |
def _task_send_text(self, task: Dict[str, Any]):
|
540 |
chat_id, message_id, message = task["chat_id"], task["message_id"], task["message"]
|
541 |
self.api_client.send_message(chat_id, message, message_id)
|
542 |
self.conv_manager.add_bot_message(chat_id, message)
|
543 |
-
# Asynchronously generate a voice reply for the sent text
|
544 |
self.task_queue.put({"type": "voice_reply", "chat_id": chat_id, "message_id": message_id, "text": message})
|
545 |
|
546 |
def _task_generate_image(self, task: Dict[str, Any]):
|
547 |
-
chat_id, mid, prompt
|
|
|
548 |
self.api_client.send_message(chat_id, f"π¨ Generating {count} image(s) for: \"{prompt}\"...", mid)
|
549 |
|
550 |
for i in range(count):
|
|
|
551 |
try:
|
552 |
-
_, path, _,
|
553 |
caption = f"β¨ Image {i+1}/{count}: {prompt}"
|
554 |
-
self.api_client.send_file(chat_id, path, caption, mid)
|
555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
556 |
except Exception as e:
|
557 |
self.logger.error(f"Image generation {i+1} failed: {e}", exc_info=True)
|
558 |
self.api_client.send_message(chat_id, f"π’ Failed to generate image {i+1}.", mid)
|
|
|
559 |
|
560 |
def _task_edit_image(self, task: Dict[str, Any]):
|
561 |
-
chat_id, mid, prompt
|
|
|
562 |
self.api_client.send_message(chat_id, f"π¨ Editing image with prompt: \"{prompt}\"...", mid)
|
563 |
|
564 |
-
|
565 |
try:
|
566 |
-
image_data = self.api_client.download_file(url)
|
567 |
-
if not image_data:
|
568 |
-
raise ValueError("Failed to download image.")
|
569 |
-
|
570 |
os.makedirs(self.config.TEMP_DIR, exist_ok=True)
|
571 |
-
input_path = os.path.join(self.config.TEMP_DIR, f"input_{mid}.jpg")
|
572 |
output_path = os.path.join(self.config.TEMP_DIR, f"output_{mid}.jpg")
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
578 |
|
579 |
if os.path.exists(output_path):
|
580 |
caption = f"β¨ Edited: {prompt}"
|
581 |
-
self.api_client.send_file(chat_id, output_path, caption, mid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
582 |
else:
|
583 |
raise ValueError("Edited image file not found.")
|
584 |
-
|
585 |
except Exception as e:
|
586 |
self.logger.error(f"Image editing task failed: {e}", exc_info=True)
|
587 |
self.api_client.send_message(chat_id, "π’ Sorry, I failed to edit the image.", mid)
|
|
|
588 |
finally:
|
589 |
-
|
590 |
-
if path and os.path.exists(path):
|
591 |
-
os.remove(path)
|
592 |
|
593 |
def _task_voice_reply(self, task: Dict[str, Any]):
|
594 |
text = task["text"]
|
@@ -635,7 +642,6 @@ class WhatsAppBot:
|
|
635 |
os.makedirs(d, exist_ok=True)
|
636 |
self.logger.debug(f"Ensured directory exists: {d}")
|
637 |
|
638 |
-
# Send a startup message to one of the allowed chats to confirm it's online
|
639 |
self.api_client.send_message(
|
640 | |
641 |
"π Eve is online and ready to help! Type /help to see commands."
|
@@ -651,7 +657,6 @@ if __name__ == "__main__":
|
|
651 |
bot = WhatsAppBot(bot_config)
|
652 |
bot.run()
|
653 |
except ValueError as e:
|
654 |
-
# Catch config validation errors
|
655 |
logging.basicConfig()
|
656 |
logging.getLogger().critical(f"β CONFIGURATION ERROR: {e}")
|
657 |
except KeyboardInterrupt:
|
|
|
5 |
Features include image generation, image editing, voice replies,
|
6 |
and various utility functions, all handled by an asynchronous task queue.
|
7 |
Includes request logging and chat ID filtering.
|
8 |
+
Version 2.1.0: Added caching for bot-sent images to allow for editing via replies.
|
9 |
"""
|
10 |
|
11 |
import os
|
|
|
300 |
self.conv_manager = ConversationManager(config.MAX_HISTORY_SIZE)
|
301 |
self.intent_router = IntentRouter(self.conv_manager, self.logger)
|
302 |
self.task_queue = queue.Queue()
|
303 |
+
|
304 |
+
# Cache for user-sent image message IDs and their download URLs
|
305 |
self.image_cache = defaultdict(lambda: deque(maxlen=50))
|
306 |
+
# NEW: Cache for bot-sent image message IDs and their local file paths
|
307 |
+
self.sent_image_path_cache = defaultdict(lambda: deque(maxlen=50))
|
308 |
+
|
309 |
+
self.fastapi_app = FastAPI(title="WhatsApp Eve Bot", version="2.1.0")
|
310 |
self._setup_routes()
|
311 |
self._start_workers()
|
312 |
|
|
|
319 |
|
320 |
try:
|
321 |
payload = await request.json()
|
|
|
322 |
self.logger.debug(f"Incoming webhook payload: {json.dumps(payload, indent=2)}")
|
323 |
|
|
|
324 |
if payload.get("typeWebhook") == "incomingMessageReceived":
|
325 |
executor.submit(self._process_incoming_message, payload)
|
326 |
else:
|
|
|
345 |
while True:
|
346 |
task = self.task_queue.get()
|
347 |
try:
|
|
|
348 |
ThreadContext.set_context(task["chat_id"], task["message_id"])
|
349 |
handler = getattr(self, f"_task_{task['type']}", None)
|
350 |
if handler:
|
|
|
363 |
chat_id = payload["senderData"]["chatId"]
|
364 |
message_id = payload["idMessage"]
|
365 |
|
|
|
366 |
if chat_id not in self.config.ALLOWED_CHATS:
|
367 |
self.logger.warning(f"Ignoring message from unauthorized chat ID: {chat_id}")
|
368 |
return
|
|
|
372 |
message_data = payload.get("messageData", {})
|
373 |
type_message = message_data.get("typeMessage")
|
374 |
|
375 |
+
text, download_url, is_image_file = "", None, False
|
|
|
|
|
|
|
376 |
|
|
|
377 |
if type_message in ["imageMessage", "documentMessage"]:
|
378 |
file_data = message_data.get("fileMessageData", {})
|
379 |
mime_type = file_data.get("mimeType", "")
|
380 |
+
if type_message == "imageMessage" or mime_type.startswith("image/"):
|
|
|
381 |
is_image_file = True
|
382 |
download_url = file_data.get("downloadUrl")
|
383 |
text = file_data.get("caption", "").strip()
|
384 |
if download_url:
|
385 |
+
self.logger.debug(f"Caching incoming image message {message_id} from {chat_id}")
|
386 |
self.image_cache[chat_id].append((message_id, download_url))
|
387 |
|
|
|
388 |
elif type_message == "quotedMessage":
|
389 |
text = message_data.get("extendedTextMessageData", {}).get("text", "").strip()
|
|
|
|
|
390 |
elif type_message == "textMessage":
|
391 |
text = message_data.get("textMessageData", {}).get("textMessage", "").strip()
|
392 |
elif type_message == "extendedTextMessage":
|
393 |
text = message_data.get("extendedTextMessageData", {}).get("text", "").strip()
|
394 |
|
|
|
395 |
if not text:
|
396 |
self.logger.debug(f"Received message type '{type_message}' without actionable text from {chat_id}.")
|
397 |
return
|
|
|
399 |
self.logger.info(f"Processing text from {chat_id}: '{text}'")
|
400 |
self.conv_manager.add_user_message(chat_id, text)
|
401 |
|
|
|
402 |
if is_image_file and text:
|
403 |
intent = self.intent_router.get_intent(text, chat_id)
|
404 |
if isinstance(intent, EditImageIntent):
|
405 |
self.logger.info("Detected direct edit command in image caption.")
|
406 |
self.task_queue.put({
|
407 |
+
"type": "edit_image", "chat_id": chat_id, "message_id": message_id,
|
408 |
+
"prompt": intent.prompt, "input_source": download_url, "source_type": 'url',
|
|
|
|
|
|
|
409 |
})
|
410 |
+
return
|
411 |
|
|
|
412 |
if text.startswith('/'):
|
413 |
self._handle_command(chat_id, message_id, text, payload)
|
|
|
414 |
else:
|
415 |
self._handle_natural_language(chat_id, message_id, text, payload)
|
416 |
|
|
|
438 |
)
|
439 |
self.api_client.send_message(chat_id, help_text, message_id)
|
440 |
elif command == "/gen":
|
441 |
+
self.task_queue.put({"type": "generate_image", "chat_id": chat_id, "message_id": message_id, "prompt": args, "count": self.config.DEFAULT_IMAGE_COUNT})
|
442 |
elif command == "/edit":
|
443 |
self._dispatch_edit_image(chat_id, message_id, args, payload)
|
444 |
elif command == "/joke":
|
|
|
453 |
def _handle_natural_language(self, chat_id, message_id, text, payload):
|
454 |
"""Processes natural language using the intent router."""
|
455 |
intent = self.intent_router.get_intent(text, chat_id)
|
456 |
+
task_data = {"chat_id": chat_id, "message_id": message_id, **intent.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
457 |
|
458 |
if intent.action == "edit_image":
|
|
|
459 |
self._dispatch_edit_image(chat_id, message_id, intent.prompt, payload)
|
460 |
elif hasattr(self, f"_task_{intent.action}"):
|
|
|
461 |
self.task_queue.put({"type": intent.action, **task_data})
|
462 |
else:
|
463 |
self.logger.warning(f"No handler found for intent action: {intent.action}")
|
464 |
self.api_client.send_message(chat_id, "Sorry, I'm not sure how to handle that.", message_id)
|
465 |
|
466 |
def _dispatch_edit_image(self, chat_id, message_id, prompt, payload):
|
467 |
+
"""Finds a replied-to image from cache and dispatches the edit task."""
|
468 |
message_data = payload.get("messageData", {})
|
|
|
|
|
469 |
if message_data.get("typeMessage") != "quotedMessage":
|
470 |
self.api_client.send_message(chat_id, "To edit an image, please reply to an image with your instructions.", message_id)
|
471 |
return
|
472 |
|
473 |
+
quoted_info = message_data.get("quotedMessage", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
quoted_message_id = quoted_info.get("stanzaId")
|
475 |
if not quoted_message_id:
|
476 |
self.logger.error(f"Could not find stanzaId in quoted message: {quoted_info}")
|
477 |
self.api_client.send_message(chat_id, "Sorry, I couldn't identify which image to edit.", message_id)
|
478 |
return
|
479 |
|
480 |
+
input_source, source_type = None, None
|
481 |
+
|
482 |
+
# 1. Check cache for bot-sent images (local paths)
|
483 |
+
if chat_id in self.sent_image_path_cache:
|
484 |
+
for msg_id, path in reversed(self.sent_image_path_cache[chat_id]):
|
485 |
if msg_id == quoted_message_id:
|
486 |
+
if os.path.exists(path):
|
487 |
+
input_source, source_type = path, 'path'
|
488 |
+
self.logger.info(f"Found replied-to image in local path cache: {path}")
|
489 |
+
else:
|
490 |
+
self.logger.warning(f"Found path for message {msg_id} but file missing: {path}")
|
491 |
break
|
492 |
|
493 |
+
# 2. If not found, check cache for user-sent images (URLs)
|
494 |
+
if not input_source and chat_id in self.image_cache:
|
495 |
+
for msg_id, url in reversed(self.image_cache[chat_id]):
|
496 |
+
if msg_id == quoted_message_id:
|
497 |
+
input_source, source_type = url, 'url'
|
498 |
+
self.logger.info(f"Found replied-to image in URL cache: {url}")
|
499 |
+
break
|
500 |
+
|
501 |
+
if not input_source:
|
502 |
+
self.logger.warning(f"Could not find any image source for message ID {quoted_message_id} in cache for chat {chat_id}.")
|
503 |
self.api_client.send_message(chat_id, "I couldn't find the original image. It might be too old. Please send it again before editing.", message_id)
|
504 |
return
|
505 |
|
506 |
+
self.logger.info(f"Dispatching edit task for message {quoted_message_id} with source type '{source_type}'.")
|
507 |
self.task_queue.put({
|
508 |
+
"type": "edit_image", "chat_id": chat_id, "message_id": message_id,
|
509 |
+
"prompt": prompt, "input_source": input_source, "source_type": source_type,
|
|
|
|
|
|
|
510 |
})
|
511 |
|
512 |
+
def _manage_cached_file(self, chat_id: str, message_id: str, file_path: str):
|
513 |
+
"""Adds a file to the cache and cleans up the oldest file if the cache is full."""
|
514 |
+
# Check if the cache is full before adding a new item
|
515 |
+
if len(self.sent_image_path_cache[chat_id]) == self.sent_image_path_cache[chat_id].maxlen:
|
516 |
+
old_id, old_path = self.sent_image_path_cache[chat_id][0] # Oldest item is at the left
|
517 |
+
if os.path.exists(old_path):
|
518 |
+
self.logger.debug(f"Cache full. Removing oldest cached image file: {old_path}")
|
519 |
+
try:
|
520 |
+
os.remove(old_path)
|
521 |
+
except OSError as e:
|
522 |
+
self.logger.error(f"Error removing old cached file {old_path}: {e}")
|
523 |
+
|
524 |
+
self.sent_image_path_cache[chat_id].append((message_id, file_path))
|
525 |
+
|
526 |
# --- Task Handler Methods ---
|
527 |
|
528 |
def _task_send_text(self, task: Dict[str, Any]):
|
529 |
chat_id, message_id, message = task["chat_id"], task["message_id"], task["message"]
|
530 |
self.api_client.send_message(chat_id, message, message_id)
|
531 |
self.conv_manager.add_bot_message(chat_id, message)
|
|
|
532 |
self.task_queue.put({"type": "voice_reply", "chat_id": chat_id, "message_id": message_id, "text": message})
|
533 |
|
534 |
def _task_generate_image(self, task: Dict[str, Any]):
|
535 |
+
chat_id, mid, prompt = task["chat_id"], task["message_id"], task["prompt"]
|
536 |
+
count = task.get("count", self.config.DEFAULT_IMAGE_COUNT)
|
537 |
self.api_client.send_message(chat_id, f"π¨ Generating {count} image(s) for: \"{prompt}\"...", mid)
|
538 |
|
539 |
for i in range(count):
|
540 |
+
path = None
|
541 |
try:
|
542 |
+
_, path, _, _ = generate_image(prompt, mid, str(i), self.config.IMAGE_DIR, width=task.get("width"), height=task.get("height"))
|
543 |
caption = f"β¨ Image {i+1}/{count}: {prompt}"
|
544 |
+
response = self.api_client.send_file(chat_id, path, caption, mid)
|
545 |
+
|
546 |
+
if response and response.get("idMessage"):
|
547 |
+
sent_message_id = response["idMessage"]
|
548 |
+
self.logger.info(f"Caching sent image path for message {sent_message_id}: {path}")
|
549 |
+
self._manage_cached_file(chat_id, sent_message_id, path)
|
550 |
+
else:
|
551 |
+
self.logger.warning(f"Could not get sent message ID for {path}. Deleting file.")
|
552 |
+
os.remove(path)
|
553 |
except Exception as e:
|
554 |
self.logger.error(f"Image generation {i+1} failed: {e}", exc_info=True)
|
555 |
self.api_client.send_message(chat_id, f"π’ Failed to generate image {i+1}.", mid)
|
556 |
+
if path and os.path.exists(path): os.remove(path)
|
557 |
|
558 |
def _task_edit_image(self, task: Dict[str, Any]):
|
559 |
+
chat_id, mid, prompt = task["chat_id"], task["message_id"], task["prompt"]
|
560 |
+
input_source, source_type = task["input_source"], task["source_type"]
|
561 |
self.api_client.send_message(chat_id, f"π¨ Editing image with prompt: \"{prompt}\"...", mid)
|
562 |
|
563 |
+
input_path_temp, output_path = None, None
|
564 |
try:
|
|
|
|
|
|
|
|
|
565 |
os.makedirs(self.config.TEMP_DIR, exist_ok=True)
|
|
|
566 |
output_path = os.path.join(self.config.TEMP_DIR, f"output_{mid}.jpg")
|
567 |
+
|
568 |
+
if source_type == 'url':
|
569 |
+
image_data = self.api_client.download_file(input_source)
|
570 |
+
if not image_data: raise ValueError(f"Failed to download image from URL: {input_source}")
|
571 |
+
input_path_temp = os.path.join(self.config.TEMP_DIR, f"input_{mid}.jpg")
|
572 |
+
with open(input_path_temp, 'wb') as f: f.write(image_data)
|
573 |
+
edit_input_path = input_path_temp
|
574 |
+
elif source_type == 'path':
|
575 |
+
edit_input_path = input_source
|
576 |
+
else:
|
577 |
+
raise ValueError(f"Unknown source_type: {source_type}")
|
578 |
+
|
579 |
+
flux_kontext_lib.generate_image(prompt, edit_input_path, download_path=output_path)
|
580 |
|
581 |
if os.path.exists(output_path):
|
582 |
caption = f"β¨ Edited: {prompt}"
|
583 |
+
response = self.api_client.send_file(chat_id, output_path, caption, mid)
|
584 |
+
if response and response.get("idMessage"):
|
585 |
+
sent_message_id = response["idMessage"]
|
586 |
+
self.logger.info(f"Caching sent image path for message {sent_message_id}: {output_path}")
|
587 |
+
self._manage_cached_file(chat_id, sent_message_id, output_path)
|
588 |
+
else:
|
589 |
+
self.logger.warning(f"Could not get sent message ID for {output_path}. Deleting file.")
|
590 |
+
os.remove(output_path)
|
591 |
else:
|
592 |
raise ValueError("Edited image file not found.")
|
|
|
593 |
except Exception as e:
|
594 |
self.logger.error(f"Image editing task failed: {e}", exc_info=True)
|
595 |
self.api_client.send_message(chat_id, "π’ Sorry, I failed to edit the image.", mid)
|
596 |
+
if output_path and os.path.exists(output_path): os.remove(output_path)
|
597 |
finally:
|
598 |
+
if input_path_temp and os.path.exists(input_path_temp): os.remove(input_path_temp)
|
|
|
|
|
599 |
|
600 |
def _task_voice_reply(self, task: Dict[str, Any]):
|
601 |
text = task["text"]
|
|
|
642 |
os.makedirs(d, exist_ok=True)
|
643 |
self.logger.debug(f"Ensured directory exists: {d}")
|
644 |
|
|
|
645 |
self.api_client.send_message(
|
646 | |
647 |
"π Eve is online and ready to help! Type /help to see commands."
|
|
|
657 |
bot = WhatsAppBot(bot_config)
|
658 |
bot.run()
|
659 |
except ValueError as e:
|
|
|
660 |
logging.basicConfig()
|
661 |
logging.getLogger().critical(f"β CONFIGURATION ERROR: {e}")
|
662 |
except KeyboardInterrupt:
|