#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ API pour traiter les requêtes de correction depuis la page web. Ce script: 1. Fournit des endpoints pour appliquer des corrections au fichier livre.org 2. Permet d'ajouter des mots au dictionnaire personnalisé 3. Permet de marquer des erreurs comme "à ne pas traiter" dans le CSV Modifications (2025-08-30): - Amélioration de la recherche des chapitres dans le fichier livre.org - Le motif de recherche est maintenant plus flexible pour gérer les chapitres avec des tags ou du texte supplémentaire - Ajout de logs de débogage pour aider à diagnostiquer les problèmes de correspondance de chapitres - Amélioration des fonctions load_spelling_errors et load_grammar_errors pour utiliser une correspondance flexible """ import os import re import json import csv from flask import Flask, request, jsonify, Blueprint # Créer un Blueprint pour l'API corrections_api = Blueprint('corrections_api', __name__) # Chemin vers les fichiers LIVRE_PATH = 'livre.org' DICT_PATH = 'dictionnaire_personnalise.txt' CSV_PATH = 'resume_erreurs.csv' @corrections_api.route('/api/corrections', methods=['POST']) def handle_corrections(): """Endpoint pour traiter les requêtes de correction.""" data = request.json if not data or 'action' not in data: return jsonify({'success': False, 'message': 'Action non spécifiée'}), 400 action = data['action'] if action == 'apply_correction': return apply_correction(data) elif action == 'ignore_error': return ignore_error(data) elif action == 'add_to_dictionary': return add_to_dictionary(data) else: return jsonify({'success': False, 'message': 'Action non reconnue'}), 400 def apply_correction(data): """Applique une correction au fichier livre.org.""" # Vérifier les paramètres requis required_params = ['error_type', 'chapter', 'error_index', 'correction'] if not all(param in data for param in required_params): return jsonify({'success': False, 'message': 'Paramètres manquants'}), 400 error_type = data['error_type'] chapter = data['chapter'] error_index = data['error_index'] correction = data['correction'] try: # Lire le contenu du fichier with open(LIVRE_PATH, 'r', encoding='utf-8') as file: content = file.read() # Trouver la section du chapitre # Motif plus flexible qui permet des textes supplémentaires après le nom du chapitre chapter_pattern = r'^\*\* ' + re.escape(chapter) + r'(?:\s+.*?)?$(.*?)(?=^\*\* |\Z)' chapter_match = re.search(chapter_pattern, content, re.MULTILINE | re.DOTALL) # Si le motif flexible ne trouve pas de correspondance, essayer avec le motif original if not chapter_match: # Ajouter des logs pour le débogage print(f"Recherche du chapitre: '{chapter}'") print(f"Motif utilisé: {chapter_pattern}") # Lister tous les titres de chapitres dans le fichier pour le débogage all_chapters = re.findall(r'^\*\* (.*?)$', content, re.MULTILINE) print(f"Chapitres trouvés dans le fichier: {all_chapters}") return jsonify({ 'success': False, 'message': f'Chapitre "{chapter}" non trouvé', 'debug_info': { 'pattern_used': chapter_pattern, 'chapters_in_file': all_chapters } }), 404 chapter_content = chapter_match.group(1) chapter_start = chapter_match.start() chapter_end = chapter_match.end() # Appliquer la correction en fonction du type d'erreur if error_type == 'spelling': # Charger les erreurs d'orthographe pour ce chapitre errors = load_spelling_errors(chapter) if error_index >= len(errors): return jsonify({'success': False, 'message': 'Index d\'erreur invalide'}), 400 error = errors[error_index] word = error['word'] # Remplacer le mot dans le contenu du chapitre modified_chapter = re.sub(r'\b' + re.escape(word) + r'\b', correction, chapter_content) # Mettre à jour le contenu complet new_content = content[:chapter_start] + '** ' + chapter + modified_chapter + content[chapter_end:] elif error_type == 'grammar': # Charger les erreurs grammaticales pour ce chapitre errors = load_grammar_errors(chapter) if error_index >= len(errors): return jsonify({'success': False, 'message': 'Index d\'erreur invalide'}), 400 error = errors[error_index] error_text = error['error_text'] context = error['context'] # Trouver le contexte dans le chapitre # Créer un motif plus flexible pour la recherche de contexte # 1. Échapper les caractères spéciaux dans le contexte escaped_context = re.escape(context) # 2. Remplacer les espaces échappées par un motif qui accepte plusieurs espaces flexible_context = escaped_context.replace('\\ ', '\\s+') # 3. Créer un motif qui permet des variations au début et à la fin du contexte flexible_pattern = flexible_context # Essayer d'abord avec le motif flexible context_match = re.search(flexible_pattern, chapter_content) # Si ça ne fonctionne pas, essayer avec une approche encore plus flexible if not context_match: # Extraire les mots significatifs du contexte (ignorer les mots très courts) words = [word for word in re.findall(r'\b\w+\b', context) if len(word) > 3] if words: # Créer un motif qui recherche une phrase contenant ces mots dans l'ordre words_pattern = r'.*'.join([re.escape(word) for word in words]) context_match = re.search(words_pattern, chapter_content) # Si toujours pas de correspondance, essayer de trouver juste l'erreur et son contexte immédiat if not context_match and error_text: # Chercher l'erreur avec quelques mots avant et après error_vicinity = r'[^\n]{0,30}' + re.escape(error_text) + r'[^\n]{0,30}' context_match = re.search(error_vicinity, chapter_content) if not context_match: # Ajouter des logs de débogage print(f"Contexte non trouvé: '{context}'") print(f"Motif flexible utilisé: {flexible_pattern}") print(f"Erreur à corriger: '{error_text}'") print(f"Correction à appliquer: '{correction}'") print(f"Extrait du chapitre (100 premiers caractères): '{chapter_content[:100]}'") return jsonify({ 'success': False, 'message': 'Contexte non trouvé dans le chapitre', 'debug_info': { 'context': context, 'error_text': error_text, 'correction': correction, 'chapter_excerpt': chapter_content[:200] # Premiers 200 caractères du chapitre } }), 404 # Remplacer l'erreur dans le contexte trouvé matched_text = context_match.group(0) # Déterminer quelle stratégie de remplacement utiliser if matched_text == context: # Si le texte correspond exactement au contexte, utiliser la méthode simple corrected_context = context.replace(error_text, correction) modified_chapter = chapter_content.replace(matched_text, corrected_context) else: # Si nous avons utilisé une correspondance flexible, nous devons être plus précis # Trouver la position de l'erreur dans le contexte original error_pos = context.find(error_text) if error_pos >= 0: # Calculer les positions relatives dans le texte trouvé error_length = len(error_text) # Essayer de trouver l'erreur exacte dans le texte trouvé error_in_match = re.search(re.escape(error_text), matched_text) if error_in_match: # Remplacer directement l'erreur dans le texte trouvé start, end = error_in_match.span() corrected_matched_text = matched_text[:start] + correction + matched_text[end:] modified_chapter = chapter_content.replace(matched_text, corrected_matched_text) else: # Fallback: remplacer tout le texte trouvé corrected_context = context.replace(error_text, correction) modified_chapter = chapter_content.replace(matched_text, corrected_context) else: # Si nous ne pouvons pas localiser l'erreur, utiliser une approche plus simple corrected_context = context.replace(error_text, correction) modified_chapter = chapter_content.replace(matched_text, corrected_context) # Ajouter des logs pour le débogage print(f"Correction appliquée: '{error_text}' -> '{correction}'") print(f"Contexte trouvé: '{matched_text}'") print(f"Contexte après correction: '{corrected_context if 'corrected_context' in locals() else 'N/A'}'") # Si le chapitre n'a pas été modifié, c'est qu'il y a un problème if modified_chapter == chapter_content: print("AVERTISSEMENT: Le chapitre n'a pas été modifié après la correction!") # Mettre à jour le contenu complet new_content = content[:chapter_start] + '** ' + chapter + modified_chapter + content[chapter_end:] else: return jsonify({'success': False, 'message': 'Type d\'erreur non reconnu'}), 400 # Écrire le contenu modifié dans le fichier with open(LIVRE_PATH, 'w', encoding='utf-8') as file: file.write(new_content) return jsonify({'success': True, 'message': 'Correction appliquée avec succès'}) except Exception as e: return jsonify({'success': False, 'message': f'Erreur lors de l\'application de la correction: {str(e)}'}), 500 def ignore_error(data): """Marque une erreur comme 'à ne pas traiter' dans le CSV.""" # Vérifier les paramètres requis required_params = ['error_type', 'chapter', 'error_index'] if not all(param in data for param in required_params): return jsonify({'success': False, 'message': 'Paramètres manquants'}), 400 error_type = data['error_type'] chapter = data['chapter'] error_index = data['error_index'] try: # Lire le CSV des erreurs rows = [] with open(CSV_PATH, 'r', newline='', encoding='utf-8') as csvfile: reader = csv.reader(csvfile) headers = next(reader) # Lire les en-têtes # Ajouter les en-têtes à la liste des lignes rows.append(headers) # Parcourir les lignes et mettre à jour celle du chapitre concerné for row in reader: if len(row) > 0 and row[0] == chapter: # Mettre à jour la ligne en fonction du type d'erreur if error_type == 'spelling' and len(row) > 1: # Décrémenter le nombre d'erreurs d'orthographe spelling_errors = int(row[1]) - 1 row[1] = str(max(0, spelling_errors)) # Mettre à jour le total if len(row) > 3: row[3] = str(int(row[3]) - 1) elif error_type == 'grammar' and len(row) > 2: # Décrémenter le nombre d'erreurs grammaticales grammar_errors = int(row[2]) - 1 row[2] = str(max(0, grammar_errors)) # Mettre à jour le total if len(row) > 3: row[3] = str(int(row[3]) - 1) rows.append(row) # Écrire les modifications dans le CSV with open(CSV_PATH, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.writer(csvfile) writer.writerows(rows) return jsonify({'success': True, 'message': 'Erreur ignorée avec succès'}) except Exception as e: return jsonify({'success': False, 'message': f'Erreur lors de l\'ignorance de l\'erreur: {str(e)}'}), 500 def add_to_dictionary(data): """Ajoute un mot au dictionnaire personnalisé.""" # Vérifier les paramètres requis if 'word' not in data: return jsonify({'success': False, 'message': 'Mot non spécifié'}), 400 word = data['word'] try: # Vérifier si le mot est déjà dans le dictionnaire existing_words = set() if os.path.exists(DICT_PATH): with open(DICT_PATH, 'r', encoding='utf-8') as file: for line in file: line = line.strip() if line and not line.startswith('#'): existing_words.add(line.lower()) # Si le mot n'est pas déjà dans le dictionnaire, l'ajouter if word.lower() not in existing_words: with open(DICT_PATH, 'a', encoding='utf-8') as file: file.write(f"\n{word}") return jsonify({'success': True, 'message': 'Mot ajouté au dictionnaire avec succès'}) except Exception as e: return jsonify({'success': False, 'message': f'Erreur lors de l\'ajout du mot au dictionnaire: {str(e)}'}), 500 def load_spelling_errors(chapter): """Charge les erreurs d'orthographe pour un chapitre donné depuis le rapport.""" # Cette fonction devrait charger les erreurs d'orthographe depuis le rapport # Pour simplifier, nous utilisons une implémentation fictive # Dans une implémentation réelle, il faudrait parser le rapport d'erreurs # Exemple d'implémentation fictive from generate_corrections_page import parse_error_report errors_by_chapter = parse_error_report('rapport_orthographe_grammaire.md') # Recherche exacte d'abord if chapter in errors_by_chapter: return errors_by_chapter[chapter]['spelling'] # Si pas trouvé, essayer une recherche plus flexible # Chercher un chapitre qui commence par le même nom (sans les tags ou autres textes) for chap_name in errors_by_chapter.keys(): # Vérifier si le chapitre commence par le nom recherché if chap_name.startswith(chapter) or chapter.startswith(chap_name): print(f"Correspondance flexible trouvée: '{chapter}' -> '{chap_name}'") return errors_by_chapter[chap_name]['spelling'] print(f"Aucune erreur d'orthographe trouvée pour le chapitre: '{chapter}'") print(f"Chapitres disponibles: {list(errors_by_chapter.keys())}") return [] def load_grammar_errors(chapter): """Charge les erreurs grammaticales pour un chapitre donné depuis le rapport.""" # Cette fonction devrait charger les erreurs grammaticales depuis le rapport # Pour simplifier, nous utilisons une implémentation fictive # Dans une implémentation réelle, il faudrait parser le rapport d'erreurs # Exemple d'implémentation fictive from generate_corrections_page import parse_error_report errors_by_chapter = parse_error_report('rapport_orthographe_grammaire.md') # Recherche exacte d'abord if chapter in errors_by_chapter: return errors_by_chapter[chapter]['grammar'] # Si pas trouvé, essayer une recherche plus flexible # Chercher un chapitre qui commence par le même nom (sans les tags ou autres textes) for chap_name in errors_by_chapter.keys(): # Vérifier si le chapitre commence par le nom recherché if chap_name.startswith(chapter) or chapter.startswith(chap_name): print(f"Correspondance flexible trouvée: '{chapter}' -> '{chap_name}'") return errors_by_chapter[chap_name]['grammar'] print(f"Aucune erreur grammaticale trouvée pour le chapitre: '{chapter}'") print(f"Chapitres disponibles: {list(errors_by_chapter.keys())}") return [] # Pour tester l'API indépendamment if __name__ == '__main__': app = Flask(__name__) app.register_blueprint(corrections_api) app.run(debug=True, port=5001)