danicor commited on
Commit
dc42d7e
·
verified ·
1 Parent(s): f25a1f1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +551 -360
app.py CHANGED
@@ -1,6 +1,8 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
2
  from fastapi.responses import JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
 
 
4
  import whisper
5
  import torch
6
  import tempfile
@@ -10,49 +12,84 @@ import logging
10
  import hashlib
11
  import json
12
  import sqlite3
13
- from datetime import datetime
14
  import threading
15
  import time
16
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
 
 
 
17
 
18
- logging.basicConfig(level=logging.INFO)
19
- logger = logging.getLogger(__name__)
20
-
21
- app = FastAPI()
22
-
23
- app.add_middleware(
24
- CORSMiddleware,
25
- allow_origins=["*"],
26
- allow_credentials=True,
27
- allow_methods=["*"],
28
- allow_headers=["*"],
29
  )
 
30
 
31
- device = "cuda" if torch.cuda.is_available() else "cpu"
32
- logger.info(f"Loading Whisper model on {device}")
33
- whisper_model = whisper.load_model("large-v3", device=device)
34
- logger.info("Whisper model loaded successfully")
35
-
36
- logger.info("Loading translation model...")
37
  translation_tokenizer = None
38
  translation_model = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- def load_translation_model():
41
- global translation_tokenizer, translation_model
42
  try:
 
43
  model_name = "facebook/nllb-200-distilled-600M"
44
  translation_tokenizer = AutoTokenizer.from_pretrained(model_name)
45
  translation_model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
46
- if device == "cuda" and torch.cuda.is_available():
47
  translation_model = translation_model.to(device)
48
  logger.info(f"Translation model loaded on {device}")
49
- return True
50
  except Exception as e:
51
- logger.error(f"Failed to load translation model: {e}")
52
- return False
53
 
54
- translation_loaded = load_translation_model()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
 
56
  LANGUAGE_CODES = {
57
  'persian': 'pes_Arab',
58
  'farsi': 'pes_Arab',
@@ -76,160 +113,197 @@ LANGUAGE_CODES = {
76
  'finnish': 'fin_Latn'
77
  }
78
 
79
- def translate_text(text, target_language):
80
- if not translation_loaded or not translation_model or not translation_tokenizer:
81
- return None
82
 
83
- try:
84
- target_code = LANGUAGE_CODES.get(target_language.lower())
85
- if not target_code:
86
- return None
 
 
 
 
87
 
88
- inputs = translation_tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
89
- if device == "cuda" and torch.cuda.is_available():
90
- inputs = {k: v.to(device) for k, v in inputs.items()}
91
-
92
- translated_tokens = translation_model.generate(
93
- **inputs,
94
- forced_bos_token_id=translation_tokenizer.lang_code_to_id[target_code],
95
- max_length=512,
96
- num_beams=5,
97
- early_stopping=True
98
- )
99
-
100
- translated_text = translation_tokenizer.batch_decode(translated_tokens, skip_special_tokens=True)[0]
101
- return translated_text.strip()
102
-
103
- except Exception as e:
104
- logger.error(f"Translation error: {e}")
105
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  def init_cache_db():
108
- conn = sqlite3.connect('transcription_cache.db')
109
- cursor = conn.cursor()
110
- cursor.execute('''
111
- CREATE TABLE IF NOT EXISTS cache (
112
- id INTEGER PRIMARY KEY AUTOINCREMENT,
113
- file_hash TEXT UNIQUE,
114
- filename TEXT,
115
- file_size INTEGER,
116
- transcription TEXT,
117
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
118
- )
119
- ''')
120
-
121
- cursor.execute('''
122
- CREATE TABLE IF NOT EXISTS processing_status (
123
- id INTEGER PRIMARY KEY AUTOINCREMENT,
124
- file_hash TEXT UNIQUE,
125
- filename TEXT,
126
- file_size INTEGER,
127
- status TEXT DEFAULT 'processing',
128
- progress INTEGER DEFAULT 0,
129
- estimated_time INTEGER DEFAULT 0,
130
- started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
131
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
132
- )
133
- ''')
134
-
135
- cursor.execute('''
136
- CREATE TABLE IF NOT EXISTS translation_cache (
137
- id INTEGER PRIMARY KEY AUTOINCREMENT,
138
- text_hash TEXT,
139
- target_language TEXT,
140
- translated_text TEXT,
141
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
142
- UNIQUE(text_hash, target_language)
143
- )
144
- ''')
145
-
146
- conn.commit()
147
- conn.close()
148
 
149
- def calculate_file_hash(content, filename, file_size):
 
150
  hash_input = f"{filename}_{file_size}_{len(content)}"
151
- file_hash = hashlib.md5(content[:1024] + content[-1024:] + hash_input.encode()).hexdigest()
152
- return file_hash
153
 
154
- def calculate_text_hash(text):
 
155
  return hashlib.md5(text.encode('utf-8')).hexdigest()
156
 
157
- def get_from_cache(file_hash):
 
158
  try:
159
- conn = sqlite3.connect('transcription_cache.db')
160
- cursor = conn.cursor()
161
- cursor.execute('SELECT transcription FROM cache WHERE file_hash = ?', (file_hash,))
162
- result = cursor.fetchone()
163
- conn.close()
164
- return result[0] if result else None
165
- except:
 
 
 
 
 
 
 
 
 
 
 
 
166
  return None
167
 
168
- def get_translation_from_cache(text_hash, target_language):
 
169
  try:
170
- conn = sqlite3.connect('transcription_cache.db')
171
- cursor = conn.cursor()
172
- cursor.execute('SELECT translated_text FROM translation_cache WHERE text_hash = ? AND target_language = ?',
173
- (text_hash, target_language))
174
- result = cursor.fetchone()
175
- conn.close()
176
- return result[0] if result else None
177
- except:
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  return None
179
 
180
- def save_to_cache(file_hash, filename, file_size, transcription):
 
181
  try:
182
- conn = sqlite3.connect('transcription_cache.db')
183
- cursor = conn.cursor()
184
- cursor.execute('''
185
- INSERT OR REPLACE INTO cache (file_hash, filename, file_size, transcription)
186
- VALUES (?, ?, ?, ?)
187
- ''', (file_hash, filename, file_size, transcription))
188
- conn.commit()
189
- conn.close()
 
190
  except Exception as e:
191
  logger.error(f"Error saving to cache: {e}")
192
 
193
- def save_translation_to_cache(text_hash, target_language, translated_text):
 
194
  try:
195
- conn = sqlite3.connect('transcription_cache.db')
196
- cursor = conn.cursor()
197
- cursor.execute('''
198
- INSERT OR REPLACE INTO translation_cache (text_hash, target_language, translated_text)
199
- VALUES (?, ?, ?)
200
- ''', (text_hash, target_language, translated_text))
201
- conn.commit()
202
- conn.close()
 
203
  except Exception as e:
204
  logger.error(f"Error saving translation to cache: {e}")
205
 
206
- def get_processing_status(file_hash):
 
207
  try:
208
- conn = sqlite3.connect('transcription_cache.db')
209
- cursor = conn.cursor()
210
- cursor.execute('''
211
- SELECT status, progress, estimated_time,
212
  (julianday('now') - julianday(started_at)) * 24 * 60 as elapsed_minutes
213
- FROM processing_status WHERE file_hash = ?
214
- ''', (file_hash,))
215
- result = cursor.fetchone()
216
- conn.close()
217
- if result:
218
- return {
219
- 'status': result[0],
220
- 'progress': result[1],
221
- 'estimated_time': result[2],
222
- 'elapsed_minutes': int(result[3])
223
- }
 
224
  return None
225
- except:
 
226
  return None
227
 
228
- def update_processing_status(file_hash, status=None, progress=None, estimated_time=None):
 
229
  try:
230
- conn = sqlite3.connect('transcription_cache.db')
231
- cursor = conn.cursor()
232
-
233
  updates = []
234
  params = []
235
 
@@ -247,70 +321,82 @@ def update_processing_status(file_hash, status=None, progress=None, estimated_ti
247
  params.append(file_hash)
248
 
249
  query = f"UPDATE processing_status SET {', '.join(updates)} WHERE file_hash = ?"
250
- cursor.execute(query, params)
251
- conn.commit()
252
- conn.close()
 
 
253
  except Exception as e:
254
  logger.error(f"Error updating status: {e}")
255
 
256
- def add_processing_status(file_hash, filename, file_size, estimated_time):
 
257
  try:
258
- conn = sqlite3.connect('transcription_cache.db')
259
- cursor = conn.cursor()
260
- cursor.execute('''
261
- INSERT OR REPLACE INTO processing_status
262
- (file_hash, filename, file_size, status, progress, estimated_time)
263
- VALUES (?, ?, ?, 'processing', 0, ?)
264
- ''', (file_hash, filename, file_size, estimated_time))
265
- conn.commit()
266
- conn.close()
267
  except Exception as e:
268
  logger.error(f"Error adding processing status: {e}")
269
 
270
- def remove_processing_status(file_hash):
 
271
  try:
272
- conn = sqlite3.connect('transcription_cache.db')
273
- cursor = conn.cursor()
274
- cursor.execute('DELETE FROM processing_status WHERE file_hash = ?', (file_hash,))
275
- conn.commit()
276
- conn.close()
 
 
277
  except Exception as e:
278
  logger.error(f"Error removing processing status: {e}")
279
 
280
- def estimate_processing_time(file_size_mb):
281
- estimated_seconds = file_size_mb * 30
282
- return int(estimated_seconds / 60) + 1
 
283
 
284
- def background_transcription(file_path, file_hash, filename, file_size, translate_to_english=False):
 
285
  try:
286
  logger.info(f"Starting background transcription for {filename}")
287
 
288
- update_processing_status(file_hash, status='processing', progress=10)
289
 
 
290
  result = whisper_model.transcribe(
291
  file_path,
292
- fp16=False if device == "cpu" else True,
293
  language=None,
294
  task="transcribe",
295
  verbose=False,
296
  word_timestamps=False
297
  )
298
 
299
- update_processing_status(file_hash, progress=60)
300
 
301
- text = result["text"].strip()
302
- if not text:
303
- text = "No text detected"
304
-
305
- response_data = {"text": text, "from_cache": False}
306
 
307
- if translate_to_english and result.get("language") != "en":
308
- update_processing_status(file_hash, progress=80)
309
- logger.info("Background: Translating to English...")
 
 
 
 
 
 
310
 
311
  english_result = whisper_model.transcribe(
312
  file_path,
313
- fp16=False if device == "cpu" else True,
314
  language=None,
315
  task="translate",
316
  verbose=False,
@@ -320,102 +406,132 @@ def background_transcription(file_path, file_hash, filename, file_size, translat
320
  english_text = english_result["text"].strip()
321
  if english_text:
322
  response_data["english_text"] = english_text
323
- logger.info("Background: English translation completed")
324
 
325
- save_to_cache(file_hash, filename, file_size, json.dumps(response_data))
326
-
327
- update_processing_status(file_hash, status='completed', progress=100)
 
 
328
 
 
329
  logger.info(f"Background transcription completed for {filename}")
330
 
331
  except Exception as e:
332
  logger.error(f"Error in background transcription: {e}")
333
- update_processing_status(file_hash, status='error', progress=0)
334
 
335
  finally:
336
- if os.path.exists(file_path):
337
- try:
 
338
  os.unlink(file_path)
339
- except:
340
- pass
341
 
342
- def cleanup_old_cache():
 
343
  try:
344
- conn = sqlite3.connect('transcription_cache.db')
345
- cursor = conn.cursor()
346
- cursor.execute("DELETE FROM cache WHERE created_at < datetime('now', '-30 days')")
347
- cursor.execute("DELETE FROM processing_status WHERE started_at < datetime('now', '-1 days')")
348
- cursor.execute("DELETE FROM translation_cache WHERE created_at < datetime('now', '-7 days')")
349
- conn.commit()
350
- conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  except Exception as e:
352
  logger.error(f"Error cleaning cache: {e}")
353
 
354
- init_cache_db()
355
-
356
  @app.get("/")
357
  async def root():
358
- conn = sqlite3.connect('transcription_cache.db')
359
- cursor = conn.cursor()
360
- cursor.execute('SELECT COUNT(*) FROM cache')
361
- cache_count = cursor.fetchone()[0]
362
- cursor.execute('SELECT COUNT(*) FROM processing_status WHERE status = "processing"')
363
- processing_count = cursor.fetchone()[0]
364
- cursor.execute('SELECT COUNT(*) FROM translation_cache')
365
- translation_cache_count = cursor.fetchone()[0]
366
- conn.close()
367
-
368
- return {
369
- "message": "Whisper API with Translation is running",
370
- "device": device,
371
- "cached_files": cache_count,
372
- "translation_cache": translation_cache_count,
373
- "currently_processing": processing_count,
374
- "translation_available": translation_loaded,
375
- "supported_languages": list(LANGUAGE_CODES.keys())
376
- }
 
 
 
 
 
 
 
 
377
 
378
  @app.post("/transcribe")
379
- async def transcribe_audio(background_tasks: BackgroundTasks, file: UploadFile = File(...), translate_to_english: bool = False):
 
 
 
 
 
 
380
  tmp_file_path = None
381
 
382
  try:
383
- logger.info(f"Received file: {file.filename}, size: {file.size}, translate_to_english: {translate_to_english}")
384
 
385
  if not file or not file.filename:
386
  raise HTTPException(status_code=400, detail="No valid file provided")
387
 
 
388
  contents = await file.read()
389
  file_size = len(contents)
390
  file_size_mb = file_size / (1024 * 1024)
391
 
392
- logger.info(f"File read successfully, size: {file_size} bytes ({file_size_mb:.1f} MB)")
393
 
394
- if file_size > 50 * 1024 * 1024:
395
- raise HTTPException(status_code=413, detail="File too large")
 
396
 
397
  if file_size == 0:
398
  raise HTTPException(status_code=400, detail="Empty file")
399
 
 
400
  file_hash = calculate_file_hash(contents, file.filename, file_size)
401
  logger.info(f"File hash: {file_hash}")
402
 
403
- cached_result = get_from_cache(file_hash)
 
404
  if cached_result:
405
- logger.info("Found in cache, returning cached result")
406
- remove_processing_status(file_hash)
407
- try:
408
- cached_data = json.loads(cached_result)
409
- cached_data["from_cache"] = True
410
- return JSONResponse(cached_data)
411
- except:
412
- return JSONResponse({
413
- "text": cached_result,
414
- "from_cache": True,
415
- "message": "Result returned from cache"
416
- })
417
-
418
- processing_status = get_processing_status(file_hash)
419
  if processing_status:
420
  logger.info("File is currently being processed")
421
  return JSONResponse({
@@ -423,104 +539,119 @@ async def transcribe_audio(background_tasks: BackgroundTasks, file: UploadFile =
423
  "progress": processing_status['progress'],
424
  "estimated_time": processing_status['estimated_time'],
425
  "elapsed_minutes": processing_status['elapsed_minutes'],
426
- "message": f"File is being processed. Please wait {processing_status['estimated_time'] - processing_status['elapsed_minutes']} minutes"
427
  })
428
 
429
  logger.info("Starting new processing...")
430
 
431
- file_ext = os.path.splitext(file.filename)[1].lower()
432
- if not file_ext:
433
- file_ext = ".wav"
434
-
435
  with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
436
  tmp_file.write(contents)
437
  tmp_file_path = tmp_file.name
438
 
439
- logger.info(f"Temp file created: {tmp_file_path}")
440
 
 
441
  estimated_time = estimate_processing_time(file_size_mb)
442
 
443
- if file_size_mb < 5:
444
- result = whisper_model.transcribe(
445
- tmp_file_path,
446
- fp16=False if device == "cpu" else True,
447
- language=None,
448
- task="transcribe",
449
- verbose=False,
450
- word_timestamps=False
451
- )
452
-
453
- text = result["text"].strip()
454
- if not text:
455
- text = "No text detected"
456
 
457
- response_data = {"text": text, "from_cache": False}
458
-
459
- if translate_to_english and result.get("language") != "en":
460
- english_result = whisper_model.transcribe(
461
- tmp_file_path,
462
- fp16=False if device == "cpu" else True,
463
- language=None,
464
- task="translate",
465
- verbose=False,
466
- word_timestamps=False
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  )
468
 
469
- english_text = english_result["text"].strip()
470
- if english_text:
471
- response_data["english_text"] = english_text
472
-
473
- save_to_cache(file_hash, file.filename, file_size, json.dumps(response_data))
474
-
475
- return JSONResponse(response_data)
476
 
477
  else:
478
- add_processing_status(file_hash, file.filename, file_size, estimated_time)
 
479
 
480
- background_tasks.add_task(background_transcription, tmp_file_path, file_hash, file.filename, file_size, translate_to_english)
 
 
 
481
 
482
  return JSONResponse({
483
  "status": "processing_started",
484
  "estimated_time": estimated_time,
485
  "file_hash": file_hash,
486
- "message": f"Processing started. It will take about {estimated_time} minutes. You can check the result later"
487
  })
488
 
 
 
489
  except Exception as e:
490
- logger.error(f"Error in transcription: {str(e)}")
491
- if "No module named" in str(e):
492
- raise HTTPException(status_code=500, detail="Missing required modules")
493
- elif "CUDA" in str(e):
494
- raise HTTPException(status_code=500, detail="GPU error")
495
- elif "FFmpeg" in str(e):
496
- raise HTTPException(status_code=500, detail="Audio processing error")
497
- else:
498
- raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
499
 
500
  finally:
501
- if tmp_file_path and os.path.exists(tmp_file_path) and file_size < 5 * 1024 * 1024:
 
502
  try:
503
  os.unlink(tmp_file_path)
504
- logger.info(f"Temp file deleted: {tmp_file_path}")
505
- except:
506
- pass
507
 
508
  @app.post("/translate")
509
- async def translate_endpoint(text: str, target_language: str):
 
 
 
510
  """Translate text to target language"""
511
 
512
- if not translation_loaded:
513
  raise HTTPException(status_code=503, detail="Translation service not available")
514
 
515
- if not text or not text.strip():
 
516
  raise HTTPException(status_code=400, detail="Text is required")
517
 
518
- if target_language.lower() not in LANGUAGE_CODES:
519
- raise HTTPException(status_code=400, detail=f"Unsupported language. Supported: {list(LANGUAGE_CODES.keys())}")
 
 
 
 
520
 
 
521
  text_hash = calculate_text_hash(text)
522
-
523
- cached_translation = get_translation_from_cache(text_hash, target_language.lower())
524
  if cached_translation:
525
  return JSONResponse({
526
  "text": text,
@@ -529,48 +660,80 @@ async def translate_endpoint(text: str, target_language: str):
529
  "from_cache": True
530
  })
531
 
532
- translated_text = translate_text(text, target_language)
533
-
534
- if not translated_text:
535
- raise HTTPException(status_code=500, detail="Translation failed")
536
-
537
- save_translation_to_cache(text_hash, target_language.lower(), translated_text)
538
-
539
- return JSONResponse({
540
- "text": text,
541
- "translated_text": translated_text,
542
- "target_language": target_language,
543
- "from_cache": False
544
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
 
546
  @app.get("/languages")
547
  async def get_supported_languages():
548
  """Get list of supported languages for translation"""
549
  return JSONResponse({
550
- "supported_languages": LANGUAGE_CODES,
551
- "translation_available": translation_loaded
 
552
  })
553
 
554
  @app.get("/status/{file_hash}")
555
  async def check_status(file_hash: str):
556
- cached_result = get_from_cache(file_hash)
 
 
557
  if cached_result:
558
- remove_processing_status(file_hash)
559
- try:
560
- cached_data = json.loads(cached_result)
561
- cached_data["status"] = "completed"
562
- cached_data["from_cache"] = True
563
- cached_data["message"] = "Processing completed and result is ready"
564
- return JSONResponse(cached_data)
565
- except:
566
- return JSONResponse({
567
- "status": "completed",
568
- "text": cached_result,
569
- "from_cache": True,
570
- "message": "Processing completed and result is ready"
571
- })
572
 
573
- processing_status = get_processing_status(file_hash)
 
574
  if processing_status:
575
  remaining_time = max(0, processing_status['estimated_time'] - processing_status['elapsed_minutes'])
576
  return JSONResponse({
@@ -584,42 +747,70 @@ async def check_status(file_hash: str):
584
 
585
  return JSONResponse({
586
  "status": "not_found",
587
- "message": "File not found"
588
- })
589
 
590
  @app.get("/cache/stats")
591
  async def cache_stats():
 
592
  try:
593
- conn = sqlite3.connect('transcription_cache.db')
594
- cursor = conn.cursor()
595
-
596
- cursor.execute('SELECT COUNT(*) FROM cache')
597
- total_count = cursor.fetchone()[0]
598
-
599
- cursor.execute('SELECT COUNT(*) FROM cache WHERE created_at >= datetime("now", "-1 day")')
600
- today_count = cursor.fetchone()[0]
601
-
602
- cursor.execute('SELECT AVG(LENGTH(transcription)) FROM cache')
603
- avg_text_length = cursor.fetchone()[0] or 0
604
-
605
- cursor.execute('SELECT COUNT(*) FROM processing_status WHERE status = "processing"')
606
- processing_count = cursor.fetchone()[0]
607
-
608
- cursor.execute('SELECT COUNT(*) FROM translation_cache')
609
- translation_count = cursor.fetchone()[0]
610
-
611
- conn.close()
612
-
 
613
  return {
614
  "total_cached_files": total_count,
615
  "cached_today": today_count,
 
616
  "average_text_length": int(avg_text_length),
617
  "currently_processing": processing_count,
618
  "translation_cache_count": translation_count,
619
- "translation_available": translation_loaded
620
  }
621
  except Exception as e:
622
- return {"error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
 
624
  if __name__ == "__main__":
625
- uvicorn.run(app, host="0.0.0.0", port=7860, timeout_keep_alive=900)
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Query
2
  from fastapi.responses import JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse
6
  import whisper
7
  import torch
8
  import tempfile
 
12
  import hashlib
13
  import json
14
  import sqlite3
15
+ from datetime import datetime, timedelta
16
  import threading
17
  import time
18
+ from typing import Optional, Dict, Any
19
+ from pathlib import Path
20
+ import aiofiles
21
+ from contextlib import asynccontextmanager
22
 
23
+ # Configure logging
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.FileHandler("app.log"),
29
+ logging.StreamHandler()
30
+ ]
 
 
 
31
  )
32
+ logger = logging.getLogger(__name__)
33
 
34
+ # Global variables
35
+ whisper_model = None
 
 
 
 
36
  translation_tokenizer = None
37
  translation_model = None
38
+ device = "cuda" if torch.cuda.is_available() else "cpu"
39
+
40
+ @asynccontextmanager
41
+ async def lifespan(app: FastAPI):
42
+ """Lifespan manager for startup and shutdown events"""
43
+ # Startup
44
+ global whisper_model, translation_tokenizer, translation_model
45
+
46
+ try:
47
+ logger.info(f"Loading Whisper model on {device}")
48
+ whisper_model = whisper.load_model("large-v3", device=device)
49
+ logger.info("Whisper model loaded successfully")
50
+ except Exception as e:
51
+ logger.error(f"Failed to load Whisper model: {e}")
52
+ raise RuntimeError(f"Whisper model loading failed: {e}")
53
 
 
 
54
  try:
55
+ logger.info("Loading translation model...")
56
  model_name = "facebook/nllb-200-distilled-600M"
57
  translation_tokenizer = AutoTokenizer.from_pretrained(model_name)
58
  translation_model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
59
+ if device == "cuda":
60
  translation_model = translation_model.to(device)
61
  logger.info(f"Translation model loaded on {device}")
 
62
  except Exception as e:
63
+ logger.warning(f"Translation model not available: {e}")
64
+ translation_model = None
65
 
66
+ # Initialize database
67
+ init_cache_db()
68
+
69
+ yield
70
+
71
+ # Shutdown
72
+ logger.info("Shutting down application...")
73
+ if torch.cuda.is_available():
74
+ torch.cuda.empty_cache()
75
+
76
+ app = FastAPI(
77
+ title="Whisper Transcription API",
78
+ description="API for audio transcription and translation using OpenAI Whisper",
79
+ version="1.0.0",
80
+ lifespan=lifespan
81
+ )
82
+
83
+ # CORS middleware
84
+ app.add_middleware(
85
+ CORSMiddleware,
86
+ allow_origins=["*"],
87
+ allow_credentials=True,
88
+ allow_methods=["*"],
89
+ allow_headers=["*"],
90
+ )
91
 
92
+ # Language codes mapping
93
  LANGUAGE_CODES = {
94
  'persian': 'pes_Arab',
95
  'farsi': 'pes_Arab',
 
113
  'finnish': 'fin_Latn'
114
  }
115
 
116
+ class DatabaseManager:
117
+ """Database management class with connection pooling"""
 
118
 
119
+ def __init__(self, db_path: str = 'transcription_cache.db'):
120
+ self.db_path = db_path
121
+ self._init_db()
122
+
123
+ def _init_db(self):
124
+ """Initialize database tables"""
125
+ with sqlite3.connect(self.db_path) as conn:
126
+ cursor = conn.cursor()
127
 
128
+ # Cache table
129
+ cursor.execute('''
130
+ CREATE TABLE IF NOT EXISTS cache (
131
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
132
+ file_hash TEXT UNIQUE,
133
+ filename TEXT,
134
+ file_size INTEGER,
135
+ transcription TEXT,
136
+ language TEXT,
137
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
138
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP
139
+ )
140
+ ''')
141
+
142
+ # Processing status table
143
+ cursor.execute('''
144
+ CREATE TABLE IF NOT EXISTS processing_status (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ file_hash TEXT UNIQUE,
147
+ filename TEXT,
148
+ file_size INTEGER,
149
+ status TEXT DEFAULT 'processing',
150
+ progress INTEGER DEFAULT 0,
151
+ estimated_time INTEGER DEFAULT 0,
152
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
154
+ )
155
+ ''')
156
+
157
+ # Translation cache table
158
+ cursor.execute('''
159
+ CREATE TABLE IF NOT EXISTS translation_cache (
160
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
161
+ text_hash TEXT,
162
+ target_language TEXT,
163
+ translated_text TEXT,
164
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
165
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
166
+ UNIQUE(text_hash, target_language)
167
+ )
168
+ ''')
169
+
170
+ # Create indexes for better performance
171
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_cache_hash ON cache(file_hash)')
172
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_cache_created ON cache(created_at)')
173
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_status_hash ON processing_status(file_hash)')
174
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_translation_hash ON translation_cache(text_hash, target_language)')
175
+
176
+ conn.commit()
177
+
178
+ def get_connection(self):
179
+ """Get database connection"""
180
+ return sqlite3.connect(self.db_path)
181
+
182
+ # Global database manager
183
+ db_manager = DatabaseManager()
184
 
185
  def init_cache_db():
186
+ """Initialize cache database"""
187
+ # Already handled by DatabaseManager
188
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ def calculate_file_hash(content: bytes, filename: str, file_size: int) -> str:
191
+ """Calculate hash for file identification"""
192
  hash_input = f"{filename}_{file_size}_{len(content)}"
193
+ return hashlib.md5(content[:1024] + content[-1024:] + hash_input.encode()).hexdigest()
 
194
 
195
+ def calculate_text_hash(text: str) -> str:
196
+ """Calculate hash for text"""
197
  return hashlib.md5(text.encode('utf-8')).hexdigest()
198
 
199
+ async def get_from_cache(file_hash: str) -> Optional[str]:
200
+ """Get transcription from cache"""
201
  try:
202
+ with db_manager.get_connection() as conn:
203
+ cursor = conn.cursor()
204
+ cursor.execute(
205
+ 'SELECT transcription FROM cache WHERE file_hash = ?',
206
+ (file_hash,)
207
+ )
208
+ result = cursor.fetchone()
209
+
210
+ if result:
211
+ # Update last accessed time
212
+ cursor.execute(
213
+ 'UPDATE cache SET last_accessed = CURRENT_TIMESTAMP WHERE file_hash = ?',
214
+ (file_hash,)
215
+ )
216
+ conn.commit()
217
+ return result[0]
218
+ return None
219
+ except Exception as e:
220
+ logger.error(f"Error getting from cache: {e}")
221
  return None
222
 
223
+ async def get_translation_from_cache(text_hash: str, target_language: str) -> Optional[str]:
224
+ """Get translation from cache"""
225
  try:
226
+ with db_manager.get_connection() as conn:
227
+ cursor = conn.cursor()
228
+ cursor.execute(
229
+ '''SELECT translated_text FROM translation_cache
230
+ WHERE text_hash = ? AND target_language = ?''',
231
+ (text_hash, target_language)
232
+ )
233
+ result = cursor.fetchone()
234
+
235
+ if result:
236
+ # Update last accessed time
237
+ cursor.execute(
238
+ '''UPDATE translation_cache SET last_accessed = CURRENT_TIMESTAMP
239
+ WHERE text_hash = ? AND target_language = ?''',
240
+ (text_hash, target_language)
241
+ )
242
+ conn.commit()
243
+ return result[0]
244
+ return None
245
+ except Exception as e:
246
+ logger.error(f"Error getting translation from cache: {e}")
247
  return None
248
 
249
+ async def save_to_cache(file_hash: str, filename: str, file_size: int, transcription: str, language: str = None):
250
+ """Save transcription to cache"""
251
  try:
252
+ with db_manager.get_connection() as conn:
253
+ cursor = conn.cursor()
254
+ cursor.execute(
255
+ '''INSERT OR REPLACE INTO cache
256
+ (file_hash, filename, file_size, transcription, language)
257
+ VALUES (?, ?, ?, ?, ?)''',
258
+ (file_hash, filename, file_size, transcription, language)
259
+ )
260
+ conn.commit()
261
  except Exception as e:
262
  logger.error(f"Error saving to cache: {e}")
263
 
264
+ async def save_translation_to_cache(text_hash: str, target_language: str, translated_text: str):
265
+ """Save translation to cache"""
266
  try:
267
+ with db_manager.get_connection() as conn:
268
+ cursor = conn.cursor()
269
+ cursor.execute(
270
+ '''INSERT OR REPLACE INTO translation_cache
271
+ (text_hash, target_language, translated_text)
272
+ VALUES (?, ?, ?)''',
273
+ (text_hash, target_language, translated_text)
274
+ )
275
+ conn.commit()
276
  except Exception as e:
277
  logger.error(f"Error saving translation to cache: {e}")
278
 
279
+ async def get_processing_status(file_hash: str) -> Optional[Dict[str, Any]]:
280
+ """Get processing status for a file"""
281
  try:
282
+ with db_manager.get_connection() as conn:
283
+ cursor = conn.cursor()
284
+ cursor.execute(
285
+ '''SELECT status, progress, estimated_time,
286
  (julianday('now') - julianday(started_at)) * 24 * 60 as elapsed_minutes
287
+ FROM processing_status WHERE file_hash = ?''',
288
+ (file_hash,)
289
+ )
290
+ result = cursor.fetchone()
291
+
292
+ if result:
293
+ return {
294
+ 'status': result[0],
295
+ 'progress': result[1],
296
+ 'estimated_time': result[2],
297
+ 'elapsed_minutes': int(result[3] or 0)
298
+ }
299
  return None
300
+ except Exception as e:
301
+ logger.error(f"Error getting processing status: {e}")
302
  return None
303
 
304
+ async def update_processing_status(file_hash: str, status: str = None, progress: int = None, estimated_time: int = None):
305
+ """Update processing status"""
306
  try:
 
 
 
307
  updates = []
308
  params = []
309
 
 
321
  params.append(file_hash)
322
 
323
  query = f"UPDATE processing_status SET {', '.join(updates)} WHERE file_hash = ?"
324
+
325
+ with db_manager.get_connection() as conn:
326
+ cursor = conn.cursor()
327
+ cursor.execute(query, params)
328
+ conn.commit()
329
  except Exception as e:
330
  logger.error(f"Error updating status: {e}")
331
 
332
+ async def add_processing_status(file_hash: str, filename: str, file_size: int, estimated_time: int):
333
+ """Add new processing status entry"""
334
  try:
335
+ with db_manager.get_connection() as conn:
336
+ cursor = conn.cursor()
337
+ cursor.execute(
338
+ '''INSERT OR REPLACE INTO processing_status
339
+ (file_hash, filename, file_size, status, progress, estimated_time)
340
+ VALUES (?, ?, ?, 'processing', 0, ?)''',
341
+ (file_hash, filename, file_size, estimated_time)
342
+ )
343
+ conn.commit()
344
  except Exception as e:
345
  logger.error(f"Error adding processing status: {e}")
346
 
347
+ async def remove_processing_status(file_hash: str):
348
+ """Remove processing status entry"""
349
  try:
350
+ with db_manager.get_connection() as conn:
351
+ cursor = conn.cursor()
352
+ cursor.execute(
353
+ 'DELETE FROM processing_status WHERE file_hash = ?',
354
+ (file_hash,)
355
+ )
356
+ conn.commit()
357
  except Exception as e:
358
  logger.error(f"Error removing processing status: {e}")
359
 
360
+ def estimate_processing_time(file_size_mb: float) -> int:
361
+ """Estimate processing time in minutes"""
362
+ estimated_seconds = file_size_mb * 30 # 30 seconds per MB
363
+ return max(1, int(estimated_seconds / 60))
364
 
365
+ async def background_transcription(file_path: str, file_hash: str, filename: str, file_size: int, translate_to_english: bool = False):
366
+ """Background task for transcription"""
367
  try:
368
  logger.info(f"Starting background transcription for {filename}")
369
 
370
+ await update_processing_status(file_hash, status='processing', progress=10)
371
 
372
+ # Transcribe audio
373
  result = whisper_model.transcribe(
374
  file_path,
375
+ fp16=(device != "cpu"),
376
  language=None,
377
  task="transcribe",
378
  verbose=False,
379
  word_timestamps=False
380
  )
381
 
382
+ await update_processing_status(file_hash, progress=60)
383
 
384
+ text = result["text"].strip() or "No text detected"
385
+ detected_language = result.get("language", "unknown")
 
 
 
386
 
387
+ response_data = {
388
+ "text": text,
389
+ "language": detected_language,
390
+ "from_cache": False
391
+ }
392
+
393
+ # Translate if requested and needed
394
+ if translate_to_english and detected_language != "en":
395
+ await update_processing_status(file_hash, progress=80)
396
 
397
  english_result = whisper_model.transcribe(
398
  file_path,
399
+ fp16=(device != "cpu"),
400
  language=None,
401
  task="translate",
402
  verbose=False,
 
406
  english_text = english_result["text"].strip()
407
  if english_text:
408
  response_data["english_text"] = english_text
 
409
 
410
+ # Save to cache
411
+ await save_to_cache(
412
+ file_hash, filename, file_size,
413
+ json.dumps(response_data), detected_language
414
+ )
415
 
416
+ await update_processing_status(file_hash, status='completed', progress=100)
417
  logger.info(f"Background transcription completed for {filename}")
418
 
419
  except Exception as e:
420
  logger.error(f"Error in background transcription: {e}")
421
+ await update_processing_status(file_hash, status='error', progress=0)
422
 
423
  finally:
424
+ # Clean up temporary file
425
+ try:
426
+ if os.path.exists(file_path):
427
  os.unlink(file_path)
428
+ except Exception as e:
429
+ logger.error(f"Error deleting temp file: {e}")
430
 
431
+ async def cleanup_old_cache(days: int = 30):
432
+ """Clean up old cache entries"""
433
  try:
434
+ cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
435
+
436
+ with db_manager.get_connection() as conn:
437
+ cursor = conn.cursor()
438
+
439
+ # Clean cache
440
+ cursor.execute(
441
+ "DELETE FROM cache WHERE last_accessed < ?",
442
+ (cutoff_date,)
443
+ )
444
+
445
+ # Clean processing status (keep only 1 day)
446
+ cursor.execute(
447
+ "DELETE FROM processing_status WHERE started_at < datetime('now', '-1 day')"
448
+ )
449
+
450
+ # Clean translation cache
451
+ cursor.execute(
452
+ "DELETE FROM translation_cache WHERE last_accessed < ?",
453
+ (cutoff_date,)
454
+ )
455
+
456
+ conn.commit()
457
+ logger.info(f"Cache cleanup completed. Removed entries older than {days} days.")
458
+
459
  except Exception as e:
460
  logger.error(f"Error cleaning cache: {e}")
461
 
 
 
462
  @app.get("/")
463
  async def root():
464
+ """Root endpoint with system information"""
465
+ try:
466
+ with db_manager.get_connection() as conn:
467
+ cursor = conn.cursor()
468
+
469
+ cursor.execute('SELECT COUNT(*) FROM cache')
470
+ cache_count = cursor.fetchone()[0] or 0
471
+
472
+ cursor.execute('SELECT COUNT(*) FROM processing_status WHERE status = "processing"')
473
+ processing_count = cursor.fetchone()[0] or 0
474
+
475
+ cursor.execute('SELECT COUNT(*) FROM translation_cache')
476
+ translation_cache_count = cursor.fetchone()[0] or 0
477
+
478
+ return {
479
+ "message": "Whisper API with Translation is running",
480
+ "device": device,
481
+ "cuda_available": torch.cuda.is_available(),
482
+ "cached_files": cache_count,
483
+ "translation_cache": translation_cache_count,
484
+ "currently_processing": processing_count,
485
+ "translation_available": translation_model is not None,
486
+ "supported_languages": list(LANGUAGE_CODES.keys())
487
+ }
488
+ except Exception as e:
489
+ logger.error(f"Error in root endpoint: {e}")
490
+ return {"error": "Unable to retrieve system information"}
491
 
492
  @app.post("/transcribe")
493
+ async def transcribe_audio(
494
+ background_tasks: BackgroundTasks,
495
+ file: UploadFile = File(...),
496
+ translate_to_english: bool = False,
497
+ language: Optional[str] = Query(None, description="Specify language code for better accuracy")
498
+ ):
499
+ """Transcribe audio file to text"""
500
  tmp_file_path = None
501
 
502
  try:
503
+ logger.info(f"Received file: {file.filename}, size: {file.size}")
504
 
505
  if not file or not file.filename:
506
  raise HTTPException(status_code=400, detail="No valid file provided")
507
 
508
+ # Read file content
509
  contents = await file.read()
510
  file_size = len(contents)
511
  file_size_mb = file_size / (1024 * 1024)
512
 
513
+ logger.info(f"File size: {file_size} bytes ({file_size_mb:.1f} MB)")
514
 
515
+ # Validate file size
516
+ if file_size > 100 * 1024 * 1024: # 100MB limit
517
+ raise HTTPException(status_code=413, detail="File too large (max 100MB)")
518
 
519
  if file_size == 0:
520
  raise HTTPException(status_code=400, detail="Empty file")
521
 
522
+ # Calculate file hash for caching
523
  file_hash = calculate_file_hash(contents, file.filename, file_size)
524
  logger.info(f"File hash: {file_hash}")
525
 
526
+ # Check cache
527
+ cached_result = await get_from_cache(file_hash)
528
  if cached_result:
529
+ logger.info("Cache hit - returning cached result")
530
+ await remove_processing_status(file_hash)
531
+ return JSONResponse(json.loads(cached_result))
532
+
533
+ # Check if already processing
534
+ processing_status = await get_processing_status(file_hash)
 
 
 
 
 
 
 
 
535
  if processing_status:
536
  logger.info("File is currently being processed")
537
  return JSONResponse({
 
539
  "progress": processing_status['progress'],
540
  "estimated_time": processing_status['estimated_time'],
541
  "elapsed_minutes": processing_status['elapsed_minutes'],
542
+ "message": f"File is being processed. Estimated time remaining: {processing_status['estimated_time'] - processing_status['elapsed_minutes']} minutes"
543
  })
544
 
545
  logger.info("Starting new processing...")
546
 
547
+ # Create temporary file
548
+ file_ext = os.path.splitext(file.filename)[1].lower() or ".wav"
 
 
549
  with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
550
  tmp_file.write(contents)
551
  tmp_file_path = tmp_file.name
552
 
553
+ logger.info(f"Created temp file: {tmp_file_path}")
554
 
555
+ # Estimate processing time
556
  estimated_time = estimate_processing_time(file_size_mb)
557
 
558
+ # Process small files immediately
559
+ if file_size_mb < 10: # Increased threshold to 10MB
560
+ try:
561
+ # Transcribe with optional language hint
562
+ transcribe_args = {
563
+ 'fp16': (device != "cpu"),
564
+ 'language': language,
565
+ 'task': "transcribe",
566
+ 'verbose': False,
567
+ 'word_timestamps': False
568
+ }
 
 
569
 
570
+ result = whisper_model.transcribe(tmp_file_path, **transcribe_args)
571
+
572
+ text = result["text"].strip() or "No text detected"
573
+ detected_language = result.get("language", "unknown")
574
+
575
+ response_data = {
576
+ "text": text,
577
+ "language": detected_language,
578
+ "from_cache": False
579
+ }
580
+
581
+ # Translate if requested
582
+ if translate_to_english and detected_language != "en":
583
+ transcribe_args['task'] = "translate"
584
+ english_result = whisper_model.transcribe(tmp_file_path, **transcribe_args)
585
+ english_text = english_result["text"].strip()
586
+ if english_text:
587
+ response_data["english_text"] = english_text
588
+
589
+ # Save to cache
590
+ await save_to_cache(
591
+ file_hash, file.filename, file_size,
592
+ json.dumps(response_data), detected_language
593
  )
594
 
595
+ return JSONResponse(response_data)
596
+
597
+ except Exception as e:
598
+ logger.error(f"Error in immediate transcription: {e}")
599
+ raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}")
 
 
600
 
601
  else:
602
+ # Large file - process in background
603
+ await add_processing_status(file_hash, file.filename, file_size, estimated_time)
604
 
605
+ background_tasks.add_task(
606
+ background_transcription,
607
+ tmp_file_path, file_hash, file.filename, file_size, translate_to_english
608
+ )
609
 
610
  return JSONResponse({
611
  "status": "processing_started",
612
  "estimated_time": estimated_time,
613
  "file_hash": file_hash,
614
+ "message": f"Processing started. Estimated time: {estimated_time} minutes."
615
  })
616
 
617
+ except HTTPException:
618
+ raise
619
  except Exception as e:
620
+ logger.error(f"Error in transcription endpoint: {str(e)}")
621
+ raise HTTPException(status_code=500, detail=f"Processing error: {str(e)}")
 
 
 
 
 
 
 
622
 
623
  finally:
624
+ # Clean up temporary file for small immediate processing
625
+ if tmp_file_path and os.path.exists(tmp_file_path) and file_size_mb < 10:
626
  try:
627
  os.unlink(tmp_file_path)
628
+ except Exception as e:
629
+ logger.error(f"Error deleting temp file: {e}")
 
630
 
631
  @app.post("/translate")
632
+ async def translate_endpoint(
633
+ text: str = Query(..., min_length=1, description="Text to translate"),
634
+ target_language: str = Query(..., description="Target language")
635
+ ):
636
  """Translate text to target language"""
637
 
638
+ if not translation_model:
639
  raise HTTPException(status_code=503, detail="Translation service not available")
640
 
641
+ text = text.strip()
642
+ if not text:
643
  raise HTTPException(status_code=400, detail="Text is required")
644
 
645
+ target_language_lower = target_language.lower()
646
+ if target_language_lower not in LANGUAGE_CODES:
647
+ raise HTTPException(
648
+ status_code=400,
649
+ detail=f"Unsupported language. Supported: {list(LANGUAGE_CODES.keys())}"
650
+ )
651
 
652
+ # Check cache
653
  text_hash = calculate_text_hash(text)
654
+ cached_translation = await get_translation_from_cache(text_hash, target_language_lower)
 
655
  if cached_translation:
656
  return JSONResponse({
657
  "text": text,
 
660
  "from_cache": True
661
  })
662
 
663
+ # Perform translation
664
+ try:
665
+ target_code = LANGUAGE_CODES[target_language_lower]
666
+
667
+ # Tokenize input
668
+ inputs = translation_tokenizer(
669
+ text,
670
+ return_tensors="pt",
671
+ padding=True,
672
+ truncation=True,
673
+ max_length=512
674
+ )
675
+
676
+ if device == "cuda":
677
+ inputs = {k: v.to(device) for k, v in inputs.items()}
678
+
679
+ # Generate translation
680
+ translated_tokens = translation_model.generate(
681
+ **inputs,
682
+ forced_bos_token_id=translation_tokenizer.lang_code_to_id[target_code],
683
+ max_length=512,
684
+ num_beams=5,
685
+ early_stopping=True
686
+ )
687
+
688
+ # Decode output
689
+ translated_text = translation_tokenizer.batch_decode(
690
+ translated_tokens,
691
+ skip_special_tokens=True
692
+ )[0].strip()
693
+
694
+ if not translated_text:
695
+ raise HTTPException(status_code=500, detail="Translation returned empty result")
696
+
697
+ # Save to cache
698
+ await save_translation_to_cache(text_hash, target_language_lower, translated_text)
699
+
700
+ return JSONResponse({
701
+ "text": text,
702
+ "translated_text": translated_text,
703
+ "target_language": target_language,
704
+ "from_cache": False
705
+ })
706
+
707
+ except Exception as e:
708
+ logger.error(f"Translation error: {e}")
709
+ raise HTTPException(status_code=500, detail=f"Translation failed: {str(e)}")
710
 
711
  @app.get("/languages")
712
  async def get_supported_languages():
713
  """Get list of supported languages for translation"""
714
  return JSONResponse({
715
+ "supported_languages": list(LANGUAGE_CODES.keys()),
716
+ "language_codes": LANGUAGE_CODES,
717
+ "translation_available": translation_model is not None
718
  })
719
 
720
  @app.get("/status/{file_hash}")
721
  async def check_status(file_hash: str):
722
+ """Check processing status for a file"""
723
+ # Check cache first
724
+ cached_result = await get_from_cache(file_hash)
725
  if cached_result:
726
+ await remove_processing_status(file_hash)
727
+ cached_data = json.loads(cached_result)
728
+ cached_data.update({
729
+ "status": "completed",
730
+ "from_cache": True,
731
+ "message": "Processing completed and result is ready"
732
+ })
733
+ return JSONResponse(cached_data)
 
 
 
 
 
 
734
 
735
+ # Check processing status
736
+ processing_status = await get_processing_status(file_hash)
737
  if processing_status:
738
  remaining_time = max(0, processing_status['estimated_time'] - processing_status['elapsed_minutes'])
739
  return JSONResponse({
 
747
 
748
  return JSONResponse({
749
  "status": "not_found",
750
+ "message": "File not found in cache or processing queue"
751
+ }, status_code=404)
752
 
753
  @app.get("/cache/stats")
754
  async def cache_stats():
755
+ """Get cache statistics"""
756
  try:
757
+ with db_manager.get_connection() as conn:
758
+ cursor = conn.cursor()
759
+
760
+ cursor.execute('SELECT COUNT(*) FROM cache')
761
+ total_count = cursor.fetchone()[0] or 0
762
+
763
+ cursor.execute('SELECT COUNT(*) FROM cache WHERE created_at >= datetime("now", "-1 day")')
764
+ today_count = cursor.fetchone()[0] or 0
765
+
766
+ cursor.execute('SELECT AVG(LENGTH(transcription)) FROM cache')
767
+ avg_text_length = cursor.fetchone()[0] or 0
768
+
769
+ cursor.execute('SELECT COUNT(*) FROM processing_status WHERE status = "processing"')
770
+ processing_count = cursor.fetchone()[0] or 0
771
+
772
+ cursor.execute('SELECT COUNT(*) FROM translation_cache')
773
+ translation_count = cursor.fetchone()[0] or 0
774
+
775
+ cursor.execute('SELECT SUM(file_size) FROM cache')
776
+ total_cache_size = cursor.fetchone()[0] or 0
777
+
778
  return {
779
  "total_cached_files": total_count,
780
  "cached_today": today_count,
781
+ "total_cache_size_mb": round(total_cache_size / (1024 * 1024), 2),
782
  "average_text_length": int(avg_text_length),
783
  "currently_processing": processing_count,
784
  "translation_cache_count": translation_count,
785
+ "translation_available": translation_model is not None
786
  }
787
  except Exception as e:
788
+ logger.error(f"Error getting cache stats: {e}")
789
+ return {"error": "Unable to retrieve cache statistics"}
790
+
791
+ @app.post("/cache/cleanup")
792
+ async def manual_cache_cleanup(days: int = 30):
793
+ """Manually trigger cache cleanup"""
794
+ await cleanup_old_cache(days)
795
+ return {"message": f"Cache cleanup initiated for entries older than {days} days"}
796
+
797
+ @app.get("/health")
798
+ async def health_check():
799
+ """Health check endpoint"""
800
+ return {
801
+ "status": "healthy",
802
+ "timestamp": datetime.now().isoformat(),
803
+ "device": device,
804
+ "cuda_available": torch.cuda.is_available(),
805
+ "whisper_loaded": whisper_model is not None,
806
+ "translation_loaded": translation_model is not None
807
+ }
808
 
809
  if __name__ == "__main__":
810
+ uvicorn.run(
811
+ app,
812
+ host="0.0.0.0",
813
+ port=7860,
814
+ timeout_keep_alive=900,
815
+ log_config=None
816
+ )