Coverage for presidio_analyzer / chunkers / base_chunker.py: 94%
49 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1"""Abstract base class for text chunking strategies."""
2from abc import ABC, abstractmethod
3from dataclasses import dataclass
4from typing import TYPE_CHECKING, Callable, List
6if TYPE_CHECKING:
7 from presidio_analyzer import RecognizerResult
10@dataclass
11class TextChunk:
12 """Represents a chunk of text with its position in the original text.
14 :param text: The chunk content
15 :param start: Start position in the original text (inclusive)
16 :param end: End position in the original text (exclusive)
17 """
19 text: str
20 start: int
21 end: int
24class BaseTextChunker(ABC):
25 """Abstract base class for text chunking strategies.
27 Subclasses must implement the chunk() method to split text into
28 TextChunk objects that include both content and position information.
30 Provides methods for processing predictions across chunks and
31 deduplicating overlapping entities.
32 """
34 @abstractmethod
35 def chunk(self, text: str) -> List[TextChunk]:
36 """Split text into chunks with position information.
38 :param text: The input text to split
39 :return: List of TextChunk objects with text and position data
40 """
41 pass
43 def predict_with_chunking(
44 self,
45 text: str,
46 predict_func: Callable[[str], List["RecognizerResult"]],
47 ) -> List["RecognizerResult"]:
48 """Process text with automatic chunking for long texts.
50 For short text, calls predict_func directly.
51 For long text, chunks it and merges predictions with deduplication.
53 :param text: Input text to process
54 :param predict_func: Function that takes text and returns
55 RecognizerResult objects
56 :return: List of RecognizerResult with correct offsets
57 """
58 chunks = self.chunk(text)
59 if not chunks:
60 return []
61 if len(chunks) == 1:
62 return predict_func(text)
64 predictions = self._process_chunks(chunks, predict_func)
65 return self.deduplicate_overlapping_entities(predictions)
67 def _process_chunks(
68 self,
69 chunks: List[TextChunk],
70 process_func: Callable[[str], List["RecognizerResult"]],
71 ) -> List["RecognizerResult"]:
72 """Process text chunks and adjust entity offsets.
74 :param chunks: List of TextChunk objects with text and position information
75 :param process_func: Function that takes chunk text and returns
76 RecognizerResult objects
77 :return: List of RecognizerResult with adjusted offsets
78 """
79 from presidio_analyzer import RecognizerResult
81 all_predictions = []
83 for chunk in chunks:
84 chunk_predictions = process_func(chunk.text)
86 # Create new RecognizerResult objects with adjusted offsets
87 # to avoid mutating the original predictions
88 for pred in chunk_predictions:
89 adjusted_pred = RecognizerResult(
90 entity_type=pred.entity_type,
91 start=pred.start + chunk.start,
92 end=pred.end + chunk.start,
93 score=pred.score,
94 analysis_explanation=pred.analysis_explanation,
95 recognition_metadata=pred.recognition_metadata,
96 )
97 all_predictions.append(adjusted_pred)
99 return all_predictions
101 def deduplicate_overlapping_entities(
102 self,
103 predictions: List["RecognizerResult"],
104 overlap_threshold: float = 0.5,
105 ) -> List["RecognizerResult"]:
106 """Remove duplicate entities from overlapping chunks.
108 :param predictions: List of RecognizerResult objects
109 :param overlap_threshold: Overlap ratio threshold to consider duplicates
110 (default: 0.5)
111 :return: Deduplicated list of RecognizerResult sorted by position
112 """
113 if not predictions:
114 return predictions
116 # Sort by score descending to keep highest scoring entities
117 sorted_preds = sorted(predictions, key=lambda p: p.score, reverse=True)
118 unique = []
120 for pred in sorted_preds:
121 is_duplicate = False
122 for kept in unique:
123 # Check if same entity type and overlapping positions
124 if pred.entity_type == kept.entity_type:
125 overlap_start = max(pred.start, kept.start)
126 overlap_end = min(pred.end, kept.end)
128 if overlap_start < overlap_end:
129 # Calculate overlap ratio
130 overlap_len = overlap_end - overlap_start
131 pred_len = pred.end - pred.start
132 kept_len = kept.end - kept.start
134 if pred_len <= 0 or kept_len <= 0:
135 continue
137 # Check if overlap exceeds threshold
138 if overlap_len / min(pred_len, kept_len) > overlap_threshold:
139 is_duplicate = True
140 break
142 if not is_duplicate:
143 unique.append(pred)
145 # Sort by position for consistent output
146 return sorted(unique, key=lambda p: p.start)