Spaces:
Sleeping
Sleeping
| # streamlit_app.py | |
| """ | |
| Application Streamlit pour la traduction de livres électroniques | |
| Interface moderne avec support multi-formats et multi-moteurs | |
| """ | |
| import streamlit as st | |
| import os | |
| import sys | |
| import time | |
| import json | |
| from pathlib import Path | |
| from datetime import datetime | |
| import tempfile | |
| import shutil | |
| from typing import Dict, Any, Optional, List | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| # Import des modules personnalisés | |
| sys.path.append(os.path.dirname(os.path.dirname(__file__))) | |
| from translator_engine import MultiEngineTranslator | |
| from file_handlers import get_handler | |
| from utils import ( | |
| TextChunker, | |
| format_file_size, | |
| estimate_translation_time, | |
| sanitize_filename, | |
| setup_logger | |
| ) | |
| # Configuration de la page | |
| st.set_page_config( | |
| page_title="📚 TranslateMyBook - Traduction de Livres", | |
| page_icon="📚", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # CSS personnalisé pour une interface moderne | |
| st.markdown(""" | |
| <style> | |
| /* Palette de couleurs moderne */ | |
| :root { | |
| --primary-color: #6366F1; | |
| --secondary-color: #8B5CF6; | |
| --success-color: #10B981; | |
| --warning-color: #F59E0B; | |
| --danger-color: #EF4444; | |
| --dark-bg: #0F172A; | |
| --card-bg: #1E293B; | |
| --text-primary: #F1F5F9; | |
| --text-secondary: #94A3B8; | |
| } | |
| /* Animation de gradient pour le header */ | |
| .main-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 2rem; | |
| border-radius: 20px; | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 2rem; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); | |
| animation: gradient 15s ease infinite; | |
| background-size: 400% 400%; | |
| } | |
| @keyframes gradient { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| /* Cards modernes */ | |
| .metric-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 1.5rem; | |
| border-radius: 15px; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease; | |
| } | |
| .metric-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.2); | |
| } | |
| /* Boutons stylisés */ | |
| .stButton > button { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 2rem; | |
| border-radius: 50px; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 25px rgba(102, 126, 234, 0.6); | |
| } | |
| /* Tabs personnalisés */ | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 10px; | |
| background-color: rgba(30, 41, 59, 0.5); | |
| padding: 10px; | |
| border-radius: 15px; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| height: 50px; | |
| padding: 0 25px; | |
| background-color: transparent; | |
| border-radius: 10px; | |
| color: #94A3B8; | |
| transition: all 0.3s ease; | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| /* Progress bar personnalisée */ | |
| .stProgress > div > div > div > div { | |
| background: linear-gradient(90deg, #667eea, #764ba2, #667eea); | |
| background-size: 200% 100%; | |
| animation: progress-animation 2s ease-in-out infinite; | |
| } | |
| @keyframes progress-animation { | |
| 0% { background-position: 0% 0%; } | |
| 100% { background-position: 200% 0%; } | |
| } | |
| /* File uploader stylisé */ | |
| .stFileUploader { | |
| background: rgba(30, 41, 59, 0.5); | |
| border: 2px dashed #6366F1; | |
| border-radius: 15px; | |
| padding: 2rem; | |
| transition: all 0.3s ease; | |
| } | |
| .stFileUploader:hover { | |
| border-color: #8B5CF6; | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| /* Selectbox amélioré */ | |
| .stSelectbox > div > div { | |
| background: rgba(30, 41, 59, 0.5); | |
| border-radius: 10px; | |
| border: 1px solid #6366F1; | |
| } | |
| /* Animations */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| /* Success message */ | |
| .success-message { | |
| background: linear-gradient(135deg, #10B981 0%, #059669 100%); | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 10px; | |
| margin: 1rem 0; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| /* Warning message */ | |
| .warning-message { | |
| background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%); | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 10px; | |
| margin: 1rem 0; | |
| box-shadow: 0 4px 6px rgba(0,0,0,0.1); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # État de session | |
| if 'translator' not in st.session_state: | |
| st.session_state.translator = None | |
| if 'translation_history' not in st.session_state: | |
| st.session_state.translation_history = [] | |
| if 'api_keys' not in st.session_state: | |
| st.session_state.api_keys = {} | |
| if 'current_task' not in st.session_state: | |
| st.session_state.current_task = None | |
| # Header avec animation | |
| st.markdown(""" | |
| <div class="main-header"> | |
| <h1 style="font-size: 3rem; margin-bottom: 0.5rem;">📚 TraductorPro</h1> | |
| <p style="font-size: 1.2rem; opacity: 0.9;">Traduction professionnelle de livres électroniques avec IA</p> | |
| <p style="font-size: 0.9rem; opacity: 0.7;">Supporte PDF, DOCX, EPUB, TXT et JSON</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Fonction pour afficher les métriques | |
| def display_metrics(): | |
| """Affiche les métriques de l'application""" | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.markdown(""" | |
| <div class="metric-card fade-in"> | |
| <h3 style="color: white; margin: 0;">📄 Formats</h3> | |
| <p style="font-size: 2rem; margin: 0.5rem 0;">5</p> | |
| <p style="opacity: 0.8; margin: 0;">Types supportés</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col2: | |
| engines_count = len(st.session_state.translator.get_available_engines()) if st.session_state.translator else 0 | |
| st.markdown(f""" | |
| <div class="metric-card fade-in" style="animation-delay: 0.1s;"> | |
| <h3 style="color: white; margin: 0;">🔧 Moteurs</h3> | |
| <p style="font-size: 2rem; margin: 0.5rem 0;">{engines_count}</p> | |
| <p style="opacity: 0.8; margin: 0;">Disponibles</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col3: | |
| translations_count = len(st.session_state.translation_history) | |
| st.markdown(f""" | |
| <div class="metric-card fade-in" style="animation-delay: 0.2s;"> | |
| <h3 style="color: white; margin: 0;">✅ Traductions</h3> | |
| <p style="font-size: 2rem; margin: 0.5rem 0;">{translations_count}</p> | |
| <p style="opacity: 0.8; margin: 0;">Complétées</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with col4: | |
| success_rate = 100 if translations_count == 0 else (sum(1 for t in st.session_state.translation_history if t['status'] == 'success') / translations_count * 100) | |
| st.markdown(f""" | |
| <div class="metric-card fade-in" style="animation-delay: 0.3s;"> | |
| <h3 style="color: white; margin: 0;">📊 Succès</h3> | |
| <p style="font-size: 2rem; margin: 0.5rem 0;">{success_rate:.0f}%</p> | |
| <p style="opacity: 0.8; margin: 0;">Taux de réussite</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Afficher les métriques | |
| display_metrics() | |
| # Tabs principaux | |
| tab1, tab2, tab3, tab4 = st.tabs(["🚀 Traduction Gratuite", "🔑 Traduction avec API", "📊 Statistiques", "⚙️ Paramètres"]) | |
| # Liste des langues disponibles | |
| LANGUAGES = { | |
| "fr": "🇫🇷 Français", | |
| "en": "🇬🇧 Anglais", | |
| "es": "🇪🇸 Espagnol", | |
| "de": "🇩🇪 Allemand", | |
| "it": "🇮🇹 Italien", | |
| "pt": "🇵🇹 Portugais", | |
| "ru": "🇷🇺 Russe", | |
| "ja": "🇯🇵 Japonais", | |
| "ko": "🇰🇷 Coréen", | |
| "zh": "🇨🇳 Chinois", | |
| "ar": "🇸🇦 Arabe", | |
| "hi": "🇮🇳 Hindi", | |
| "nl": "🇳🇱 Néerlandais", | |
| "pl": "🇵🇱 Polonais", | |
| "tr": "🇹🇷 Turc", | |
| "sv": "🇸🇪 Suédois", | |
| "da": "🇩🇰 Danois", | |
| "no": "🇳🇴 Norvégien", | |
| "fi": "🇫🇮 Finnois", | |
| "el": "🇬🇷 Grec", | |
| "he": "🇮🇱 Hébreu", | |
| "th": "🇹🇭 Thaï", | |
| "vi": "🇻🇳 Vietnamien", | |
| "id": "🇮🇩 Indonésien", | |
| "ms": "🇲🇾 Malais", | |
| "uk": "🇺🇦 Ukrainien", | |
| "cs": "🇨🇿 Tchèque", | |
| "hu": "🇭🇺 Hongrois", | |
| "ro": "🇷🇴 Roumain", | |
| "bg": "🇧🇬 Bulgare" | |
| } | |
| def get_settings_path(): | |
| """Retourne le chemin pour sauvegarder les settings""" | |
| # Utiliser la variable d'environnement si disponible | |
| settings_dir = os.environ.get('SETTINGS_PATH', '/tmp') | |
| # S'assurer que le dossier existe | |
| Path(settings_dir).mkdir(parents=True, exist_ok=True) | |
| return Path(settings_dir) / 'settings.json' | |
| def save_settings(settings): | |
| """Sauvegarde les paramètres de manière sécurisée""" | |
| try: | |
| settings_path = get_settings_path() | |
| with open(settings_path, 'w') as f: | |
| json.dump(settings, f, indent=2) | |
| return True, f"Paramètres sauvegardés dans {settings_path}" | |
| except PermissionError: | |
| # Si pas de permission, sauvegarder dans session state | |
| st.session_state['saved_settings'] = settings | |
| return True, "Paramètres sauvegardés dans la session" | |
| except Exception as e: | |
| return False, f"Erreur: {str(e)}" | |
| def load_settings(): | |
| """Charge les paramètres sauvegardés""" | |
| try: | |
| settings_path = get_settings_path() | |
| if settings_path.exists(): | |
| with open(settings_path, 'r') as f: | |
| return json.load(f) | |
| except: | |
| pass | |
| # Vérifier dans session state | |
| if 'saved_settings' in st.session_state: | |
| return st.session_state['saved_settings'] | |
| # Paramètres par défaut | |
| return { | |
| 'theme': 'Dark', | |
| 'show_animations': True, | |
| 'chunk_size': 3000, | |
| 'max_retries': 3, | |
| 'enable_cache': True, | |
| 'auto_save': True, | |
| 'enable_logging': False, | |
| 'log_level': 'INFO' | |
| } | |
| def process_file(uploaded_file, source_lang, target_lang, use_api_keys=False): | |
| """Traite le fichier uploadé""" | |
| if not uploaded_file: | |
| st.error("⚠️ Veuillez sélectionner un fichier avant de lancer la traduction.") | |
| return None | |
| # Vérifier que les langues sont différentes | |
| if source_lang == target_lang and source_lang != 'auto': | |
| st.warning("⚠️ La langue source et cible sont identiques.") | |
| return None | |
| # Créer le traducteur si nécessaire | |
| config = {} | |
| if use_api_keys: | |
| config = st.session_state.api_keys | |
| if not st.session_state.translator or (use_api_keys and config != st.session_state.translator.config): | |
| with st.spinner("🔄 Initialisation des moteurs de traduction..."): | |
| st.session_state.translator = MultiEngineTranslator(config) | |
| engines = st.session_state.translator.get_available_engines() | |
| if engines: | |
| st.success(f"✅ {len(engines)} moteur(s) disponible(s): {', '.join(engines)}") | |
| else: | |
| st.error("❌ Aucun moteur de traduction disponible") | |
| return None | |
| # Créer un dossier temporaire | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| # Sauvegarder le fichier uploadé | |
| input_path = Path(tmpdir) / uploaded_file.name | |
| with open(input_path, 'wb') as f: | |
| f.write(uploaded_file.getbuffer()) | |
| # Déterminer le format et créer le handler | |
| file_extension = Path(uploaded_file.name).suffix | |
| try: | |
| handler = get_handler(file_extension, st.session_state.translator) | |
| except ValueError as e: | |
| st.error(f"❌ {e}") | |
| return None | |
| # Afficher l'aperçu | |
| with st.expander("👁️ Aperçu du fichier original", expanded=False): | |
| preview = handler.get_preview(str(input_path)) | |
| st.text(preview) | |
| # Estimer le temps de traduction | |
| file_size = uploaded_file.size | |
| char_estimate = file_size // 2 # Estimation approximative | |
| time_estimate = estimate_translation_time(char_estimate) | |
| st.info(f""" | |
| 📊 **Informations sur le fichier:** | |
| - Nom: {uploaded_file.name} | |
| - Taille: {format_file_size(file_size)} | |
| - Format: {file_extension.upper()} | |
| - Temps estimé: {time_estimate} | |
| """) | |
| # Nom du fichier de sortie | |
| output_name = sanitize_filename( | |
| f"{Path(uploaded_file.name).stem}_translated_{target_lang}{file_extension}" | |
| ) | |
| output_path = Path(tmpdir) / output_name | |
| # Barre de progression | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| def progress_callback(progress): | |
| progress_bar.progress(int(progress)) | |
| status_text.text(f"Progression: {progress:.1f}%") | |
| # Lancer la traduction | |
| start_time = time.time() | |
| status_text.text("🔄 Traduction en cours...") | |
| try: | |
| success, message = handler.process( | |
| str(input_path), | |
| str(output_path), | |
| source_lang, | |
| target_lang, | |
| progress_callback | |
| ) | |
| elapsed_time = time.time() - start_time | |
| if success: | |
| progress_bar.progress(100) | |
| status_text.empty() | |
| # Afficher le succès | |
| st.markdown(f""" | |
| <div class="success-message"> | |
| ✅ <strong>Traduction réussie!</strong><br> | |
| Temps écoulé: {elapsed_time:.1f} secondes | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Ajouter à l'historique | |
| st.session_state.translation_history.append({ | |
| 'timestamp': datetime.now().isoformat(), | |
| 'filename': uploaded_file.name, | |
| 'source_lang': source_lang, | |
| 'target_lang': target_lang, | |
| 'status': 'success', | |
| 'duration': elapsed_time, | |
| 'size': file_size | |
| }) | |
| # Lire le fichier traduit de manière sécurisée | |
| try: | |
| with open(output_path, 'rb') as f: | |
| translated_content = f.read() | |
| # Vérifier la taille du fichier | |
| translated_size = len(translated_content) | |
| if translated_size > 200 * 1024 * 1024: # 200 MB | |
| st.warning("⚠️ Le fichier traduit est trop volumineux (>200MB). Veuillez le récupérer manuellement.") | |
| # Alternative : sauvegarder localement | |
| local_path = Path('/tmp') / output_name | |
| with open(local_path, 'wb') as f: | |
| f.write(translated_content) | |
| st.info(f"📁 Fichier sauvegardé dans: {local_path}") | |
| else: | |
| # Créer plusieurs boutons de téléchargement avec différents MIME types | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Bouton principal | |
| st.download_button( | |
| label=f"📥 Télécharger {output_name}", | |
| data=translated_content, | |
| file_name=output_name, | |
| mime='application/octet-stream', | |
| key=f"download_main_{time.time()}" | |
| ) | |
| with col2: | |
| # Bouton alternatif avec MIME type spécifique | |
| mime_type = get_mime_type(file_extension) | |
| st.download_button( | |
| label=f"💾 Sauvegarder ({file_extension.upper()})", | |
| data=translated_content, | |
| file_name=output_name, | |
| mime=mime_type, | |
| key=f"download_alt_{time.time()}" | |
| ) | |
| # Aperçu du fichier traduit | |
| with st.expander("👁️ Aperçu du fichier traduit", expanded=False): | |
| try: | |
| preview = handler.get_preview(str(output_path)) | |
| st.text(preview) | |
| except Exception as e: | |
| st.warning(f"Aperçu non disponible: {e}") | |
| return output_path | |
| except Exception as e: | |
| st.error(f"❌ Erreur lors de la lecture du fichier traduit: {e}") | |
| # Tentative de récupération | |
| st.info("💡 Tentative de récupération...") | |
| try: | |
| # Essayer de créer un lien de téléchargement alternatif | |
| import base64 | |
| with open(output_path, 'rb') as f: | |
| file_content = f.read() | |
| b64 = base64.b64encode(file_content).decode() | |
| href = f'<a href="data:application/octet-stream;base64,{b64}" download="{output_name}">📥 Cliquez ici pour télécharger</a>' | |
| st.markdown(href, unsafe_allow_html=True) | |
| except: | |
| st.error("❌ Impossible de créer le lien de téléchargement") | |
| return None | |
| else: | |
| # Afficher l'erreur | |
| st.error(f"❌ Erreur lors de la traduction: {message}") | |
| # Ajouter à l'historique même en cas d'erreur | |
| st.session_state.translation_history.append({ | |
| 'timestamp': datetime.now().isoformat(), | |
| 'filename': uploaded_file.name, | |
| 'source_lang': source_lang, | |
| 'target_lang': target_lang, | |
| 'status': 'error', | |
| 'duration': elapsed_time, | |
| 'size': file_size, | |
| 'error': message | |
| }) | |
| return None | |
| except Exception as e: | |
| st.error(f"❌ Erreur lors du traitement: {str(e)}") | |
| return None | |
| def get_mime_type(extension): | |
| """Retourne le MIME type approprié pour l'extension""" | |
| mime_types = { | |
| '.pdf': 'application/pdf', | |
| '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', | |
| '.doc': 'application/msword', | |
| '.txt': 'text/plain', | |
| '.json': 'application/json', | |
| '.epub': 'application/epub+zip' | |
| } | |
| return mime_types.get(extension.lower(), 'application/octet-stream') | |
| # Tab 1: Traduction Gratuite | |
| with tab1: | |
| st.markdown("### 🚀 Traduction gratuite avec moteurs intégrés") | |
| st.info("💡 Utilisez les moteurs de traduction gratuits pour traduire vos documents sans clé API") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("#### 📤 Fichier à traduire") | |
| uploaded_file = st.file_uploader( | |
| "Glissez votre fichier ici ou cliquez pour parcourir", | |
| type=['pdf', 'docx', 'doc', 'epub', 'txt', 'json'], | |
| help="Formats supportés: PDF, DOCX, EPUB, TXT, JSON" | |
| ) | |
| if uploaded_file: | |
| st.success(f"✅ Fichier chargé: {uploaded_file.name}") | |
| with col2: | |
| st.markdown("#### 🌍 Paramètres de traduction") | |
| # Langue source | |
| source_lang = st.selectbox( | |
| "Langue du document original", | |
| options=['auto'] + list(LANGUAGES.keys()), | |
| format_func=lambda x: "🔍 Détection automatique" if x == 'auto' else LANGUAGES[x], | |
| help="Sélectionnez 'Détection automatique' si vous n'êtes pas sûr" | |
| ) | |
| # Langue cible | |
| target_lang = st.selectbox( | |
| "Langue de traduction", | |
| options=list(LANGUAGES.keys()), | |
| index=0, # Français par défaut | |
| format_func=lambda x: LANGUAGES[x], | |
| help="Sélectionnez la langue vers laquelle traduire" | |
| ) | |
| # Bouton de traduction | |
| st.markdown("---") | |
| col1, col2, col3 = st.columns([2, 1, 2]) | |
| with col2: | |
| if st.button("🎯 Lancer la traduction", type="primary", use_container_width=True): | |
| process_file(uploaded_file, source_lang, target_lang, use_api_keys=False) | |
| # Tab 2: Traduction avec API | |
| with tab2: | |
| st.markdown("### 🔑 Traduction premium avec vos clés API") | |
| st.info("💡 Utilisez vos propres clés API pour une traduction de meilleure qualité") | |
| # Configuration des clés API | |
| with st.expander("🔐 Configuration des clés API", expanded=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| deepl_key = st.text_input( | |
| "🔷 Clé API DeepL", | |
| type="password", | |
| value=st.session_state.api_keys.get('deepl_api_key', ''), | |
| help="Obtenez votre clé sur https://www.deepl.com/pro-api" | |
| ) | |
| if deepl_key: | |
| st.session_state.api_keys['deepl_api_key'] = deepl_key | |
| openai_key = st.text_input( | |
| "🤖 Clé API OpenAI", | |
| type="password", | |
| value=st.session_state.api_keys.get('openai_api_key', ''), | |
| help="Obtenez votre clé sur https://platform.openai.com" | |
| ) | |
| if openai_key: | |
| st.session_state.api_keys['openai_api_key'] = openai_key | |
| # Modèle OpenAI | |
| openai_model = st.selectbox( | |
| "Modèle OpenAI", | |
| options=['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo'], | |
| index=0, | |
| help="GPT-4 offre une meilleure qualité mais coûte plus cher" | |
| ) | |
| st.session_state.api_keys['openai_model'] = openai_model | |
| with col2: | |
| anthropic_key = st.text_input( | |
| "🎭 Clé API Anthropic (Claude)", | |
| type="password", | |
| value=st.session_state.api_keys.get('anthropic_api_key', ''), | |
| help="Obtenez votre clé sur https://console.anthropic.com" | |
| ) | |
| if anthropic_key: | |
| st.session_state.api_keys['anthropic_api_key'] = anthropic_key | |
| # Estimation des coûts | |
| if st.session_state.api_keys: | |
| st.markdown("#### 💰 Estimation des coûts") | |
| st.info(""" | |
| **Tarifs approximatifs:** | |
| - DeepL: ~20€ / million de caractères | |
| - OpenAI GPT-3.5: ~0.002$ / 1K tokens | |
| - OpenAI GPT-4: ~0.03$ / 1K tokens | |
| - Anthropic Claude: ~0.003$ / 1K tokens | |
| """) | |
| st.markdown("---") | |
| # Interface de traduction | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("#### 📤 Fichier à traduire") | |
| api_uploaded_file = st.file_uploader( | |
| "Glissez votre fichier ici", | |
| type=['pdf', 'docx', 'doc', 'epub', 'txt', 'json'], | |
| key="api_uploader" | |
| ) | |
| if api_uploaded_file: | |
| st.success(f"✅ Fichier chargé: {api_uploaded_file.name}") | |
| with col2: | |
| st.markdown("#### 🌍 Paramètres de traduction") | |
| api_source_lang = st.selectbox( | |
| "Langue source", | |
| options=['auto'] + list(LANGUAGES.keys()), | |
| format_func=lambda x: "🔍 Détection automatique" if x == 'auto' else LANGUAGES[x], | |
| key="api_source_lang" | |
| ) | |
| api_target_lang = st.selectbox( | |
| "Langue cible", | |
| options=list(LANGUAGES.keys()), | |
| index=0, | |
| format_func=lambda x: LANGUAGES[x], | |
| key="api_target_lang" | |
| ) | |
| # Bouton de traduction | |
| col1, col2, col3 = st.columns([2, 1, 2]) | |
| with col2: | |
| if st.button("🚀 Traduire avec API", type="primary", use_container_width=True): | |
| if not st.session_state.api_keys: | |
| st.error("⚠️ Veuillez configurer au moins une clé API") | |
| else: | |
| process_file(api_uploaded_file, api_source_lang, api_target_lang, use_api_keys=True) | |
| # Tab 3: Statistiques | |
| with tab3: | |
| st.markdown("### 📊 Statistiques et historique") | |
| if st.session_state.translation_history: | |
| # Graphiques | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Graphique des langues les plus traduites | |
| lang_stats = {} | |
| for item in st.session_state.translation_history: | |
| target = item.get('target_lang', 'unknown') | |
| lang_stats[target] = lang_stats.get(target, 0) + 1 | |
| fig_langs = go.Figure(data=[ | |
| go.Bar( | |
| x=list(lang_stats.keys()), | |
| y=list(lang_stats.values()), | |
| marker=dict( | |
| color=list(lang_stats.values()), | |
| colorscale='Viridis' | |
| ) | |
| ) | |
| ]) | |
| fig_langs.update_layout( | |
| title="Langues les plus traduites", | |
| xaxis_title="Langue", | |
| yaxis_title="Nombre de traductions", | |
| template="plotly_dark", | |
| height=400 | |
| ) | |
| st.plotly_chart(fig_langs, use_container_width=True) | |
| with col2: | |
| # Graphique du temps de traduction | |
| success_items = [item for item in st.session_state.translation_history if item.get('status') == 'success'] | |
| if success_items: | |
| durations = [item.get('duration', 0) for item in success_items[-10:]] | |
| files = [item.get('filename', 'unknown')[:20] for item in success_items[-10:]] | |
| fig_time = go.Figure(data=[ | |
| go.Scatter( | |
| x=list(range(len(durations))), | |
| y=durations, | |
| mode='lines+markers', | |
| name='Temps (s)', | |
| line=dict(color='#6366F1', width=3), | |
| marker=dict(size=10) | |
| ) | |
| ]) | |
| fig_time.update_layout( | |
| title="Temps de traduction (10 dernières)", | |
| xaxis_title="Traduction", | |
| yaxis_title="Temps (secondes)", | |
| template="plotly_dark", | |
| height=400 | |
| ) | |
| st.plotly_chart(fig_time, use_container_width=True) | |
| # Tableau d'historique | |
| st.markdown("#### 📜 Historique des traductions") | |
| # Convertir l'historique en DataFrame pour l'affichage | |
| import pandas as pd | |
| history_data = [] | |
| for item in reversed(st.session_state.translation_history[-20:]): # 20 dernières | |
| history_data.append({ | |
| 'Date': datetime.fromisoformat(item['timestamp']).strftime('%Y-%m-%d %H:%M'), | |
| 'Fichier': item.get('filename', 'unknown')[:30], | |
| 'De': LANGUAGES.get(item.get('source_lang', 'auto'), item.get('source_lang', 'auto')), | |
| 'Vers': LANGUAGES.get(item.get('target_lang', '?'), '?'), | |
| 'Statut': '✅ Succès' if item.get('status') == 'success' else '❌ Erreur', | |
| 'Durée': f"{item.get('duration', 0):.1f}s" if item.get('duration') else 'N/A' | |
| }) | |
| df_history = pd.DataFrame(history_data) | |
| st.dataframe( | |
| df_history, | |
| use_container_width=True, | |
| hide_index=True, | |
| column_config={ | |
| 'Date': st.column_config.TextColumn('Date', width='medium'), | |
| 'Fichier': st.column_config.TextColumn('Fichier', width='large'), | |
| 'De': st.column_config.TextColumn('De', width='small'), | |
| 'Vers': st.column_config.TextColumn('Vers', width='small'), | |
| 'Statut': st.column_config.TextColumn('Statut', width='small'), | |
| 'Durée': st.column_config.TextColumn('Durée', width='small') | |
| } | |
| ) | |
| # Bouton pour effacer l'historique | |
| if st.button("🗑️ Effacer l'historique"): | |
| st.session_state.translation_history = [] | |
| st.rerun() | |
| else: | |
| st.info("📭 Aucune traduction effectuée pour le moment") | |
| # Tab 4: Paramètres | |
| with tab4: | |
| st.markdown("### ⚙️ Paramètres de l'application") | |
| # Charger les paramètres existants | |
| current_settings = load_settings() | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("#### 🎨 Apparence") | |
| theme = st.selectbox( | |
| "Thème de l'interface", | |
| options=["Dark", "Light", "Auto"], | |
| index=["Dark", "Light", "Auto"].index(current_settings.get('theme', 'Dark')), | |
| help="Le thème sombre est recommandé pour une meilleure expérience" | |
| ) | |
| show_animations = st.checkbox( | |
| "Activer les animations", | |
| value=current_settings.get('show_animations', True), | |
| help="Désactiver pour améliorer les performances" | |
| ) | |
| st.markdown("#### 🔧 Performance") | |
| chunk_size = st.slider( | |
| "Taille des chunks (caractères)", | |
| min_value=1000, | |
| max_value=5000, | |
| value=current_settings.get('chunk_size', 3000), | |
| step=500, | |
| help="Plus petit = meilleure qualité, plus lent" | |
| ) | |
| max_retries = st.slider( | |
| "Nombre de tentatives en cas d'erreur", | |
| min_value=1, | |
| max_value=5, | |
| value=current_settings.get('max_retries', 3), | |
| help="Nombre de tentatives avant d'abandonner" | |
| ) | |
| with col2: | |
| st.markdown("#### 💾 Cache et sauvegarde") | |
| enable_cache = st.checkbox( | |
| "Activer le cache de traduction", | |
| value=current_settings.get('enable_cache', True), | |
| help="Réutilise les traductions déjà effectuées" | |
| ) | |
| if enable_cache: | |
| if st.button("🧹 Vider le cache"): | |
| try: | |
| cache_dir = Path(os.environ.get('CACHE_PATH', '.translation_cache')) | |
| if cache_dir.exists(): | |
| import shutil | |
| shutil.rmtree(cache_dir) | |
| cache_dir.mkdir(parents=True, exist_ok=True) | |
| st.success("✅ Cache vidé avec succès") | |
| except Exception as e: | |
| st.warning(f"⚠️ Impossible de vider le cache: {e}") | |
| auto_save = st.checkbox( | |
| "Sauvegarde automatique de l'historique", | |
| value=current_settings.get('auto_save', True), | |
| help="Sauvegarde l'historique pour la prochaine session" | |
| ) | |
| st.markdown("#### 📝 Logs") | |
| enable_logging = st.checkbox( | |
| "Activer les logs détaillés", | |
| value=current_settings.get('enable_logging', False), | |
| help="Pour le débogage avancé" | |
| ) | |
| log_level = "INFO" | |
| if enable_logging: | |
| log_level = st.select_slider( | |
| "Niveau de log", | |
| options=["ERROR", "WARNING", "INFO", "DEBUG"], | |
| value=current_settings.get('log_level', 'INFO') | |
| ) | |
| # Bouton de sauvegarde | |
| st.markdown("---") | |
| col1, col2, col3 = st.columns([2, 1, 2]) | |
| with col2: | |
| if st.button("💾 Sauvegarder", type="primary", use_container_width=True): | |
| # Préparer les paramètres | |
| settings = { | |
| 'theme': theme, | |
| 'show_animations': show_animations, | |
| 'chunk_size': chunk_size, | |
| 'max_retries': max_retries, | |
| 'enable_cache': enable_cache, | |
| 'auto_save': auto_save, | |
| 'enable_logging': enable_logging, | |
| 'log_level': log_level | |
| } | |
| # Sauvegarder avec gestion d'erreur | |
| success, message = save_settings(settings) | |
| if success: | |
| st.success(f"✅ {message}") | |
| else: | |
| st.error(f"❌ {message}") | |
| # Sidebar avec informations supplémentaires | |
| with st.sidebar: | |
| st.markdown("## 📚 TranslateMyBook") | |
| st.markdown("---") | |
| # Guide rapide | |
| with st.expander("📖 Guide rapide", expanded=False): | |
| st.markdown(""" | |
| **Comment utiliser TranslateMyBook:** | |
| 1. **Choisissez un mode:** | |
| - 🚀 **Gratuit**: Utilise les moteurs gratuits | |
| - 🔑 **Premium**: Avec vos clés API | |
| 2. **Uploadez votre fichier:** | |
| - Formats: PDF, DOCX, EPUB, TXT, JSON | |
| - Taille max: 200 MB | |
| 3. **Sélectionnez les langues:** | |
| - Source: Auto-détection ou manuel | |
| - Cible: 30+ langues disponibles | |
| 4. **Lancez la traduction:** | |
| - Suivez la progression en temps réel | |
| - Téléchargez le résultat | |
| """) | |
| # Formats supportés | |
| with st.expander("📄 Formats supportés", expanded=False): | |
| st.markdown(""" | |
| | Format | Extension | Support | | |
| |--------|-----------|---------| | |
| | PDF | .pdf | ✅ Complet | | |
| | Word | .docx, .doc | ✅ Complet | | |
| | EPUB | .epub | ✅ Complet | | |
| | Texte | .txt | ✅ Complet | | |
| | JSON | .json | ✅ Complet | | |
| """) | |
| # Langues disponibles | |
| with st.expander("🌍 Langues disponibles", expanded=False): | |
| st.markdown("**30+ langues supportées:**") | |
| for code, name in list(LANGUAGES.items())[:10]: | |
| st.text(f"• {name}") | |
| st.text("... et plus encore!") | |
| # Astuces | |
| with st.expander("💡 Astuces", expanded=False): | |
| st.markdown(""" | |
| **Pour de meilleurs résultats:** | |
| • **Qualité**: Utilisez DeepL ou GPT-4 pour une traduction premium | |
| • **Vitesse**: Les moteurs gratuits sont plus rapides mais moins précis | |
| • **Gros fichiers**: Divisez en plusieurs parties si > 100 pages | |
| • **Format**: DOCX préserve mieux la mise en forme que PDF | |
| • **Cache**: Activé par défaut pour économiser du temps | |
| """) | |
| # À propos | |
| st.markdown("---") | |
| st.markdown("### À propos") | |
| st.markdown(""" | |
| **TranslateMyBook v2.0** | |
| Application de traduction professionnelle | |
| développée avec ❤️ et 🐍 Python | |
| [GitHub](https://github.com/) | [Documentation](https://docs.example.com) | |
| """) | |
| # Status des moteurs | |
| if st.session_state.translator: | |
| st.markdown("---") | |
| st.markdown("### 🔧 Status des moteurs") | |
| engines = st.session_state.translator.get_available_engines() | |
| for engine in engines[:5]: # Afficher max 5 | |
| st.success(f"✅ {engine}") | |
| if len(engines) > 5: | |
| st.text(f"... et {len(engines) - 5} autres") | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style="text-align: center; opacity: 0.7;"> | |
| <p>© 2025 TranslateMyBook | Support: adjoumanideveloppeurwebmob@gmail.com</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Script pour animations supplémentaires | |
| st.markdown(""" | |
| <script> | |
| // Animation de particules en arrière-plan | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Ajouter des animations smooth scroll | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| document.querySelector(this.getAttribute('href')).scrollIntoView({ | |
| behavior: 'smooth' | |
| }); | |
| }); | |
| }); | |
| }); | |
| </script> | |
| """, unsafe_allow_html=True) |