Pointf5ive commited on
Commit
a86b430
verified
1 Parent(s): 2d7922b

Improve Smoke Signal noise-pattern learning and matching

Browse files
Files changed (1) hide show
  1. smoke_signal_tab.py +91 -21
smoke_signal_tab.py CHANGED
@@ -58,6 +58,18 @@ for d in [SOURCE_DIR, MANIFEST_CSV.parent, PROFILES_DIR, OCR_RAW_DIR,
58
 
59
  GOLD_FILE = GOLD_DIR / "gold_corrections.jsonl"
60
  NOISE_DIR = SS_ROOT / "calibration" / "noise_patterns"
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
 
63
  def _noise_path(book_id: str) -> Path:
@@ -65,38 +77,90 @@ def _noise_path(book_id: str) -> Path:
65
  return NOISE_DIR / f"{book_id}_noise.json"
66
 
67
 
68
- def _load_noise_patterns(book_id: str) -> list:
69
- path = _noise_path(book_id)
70
  if not path.exists():
71
  return []
72
  try:
73
- return json.loads(path.read_text())
74
  except Exception:
75
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
 
78
- def _save_noise_pattern(book_id: str, pattern: str) -> None:
79
- pattern = pattern.strip()
80
- if not pattern or len(pattern) < 3:
81
- return
82
- patterns = _load_noise_patterns(book_id)
83
- if pattern not in patterns:
84
- patterns.append(pattern)
85
- _noise_path(book_id).write_text(json.dumps(patterns, indent=2))
 
 
 
 
 
 
 
 
 
 
 
86
 
87
 
88
  def _text_matches_noise(text: str, patterns: list) -> bool:
89
  if not patterns or not text:
90
  return False
91
- text_tokens = set(text.lower().split())
 
 
 
92
  if not text_tokens:
93
  return False
94
  for pattern in patterns:
95
- pat_tokens = set(pattern.lower().split())
 
 
 
 
 
 
96
  if not pat_tokens:
97
  continue
98
- overlap = len(text_tokens & pat_tokens) / len(text_tokens)
99
- if overlap >= 0.6:
 
 
 
 
 
 
 
100
  return True
101
  return False
102
  DECISIONS_CSV = REVIEW_DIR / "review_decisions.csv"
@@ -2705,18 +2769,21 @@ def save_review_decision(idx: int, final_text: str, action: str, noise_text_inpu
2705
  }) + "\n")
2706
 
2707
  learned_pairs = 0
 
 
2708
  if action == "rejected" and str(reason).strip() == "NON_STORY_TEXT":
2709
  # Use pasted noise text if provided, otherwise fall back to raw OCR
2710
  noise_source = str(noise_text_input or "").strip() or final_text.strip() or raw_text.strip()
2711
  book_id_for_noise = str(item.get("book_id", ""))
2712
- saved_count = 0
2713
  for line in noise_source.splitlines():
2714
  line = line.strip()
2715
  if line:
2716
- _save_noise_pattern(book_id_for_noise, line)
2717
- saved_count += 1
2718
- if saved_count == 0 and noise_source:
2719
- _save_noise_pattern(book_id_for_noise, noise_source)
 
 
2720
  if action == "edited":
2721
  learned_pairs = _record_punctuation_correction(
2722
  raw_text_original,
@@ -2732,9 +2799,12 @@ def save_review_decision(idx: int, final_text: str, action: str, noise_text_inpu
2732
 
2733
  punct_pairs = int(_punctuation_map_summary().get("total_pairs", 0))
2734
  learned_note = f" 路 +{learned_pairs} punct learns" if learned_pairs else ""
 
 
 
2735
  feedback = (f"<div class='ss-training-badge'><div class='ss-pulse'></div>"
2736
  f"Gold set: {gold_count} examples 路 {corrections} corrections 路 punct map: {punct_pairs} pairs"
2737
- f"{learned_note} 路 auto-accept threshold: {default['auto_accept']:.0%}</div>")
2738
 
2739
  return feedback, *get_review_item(0)
2740
 
 
58
 
59
  GOLD_FILE = GOLD_DIR / "gold_corrections.jsonl"
60
  NOISE_DIR = SS_ROOT / "calibration" / "noise_patterns"
61
+ NOISE_GLOBAL_FILE = NOISE_DIR / "_global_noise.json"
62
+ NOISE_MIN_CHARS = int(os.environ.get("SS_NOISE_MIN_CHARS", "3"))
63
+ NOISE_MATCH_MIN_PATTERN_COVERAGE = float(os.environ.get("SS_NOISE_MATCH_MIN_PATTERN_COVERAGE", "0.65"))
64
+ NOISE_MATCH_MIN_TEXT_COVERAGE = float(os.environ.get("SS_NOISE_MATCH_MIN_TEXT_COVERAGE", "0.08"))
65
+
66
+
67
+ def _normalize_noise_text(text: str) -> str:
68
+ txt = (text or "").lower()
69
+ txt = re.sub(r"[\r\n\t]+", " ", txt)
70
+ txt = re.sub(r"[^a-z0-9\u4e00-\u9fff\s]+", " ", txt)
71
+ txt = re.sub(r"\s+", " ", txt).strip()
72
+ return txt
73
 
74
 
75
  def _noise_path(book_id: str) -> Path:
 
77
  return NOISE_DIR / f"{book_id}_noise.json"
78
 
79
 
80
+ def _load_noise_patterns_for_scope(path: Path) -> list[str]:
 
81
  if not path.exists():
82
  return []
83
  try:
84
+ data = json.loads(path.read_text())
85
  except Exception:
86
  return []
87
+ if not isinstance(data, list):
88
+ return []
89
+ out: list[str] = []
90
+ for p in data:
91
+ raw = str(p or "").strip()
92
+ norm = _normalize_noise_text(raw)
93
+ if len(norm) < NOISE_MIN_CHARS:
94
+ continue
95
+ out.append(raw)
96
+ return out
97
+
98
+
99
+ def _load_noise_patterns(book_id: str, include_global: bool = True) -> list[str]:
100
+ combined: list[str] = []
101
+ seen: set[str] = set()
102
+ paths = [_noise_path(book_id)]
103
+ if include_global:
104
+ paths.append(NOISE_GLOBAL_FILE)
105
+ for path in paths:
106
+ for pattern in _load_noise_patterns_for_scope(path):
107
+ norm = _normalize_noise_text(pattern)
108
+ if norm in seen:
109
+ continue
110
+ seen.add(norm)
111
+ combined.append(pattern)
112
+ return combined
113
 
114
 
115
+ def _save_noise_pattern(book_id: str, pattern: str, save_global: bool = True) -> bool:
116
+ pattern = str(pattern or "").strip()
117
+ norm = _normalize_noise_text(pattern)
118
+ if len(norm) < NOISE_MIN_CHARS:
119
+ return False
120
+ changed = False
121
+ targets = [_noise_path(book_id)]
122
+ if save_global:
123
+ targets.append(NOISE_GLOBAL_FILE)
124
+ for target in targets:
125
+ existing = _load_noise_patterns_for_scope(target)
126
+ existing_norm = {_normalize_noise_text(p) for p in existing}
127
+ if norm in existing_norm:
128
+ continue
129
+ existing.append(pattern)
130
+ target.parent.mkdir(parents=True, exist_ok=True)
131
+ target.write_text(json.dumps(existing, indent=2, ensure_ascii=False))
132
+ changed = True
133
+ return changed
134
 
135
 
136
  def _text_matches_noise(text: str, patterns: list) -> bool:
137
  if not patterns or not text:
138
  return False
139
+ normalized_text = _normalize_noise_text(text)
140
+ if not normalized_text:
141
+ return False
142
+ text_tokens = set(normalized_text.split())
143
  if not text_tokens:
144
  return False
145
  for pattern in patterns:
146
+ pat_norm = _normalize_noise_text(str(pattern or ""))
147
+ if not pat_norm:
148
+ continue
149
+ # Best signal for recurring watermarks/headers
150
+ if pat_norm in normalized_text:
151
+ return True
152
+ pat_tokens = set(pat_norm.split())
153
  if not pat_tokens:
154
  continue
155
+ overlap_count = len(text_tokens & pat_tokens)
156
+ if overlap_count == 0:
157
+ continue
158
+ pattern_coverage = overlap_count / len(pat_tokens)
159
+ text_coverage = overlap_count / len(text_tokens)
160
+ if (
161
+ pattern_coverage >= NOISE_MATCH_MIN_PATTERN_COVERAGE
162
+ and text_coverage >= NOISE_MATCH_MIN_TEXT_COVERAGE
163
+ ):
164
  return True
165
  return False
166
  DECISIONS_CSV = REVIEW_DIR / "review_decisions.csv"
 
2769
  }) + "\n")
2770
 
2771
  learned_pairs = 0
2772
+ learned_noise_added = 0
2773
+ active_noise_patterns = 0
2774
  if action == "rejected" and str(reason).strip() == "NON_STORY_TEXT":
2775
  # Use pasted noise text if provided, otherwise fall back to raw OCR
2776
  noise_source = str(noise_text_input or "").strip() or final_text.strip() or raw_text.strip()
2777
  book_id_for_noise = str(item.get("book_id", ""))
 
2778
  for line in noise_source.splitlines():
2779
  line = line.strip()
2780
  if line:
2781
+ if _save_noise_pattern(book_id_for_noise, line):
2782
+ learned_noise_added += 1
2783
+ if learned_noise_added == 0 and noise_source:
2784
+ if _save_noise_pattern(book_id_for_noise, noise_source):
2785
+ learned_noise_added += 1
2786
+ active_noise_patterns = len(_load_noise_patterns(book_id_for_noise))
2787
  if action == "edited":
2788
  learned_pairs = _record_punctuation_correction(
2789
  raw_text_original,
 
2799
 
2800
  punct_pairs = int(_punctuation_map_summary().get("total_pairs", 0))
2801
  learned_note = f" 路 +{learned_pairs} punct learns" if learned_pairs else ""
2802
+ noise_note = ""
2803
+ if action == "rejected" and str(reason).strip() == "NON_STORY_TEXT":
2804
+ noise_note = f" 路 +{learned_noise_added} noise learns 路 active noise patterns: {active_noise_patterns}"
2805
  feedback = (f"<div class='ss-training-badge'><div class='ss-pulse'></div>"
2806
  f"Gold set: {gold_count} examples 路 {corrections} corrections 路 punct map: {punct_pairs} pairs"
2807
+ f"{learned_note}{noise_note} 路 auto-accept threshold: {default['auto_accept']:.0%}</div>")
2808
 
2809
  return feedback, *get_review_item(0)
2810