|
|
|
|
|
|
|
""" |
|
وحدة الترجمة الصوتية متعددة اللغات لتفاصيل المشروع |
|
تتيح هذه الوحدة تحويل محتوى المشروع النصي إلى مقاطع صوتية بلغات متعددة للتسهيل على المستخدمين |
|
""" |
|
|
|
import os |
|
import sys |
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import json |
|
import base64 |
|
import tempfile |
|
import time |
|
import datetime |
|
import logging |
|
from typing import List, Dict, Any, Tuple, Optional, Union |
|
import io |
|
from io import BytesIO |
|
import re |
|
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) |
|
|
|
|
|
from utils.components.header import render_header |
|
from utils.components.credits import render_credits |
|
from utils.helpers import format_number, format_currency, styled_button |
|
|
|
|
|
class VoiceOverSystem: |
|
"""فئة نظام الترجمة الصوتية متعددة اللغات""" |
|
|
|
def __init__(self): |
|
"""تهيئة نظام الترجمة الصوتية""" |
|
|
|
self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/voice_narration")) |
|
os.makedirs(self.data_dir, exist_ok=True) |
|
|
|
|
|
self.cache_dir = os.path.join(self.data_dir, "cache") |
|
os.makedirs(self.cache_dir, exist_ok=True) |
|
|
|
|
|
self.cache_index_file = os.path.join(self.data_dir, "voice_cache_index.json") |
|
self.cache_index = self._load_cache_index() |
|
|
|
|
|
self.supported_languages = { |
|
"ar": "العربية", |
|
"en": "الإنجليزية", |
|
"fr": "الفرنسية", |
|
"es": "الإسبانية", |
|
"de": "الألمانية", |
|
"it": "الإيطالية", |
|
"zh": "الصينية", |
|
"ja": "اليابانية", |
|
"ru": "الروسية", |
|
"tr": "التركية" |
|
} |
|
|
|
|
|
self.voices_by_language = { |
|
"ar": [ |
|
{"id": "ar-female-1", "name": "فاطمة", "gender": "أنثى"}, |
|
{"id": "ar-male-1", "name": "محمد", "gender": "ذكر"}, |
|
{"id": "ar-female-2", "name": "نور", "gender": "أنثى"}, |
|
{"id": "ar-male-2", "name": "أحمد", "gender": "ذكر"} |
|
], |
|
"en": [ |
|
{"id": "en-female-1", "name": "Sarah", "gender": "أنثى"}, |
|
{"id": "en-male-1", "name": "John", "gender": "ذكر"}, |
|
{"id": "en-female-2", "name": "Emily", "gender": "أنثى"}, |
|
{"id": "en-male-2", "name": "Robert", "gender": "ذكر"} |
|
], |
|
"fr": [ |
|
{"id": "fr-female-1", "name": "Marie", "gender": "أنثى"}, |
|
{"id": "fr-male-1", "name": "Jean", "gender": "ذكر"} |
|
], |
|
"es": [ |
|
{"id": "es-female-1", "name": "Maria", "gender": "أنثى"}, |
|
{"id": "es-male-1", "name": "Carlos", "gender": "ذكر"} |
|
], |
|
"de": [ |
|
{"id": "de-female-1", "name": "Hannah", "gender": "أنثى"}, |
|
{"id": "de-male-1", "name": "Max", "gender": "ذكر"} |
|
], |
|
"it": [ |
|
{"id": "it-female-1", "name": "Sofia", "gender": "أنثى"}, |
|
{"id": "it-male-1", "name": "Marco", "gender": "ذكر"} |
|
], |
|
"zh": [ |
|
{"id": "zh-female-1", "name": "Li Wei", "gender": "أنثى"}, |
|
{"id": "zh-male-1", "name": "Zhang Wei", "gender": "ذكر"} |
|
], |
|
"ja": [ |
|
{"id": "ja-female-1", "name": "Yuki", "gender": "أنثى"}, |
|
{"id": "ja-male-1", "name": "Hiroshi", "gender": "ذكر"} |
|
], |
|
"ru": [ |
|
{"id": "ru-female-1", "name": "Olga", "gender": "أنثى"}, |
|
{"id": "ru-male-1", "name": "Ivan", "gender": "ذكر"} |
|
], |
|
"tr": [ |
|
{"id": "tr-female-1", "name": "Ayşe", "gender": "أنثى"}, |
|
{"id": "tr-male-1", "name": "Mehmet", "gender": "ذكر"} |
|
] |
|
} |
|
|
|
|
|
if "voice_settings" not in st.session_state: |
|
st.session_state.voice_settings = { |
|
"primary_language": "ar", |
|
"secondary_language": "en", |
|
"primary_voice": "ar-female-1", |
|
"secondary_voice": "en-female-1", |
|
"speaking_rate": 1.0, |
|
"pitch": 0.0, |
|
"auto_translate": True, |
|
"include_subtitles": True, |
|
"emphasis_keywords": True |
|
} |
|
|
|
|
|
self.voice_history_file = os.path.join(self.data_dir, "voice_history.json") |
|
self.voice_history = self._load_voice_history() |
|
|
|
|
|
logging.basicConfig( |
|
level=logging.INFO, |
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', |
|
handlers=[ |
|
logging.FileHandler(os.path.join(self.data_dir, "voice_narration.log")), |
|
logging.StreamHandler() |
|
] |
|
) |
|
self.logger = logging.getLogger("voice_over_system") |
|
|
|
def render(self): |
|
"""عرض واجهة نظام الترجمة الصوتية متعددة اللغات""" |
|
render_header("نظام الترجمة الصوتية متعددة اللغات") |
|
|
|
|
|
tabs = st.tabs([ |
|
"إنشاء ترجمة صوتية", |
|
"مدير الترجمات الصوتية", |
|
"إعدادات الصوت", |
|
"ترجمة مستندات كاملة", |
|
"إحصائيات ومقاييس" |
|
]) |
|
|
|
|
|
with tabs[0]: |
|
self._render_create_voice_over() |
|
|
|
|
|
with tabs[1]: |
|
self._render_voice_over_manager() |
|
|
|
|
|
with tabs[2]: |
|
self._render_voice_settings() |
|
|
|
|
|
with tabs[3]: |
|
self._render_document_narration() |
|
|
|
|
|
with tabs[4]: |
|
self._render_voice_over_analytics() |
|
|
|
|
|
render_credits() |
|
|
|
def _render_create_voice_over(self): |
|
"""عرض واجهة إنشاء ترجمة صوتية""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>🎙️ إنشاء ترجمة صوتية</h3> |
|
<p>إنشاء ترجمة صوتية لنص معين بلغات متعددة.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
content_type = st.radio( |
|
"نوع المحتوى", |
|
options=["نص حر", "بيانات مشروع", "ملخص مناقصة", "بنود عقد"], |
|
horizontal=True, |
|
key="voice_content_type" |
|
) |
|
|
|
|
|
if content_type == "نص حر": |
|
content_text = st.text_area( |
|
"النص المراد تحويله إلى صوت", |
|
height=150, |
|
placeholder="أدخل النص الذي ترغب في تحويله إلى صوت هنا...", |
|
key="voice_content_text" |
|
) |
|
|
|
title = st.text_input( |
|
"عنوان الملف الصوتي", |
|
placeholder="عنوان لتسهيل الوصول للملف الصوتي لاحقاً", |
|
key="voice_title" |
|
) |
|
|
|
elif content_type == "بيانات مشروع": |
|
|
|
projects = self._get_projects() |
|
|
|
if projects: |
|
selected_project_id = st.selectbox( |
|
"اختر المشروع", |
|
options=[p["id"] for p in projects], |
|
format_func=lambda x: next((p["name"] for p in projects if p["id"] == x), ""), |
|
key="voice_project_id" |
|
) |
|
|
|
|
|
selected_project = next((p for p in projects if p["id"] == selected_project_id), None) |
|
|
|
if selected_project: |
|
|
|
st.subheader(f"بيانات المشروع: {selected_project['name']}") |
|
|
|
project_details = f""" |
|
اسم المشروع: {selected_project['name']} |
|
رقم المشروع: {selected_project['id']} |
|
الحالة: {selected_project.get('status', 'غير محدد')} |
|
الموقع: {selected_project.get('location', 'غير محدد')} |
|
تاريخ البدء: {selected_project.get('start_date', 'غير محدد')} |
|
تاريخ الانتهاء المتوقع: {selected_project.get('expected_end_date', 'غير محدد')} |
|
الميزانية: {selected_project.get('budget', 'غير محدد')} |
|
|
|
وصف المشروع: {selected_project.get('description', 'لا يوجد وصف متاح')} |
|
""" |
|
|
|
st.text_area( |
|
"تفاصيل المشروع (يمكنك تعديلها قبل التحويل إلى صوت)", |
|
value=project_details, |
|
height=250, |
|
key="voice_project_details" |
|
) |
|
|
|
content_text = st.session_state.voice_project_details |
|
title = f"ملخص مشروع {selected_project['name']}" |
|
else: |
|
st.warning("لم يتم العثور على المشروع المحدد") |
|
content_text = "" |
|
title = "" |
|
else: |
|
st.info("لا توجد مشاريع متاحة حالياً") |
|
content_text = "" |
|
title = "" |
|
|
|
elif content_type == "ملخص مناقصة": |
|
|
|
tenders = self._get_tenders() |
|
|
|
if tenders: |
|
selected_tender_id = st.selectbox( |
|
"اختر المناقصة", |
|
options=[t["id"] for t in tenders], |
|
format_func=lambda x: next((t["name"] for t in tenders if t["id"] == x), ""), |
|
key="voice_tender_id" |
|
) |
|
|
|
|
|
selected_tender = next((t for t in tenders if t["id"] == selected_tender_id), None) |
|
|
|
if selected_tender: |
|
|
|
st.subheader(f"بيانات المناقصة: {selected_tender['name']}") |
|
|
|
tender_details = f""" |
|
اسم المناقصة: {selected_tender['name']} |
|
رقم المناقصة: {selected_tender['id']} |
|
الجهة المالكة: {selected_tender.get('owner', 'غير محدد')} |
|
تاريخ الطرح: {selected_tender.get('issue_date', 'غير محدد')} |
|
تاريخ التسليم: {selected_tender.get('submission_date', 'غير محدد')} |
|
القيمة التقديرية: {selected_tender.get('estimated_value', 'غير محدد')} |
|
|
|
وصف المناقصة: {selected_tender.get('description', 'لا يوجد وصف متاح')} |
|
""" |
|
|
|
st.text_area( |
|
"تفاصيل المناقصة (يمكنك تعديلها قبل التحويل إلى صوت)", |
|
value=tender_details, |
|
height=250, |
|
key="voice_tender_details" |
|
) |
|
|
|
content_text = st.session_state.voice_tender_details |
|
title = f"ملخص مناقصة {selected_tender['name']}" |
|
else: |
|
st.warning("لم يتم العثور على المناقصة المحددة") |
|
content_text = "" |
|
title = "" |
|
else: |
|
st.info("لا توجد مناقصات متاحة حالياً") |
|
content_text = "" |
|
title = "" |
|
|
|
elif content_type == "بنود عقد": |
|
|
|
contracts = self._get_contracts() |
|
|
|
if contracts: |
|
selected_contract_id = st.selectbox( |
|
"اختر العقد", |
|
options=[c["id"] for c in contracts], |
|
format_func=lambda x: next((c["name"] for c in contracts if c["id"] == x), ""), |
|
key="voice_contract_id" |
|
) |
|
|
|
|
|
selected_contract = next((c for c in contracts if c["id"] == selected_contract_id), None) |
|
|
|
if selected_contract: |
|
|
|
st.subheader(f"بيانات العقد: {selected_contract['name']}") |
|
|
|
|
|
contract_clauses = selected_contract.get("clauses", []) |
|
|
|
if contract_clauses: |
|
|
|
selected_clauses = st.multiselect( |
|
"اختر البنود المراد تحويلها إلى صوت", |
|
options=list(range(len(contract_clauses))), |
|
format_func=lambda i: f"البند {i+1}: {contract_clauses[i]['title']}", |
|
key="voice_contract_clauses" |
|
) |
|
|
|
if selected_clauses: |
|
|
|
clauses_text = "" |
|
for i in selected_clauses: |
|
clauses_text += f"البند {i+1}: {contract_clauses[i]['title']}\n" |
|
clauses_text += f"{contract_clauses[i]['content']}\n\n" |
|
|
|
st.text_area( |
|
"نص البنود المختارة (يمكنك تعديلها قبل التحويل إلى صوت)", |
|
value=clauses_text, |
|
height=250, |
|
key="voice_contract_text" |
|
) |
|
|
|
content_text = st.session_state.voice_contract_text |
|
title = f"بنود من عقد {selected_contract['name']}" |
|
else: |
|
st.info("الرجاء اختيار بند واحد على الأقل") |
|
content_text = "" |
|
title = "" |
|
else: |
|
st.info("لا توجد بنود متاحة لهذا العقد") |
|
content_text = "" |
|
title = "" |
|
else: |
|
st.warning("لم يتم العثور على العقد المحدد") |
|
content_text = "" |
|
title = "" |
|
else: |
|
st.info("لا توجد عقود متاحة حالياً") |
|
content_text = "" |
|
title = "" |
|
|
|
|
|
st.markdown("### إعدادات اللغة") |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
source_language = st.selectbox( |
|
"لغة النص المصدر", |
|
options=list(self.supported_languages.keys()), |
|
format_func=lambda x: self.supported_languages[x], |
|
index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), |
|
key="voice_source_language" |
|
) |
|
|
|
voice_id = st.selectbox( |
|
"الصوت", |
|
options=[v["id"] for v in self.voices_by_language[source_language]], |
|
format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[source_language] if v["id"] == x), ""), |
|
index=0, |
|
key="voice_source_voice" |
|
) |
|
|
|
with col2: |
|
target_language = st.selectbox( |
|
"لغة الترجمة (اختياري)", |
|
options=["none"] + list(self.supported_languages.keys()), |
|
format_func=lambda x: "بدون ترجمة" if x == "none" else self.supported_languages[x], |
|
index=0, |
|
key="voice_target_language" |
|
) |
|
|
|
if target_language != "none": |
|
target_voice_id = st.selectbox( |
|
"صوت الترجمة", |
|
options=[v["id"] for v in self.voices_by_language[target_language]], |
|
format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[target_language] if v["id"] == x), ""), |
|
index=0, |
|
key="voice_target_voice" |
|
) |
|
else: |
|
target_voice_id = None |
|
|
|
|
|
with st.expander("خيارات متقدمة"): |
|
advanced_col1, advanced_col2 = st.columns(2) |
|
|
|
with advanced_col1: |
|
speaking_rate = st.slider( |
|
"سرعة النطق", |
|
min_value=0.5, |
|
max_value=2.0, |
|
value=st.session_state.voice_settings["speaking_rate"], |
|
step=0.1, |
|
key="voice_speaking_rate" |
|
) |
|
|
|
include_subtitles = st.checkbox( |
|
"تضمين النص مع الصوت", |
|
value=st.session_state.voice_settings["include_subtitles"], |
|
key="voice_include_subtitles" |
|
) |
|
|
|
with advanced_col2: |
|
pitch = st.slider( |
|
"درجة الصوت", |
|
min_value=-10.0, |
|
max_value=10.0, |
|
value=st.session_state.voice_settings["pitch"], |
|
step=1.0, |
|
key="voice_pitch" |
|
) |
|
|
|
emphasize_keywords = st.checkbox( |
|
"تمييز الكلمات المهمة", |
|
value=st.session_state.voice_settings["emphasis_keywords"], |
|
key="voice_emphasize_keywords" |
|
) |
|
|
|
|
|
create_voice_over = False |
|
|
|
if content_text: |
|
|
|
if styled_button("إنشاء الترجمة الصوتية", key="create_voice_over_btn", type="primary", icon="🎙️"): |
|
if title: |
|
create_voice_over = True |
|
else: |
|
st.warning("الرجاء إدخال عنوان للملف الصوتي") |
|
else: |
|
st.info("الرجاء إدخال أو اختيار محتوى للترجمة الصوتية") |
|
|
|
|
|
if create_voice_over: |
|
with st.spinner("جاري إنشاء الترجمة الصوتية..."): |
|
try: |
|
|
|
cache_key = self._generate_cache_key( |
|
content_text, |
|
source_language, |
|
voice_id, |
|
speaking_rate, |
|
pitch |
|
) |
|
|
|
cached_file = self._get_from_cache(cache_key) |
|
|
|
if cached_file: |
|
st.success("تم استرجاع الترجمة الصوتية من الكاش") |
|
audio_file = cached_file |
|
audio_duration = self._get_audio_duration(audio_file) |
|
else: |
|
|
|
audio_file, audio_duration = self._generate_voice_over( |
|
content_text, |
|
source_language, |
|
voice_id, |
|
speaking_rate, |
|
pitch |
|
) |
|
|
|
|
|
self._add_to_cache(cache_key, audio_file) |
|
|
|
|
|
voice_over_id = self._add_voice_to_history( |
|
title=title, |
|
content=content_text, |
|
source_language=source_language, |
|
voice_id=voice_id, |
|
duration=audio_duration, |
|
audio_file=os.path.basename(audio_file), |
|
content_type=content_type |
|
) |
|
|
|
|
|
if target_language != "none": |
|
with st.spinner(f"جاري الترجمة إلى {self.supported_languages[target_language]}..."): |
|
|
|
translated_text = self._translate_text( |
|
content_text, |
|
source_language, |
|
target_language |
|
) |
|
|
|
|
|
translated_cache_key = self._generate_cache_key( |
|
translated_text, |
|
target_language, |
|
target_voice_id, |
|
speaking_rate, |
|
pitch |
|
) |
|
|
|
cached_translated_file = self._get_from_cache(translated_cache_key) |
|
|
|
if cached_translated_file: |
|
st.success("تم استرجاع الترجمة الصوتية المترجمة من الكاش") |
|
translated_audio_file = cached_translated_file |
|
translated_audio_duration = self._get_audio_duration(translated_audio_file) |
|
else: |
|
|
|
translated_audio_file, translated_audio_duration = self._generate_voice_over( |
|
translated_text, |
|
target_language, |
|
target_voice_id, |
|
speaking_rate, |
|
pitch |
|
) |
|
|
|
|
|
self._add_to_cache(translated_cache_key, translated_audio_file) |
|
|
|
|
|
translated_voice_over_id = self._add_voice_to_history( |
|
title=f"{title} ({self.supported_languages[target_language]})", |
|
content=translated_text, |
|
source_language=target_language, |
|
voice_id=target_voice_id, |
|
duration=translated_audio_duration, |
|
audio_file=os.path.basename(translated_audio_file), |
|
content_type=content_type, |
|
is_translation=True, |
|
original_id=voice_over_id |
|
) |
|
|
|
|
|
st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[target_language]}") |
|
|
|
|
|
if include_subtitles: |
|
st.markdown(f"**النص المترجم:**\n{translated_text}") |
|
|
|
|
|
self._display_audio_player(translated_audio_file) |
|
|
|
|
|
st.subheader(f"الترجمة الصوتية بـ{self.supported_languages[source_language]}") |
|
|
|
|
|
if include_subtitles: |
|
st.markdown(f"**النص:**\n{content_text}") |
|
|
|
|
|
self._display_audio_player(audio_file) |
|
|
|
|
|
with open(audio_file, "rb") as f: |
|
audio_bytes = f.read() |
|
|
|
st.download_button( |
|
label="تنزيل الملف الصوتي", |
|
data=audio_bytes, |
|
file_name=f"{title}.mp3", |
|
mime="audio/mpeg", |
|
key="download_voice_over" |
|
) |
|
|
|
st.success("تم إنشاء الترجمة الصوتية بنجاح!") |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء إنشاء الترجمة الصوتية: {str(e)}") |
|
self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}") |
|
|
|
def _render_voice_over_manager(self): |
|
"""عرض واجهة مدير الترجمات الصوتية""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>🎧 مدير الترجمات الصوتية</h3> |
|
<p>استعراض وإدارة الترجمات الصوتية المخزنة.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
self.voice_history = self._load_voice_history() |
|
|
|
|
|
if not self.voice_history: |
|
st.info("لا توجد ترجمات صوتية مخزنة.") |
|
return |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
content_types = ["الكل"] + list(set(item.get("content_type", "نص حر") for item in self.voice_history)) |
|
filter_content_type = st.selectbox( |
|
"فلترة حسب نوع المحتوى", |
|
options=content_types, |
|
key="filter_content_type" |
|
) |
|
|
|
with col2: |
|
|
|
languages = ["الكل"] + [self.supported_languages.get(item.get("source_language", "ar"), "العربية") for item in self.voice_history] |
|
filter_language = st.selectbox( |
|
"فلترة حسب اللغة", |
|
options=list(set(languages)), |
|
key="filter_language" |
|
) |
|
|
|
|
|
filtered_history = self.voice_history |
|
|
|
if filter_content_type != "الكل": |
|
filtered_history = [item for item in filtered_history if item.get("content_type", "نص حر") == filter_content_type] |
|
|
|
if filter_language != "الكل": |
|
filtered_history = [ |
|
item for item in filtered_history |
|
if self.supported_languages.get(item.get("source_language", "ar"), "العربية") == filter_language |
|
] |
|
|
|
|
|
for voice_item in filtered_history: |
|
with st.expander(f"{voice_item['title']} ({voice_item.get('created_at', 'تاريخ غير معروف')})", expanded=False): |
|
|
|
item_col1, item_col2 = st.columns([3, 1]) |
|
|
|
with item_col1: |
|
st.markdown(f"**النوع:** {voice_item.get('content_type', 'نص حر')}") |
|
st.markdown(f"**اللغة:** {self.supported_languages.get(voice_item.get('source_language', 'ar'), 'العربية')}") |
|
st.markdown(f"**المدة:** {voice_item.get('duration', 0):.2f} ثانية") |
|
|
|
|
|
audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', '')) |
|
if os.path.exists(audio_file_path): |
|
self._display_audio_player(audio_file_path) |
|
else: |
|
st.warning("ملف الصوت غير متوفر") |
|
|
|
with item_col2: |
|
|
|
if st.button("عرض النص", key=f"show_text_{voice_item.get('id', '')}"): |
|
st.text_area( |
|
"نص الترجمة الصوتية", |
|
value=voice_item.get('content', ''), |
|
height=150, |
|
key=f"text_{voice_item.get('id', '')}", |
|
disabled=True |
|
) |
|
|
|
|
|
audio_file_path = os.path.join(self.data_dir, voice_item.get('audio_file', '')) |
|
if os.path.exists(audio_file_path): |
|
with open(audio_file_path, "rb") as f: |
|
audio_bytes = f.read() |
|
|
|
st.download_button( |
|
label="تنزيل الملف الصوتي", |
|
data=audio_bytes, |
|
file_name=f"{voice_item['title']}.mp3", |
|
mime="audio/mpeg", |
|
key=f"download_{voice_item.get('id', '')}" |
|
) |
|
|
|
|
|
if st.button("حذف", key=f"delete_{voice_item.get('id', '')}", type="primary"): |
|
if self._delete_voice_from_history(voice_item.get('id', '')): |
|
st.success("تم حذف الترجمة الصوتية بنجاح!") |
|
st.rerun() |
|
else: |
|
st.error("حدث خطأ أثناء حذف الترجمة الصوتية") |
|
|
|
def _render_voice_settings(self): |
|
"""عرض واجهة إعدادات الصوت""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>⚙️ إعدادات الصوت</h3> |
|
<p>تخصيص إعدادات الترجمة الصوتية الافتراضية.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("### إعدادات اللغة") |
|
|
|
lang_col1, lang_col2 = st.columns(2) |
|
|
|
with lang_col1: |
|
|
|
primary_language = st.selectbox( |
|
"اللغة الأساسية", |
|
options=list(self.supported_languages.keys()), |
|
format_func=lambda x: self.supported_languages[x], |
|
index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), |
|
key="settings_primary_language" |
|
) |
|
|
|
|
|
primary_voice = st.selectbox( |
|
"الصوت الأساسي", |
|
options=[v["id"] for v in self.voices_by_language[primary_language]], |
|
format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[primary_language] if v["id"] == x), ""), |
|
index=0, |
|
key="settings_primary_voice" |
|
) |
|
|
|
with lang_col2: |
|
|
|
secondary_language = st.selectbox( |
|
"اللغة الثانوية", |
|
options=list(self.supported_languages.keys()), |
|
format_func=lambda x: self.supported_languages[x], |
|
index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["secondary_language"]), |
|
key="settings_secondary_language" |
|
) |
|
|
|
|
|
secondary_voice = st.selectbox( |
|
"الصوت الثانوي", |
|
options=[v["id"] for v in self.voices_by_language[secondary_language]], |
|
format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[secondary_language] if v["id"] == x), ""), |
|
index=0, |
|
key="settings_secondary_voice" |
|
) |
|
|
|
|
|
st.markdown("### إعدادات جودة الصوت") |
|
|
|
quality_col1, quality_col2 = st.columns(2) |
|
|
|
with quality_col1: |
|
|
|
speaking_rate = st.slider( |
|
"سرعة النطق الافتراضية", |
|
min_value=0.5, |
|
max_value=2.0, |
|
value=st.session_state.voice_settings["speaking_rate"], |
|
step=0.1, |
|
key="settings_speaking_rate" |
|
) |
|
|
|
with quality_col2: |
|
|
|
pitch = st.slider( |
|
"درجة الصوت الافتراضية", |
|
min_value=-10.0, |
|
max_value=10.0, |
|
value=st.session_state.voice_settings["pitch"], |
|
step=1.0, |
|
key="settings_pitch" |
|
) |
|
|
|
|
|
st.markdown("### إعدادات أخرى") |
|
|
|
other_col1, other_col2 = st.columns(2) |
|
|
|
with other_col1: |
|
|
|
auto_translate = st.checkbox( |
|
"ترجمة تلقائية إلى اللغة الثانوية", |
|
value=st.session_state.voice_settings["auto_translate"], |
|
key="settings_auto_translate" |
|
) |
|
|
|
|
|
include_subtitles = st.checkbox( |
|
"تضمين النص مع الصوت افتراضياً", |
|
value=st.session_state.voice_settings["include_subtitles"], |
|
key="settings_include_subtitles" |
|
) |
|
|
|
with other_col2: |
|
|
|
emphasis_keywords = st.checkbox( |
|
"تمييز الكلمات المهمة تلقائياً", |
|
value=st.session_state.voice_settings["emphasis_keywords"], |
|
key="settings_emphasis_keywords" |
|
) |
|
|
|
|
|
if styled_button("حفظ الإعدادات", key="save_voice_settings", type="primary", icon="💾"): |
|
|
|
st.session_state.voice_settings = { |
|
"primary_language": primary_language, |
|
"secondary_language": secondary_language, |
|
"primary_voice": primary_voice, |
|
"secondary_voice": secondary_voice, |
|
"speaking_rate": speaking_rate, |
|
"pitch": pitch, |
|
"auto_translate": auto_translate, |
|
"include_subtitles": include_subtitles, |
|
"emphasis_keywords": emphasis_keywords |
|
} |
|
|
|
|
|
self._save_voice_settings() |
|
|
|
st.success("تم حفظ الإعدادات بنجاح!") |
|
|
|
|
|
with st.expander("إعدادات متقدمة", expanded=False): |
|
st.markdown("### إعدادات الكاش") |
|
|
|
cache_size = self._get_cache_size() |
|
st.markdown(f"حجم الكاش الحالي: {cache_size / (1024 * 1024):.2f} ميجابايت") |
|
|
|
if styled_button("مسح الكاش", key="clear_cache", type="danger", icon="🗑️"): |
|
if self._clear_cache(): |
|
st.success("تم مسح الكاش بنجاح!") |
|
else: |
|
st.error("حدث خطأ أثناء مسح الكاش") |
|
|
|
st.markdown("### إعدادات API") |
|
|
|
|
|
api_model = st.selectbox( |
|
"نموذج API للترجمة الصوتية", |
|
options=["local", "huggingface", "google", "amazon", "microsoft"], |
|
format_func=lambda x: { |
|
"local": "محلي (عرض توضيحي)", |
|
"huggingface": "Hugging Face", |
|
"google": "Google Cloud Text-to-Speech", |
|
"amazon": "Amazon Polly", |
|
"microsoft": "Microsoft Azure" |
|
}[x], |
|
index=0, |
|
key="api_model" |
|
) |
|
|
|
|
|
api_info = { |
|
"local": "هذا وضع العرض التوضيحي حيث يتم تشبيه الترجمة الصوتية دون الحاجة إلى اتصال API خارجي.", |
|
"huggingface": "استخدام Hugging Face API لتحويل النص إلى صوت وترجمة النصوص.", |
|
"google": "استخدام Google Cloud Text-to-Speech لإنتاج صوت عالي الجودة.", |
|
"amazon": "استخدام Amazon Polly للترجمة الصوتية بجودة عالية ومجموعة متنوعة من الأصوات.", |
|
"microsoft": "استخدام Microsoft Azure Speech Services للترجمة الصوتية والترجمة." |
|
} |
|
|
|
st.markdown(f"**معلومات:** {api_info[api_model]}") |
|
|
|
if api_model != "local": |
|
api_key = st.text_input( |
|
f"مفتاح API لـ {api_model}", |
|
type="password", |
|
key=f"{api_model}_api_key" |
|
) |
|
|
|
if styled_button("حفظ مفتاح API", key="save_api_key", type="primary"): |
|
st.success(f"تم حفظ مفتاح API لـ {api_model} بنجاح!") |
|
|
|
def _render_document_narration(self): |
|
"""عرض واجهة ترجمة مستندات كاملة""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>📄 ترجمة مستندات كاملة</h3> |
|
<p>تحويل مستندات كاملة إلى ملفات صوتية وتقسيمها إلى فصول أو أقسام.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
documents = self._get_documents() |
|
|
|
if documents: |
|
selected_document_id = st.selectbox( |
|
"اختر المستند", |
|
options=[d["id"] for d in documents], |
|
format_func=lambda x: next((d["name"] for d in documents if d["id"] == x), ""), |
|
key="narration_document_id" |
|
) |
|
|
|
|
|
selected_document = next((d for d in documents if d["id"] == selected_document_id), None) |
|
|
|
if selected_document: |
|
|
|
st.subheader(f"معلومات المستند: {selected_document['name']}") |
|
|
|
doc_col1, doc_col2 = st.columns(2) |
|
|
|
with doc_col1: |
|
st.markdown(f"**النوع:** {selected_document.get('type', 'غير محدد')}") |
|
st.markdown(f"**عدد الصفحات:** {selected_document.get('page_count', 'غير محدد')}") |
|
|
|
with doc_col2: |
|
st.markdown(f"**حجم المستند:** {selected_document.get('file_size', 'غير محدد')}") |
|
st.markdown(f"**تاريخ الرفع:** {selected_document.get('upload_date', 'غير محدد')}") |
|
|
|
|
|
st.markdown("### خيارات الترجمة الصوتية") |
|
|
|
options_col1, options_col2 = st.columns(2) |
|
|
|
with options_col1: |
|
|
|
narration_language = st.selectbox( |
|
"لغة الترجمة الصوتية", |
|
options=list(self.supported_languages.keys()), |
|
format_func=lambda x: self.supported_languages[x], |
|
index=list(self.supported_languages.keys()).index(st.session_state.voice_settings["primary_language"]), |
|
key="narration_language" |
|
) |
|
|
|
|
|
narration_voice = st.selectbox( |
|
"الصوت", |
|
options=[v["id"] for v in self.voices_by_language[narration_language]], |
|
format_func=lambda x: next((v["name"] + f" ({v['gender']})" for v in self.voices_by_language[narration_language] if v["id"] == x), ""), |
|
index=0, |
|
key="narration_voice" |
|
) |
|
|
|
|
|
narration_split = st.selectbox( |
|
"تقسيم المستند", |
|
options=["لا تقسيم", "حسب الصفحات", "حسب العناوين", "حسب الفصول"], |
|
key="narration_split" |
|
) |
|
|
|
with options_col2: |
|
|
|
narration_speaking_rate = st.slider( |
|
"سرعة النطق", |
|
min_value=0.5, |
|
max_value=2.0, |
|
value=st.session_state.voice_settings["speaking_rate"], |
|
step=0.1, |
|
key="narration_speaking_rate" |
|
) |
|
|
|
|
|
narration_pitch = st.slider( |
|
"درجة الصوت", |
|
min_value=-10.0, |
|
max_value=10.0, |
|
value=st.session_state.voice_settings["pitch"], |
|
step=1.0, |
|
key="narration_pitch" |
|
) |
|
|
|
|
|
narration_include_toc = st.checkbox( |
|
"تضمين فهرس صوتي", |
|
value=True, |
|
key="narration_include_toc" |
|
) |
|
|
|
|
|
with st.expander("خيارات إضافية", expanded=False): |
|
|
|
narration_skip_pages = st.text_input( |
|
"تجاهل الصفحات (أرقام مفصولة بفواصل)", |
|
placeholder="مثال: 1,2,5-7", |
|
key="narration_skip_pages" |
|
) |
|
|
|
|
|
narration_intro = st.text_area( |
|
"مقدمة خاصة (سيتم إضافتها في بداية الترجمة الصوتية)", |
|
placeholder="مقدمة اختيارية...", |
|
key="narration_intro" |
|
) |
|
|
|
|
|
narration_outro = st.text_area( |
|
"خاتمة خاصة (سيتم إضافتها في نهاية الترجمة الصوتية)", |
|
placeholder="خاتمة اختيارية...", |
|
key="narration_outro" |
|
) |
|
|
|
|
|
if styled_button("إنشاء الترجمة الصوتية للمستند", key="create_document_narration", type="primary", icon="🎙️"): |
|
|
|
narration_folder = os.path.join(self.data_dir, "document_narrations", str(selected_document_id)) |
|
os.makedirs(narration_folder, exist_ok=True) |
|
|
|
|
|
progress_bar = st.progress(0) |
|
status_text = st.empty() |
|
|
|
|
|
total_sections = 5 |
|
|
|
for i in range(total_sections + 1): |
|
|
|
progress = i / total_sections |
|
progress_bar.progress(progress) |
|
|
|
if i == 0: |
|
status_text.text("جاري تحليل المستند...") |
|
elif i == 1: |
|
status_text.text("جاري استخراج النص...") |
|
elif i < total_sections: |
|
status_text.text(f"جاري إنشاء الترجمة الصوتية للقسم {i}...") |
|
else: |
|
status_text.text("جاري تجميع الملفات الصوتية النهائية...") |
|
|
|
time.sleep(1) |
|
|
|
|
|
progress_bar.progress(1.0) |
|
status_text.text("تم إنشاء الترجمة الصوتية بنجاح!") |
|
|
|
|
|
st.subheader("الترجمة الصوتية للمستند") |
|
|
|
|
|
for i in range(1, total_sections): |
|
with st.expander(f"القسم {i}: العنوان الافتراضي {i}", expanded=i==1): |
|
|
|
st.audio("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3") |
|
|
|
|
|
st.markdown(f"**المدة:** {i + 2} دقائق") |
|
st.markdown(f"**عدد الكلمات:** {i * 100} كلمة") |
|
|
|
|
|
st.download_button( |
|
label="تنزيل الترجمة الصوتية الكاملة", |
|
data=b"Mock audio file", |
|
file_name=f"{selected_document['name']}_narration.mp3", |
|
mime="audio/mpeg", |
|
key="download_full_narration" |
|
) |
|
else: |
|
st.warning("لم يتم العثور على المستند المحدد") |
|
else: |
|
st.info("لا توجد مستندات متاحة حالياً") |
|
|
|
|
|
st.markdown("### رفع مستند جديد للترجمة الصوتية") |
|
|
|
uploaded_file = st.file_uploader( |
|
"اختر ملف للرفع (PDF, DOCX, TXT)", |
|
type=["pdf", "docx", "txt"], |
|
key="narration_upload_file" |
|
) |
|
|
|
if uploaded_file: |
|
file_name = uploaded_file.name |
|
|
|
|
|
st.markdown(f"**اسم الملف:** {file_name}") |
|
st.markdown(f"**حجم الملف:** {uploaded_file.size / 1024:.2f} كيلوبايت") |
|
|
|
document_name = st.text_input( |
|
"اسم المستند", |
|
value=file_name, |
|
key="narration_document_name" |
|
) |
|
|
|
if styled_button("رفع المستند", key="upload_document", type="primary", icon="📤"): |
|
st.success(f"تم رفع المستند '{document_name}' بنجاح!") |
|
st.info("يمكنك الآن اختيار المستند لإنشاء ترجمة صوتية له.") |
|
st.rerun() |
|
|
|
def _render_voice_over_analytics(self): |
|
"""عرض إحصائيات ومقاييس الترجمات الصوتية""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>📊 إحصائيات ومقاييس</h3> |
|
<p>إحصائيات ومقاييس استخدام نظام الترجمة الصوتية.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
self.voice_history = self._load_voice_history() |
|
|
|
|
|
if not self.voice_history: |
|
st.info("لا توجد ترجمات صوتية مخزنة.") |
|
return |
|
|
|
|
|
st.markdown("### إحصائيات عامة") |
|
|
|
|
|
total_voices = len(self.voice_history) |
|
total_duration = sum(item.get("duration", 0) for item in self.voice_history) |
|
total_languages = len(set(item.get("source_language", "ar") for item in self.voice_history)) |
|
|
|
|
|
stats_col1, stats_col2, stats_col3 = st.columns(3) |
|
|
|
with stats_col1: |
|
st.metric("إجمالي الترجمات الصوتية", total_voices) |
|
|
|
with stats_col2: |
|
st.metric("إجمالي المدة", f"{total_duration:.2f} ثانية") |
|
|
|
with stats_col3: |
|
st.metric("عدد اللغات المستخدمة", total_languages) |
|
|
|
|
|
st.markdown("### توزيع الترجمات الصوتية حسب اللغة") |
|
|
|
language_counts = {} |
|
for item in self.voice_history: |
|
lang = item.get("source_language", "ar") |
|
lang_name = self.supported_languages.get(lang, "غير معروف") |
|
language_counts[lang_name] = language_counts.get(lang_name, 0) + 1 |
|
|
|
|
|
language_df = pd.DataFrame({ |
|
"اللغة": list(language_counts.keys()), |
|
"العدد": list(language_counts.values()) |
|
}) |
|
|
|
|
|
import plotly.express as px |
|
|
|
fig1 = px.pie( |
|
language_df, |
|
values="العدد", |
|
names="اللغة", |
|
title="توزيع الترجمات الصوتية حسب اللغة", |
|
color_discrete_sequence=px.colors.qualitative.Pastel |
|
) |
|
|
|
fig1.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig1, use_container_width=True) |
|
|
|
|
|
st.markdown("### توزيع الترجمات الصوتية حسب نوع المحتوى") |
|
|
|
content_type_counts = {} |
|
for item in self.voice_history: |
|
content_type = item.get("content_type", "نص حر") |
|
content_type_counts[content_type] = content_type_counts.get(content_type, 0) + 1 |
|
|
|
|
|
content_df = pd.DataFrame({ |
|
"نوع المحتوى": list(content_type_counts.keys()), |
|
"العدد": list(content_type_counts.values()) |
|
}) |
|
|
|
|
|
fig2 = px.bar( |
|
content_df, |
|
x="نوع المحتوى", |
|
y="العدد", |
|
title="توزيع الترجمات الصوتية حسب نوع المحتوى", |
|
color="نوع المحتوى", |
|
color_discrete_sequence=px.colors.qualitative.Pastel |
|
) |
|
|
|
fig2.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig2, use_container_width=True) |
|
|
|
|
|
st.markdown("### توزيع الترجمات الصوتية حسب التاريخ") |
|
|
|
|
|
dates = [] |
|
for item in self.voice_history: |
|
created_at = item.get("created_at", "") |
|
if created_at: |
|
try: |
|
date = datetime.datetime.strptime(created_at.split(" ")[0], "%Y-%m-%d").date() |
|
dates.append(date) |
|
except (ValueError, IndexError): |
|
continue |
|
|
|
if dates: |
|
|
|
date_counts = {} |
|
for date in dates: |
|
date_str = date.strftime("%Y-%m-%d") |
|
date_counts[date_str] = date_counts.get(date_str, 0) + 1 |
|
|
|
|
|
date_df = pd.DataFrame({ |
|
"التاريخ": list(date_counts.keys()), |
|
"العدد": list(date_counts.values()) |
|
}) |
|
|
|
|
|
date_df["التاريخ"] = pd.to_datetime(date_df["التاريخ"]) |
|
date_df = date_df.sort_values("التاريخ") |
|
|
|
|
|
fig3 = px.line( |
|
date_df, |
|
x="التاريخ", |
|
y="العدد", |
|
title="توزيع الترجمات الصوتية حسب التاريخ", |
|
markers=True |
|
) |
|
|
|
fig3.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig3, use_container_width=True) |
|
else: |
|
st.info("لا توجد بيانات تاريخ كافية لعرض الرسم البياني") |
|
|
|
|
|
st.markdown("### تصدير البيانات") |
|
|
|
export_col1, export_col2 = st.columns(2) |
|
|
|
with export_col1: |
|
if styled_button("تصدير CSV", key="export_voice_csv", type="primary", icon="📄"): |
|
|
|
export_df = pd.DataFrame(self.voice_history) |
|
|
|
|
|
csv_data = export_df.to_csv(index=False) |
|
|
|
st.download_button( |
|
label="تنزيل ملف CSV", |
|
data=csv_data, |
|
file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.csv", |
|
mime="text/csv", |
|
key="download_voice_csv" |
|
) |
|
|
|
with export_col2: |
|
if styled_button("تصدير JSON", key="export_voice_json", type="primary", icon="📄"): |
|
|
|
json_data = json.dumps(self.voice_history, indent=2) |
|
|
|
st.download_button( |
|
label="تنزيل ملف JSON", |
|
data=json_data, |
|
file_name=f"voice_over_history_{datetime.datetime.now().strftime('%Y%m%d')}.json", |
|
mime="application/json", |
|
key="download_voice_json" |
|
) |
|
|
|
def _generate_voice_over(self, text, language, voice_id, speaking_rate=1.0, pitch=0.0): |
|
""" |
|
إنشاء ترجمة صوتية (محاكاة) |
|
|
|
المعلمات: |
|
text: النص المراد تحويله إلى صوت |
|
language: رمز اللغة |
|
voice_id: معرف الصوت |
|
speaking_rate: سرعة النطق |
|
pitch: درجة الصوت |
|
|
|
الإرجاع: |
|
مسار الملف الصوتي ومدته |
|
""" |
|
try: |
|
|
|
|
|
|
|
|
|
temp_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) |
|
temp_file.close() |
|
|
|
|
|
audio_file = os.path.join(self.data_dir, f"voice_{language}_{voice_id}_{int(time.time())}.mp3") |
|
|
|
|
|
|
|
import wave |
|
import struct |
|
|
|
|
|
duration = len(text) * 0.1 |
|
sample_rate = 44100 |
|
|
|
|
|
duration = duration / speaking_rate |
|
|
|
|
|
wav_file = temp_file.name.replace(".mp3", ".wav") |
|
|
|
with wave.open(wav_file, "w") as f: |
|
f.setnchannels(1) |
|
f.setsampwidth(2) |
|
f.setframerate(sample_rate) |
|
|
|
|
|
for i in range(int(duration * sample_rate)): |
|
|
|
value = 32767 * 0.3 * np.sin(2 * np.pi * (440 + pitch * 20) * i / sample_rate) |
|
f.writeframes(struct.pack('h', int(value))) |
|
|
|
|
|
|
|
import shutil |
|
shutil.copy(wav_file, audio_file) |
|
|
|
|
|
try: |
|
os.remove(wav_file) |
|
os.remove(temp_file.name) |
|
except: |
|
pass |
|
|
|
|
|
self.logger.info(f"تم إنشاء ترجمة صوتية للنص (طول: {len(text)}) باللغة: {language}") |
|
|
|
return audio_file, duration |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في إنشاء الترجمة الصوتية: {str(e)}") |
|
raise e |
|
|
|
def _translate_text(self, text, source_language, target_language): |
|
""" |
|
ترجمة نص من لغة إلى أخرى (محاكاة) |
|
|
|
المعلمات: |
|
text: النص المراد ترجمته |
|
source_language: رمز اللغة المصدر |
|
target_language: رمز اللغة الهدف |
|
|
|
الإرجاع: |
|
النص المترجم |
|
""" |
|
try: |
|
|
|
|
|
|
|
|
|
self.logger.info(f"ترجمة نص (طول: {len(text)}) من {source_language} إلى {target_language}") |
|
|
|
|
|
translated_prefix = { |
|
"en": "This is a sample translation of the text into English.", |
|
"ar": "هذه ترجمة عينة للنص إلى اللغة العربية.", |
|
"fr": "Ceci est un exemple de traduction du texte en français.", |
|
"es": "Esta es una traducción de muestra del texto al español.", |
|
"de": "Dies ist eine Beispielübersetzung des Textes ins Deutsche.", |
|
"it": "Questa è una traduzione di esempio del testo in italiano.", |
|
"zh": "这是文本翻译成中文的示例。", |
|
"ja": "これはテキストの日本語への翻訳例です。", |
|
"ru": "Это пример перевода текста на русский язык.", |
|
"tr": "Bu, metnin Türkçe çevirisinin bir örneğidir." |
|
} |
|
|
|
|
|
return f"{translated_prefix.get(target_language, 'Translated sample')} {text[:100]}..." |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في ترجمة النص: {str(e)}") |
|
raise e |
|
|
|
def _get_audio_duration(self, audio_file): |
|
""" |
|
الحصول على مدة ملف صوتي |
|
|
|
المعلمات: |
|
audio_file: مسار الملف الصوتي |
|
|
|
الإرجاع: |
|
مدة الملف الصوتي بالثواني |
|
""" |
|
try: |
|
if audio_file.endswith(".wav"): |
|
|
|
with wave.open(audio_file, "rb") as f: |
|
frames = f.getnframes() |
|
rate = f.getframerate() |
|
duration = frames / float(rate) |
|
else: |
|
|
|
size_in_bytes = os.path.getsize(audio_file) |
|
duration = size_in_bytes / 16000 |
|
|
|
return duration |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في الحصول على مدة الملف الصوتي: {str(e)}") |
|
return 30.0 |
|
|
|
def _get_from_cache(self, cache_key): |
|
""" |
|
البحث عن ملف في الكاش |
|
|
|
المعلمات: |
|
cache_key: مفتاح الكاش |
|
|
|
الإرجاع: |
|
مسار الملف إذا وجد، وإلا None |
|
""" |
|
if cache_key in self.cache_index: |
|
cache_file = os.path.join(self.cache_dir, self.cache_index[cache_key]) |
|
if os.path.exists(cache_file): |
|
return cache_file |
|
|
|
return None |
|
|
|
def _add_to_cache(self, cache_key, file_path): |
|
""" |
|
إضافة ملف إلى الكاش |
|
|
|
المعلمات: |
|
cache_key: مفتاح الكاش |
|
file_path: مسار الملف |
|
|
|
الإرجاع: |
|
True إذا تمت الإضافة بنجاح، وإلا False |
|
""" |
|
try: |
|
|
|
cache_file = os.path.join(self.cache_dir, os.path.basename(file_path)) |
|
|
|
if file_path != cache_file: |
|
shutil.copy(file_path, cache_file) |
|
|
|
|
|
self.cache_index[cache_key] = os.path.basename(file_path) |
|
|
|
|
|
with open(self.cache_index_file, "w", encoding="utf-8") as f: |
|
json.dump(self.cache_index, f, ensure_ascii=False, indent=2) |
|
|
|
return True |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في إضافة الملف إلى الكاش: {str(e)}") |
|
return False |
|
|
|
def _generate_cache_key(self, text, language, voice_id, speaking_rate, pitch): |
|
""" |
|
إنشاء مفتاح كاش للترجمة الصوتية |
|
|
|
المعلمات: |
|
text: النص |
|
language: اللغة |
|
voice_id: معرف الصوت |
|
speaking_rate: سرعة النطق |
|
pitch: درجة الصوت |
|
|
|
الإرجاع: |
|
مفتاح الكاش |
|
""" |
|
import hashlib |
|
|
|
|
|
cache_text = f"{text}|{language}|{voice_id}|{speaking_rate}|{pitch}" |
|
|
|
|
|
hash_obj = hashlib.md5(cache_text.encode()) |
|
|
|
return hash_obj.hexdigest() |
|
|
|
def _load_cache_index(self): |
|
""" |
|
تحميل فهرس الكاش |
|
|
|
الإرجاع: |
|
قاموس فهرس الكاش |
|
""" |
|
if os.path.exists(self.cache_index_file): |
|
try: |
|
with open(self.cache_index_file, "r", encoding="utf-8") as f: |
|
return json.load(f) |
|
except Exception as e: |
|
self.logger.error(f"خطأ في تحميل فهرس الكاش: {str(e)}") |
|
|
|
return {} |
|
|
|
def _get_cache_size(self): |
|
""" |
|
الحصول على حجم الكاش بالبايت |
|
|
|
الإرجاع: |
|
حجم الكاش بالبايت |
|
""" |
|
total_size = 0 |
|
|
|
for filename in os.listdir(self.cache_dir): |
|
file_path = os.path.join(self.cache_dir, filename) |
|
if os.path.isfile(file_path): |
|
total_size += os.path.getsize(file_path) |
|
|
|
return total_size |
|
|
|
def _clear_cache(self): |
|
""" |
|
مسح كاش الترجمات الصوتية |
|
|
|
الإرجاع: |
|
True إذا تم المسح بنجاح، وإلا False |
|
""" |
|
try: |
|
|
|
for filename in os.listdir(self.cache_dir): |
|
file_path = os.path.join(self.cache_dir, filename) |
|
if os.path.isfile(file_path): |
|
os.remove(file_path) |
|
|
|
|
|
self.cache_index = {} |
|
|
|
|
|
with open(self.cache_index_file, "w", encoding="utf-8") as f: |
|
json.dump(self.cache_index, f, ensure_ascii=False, indent=2) |
|
|
|
return True |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في مسح الكاش: {str(e)}") |
|
return False |
|
|
|
def _add_voice_to_history(self, title, content, source_language, voice_id, duration, audio_file, content_type="نص حر", is_translation=False, original_id=None): |
|
""" |
|
إضافة ترجمة صوتية إلى التاريخ |
|
|
|
المعلمات: |
|
title: عنوان الترجمة الصوتية |
|
content: محتوى النص |
|
source_language: اللغة المصدر |
|
voice_id: معرف الصوت |
|
duration: مدة الترجمة الصوتية |
|
audio_file: اسم ملف الترجمة الصوتية |
|
content_type: نوع المحتوى |
|
is_translation: هل هي ترجمة لنص آخر |
|
original_id: معرف النص الأصلي |
|
|
|
الإرجاع: |
|
معرف الترجمة الصوتية |
|
""" |
|
try: |
|
|
|
voice_id = f"voice_{int(time.time())}_{len(self.voice_history)}" |
|
|
|
|
|
voice_item = { |
|
"id": voice_id, |
|
"title": title, |
|
"content": content, |
|
"source_language": source_language, |
|
"voice_id": voice_id, |
|
"duration": duration, |
|
"audio_file": audio_file, |
|
"content_type": content_type, |
|
"created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), |
|
"is_translation": is_translation, |
|
"original_id": original_id |
|
} |
|
|
|
|
|
self.voice_history.append(voice_item) |
|
|
|
|
|
self._save_voice_history() |
|
|
|
return voice_id |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في إضافة الترجمة الصوتية إلى التاريخ: {str(e)}") |
|
return None |
|
|
|
def _delete_voice_from_history(self, voice_id): |
|
""" |
|
حذف ترجمة صوتية من التاريخ |
|
|
|
المعلمات: |
|
voice_id: معرف الترجمة الصوتية |
|
|
|
الإرجاع: |
|
True إذا تم الحذف بنجاح، وإلا False |
|
""" |
|
try: |
|
|
|
for i, item in enumerate(self.voice_history): |
|
if item.get("id") == voice_id: |
|
|
|
audio_file = os.path.join(self.data_dir, item.get("audio_file", "")) |
|
if os.path.exists(audio_file): |
|
os.remove(audio_file) |
|
|
|
|
|
del self.voice_history[i] |
|
|
|
|
|
self._save_voice_history() |
|
|
|
return True |
|
|
|
return False |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في حذف الترجمة الصوتية من التاريخ: {str(e)}") |
|
return False |
|
|
|
def _load_voice_history(self): |
|
""" |
|
تحميل تاريخ الترجمات الصوتية |
|
|
|
الإرجاع: |
|
قائمة تاريخ الترجمات الصوتية |
|
""" |
|
if os.path.exists(self.voice_history_file): |
|
try: |
|
with open(self.voice_history_file, "r", encoding="utf-8") as f: |
|
return json.load(f) |
|
except Exception as e: |
|
self.logger.error(f"خطأ في تحميل تاريخ الترجمات الصوتية: {str(e)}") |
|
|
|
return [] |
|
|
|
def _save_voice_history(self): |
|
""" |
|
حفظ تاريخ الترجمات الصوتية |
|
|
|
الإرجاع: |
|
True إذا تم الحفظ بنجاح، وإلا False |
|
""" |
|
try: |
|
|
|
os.makedirs(os.path.dirname(self.voice_history_file), exist_ok=True) |
|
|
|
|
|
with open(self.voice_history_file, "w", encoding="utf-8") as f: |
|
json.dump(self.voice_history, f, ensure_ascii=False, indent=2) |
|
|
|
return True |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في حفظ تاريخ الترجمات الصوتية: {str(e)}") |
|
return False |
|
|
|
def _save_voice_settings(self): |
|
""" |
|
حفظ إعدادات الترجمة الصوتية |
|
|
|
الإرجاع: |
|
True إذا تم الحفظ بنجاح، وإلا False |
|
""" |
|
try: |
|
|
|
os.makedirs(self.data_dir, exist_ok=True) |
|
|
|
|
|
settings_file = os.path.join(self.data_dir, "voice_settings.json") |
|
|
|
with open(settings_file, "w", encoding="utf-8") as f: |
|
json.dump(st.session_state.voice_settings, f, ensure_ascii=False, indent=2) |
|
|
|
return True |
|
|
|
except Exception as e: |
|
self.logger.error(f"خطأ في حفظ إعدادات الترجمة الصوتية: {str(e)}") |
|
return False |
|
|
|
def _display_audio_player(self, audio_file): |
|
""" |
|
عرض مشغل الصوت |
|
|
|
المعلمات: |
|
audio_file: مسار الملف الصوتي |
|
""" |
|
if os.path.exists(audio_file): |
|
|
|
with open(audio_file, "rb") as f: |
|
audio_bytes = f.read() |
|
|
|
|
|
st.audio(audio_bytes, format="audio/mp3") |
|
else: |
|
st.warning("الملف الصوتي غير متوفر") |
|
|
|
def _get_projects(self): |
|
""" |
|
الحصول على قائمة المشاريع |
|
|
|
الإرجاع: |
|
قائمة المشاريع |
|
""" |
|
|
|
|
|
return [ |
|
{ |
|
"id": "PRJ001", |
|
"name": "مشروع تطوير البنية التحتية لمنطقة الرياض", |
|
"status": "قيد التنفيذ", |
|
"location": "الرياض", |
|
"start_date": "2025-01-15", |
|
"expected_end_date": "2026-06-30", |
|
"budget": "15,000,000 ريال", |
|
"description": "مشروع تطوير البنية التحتية في منطقة الرياض، ويشمل إنشاء طرق جديدة وتطوير شبكات الصرف الصحي وتحسين شبكات المياه والكهرباء." |
|
}, |
|
{ |
|
"id": "PRJ002", |
|
"name": "إنشاء مجمع سكني في جدة", |
|
"status": "جديد", |
|
"location": "جدة", |
|
"start_date": "2025-04-01", |
|
"expected_end_date": "2027-03-31", |
|
"budget": "25,000,000 ريال", |
|
"description": "مشروع إنشاء مجمع سكني في مدينة جدة، ويتكون من 50 فيلا و 100 شقة سكنية، بالإضافة إلى مرافق خدمية ومناطق ترفيهية." |
|
}, |
|
{ |
|
"id": "PRJ003", |
|
"name": "توسعة مستشفى الملك فهد", |
|
"status": "قيد التنفيذ", |
|
"location": "الدمام", |
|
"start_date": "2024-10-15", |
|
"expected_end_date": "2026-02-28", |
|
"budget": "18,500,000 ريال", |
|
"description": "مشروع توسعة مستشفى الملك فهد في مدينة الدمام، ويشمل إضافة مبنى جديد للعيادات الخارجية وزيادة عدد أسرّة المستشفى." |
|
} |
|
] |
|
|
|
def _get_tenders(self): |
|
""" |
|
الحصول على قائمة المناقصات |
|
|
|
الإرجاع: |
|
قائمة المناقصات |
|
""" |
|
|
|
|
|
return [ |
|
{ |
|
"id": "TND001", |
|
"name": "مناقصة تطوير طريق الملك عبدالله", |
|
"owner": "وزارة النقل", |
|
"issue_date": "2025-02-10", |
|
"submission_date": "2025-03-15", |
|
"estimated_value": "12,000,000 ريال", |
|
"description": "مناقصة لتطوير وتوسعة طريق الملك عبدالله بطول 15 كم، وتشمل الأعمال إنشاء مسارات جديدة وتحسين البنية التحتية للطريق." |
|
}, |
|
{ |
|
"id": "TND002", |
|
"name": "مناقصة إنشاء مدرسة ثانوية", |
|
"owner": "وزارة التعليم", |
|
"issue_date": "2025-01-20", |
|
"submission_date": "2025-02-25", |
|
"estimated_value": "8,500,000 ريال", |
|
"description": "مناقصة لإنشاء مدرسة ثانوية جديدة في حي النزهة بمدينة الرياض، وتشمل الأعمال إنشاء مبنى المدرسة والمرافق التابعة لها." |
|
}, |
|
{ |
|
"id": "TND003", |
|
"name": "مناقصة صيانة وتأهيل محطات تحلية المياه", |
|
"owner": "المؤسسة العامة لتحلية المياه المالحة", |
|
"issue_date": "2025-03-01", |
|
"submission_date": "2025-04-15", |
|
"estimated_value": "22,000,000 ريال", |
|
"description": "مناقصة لصيانة وتأهيل محطات تحلية المياه في المنطقة الشرقية، وتشمل الأعمال استبدال المعدات القديمة وتطوير أنظمة التحكم." |
|
} |
|
] |
|
|
|
def _get_contracts(self): |
|
""" |
|
الحصول على قائمة العقود |
|
|
|
الإرجاع: |
|
قائمة العقود |
|
""" |
|
|
|
|
|
return [ |
|
{ |
|
"id": "CNT001", |
|
"name": "عقد إنشاء مجمع سكني", |
|
"client": "شركة الرياض للتطوير العقاري", |
|
"start_date": "2025-04-15", |
|
"end_date": "2027-04-14", |
|
"value": "28,500,000 ريال", |
|
"clauses": [ |
|
{ |
|
"title": "نطاق الأعمال", |
|
"content": "يشمل نطاق الأعمال في هذا العقد إنشاء مجمع سكني مكون من 40 فيلا و 80 شقة سكنية، بالإضافة إلى المرافق الخدمية والترفيهية." |
|
}, |
|
{ |
|
"title": "مدة التنفيذ", |
|
"content": "مدة تنفيذ المشروع 24 شهراً تبدأ من تاريخ استلام الموقع، ويمكن تمديد المدة في حال وجود ظروف قاهرة يتفق عليها الطرفان." |
|
}, |
|
{ |
|
"title": "قيمة العقد وطريقة الدفع", |
|
"content": "قيمة العقد الإجمالية هي 28,500,000 ريال سعودي، ويتم السداد على دفعات شهرية بناءً على نسبة الإنجاز في المشروع." |
|
}, |
|
{ |
|
"title": "الضمانات", |
|
"content": "يلتزم المقاول بتقديم ضمان بنكي بقيمة 5% من قيمة العقد لضمان حسن التنفيذ، وضمان صيانة لمدة سنة بعد الانتهاء من المشروع." |
|
}, |
|
{ |
|
"title": "الغرامات والجزاءات", |
|
"content": "في حال تأخر المقاول عن تسليم المشروع في الموعد المحدد، يتم فرض غرامة تأخير بنسبة 0.1% من قيمة العقد عن كل يوم تأخير، بحد أقصى 10% من قيمة العقد." |
|
} |
|
] |
|
}, |
|
{ |
|
"id": "CNT002", |
|
"name": "عقد توريد وتركيب أنظمة تكييف", |
|
"client": "شركة التطوير العقاري المحدودة", |
|
"start_date": "2025-03-01", |
|
"end_date": "2025-08-31", |
|
"value": "4,200,000 ريال", |
|
"clauses": [ |
|
{ |
|
"title": "نطاق التوريد", |
|
"content": "يشمل نطاق التوريد في هذا العقد توفير وتركيب 120 وحدة تكييف مركزي للمبنى الإداري الجديد، بالإضافة إلى خدمات الصيانة لمدة عام." |
|
}, |
|
{ |
|
"title": "مواصفات الأجهزة", |
|
"content": "يجب أن تكون جميع الأجهزة الموردة من إحدى العلامات التجارية المعتمدة (كارير، دايكن، أو ميتسوبيشي)، وأن تكون مطابقة للمواصفات الفنية المرفقة بالعقد." |
|
}, |
|
{ |
|
"title": "مدة التوريد والتركيب", |
|
"content": "يلتزم المورد بتوريد وتركيب جميع الأجهزة خلال مدة لا تتجاوز 6 أشهر من تاريخ توقيع العقد." |
|
}, |
|
{ |
|
"title": "الضمان", |
|
"content": "يقدم المورد ضماناً لجميع الأجهزة لمدة 3 سنوات من تاريخ التشغيل، ويشمل الضمان جميع أعمال الصيانة وقطع الغيار." |
|
} |
|
] |
|
} |
|
] |
|
|
|
def _get_documents(self): |
|
""" |
|
الحصول على قائمة المستندات |
|
|
|
الإرجاع: |
|
قائمة المستندات |
|
""" |
|
|
|
|
|
return [ |
|
{ |
|
"id": "DOC001", |
|
"name": "كراسة شروط مناقصة تطوير طريق الملك عبدالله", |
|
"type": "كراسة شروط", |
|
"page_count": 85, |
|
"file_size": "2.4 ميجابايت", |
|
"upload_date": "2025-02-10" |
|
}, |
|
{ |
|
"id": "DOC002", |
|
"name": "عقد إنشاء مجمع سكني", |
|
"type": "عقد", |
|
"page_count": 42, |
|
"file_size": "1.8 ميجابايت", |
|
"upload_date": "2025-04-12" |
|
}, |
|
{ |
|
"id": "DOC003", |
|
"name": "تقرير دراسة جدوى مشروع توسعة مستشفى", |
|
"type": "تقرير", |
|
"page_count": 65, |
|
"file_size": "3.1 ميجابايت", |
|
"upload_date": "2025-01-25" |
|
} |
|
] |
|
|
|
|
|
|
|
class VoiceNarrationApp: |
|
"""وحدة تطبيق الترجمة الصوتية متعددة اللغات""" |
|
|
|
def __init__(self): |
|
"""تهيئة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" |
|
self.voice_over_system = VoiceOverSystem() |
|
|
|
def render(self): |
|
"""عرض واجهة وحدة تطبيق الترجمة الصوتية متعددة اللغات""" |
|
st.markdown("<h2 class='module-title'>نظام الترجمة الصوتية متعددة اللغات</h2>", unsafe_allow_html=True) |
|
|
|
st.markdown(""" |
|
<div class="module-description"> |
|
يتيح لك نظام الترجمة الصوتية متعددة اللغات تحويل النصوص والمستندات إلى ملفات صوتية بلغات متعددة، |
|
مما يساعد في توصيل المعلومات بشكل أفضل للأشخاص من خلفيات لغوية مختلفة. |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
self.voice_over_system.render() |
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
st.set_page_config( |
|
page_title="الترجمة الصوتية متعددة اللغات | WAHBi AI", |
|
page_icon="🎙️", |
|
layout="wide", |
|
initial_sidebar_state="expanded" |
|
) |
|
|
|
app = VoiceNarrationApp() |
|
app.render() |