# -*- coding: utf-8 -*- """ Application de traduction automatique Français -> Mooré (batch CSV/Excel) Déployable sur Hugging Face Spaces (SDK: Gradio) Fonctionnalités : - Upload d'un fichier CSV / Excel contenant une colonne "français" - Choix d'un modèle de traduction (Masakhane M2M100 ou NLLB-200) - Traduction ligne par ligne avec barre de progression - Export du résultat en CSV (utf-8-sig) ou Excel """ import os import traceback # IMPORTANT : sur un Space Hugging Face utilisant le matériel "ZeroGPU", # le paquet `spaces` DOIT être importé avant tout paquet lié à CUDA # (notamment avant `torch`), sous peine d'erreur au démarrage. En local # (ou sur un Space CPU/GPU classique), ce paquet n'existe pas : on gère # donc cet import de façon optionnelle, mais toujours en premier. try: import spaces # noqa: E402 (doit rester avant l'import de torch) _SPACES_DISPONIBLE = True except ImportError: _SPACES_DISPONIBLE = False import gradio as gr import pandas as pd import torch from transformers import AutoModelForSeq2SeqLM, AutoTokenizer # --------------------------------------------------------------------------- # 1. CONFIGURATION DES MODÈLES DISPONIBLES # --------------------------------------------------------------------------- # Chaque entrée décrit comment charger le modèle et comment forcer la langue # cible lors de la génération. Les identifiants correspondent aux dépôts # Hugging Face réels (les noms donnés par l'utilisateur ont été normalisés). MODELES = { "Masakhane M2M100 (fr -> mos, spécialisé actualités)": { "repo_id": "masakhane/m2m100_418M_fr_mos_news", "type": "m2m100", "src_lang": "fr", "tgt_lang": "mos", }, "NLLB-200 3.3B (Meta, multilingue)": { "repo_id": "facebook/nllb-200-3.3B", "type": "nllb", "src_lang": "fra_Latn", "tgt_lang": "mos_Latn", }, "NLLB-200 distilled 600M (Meta, plus rapide)": { "repo_id": "facebook/nllb-200-distilled-600M", "type": "nllb", "src_lang": "fra_Latn", "tgt_lang": "mos_Latn", }, } # Cache (singleton) : évite de recharger un modèle déjà utilisé _CACHE_MODELES = {} DEVICE = "cuda" if torch.cuda.is_available() else "cpu" # --------------------------------------------------------------------------- # 2. CHARGEMENT DU MODÈLE (mise en cache / singleton) # --------------------------------------------------------------------------- def load_model(nom_modele: str): """ Charge (ou récupère depuis le cache) le tokenizer et le modèle correspondant au nom sélectionné dans le menu déroulant. Déplace automatiquement le modèle sur le GPU si disponible. """ if nom_modele in _CACHE_MODELES: return _CACHE_MODELES[nom_modele] config = MODELES[nom_modele] repo_id = config["repo_id"] tokenizer = AutoTokenizer.from_pretrained(repo_id) modele = AutoModelForSeq2SeqLM.from_pretrained(repo_id) modele.to(DEVICE) modele.eval() _CACHE_MODELES[nom_modele] = (tokenizer, modele, config) return tokenizer, modele, config # --------------------------------------------------------------------------- # 3. TRADUCTION D'UN TEXTE # --------------------------------------------------------------------------- def translate_text(texte: str, tokenizer, modele, config) -> str: """ Traduit une seule chaîne de caractères du français vers le mooré. Renvoie une chaîne vide si le texte d'entrée est vide/NaN. """ if texte is None: return "" texte = str(texte).strip() if texte == "" or texte.lower() == "nan": return "" type_modele = config["type"] with torch.no_grad(): if type_modele == "nllb": # Les modèles NLLB nécessitent la langue source sur le tokenizer # et l'identifiant de la langue cible comme premier token forcé. tokenizer.src_lang = config["src_lang"] entrees = tokenizer(texte, return_tensors="pt", truncation=True, max_length=512).to(DEVICE) # Compatibilité selon les versions de transformers try: tgt_id = tokenizer.convert_tokens_to_ids(config["tgt_lang"]) except Exception: tgt_id = tokenizer.lang_code_to_id[config["tgt_lang"]] sortie = modele.generate( **entrees, forced_bos_token_id=tgt_id, max_length=512, num_beams=4, ) else: # "m2m100" (y compris le modèle Masakhane finetuné) tokenizer.src_lang = config["src_lang"] entrees = tokenizer(texte, return_tensors="pt", truncation=True, max_length=512).to(DEVICE) try: tgt_id = tokenizer.get_lang_id(config["tgt_lang"]) except Exception: # Le tokenizer finetuné n'a parfois pas cette méthode : on # se rabat sur une génération sans forçage de langue cible. tgt_id = None if tgt_id is not None: sortie = modele.generate(**entrees, forced_bos_token_id=tgt_id, max_length=512, num_beams=4) else: sortie = modele.generate(**entrees, max_length=512, num_beams=4) traduction = tokenizer.batch_decode(sortie, skip_special_tokens=True)[0] return traduction.strip() # --------------------------------------------------------------------------- # 4. LECTURE DU FICHIER D'ENTRÉE # --------------------------------------------------------------------------- def lire_fichier(chemin_fichier: str) -> pd.DataFrame: """ Lit un fichier CSV ou Excel et renvoie un DataFrame. Lève une exception explicite en cas de problème de lecture. """ extension = os.path.splitext(chemin_fichier)[1].lower() try: if extension == ".csv": # On essaie plusieurs encodages courants pour plus de robustesse try: df = pd.read_csv(chemin_fichier, encoding="utf-8-sig") except UnicodeDecodeError: df = pd.read_csv(chemin_fichier, encoding="latin-1") elif extension in (".xlsx", ".xls"): df = pd.read_excel(chemin_fichier) else: raise ValueError(f"Format de fichier non supporté : {extension}") except Exception as e: raise ValueError(f"Impossible de lire le fichier ({e})") return df # --------------------------------------------------------------------------- # 5. TRAITEMENT COMPLET DU FICHIER (fonction appelée par le bouton Gradio) # --------------------------------------------------------------------------- def _process_file_impl(fichier, nom_modele, format_sortie, progress=gr.Progress()): """ Fonction principale déclenchée par le bouton "Lancer la traduction". - Lit le fichier - Traduit chaque ligne de la colonne "français" - Écrit/écrase la colonne "moore" - Exporte le résultat au format choisi (CSV ou Excel) """ if fichier is None: return None, "❌ Aucun fichier n'a été fourni." # --- Lecture du fichier --- try: df = lire_fichier(fichier.name) except Exception as e: return None, f"❌ Erreur de lecture du fichier : {e}" if "français" not in df.columns: return None, "❌ Le fichier doit contenir une colonne nommée exactement 'français'." # --- Chargement du modèle (mis en cache après le premier appel) --- progress(0, desc="Chargement du modèle...") try: tokenizer, modele, config = load_model(nom_modele) except Exception as e: return None, f"❌ Impossible de charger le modèle sélectionné : {e}" # --- Traduction ligne par ligne avec barre de progression --- nb_lignes = len(df) traductions = [] nb_reussies = 0 nb_vides = 0 nb_erreurs = 0 for i, valeur in enumerate(df["français"].tolist()): progress((i + 1) / max(nb_lignes, 1), desc=f"Traduction {i + 1}/{nb_lignes}") texte = "" if pd.isna(valeur) else str(valeur) if texte.strip() == "": traductions.append("") nb_vides += 1 continue try: resultat = translate_text(texte, tokenizer, modele, config) traductions.append(resultat) nb_reussies += 1 except Exception: # On n'interrompt jamais le traitement global : on trace # l'erreur dans la cellule et on continue avec la ligne suivante. traductions.append("[ERREUR]") nb_erreurs += 1 print(f"[ERREUR ligne {i}] {traceback.format_exc()}") # La colonne "moore" est créée ou écrasée si elle existait déjà df["moore"] = traductions # --- Export du fichier résultat --- nom_base = "resultat_traduction" dossier_sortie = "sorties_traduction" os.makedirs(dossier_sortie, exist_ok=True) if format_sortie == "Excel (.xlsx)": chemin_sortie = os.path.join(dossier_sortie, f"{nom_base}.xlsx") df.to_excel(chemin_sortie, index=False) else: # "CSV (.csv)" chemin_sortie = os.path.join(dossier_sortie, f"{nom_base}.csv") # Encodage UTF-8 avec BOM pour une ouverture correcte dans Excel/Windows df.to_csv(chemin_sortie, index=False, encoding="utf-8-sig") rapport = ( f"✅ Traitement terminé sur {DEVICE.upper()}.\n" f"Total : {nb_lignes} lignes | " f"Traduites : {nb_reussies} | " f"Vides ignorées : {nb_vides} | " f"Erreurs : {nb_erreurs}" ) return chemin_sortie, rapport # Sur un Space "ZeroGPU", la fonction qui utilise réellement le GPU doit être # décorée avec @spaces.GPU pour que Hugging Face lui alloue dynamiquement une # carte le temps de l'exécution. `duration` (en secondes) borne le temps # d'allocation GPU accordé pour un appel : on le majore pour laisser le temps # de traduire des fichiers de plusieurs centaines de lignes. if _SPACES_DISPONIBLE: process_file = spaces.GPU(duration=120)(_process_file_impl) else: process_file = _process_file_impl # --------------------------------------------------------------------------- # 6. INTERFACE GRADIO # --------------------------------------------------------------------------- with gr.Blocks(title="Traducteur Français ↔ Mooré") as demo: gr.Markdown( """ # 🇧🇫 Traducteur Français → Mooré (traitement par lots) Téléversez un fichier CSV ou Excel contenant une colonne **"français"**, choisissez un modèle de traduction, puis lancez le traitement. Le fichier résultat contiendra une colonne **"moore"** avec les traductions. """ ) with gr.Row(): with gr.Column(): fichier_entree = gr.File( label="Fichier à traduire (.csv, .xlsx, .xls)", file_types=[".csv", ".xlsx", ".xls"], ) choix_modele = gr.Dropdown( label="Modèle de traduction", choices=list(MODELES.keys()), value=list(MODELES.keys())[0], ) format_sortie = gr.Radio( label="Format du fichier de sortie", choices=["CSV (.csv)", "Excel (.xlsx)"], value="CSV (.csv)", ) bouton_lancer = gr.Button("🚀 Lancer la traduction", variant="primary") with gr.Column(): fichier_sortie = gr.File(label="Fichier traduit (téléchargement)") statut = gr.Textbox(label="Statut / Rapport", lines=4, interactive=False) bouton_lancer.click( fn=process_file, inputs=[fichier_entree, choix_modele, format_sortie], outputs=[fichier_sortie, statut], ) gr.Markdown( f""" --- ℹ️ Appareil de calcul détecté : **{DEVICE.upper()}**. Les modèles sont chargés une seule fois puis conservés en mémoire (cache). """ ) if __name__ == "__main__": demo.launch()