dstpr 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstpr
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A high-throughput, domain-specific text preprocessing cascading pipeline filter to rank core sentences in texts and filter away boilerplate.
5
5
  Author-email: Stephen Meisenbacher <sjmeis@gtgd.com>
6
6
  Maintainer-email: Stephen Meisenbacher <sjmeis@gtgd.com>
@@ -76,7 +76,7 @@ This is the recommended approach for large-scale data pipelines.
76
76
  ```python
77
77
  from dstpr import ParallelPreprocessingPipeline, JOB_POSTING_PROFILE
78
78
 
79
- pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE, batch_size=64)
79
+ pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE)
80
80
 
81
81
  raw_documents = [
82
82
  "DOC 1",
@@ -84,41 +84,13 @@ raw_documents = [
84
84
  "..."
85
85
  ]
86
86
 
87
- cleaned_documents = pipeline.process(raw_documents, num_workers=8, threshold=0.25)
88
- ```
89
-
90
- ### Advanced Usage
91
-
92
- If you want to integrate specific pipeline layers directly into an existing workflow, or tweak the internal parameters, you can import individual modules manually:
93
-
94
- ```python
95
- from dstpr.cleaners import clean_and_split_chunks
96
- from dstpr.segmenters import SemanticSectionRouter
97
- from dstpr.rankers import HybridRanker
98
- from sentence_transformers import SentenceTransformer
99
-
100
- embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
101
-
102
- text = "Job posting text goes here!"
103
-
104
- # Clean and segment sentences
105
- sentences = clean_and_split_chunks(text)
106
-
107
- # Route by context
108
- router = SemanticSectionRouter(model=embedding_model)
109
- buckets = router.route_sentences(sentences)
110
- target_sentences = buckets['CORE'] + buckets['REQUIREMENTS']
111
-
112
- # Grade and sort text features
113
- ranker = HybridRanker()
114
- final = ranker.rank_and_filter(target_sentences, bi_cutoff_pct=0.25, final_threshold=0.40)
87
+ cleaned_documents = pipeline.process(raw_documents, threshold=0.25)
115
88
  ```
116
89
 
117
90
  ### Parameter Tuning
118
91
 
119
92
  To adjust the trade-off between strict filtering and execution speed, consider tweaking these variables in `ParallelPreprocessingPipeline`:
120
93
  - `threshold` (Default: `0.4`): Controls how aggressively sentences are discarded. Raising this value toward `0.6`, for example, ensures only stronger matching sentences are passed through. Lowering it toward `0.3`, on the other hand, acts as a wider net.
121
- - `batch_size` (Default: `256`): This depends on your hardware! Adjust for best performance.
122
94
 
123
95
  ## Creating Custom Domain Profiles (via the provided wizard)
124
96
 
@@ -36,7 +36,7 @@ This is the recommended approach for large-scale data pipelines.
36
36
  ```python
37
37
  from dstpr import ParallelPreprocessingPipeline, JOB_POSTING_PROFILE
38
38
 
39
- pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE, batch_size=64)
39
+ pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE)
40
40
 
41
41
  raw_documents = [
42
42
  "DOC 1",
@@ -44,41 +44,13 @@ raw_documents = [
44
44
  "..."
45
45
  ]
46
46
 
47
- cleaned_documents = pipeline.process(raw_documents, num_workers=8, threshold=0.25)
48
- ```
49
-
50
- ### Advanced Usage
51
-
52
- If you want to integrate specific pipeline layers directly into an existing workflow, or tweak the internal parameters, you can import individual modules manually:
53
-
54
- ```python
55
- from dstpr.cleaners import clean_and_split_chunks
56
- from dstpr.segmenters import SemanticSectionRouter
57
- from dstpr.rankers import HybridRanker
58
- from sentence_transformers import SentenceTransformer
59
-
60
- embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
61
-
62
- text = "Job posting text goes here!"
63
-
64
- # Clean and segment sentences
65
- sentences = clean_and_split_chunks(text)
66
-
67
- # Route by context
68
- router = SemanticSectionRouter(model=embedding_model)
69
- buckets = router.route_sentences(sentences)
70
- target_sentences = buckets['CORE'] + buckets['REQUIREMENTS']
71
-
72
- # Grade and sort text features
73
- ranker = HybridRanker()
74
- final = ranker.rank_and_filter(target_sentences, bi_cutoff_pct=0.25, final_threshold=0.40)
47
+ cleaned_documents = pipeline.process(raw_documents, threshold=0.25)
75
48
  ```
76
49
 
77
50
  ### Parameter Tuning
78
51
 
79
52
  To adjust the trade-off between strict filtering and execution speed, consider tweaking these variables in `ParallelPreprocessingPipeline`:
80
53
  - `threshold` (Default: `0.4`): Controls how aggressively sentences are discarded. Raising this value toward `0.6`, for example, ensures only stronger matching sentences are passed through. Lowering it toward `0.3`, on the other hand, acts as a wider net.
81
- - `batch_size` (Default: `256`): This depends on your hardware! Adjust for best performance.
82
54
 
83
55
  ## Creating Custom Domain Profiles (via the provided wizard)
84
56
 
@@ -1,15 +1,18 @@
1
1
  import re
2
2
  from dstpr.profiles import DomainProfile
3
3
 
4
- def sanitize_outputs(sentences: list[str], profile: DomainProfile, min_length: int = 35) -> str:
4
+ LEADING_JUNK_RE = re.compile(r'^[\s\-\d\.\,\*•■\:\>]+')
5
+ TRAILING_NUM_RE = re.compile(r'\b\d+[\.,]\d+$|\b\d+$')
6
+ MULTI_DOT_SPACE_RE = re.compile(r'\s*\.+\s*')
7
+
8
+ def sanitize_outputs(sentences: list[str], profile: DomainProfile, min_length: int = 20) -> str:
5
9
  if not sentences:
6
10
  return ""
7
11
 
8
12
  seen = set()
9
13
  cleaned_pool = []
10
14
 
11
- leading_junk_pattern = re.compile(r'^[\s\-\d\.\,\*•■\:\>]+', re.IGNORECASE)
12
- junk_matcher = re.compile(profile.junk_line_regex, re.IGNORECASE) if profile.junk_line_regex else None
15
+ junk_matcher = profile._junk_line_re
13
16
 
14
17
  for sentence in sentences:
15
18
  sent = sentence.strip()
@@ -21,18 +24,18 @@ def sanitize_outputs(sentences: list[str], profile: DomainProfile, min_length: i
21
24
  if junk_matcher and junk_matcher.search(sent):
22
25
  continue
23
26
 
24
- sent = leading_junk_pattern.sub('', sent)
27
+ sent = LEADING_JUNK_RE.sub('', sent)
25
28
 
26
29
  if sent:
27
30
  sent = sent[0].upper() + sent[1:]
28
- sent = re.sub(r'\s*\.+\s*', '. ', sent).strip()
31
+ sent = MULTI_DOT_SPACE_RE.sub('. ', sent).strip()
29
32
  if not sent.endswith('.'):
30
33
  sent += '.'
31
34
 
32
35
  if len(sent) < min_length:
33
36
  continue
34
37
 
35
- if re.search(r'\b\d+[\.,]\d+$|\b\d+$', sent):
38
+ if TRAILING_NUM_RE.search(sent):
36
39
  continue
37
40
 
38
41
  cleaned_pool.append(sent)
@@ -41,6 +44,5 @@ def sanitize_outputs(sentences: list[str], profile: DomainProfile, min_length: i
41
44
  final_text = re.sub(r'\s+', ' ', final_text)
42
45
  final_text = re.sub(r'\s*\.\s*', '. ', final_text)
43
46
  final_text = re.sub(r'\.+', '.', final_text)
44
- final_text = re.sub(r'\.+', '.', final_text)
45
47
 
46
48
  return final_text.strip()
@@ -2,16 +2,19 @@ import re
2
2
  import nltk
3
3
  from dstpr.profiles import DomainProfile
4
4
 
5
+ INLINE_SPLIT_RE = re.compile(r'(?:[\•\·\■\*]|\(\d+\)|\b\d+\)|\;\s+(?=[A-Z]))')
6
+
5
7
  def clean_and_split_chunks(text: str, profile: DomainProfile, min_length: int = 15) -> list[str]:
6
8
  if not text or not isinstance(text, str):
7
9
  return []
8
10
 
9
- if profile.raw_noise_regex:
10
- text = re.sub(profile.raw_noise_regex, '', text, flags=re.IGNORECASE)
11
- lines = [line.strip() for line in text.split("\n") if line.strip()]
11
+ if profile._raw_noise_re:
12
+ text = profile._raw_noise_re.sub(' ', text)
12
13
 
13
- meta_matcher = re.compile(profile.metadata_line_regex, re.IGNORECASE) if profile.metadata_line_regex else None
14
-
14
+ text = INLINE_SPLIT_RE.sub('\n', text)
15
+ lines = [line.strip() for line in text.split("\n") if line.strip()]
16
+ meta_matcher = profile._meta_line_re
17
+
15
18
  normalized_chunks = []
16
19
  for line in lines:
17
20
  if meta_matcher and meta_matcher.search(line):
@@ -20,9 +23,8 @@ def clean_and_split_chunks(text: str, profile: DomainProfile, min_length: int =
20
23
  if not line.endswith(('.', '!', '?', ';', ':')):
21
24
  # If it's short (< 6 words) and lacks punctuation, it's a header line, do not append
22
25
  if len(line.split()) < 6:
23
- line = line + ":"
24
- else:
25
- line = line + "."
26
+ continue
27
+ line = line + "."
26
28
  normalized_chunks.append(line)
27
29
 
28
30
  normalized_text = " ".join(normalized_chunks)
@@ -33,11 +35,9 @@ def clean_and_split_chunks(text: str, profile: DomainProfile, min_length: int =
33
35
  clean_sentences = []
34
36
  for sentence in sentences:
35
37
  sentence_clean = sentence.strip()
36
- sentence_lower = sentence_clean.lower()
37
-
38
38
  if len(sentence_clean) < min_length:
39
39
  continue
40
- if profile.global_stop_phrases and any(phrase in sentence_lower for phrase in profile.global_stop_phrases):
40
+ if profile._stop_phrases_re and profile._stop_phrases_re.search(sentence_clean):
41
41
  continue
42
42
 
43
43
  clean_sentences.append(sentence_clean)
@@ -74,7 +74,6 @@ class ParallelPreprocessingPipeline:
74
74
  def _cpu_preprocess_chunk(self, raw_docs: list[str]) -> dict:
75
75
  doc_sentence_counts = []
76
76
  flat_sentences = []
77
- flat_windows = []
78
77
 
79
78
  for raw_text in raw_docs:
80
79
  if not raw_text or not isinstance(raw_text, str):
@@ -86,36 +85,32 @@ class ParallelPreprocessingPipeline:
86
85
  doc_sentence_counts.append(len(sents))
87
86
  flat_sentences.extend(sents)
88
87
 
89
- for i in range(n_sents):
90
- window = " ".join(sents[max(0, i - 1) : min(n_sents, i + 2)])
91
- flat_windows.append(window)
92
-
93
88
  return {
94
89
  "doc_sentence_counts": doc_sentence_counts,
95
90
  "flat_sentences": flat_sentences,
96
- "flat_windows": flat_windows,
97
91
  "raw_docs_count": len(raw_docs)
98
92
  }
99
93
 
100
94
  def _gpu_execution_pass(self, prep_data: dict, threshold: float) -> list[str]:
101
95
  flat_sentences = prep_data["flat_sentences"]
102
- flat_windows = prep_data["flat_windows"]
103
96
  doc_sentence_counts = prep_data["doc_sentence_counts"]
104
97
  raw_docs_count = prep_data["raw_docs_count"]
105
98
 
106
99
  if not flat_sentences:
107
100
  return [""] * raw_docs_count
108
101
 
109
- doc_candidates = self.router.route_sentences(
102
+ flat_candidates, flat_cand_embs, doc_cand_counts = self.router.route_sentences(
110
103
  sentences=flat_sentences,
111
- windows=flat_windows,
112
104
  doc_sentence_counts=doc_sentence_counts,
113
105
  batch_size=self.inference_batch_size,
114
106
  )
115
107
 
116
108
  filtered_doc_sentences = self.ranker.rank_and_filter(
117
- sentences=doc_candidates,
118
- batch_size=self.inference_batch_size
109
+ flat_candidates=flat_candidates,
110
+ flat_cand_embs=flat_cand_embs,
111
+ doc_cand_counts=doc_cand_counts,
112
+ cross_prob_threshold=threshold,
113
+ batch_size=self.inference_batch_size,
119
114
  )
120
115
 
121
116
  return [sanitize_outputs(sents, self.profile) for sents in filtered_doc_sentences]
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass, field, asdict
2
2
  import json
3
3
  from pathlib import Path
4
+ import re
4
5
 
5
6
  @dataclass
6
7
  class DomainProfile:
@@ -20,12 +21,24 @@ class DomainProfile:
20
21
  metadata_line_regex: str | None = None
21
22
  junk_line_regex: str | None = None
22
23
 
24
+ def __post_init__(self):
25
+ self._raw_noise_re = re.compile(self.raw_noise_regex, re.IGNORECASE) if self.raw_noise_regex else None
26
+ self._meta_line_re = re.compile(self.metadata_line_regex, re.IGNORECASE) if self.metadata_line_regex else None
27
+ self._junk_line_re = re.compile(self.junk_line_regex, re.IGNORECASE) if self.junk_line_regex else None
28
+ self._action_kw_re = re.compile(self.action_keywords_regex, re.IGNORECASE) if self.action_keywords_regex else None
29
+ self._syntax_start_re = re.compile(self.syntax_start_pattern, re.IGNORECASE) if self.syntax_start_pattern else None
30
+ if self.global_stop_phrases:
31
+ pattern = "|".join(map(re.escape, self.global_stop_phrases))
32
+ self._stop_phrases_re = re.compile(rf"(?:{pattern})", re.IGNORECASE)
33
+ else:
34
+ self._stop_phrases_re = None
35
+
23
36
  def validate(self):
24
37
  if not self.positive_semantic_anchors:
25
38
  raise ValueError("DomainProfile must define at least one positive_semantic_anchor.")
26
- if not self.name.isalnum():
27
- raise ValueError("Profile name must be alphanumeric (no spaces or special characters).")
28
- if not self.section_anchors:
39
+ if not self.name.replace(" ", "").replace("_", "").isalnum():
40
+ raise ValueError("Profile name must be alphanumeric.")
41
+ if not self.anchors:
29
42
  raise ValueError("DomainProfile must define at least one section anchor.")
30
43
  if self.default_section not in self.anchors:
31
44
  raise ValueError(f"Default section '{self.default_section}' must be one of the defined section anchors.")
@@ -75,12 +88,12 @@ JOB_POSTING_PROFILE = DomainProfile(
75
88
  default_section="CORE",
76
89
  target_sections=['CORE', 'REQUIREMENTS'],
77
90
  positive_semantic_anchors=[
78
- "execute daily responsibilities, manage operational workflows, deliver projects, and accomplish work assignments",
79
- "design, build, develop, and maintain technical systems, codebases, software, or infrastructure",
80
- "analyze data, conduct research, prepare comprehensive reports, formulate budgets, and monitor key metrics",
81
- "collaborate with cross-functional teams, communicate with stakeholders, and partner across departments",
82
- "ensure regulatory compliance, perform audits or inspections, manage inventory, and deliver service solutions",
83
- "lead teams, assign tasks to staff, direct project timelines, and manage program resources"
91
+ "What are the core daily duties, responsibilities, and operational tasks performed in this role?",
92
+ "Responsible for designing, building, developing, and maintaining technical systems, software, or equipment.",
93
+ "Performs data analysis, conducts research, prepares reports, formulates budgets, and monitors program metrics.",
94
+ "Collaborates with cross-functional teams, advises stakeholders, and coordinates activities across departments.",
95
+ "Ensures regulatory compliance, performs inspections or clinical care, manages inventory, and delivers services.",
96
+ "Supervises staff, assigns work to subordinates, leads projects, and manages organizational resources."
84
97
  ],
85
98
  negative_semantic_anchors=[
86
99
  "company history, corporate mission statement, organizational culture, workplace values, company overview",
@@ -58,79 +58,69 @@ class HybridRanker:
58
58
 
59
59
  def rank_and_filter(
60
60
  self,
61
- sentences: list[list[str]],
62
- high_certainty_threshold: float = 0.60,
61
+ flat_candidates: list[str],
62
+ flat_cand_embs: torch.Tensor,
63
+ doc_cand_counts: list[int],
64
+ high_certainty_threshold: float = 0.40,
63
65
  low_certainty_threshold: float = 0.20,
66
+ cross_prob_threshold: float = 0.30,
67
+ min_fallback_sentences: int = 3,
64
68
  batch_size: int = 512
65
69
  ) -> list[list[str]]:
66
- flat_candidates = []
67
- doc_cand_counts = []
68
- for c_list in sentences:
69
- doc_cand_counts.append(len(c_list))
70
- flat_candidates.extend(c_list)
71
-
72
70
  if not flat_candidates:
73
- return [[] for _ in sentences]
74
-
75
- unique_candidates = list(dict.fromkeys(flat_candidates))
76
- norm_unique = self._encode_sorted(unique_candidates, batch_size=batch_size)
71
+ return [[] for _ in doc_cand_counts]
72
+
73
+ action_re = self.profile._action_kw_re
74
+ if action_re:
75
+ lexical_boost = torch.tensor(
76
+ [0.06 if action_re.search(s) else 0.0 for s in flat_candidates],
77
+ dtype=flat_cand_embs.dtype,
78
+ device=flat_cand_embs.device
79
+ )
80
+ else:
81
+ lexical_boost = 0.0
77
82
 
78
83
  with torch.inference_mode():
79
- emb_map = {s: norm_unique[i] for i, s in enumerate(unique_candidates)}
80
- flat_cand_embs = torch.stack([emb_map[s] for s in flat_candidates])
81
-
82
84
  pos_sim = torch.matmul(flat_cand_embs, self.pos_anchors.T)
83
85
  max_pos, _ = torch.max(pos_sim, dim=1)
84
86
 
85
87
  if self.neg_anchors is not None:
86
88
  neg_sim = torch.matmul(flat_cand_embs, self.neg_anchors.T)
87
89
  max_neg, _ = torch.max(neg_sim, dim=1)
88
- net_scores = max_pos - (0.5 * max_neg)
90
+ # Only penalize when negative similarity is genuinely high (> 0.30)
91
+ neg_penalty = torch.clamp(max_neg - 0.30, min=0.0) * 0.6
92
+ net_scores = max_pos - neg_penalty + lexical_boost
89
93
  else:
90
- net_scores = max_pos
94
+ net_scores = max_pos + lexical_boost
91
95
 
92
- score_min, score_max = net_scores.min().item(), net_scores.max().item()
93
- if (score_max - score_min) > 1e-6:
94
- norm_scores = (net_scores - score_min) / (score_max - score_min)
95
- else:
96
- norm_scores = net_scores
97
-
98
- auto_mask = (norm_scores >= high_certainty_threshold).cpu().tolist()
99
- cross_mask = (
100
- (norm_scores >= low_certainty_threshold)
101
- & (norm_scores < high_certainty_threshold)
102
- ).cpu().tolist()
103
- needs_cross = list(
104
- dict.fromkeys(
105
- [s for s, flag in zip(flat_candidates, cross_mask) if flag]
106
- )
107
- )
96
+ auto_mask = (net_scores >= high_certainty_threshold).cpu().tolist()
97
+ cross_mask = ((net_scores >= low_certainty_threshold) & (net_scores < high_certainty_threshold)).cpu().tolist()
98
+ net_scores_cpu = net_scores.cpu().tolist()
108
99
 
100
+ needs_cross = list(dict.fromkeys([s for s, flag in zip(flat_candidates, cross_mask) if flag]))
109
101
  cross_keep_set = set()
110
102
  if needs_cross:
103
+ needs_cross_sorted = sorted(needs_cross, key=len)
111
104
  primary_anchor = self.profile.positive_semantic_anchors[0]
112
- cross_inputs = [[primary_anchor, sent] for sent in needs_cross]
105
+ cross_inputs = [[primary_anchor, sent] for sent in needs_cross_sorted]
113
106
 
114
107
  cross_scores = self.cross_encoder.predict(
115
108
  cross_inputs,
116
109
  batch_size=batch_size,
117
110
  show_progress_bar=False,
118
- convert_to_tensor=True,
111
+ convert_to_tensor=True
119
112
  )
120
113
 
121
114
  with torch.inference_mode():
122
- survived_flags = (cross_scores >= 0.0).cpu().tolist()
115
+ cross_probs = torch.sigmoid(cross_scores)
116
+ survived_flags = (cross_probs >= cross_prob_threshold).cpu().tolist()
123
117
 
124
- cross_keep_set = set(
125
- [sent for sent, keep in zip(needs_cross, survived_flags) if keep]
126
- )
118
+ cross_keep_set = {sent for sent, keep in zip(needs_cross, survived_flags) if keep}
127
119
 
128
- surviving_mask = [False] * len(flat_candidates)
129
- for idx, is_auto in enumerate(auto_mask):
130
- if is_auto:
131
- surviving_mask[idx] = True
132
- elif cross_mask[idx] and (flat_candidates[idx] in cross_keep_set):
133
- surviving_mask[idx] = True
120
+ surviving_mask = [
121
+ is_auto or (is_cross and (flat_candidates[idx] in cross_keep_set))
122
+ for idx, (is_auto, is_cross) in enumerate(zip(auto_mask, cross_mask))
123
+ ]
134
124
 
135
125
  filtered_docs = []
136
126
  c_offset = 0
@@ -141,7 +131,21 @@ class HybridRanker:
141
131
 
142
132
  doc_pool = flat_candidates[c_offset : c_offset + c_count]
143
133
  doc_mask = surviving_mask[c_offset : c_offset + c_count]
144
- filtered_docs.append([sent for sent, keep in zip(doc_pool, doc_mask) if keep])
134
+ kept = [sent for sent, keep in zip(doc_pool, doc_mask) if keep]
135
+
136
+ if not kept and min_fallback_sentences > 0:
137
+ doc_scores = net_scores_cpu[c_offset : c_offset + c_count]
138
+ # Rank indices by score descending, keep top K that have at least > 0.10 score
139
+ ranked_local_idxs = sorted(
140
+ range(c_count), key=lambda i: doc_scores[i], reverse=True
141
+ )
142
+ rescued_idxs = sorted([
143
+ i for i in ranked_local_idxs[:min_fallback_sentences]
144
+ if doc_scores[i] > 0.10
145
+ ])
146
+ kept = [doc_pool[i] for i in rescued_idxs]
147
+
148
+ filtered_docs.append(kept)
145
149
  c_offset += c_count
146
150
 
147
151
  return filtered_docs
@@ -45,8 +45,7 @@ class SemanticSectionRouter:
45
45
 
46
46
  def route_sentences(
47
47
  self,
48
- sentences: list[str],
49
- windows: list[str],
48
+ sentences: list[str],
50
49
  doc_sentence_counts: list[int],
51
50
  min_confidence: float = 0.20,
52
51
  batch_size: int = 512
@@ -54,39 +53,46 @@ class SemanticSectionRouter:
54
53
  if not sentences:
55
54
  return [[] for _ in doc_sentence_counts]
56
55
 
57
- window_embs = self._encode_sorted(windows, batch_size=batch_size)
56
+ unique_sents = list(dict.fromkeys(sentences))
57
+ norm_unique = self._encode_sorted(unique_sents, batch_size=batch_size)
58
58
 
59
59
  with torch.inference_mode():
60
+ emb_map = {s: norm_unique[i] for i, s in enumerate(unique_sents)}
61
+ sent_embs = torch.stack([emb_map[s] for s in sentences])
62
+
63
+ # 2. Compute 3-sentence context windows in VRAM via 1D avg pooling (0ms model cost!)
64
+ padded = torch.nn.functional.pad(sent_embs.unsqueeze(0).transpose(1, 2), (1, 1), mode="replicate")
65
+ window_embs = torch.nn.functional.avg_pool1d(padded, kernel_size=3, stride=1).transpose(1, 2).squeeze(0)
66
+ window_embs = torch.nn.functional.normalize(window_embs, p=2, dim=1)
67
+
60
68
  sim_sections = torch.matmul(window_embs, self.anchor_embeddings.T)
61
69
  best_scores, best_indices = torch.max(sim_sections, dim=1)
62
70
 
63
71
  confident_mask = best_scores >= min_confidence
64
72
  fallback_tensor = torch.full_like(best_indices, self.default_idx)
65
- routed_indices = torch.where(
66
- confident_mask, best_indices, fallback_tensor
67
- ).cpu().tolist()
73
+ routed_indices = torch.where(confident_mask, best_indices, fallback_tensor).cpu().tolist()
68
74
 
69
- target_section_set = set(self.profile.target_sections)
70
- doc_candidates = []
75
+ target_idx_set = {self.section_keys.index(k) for k in self.profile.target_sections if k in self.section_keys}
76
+ flat_candidates = []
77
+ keep_emb_indices = []
78
+ doc_cand_counts = []
71
79
  curr_offset = 0
72
80
 
73
81
  for count in doc_sentence_counts:
74
82
  if count == 0:
75
- doc_candidates.append([])
83
+ doc_cand_counts.append(0)
76
84
  continue
77
85
 
78
- doc_sents = sentences[curr_offset : curr_offset + count]
79
- doc_secs = [
80
- self.section_keys[idx]
81
- for idx in routed_indices[curr_offset : curr_offset + count]
86
+ doc_indices = [
87
+ curr_offset + i for i in range(count)
88
+ if routed_indices[curr_offset + i] in target_idx_set
82
89
  ]
90
+ if len(doc_indices) < min(3, count):
91
+ doc_indices = list(range(curr_offset, curr_offset + count))
83
92
 
84
- targets = [s for s, sec in zip(doc_sents, doc_secs) if sec in target_section_set]
85
-
86
- if not targets:
87
- targets = doc_sents
88
-
89
- doc_candidates.append(targets)
93
+ doc_cand_counts.append(len(doc_indices))
94
+ keep_emb_indices.extend(doc_indices)
95
+ flat_candidates.extend([sentences[i] for i in doc_indices])
90
96
  curr_offset += count
91
97
 
92
- return doc_candidates
98
+ return flat_candidates, sent_embs[keep_emb_indices], doc_cand_counts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstpr
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A high-throughput, domain-specific text preprocessing cascading pipeline filter to rank core sentences in texts and filter away boilerplate.
5
5
  Author-email: Stephen Meisenbacher <sjmeis@gtgd.com>
6
6
  Maintainer-email: Stephen Meisenbacher <sjmeis@gtgd.com>
@@ -76,7 +76,7 @@ This is the recommended approach for large-scale data pipelines.
76
76
  ```python
77
77
  from dstpr import ParallelPreprocessingPipeline, JOB_POSTING_PROFILE
78
78
 
79
- pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE, batch_size=64)
79
+ pipeline = ParallelPreprocessingPipeline(profile=JOB_POSTING_PROFILE)
80
80
 
81
81
  raw_documents = [
82
82
  "DOC 1",
@@ -84,41 +84,13 @@ raw_documents = [
84
84
  "..."
85
85
  ]
86
86
 
87
- cleaned_documents = pipeline.process(raw_documents, num_workers=8, threshold=0.25)
88
- ```
89
-
90
- ### Advanced Usage
91
-
92
- If you want to integrate specific pipeline layers directly into an existing workflow, or tweak the internal parameters, you can import individual modules manually:
93
-
94
- ```python
95
- from dstpr.cleaners import clean_and_split_chunks
96
- from dstpr.segmenters import SemanticSectionRouter
97
- from dstpr.rankers import HybridRanker
98
- from sentence_transformers import SentenceTransformer
99
-
100
- embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
101
-
102
- text = "Job posting text goes here!"
103
-
104
- # Clean and segment sentences
105
- sentences = clean_and_split_chunks(text)
106
-
107
- # Route by context
108
- router = SemanticSectionRouter(model=embedding_model)
109
- buckets = router.route_sentences(sentences)
110
- target_sentences = buckets['CORE'] + buckets['REQUIREMENTS']
111
-
112
- # Grade and sort text features
113
- ranker = HybridRanker()
114
- final = ranker.rank_and_filter(target_sentences, bi_cutoff_pct=0.25, final_threshold=0.40)
87
+ cleaned_documents = pipeline.process(raw_documents, threshold=0.25)
115
88
  ```
116
89
 
117
90
  ### Parameter Tuning
118
91
 
119
92
  To adjust the trade-off between strict filtering and execution speed, consider tweaking these variables in `ParallelPreprocessingPipeline`:
120
93
  - `threshold` (Default: `0.4`): Controls how aggressively sentences are discarded. Raising this value toward `0.6`, for example, ensures only stronger matching sentences are passed through. Lowering it toward `0.3`, on the other hand, acts as a wider net.
121
- - `batch_size` (Default: `256`): This depends on your hardware! Adjust for best performance.
122
94
 
123
95
  ## Creating Custom Domain Profiles (via the provided wizard)
124
96
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dstpr"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "A high-throughput, domain-specific text preprocessing cascading pipeline filter to rank core sentences in texts and filter away boilerplate."
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  license = {file = "LICENSE"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes