| """ |
| معالج الترجمات - تحويل ومزامنة وتنسيق |
| Subtitle Handler - Conversion, Sync, and Formatting |
| """ |
|
|
| import re |
| import os |
| import logging |
| from typing import Optional, Tuple |
| from dataclasses import dataclass |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| @dataclass |
| class SubtitleInfo: |
| """معلومات الترجمة""" |
| language: str |
| language_code: str |
| auto_generated: bool |
| url: str = "" |
| content: str = "" |
|
|
|
|
| class SubtitleConverter: |
| """محول الترجمات بين الصيغ المختلفة مع مزامنة دقيقة""" |
|
|
| @staticmethod |
| def vtt_to_srt(vtt_content: str) -> str: |
| """ |
| تحويل ملف VTT إلى صيغة SRT |
| مع الحفاظ على المزامنة الدقيقة والتوقيت |
| """ |
| lines = vtt_content.strip().split('\n') |
| srt_lines = [] |
| subtitle_index = 1 |
| i = 0 |
|
|
| |
| while i < len(lines) and not re.match(r'\d{2}:\d{2}', lines[i]): |
| i += 1 |
|
|
| while i < len(lines): |
| line = lines[i].strip() |
|
|
| |
| time_match = re.match( |
| r'(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}\.\d{3})', |
| line |
| ) |
| if not time_match: |
| |
| time_match = re.match( |
| r'(\d{2}:\d{2}\.\d{3})\s*-->\s*(\d{2}:\d{2}\.\d{3})', |
| line |
| ) |
| if time_match: |
| start_time = "00:" + time_match.group(1) |
| end_time = "00:" + time_match.group(2) |
| else: |
| i += 1 |
| continue |
| else: |
| start_time = time_match.group(1) |
| end_time = time_match.group(2) |
|
|
| |
| start_srt = start_time.replace('.', ',') |
| end_srt = end_time.replace('.', ',') |
|
|
| |
| i += 1 |
| text_lines = [] |
| while i < len(lines): |
| text_line = lines[i].strip() |
| if not text_line or re.match(r'\d{2}:\d{2}', text_line): |
| break |
| |
| text_line = re.sub(r'<[^>]+>', '', text_line) |
| if text_line: |
| text_lines.append(text_line) |
| i += 1 |
|
|
| if text_lines: |
| srt_lines.append(str(subtitle_index)) |
| srt_lines.append(f"{start_srt} --> {end_srt}") |
| srt_lines.extend(text_lines) |
| srt_lines.append("") |
| subtitle_index += 1 |
| else: |
| i += 1 |
|
|
| return '\n'.join(srt_lines) |
|
|
| @staticmethod |
| def srt_to_vtt(srt_content: str) -> str: |
| """تحويل ملف SRT إلى صيغة VTT""" |
| lines = srt_content.strip().split('\n') |
| vtt_lines = ["WEBVTT", ""] |
|
|
| i = 0 |
| while i < len(lines): |
| line = lines[i].strip() |
|
|
| |
| if line.isdigit(): |
| i += 1 |
| if i < len(lines): |
| time_line = lines[i].strip() |
| |
| time_line = time_line.replace(',', '.') |
| vtt_lines.append(time_line) |
| i += 1 |
|
|
| |
| while i < len(lines) and lines[i].strip(): |
| vtt_lines.append(lines[i].strip()) |
| i += 1 |
| vtt_lines.append("") |
| i += 1 |
|
|
| return '\n'.join(vtt_lines) |
|
|
| @staticmethod |
| def adjust_subtitle_timing( |
| content: str, |
| offset_ms: float = 0, |
| format: str = "srt" |
| ) -> str: |
| """ |
| تعديل توقيت الترجمة بمقدار offset بالمللي ثانية |
| للمزامنة الدقيقة مع الصوت |
| """ |
| def adjust_time(time_str: str, offset: float) -> str: |
| """تعديل وقت واحد""" |
| |
| if '.' in time_str: |
| main_part, ms_part = time_str.rsplit('.', 1) |
| elif ',' in time_str: |
| main_part, ms_part = time_str.rsplit(',', 1) |
| else: |
| return time_str |
|
|
| h, m, s = main_part.split(':') |
| total_ms = (int(h) * 3600 + int(m) * 60 + int(s)) * 1000 + int(ms_part[:3].ljust(3, '0')) |
| total_ms += int(offset) |
|
|
| if total_ms < 0: |
| total_ms = 0 |
|
|
| new_h = total_ms // 3600000 |
| total_ms %= 3600000 |
| new_m = total_ms // 60000 |
| total_ms %= 60000 |
| new_s = total_ms // 1000 |
| new_ms = total_ms % 1000 |
|
|
| sep = '.' if format == 'vtt' else ',' |
| return f"{new_h:02d}:{new_m:02d}:{new_s:02d}{sep}{new_ms:03d}" |
|
|
| lines = content.split('\n') |
| adjusted_lines = [] |
|
|
| for line in lines: |
| |
| time_match = re.match( |
| r'(\d{2}:\d{2}[:\.]\d{2}[\.\,]\d{3})\s*-->\s*(\d{2}:\d{2}[:\.]\d{2}[\.\,]\d{3})', |
| line |
| ) |
| if time_match: |
| start = adjust_time(time_match.group(1), offset_ms) |
| end = adjust_time(time_match.group(2), offset_ms) |
| adjusted_lines.append(f"{start} --> {end}") |
| else: |
| adjusted_lines.append(line) |
|
|
| return '\n'.join(adjusted_lines) |
|
|
| @staticmethod |
| def clean_subtitle(content: str) -> str: |
| """ |
| تنظيف محتوى الترجمة من العناصر غير المرغوبة |
| وإزالة التكرارات والأسطر الفارغة المتعددة |
| """ |
| |
| content = re.sub(r'<[^>]+>', '', content) |
|
|
| |
| content = re.sub(r'Kind:\s*\w+\n', '', content) |
| content = re.sub(r'Language:\s*\w+\n', '', content) |
|
|
| |
| content = re.sub(r'\n{3,}', '\n\n', content) |
|
|
| |
| lines = [line.strip() for line in content.split('\n')] |
| content = '\n'.join(lines) |
|
|
| |
| content = content.strip() |
|
|
| return content |
|
|
| @staticmethod |
| def format_subtitle(content: str, format: str = "srt") -> str: |
| """ |
| تنسيق الترجمة بشكل نظيف ومنظم |
| """ |
| |
| content = SubtitleConverter.clean_subtitle(content) |
|
|
| if format == "vtt": |
| |
| if not content.startswith("WEBVTT"): |
| content = "WEBVTT\n\n" + content |
| |
| content = content.replace(',', '.') |
| elif format == "srt": |
| |
| content = re.sub(r'^WEBVTT.*?\n\n', '', content, flags=re.DOTALL) |
| |
| content = content.replace('.', ',') |
|
|
| return content |
|
|
| @staticmethod |
| def save_subtitle( |
| content: str, |
| filepath: str, |
| format: str = "srt", |
| offset_ms: float = 0 |
| ) -> str: |
| """ |
| حفظ الترجمة في ملف بالصيغة المحددة مع المزامنة |
| """ |
| |
| if offset_ms != 0: |
| content = SubtitleConverter.adjust_subtitle_timing( |
| content, offset_ms, format |
| ) |
|
|
| |
| content = SubtitleConverter.format_subtitle(content, format) |
|
|
| |
| ext = ".vtt" if format == "vtt" else ".srt" |
| if not filepath.endswith(ext): |
| filepath = os.path.splitext(filepath)[0] + ext |
|
|
| |
| with open(filepath, 'w', encoding='utf-8') as f: |
| f.write(content) |
|
|
| logger.info(f"Subtitle saved to {filepath} (format: {format})") |
| return filepath |
|
|
|
|
| def detect_subtitle_format(content: str) -> str: |
| """كشف صيغة الترجمة تلقائياً""" |
| if content.strip().startswith("WEBVTT"): |
| return "vtt" |
| elif re.search(r'\d{2}:\d{2}:\d{2},\d{3}\s*-->', content): |
| return "srt" |
| return "unknown" |
|
|