Pointf5ive commited on
Commit
5740b5d
·
verified ·
1 Parent(s): a86b430

Harden Smoke Signal noise learning and JSON decision writes

Browse files
Files changed (1) hide show
  1. smoke_signal_tab.py +66 -17
smoke_signal_tab.py CHANGED
@@ -62,6 +62,7 @@ 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:
@@ -96,19 +97,38 @@ def _load_noise_patterns_for_scope(path: Path) -> list[str]:
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
 
@@ -121,18 +141,47 @@ def _save_noise_pattern(book_id: str, pattern: str, save_global: bool = True) ->
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
@@ -2766,7 +2815,7 @@ def save_review_decision(idx: int, final_text: str, action: str, noise_text_inpu
2766
  "conf_class": "verified-100" if conf_override else item.get("confidence_class",""),
2767
  "punct_score": punct_score,
2768
  "punct_flags_count": punct_flags_count,
2769
- }) + "\n")
2770
 
2771
  learned_pairs = 0
2772
  learned_noise_added = 0
 
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
+ _NOISE_IO_LOCK = threading.RLock()
66
 
67
 
68
  def _normalize_noise_text(text: str) -> str:
 
97
  return out
98
 
99
 
100
+ def _write_json_atomic(path: Path, payload) -> None:
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+ fd, tmp_name = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", dir=str(path.parent))
103
+ tmp_path = Path(tmp_name)
104
+ try:
105
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
106
+ json.dump(payload, fh, indent=2, ensure_ascii=False)
107
+ fh.flush()
108
+ os.fsync(fh.fileno())
109
+ os.replace(str(tmp_path), str(path))
110
+ finally:
111
+ if tmp_path.exists():
112
+ try:
113
+ tmp_path.unlink()
114
+ except Exception:
115
+ pass
116
+
117
+
118
  def _load_noise_patterns(book_id: str, include_global: bool = True) -> list[str]:
119
  combined: list[str] = []
120
  seen: set[str] = set()
121
  paths = [_noise_path(book_id)]
122
  if include_global:
123
  paths.append(NOISE_GLOBAL_FILE)
124
+ with _NOISE_IO_LOCK:
125
+ for path in paths:
126
+ for pattern in _load_noise_patterns_for_scope(path):
127
+ norm = _normalize_noise_text(pattern)
128
+ if norm in seen:
129
+ continue
130
+ seen.add(norm)
131
+ combined.append(pattern)
132
  return combined
133
 
134
 
 
141
  targets = [_noise_path(book_id)]
142
  if save_global:
143
  targets.append(NOISE_GLOBAL_FILE)
144
+ with _NOISE_IO_LOCK:
145
+ for target in targets:
146
+ existing = _load_noise_patterns_for_scope(target)
147
+ existing_norm = {_normalize_noise_text(p) for p in existing}
148
+ if norm in existing_norm:
149
+ continue
150
+ existing.append(pattern)
151
+ _write_json_atomic(target, existing)
152
+ changed = True
153
  return changed
154
 
155
 
156
+ def _json_default(obj):
157
+ # numpy / pandas scalar safety for json dumps
158
+ try:
159
+ import numpy as np # local import to avoid hard dependency at import-time
160
+ if isinstance(obj, np.generic):
161
+ return obj.item()
162
+ except Exception:
163
+ pass
164
+
165
+ if isinstance(obj, Path):
166
+ return str(obj)
167
+
168
+ # pandas Timestamp/NA etc.
169
+ try:
170
+ if isinstance(obj, pd.Timestamp):
171
+ return obj.isoformat()
172
+ except Exception:
173
+ pass
174
+
175
+ # Generic numeric coercion fallback
176
+ try:
177
+ if hasattr(obj, "item"):
178
+ return obj.item()
179
+ except Exception:
180
+ pass
181
+
182
+ return str(obj)
183
+
184
+
185
  def _text_matches_noise(text: str, patterns: list) -> bool:
186
  if not patterns or not text:
187
  return False
 
2815
  "conf_class": "verified-100" if conf_override else item.get("confidence_class",""),
2816
  "punct_score": punct_score,
2817
  "punct_flags_count": punct_flags_count,
2818
+ }, ensure_ascii=False, default=_json_default) + "\n")
2819
 
2820
  learned_pairs = 0
2821
  learned_noise_added = 0