slim-S commited on
Commit
db03872
·
1 Parent(s): 0dec5db

fix: error in the starting bot

Browse files
Files changed (1) hide show
  1. app.py +757 -0
app.py ADDED
@@ -0,0 +1,757 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import random
4
+ import shutil
5
+ import re
6
+ import asyncio
7
+ import signal
8
+ import traceback
9
+ import stat
10
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
11
+ from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes, ConversationHandler, CallbackQueryHandler
12
+ from typing import Dict, Set, Tuple, Optional
13
+
14
+ # Enable more detailed logging
15
+ logging.basicConfig(
16
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
17
+ level=logging.DEBUG # Changed to DEBUG for more details
18
+ )
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Add specific logger for telegram
22
+ telegram_logger = logging.getLogger('telegram')
23
+ telegram_logger.setLevel(logging.DEBUG)
24
+
25
+ # Define conversation states
26
+ AWAITING_CORRECTION = 1
27
+ AWAITING_VALIDATION = 2
28
+
29
+ # Store active tasks per user
30
+ # {user_id: (voice_filename, text_filename, seq_num, corrected_text)}
31
+ user_tasks = {}
32
+
33
+ # Track chunks being processed
34
+ chunks_in_process = set() # set of sequence numbers that are currently being processed
35
+
36
+ # Directory paths
37
+ DATA_DIR = os.environ.get('DATA_DIR', 'data')
38
+ PENDING_DIR = os.path.join(DATA_DIR, 'pending')
39
+ PENDING_VOICE_DIR = os.path.join(PENDING_DIR, 'voice')
40
+ PENDING_TEXT_DIR = os.path.join(PENDING_DIR, 'text')
41
+ PROCESSED_DIR = os.path.join(DATA_DIR, 'processed')
42
+ PROCESSED_VOICE_DIR = os.path.join(PROCESSED_DIR, 'voice')
43
+ PROCESSED_TEXT_DIR = os.path.join(PROCESSED_DIR, 'text')
44
+
45
+ # Ensure directories exist with proper permissions
46
+
47
+
48
+ def ensure_directory_with_permissions(directory):
49
+ """Create directory if it doesn't exist and set permissions."""
50
+ try:
51
+ if not os.path.exists(directory):
52
+ os.makedirs(directory, exist_ok=True)
53
+ # Set permissions: read/write/execute for everyone
54
+ os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG |
55
+ stat.S_IRWXO) # 0777 permissions
56
+ logger.info(f"Directory ensured with permissions: {directory}")
57
+ except Exception as e:
58
+ logger.error(f"Error setting permissions for {directory}: {str(e)}")
59
+
60
+
61
+ # Initialize directories with proper permissions
62
+ ensure_directory_with_permissions(PENDING_VOICE_DIR)
63
+ ensure_directory_with_permissions(PENDING_TEXT_DIR)
64
+ ensure_directory_with_permissions(PROCESSED_VOICE_DIR)
65
+ ensure_directory_with_permissions(PROCESSED_TEXT_DIR)
66
+
67
+ # Global application variable
68
+ application = None
69
+
70
+
71
+ def extract_sequence_number(filename: str) -> Optional[int]:
72
+ """Extract sequence number from filename, e.g., 'Sound 100.wav' -> 100."""
73
+ match = re.search(r'(\d+)', filename)
74
+ if match:
75
+ return int(match.group(1))
76
+ return None
77
+
78
+
79
+ def get_files_by_sequence_number():
80
+ """Create dictionaries mapping sequence numbers to filenames."""
81
+ voice_files = {}
82
+ text_files = {}
83
+
84
+ # Map voice files to sequence numbers
85
+ if os.path.exists(PENDING_VOICE_DIR):
86
+ for filename in os.listdir(PENDING_VOICE_DIR):
87
+ if filename.endswith('.wav'):
88
+ seq_num = extract_sequence_number(filename)
89
+ if seq_num is not None:
90
+ voice_files[seq_num] = filename
91
+
92
+ # Map text files to sequence numbers
93
+ if os.path.exists(PENDING_TEXT_DIR):
94
+ for filename in os.listdir(PENDING_TEXT_DIR):
95
+ if filename.endswith('.txt'):
96
+ seq_num = extract_sequence_number(filename)
97
+ if seq_num is not None:
98
+ text_files[seq_num] = filename
99
+
100
+ return voice_files, text_files
101
+
102
+
103
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
104
+ """Send a welcome message when the /start command is issued."""
105
+ logger.info(f"Start command received from user {update.effective_user.id}")
106
+ try:
107
+ await update.message.reply_text(
108
+ "السلام عليكم في The Algerian Darija Transcription Correction Bot!\n\n"
109
+ '''
110
+ 🌟 قواعد التنميط اللغوي والبدائل 🌟
111
+
112
+
113
+ 🟦🟦🟦 قواعد الكتابة 🟦🟦🟦
114
+
115
+ 🔷 القاعدة 00
116
+ يُسمح فقط باستخدام الكلمات والحروف العربية في عملية التنصيص. يُمنع استعمال الأحرف الأجنبية أو ما يُعرف بـ "العربيزي".
117
+ 📝 مثال: "على" ✅ وليس "3la" ❌
118
+
119
+ 🔷 القاعدة 01
120
+ يجوز إلصاق الجار والمجرور بالفعل مباشرة دون فصله.
121
+ 📝 مثال: قالولي ✅ بدلاً من قالوا لي ❌
122
+
123
+ 🔷 القاعدة 02
124
+ لا يُشترط الالتزام بألف الجماعة في الأفعال.
125
+ 📝 مثال: قالو ✅ بدلاً من قالوا ❌
126
+
127
+ 🔷 القاعدة 03
128
+ يتم استبدال همزة الألف (أ) بحرف (ا) دائماً.
129
+ 📝 مثال: اكتوب ✅ بدلاً من أكتب ❌
130
+
131
+ 🔷 القاعدة 04
132
+ يُكتب الضمير المتصل (هو) بالشكل الصحيح دون تغيير.
133
+ 📝 مثال: عنده ✅ بدلاً من عندو ❌
134
+
135
+ 🔷 القاعدة 05
136
+ عند ا��نطق، يتم استبدال حرف (ت) بحرف (ث) إذا نُطق كذلك.
137
+ 📝 مثال: ثاني ✅ بدلاً من تاني ❌
138
+
139
+ 🔷 القاعدة 06
140
+ تحويل الفاء الملتصقة بالاسم إلى حرف الجر "في".
141
+ 📝 مثال: في البيت ✅ بدلاً من فالبيت ❌
142
+
143
+ 🔷 القاعدة 07
144
+ يُفضل وصل "ما" و"واو العطف" بالكلمة مباشرة.
145
+ 📝 مثال: ماقولتش ✅ بدلاً من ماقولتش ❌
146
+
147
+ 🔷 القاعدة 09
148
+ تُحافظ حروف المستقبل مثل "عنقول" أو "كنقول" على شكلها الأصلي دون تغيير.
149
+ 📝 مثال: عنقول ✅ (لا تغيير)
150
+
151
+
152
+ 🟩🟩🟩 البدائل اللغوية 🟩🟩🟩
153
+ 🚫 اللفظ غير المعياري ← ✅ اللفظ المعياري
154
+
155
+ درك ← ضرك ✅
156
+
157
+ علابالي ← على بالي ✅
158
+
159
+ شويا ← شوية ✅
160
+
161
+ واش ← وش ✅
162
+
163
+ ڨالو ← قالو ✅
164
+
165
+ نتاع / نتع / تع ← تاع ✅
166
+
167
+ بالصح / بصاح ← بصح ✅
168
+
169
+ قتلك ← قلتلك ✅
170
+
171
+ يقول لك ← يقولك ✅
172
+
173
+ عنا ← عندنا ✅
174
+
175
+ هاداك / هذاك / هاذاك ← هداك ✅
176
+
177
+ لي / ليه / ليك ← اللي / له / لك ✅
178
+
179
+
180
+ 😉 قم بعمل pin أو تثبيت لهذه الرسالة لكي يسهل عليك الرجوع إليها لاحقا لمراجعة القواعد‼️
181
+ '''
182
+ "استعمل /correct للبدأ في تصحيح النصوص\n"
183
+ "استعمل /skip لتجاوز مقطع صوتي معين\n"
184
+ "استعمل /cancel لإلغاء العملية\n"
185
+ )
186
+ logger.info("Start message sent successfully")
187
+ except Exception as e:
188
+ logger.error(f"Error sending start message: {e}")
189
+
190
+
191
+ async def get_next_available_chunk() -> Tuple[Optional[str], Optional[str], Optional[int]]:
192
+ """Find the next available audio chunk that isn't being processed.
193
+ Returns (voice_filename, text_filename, sequence_number) or (None, None, None)"""
194
+ voice_files, text_files = get_files_by_sequence_number()
195
+
196
+ # Find sequence numbers that exist in both voice and text files
197
+ common_seq_nums = set(voice_files.keys()).intersection(
198
+ set(text_files.keys()))
199
+
200
+ # Filter out sequence numbers that are already being processed
201
+ available_seq_nums = [
202
+ seq_num for seq_num in common_seq_nums if seq_num not in chunks_in_process]
203
+
204
+ logger.debug(f"Available sequence numbers: {available_seq_nums}")
205
+
206
+ if available_seq_nums:
207
+ # Get random available sequence number from the list
208
+ seq_num = random.choice(available_seq_nums)
209
+ logger.debug(f"Selected sequence number: {seq_num}")
210
+ return voice_files[seq_num], text_files[seq_num], seq_num
211
+
212
+ logger.debug("No available chunks found")
213
+ return None, None, None
214
+
215
+
216
+ async def send_chunk_to_user(update: Update, user_id: int, voice_filename: str, text_filename: str, seq_num: int) -> bool:
217
+ """Send audio chunk and text to user. Returns True if successful, False otherwise."""
218
+ # Send audio file
219
+ audio_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
220
+
221
+ # Check if audio file exists
222
+ if not os.path.exists(audio_path):
223
+ logger.error(f"Audio file not found: {audio_path}")
224
+ await update.message.reply_text("Error: Audio file not found. Please try again.")
225
+ return False
226
+
227
+ # Read the original transcription to send as reference
228
+ text_path = os.path.join(PENDING_TEXT_DIR, text_filename)
229
+ if not os.path.exists(text_path):
230
+ logger.error(f"Text file not found: {text_path}")
231
+ await update.message.reply_text("Error: Text file not found. Please try again.")
232
+ return False
233
+
234
+ try:
235
+ with open(text_path, 'r', encoding='utf-8') as f:
236
+ original_text = f.read().strip()
237
+ except Exception as e:
238
+ logger.error(f"Error reading text file: {e}")
239
+ await update.message.reply_text(f"Error reading transcription file. Please try again.")
240
+ return False
241
+
242
+ # Send audio and original text
243
+ try:
244
+ with open(audio_path, 'rb') as audio_file:
245
+ await update.message.reply_voice(voice=audio_file)
246
+
247
+ await update.message.reply_text(
248
+ f"من فضلك استمع إلى المقطع الصوتي وأعد كتابة النص.\n\n"
249
+ f"النص الأصلي:\n{original_text}\n\n"
250
+ f"من فضلك قم بإرسال النص الذي تم تصحيحه أو استعمل /skip لتجاوز هذا المقطع."
251
+ f"استعمل /cancel لإلغاء العملية."
252
+ )
253
+
254
+ logger.info(f"Successfully sent audio and text to user {user_id}")
255
+ return True
256
+ except Exception as e:
257
+ logger.error(f"Error sending audio or text: {e}")
258
+ await update.message.reply_text("Error sending audio. Please try again.")
259
+ return False
260
+
261
+
262
+ async def correct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
263
+ """Start the correction process."""
264
+ logger.info(
265
+ f"تم استقبال أمر التصحيح من المستعمل {update.effective_user.id}")
266
+ user_id = update.effective_user.id
267
+
268
+ # Check if user already has an active task
269
+ if user_id in user_tasks:
270
+ await update.message.reply_text("أنت الآن في غضون عملية تصحيح. من فضلك قم بإكمالها أو ألغي العملية عبر /cancel.")
271
+ current_state = AWAITING_VALIDATION if len(
272
+ user_tasks[user_id]) > 3 else AWAITING_CORRECTION
273
+ return current_state
274
+
275
+ # Ensure directory paths exist with proper permissions
276
+ ensure_directory_with_permissions(PENDING_VOICE_DIR)
277
+ ensure_directory_with_permissions(PENDING_TEXT_DIR)
278
+
279
+ # Log directory contents for debugging
280
+ logger.debug(
281
+ f"PENDING_VOICE_DIR contents: {os.listdir(PENDING_VOICE_DIR)}")
282
+ logger.debug(f"PENDING_TEXT_DIR contents: {os.listdir(PENDING_TEXT_DIR)}")
283
+
284
+ # Get next available chunk
285
+ voice_filename, text_filename, seq_num = await get_next_available_chunk()
286
+
287
+ if not voice_filename or not text_filename:
288
+ await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.")
289
+ return ConversationHandler.END
290
+
291
+ # Mark chunk as being processed
292
+ chunks_in_process.add(seq_num)
293
+ user_tasks[user_id] = (voice_filename, text_filename, seq_num)
294
+
295
+ # Send chunk to user
296
+ success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num)
297
+
298
+ if not success:
299
+ # Clean up on failure
300
+ chunks_in_process.remove(seq_num)
301
+ del user_tasks[user_id]
302
+ return ConversationHandler.END
303
+
304
+ return AWAITING_CORRECTION
305
+
306
+
307
+ async def skip_chunk(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
308
+ """Skip the current chunk and get a new one."""
309
+ user_id = update.effective_user.id
310
+
311
+ if user_id not in user_tasks:
312
+ await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.")
313
+ return ConversationHandler.END
314
+
315
+ # Get current task info
316
+ current_task = user_tasks[user_id]
317
+ if len(current_task) >= 3: # Has at least voice, text, seq_num
318
+ _, _, current_seq_num = current_task[:3]
319
+
320
+ # Remove current chunk from processing (making it available for others)
321
+ chunks_in_process.discard(current_seq_num)
322
+ logger.info(f"User {user_id} skipped chunk {current_seq_num}")
323
+
324
+ # Remove current task
325
+ del user_tasks[user_id]
326
+
327
+ await update.message.reply_text("تجاوزت المقطع الصوتي. دعني أبحث عن مقطع آخر لك.")
328
+
329
+ # Get next available chunk
330
+ voice_filename, text_filename, seq_num = await get_next_available_chunk()
331
+
332
+ if not voice_filename or not text_filename:
333
+ await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.")
334
+ return ConversationHandler.END
335
+
336
+ # Mark new chunk as being processed
337
+ chunks_in_process.add(seq_num)
338
+ user_tasks[user_id] = (voice_filename, text_filename, seq_num)
339
+
340
+ # Send new chunk to user
341
+ success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num)
342
+
343
+ if not success:
344
+ # Clean up on failure
345
+ chunks_in_process.remove(seq_num)
346
+ del user_tasks[user_id]
347
+ return ConversationHandler.END
348
+
349
+ return AWAITING_CORRECTION
350
+
351
+
352
+ async def receive_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
353
+ """Receive the user's correction and ask for validation."""
354
+ user_id = update.effective_user.id
355
+
356
+ if user_id not in user_tasks:
357
+ await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.")
358
+ return ConversationHandler.END
359
+
360
+ voice_filename, text_filename, seq_num = user_tasks[user_id]
361
+ corrected_text = update.message.text
362
+
363
+ # Update user task with corrected text
364
+ user_tasks[user_id] = (voice_filename, text_filename,
365
+ seq_num, corrected_text)
366
+
367
+ # Create validation buttons
368
+ keyboard = [
369
+ [
370
+ InlineKeyboardButton("✅ نعم, أنا متأكد من التصحيح",
371
+ callback_data="validate_yes"),
372
+ InlineKeyboardButton("❌ لا, أريد التصحيح مرة أخرى",
373
+ callback_data="validate_no")
374
+ ]
375
+ ]
376
+ reply_markup = InlineKeyboardMarkup(keyboard)
377
+
378
+ await update.message.reply_text(
379
+ f"النص الذي قمت بتصحيحه:\n{corrected_text}\n\n"
380
+ f"هل أنت متأكد من التصحيح؟",
381
+ reply_markup=reply_markup
382
+ )
383
+
384
+ return AWAITING_VALIDATION
385
+
386
+
387
+ async def handle_validation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
388
+ """Handle the validation response."""
389
+ query = update.callback_query
390
+ await query.answer()
391
+
392
+ user_id = query.from_user.id
393
+
394
+ if user_id not in user_tasks or len(user_tasks[user_id]) < 4:
395
+ await query.edit_message_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.")
396
+ return ConversationHandler.END
397
+
398
+ voice_filename, text_filename, seq_num, corrected_text = user_tasks[user_id]
399
+
400
+ if query.data == "validate_yes":
401
+ # User is confident - save the correction
402
+ success = await save_final_correction(query, user_id, voice_filename, text_filename, seq_num, corrected_text)
403
+ if success:
404
+ await query.edit_message_text(
405
+ "شكرا. تصحيحك تم حفظه\n"
406
+ "استعمل /correct لاستقبال عملية تصحيح جديدة"
407
+ )
408
+ return ConversationHandler.END
409
+ else:
410
+ # If saving failed, go back to correction state
411
+ # Remove corrected text
412
+ user_tasks[user_id] = (voice_filename, text_filename, seq_num)
413
+ await query.edit_message_text(
414
+ "حدث خطأ أثناء حفظ التصحيح. من فضلك أعد إرساله."
415
+ )
416
+ return AWAITING_CORRECTION
417
+
418
+ elif query.data == "validate_no":
419
+ # User wants to correct again - remove corrected text and go back to correction state
420
+ user_tasks[user_id] = (voice_filename, text_filename, seq_num)
421
+ await query.edit_message_text(
422
+ "من فضلك أعد كتابة النص التصحيحي مرة أخرى."
423
+ )
424
+ return AWAITING_CORRECTION
425
+
426
+ return AWAITING_VALIDATION
427
+
428
+
429
+ async def save_final_correction(query, user_id: int, voice_filename: str, text_filename: str, seq_num: int, corrected_text: str) -> bool:
430
+ """Save the final validated correction."""
431
+ # Get original file paths
432
+ original_txt_path = os.path.join(PENDING_TEXT_DIR, text_filename)
433
+ original_wav_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
434
+
435
+ # Create user-specific directory in processed TEXT folder only
436
+ user_processed_text_dir = os.path.join(
437
+ PROCESSED_TEXT_DIR, f"user_{user_id}")
438
+
439
+ # Log the directory path for debugging
440
+ logger.debug(f"Creating text directory: {user_processed_text_dir}")
441
+
442
+ try:
443
+ # Ensure text directory with proper permissions
444
+ ensure_directory_with_permissions(user_processed_text_dir)
445
+
446
+ # Save corrected text to processed directory
447
+ processed_txt_path = os.path.join(
448
+ user_processed_text_dir, text_filename)
449
+
450
+ # Log file paths for debugging
451
+ logger.debug(
452
+ f"Original text path: {original_txt_path}, exists: {os.path.exists(original_txt_path)}")
453
+ logger.debug(
454
+ f"Original wav path: {original_wav_path}, exists: {os.path.exists(original_wav_path)}")
455
+ logger.debug(f"Processed text path: {processed_txt_path}")
456
+
457
+ # Save corrected text
458
+ logger.debug(f"Saving corrected text to {processed_txt_path}")
459
+ with open(processed_txt_path, 'w', encoding='utf-8') as f:
460
+ f.write(corrected_text)
461
+ # Set file permissions
462
+ os.chmod(processed_txt_path, stat.S_IRUSR | stat.S_IWUSR |
463
+ stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
464
+
465
+ # Verify processed text file exists
466
+ logger.debug(
467
+ f"Verifying processed text file exists: {os.path.exists(processed_txt_path)}")
468
+
469
+ # Remove both files from pending directory (text and voice)
470
+ if os.path.exists(original_txt_path):
471
+ logger.debug(f"Removing original text file at {original_txt_path}")
472
+ os.remove(original_txt_path)
473
+ else:
474
+ logger.warning(
475
+ f"Could not remove original text file as it doesn't exist at {original_txt_path}")
476
+
477
+ if os.path.exists(original_wav_path):
478
+ logger.debug(f"Removing original wav file at {original_wav_path}")
479
+ os.remove(original_wav_path)
480
+ else:
481
+ logger.warning(
482
+ f"Could not remove original wav file as it doesn't exist at {original_wav_path}")
483
+
484
+ # Remove from active tasks
485
+ logger.debug(f"Removing seq_num {seq_num} from chunks_in_process")
486
+ chunks_in_process.remove(seq_num)
487
+
488
+ logger.debug(f"Removing user {user_id} from user_tasks")
489
+ del user_tasks[user_id]
490
+
491
+ logger.info(
492
+ f"Successfully saved validated correction for user {user_id}")
493
+ return True
494
+
495
+ except Exception as e:
496
+ # Enhanced error logging with stack trace
497
+ error_msg = f"Error saving correction: {str(e)}"
498
+ logger.error(error_msg)
499
+ logger.error(traceback.format_exc())
500
+
501
+ # Try to provide more specific error messages
502
+ if "Permission denied" in str(e):
503
+ await query.edit_message_text("Error: Permission denied while saving files. Please contact the administrator.")
504
+ elif "No such file or directory" in str(e):
505
+ await query.edit_message_text("Error: File not found. The system couldn't find one of the files.")
506
+ else:
507
+ await query.edit_message_text(f"Error saving your correction: {str(e)}. Please try again.")
508
+
509
+ return False
510
+
511
+
512
+ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
513
+ """Cancel the current correction task."""
514
+ user_id = update.effective_user.id
515
+
516
+ if user_id in user_tasks:
517
+ task_info = user_tasks[user_id]
518
+ if len(task_info) >= 3: # Has at least voice, text, seq_num
519
+ seq_num = task_info[2]
520
+ chunks_in_process.discard(seq_num)
521
+ del user_tasks[user_id]
522
+
523
+ await update.message.reply_text("العملية قد ألغيت :( استعمل /correct للبدء تصحيح جديد.")
524
+ else:
525
+ await update.message.reply_text("لا توجد عملية تصحيح نشطة لإلغائها.")
526
+
527
+ return ConversationHandler.END
528
+
529
+
530
+ async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
531
+ """Show status of pending and processed chunks."""
532
+ logger.info(
533
+ f"Status command received from user {update.effective_user.id}")
534
+
535
+ # Count pending files
536
+ pending_voice_count = len(
537
+ [f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')])
538
+ pending_text_count = len(
539
+ [f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')])
540
+
541
+ # Get sequence numbers
542
+ voice_files, text_files = get_files_by_sequence_number()
543
+ matching_pairs = len(
544
+ set(voice_files.keys()).intersection(set(text_files.keys())))
545
+
546
+ # Count processed files (only text files now)
547
+ processed_text_count = 0
548
+
549
+ # Count text files
550
+ for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
551
+ processed_text_count += len([f for f in files if f.endswith('.txt')])
552
+
553
+ active_tasks = len(user_tasks)
554
+
555
+ # Check file permissions
556
+ permissions_info = ""
557
+ try:
558
+ # Check if we can write to the processed text directory
559
+ can_write_text = os.access(PROCESSED_TEXT_DIR, os.W_OK)
560
+ permissions_info = f"\n• Write permissions: Text: {can_write_text}"
561
+ except Exception as e:
562
+ permissions_info = f"\n• Error checking permissions: {str(e)}"
563
+
564
+ await update.message.reply_text(
565
+ f"📊 Transcription Status:\n"
566
+ f"• Pending voice files: {pending_voice_count}\n"
567
+ f"• Pending text files: {pending_text_count}\n"
568
+ f"• Matching pending pairs: {matching_pairs}\n"
569
+ f"• Processed text files: {processed_text_count}\n"
570
+ f"• Active tasks: {active_tasks}{permissions_info}\n"
571
+ f"• Note: Only corrected text files are saved, voice files are not copied to processed folder"
572
+ )
573
+
574
+
575
+ async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
576
+ """Command to show debug information about directories and permissions."""
577
+ user_id = update.effective_user.id
578
+
579
+ # Check directory structure
580
+ debug_info = [
581
+ "📁 Directory Check:",
582
+ f"• DATA_DIR: {DATA_DIR}, exists: {os.path.exists(DATA_DIR)}",
583
+ f"• PENDING_DIR: {PENDING_DIR}, exists: {os.path.exists(PENDING_DIR)}",
584
+ f"• PENDING_VOICE_DIR: {PENDING_VOICE_DIR}, exists: {os.path.exists(PENDING_VOICE_DIR)}",
585
+ f"• PENDING_TEXT_DIR: {PENDING_TEXT_DIR}, exists: {os.path.exists(PENDING_TEXT_DIR)}",
586
+ f"• PROCESSED_DIR: {PROCESSED_DIR}, exists: {os.path.exists(PROCESSED_DIR)}",
587
+ f"• PROCESSED_TEXT_DIR: {PROCESSED_TEXT_DIR}, exists: {os.path.exists(PROCESSED_TEXT_DIR)}",
588
+ ]
589
+
590
+ # Check permissions
591
+ try:
592
+ permission_info = [
593
+ "🔑 Permission Check:",
594
+ f"• PENDING_VOICE_DIR writable: {os.access(PENDING_VOICE_DIR, os.W_OK)}",
595
+ f"• PENDING_TEXT_DIR writable: {os.access(PENDING_TEXT_DIR, os.W_OK)}",
596
+ f"• PROCESSED_TEXT_DIR writable: {os.access(PROCESSED_TEXT_DIR, os.W_OK)}",
597
+ ]
598
+ debug_info.extend(permission_info)
599
+ except Exception as e:
600
+ debug_info.append(f"Error checking permissions: {str(e)}")
601
+
602
+ # Check active tasks
603
+ task_info = [
604
+ "📋 Active Tasks:",
605
+ f"• Number of active users: {len(user_tasks)}",
606
+ f"• Active chunks: {len(chunks_in_process)}",
607
+ ]
608
+ debug_info.extend(task_info)
609
+
610
+ await update.message.reply_text("\n".join(debug_info))
611
+
612
+
613
+ async def test_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
614
+ """Test handler to verify bot is receiving messages."""
615
+ logger.info(f"Test command received from user {update.effective_user.id}")
616
+ await update.message.reply_text("Bot is working! ✅")
617
+
618
+
619
+ async def setup_application():
620
+ """Setup the Application with all handlers."""
621
+ global application
622
+
623
+ # Get the token from environment variable
624
+ token = os.environ.get("TELEGRAM_TOKEN")
625
+ if not token:
626
+ logger.error("TELEGRAM_TOKEN environment variable not set!")
627
+ return None
628
+
629
+ logger.info(f"Setting up bot with token: {token[:10]}...")
630
+
631
+ # Create the Application with additional debugging
632
+ application = Application.builder().token(token).build()
633
+
634
+ # Add a test handler first
635
+ application.add_handler(CommandHandler("test", test_handler))
636
+
637
+ # Add conversation handler for correction workflow
638
+ conv_handler = ConversationHandler(
639
+ entry_points=[CommandHandler("correct", correct)],
640
+ states={
641
+ AWAITING_CORRECTION: [
642
+ MessageHandler(filters.TEXT & ~filters.COMMAND,
643
+ receive_correction),
644
+ CommandHandler("skip", skip_chunk),
645
+ ],
646
+ AWAITING_VALIDATION: [
647
+ CallbackQueryHandler(handle_validation, pattern="^validate_"),
648
+ ],
649
+ },
650
+ fallbacks=[
651
+ CommandHandler("cancel", cancel),
652
+ CommandHandler("skip", skip_chunk),
653
+ ],
654
+ name="correction_conversation",
655
+ )
656
+
657
+ application.add_handler(conv_handler)
658
+
659
+ # Add command handlers
660
+ application.add_handler(CommandHandler("start", start))
661
+ application.add_handler(CommandHandler("status", status))
662
+ application.add_handler(CommandHandler("cancel", cancel))
663
+ application.add_handler(CommandHandler(
664
+ "skip", skip_chunk)) # Global skip handler
665
+ application.add_handler(CommandHandler("debug", debug_info))
666
+
667
+ logger.info("All handlers added successfully")
668
+ return application
669
+
670
+
671
+ async def start_bot():
672
+ """Start the bot with proper signal handling."""
673
+ global application
674
+
675
+ logger.info("Starting bot setup...")
676
+
677
+ # Create and configure the bot
678
+ app = await setup_application()
679
+ if not app:
680
+ logger.error("Failed to create application")
681
+ return
682
+
683
+ logger.info("Application created successfully")
684
+
685
+ # Start the Bot
686
+ try:
687
+ await app.initialize()
688
+ logger.info("Application initialized")
689
+
690
+ await app.start()
691
+ logger.info("Application started")
692
+
693
+ # Start polling for updates
694
+ await app.updater.start_polling(drop_pending_updates=False)
695
+ logger.info("Started polling for updates")
696
+
697
+ except Exception as e:
698
+ logger.error(f"Error starting bot: {e}")
699
+ return
700
+
701
+ # Setup signal handlers for graceful shutdown
702
+ loop = asyncio.get_event_loop()
703
+
704
+ for signal_name in ('SIGINT', 'SIGTERM'):
705
+ try:
706
+ loop.add_signal_handler(
707
+ getattr(signal, signal_name),
708
+ lambda s=signal_name: asyncio.create_task(shutdown(s))
709
+ )
710
+ except NotImplementedError:
711
+ # Windows doesn't support this
712
+ logger.info("Signal handlers not supported on this platform")
713
+ pass
714
+
715
+ try:
716
+ # Just run forever until interrupted
717
+ logger.info("Bot is now running and waiting for messages...")
718
+ await asyncio.Event().wait()
719
+ finally:
720
+ # Ensure the bot is properly shut down
721
+ await shutdown("Manual")
722
+
723
+
724
+ async def shutdown(signal_type):
725
+ """Cleanup tasks tied to the service's shutdown."""
726
+ global application
727
+
728
+ logger.info(f"Received exit signal {signal_type}...")
729
+
730
+ # Stop the bot
731
+ if application:
732
+ logger.info("Stopping application...")
733
+ await application.updater.stop()
734
+ await application.stop()
735
+ await application.shutdown()
736
+
737
+ # Cancel all running tasks
738
+ tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
739
+ for task in tasks:
740
+ task.cancel()
741
+
742
+ # Wait for all tasks to be cancelled
743
+ if tasks:
744
+ logger.info(f"Waiting for {len(tasks)} tasks to complete...")
745
+ await asyncio.gather(*tasks, return_exceptions=True)
746
+
747
+ logger.info("Application shutdown complete")
748
+
749
+
750
+ async def main():
751
+ """Main function to setup and run the bot."""
752
+ # Setup and start the bot
753
+ await start_bot()
754
+
755
+ # This is the entry point when run as the main process
756
+ if __name__ == "__main__":
757
+ asyncio.run(main())