# 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(""" """, 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("""

📚 TraductorPro

Traduction professionnelle de livres électroniques avec IA

Supporte PDF, DOCX, EPUB, TXT et JSON

""", 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("""

📄 Formats

5

Types supportés

""", 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"""

🔧 Moteurs

{engines_count}

Disponibles

""", unsafe_allow_html=True) with col3: translations_count = len(st.session_state.translation_history) st.markdown(f"""

✅ Traductions

{translations_count}

Complétées

""", 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"""

📊 Succès

{success_rate:.0f}%

Taux de réussite

""", 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"""
Traduction réussie!
Temps écoulé: {elapsed_time:.1f} secondes
""", 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'📥 Cliquez ici pour télécharger' 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("""

© 2025 TranslateMyBook | Support: adjoumanideveloppeurwebmob@gmail.com

""", unsafe_allow_html=True) # Script pour animations supplémentaires st.markdown(""" """, unsafe_allow_html=True)