Spaces:
Sleeping
Sleeping
Improve Smoke Signal noise-pattern learning and matching
Browse files- 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
|
| 69 |
-
path = _noise_path(book_id)
|
| 70 |
if not path.exists():
|
| 71 |
return []
|
| 72 |
try:
|
| 73 |
-
|
| 74 |
except Exception:
|
| 75 |
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
-
def _save_noise_pattern(book_id: str, pattern: str) ->
|
| 79 |
-
pattern = pattern.strip()
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
def _text_matches_noise(text: str, patterns: list) -> bool:
|
| 89 |
if not patterns or not text:
|
| 90 |
return False
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
| 92 |
if not text_tokens:
|
| 93 |
return False
|
| 94 |
for pattern in patterns:
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
if not pat_tokens:
|
| 97 |
continue
|
| 98 |
-
|
| 99 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2718 |
-
if
|
| 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 |
|