downtube / core /subtitle_handler.py
b4x's picture
Initial commit: DownTube - YouTube Downloader v1.0
f350839
Raw
History Blame Contribute Delete
8.9 kB
"""
معالج الترجمات - تحويل ومزامنة وتنسيق
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
# تخطي رأس VTT
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)
# تحويل الفاصل العشري من نقطة إلى فاصلة لـ SRT
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
# إزالة علامات VTT
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()
# تحويل الفاصل من فاصلة إلى نقطة لـ VTT
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:
"""
تنظيف محتوى الترجمة من العناصر غير المرغوبة
وإزالة التكرارات والأسطر الفارغة المتعددة
"""
# إزالة علامات HTML/XML
content = re.sub(r'<[^>]+>', '', content)
# إزالة تعليمات VTT الإضافية
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":
# التأكد من وجود رأس VTT
if not content.startswith("WEBVTT"):
content = "WEBVTT\n\n" + content
# توحيد الفاصل العشري
content = content.replace(',', '.')
elif format == "srt":
# إزالة رأس VTT إن وجد
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"