/ utils / segmentation.py
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