| |
| |
|
|
| """ |
| وحدة الترجمة الصوتية متعددة اللغات لتفاصيل المشروع |
| تتيح هذه الوحدة تحويل محتوى المشروع النصي إلى مقاطع صوتية بلغات متعددة للتسهيل على المستخدمين |
| """ |
|
|
| 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() |