segmentation.py
1 """ 2 Text segmentation utilities for processing large texts. 3 """ 4 import logging 5 import re 6 from typing import List, Optional, Tuple 7 8 # Logging configuration 9 logger = logging.getLogger("utils.segmentation") 10 11 def split_text_into_segments( 12 text: str, 13 max_length: int = 2000, 14 overlap: int = 200 15 ) -> List[str]: 16 """ 17 Splits text into segments while preserving semantic integrity using 18 intelligent boundary detection. 19 20 Args: 21 text: Text to split 22 max_length: Maximum length of each segment 23 overlap: Number of characters to overlap between segments 24 25 Returns: 26 List of text segments 27 """ 28 if not text: 29 return [] 30 31 if len(text) <= max_length: 32 return [text] 33 34 # Prétraitement: normalisation des sauts de ligne 35 text = re.sub(r'\r\n', '\n', text) 36 37 segments = [] 38 start = 0 39 40 # Reconnaissance améliorée des limites de phrases 41 sentence_boundaries = r'(?<=[.!?;])\s+(?=[A-Z0-9])' 42 paragraph_boundaries = r'\n\s*\n' 43 section_boundaries = r'(?:^|\n)#+\s+.+?(?:\n|$)|(?:^|\n)\*\*.*?\*\*(?:\n|$)' 44 45 while start < len(text): 46 # Déterminer la position de fin potentielle 47 end_pos = start + max_length 48 49 if end_pos >= len(text): 50 segments.append(text[start:]) 51 break 52 53 # Trouver le meilleur point de coupure en analysant le contexte 54 best_break = find_optimal_break_point(text, start, end_pos) 55 56 # Éviter la coupure à l'intérieur des structures comme les citations ou les listes 57 if is_inside_structure(text, best_break): 58 # Chercher la fin ou le début de la structure 59 best_break = adjust_for_structures(text, best_break, start, end_pos) 60 61 # Ajouter le segment 62 segments.append(text[start:best_break]) 63 64 # Déplacer le début avec chevauchement 65 start = max(start + 1, best_break - overlap) 66 67 return segments 68 69 def find_optimal_break_point(text: str, start: int, end: int) -> int: 70 """ 71 Trouve le point de coupure optimal en fonction du contexte linguistique. 72 """ 73 # Priorité pour la recherche des points de coupure 74 break_hierarchy = [ 75 # 1. Limites de sections (titres, sous-titres) 76 (r'\n#{1,6}\s+', 800), 77 78 # 2. Sauts de paragraphe 79 (r'\n\s*\n', 700), 80 81 # 3. Fin de phrase suivie d'une nouvelle phrase 82 (r'(?<=[.!?])\s+(?=[A-Z])', 600), 83 84 # 4. Fin de phrase (autres cas) 85 (r'(?<=[.!?])\s+', 500), 86 87 # 5. Points-virgules et autres séparateurs forts 88 (r'(?<=;)\s+', 400), 89 90 # 6. Virgules entre clauses 91 (r'(?<=,)\s+(?=(?:and|or|but|nor|yet|so|whereas|while|although|though|because|since|unless|until|if|when|where|which|who|whom|whose))', 300), 92 93 # 7. Simples virgules 94 (r'(?<=,)\s+', 200), 95 96 # 8. Sauts de ligne simples 97 (r'\n', 100), 98 ] 99 100 # Partir de la fin et chercher vers le début 101 search_area = text[start:end] 102 best_break = end 103 best_priority = 0 104 105 # Rechercher le meilleur point selon la hiérarchie 106 for pattern, priority in break_hierarchy: 107 matches = list(re.finditer(pattern, search_area)) 108 if matches: 109 # Privilégier les coupures qui sont au moins à mi-chemin 110 for match in reversed(matches): 111 match_pos = start + match.start() 112 # S'assurer que nous sommes au moins à 40% du chemin dans le segment 113 min_pos = start + (end - start) * 0.4 114 if match_pos >= min_pos and priority > best_priority: 115 best_break = match_pos + len(match.group(0)) 116 best_priority = priority 117 break 118 119 # Si aucun point de coupure idéal n'a été trouvé, utiliser la position maximum 120 return best_break 121 122 def is_inside_structure(text: str, position: int) -> bool: 123 """ 124 Vérifie si la position se trouve à l'intérieur d'une structure spéciale 125 comme une citation, un bloc de code, une liste, etc. 126 """ 127 # Définir les marqueurs de début et de fin des structures spéciales 128 structures = [ 129 (r'```', r'```'), # Blocs de code 130 (r'~~~', r'~~~'), # Blocs alternatifs 131 (r'`', r'`'), # Code inline 132 (r'\(', r'\)'), # Parenthèses 133 (r'\[', r'\]'), # Crochets 134 (r'"', r'"'), # Citations doubles 135 (r"'", r"'"), # Citations simples 136 (r'<', r'>'), # Tags HTML 137 ] 138 139 # Vérifier pour chaque type de structure 140 pre_context = text[:position] 141 post_context = text[position:] 142 143 for start_marker, end_marker in structures: 144 # Compter les occurrences des marqueurs de début et de fin avant la position 145 starts = len(re.findall(start_marker, pre_context)) 146 ends = len(re.findall(end_marker, pre_context)) 147 148 # Si le nombre de débuts est supérieur au nombre de fins, 149 # nous sommes à l'intérieur d'une structure 150 if starts > ends: 151 return True 152 153 return False 154 155 def adjust_for_structures(text: str, position: int, min_pos: int, max_pos: int) -> int: 156 """ 157 Ajuste la position pour éviter de couper à l'intérieur des structures spéciales. 158 """ 159 # Chercher la fin de la structure la plus proche 160 post_context = text[position:max_pos] 161 structure_end_patterns = [r'```', r'~~~', r'`', r'\)', r'\]', r'"', r"'", r'>'] 162 163 for pattern in structure_end_patterns: 164 match = re.search(pattern, post_context) 165 if match: 166 return position + match.end() 167 168 # Si aucune fin de structure n'est trouvée, chercher le début de structure précédent 169 pre_context = text[min_pos:position] 170 structure_start_patterns = [r'```', r'~~~', r'`', r'\(', r'\[', r'"', r"'", r'<'] 171 172 for pattern in reversed(structure_start_patterns): 173 matches = list(re.finditer(pattern, pre_context)) 174 if matches: 175 return min_pos + matches[-1].start() 176 177 # Si aucun ajustement n'est possible, retourner la position d'origine 178 return position