Download app.py from slim-S/STT-Bot: direct link, hf CLI and curl.
- Browser
- Download file 29.1 kB
-
https://huggingface.co/spaces/slim-S/STT-Bot/resolve/main/app.py
- Command line
-
hf download hf://spaces/slim-S/STT-Bot/app.py
-
curl -L -o app.py https://huggingface.co/spaces/slim-S/STT-Bot/resolve/main/app.py
29.1 kB
| import os | |
| import logging | |
| import random | |
| import shutil | |
| import re | |
| import asyncio | |
| import signal | |
| import traceback | |
| import stat | |
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup | |
| from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes, ConversationHandler, CallbackQueryHandler | |
| from typing import Dict, Set, Tuple, Optional | |
| # Enable more detailed logging | |
| logging.basicConfig( | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
| level=logging.DEBUG # Changed to DEBUG for more details | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Add specific logger for telegram | |
| telegram_logger = logging.getLogger('telegram') | |
| telegram_logger.setLevel(logging.DEBUG) | |
| # Define conversation states | |
| AWAITING_CORRECTION = 1 | |
| AWAITING_VALIDATION = 2 | |
| # Store active tasks per user | |
| # {user_id: (voice_filename, text_filename, seq_num, corrected_text)} | |
| user_tasks = {} | |
| # Track chunks being processed | |
| chunks_in_process = set() # set of sequence numbers that are currently being processed | |
| # Directory paths | |
| DATA_DIR = os.environ.get('DATA_DIR', 'data') | |
| PENDING_DIR = os.path.join(DATA_DIR, 'pending') | |
| PENDING_VOICE_DIR = os.path.join(PENDING_DIR, 'voice') | |
| PENDING_TEXT_DIR = os.path.join(PENDING_DIR, 'text') | |
| PROCESSED_DIR = os.path.join(DATA_DIR, 'processed') | |
| PROCESSED_VOICE_DIR = os.path.join(PROCESSED_DIR, 'voice') | |
| PROCESSED_TEXT_DIR = os.path.join(PROCESSED_DIR, 'text') | |
| # Ensure directories exist with proper permissions | |
| def ensure_directory_with_permissions(directory): | |
| """Create directory if it doesn't exist and set permissions.""" | |
| try: | |
| if not os.path.exists(directory): | |
| os.makedirs(directory, exist_ok=True) | |
| # Set permissions: read/write/execute for everyone | |
| os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG | | |
| stat.S_IRWXO) # 0777 permissions | |
| logger.info(f"Directory ensured with permissions: {directory}") | |
| except Exception as e: | |
| logger.error(f"Error setting permissions for {directory}: {str(e)}") | |
| # Initialize directories with proper permissions | |
| ensure_directory_with_permissions(PENDING_VOICE_DIR) | |
| ensure_directory_with_permissions(PENDING_TEXT_DIR) | |
| ensure_directory_with_permissions(PROCESSED_VOICE_DIR) | |
| ensure_directory_with_permissions(PROCESSED_TEXT_DIR) | |
| # Global application variable | |
| application = None | |
| def extract_sequence_number(filename: str) -> Optional[int]: | |
| """Extract sequence number from filename, e.g., 'Sound 100.wav' -> 100.""" | |
| match = re.search(r'(\d+)', filename) | |
| if match: | |
| return int(match.group(1)) | |
| return None | |
| def get_files_by_sequence_number(): | |
| """Create dictionaries mapping sequence numbers to filenames.""" | |
| voice_files = {} | |
| text_files = {} | |
| # Map voice files to sequence numbers | |
| if os.path.exists(PENDING_VOICE_DIR): | |
| for filename in os.listdir(PENDING_VOICE_DIR): | |
| if filename.endswith('.wav'): | |
| seq_num = extract_sequence_number(filename) | |
| if seq_num is not None: | |
| voice_files[seq_num] = filename | |
| # Map text files to sequence numbers | |
| if os.path.exists(PENDING_TEXT_DIR): | |
| for filename in os.listdir(PENDING_TEXT_DIR): | |
| if filename.endswith('.txt'): | |
| seq_num = extract_sequence_number(filename) | |
| if seq_num is not None: | |
| text_files[seq_num] = filename | |
| return voice_files, text_files | |
| async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Send a welcome message when the /start command is issued.""" | |
| logger.info(f"Start command received from user {update.effective_user.id}") | |
| try: | |
| await update.message.reply_text( | |
| "السلام عليكم في The Algerian Darija Transcription Correction Bot!\n\n" | |
| ''' | |
| 🌟 قواعد التنميط اللغوي والبدائل 🌟 | |
| 🟦🟦🟦 قواعد الكتابة 🟦🟦🟦 | |
| 🔷 القاعدة 00 | |
| يُسمح فقط باستخدام الكلمات والحروف العربية في عملية التنصيص. يُمنع استعمال الأحرف الأجنبية أو ما يُعرف بـ "العربيزي". | |
| 📝 مثال: "على" ✅ وليس "3la" ❌ | |
| 🔷 القاعدة 01 | |
| يجوز إلصاق الجار والمجرور بالفعل مباشرة دون فصله. | |
| 📝 مثال: قالولي ✅ بدلاً من قالوا لي ❌ | |
| 🔷 القاعدة 02 | |
| لا يُشترط الالتزام بألف الجماعة في الأفعال. | |
| 📝 مثال: قالو ✅ بدلاً من قالوا ❌ | |
| 🔷 القاعدة 03 | |
| يتم استبدال همزة الألف (أ) بحرف (ا) دائماً. | |
| 📝 مثال: اكتوب ✅ بدلاً من أكتب ❌ | |
| 🔷 القاعدة 04 | |
| يُكتب الضمير المتصل (هو) بالشكل الصحيح دون تغيير. | |
| 📝 مثال: عنده ✅ بدلاً من عندو ❌ | |
| 🔷 القاعدة 05 | |
| عند النطق، يتم استبدال حرف (ت) بحرف (ث) إذا نُطق كذلك. | |
| 📝 مثال: ثاني ✅ بدلاً من تاني ❌ | |
| 🔷 القاعدة 06 | |
| تحويل الفاء الملتصقة بالاسم إلى حرف الجر "في". | |
| 📝 مثال: في البيت ✅ بدلاً من فالبيت ❌ | |
| 🔷 القاعدة 07 | |
| يُفضل وصل "ما" و"واو العطف" بالكلمة مباشرة. | |
| 📝 مثال: ماقولتش ✅ بدلاً من ماقولتش ❌ | |
| 🔷 القاعدة 09 | |
| تُحافظ حروف المستقبل مثل "عنقول" أو "كنقول" على شكلها الأصلي دون تغيير. | |
| 📝 مثال: عنقول ✅ (لا تغيير) | |
| 🟩🟩🟩 البدائل اللغوية 🟩🟩🟩 | |
| 🚫 اللفظ غير المعياري ← ✅ اللفظ المعياري | |
| درك ← ضرك ✅ | |
| علابالي ← على بالي ✅ | |
| شويا ← شوية ✅ | |
| واش ← وش ✅ | |
| ڨالو ← قالو ✅ | |
| نتاع / نتع / تع ← تاع ✅ | |
| بالصح / بصاح ← بصح ✅ | |
| قتلك ← قلتلك ✅ | |
| يقول لك ← يقولك ✅ | |
| عنا ← عندنا ✅ | |
| هاداك / هذاك / هاذاك ← هداك ✅ | |
| لي / ليه / ليك ← اللي / له / لك ✅ | |
| 😉 قم بعمل pin أو تثبيت لهذه الرسالة لكي يسهل عليك الرجوع إليها لاحقا لمراجعة القواعد‼️ | |
| ''' | |
| "استعمل /correct للبدأ في تصحيح النصوص\n" | |
| "استعمل /skip لتجاوز مقطع صوتي معين\n" | |
| "استعمل /cancel لإلغاء العملية\n" | |
| ) | |
| logger.info("Start message sent successfully") | |
| except Exception as e: | |
| logger.error(f"Error sending start message: {e}") | |
| async def get_next_available_chunk() -> Tuple[Optional[str], Optional[str], Optional[int]]: | |
| """Find the next available audio chunk that isn't being processed. | |
| Returns (voice_filename, text_filename, sequence_number) or (None, None, None)""" | |
| voice_files, text_files = get_files_by_sequence_number() | |
| # Find sequence numbers that exist in both voice and text files | |
| common_seq_nums = set(voice_files.keys()).intersection( | |
| set(text_files.keys())) | |
| # Filter out sequence numbers that are already being processed | |
| available_seq_nums = [ | |
| seq_num for seq_num in common_seq_nums if seq_num not in chunks_in_process] | |
| logger.debug(f"Available sequence numbers: {available_seq_nums}") | |
| if available_seq_nums: | |
| # Get random available sequence number from the list | |
| seq_num = random.choice(available_seq_nums) | |
| logger.debug(f"Selected sequence number: {seq_num}") | |
| return voice_files[seq_num], text_files[seq_num], seq_num | |
| logger.debug("No available chunks found") | |
| return None, None, None | |
| async def send_chunk_to_user(update: Update, user_id: int, voice_filename: str, text_filename: str, seq_num: int) -> bool: | |
| """Send audio chunk and text to user. Returns True if successful, False otherwise.""" | |
| # Send audio file | |
| audio_path = os.path.join(PENDING_VOICE_DIR, voice_filename) | |
| # Check if audio file exists | |
| if not os.path.exists(audio_path): | |
| logger.error(f"Audio file not found: {audio_path}") | |
| await update.message.reply_text("Error: Audio file not found. Please try again.") | |
| return False | |
| # Read the original transcription to send as reference | |
| text_path = os.path.join(PENDING_TEXT_DIR, text_filename) | |
| if not os.path.exists(text_path): | |
| logger.error(f"Text file not found: {text_path}") | |
| await update.message.reply_text("Error: Text file not found. Please try again.") | |
| return False | |
| try: | |
| with open(text_path, 'r', encoding='utf-8') as f: | |
| original_text = f.read().strip() | |
| except Exception as e: | |
| logger.error(f"Error reading text file: {e}") | |
| await update.message.reply_text(f"Error reading transcription file. Please try again.") | |
| return False | |
| # Send audio and original text | |
| try: | |
| with open(audio_path, 'rb') as audio_file: | |
| await update.message.reply_voice(voice=audio_file) | |
| await update.message.reply_text( | |
| f"من فضلك استمع إلى المقطع الصوتي وأعد كتابة النص.\n" | |
| f"النص الأصلي:" | |
| ) | |
| await update.message.reply_text( | |
| f"{original_text}" | |
| ) | |
| await update.message.reply_text( | |
| f"من فضلك قم بإرسال النص الذي تم تصحيحه أو استعمل /skip لتجاوز هذا المقطع." | |
| f"استعمل /cancel لإلغاء العملية." | |
| ) | |
| logger.info(f"Successfully sent audio and text to user {user_id}") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error sending audio or text: {e}") | |
| await update.message.reply_text("Error sending audio. Please try again.") | |
| return False | |
| async def correct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | |
| """Start the correction process.""" | |
| logger.info( | |
| f"تم استقبال أمر التصحيح من المستعمل {update.effective_user.id}") | |
| user_id = update.effective_user.id | |
| # Check if user already has an active task | |
| if user_id in user_tasks: | |
| await update.message.reply_text("أنت الآن في غضون عملية تصحيح. من فضلك قم بإكمالها أو ألغي العملية عبر /cancel.") | |
| current_state = AWAITING_VALIDATION if len( | |
| user_tasks[user_id]) > 3 else AWAITING_CORRECTION | |
| return current_state | |
| # Ensure directory paths exist with proper permissions | |
| ensure_directory_with_permissions(PENDING_VOICE_DIR) | |
| ensure_directory_with_permissions(PENDING_TEXT_DIR) | |
| # Log directory contents for debugging | |
| logger.debug( | |
| f"PENDING_VOICE_DIR contents: {os.listdir(PENDING_VOICE_DIR)}") | |
| logger.debug(f"PENDING_TEXT_DIR contents: {os.listdir(PENDING_TEXT_DIR)}") | |
| # Get next available chunk | |
| voice_filename, text_filename, seq_num = await get_next_available_chunk() | |
| if not voice_filename or not text_filename: | |
| await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.") | |
| return ConversationHandler.END | |
| # Mark chunk as being processed | |
| chunks_in_process.add(seq_num) | |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) | |
| # Send chunk to user | |
| success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num) | |
| if not success: | |
| # Clean up on failure | |
| chunks_in_process.remove(seq_num) | |
| del user_tasks[user_id] | |
| return ConversationHandler.END | |
| return AWAITING_CORRECTION | |
| async def skip_chunk(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | |
| """Skip the current chunk and get a new one.""" | |
| user_id = update.effective_user.id | |
| if user_id not in user_tasks: | |
| await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") | |
| return ConversationHandler.END | |
| # Get current task info | |
| current_task = user_tasks[user_id] | |
| if len(current_task) >= 3: # Has at least voice, text, seq_num | |
| _, _, current_seq_num = current_task[:3] | |
| # Remove current chunk from processing (making it available for others) | |
| chunks_in_process.discard(current_seq_num) | |
| logger.info(f"User {user_id} skipped chunk {current_seq_num}") | |
| # Remove current task | |
| del user_tasks[user_id] | |
| await update.message.reply_text("تجاوزت المقطع الصوتي. دعني أبحث عن مقطع آخر لك.") | |
| # Get next available chunk | |
| voice_filename, text_filename, seq_num = await get_next_available_chunk() | |
| if not voice_filename or not text_filename: | |
| await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.") | |
| return ConversationHandler.END | |
| # Mark new chunk as being processed | |
| chunks_in_process.add(seq_num) | |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) | |
| # Send new chunk to user | |
| success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num) | |
| if not success: | |
| # Clean up on failure | |
| chunks_in_process.remove(seq_num) | |
| del user_tasks[user_id] | |
| return ConversationHandler.END | |
| return AWAITING_CORRECTION | |
| async def receive_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | |
| """Receive the user's correction and ask for validation.""" | |
| user_id = update.effective_user.id | |
| if user_id not in user_tasks: | |
| await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") | |
| return ConversationHandler.END | |
| voice_filename, text_filename, seq_num = user_tasks[user_id] | |
| corrected_text = update.message.text | |
| # Update user task with corrected text | |
| user_tasks[user_id] = (voice_filename, text_filename, | |
| seq_num, corrected_text) | |
| # Create validation buttons | |
| keyboard = [ | |
| [ | |
| InlineKeyboardButton("✅ نعم, أنا متأكد من التصحيح", | |
| callback_data="validate_yes"), | |
| InlineKeyboardButton("❌ لا, أريد التصحيح مرة أخرى", | |
| callback_data="validate_no") | |
| ] | |
| ] | |
| reply_markup = InlineKeyboardMarkup(keyboard) | |
| await update.message.reply_text( | |
| f"النص الذي قمت بتصحيحه:\n{corrected_text}\n\n" | |
| f"هل أنت متأكد من التصحيح؟", | |
| reply_markup=reply_markup | |
| ) | |
| return AWAITING_VALIDATION | |
| async def handle_validation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | |
| """Handle the validation response.""" | |
| query = update.callback_query | |
| await query.answer() | |
| user_id = query.from_user.id | |
| if user_id not in user_tasks or len(user_tasks[user_id]) < 4: | |
| await query.edit_message_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") | |
| return ConversationHandler.END | |
| voice_filename, text_filename, seq_num, corrected_text = user_tasks[user_id] | |
| if query.data == "validate_yes": | |
| # User is confident - save the correction | |
| success = await save_final_correction(query, user_id, voice_filename, text_filename, seq_num, corrected_text) | |
| if success: | |
| await query.edit_message_text( | |
| "شكرا. تصحيحك تم حفظه\n" | |
| "استعمل /correct لاستقبال عملية تصحيح جديدة" | |
| ) | |
| return ConversationHandler.END | |
| else: | |
| # If saving failed, go back to correction state | |
| # Remove corrected text | |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) | |
| await query.edit_message_text( | |
| "حدث خطأ أثناء حفظ التصحيح. من فضلك أعد إرساله." | |
| ) | |
| return AWAITING_CORRECTION | |
| elif query.data == "validate_no": | |
| # User wants to correct again - remove corrected text and go back to correction state | |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) | |
| await query.edit_message_text( | |
| "من فضلك أعد كتابة النص التصحيحي مرة أخرى." | |
| ) | |
| return AWAITING_CORRECTION | |
| return AWAITING_VALIDATION | |
| async def save_final_correction(query, user_id: int, voice_filename: str, text_filename: str, seq_num: int, corrected_text: str) -> bool: | |
| """Save the final validated correction.""" | |
| # Get original file paths | |
| original_txt_path = os.path.join(PENDING_TEXT_DIR, text_filename) | |
| original_wav_path = os.path.join(PENDING_VOICE_DIR, voice_filename) | |
| # Create user-specific directory in processed TEXT folder only | |
| user_processed_text_dir = os.path.join( | |
| PROCESSED_TEXT_DIR, f"user_{user_id}") | |
| # Log the directory path for debugging | |
| logger.debug(f"Creating text directory: {user_processed_text_dir}") | |
| try: | |
| # Ensure text directory with proper permissions | |
| ensure_directory_with_permissions(user_processed_text_dir) | |
| # Save corrected text to processed directory | |
| processed_txt_path = os.path.join( | |
| user_processed_text_dir, text_filename) | |
| # Log file paths for debugging | |
| logger.debug( | |
| f"Original text path: {original_txt_path}, exists: {os.path.exists(original_txt_path)}") | |
| logger.debug( | |
| f"Original wav path: {original_wav_path}, exists: {os.path.exists(original_wav_path)}") | |
| logger.debug(f"Processed text path: {processed_txt_path}") | |
| # Save corrected text | |
| logger.debug(f"Saving corrected text to {processed_txt_path}") | |
| with open(processed_txt_path, 'w', encoding='utf-8') as f: | |
| f.write(corrected_text) | |
| # Set file permissions | |
| os.chmod(processed_txt_path, stat.S_IRUSR | stat.S_IWUSR | | |
| stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) | |
| # Verify processed text file exists | |
| logger.debug( | |
| f"Verifying processed text file exists: {os.path.exists(processed_txt_path)}") | |
| # Remove both files from pending directory (text and voice) | |
| if os.path.exists(original_txt_path): | |
| logger.debug(f"Removing original text file at {original_txt_path}") | |
| os.remove(original_txt_path) | |
| else: | |
| logger.warning( | |
| f"Could not remove original text file as it doesn't exist at {original_txt_path}") | |
| if os.path.exists(original_wav_path): | |
| logger.debug(f"Removing original wav file at {original_wav_path}") | |
| os.remove(original_wav_path) | |
| else: | |
| logger.warning( | |
| f"Could not remove original wav file as it doesn't exist at {original_wav_path}") | |
| # Remove from active tasks | |
| logger.debug(f"Removing seq_num {seq_num} from chunks_in_process") | |
| chunks_in_process.remove(seq_num) | |
| logger.debug(f"Removing user {user_id} from user_tasks") | |
| del user_tasks[user_id] | |
| logger.info( | |
| f"Successfully saved validated correction for user {user_id}") | |
| return True | |
| except Exception as e: | |
| # Enhanced error logging with stack trace | |
| error_msg = f"Error saving correction: {str(e)}" | |
| logger.error(error_msg) | |
| logger.error(traceback.format_exc()) | |
| # Try to provide more specific error messages | |
| if "Permission denied" in str(e): | |
| await query.edit_message_text("Error: Permission denied while saving files. Please contact the administrator.") | |
| elif "No such file or directory" in str(e): | |
| await query.edit_message_text("Error: File not found. The system couldn't find one of the files.") | |
| else: | |
| await query.edit_message_text(f"Error saving your correction: {str(e)}. Please try again.") | |
| return False | |
| async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | |
| """Cancel the current correction task.""" | |
| user_id = update.effective_user.id | |
| if user_id in user_tasks: | |
| task_info = user_tasks[user_id] | |
| if len(task_info) >= 3: # Has at least voice, text, seq_num | |
| seq_num = task_info[2] | |
| chunks_in_process.discard(seq_num) | |
| del user_tasks[user_id] | |
| await update.message.reply_text("العملية قد ألغيت :( استعمل /correct للبدء تصحيح جديد.") | |
| else: | |
| await update.message.reply_text("لا توجد عملية تصحيح نشطة لإلغائها.") | |
| return ConversationHandler.END | |
| async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Show status of pending and processed chunks.""" | |
| logger.info( | |
| f"Status command received from user {update.effective_user.id}") | |
| # Count pending files | |
| pending_voice_count = len( | |
| [f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')]) | |
| pending_text_count = len( | |
| [f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')]) | |
| # Get sequence numbers | |
| voice_files, text_files = get_files_by_sequence_number() | |
| matching_pairs = len( | |
| set(voice_files.keys()).intersection(set(text_files.keys()))) | |
| # Count processed files (only text files now) | |
| processed_text_count = 0 | |
| # Count text files | |
| for root, dirs, files in os.walk(PROCESSED_TEXT_DIR): | |
| processed_text_count += len([f for f in files if f.endswith('.txt')]) | |
| active_tasks = len(user_tasks) | |
| # Check file permissions | |
| permissions_info = "" | |
| try: | |
| # Check if we can write to the processed text directory | |
| can_write_text = os.access(PROCESSED_TEXT_DIR, os.W_OK) | |
| permissions_info = f"\n• Write permissions: Text: {can_write_text}" | |
| except Exception as e: | |
| permissions_info = f"\n• Error checking permissions: {str(e)}" | |
| await update.message.reply_text( | |
| f"📊 Transcription Status:\n" | |
| f"• Pending voice files: {pending_voice_count}\n" | |
| f"• Pending text files: {pending_text_count}\n" | |
| f"• Matching pending pairs: {matching_pairs}\n" | |
| f"• Processed text files: {processed_text_count}\n" | |
| f"• Active tasks: {active_tasks}{permissions_info}\n" | |
| f"• Note: Only corrected text files are saved, voice files are not copied to processed folder" | |
| ) | |
| async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Command to show debug information about directories and permissions.""" | |
| user_id = update.effective_user.id | |
| # Check directory structure | |
| debug_info = [ | |
| "📁 Directory Check:", | |
| f"• DATA_DIR: {DATA_DIR}, exists: {os.path.exists(DATA_DIR)}", | |
| f"• PENDING_DIR: {PENDING_DIR}, exists: {os.path.exists(PENDING_DIR)}", | |
| f"• PENDING_VOICE_DIR: {PENDING_VOICE_DIR}, exists: {os.path.exists(PENDING_VOICE_DIR)}", | |
| f"• PENDING_TEXT_DIR: {PENDING_TEXT_DIR}, exists: {os.path.exists(PENDING_TEXT_DIR)}", | |
| f"• PROCESSED_DIR: {PROCESSED_DIR}, exists: {os.path.exists(PROCESSED_DIR)}", | |
| f"• PROCESSED_TEXT_DIR: {PROCESSED_TEXT_DIR}, exists: {os.path.exists(PROCESSED_TEXT_DIR)}", | |
| ] | |
| # Check permissions | |
| try: | |
| permission_info = [ | |
| "🔑 Permission Check:", | |
| f"• PENDING_VOICE_DIR writable: {os.access(PENDING_VOICE_DIR, os.W_OK)}", | |
| f"• PENDING_TEXT_DIR writable: {os.access(PENDING_TEXT_DIR, os.W_OK)}", | |
| f"• PROCESSED_TEXT_DIR writable: {os.access(PROCESSED_TEXT_DIR, os.W_OK)}", | |
| ] | |
| debug_info.extend(permission_info) | |
| except Exception as e: | |
| debug_info.append(f"Error checking permissions: {str(e)}") | |
| # Check active tasks | |
| task_info = [ | |
| "📋 Active Tasks:", | |
| f"• Number of active users: {len(user_tasks)}", | |
| f"• Active chunks: {len(chunks_in_process)}", | |
| ] | |
| debug_info.extend(task_info) | |
| await update.message.reply_text("\n".join(debug_info)) | |
| async def test_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Test handler to verify bot is receiving messages.""" | |
| logger.info(f"Test command received from user {update.effective_user.id}") | |
| await update.message.reply_text("Bot is working! ✅") | |
| async def setup_application(): | |
| """Setup the Application with all handlers.""" | |
| global application | |
| # Get the token from environment variable | |
| token = os.environ.get("TELEGRAM_TOKEN") | |
| if not token: | |
| logger.error("TELEGRAM_TOKEN environment variable not set!") | |
| return None | |
| logger.info(f"Setting up bot with token: {token[:10]}...") | |
| # Create the Application with additional debugging | |
| application = Application.builder().token(token).build() | |
| # Add a test handler first | |
| application.add_handler(CommandHandler("test", test_handler)) | |
| # Add conversation handler for correction workflow | |
| conv_handler = ConversationHandler( | |
| entry_points=[CommandHandler("correct", correct)], | |
| states={ | |
| AWAITING_CORRECTION: [ | |
| MessageHandler(filters.TEXT & ~filters.COMMAND, | |
| receive_correction), | |
| CommandHandler("skip", skip_chunk), | |
| ], | |
| AWAITING_VALIDATION: [ | |
| CallbackQueryHandler(handle_validation, pattern="^validate_"), | |
| ], | |
| }, | |
| fallbacks=[ | |
| CommandHandler("cancel", cancel), | |
| CommandHandler("skip", skip_chunk), | |
| ], | |
| name="correction_conversation", | |
| ) | |
| application.add_handler(conv_handler) | |
| # Add command handlers | |
| application.add_handler(CommandHandler("start", start)) | |
| application.add_handler(CommandHandler("status", status)) | |
| application.add_handler(CommandHandler("cancel", cancel)) | |
| application.add_handler(CommandHandler( | |
| "skip", skip_chunk)) # Global skip handler | |
| application.add_handler(CommandHandler("debug", debug_info)) | |
| logger.info("All handlers added successfully") | |
| return application | |
| async def start_bot(): | |
| """Start the bot with proper signal handling.""" | |
| global application | |
| logger.info("Starting bot setup...") | |
| # Create and configure the bot | |
| app = await setup_application() | |
| if not app: | |
| logger.error("Failed to create application") | |
| return | |
| logger.info("Application created successfully") | |
| # Start the Bot | |
| try: | |
| await app.initialize() | |
| logger.info("Application initialized") | |
| await app.start() | |
| logger.info("Application started") | |
| # Start polling for updates | |
| await app.updater.start_polling(drop_pending_updates=False) | |
| logger.info("Started polling for updates") | |
| except Exception as e: | |
| logger.error(f"Error starting bot: {e}") | |
| return | |
| # Setup signal handlers for graceful shutdown | |
| loop = asyncio.get_event_loop() | |
| for signal_name in ('SIGINT', 'SIGTERM'): | |
| try: | |
| loop.add_signal_handler( | |
| getattr(signal, signal_name), | |
| lambda s=signal_name: asyncio.create_task(shutdown(s)) | |
| ) | |
| except NotImplementedError: | |
| # Windows doesn't support this | |
| logger.info("Signal handlers not supported on this platform") | |
| pass | |
| try: | |
| # Just run forever until interrupted | |
| logger.info("Bot is now running and waiting for messages...") | |
| await asyncio.Event().wait() | |
| finally: | |
| # Ensure the bot is properly shut down | |
| await shutdown("Manual") | |
| async def shutdown(signal_type): | |
| """Cleanup tasks tied to the service's shutdown.""" | |
| global application | |
| logger.info(f"Received exit signal {signal_type}...") | |
| # Stop the bot | |
| if application: | |
| logger.info("Stopping application...") | |
| await application.updater.stop() | |
| await application.stop() | |
| await application.shutdown() | |
| # Cancel all running tasks | |
| tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] | |
| for task in tasks: | |
| task.cancel() | |
| # Wait for all tasks to be cancelled | |
| if tasks: | |
| logger.info(f"Waiting for {len(tasks)} tasks to complete...") | |
| await asyncio.gather(*tasks, return_exceptions=True) | |
| logger.info("Application shutdown complete") | |
| async def main(): | |
| """Main function to setup and run the bot.""" | |
| # Setup and start the bot | |
| await start_bot() | |
| # This is the entry point when run as the main process | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |