slim-S commited on
Commit
d70912d
·
1 Parent(s): 6954320

chore: update

Browse files
Files changed (3) hide show
  1. Dockerfile +23 -23
  2. app.py +152 -138
  3. main.py +73 -90
Dockerfile CHANGED
@@ -1,24 +1,24 @@
1
- # Use a Python 3.10 base image
2
- FROM python:3.10-slim
3
-
4
- # Set working directory
5
- WORKDIR ./
6
-
7
- # 1. First install dependencies as root
8
- COPY requirements.txt .
9
- RUN pip install --no-cache-dir -r requirements.txt
10
-
11
- # 2. Create non-root user and data directories
12
- RUN useradd -m -u 1000 appuser && \
13
- mkdir -p /data/processed/text /data/processed/voice \
14
- /data/pending/text /data/pending/voice && \
15
- chown -R appuser:appuser /data
16
-
17
- # 3. Switch to non-root user
18
- USER appuser
19
-
20
- # 4. Copy app files (now owned by appuser)
21
- COPY --chown=appuser . .
22
-
23
- # Change the command to use main.py instead of app.py
24
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Use a Python 3.10 base image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR ./
6
+
7
+ # 1. First install dependencies as root
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # 2. Create non-root user and data directories
12
+ RUN useradd -m -u 1000 appuser && \
13
+ mkdir -p /data/processed/text /data/processed/voice \
14
+ /data/pending/text /data/pending/voice && \
15
+ chown -R appuser:appuser /data
16
+
17
+ # 3. Switch to non-root user
18
+ USER appuser
19
+
20
+ # 4. Copy app files (now owned by appuser)
21
+ COPY --chown=appuser . .
22
+
23
+ # Change the command to use main.py instead of app.py
24
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -40,17 +40,21 @@ PROCESSED_VOICE_DIR = os.path.join(PROCESSED_DIR, 'voice')
40
  PROCESSED_TEXT_DIR = os.path.join(PROCESSED_DIR, 'text')
41
 
42
  # Ensure directories exist with proper permissions
 
 
43
  def ensure_directory_with_permissions(directory):
44
  """Create directory if it doesn't exist and set permissions."""
45
  try:
46
  if not os.path.exists(directory):
47
  os.makedirs(directory, exist_ok=True)
48
  # Set permissions: read/write/execute for everyone
49
- os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 permissions
 
50
  logger.info(f"Directory ensured with permissions: {directory}")
51
  except Exception as e:
52
  logger.error(f"Error setting permissions for {directory}: {str(e)}")
53
 
 
54
  # Initialize directories with proper permissions
55
  ensure_directory_with_permissions(PENDING_VOICE_DIR)
56
  ensure_directory_with_permissions(PENDING_TEXT_DIR)
@@ -60,6 +64,7 @@ ensure_directory_with_permissions(PROCESSED_TEXT_DIR)
60
  # Global application variable
61
  application = None
62
 
 
63
  def extract_sequence_number(filename: str) -> Optional[int]:
64
  """Extract sequence number from filename, e.g., 'Sound 100.wav' -> 100."""
65
  match = re.search(r'(\d+)', filename)
@@ -67,11 +72,12 @@ def extract_sequence_number(filename: str) -> Optional[int]:
67
  return int(match.group(1))
68
  return None
69
 
 
70
  def get_files_by_sequence_number():
71
  """Create dictionaries mapping sequence numbers to filenames."""
72
  voice_files = {}
73
  text_files = {}
74
-
75
  # Map voice files to sequence numbers
76
  if os.path.exists(PENDING_VOICE_DIR):
77
  for filename in os.listdir(PENDING_VOICE_DIR):
@@ -79,7 +85,7 @@ def get_files_by_sequence_number():
79
  seq_num = extract_sequence_number(filename)
80
  if seq_num is not None:
81
  voice_files[seq_num] = filename
82
-
83
  # Map text files to sequence numbers
84
  if os.path.exists(PENDING_TEXT_DIR):
85
  for filename in os.listdir(PENDING_TEXT_DIR):
@@ -87,9 +93,10 @@ def get_files_by_sequence_number():
87
  seq_num = extract_sequence_number(filename)
88
  if seq_num is not None:
89
  text_files[seq_num] = filename
90
-
91
  return voice_files, text_files
92
 
 
93
  async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
94
  """Send a welcome message when the /start command is issued."""
95
  logger.info(f"Start command received from user {update.effective_user.id}")
@@ -103,205 +110,206 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
103
  except Exception as e:
104
  logger.error(f"Error sending start message: {e}")
105
 
 
106
  async def get_next_available_chunk() -> Tuple[Optional[str], Optional[str], Optional[int]]:
107
  """Find the next available audio chunk that isn't being processed.
108
  Returns (voice_filename, text_filename, sequence_number) or (None, None, None)"""
109
  voice_files, text_files = get_files_by_sequence_number()
110
-
111
  # Find sequence numbers that exist in both voice and text files
112
- common_seq_nums = set(voice_files.keys()).intersection(set(text_files.keys()))
113
-
 
114
  # Filter out sequence numbers that are already being processed
115
- available_seq_nums = [seq_num for seq_num in common_seq_nums if seq_num not in chunks_in_process]
116
-
 
117
  logger.debug(f"Available sequence numbers: {available_seq_nums}")
118
-
119
  if available_seq_nums:
120
  # Get the first available sequence number
121
  seq_num = min(available_seq_nums)
122
  return voice_files[seq_num], text_files[seq_num], seq_num
123
-
124
  logger.debug("No available chunks found")
125
  return None, None, None
126
 
 
127
  async def correct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
128
  """Start the correction process."""
129
- logger.info(f"Correct command received from user {update.effective_user.id}")
 
130
  user_id = update.effective_user.id
131
-
132
  # Check if user already has an active task
133
  if user_id in user_tasks:
134
  await update.message.reply_text("You already have an active correction task. Please finish it or use /cancel.")
135
  return AWAITING_CORRECTION
136
-
137
  # Ensure directory paths exist with proper permissions
138
  ensure_directory_with_permissions(PENDING_VOICE_DIR)
139
  ensure_directory_with_permissions(PENDING_TEXT_DIR)
140
-
141
  # Log directory contents for debugging
142
- logger.debug(f"PENDING_VOICE_DIR contents: {os.listdir(PENDING_VOICE_DIR)}")
 
143
  logger.debug(f"PENDING_TEXT_DIR contents: {os.listdir(PENDING_TEXT_DIR)}")
144
-
145
  # Get next available chunk
146
  voice_filename, text_filename, seq_num = await get_next_available_chunk()
147
-
148
  if not voice_filename or not text_filename:
149
  await update.message.reply_text("No pending transcriptions available at the moment. Please try again later.")
150
  return ConversationHandler.END
151
-
152
  # Mark chunk as being processed
153
  chunks_in_process.add(seq_num)
154
  user_tasks[user_id] = (voice_filename, text_filename, seq_num)
155
-
156
  # Send audio file
157
  audio_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
158
-
159
  # Check if audio file exists
160
  if not os.path.exists(audio_path):
161
  logger.error(f"Audio file not found: {audio_path}")
162
  await update.message.reply_text("Error: Audio file not found. Please try again.")
163
-
164
  # Clean up
165
  chunks_in_process.remove(seq_num)
166
  del user_tasks[user_id]
167
  return ConversationHandler.END
168
-
169
  # Read the original transcription to send as reference
170
  text_path = os.path.join(PENDING_TEXT_DIR, text_filename)
171
  if not os.path.exists(text_path):
172
  logger.error(f"Text file not found: {text_path}")
173
  await update.message.reply_text("Error: Text file not found. Please try again.")
174
-
175
  # Clean up
176
  chunks_in_process.remove(seq_num)
177
  del user_tasks[user_id]
178
  return ConversationHandler.END
179
-
180
  try:
181
  with open(text_path, 'r', encoding='utf-8') as f:
182
  original_text = f.read().strip()
183
  except Exception as e:
184
  logger.error(f"Error reading text file: {e}")
185
  await update.message.reply_text(f"Error reading transcription file. Please try again.")
186
-
187
  # Clean up
188
  chunks_in_process.remove(seq_num)
189
  del user_tasks[user_id]
190
  return ConversationHandler.END
191
-
192
  # Send audio and original text
193
  try:
194
  with open(audio_path, 'rb') as audio_file:
195
  await update.message.reply_voice(voice=audio_file)
196
-
197
  await update.message.reply_text(
198
  f"Please listen to the audio and correct the transcription.\n\n"
199
  f"Original transcription:\n{original_text}\n\n"
200
  f"Please send your corrected version."
201
  )
202
-
203
  logger.info(f"Successfully sent audio and text to user {user_id}")
204
  return AWAITING_CORRECTION
205
  except Exception as e:
206
  logger.error(f"Error sending audio or text: {e}")
207
  await update.message.reply_text("Error sending audio. Please try again.")
208
-
209
  # Clean up
210
  chunks_in_process.remove(seq_num)
211
  del user_tasks[user_id]
212
  return ConversationHandler.END
213
 
 
214
  async def save_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
215
- """Save the user's correction."""
216
  user_id = update.effective_user.id
217
-
218
  if user_id not in user_tasks:
219
  await update.message.reply_text("You don't have an active correction task. Use /correct to start one.")
220
  return ConversationHandler.END
221
-
222
  voice_filename, text_filename, seq_num = user_tasks[user_id]
223
  corrected_text = update.message.text
224
-
225
  # Get original file paths
226
  original_txt_path = os.path.join(PENDING_TEXT_DIR, text_filename)
227
  original_wav_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
228
-
229
- # Create user-specific directories in processed folders with proper permissions
230
- user_processed_voice_dir = os.path.join(PROCESSED_VOICE_DIR, f"user_{user_id}")
231
- user_processed_text_dir = os.path.join(PROCESSED_TEXT_DIR, f"user_{user_id}")
232
-
233
- # Log the directory paths for debugging
234
- logger.debug(f"Creating directories: {user_processed_voice_dir}, {user_processed_text_dir}")
235
-
236
  try:
237
- # Ensure directories with proper permissions
238
- ensure_directory_with_permissions(user_processed_voice_dir)
239
  ensure_directory_with_permissions(user_processed_text_dir)
240
-
241
- # Move files to processed directory with corrected text
242
- processed_txt_path = os.path.join(user_processed_text_dir, text_filename)
243
- processed_wav_path = os.path.join(user_processed_voice_dir, voice_filename)
244
-
245
  # Log file paths for debugging
246
- logger.debug(f"Original text path: {original_txt_path}, exists: {os.path.exists(original_txt_path)}")
247
- logger.debug(f"Original wav path: {original_wav_path}, exists: {os.path.exists(original_wav_path)}")
 
 
248
  logger.debug(f"Processed text path: {processed_txt_path}")
249
- logger.debug(f"Processed wav path: {processed_wav_path}")
250
-
251
- # Save corrected text first
252
  logger.debug(f"Saving corrected text to {processed_txt_path}")
253
  with open(processed_txt_path, 'w', encoding='utf-8') as f:
254
  f.write(corrected_text)
255
  # Set file permissions
256
- os.chmod(processed_txt_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
257
-
258
- # Copy wav file - verify it exists first
259
- if os.path.exists(original_wav_path):
260
- logger.debug(f"Copying wav file from {original_wav_path} to {processed_wav_path}")
261
- shutil.copy2(original_wav_path, processed_wav_path)
262
- # Set file permissions
263
- os.chmod(processed_wav_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
264
- else:
265
- logger.error(f"Original wav file does not exist at {original_wav_path}")
266
- await update.message.reply_text("Error: Original audio file not found. Please try again.")
267
- return AWAITING_CORRECTION
268
-
269
- # Verify processed files exist
270
- logger.debug(f"Verifying processed files: text exists: {os.path.exists(processed_txt_path)}, wav exists: {os.path.exists(processed_wav_path)}")
271
-
272
- # Remove from pending directory - be careful to check if they exist first
273
  if os.path.exists(original_txt_path):
274
  logger.debug(f"Removing original text file at {original_txt_path}")
275
  os.remove(original_txt_path)
276
  else:
277
- logger.warning(f"Could not remove original text file as it doesn't exist at {original_txt_path}")
278
-
 
279
  if os.path.exists(original_wav_path):
280
  logger.debug(f"Removing original wav file at {original_wav_path}")
281
  os.remove(original_wav_path)
282
  else:
283
- logger.warning(f"Could not remove original wav file as it doesn't exist at {original_wav_path}")
284
-
 
285
  # Remove from active tasks
286
  logger.debug(f"Removing seq_num {seq_num} from chunks_in_process")
287
  chunks_in_process.remove(seq_num)
288
-
289
  logger.debug(f"Removing user {user_id} from user_tasks")
290
  del user_tasks[user_id]
291
-
292
- logger.info(f"Successfully saved correction for user {user_id}")
 
293
  await update.message.reply_text(
294
  "Thank you! Your correction has been saved.\n"
295
  "Use /correct to receive another transcription task."
296
  )
297
-
298
  return ConversationHandler.END
299
  except Exception as e:
300
  # Enhanced error logging with stack trace
301
  error_msg = f"Error saving correction: {str(e)}"
302
  logger.error(error_msg)
303
  logger.error(traceback.format_exc())
304
-
305
  # Try to provide more specific error messages
306
  if "Permission denied" in str(e):
307
  await update.message.reply_text("Error: Permission denied while saving files. Please contact the administrator.")
@@ -309,74 +317,75 @@ async def save_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
309
  await update.message.reply_text("Error: File not found. The system couldn't find one of the files.")
310
  else:
311
  await update.message.reply_text(f"Error saving your correction: {str(e)}. Please try again.")
312
-
313
  return AWAITING_CORRECTION
314
 
 
315
  async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
316
  """Cancel the current correction task."""
317
  user_id = update.effective_user.id
318
-
319
  if user_id in user_tasks:
320
  _, _, seq_num = user_tasks[user_id]
321
  chunks_in_process.remove(seq_num)
322
  del user_tasks[user_id]
323
-
324
  await update.message.reply_text("Task cancelled. Use /correct to start a new one.")
325
  else:
326
  await update.message.reply_text("You don't have an active task to cancel.")
327
-
328
  return ConversationHandler.END
329
 
 
330
  async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
331
  """Show status of pending and processed chunks."""
332
- logger.info(f"Status command received from user {update.effective_user.id}")
333
-
 
334
  # Count pending files
335
- pending_voice_count = len([f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')])
336
- pending_text_count = len([f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')])
337
-
 
 
338
  # Get sequence numbers
339
  voice_files, text_files = get_files_by_sequence_number()
340
- matching_pairs = len(set(voice_files.keys()).intersection(set(text_files.keys())))
341
-
342
- # Count processed files
343
- processed_voice_count = 0
344
  processed_text_count = 0
345
-
346
- # Count voice files
347
- for root, dirs, files in os.walk(PROCESSED_VOICE_DIR):
348
- processed_voice_count += len([f for f in files if f.endswith('.wav')])
349
-
350
  # Count text files
351
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
352
  processed_text_count += len([f for f in files if f.endswith('.txt')])
353
-
354
  active_tasks = len(user_tasks)
355
-
356
  # Check file permissions
357
  permissions_info = ""
358
  try:
359
- # Check if we can write to the processed directories
360
- can_write_voice = os.access(PROCESSED_VOICE_DIR, os.W_OK)
361
  can_write_text = os.access(PROCESSED_TEXT_DIR, os.W_OK)
362
- permissions_info = f"\n• Write permissions: Voice: {can_write_voice}, Text: {can_write_text}"
363
  except Exception as e:
364
  permissions_info = f"\n• Error checking permissions: {str(e)}"
365
-
366
  await update.message.reply_text(
367
  f"📊 Transcription Status:\n"
368
  f"• Pending voice files: {pending_voice_count}\n"
369
  f"• Pending text files: {pending_text_count}\n"
370
  f"• Matching pending pairs: {matching_pairs}\n"
371
- f"• Processed voice files: {processed_voice_count}\n"
372
  f"• Processed text files: {processed_text_count}\n"
373
- f"• Active tasks: {active_tasks}{permissions_info}"
 
374
  )
375
 
 
376
  async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
377
  """Command to show debug information about directories and permissions."""
378
  user_id = update.effective_user.id
379
-
380
  # Check directory structure
381
  debug_info = [
382
  "📁 Directory Check:",
@@ -385,23 +394,21 @@ async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
385
  f"• PENDING_VOICE_DIR: {PENDING_VOICE_DIR}, exists: {os.path.exists(PENDING_VOICE_DIR)}",
386
  f"• PENDING_TEXT_DIR: {PENDING_TEXT_DIR}, exists: {os.path.exists(PENDING_TEXT_DIR)}",
387
  f"• PROCESSED_DIR: {PROCESSED_DIR}, exists: {os.path.exists(PROCESSED_DIR)}",
388
- f"• PROCESSED_VOICE_DIR: {PROCESSED_VOICE_DIR}, exists: {os.path.exists(PROCESSED_VOICE_DIR)}",
389
  f"• PROCESSED_TEXT_DIR: {PROCESSED_TEXT_DIR}, exists: {os.path.exists(PROCESSED_TEXT_DIR)}",
390
  ]
391
-
392
  # Check permissions
393
  try:
394
  permission_info = [
395
  "🔑 Permission Check:",
396
  f"• PENDING_VOICE_DIR writable: {os.access(PENDING_VOICE_DIR, os.W_OK)}",
397
  f"• PENDING_TEXT_DIR writable: {os.access(PENDING_TEXT_DIR, os.W_OK)}",
398
- f"• PROCESSED_VOICE_DIR writable: {os.access(PROCESSED_VOICE_DIR, os.W_OK)}",
399
  f"• PROCESSED_TEXT_DIR writable: {os.access(PROCESSED_TEXT_DIR, os.W_OK)}",
400
  ]
401
  debug_info.extend(permission_info)
402
  except Exception as e:
403
  debug_info.append(f"Error checking permissions: {str(e)}")
404
-
405
  # Check active tasks
406
  task_info = [
407
  "📋 Active Tasks:",
@@ -409,88 +416,93 @@ async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None
409
  f"• Active chunks: {len(chunks_in_process)}",
410
  ]
411
  debug_info.extend(task_info)
412
-
413
  await update.message.reply_text("\n".join(debug_info))
414
 
 
415
  async def test_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
416
  """Test handler to verify bot is receiving messages."""
417
  logger.info(f"Test command received from user {update.effective_user.id}")
418
  await update.message.reply_text("Bot is working! ✅")
419
 
 
420
  async def setup_application():
421
  """Setup the Application with all handlers."""
422
  global application
423
-
424
  # Get the token from environment variable
425
  token = os.environ.get("TELEGRAM_TOKEN")
426
  if not token:
427
  logger.error("TELEGRAM_TOKEN environment variable not set!")
428
  return None
429
-
430
  logger.info(f"Setting up bot with token: {token[:10]}...")
431
-
432
  # Create the Application with additional debugging
433
  application = Application.builder().token(token).build()
434
-
435
  # Add a test handler first
436
  application.add_handler(CommandHandler("test", test_handler))
437
-
438
  # Add conversation handler for correction workflow
439
  conv_handler = ConversationHandler(
440
  entry_points=[CommandHandler("correct", correct)],
441
  states={
442
  AWAITING_CORRECTION: [
443
- MessageHandler(filters.TEXT & ~filters.COMMAND, save_correction),
 
444
  ],
445
  },
446
  fallbacks=[CommandHandler("cancel", cancel)],
447
  name="correction_conversation",
448
  )
449
-
450
  application.add_handler(conv_handler)
451
-
452
  # Add command handlers
453
  application.add_handler(CommandHandler("start", start))
454
  application.add_handler(CommandHandler("status", status))
455
  application.add_handler(CommandHandler("cancel", cancel))
456
- application.add_handler(CommandHandler("debug", debug_info)) # New debug command
457
-
 
458
  logger.info("All handlers added successfully")
459
  return application
460
 
 
461
  async def start_bot():
462
  """Start the bot with proper signal handling."""
463
  global application
464
-
465
  logger.info("Starting bot setup...")
466
-
467
  # Create and configure the bot
468
  app = await setup_application()
469
  if not app:
470
  logger.error("Failed to create application")
471
  return
472
-
473
  logger.info("Application created successfully")
474
-
475
  # Start the Bot
476
  try:
477
  await app.initialize()
478
  logger.info("Application initialized")
479
-
480
  await app.start()
481
  logger.info("Application started")
482
-
483
  # Start polling for updates
484
  await app.updater.start_polling(drop_pending_updates=False)
485
  logger.info("Started polling for updates")
486
-
487
  except Exception as e:
488
  logger.error(f"Error starting bot: {e}")
489
  return
490
-
491
  # Setup signal handlers for graceful shutdown
492
  loop = asyncio.get_event_loop()
493
-
494
  for signal_name in ('SIGINT', 'SIGTERM'):
495
  try:
496
  loop.add_signal_handler(
@@ -501,7 +513,7 @@ async def start_bot():
501
  # Windows doesn't support this
502
  logger.info("Signal handlers not supported on this platform")
503
  pass
504
-
505
  try:
506
  # Just run forever until interrupted
507
  logger.info("Bot is now running and waiting for messages...")
@@ -510,31 +522,33 @@ async def start_bot():
510
  # Ensure the bot is properly shut down
511
  await shutdown("Manual")
512
 
 
513
  async def shutdown(signal_type):
514
  """Cleanup tasks tied to the service's shutdown."""
515
  global application
516
-
517
  logger.info(f"Received exit signal {signal_type}...")
518
-
519
  # Stop the bot
520
  if application:
521
  logger.info("Stopping application...")
522
  await application.updater.stop()
523
  await application.stop()
524
  await application.shutdown()
525
-
526
  # Cancel all running tasks
527
  tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
528
  for task in tasks:
529
  task.cancel()
530
-
531
  # Wait for all tasks to be cancelled
532
  if tasks:
533
  logger.info(f"Waiting for {len(tasks)} tasks to complete...")
534
  await asyncio.gather(*tasks, return_exceptions=True)
535
-
536
  logger.info("Application shutdown complete")
537
 
 
538
  async def main():
539
  """Main function to setup and run the bot."""
540
  # Setup and start the bot
@@ -542,4 +556,4 @@ async def main():
542
 
543
  # This is the entry point when run as the main process
544
  if __name__ == "__main__":
545
- asyncio.run(main())
 
40
  PROCESSED_TEXT_DIR = os.path.join(PROCESSED_DIR, 'text')
41
 
42
  # Ensure directories exist with proper permissions
43
+
44
+
45
  def ensure_directory_with_permissions(directory):
46
  """Create directory if it doesn't exist and set permissions."""
47
  try:
48
  if not os.path.exists(directory):
49
  os.makedirs(directory, exist_ok=True)
50
  # Set permissions: read/write/execute for everyone
51
+ os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG |
52
+ stat.S_IRWXO) # 0777 permissions
53
  logger.info(f"Directory ensured with permissions: {directory}")
54
  except Exception as e:
55
  logger.error(f"Error setting permissions for {directory}: {str(e)}")
56
 
57
+
58
  # Initialize directories with proper permissions
59
  ensure_directory_with_permissions(PENDING_VOICE_DIR)
60
  ensure_directory_with_permissions(PENDING_TEXT_DIR)
 
64
  # Global application variable
65
  application = None
66
 
67
+
68
  def extract_sequence_number(filename: str) -> Optional[int]:
69
  """Extract sequence number from filename, e.g., 'Sound 100.wav' -> 100."""
70
  match = re.search(r'(\d+)', filename)
 
72
  return int(match.group(1))
73
  return None
74
 
75
+
76
  def get_files_by_sequence_number():
77
  """Create dictionaries mapping sequence numbers to filenames."""
78
  voice_files = {}
79
  text_files = {}
80
+
81
  # Map voice files to sequence numbers
82
  if os.path.exists(PENDING_VOICE_DIR):
83
  for filename in os.listdir(PENDING_VOICE_DIR):
 
85
  seq_num = extract_sequence_number(filename)
86
  if seq_num is not None:
87
  voice_files[seq_num] = filename
88
+
89
  # Map text files to sequence numbers
90
  if os.path.exists(PENDING_TEXT_DIR):
91
  for filename in os.listdir(PENDING_TEXT_DIR):
 
93
  seq_num = extract_sequence_number(filename)
94
  if seq_num is not None:
95
  text_files[seq_num] = filename
96
+
97
  return voice_files, text_files
98
 
99
+
100
  async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
101
  """Send a welcome message when the /start command is issued."""
102
  logger.info(f"Start command received from user {update.effective_user.id}")
 
110
  except Exception as e:
111
  logger.error(f"Error sending start message: {e}")
112
 
113
+
114
  async def get_next_available_chunk() -> Tuple[Optional[str], Optional[str], Optional[int]]:
115
  """Find the next available audio chunk that isn't being processed.
116
  Returns (voice_filename, text_filename, sequence_number) or (None, None, None)"""
117
  voice_files, text_files = get_files_by_sequence_number()
118
+
119
  # Find sequence numbers that exist in both voice and text files
120
+ common_seq_nums = set(voice_files.keys()).intersection(
121
+ set(text_files.keys()))
122
+
123
  # Filter out sequence numbers that are already being processed
124
+ available_seq_nums = [
125
+ seq_num for seq_num in common_seq_nums if seq_num not in chunks_in_process]
126
+
127
  logger.debug(f"Available sequence numbers: {available_seq_nums}")
128
+
129
  if available_seq_nums:
130
  # Get the first available sequence number
131
  seq_num = min(available_seq_nums)
132
  return voice_files[seq_num], text_files[seq_num], seq_num
133
+
134
  logger.debug("No available chunks found")
135
  return None, None, None
136
 
137
+
138
  async def correct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
139
  """Start the correction process."""
140
+ logger.info(
141
+ f"Correct command received from user {update.effective_user.id}")
142
  user_id = update.effective_user.id
143
+
144
  # Check if user already has an active task
145
  if user_id in user_tasks:
146
  await update.message.reply_text("You already have an active correction task. Please finish it or use /cancel.")
147
  return AWAITING_CORRECTION
148
+
149
  # Ensure directory paths exist with proper permissions
150
  ensure_directory_with_permissions(PENDING_VOICE_DIR)
151
  ensure_directory_with_permissions(PENDING_TEXT_DIR)
152
+
153
  # Log directory contents for debugging
154
+ logger.debug(
155
+ f"PENDING_VOICE_DIR contents: {os.listdir(PENDING_VOICE_DIR)}")
156
  logger.debug(f"PENDING_TEXT_DIR contents: {os.listdir(PENDING_TEXT_DIR)}")
157
+
158
  # Get next available chunk
159
  voice_filename, text_filename, seq_num = await get_next_available_chunk()
160
+
161
  if not voice_filename or not text_filename:
162
  await update.message.reply_text("No pending transcriptions available at the moment. Please try again later.")
163
  return ConversationHandler.END
164
+
165
  # Mark chunk as being processed
166
  chunks_in_process.add(seq_num)
167
  user_tasks[user_id] = (voice_filename, text_filename, seq_num)
168
+
169
  # Send audio file
170
  audio_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
171
+
172
  # Check if audio file exists
173
  if not os.path.exists(audio_path):
174
  logger.error(f"Audio file not found: {audio_path}")
175
  await update.message.reply_text("Error: Audio file not found. Please try again.")
176
+
177
  # Clean up
178
  chunks_in_process.remove(seq_num)
179
  del user_tasks[user_id]
180
  return ConversationHandler.END
181
+
182
  # Read the original transcription to send as reference
183
  text_path = os.path.join(PENDING_TEXT_DIR, text_filename)
184
  if not os.path.exists(text_path):
185
  logger.error(f"Text file not found: {text_path}")
186
  await update.message.reply_text("Error: Text file not found. Please try again.")
187
+
188
  # Clean up
189
  chunks_in_process.remove(seq_num)
190
  del user_tasks[user_id]
191
  return ConversationHandler.END
192
+
193
  try:
194
  with open(text_path, 'r', encoding='utf-8') as f:
195
  original_text = f.read().strip()
196
  except Exception as e:
197
  logger.error(f"Error reading text file: {e}")
198
  await update.message.reply_text(f"Error reading transcription file. Please try again.")
199
+
200
  # Clean up
201
  chunks_in_process.remove(seq_num)
202
  del user_tasks[user_id]
203
  return ConversationHandler.END
204
+
205
  # Send audio and original text
206
  try:
207
  with open(audio_path, 'rb') as audio_file:
208
  await update.message.reply_voice(voice=audio_file)
209
+
210
  await update.message.reply_text(
211
  f"Please listen to the audio and correct the transcription.\n\n"
212
  f"Original transcription:\n{original_text}\n\n"
213
  f"Please send your corrected version."
214
  )
215
+
216
  logger.info(f"Successfully sent audio and text to user {user_id}")
217
  return AWAITING_CORRECTION
218
  except Exception as e:
219
  logger.error(f"Error sending audio or text: {e}")
220
  await update.message.reply_text("Error sending audio. Please try again.")
221
+
222
  # Clean up
223
  chunks_in_process.remove(seq_num)
224
  del user_tasks[user_id]
225
  return ConversationHandler.END
226
 
227
+
228
  async def save_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
229
+ """Save the user's correction - TEXT ONLY VERSION."""
230
  user_id = update.effective_user.id
231
+
232
  if user_id not in user_tasks:
233
  await update.message.reply_text("You don't have an active correction task. Use /correct to start one.")
234
  return ConversationHandler.END
235
+
236
  voice_filename, text_filename, seq_num = user_tasks[user_id]
237
  corrected_text = update.message.text
238
+
239
  # Get original file paths
240
  original_txt_path = os.path.join(PENDING_TEXT_DIR, text_filename)
241
  original_wav_path = os.path.join(PENDING_VOICE_DIR, voice_filename)
242
+
243
+ # Create user-specific directory in processed TEXT folder only
244
+ user_processed_text_dir = os.path.join(
245
+ PROCESSED_TEXT_DIR, f"user_{user_id}")
246
+
247
+ # Log the directory path for debugging
248
+ logger.debug(f"Creating text directory: {user_processed_text_dir}")
249
+
250
  try:
251
+ # Ensure text directory with proper permissions
 
252
  ensure_directory_with_permissions(user_processed_text_dir)
253
+
254
+ # Save corrected text to processed directory
255
+ processed_txt_path = os.path.join(
256
+ user_processed_text_dir, text_filename)
257
+
258
  # Log file paths for debugging
259
+ logger.debug(
260
+ f"Original text path: {original_txt_path}, exists: {os.path.exists(original_txt_path)}")
261
+ logger.debug(
262
+ f"Original wav path: {original_wav_path}, exists: {os.path.exists(original_wav_path)}")
263
  logger.debug(f"Processed text path: {processed_txt_path}")
264
+
265
+ # Save corrected text
 
266
  logger.debug(f"Saving corrected text to {processed_txt_path}")
267
  with open(processed_txt_path, 'w', encoding='utf-8') as f:
268
  f.write(corrected_text)
269
  # Set file permissions
270
+ os.chmod(processed_txt_path, stat.S_IRUSR | stat.S_IWUSR |
271
+ stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
272
+
273
+ # Verify processed text file exists
274
+ logger.debug(
275
+ f"Verifying processed text file exists: {os.path.exists(processed_txt_path)}")
276
+
277
+ # Remove both files from pending directory (text and voice)
 
 
 
 
 
 
 
 
 
278
  if os.path.exists(original_txt_path):
279
  logger.debug(f"Removing original text file at {original_txt_path}")
280
  os.remove(original_txt_path)
281
  else:
282
+ logger.warning(
283
+ f"Could not remove original text file as it doesn't exist at {original_txt_path}")
284
+
285
  if os.path.exists(original_wav_path):
286
  logger.debug(f"Removing original wav file at {original_wav_path}")
287
  os.remove(original_wav_path)
288
  else:
289
+ logger.warning(
290
+ f"Could not remove original wav file as it doesn't exist at {original_wav_path}")
291
+
292
  # Remove from active tasks
293
  logger.debug(f"Removing seq_num {seq_num} from chunks_in_process")
294
  chunks_in_process.remove(seq_num)
295
+
296
  logger.debug(f"Removing user {user_id} from user_tasks")
297
  del user_tasks[user_id]
298
+
299
+ logger.info(
300
+ f"Successfully saved correction for user {user_id} (text only)")
301
  await update.message.reply_text(
302
  "Thank you! Your correction has been saved.\n"
303
  "Use /correct to receive another transcription task."
304
  )
305
+
306
  return ConversationHandler.END
307
  except Exception as e:
308
  # Enhanced error logging with stack trace
309
  error_msg = f"Error saving correction: {str(e)}"
310
  logger.error(error_msg)
311
  logger.error(traceback.format_exc())
312
+
313
  # Try to provide more specific error messages
314
  if "Permission denied" in str(e):
315
  await update.message.reply_text("Error: Permission denied while saving files. Please contact the administrator.")
 
317
  await update.message.reply_text("Error: File not found. The system couldn't find one of the files.")
318
  else:
319
  await update.message.reply_text(f"Error saving your correction: {str(e)}. Please try again.")
320
+
321
  return AWAITING_CORRECTION
322
 
323
+
324
  async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
325
  """Cancel the current correction task."""
326
  user_id = update.effective_user.id
327
+
328
  if user_id in user_tasks:
329
  _, _, seq_num = user_tasks[user_id]
330
  chunks_in_process.remove(seq_num)
331
  del user_tasks[user_id]
332
+
333
  await update.message.reply_text("Task cancelled. Use /correct to start a new one.")
334
  else:
335
  await update.message.reply_text("You don't have an active task to cancel.")
336
+
337
  return ConversationHandler.END
338
 
339
+
340
  async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
341
  """Show status of pending and processed chunks."""
342
+ logger.info(
343
+ f"Status command received from user {update.effective_user.id}")
344
+
345
  # Count pending files
346
+ pending_voice_count = len(
347
+ [f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')])
348
+ pending_text_count = len(
349
+ [f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')])
350
+
351
  # Get sequence numbers
352
  voice_files, text_files = get_files_by_sequence_number()
353
+ matching_pairs = len(
354
+ set(voice_files.keys()).intersection(set(text_files.keys())))
355
+
356
+ # Count processed files (only text files now)
357
  processed_text_count = 0
358
+
 
 
 
 
359
  # Count text files
360
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
361
  processed_text_count += len([f for f in files if f.endswith('.txt')])
362
+
363
  active_tasks = len(user_tasks)
364
+
365
  # Check file permissions
366
  permissions_info = ""
367
  try:
368
+ # Check if we can write to the processed text directory
 
369
  can_write_text = os.access(PROCESSED_TEXT_DIR, os.W_OK)
370
+ permissions_info = f"\n• Write permissions: Text: {can_write_text}"
371
  except Exception as e:
372
  permissions_info = f"\n• Error checking permissions: {str(e)}"
373
+
374
  await update.message.reply_text(
375
  f"📊 Transcription Status:\n"
376
  f"• Pending voice files: {pending_voice_count}\n"
377
  f"• Pending text files: {pending_text_count}\n"
378
  f"• Matching pending pairs: {matching_pairs}\n"
 
379
  f"• Processed text files: {processed_text_count}\n"
380
+ f"• Active tasks: {active_tasks}{permissions_info}\n"
381
+ f"• Note: Only corrected text files are saved, voice files are not copied to processed folder"
382
  )
383
 
384
+
385
  async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
386
  """Command to show debug information about directories and permissions."""
387
  user_id = update.effective_user.id
388
+
389
  # Check directory structure
390
  debug_info = [
391
  "📁 Directory Check:",
 
394
  f"• PENDING_VOICE_DIR: {PENDING_VOICE_DIR}, exists: {os.path.exists(PENDING_VOICE_DIR)}",
395
  f"• PENDING_TEXT_DIR: {PENDING_TEXT_DIR}, exists: {os.path.exists(PENDING_TEXT_DIR)}",
396
  f"• PROCESSED_DIR: {PROCESSED_DIR}, exists: {os.path.exists(PROCESSED_DIR)}",
 
397
  f"• PROCESSED_TEXT_DIR: {PROCESSED_TEXT_DIR}, exists: {os.path.exists(PROCESSED_TEXT_DIR)}",
398
  ]
399
+
400
  # Check permissions
401
  try:
402
  permission_info = [
403
  "🔑 Permission Check:",
404
  f"• PENDING_VOICE_DIR writable: {os.access(PENDING_VOICE_DIR, os.W_OK)}",
405
  f"• PENDING_TEXT_DIR writable: {os.access(PENDING_TEXT_DIR, os.W_OK)}",
 
406
  f"• PROCESSED_TEXT_DIR writable: {os.access(PROCESSED_TEXT_DIR, os.W_OK)}",
407
  ]
408
  debug_info.extend(permission_info)
409
  except Exception as e:
410
  debug_info.append(f"Error checking permissions: {str(e)}")
411
+
412
  # Check active tasks
413
  task_info = [
414
  "📋 Active Tasks:",
 
416
  f"• Active chunks: {len(chunks_in_process)}",
417
  ]
418
  debug_info.extend(task_info)
419
+
420
  await update.message.reply_text("\n".join(debug_info))
421
 
422
+
423
  async def test_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
424
  """Test handler to verify bot is receiving messages."""
425
  logger.info(f"Test command received from user {update.effective_user.id}")
426
  await update.message.reply_text("Bot is working! ✅")
427
 
428
+
429
  async def setup_application():
430
  """Setup the Application with all handlers."""
431
  global application
432
+
433
  # Get the token from environment variable
434
  token = os.environ.get("TELEGRAM_TOKEN")
435
  if not token:
436
  logger.error("TELEGRAM_TOKEN environment variable not set!")
437
  return None
438
+
439
  logger.info(f"Setting up bot with token: {token[:10]}...")
440
+
441
  # Create the Application with additional debugging
442
  application = Application.builder().token(token).build()
443
+
444
  # Add a test handler first
445
  application.add_handler(CommandHandler("test", test_handler))
446
+
447
  # Add conversation handler for correction workflow
448
  conv_handler = ConversationHandler(
449
  entry_points=[CommandHandler("correct", correct)],
450
  states={
451
  AWAITING_CORRECTION: [
452
+ MessageHandler(filters.TEXT & ~filters.COMMAND,
453
+ save_correction),
454
  ],
455
  },
456
  fallbacks=[CommandHandler("cancel", cancel)],
457
  name="correction_conversation",
458
  )
459
+
460
  application.add_handler(conv_handler)
461
+
462
  # Add command handlers
463
  application.add_handler(CommandHandler("start", start))
464
  application.add_handler(CommandHandler("status", status))
465
  application.add_handler(CommandHandler("cancel", cancel))
466
+ application.add_handler(CommandHandler(
467
+ "debug", debug_info)) # New debug command
468
+
469
  logger.info("All handlers added successfully")
470
  return application
471
 
472
+
473
  async def start_bot():
474
  """Start the bot with proper signal handling."""
475
  global application
476
+
477
  logger.info("Starting bot setup...")
478
+
479
  # Create and configure the bot
480
  app = await setup_application()
481
  if not app:
482
  logger.error("Failed to create application")
483
  return
484
+
485
  logger.info("Application created successfully")
486
+
487
  # Start the Bot
488
  try:
489
  await app.initialize()
490
  logger.info("Application initialized")
491
+
492
  await app.start()
493
  logger.info("Application started")
494
+
495
  # Start polling for updates
496
  await app.updater.start_polling(drop_pending_updates=False)
497
  logger.info("Started polling for updates")
498
+
499
  except Exception as e:
500
  logger.error(f"Error starting bot: {e}")
501
  return
502
+
503
  # Setup signal handlers for graceful shutdown
504
  loop = asyncio.get_event_loop()
505
+
506
  for signal_name in ('SIGINT', 'SIGTERM'):
507
  try:
508
  loop.add_signal_handler(
 
513
  # Windows doesn't support this
514
  logger.info("Signal handlers not supported on this platform")
515
  pass
516
+
517
  try:
518
  # Just run forever until interrupted
519
  logger.info("Bot is now running and waiting for messages...")
 
522
  # Ensure the bot is properly shut down
523
  await shutdown("Manual")
524
 
525
+
526
  async def shutdown(signal_type):
527
  """Cleanup tasks tied to the service's shutdown."""
528
  global application
529
+
530
  logger.info(f"Received exit signal {signal_type}...")
531
+
532
  # Stop the bot
533
  if application:
534
  logger.info("Stopping application...")
535
  await application.updater.stop()
536
  await application.stop()
537
  await application.shutdown()
538
+
539
  # Cancel all running tasks
540
  tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
541
  for task in tasks:
542
  task.cancel()
543
+
544
  # Wait for all tasks to be cancelled
545
  if tasks:
546
  logger.info(f"Waiting for {len(tasks)} tasks to complete...")
547
  await asyncio.gather(*tasks, return_exceptions=True)
548
+
549
  logger.info("Application shutdown complete")
550
 
551
+
552
  async def main():
553
  """Main function to setup and run the bot."""
554
  # Setup and start the bot
 
556
 
557
  # This is the entry point when run as the main process
558
  if __name__ == "__main__":
559
+ asyncio.run(main())
main.py CHANGED
@@ -50,23 +50,23 @@ except Exception as e:
50
  """Create dictionaries mapping sequence numbers to filenames."""
51
  voice_files = {}
52
  text_files = {}
53
-
54
  # Map voice files to sequence numbers
55
  for filename in os.listdir(PENDING_VOICE_DIR):
56
  if filename.endswith('.wav'):
57
  seq_num = extract_sequence_number(filename)
58
  if seq_num is not None:
59
  voice_files[seq_num] = filename
60
-
61
  # Map text files to sequence numbers
62
  for filename in os.listdir(PENDING_TEXT_DIR):
63
  if filename.endswith('.txt'):
64
  seq_num = extract_sequence_number(filename)
65
  if seq_num is not None:
66
  text_files[seq_num] = filename
67
-
68
  return voice_files, text_files
69
-
70
  logging.error(f"Error importing from app.py: {str(e)}")
71
 
72
  # Enable logging
@@ -87,6 +87,8 @@ bot_running = False
87
  bot_start_time = None
88
 
89
  # Function to start the bot in a separate process instead of a thread
 
 
90
  def start_bot_process():
91
  from multiprocessing import Process
92
  global bot_process, bot_running
@@ -98,20 +100,21 @@ def start_bot_process():
98
  bot_running = True
99
  return bot_process.pid
100
 
 
101
  def run_bot_process():
102
  """Function that runs in a separate process to start the bot"""
103
  import sys
104
  import os
105
  import asyncio
106
-
107
  try:
108
  # Import and run the main function from app.py
109
  from app import main
110
-
111
  # Create a new event loop for this process
112
  loop = asyncio.new_event_loop()
113
  asyncio.set_event_loop(loop)
114
-
115
  # Run the bot
116
  loop.run_until_complete(main())
117
  loop.run_forever()
@@ -119,21 +122,23 @@ def run_bot_process():
119
  sys.stderr.write(f"Error in bot process: {str(e)}\n")
120
  sys.exit(1)
121
 
 
122
  @app.get("/", response_class=HTMLResponse)
123
  async def get_root(request: Request):
124
  return templates.TemplateResponse("index.html", {"request": request, "bot_running": bot_running})
125
 
 
126
  @app.post("/start-bot")
127
  async def start_bot_endpoint(background_tasks: BackgroundTasks):
128
  global bot_running, bot_start_time
129
-
130
  if bot_running:
131
  return {"status": "error", "message": "Bot is already running"}
132
-
133
  # Check if TELEGRAM_TOKEN is set
134
  if not os.environ.get("TELEGRAM_TOKEN"):
135
  return {"status": "error", "message": "TELEGRAM_TOKEN environment variable is not set. Please configure it in Hugging Face Space settings."}
136
-
137
  try:
138
  # Start bot in a separate process
139
  pid = start_bot_process()
@@ -143,23 +148,24 @@ async def start_bot_endpoint(background_tasks: BackgroundTasks):
143
  logger.error(f"Error starting bot: {str(e)}")
144
  return {"status": "error", "message": f"Failed to start bot: {str(e)}"}
145
 
 
146
  @app.post("/stop-bot")
147
  async def stop_bot_endpoint():
148
  global bot_process, bot_running, bot_start_time
149
-
150
  if not bot_running or bot_process is None:
151
  return {"status": "error", "message": "Bot is not running"}
152
-
153
  try:
154
  # Terminate the process
155
  bot_process.terminate()
156
  bot_process.join(timeout=5) # Wait for process to terminate
157
-
158
  # If process didn't terminate, force kill it
159
  if bot_process.is_alive():
160
  bot_process.kill()
161
  bot_process.join()
162
-
163
  bot_running = False
164
  bot_start_time = None
165
  return {"status": "success", "message": "Bot stopped successfully"}
@@ -167,58 +173,50 @@ async def stop_bot_endpoint():
167
  logger.error(f"Error stopping bot: {str(e)}")
168
  return {"status": "error", "message": f"Failed to stop bot: {str(e)}"}
169
 
 
170
  @app.get("/status")
171
  async def get_status():
172
  # Check if directories exist and create if needed
173
  os.makedirs(PENDING_VOICE_DIR, exist_ok=True)
174
  os.makedirs(PENDING_TEXT_DIR, exist_ok=True)
175
- os.makedirs(PROCESSED_VOICE_DIR, exist_ok=True)
176
  os.makedirs(PROCESSED_TEXT_DIR, exist_ok=True)
177
 
178
  # Count pending files
179
- pending_voice_count = len([f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')])
180
- pending_text_count = len([f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')])
181
-
 
 
182
  # Get sequence numbers
183
  voice_files, text_files = get_files_by_sequence_number()
184
- matching_pairs = len(set(voice_files.keys()).intersection(set(text_files.keys())))
185
-
186
- # Count processed files
187
- processed_voice_count = 0
 
188
  processed_text_count = 0
189
-
190
  # Count processed files by user
191
  user_stats = {}
192
-
193
- # Process voice files
194
- for root, dirs, files in os.walk(PROCESSED_VOICE_DIR):
195
- wav_files = [f for f in files if f.endswith('.wav')]
196
- processed_voice_count += len(wav_files)
197
-
198
- # Get user ID from directory path
199
- if os.path.basename(root).startswith("user_"):
200
- user_id = os.path.basename(root)
201
- if user_id not in user_stats:
202
- user_stats[user_id] = {"voice": 0, "text": 0}
203
- user_stats[user_id]["voice"] += len(wav_files)
204
-
205
- # Process text files
206
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
207
  txt_files = [f for f in files if f.endswith('.txt')]
208
  processed_text_count += len(txt_files)
209
-
210
  # Get user ID from directory path
211
  if os.path.basename(root).startswith("user_"):
212
  user_id = os.path.basename(root)
213
  if user_id not in user_stats:
214
  user_stats[user_id] = {"voice": 0, "text": 0}
215
  user_stats[user_id]["text"] += len(txt_files)
216
-
217
  # Calculate uptime if bot is running
218
  uptime = None
219
  if bot_running and bot_start_time:
220
  uptime = int(time.time() - bot_start_time)
221
-
222
  return {
223
  "bot_running": bot_running,
224
  "uptime_seconds": uptime,
@@ -231,38 +229,35 @@ async def get_status():
231
  "is_huggingface": True
232
  }
233
 
 
234
  @app.get("/list-users")
235
  async def list_users():
236
  users = set()
237
-
238
- # Get users from voice directories
239
- for item in os.listdir(PROCESSED_VOICE_DIR):
240
- if item.startswith("user_"):
241
- users.add(item)
242
-
243
- # Get users from text directories
244
  for item in os.listdir(PROCESSED_TEXT_DIR):
245
  if item.startswith("user_"):
246
  users.add(item)
247
-
248
  return {"users": sorted(list(users))}
249
 
 
250
  @app.get("/download/{user_id}")
251
  async def download_processed_data(user_id: str):
252
  if not user_id.startswith("user_"):
253
  raise HTTPException(status_code=400, detail="Invalid user ID format")
254
-
255
- # Check if user exists
256
- voice_dir = os.path.join(PROCESSED_VOICE_DIR, user_id)
257
  text_dir = os.path.join(PROCESSED_TEXT_DIR, user_id)
258
-
259
- if not os.path.exists(voice_dir) and not os.path.exists(text_dir):
260
- raise HTTPException(status_code=404, detail=f"No data found for user {user_id}")
261
-
 
262
  # Create a zip file in memory
263
  zip_buffer = BytesIO()
264
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
265
- # Add text files
266
  if os.path.exists(text_dir):
267
  for file_name in os.listdir(text_dir):
268
  file_path = os.path.join(text_dir, file_name)
@@ -270,19 +265,10 @@ async def download_processed_data(user_id: str):
270
  # Read the file and add it to the zip
271
  with open(file_path, 'rb') as f:
272
  zip_file.writestr(f"text/{file_name}", f.read())
273
-
274
- # Add voice files
275
- if os.path.exists(voice_dir):
276
- for file_name in os.listdir(voice_dir):
277
- file_path = os.path.join(voice_dir, file_name)
278
- if os.path.isfile(file_path) and file_name.endswith('.wav'):
279
- # Read the file and add it to the zip
280
- with open(file_path, 'rb') as f:
281
- zip_file.writestr(f"voice/{file_name}", f.read())
282
-
283
  # Reset buffer position
284
  zip_buffer.seek(0)
285
-
286
  # Return the zip file as a response
287
  return StreamingResponse(
288
  zip_buffer,
@@ -292,12 +278,13 @@ async def download_processed_data(user_id: str):
292
  }
293
  )
294
 
 
295
  @app.get("/download-all")
296
  async def download_all_processed_data():
297
  # Create a zip file in memory
298
  zip_buffer = BytesIO()
299
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
300
- # Add all processed text files
301
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
302
  for file_name in files:
303
  if file_name.endswith('.txt'):
@@ -307,21 +294,10 @@ async def download_all_processed_data():
307
  # Read the file and add it to the zip
308
  with open(file_path, 'rb') as f:
309
  zip_file.writestr(f"text/{rel_path}", f.read())
310
-
311
- # Add all processed voice files
312
- for root, dirs, files in os.walk(PROCESSED_VOICE_DIR):
313
- for file_name in files:
314
- if file_name.endswith('.wav'):
315
- file_path = os.path.join(root, file_name)
316
- # Get the relative path from the PROCESSED_VOICE_DIR
317
- rel_path = os.path.relpath(file_path, PROCESSED_VOICE_DIR)
318
- # Read the file and add it to the zip
319
- with open(file_path, 'rb') as f:
320
- zip_file.writestr(f"voice/{rel_path}", f.read())
321
-
322
  # Reset buffer position
323
  zip_buffer.seek(0)
324
-
325
  # Return the zip file as a response
326
  return StreamingResponse(
327
  zip_buffer,
@@ -331,11 +307,12 @@ async def download_all_processed_data():
331
  }
332
  )
333
 
 
334
  @app.on_event("startup")
335
  async def setup_app():
336
  # Ensure templates directory exists
337
  os.makedirs("templates", exist_ok=True)
338
-
339
  # Only create index.html if it doesn't exist
340
  if not os.path.exists("templates/index.html"):
341
  # Create a simple default template if needed
@@ -402,6 +379,13 @@ async def setup_app():
402
  padding: 8px;
403
  border-bottom: 1px solid #eee;
404
  }
 
 
 
 
 
 
 
405
  </style>
406
  </head>
407
  <body>
@@ -417,6 +401,9 @@ async def setup_app():
417
 
418
  <div class="container">
419
  <h2>Status</h2>
 
 
 
420
  <div id="statusInfo" class="status">Loading status...</div>
421
  </div>
422
 
@@ -427,8 +414,8 @@ async def setup_app():
427
 
428
  <div class="container">
429
  <h2>Data Download</h2>
430
- <p>Download all processed data or data for specific users.</p>
431
- <button onclick="downloadAll()">Download All Data</button>
432
  </div>
433
 
434
  <script>
@@ -486,10 +473,6 @@ async def setup_app():
486
  <td>Matching Pending Pairs</td>
487
  <td>${data.matching_pairs}</td>
488
  </tr>
489
- <tr>
490
- <td>Processed Voice Files</td>
491
- <td>${data.processed_voice_count}</td>
492
- </tr>
493
  <tr>
494
  <td>Processed Text Files</td>
495
  <td>${data.processed_text_count}</td>
@@ -522,7 +505,7 @@ async def setup_app():
522
  usersHtml += `
523
  <div class="user-item">
524
  <span>${user}</span>
525
- <button onclick="downloadUser('${user}')">Download Data</button>
526
  </div>
527
  `;
528
  });
@@ -583,4 +566,4 @@ async def setup_app():
583
 
584
  # Entrypoint for running the server
585
  if __name__ == "__main__":
586
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
50
  """Create dictionaries mapping sequence numbers to filenames."""
51
  voice_files = {}
52
  text_files = {}
53
+
54
  # Map voice files to sequence numbers
55
  for filename in os.listdir(PENDING_VOICE_DIR):
56
  if filename.endswith('.wav'):
57
  seq_num = extract_sequence_number(filename)
58
  if seq_num is not None:
59
  voice_files[seq_num] = filename
60
+
61
  # Map text files to sequence numbers
62
  for filename in os.listdir(PENDING_TEXT_DIR):
63
  if filename.endswith('.txt'):
64
  seq_num = extract_sequence_number(filename)
65
  if seq_num is not None:
66
  text_files[seq_num] = filename
67
+
68
  return voice_files, text_files
69
+
70
  logging.error(f"Error importing from app.py: {str(e)}")
71
 
72
  # Enable logging
 
87
  bot_start_time = None
88
 
89
  # Function to start the bot in a separate process instead of a thread
90
+
91
+
92
  def start_bot_process():
93
  from multiprocessing import Process
94
  global bot_process, bot_running
 
100
  bot_running = True
101
  return bot_process.pid
102
 
103
+
104
  def run_bot_process():
105
  """Function that runs in a separate process to start the bot"""
106
  import sys
107
  import os
108
  import asyncio
109
+
110
  try:
111
  # Import and run the main function from app.py
112
  from app import main
113
+
114
  # Create a new event loop for this process
115
  loop = asyncio.new_event_loop()
116
  asyncio.set_event_loop(loop)
117
+
118
  # Run the bot
119
  loop.run_until_complete(main())
120
  loop.run_forever()
 
122
  sys.stderr.write(f"Error in bot process: {str(e)}\n")
123
  sys.exit(1)
124
 
125
+
126
  @app.get("/", response_class=HTMLResponse)
127
  async def get_root(request: Request):
128
  return templates.TemplateResponse("index.html", {"request": request, "bot_running": bot_running})
129
 
130
+
131
  @app.post("/start-bot")
132
  async def start_bot_endpoint(background_tasks: BackgroundTasks):
133
  global bot_running, bot_start_time
134
+
135
  if bot_running:
136
  return {"status": "error", "message": "Bot is already running"}
137
+
138
  # Check if TELEGRAM_TOKEN is set
139
  if not os.environ.get("TELEGRAM_TOKEN"):
140
  return {"status": "error", "message": "TELEGRAM_TOKEN environment variable is not set. Please configure it in Hugging Face Space settings."}
141
+
142
  try:
143
  # Start bot in a separate process
144
  pid = start_bot_process()
 
148
  logger.error(f"Error starting bot: {str(e)}")
149
  return {"status": "error", "message": f"Failed to start bot: {str(e)}"}
150
 
151
+
152
  @app.post("/stop-bot")
153
  async def stop_bot_endpoint():
154
  global bot_process, bot_running, bot_start_time
155
+
156
  if not bot_running or bot_process is None:
157
  return {"status": "error", "message": "Bot is not running"}
158
+
159
  try:
160
  # Terminate the process
161
  bot_process.terminate()
162
  bot_process.join(timeout=5) # Wait for process to terminate
163
+
164
  # If process didn't terminate, force kill it
165
  if bot_process.is_alive():
166
  bot_process.kill()
167
  bot_process.join()
168
+
169
  bot_running = False
170
  bot_start_time = None
171
  return {"status": "success", "message": "Bot stopped successfully"}
 
173
  logger.error(f"Error stopping bot: {str(e)}")
174
  return {"status": "error", "message": f"Failed to stop bot: {str(e)}"}
175
 
176
+
177
  @app.get("/status")
178
  async def get_status():
179
  # Check if directories exist and create if needed
180
  os.makedirs(PENDING_VOICE_DIR, exist_ok=True)
181
  os.makedirs(PENDING_TEXT_DIR, exist_ok=True)
182
+ os.makedirs(PROCESSED_VOICE_DIR, exist_ok=True)
183
  os.makedirs(PROCESSED_TEXT_DIR, exist_ok=True)
184
 
185
  # Count pending files
186
+ pending_voice_count = len(
187
+ [f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')])
188
+ pending_text_count = len(
189
+ [f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')])
190
+
191
  # Get sequence numbers
192
  voice_files, text_files = get_files_by_sequence_number()
193
+ matching_pairs = len(
194
+ set(voice_files.keys()).intersection(set(text_files.keys())))
195
+
196
+ # Count processed files - Only count text files now as voice files are no longer saved to processed
197
+ processed_voice_count = 0 # Set to 0 since we no longer save voice files
198
  processed_text_count = 0
199
+
200
  # Count processed files by user
201
  user_stats = {}
202
+
203
+ # Process text files only (voice files are no longer saved to processed directory)
 
 
 
 
 
 
 
 
 
 
 
 
204
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
205
  txt_files = [f for f in files if f.endswith('.txt')]
206
  processed_text_count += len(txt_files)
207
+
208
  # Get user ID from directory path
209
  if os.path.basename(root).startswith("user_"):
210
  user_id = os.path.basename(root)
211
  if user_id not in user_stats:
212
  user_stats[user_id] = {"voice": 0, "text": 0}
213
  user_stats[user_id]["text"] += len(txt_files)
214
+
215
  # Calculate uptime if bot is running
216
  uptime = None
217
  if bot_running and bot_start_time:
218
  uptime = int(time.time() - bot_start_time)
219
+
220
  return {
221
  "bot_running": bot_running,
222
  "uptime_seconds": uptime,
 
229
  "is_huggingface": True
230
  }
231
 
232
+
233
  @app.get("/list-users")
234
  async def list_users():
235
  users = set()
236
+
237
+ # Get users from text directories only (since voice files are no longer saved)
 
 
 
 
 
238
  for item in os.listdir(PROCESSED_TEXT_DIR):
239
  if item.startswith("user_"):
240
  users.add(item)
241
+
242
  return {"users": sorted(list(users))}
243
 
244
+
245
  @app.get("/download/{user_id}")
246
  async def download_processed_data(user_id: str):
247
  if not user_id.startswith("user_"):
248
  raise HTTPException(status_code=400, detail="Invalid user ID format")
249
+
250
+ # Check if user exists - only check text directory now
 
251
  text_dir = os.path.join(PROCESSED_TEXT_DIR, user_id)
252
+
253
+ if not os.path.exists(text_dir):
254
+ raise HTTPException(
255
+ status_code=404, detail=f"No data found for user {user_id}")
256
+
257
  # Create a zip file in memory
258
  zip_buffer = BytesIO()
259
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
260
+ # Add text files only
261
  if os.path.exists(text_dir):
262
  for file_name in os.listdir(text_dir):
263
  file_path = os.path.join(text_dir, file_name)
 
265
  # Read the file and add it to the zip
266
  with open(file_path, 'rb') as f:
267
  zip_file.writestr(f"text/{file_name}", f.read())
268
+
 
 
 
 
 
 
 
 
 
269
  # Reset buffer position
270
  zip_buffer.seek(0)
271
+
272
  # Return the zip file as a response
273
  return StreamingResponse(
274
  zip_buffer,
 
278
  }
279
  )
280
 
281
+
282
  @app.get("/download-all")
283
  async def download_all_processed_data():
284
  # Create a zip file in memory
285
  zip_buffer = BytesIO()
286
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
287
+ # Add all processed text files only
288
  for root, dirs, files in os.walk(PROCESSED_TEXT_DIR):
289
  for file_name in files:
290
  if file_name.endswith('.txt'):
 
294
  # Read the file and add it to the zip
295
  with open(file_path, 'rb') as f:
296
  zip_file.writestr(f"text/{rel_path}", f.read())
297
+
 
 
 
 
 
 
 
 
 
 
 
298
  # Reset buffer position
299
  zip_buffer.seek(0)
300
+
301
  # Return the zip file as a response
302
  return StreamingResponse(
303
  zip_buffer,
 
307
  }
308
  )
309
 
310
+
311
  @app.on_event("startup")
312
  async def setup_app():
313
  # Ensure templates directory exists
314
  os.makedirs("templates", exist_ok=True)
315
+
316
  # Only create index.html if it doesn't exist
317
  if not os.path.exists("templates/index.html"):
318
  # Create a simple default template if needed
 
379
  padding: 8px;
380
  border-bottom: 1px solid #eee;
381
  }
382
+ .note {
383
+ background-color: #f0f8ff;
384
+ border-left: 4px solid #1e90ff;
385
+ padding: 10px;
386
+ margin: 10px 0;
387
+ font-style: italic;
388
+ }
389
  </style>
390
  </head>
391
  <body>
 
401
 
402
  <div class="container">
403
  <h2>Status</h2>
404
+ <div class="note">
405
+ Note: Only corrected text files are saved to the processed directory. Voice files are removed after processing to save storage space.
406
+ </div>
407
  <div id="statusInfo" class="status">Loading status...</div>
408
  </div>
409
 
 
414
 
415
  <div class="container">
416
  <h2>Data Download</h2>
417
+ <p>Download all processed corrected text files or data for specific users.</p>
418
+ <button onclick="downloadAll()">Download All Text Data</button>
419
  </div>
420
 
421
  <script>
 
473
  <td>Matching Pending Pairs</td>
474
  <td>${data.matching_pairs}</td>
475
  </tr>
 
 
 
 
476
  <tr>
477
  <td>Processed Text Files</td>
478
  <td>${data.processed_text_count}</td>
 
505
  usersHtml += `
506
  <div class="user-item">
507
  <span>${user}</span>
508
+ <button onclick="downloadUser('${user}')">Download Text Data</button>
509
  </div>
510
  `;
511
  });
 
566
 
567
  # Entrypoint for running the server
568
  if __name__ == "__main__":
569
+ uvicorn.run(app, host="0.0.0.0", port=7860)