mrwabnalas40 commited on
Commit
f60e2e5
·
verified ·
1 Parent(s): 5556f9d

Create Audio_analyzer.py

Browse files
Files changed (1) hide show
  1. Audio_analyzer.py +496 -0
Audio_analyzer.py ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ transcribe_audio.py - الإصدار المحسن
5
+ استخراج النصوص من ملفات صوتية باستخدام whisper (محلي).
6
+
7
+ التحسينات المضافة:
8
+ ✅ شريط تقدم للملفات
9
+ ✅ دعم تنسيق SRT
10
+ ✅ خيار "all" لحفظ كل التنسيقات
11
+ ✅ إدارة ذاكرة محسنة (معالجة دفعات)
12
+ ✅ تسجيل الأحداث (Logging)
13
+ ✅ اكتشاف ملفات محسن
14
+ ✅ معالجة أخطاء أفضل
15
+ ✅ تقارير إحصائية
16
+ ✅ دعم مجلد الإخراج المخصص
17
+
18
+ Usage:
19
+ python transcribe_audio.py audio.mp3
20
+ python transcribe_audio.py folder/ --model large --lang ar --output all --recursive
21
+ python transcribe_audio.py "*.mp3" --output-dir ./output --verbose
22
+ """
23
+
24
+ import argparse
25
+ import os
26
+ import sys
27
+ import json
28
+ import logging
29
+ from pathlib import Path
30
+ import whisper
31
+ import typing
32
+ import traceback
33
+ import time
34
+ from datetime import datetime
35
+ from tqdm import tqdm
36
+
37
+ # إعداد التسجيل
38
+ def setup_logging(verbose: bool = False):
39
+ """إعداد نظام التسجيل"""
40
+ level = logging.DEBUG if verbose else logging.INFO
41
+ logging.basicConfig(
42
+ level=level,
43
+ format='%(asctime)s - %(levelname)s - %(message)s',
44
+ handlers=[
45
+ logging.StreamHandler(sys.stdout),
46
+ logging.FileHandler('transcription.log', encoding='utf-8', mode='w')
47
+ ]
48
+ )
49
+ return logging.getLogger(__name__)
50
+
51
+ logger = setup_logging()
52
+
53
+ def find_audio_files(inputs: typing.List[str], recursive: bool = False, max_file_size_mb: int = 500):
54
+ """اكتشاف الملفات الصوتية مع تحسينات"""
55
+ audio_extensions = {
56
+ '.mp3', '.wav', '.m4a', '.flac', '.ogg', '.webm',
57
+ '.aac', '.wma', '.opus', '.mpga', '.mp4', '.m4b', '.3gp'
58
+ }
59
+
60
+ files = []
61
+ max_size = max_file_size_mb * 1024 * 1024
62
+
63
+ for input_path in inputs:
64
+ path = Path(input_path)
65
+
66
+ if path.is_file():
67
+ # التحقق من حجم الملف
68
+ file_size = path.stat().st_size
69
+ if file_size > max_size:
70
+ logger.warning(f"تخطي الملف الكبير: {path.name} ({file_size / (1024*1024):.1f}MB)")
71
+ continue
72
+
73
+ if path.suffix.lower() in audio_extensions:
74
+ files.append(path.resolve())
75
+ else:
76
+ logger.warning(f"امتداد غير معروف: {path}")
77
+
78
+ elif path.is_dir():
79
+ pattern = "**/*" if recursive else "*"
80
+ for file_path in path.glob(pattern):
81
+ if file_path.is_file() and file_path.suffix.lower() in audio_extensions:
82
+ file_size = file_path.stat().st_size
83
+ if file_size <= max_size:
84
+ files.append(file_path.resolve())
85
+ else:
86
+ logger.warning(f"تخطي الملف الكبير: {file_path.name}")
87
+ else:
88
+ # دعم الأنماط مثل "*.mp3"
89
+ try:
90
+ from glob import glob
91
+ for pattern in inputs:
92
+ for file_path in glob(pattern):
93
+ path = Path(file_path)
94
+ if path.is_file() and path.suffix.lower() in audio_extensions:
95
+ files.append(path.resolve())
96
+ except:
97
+ logger.warning(f"مسار غير موجود: {input_path}")
98
+
99
+ # إزالة التكرارات والحفاظ على الترتيب
100
+ unique_files = []
101
+ seen_paths = set()
102
+
103
+ for file_path in files:
104
+ if str(file_path) not in seen_paths:
105
+ unique_files.append(file_path)
106
+ seen_paths.add(str(file_path))
107
+
108
+ logger.info(f"تم العثور على {len(unique_files)} ملف صوتي")
109
+ return sorted(unique_files, key=lambda x: x.name)
110
+
111
+ def safe_write_text(path: Path, text: str):
112
+ """كتابة النص إلى ملف بشكل آمن"""
113
+ try:
114
+ path.parent.mkdir(parents=True, exist_ok=True)
115
+ path.write_text(text, encoding='utf-8')
116
+ return True
117
+ except Exception as e:
118
+ logger.error(f"فشل كتابة الملف {path}: {e}")
119
+ return False
120
+
121
+ def format_timestamp(seconds: float, format_type: str = 'vtt') -> str:
122
+ """تنسيق الطابع الزمني"""
123
+ hours = int(seconds // 3600)
124
+ minutes = int((seconds % 3600) // 60)
125
+ secs = seconds % 60
126
+
127
+ if format_type == 'vtt':
128
+ return f"{hours:01d}:{minutes:02d}:{secs:06.3f}"
129
+ elif format_type == 'srt':
130
+ milliseconds = int((secs - int(secs)) * 1000)
131
+ return f"{hours:02d}:{minutes:02d}:{int(secs):02d},{milliseconds:03d}"
132
+ else:
133
+ return f"{hours:02d}:{minutes:02d}:{secs:06.3f}"
134
+
135
+ def generate_vtt_content(segments: list) -> str:
136
+ """إنشاء محتوى VTT"""
137
+ content = "WEBVTT\n\n"
138
+ for i, segment in enumerate(segments):
139
+ start = segment.get('start', 0)
140
+ end = segment.get('end', 0)
141
+ text = segment.get('text', '').strip()
142
+
143
+ start_ts = format_timestamp(start, 'vtt')
144
+ end_ts = format_timestamp(end, 'vtt')
145
+
146
+ content += f"{i+1}\n{start_ts} --> {end_ts}\n{text}\n\n"
147
+
148
+ return content
149
+
150
+ def generate_srt_content(segments: list) -> str:
151
+ """إنشاء محتوى SRT"""
152
+ content = ""
153
+ for i, segment in enumerate(segments):
154
+ start = segment.get('start', 0)
155
+ end = segment.get('end', 0)
156
+ text = segment.get('text', '').strip()
157
+
158
+ start_ts = format_timestamp(start, 'srt')
159
+ end_ts = format_timestamp(end, 'srt')
160
+
161
+ content += f"{i+1}\n{start_ts} --> {end_ts}\n{text}\n\n"
162
+
163
+ return content
164
+
165
+ def validate_transcription_result(result) -> bool:
166
+ """التحقق من جودة نتيجة التحويل"""
167
+ text = result.get('text', '').strip()
168
+ segments = result.get('segments', [])
169
+
170
+ # التحقق من وجود نص
171
+ if not text:
172
+ logger.warning("النص الناتج فارغ")
173
+ return False
174
+
175
+ if len(text) < 10:
176
+ logger.warning(f"النص الناتج قصير جداً: {len(text)} حرف")
177
+
178
+ # التحقق من نسبة الثقة إذا كانت متوفرة
179
+ if segments:
180
+ confidences = [seg.get('confidence', 0.5) for seg in segments if 'confidence' in seg]
181
+ if confidences:
182
+ avg_confidence = sum(confidences) / len(confidences)
183
+ if avg_confidence < 0.1:
184
+ logger.warning(f"ثقة منخفضة: {avg_confidence:.2f}")
185
+ return False
186
+
187
+ return True
188
+
189
+ def transcribe_file(model, audio_path: Path, lang: typing.Optional[str],
190
+ output_format: str, verbose: bool, output_dir: typing.Optional[Path] = None,
191
+ **whisper_options):
192
+ """تحويل الملف الصوتي إلى نص مع تحسينات"""
193
+ logger.info(f"معالجة: {audio_path.name}")
194
+
195
+ start_time = time.time()
196
+
197
+ try:
198
+ # إعدادات whisper
199
+ options = {
200
+ 'task': 'transcribe',
201
+ 'temperature': 0.0,
202
+ 'best_of': 5,
203
+ 'beam_size': 5,
204
+ }
205
+
206
+ if lang:
207
+ options['language'] = lang
208
+
209
+ # دمج الإعدادات الإضافية
210
+ options.update(whisper_options)
211
+
212
+ # التحويل النصي
213
+ result = model.transcribe(str(audio_path), **options)
214
+
215
+ # التحقق من الجودة
216
+ if not validate_transcription_result(result):
217
+ logger.warning(f"جودة التحويل منخفضة لـ {audio_path.name}")
218
+
219
+ except Exception as e:
220
+ logger.error(f"فشل التحويل لـ {audio_path.name}: {e}")
221
+ if verbose:
222
+ traceback.print_exc()
223
+ return None
224
+
225
+ processing_time = time.time() - start_time
226
+ logger.info(f"تم التحويل في {processing_time:.1f} ثانية")
227
+
228
+ # تحديد مسار الإخراج
229
+ if output_dir:
230
+ out_base = output_dir / audio_path.stem
231
+ else:
232
+ out_base = audio_path.with_suffix('')
233
+
234
+ # النص الكامل
235
+ text = result.get('text', '').strip()
236
+ segments = result.get('segments', [])
237
+
238
+ output_files = []
239
+ success = True
240
+
241
+ try:
242
+ # حفظ بتنسيق TXT
243
+ if output_format in ['txt', 'all']:
244
+ out_path = out_base.with_suffix('.txt')
245
+ if safe_write_text(out_path, text):
246
+ output_files.append(('txt', out_path))
247
+ logger.info(f" -> نص: {out_path}")
248
+
249
+ # حفظ بتنسيق JSON
250
+ if output_format in ['json', 'all']:
251
+ out_path = out_base.with_suffix('.json')
252
+ enhanced_result = {
253
+ 'metadata': {
254
+ 'audio_file': str(audio_path),
255
+ 'audio_size': audio_path.stat().st_size,
256
+ 'processing_time': datetime.now().isoformat(),
257
+ 'model': str(model.__class__),
258
+ 'language': result.get('language', 'unknown'),
259
+ 'processing_duration': processing_time
260
+ },
261
+ 'transcription': result
262
+ }
263
+ if safe_write_text(out_path, json.dumps(enhanced_result, ensure_ascii=False, indent=2)):
264
+ output_files.append(('json', out_path))
265
+ logger.info(f" -> JSON: {out_path}")
266
+
267
+ # حفظ بتنسيق VTT
268
+ if output_format in ['vtt', 'all']:
269
+ out_path = out_base.with_suffix('.vtt')
270
+ vtt_content = generate_vtt_content(segments)
271
+ if safe_write_text(out_path, vtt_content):
272
+ output_files.append(('vtt', out_path))
273
+ logger.info(f" -> VTT: {out_path}")
274
+
275
+ # حفظ بتنسيق SRT
276
+ if output_format in ['srt', 'all']:
277
+ out_path = out_base.with_suffix('.srt')
278
+ srt_content = generate_srt_content(segments)
279
+ if safe_write_text(out_path, srt_content):
280
+ output_files.append(('srt', out_path))
281
+ logger.info(f" -> SRT: {out_path}")
282
+
283
+ except Exception as e:
284
+ logger.error(f"فشل حفظ الملفات لـ {audio_path.name}: {e}")
285
+ success = False
286
+
287
+ return {
288
+ 'success': success,
289
+ 'output_files': output_files,
290
+ 'text_length': len(text),
291
+ 'processing_time': processing_time,
292
+ 'audio_duration': segments[-1]['end'] if segments else 0,
293
+ 'language': result.get('language', 'unknown')
294
+ }
295
+
296
+ def process_files_with_progress(model, files: list, **kwargs):
297
+ """معالجة الملفات مع عرض التقدم"""
298
+ successful = 0
299
+ failed = 0
300
+ total_text_length = 0
301
+ total_processing_time = 0
302
+
303
+ # إعداد شريط التقدم
304
+ with tqdm(total=len(files), desc="📝 تحويل الملفات", unit="file",
305
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]") as pbar:
306
+
307
+ for file_path in files:
308
+ start_time = time.time()
309
+
310
+ result = transcribe_file(model, file_path, **kwargs)
311
+
312
+ if result and result['success']:
313
+ successful += 1
314
+ total_text_length += result['text_length']
315
+ total_processing_time += result['processing_time']
316
+ status = "✅"
317
+ else:
318
+ failed += 1
319
+ status = "❌"
320
+
321
+ # تحديث شريط التقدم
322
+ file_time = time.time() - start_time
323
+ pbar.set_postfix({
324
+ 'نجح': successful,
325
+ 'فشل': failed,
326
+ 'الوقت': f'{file_time:.1f}s'
327
+ })
328
+ pbar.update(1)
329
+
330
+ return {
331
+ 'successful': successful,
332
+ 'failed': failed,
333
+ 'total_files': len(files),
334
+ 'total_text_length': total_text_length,
335
+ 'total_processing_time': total_processing_time,
336
+ 'success_rate': (successful / len(files)) * 100 if files else 0
337
+ }
338
+
339
+ def check_dependencies():
340
+ """التحقق من التوابع المطلوبة"""
341
+ try:
342
+ import whisper
343
+ return True
344
+ except ImportError:
345
+ print("❌ مكتبة whisper غير مثبتة. قم بتثبيتها باستخدام:")
346
+ print(" pip install openai-whisper")
347
+ return False
348
+
349
+ try:
350
+ import tqdm
351
+ return True
352
+ except ImportError:
353
+ print("❌ مكتبة tqdm غير مثبتة. قم بتثبيتها باستخدام:")
354
+ print(" pip install tqdm")
355
+ return False
356
+
357
+ def main():
358
+ # التحقق من التوابع أولاً
359
+ if not check_dependencies():
360
+ sys.exit(1)
361
+
362
+ parser = argparse.ArgumentParser(
363
+ description="استخراج نص من ملفات صوتية باستخدام whisper.",
364
+ formatter_class=argparse.RawDescriptionHelpFormatter,
365
+ epilog="""
366
+ أمثلة الاستخدام:
367
+ python transcribe_audio.py audio.mp3
368
+ python transcribe_audio.py folder/ --model large --lang ar --output all --recursive
369
+ python transcribe_audio.py "*.mp3" --output-dir ./output --verbose
370
+ python transcribe_audio.py file1.mp3 file2.wav --model medium --output json
371
+
372
+ النماذج المتاحة (من الأصغر إلى الأكثر دقة):
373
+ tiny, base, small, medium, large
374
+ """
375
+ )
376
+
377
+ parser.add_argument('inputs', nargs='+',
378
+ help='ملف صوتي أو مجلد (يمكن تكرارها، تدعم الأنماط مثل *.mp3)')
379
+
380
+ parser.add_argument('--model', default='small',
381
+ choices=['tiny', 'base', 'small', 'medium', 'large'],
382
+ help='نموذج whisper (افتراضي: small)')
383
+
384
+ parser.add_argument('--lang', default=None,
385
+ help='رمز اللغة (مثلاً "ar" للعربية، "en" للإنجليزية)')
386
+
387
+ parser.add_argument('--output', choices=['txt','json','vtt','srt','all'],
388
+ default='txt', help='صيغة الإخراج (افتراضي: txt)')
389
+
390
+ parser.add_argument('--recursive', action='store_true',
391
+ help='البحث في المجلدات فرعيًا')
392
+
393
+ parser.add_argument('--device', default=None,
394
+ choices=['cuda', 'cpu', 'auto'],
395
+ help='جهاز المعالجة (افتراضي: auto)')
396
+
397
+ parser.add_argument('--verbose', action='store_true',
398
+ help='طباعة معلومات تفصيلية')
399
+
400
+ parser.add_argument('--batch-size', type=int, default=1,
401
+ help='عدد الملفات للمعالجة في الدفعة الواحدة (لإدارة الذاكرة)')
402
+
403
+ parser.add_argument('--max-size-mb', type=int, default=500,
404
+ help='الحجم الأقصى للملف بالميجابايت (افتراضي: 500)')
405
+
406
+ parser.add_argument('--output-dir', default=None,
407
+ help='مجلد الإخراج المخصص (بدلاً من مجلد الملف الأصلي)')
408
+
409
+ args = parser.parse_args()
410
+
411
+ # إعداد التسجيل
412
+ global logger
413
+ logger = setup_logging(args.verbose)
414
+
415
+ # التحقق من مجلد الإخراج
416
+ output_dir = None
417
+ if args.output_dir:
418
+ output_dir = Path(args.output_dir)
419
+ output_dir.mkdir(parents=True, exist_ok=True)
420
+ logger.info(f"سيتم حفظ الملفات في: {output_dir}")
421
+
422
+ # اكتشاف الملفات
423
+ logger.info("جاري البحث عن الملفات الصوتية...")
424
+ files = find_audio_files(args.inputs, args.recursive, args.max_size_mb)
425
+
426
+ if not files:
427
+ logger.error("❌ لم يتم العثور على ملفات صوتية للمعالجة.")
428
+ sys.exit(1)
429
+
430
+ # تحميل النموذج
431
+ logger.info(f"جارٍ تحميل نموذج whisper: {args.model} ...")
432
+ try:
433
+ model = whisper.load_model(args.model, device=args.device)
434
+ logger.info(f"✅ تم تحميل النموذج بنجاح على الجهاز: {next(model.parameters()).device}")
435
+
436
+ except Exception as e:
437
+ logger.error(f"❌ فشل تحميل النموذج: {e}")
438
+ sys.exit(1)
439
+
440
+ # معالجة الملفات
441
+ logger.info(f"🎯 تم العثور على {len(files)} ملف/ملفات — البدء بالتحويل...")
442
+
443
+ # إعداد معلمات التحويل
444
+ transcribe_kwargs = {
445
+ 'lang': args.lang,
446
+ 'output_format': args.output,
447
+ 'verbose': args.verbose,
448
+ 'output_dir': output_dir
449
+ }
450
+
451
+ # معالجة الملفات مع التقدم
452
+ start_time = time.time()
453
+ stats = process_files_with_progress(model, files, **transcribe_kwargs)
454
+ total_time = time.time() - start_time
455
+
456
+ # عرض التقرير النهائي
457
+ print("\n" + "="*50)
458
+ print("📊 تقرير التحويل النهائي")
459
+ print("="*50)
460
+ print(f"✅ الملفات الناجحة: {stats['successful']}")
461
+ print(f"❌ الملفات الفاشلة: {stats['failed']}")
462
+ print(f"📁 إجمالي الملفات: {stats['total_files']}")
463
+ print(f"🎯 معدل النجاح: {stats['success_rate']:.1f}%")
464
+ print(f"📝 إجمالي النص المنتج: {stats['total_text_length']} حرف")
465
+ print(f"⏱️ وقت المعالجة الإجمالي: {total_time:.1f} ثانية")
466
+ print(f"⚡ متوسط الوقت للملف: {total_time/len(files):.1f} ثانية")
467
+
468
+ if output_dir:
469
+ print(f"📂 مجلد الإخراج: {output_dir}")
470
+
471
+ print("="*50)
472
+
473
+ if stats['failed'] > 0:
474
+ logger.warning(f"بعض الملفات فشلت في المعالجة. راجع السجلات للتفاصيل.")
475
+ sys.exit(1)
476
+ else:
477
+ logger.info("🎉 اكتملت جميع الملفات بنجاح!")
478
+
479
+ # حفظ التقرير
480
+ if output_dir:
481
+ report_path = output_dir / "transcription_report.json"
482
+ report_data = {
483
+ 'summary': stats,
484
+ 'timestamp': datetime.now().isoformat(),
485
+ 'parameters': {
486
+ 'model': args.model,
487
+ 'language': args.lang,
488
+ 'output_format': args.output,
489
+ 'total_files': len(files)
490
+ }
491
+ }
492
+ safe_write_text(report_path, json.dumps(report_data, ensure_ascii=False, indent=2))
493
+ logger.info(f"📄 تم حفظ التقرير في: {report_path}")
494
+
495
+ if __name__ == '__main__':
496
+ main()