Krixter commited on
Commit
762d972
ยท
verified ยท
1 Parent(s): 23dc6d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -234
app.py CHANGED
@@ -1,236 +1,82 @@
1
- """
2
- Mixed-Alphabet Word Scanner
3
- ----------------------------
4
- Scans a .txt file word-by-word and flags "mixup" words โ€” words that mix
5
- letters with digits and/or special characters (e.g. M0M, G@ng, f1ght).
6
-
7
- Rules:
8
- - Pure numbers (e.g. "12345") -> SKIPPED
9
- - Pure special characters (e.g. "***")-> SKIPPED
10
- - Normal words, even with a trailing
11
- comma/period (e.g. "hello.", "world,") -> SKIPPED
12
- - Pure alphabet words (e.g. "hello") -> SKIPPED
13
- - Words mixing letters with digits/symbols
14
- ANYWHERE in the word (e.g. M0M, G@ng, f1ght) -> FLAGGED
15
- - Duplicate flagged words are only listed once.
16
-
17
- Works two ways:
18
- 1. As a command-line script:
19
- python app.py your_file.txt
20
- 2. As a Gradio web app (this is what runs when deployed to a
21
- Hugging Face Space โ€” just upload this file + requirements.txt).
22
- """
23
-
24
- import re
25
- import sys
26
- import io
27
- import csv
28
-
29
-
30
- # Includes decorative symbols (hearts, stars) and their emoji/text
31
- # variation selectors, so e.g. "Peo-eok\u2665\ufe0e" is treated the same
32
- # as "Peo-eok" -- but "pe0\u2665\ufe0e" still gets flagged because of the "0".
33
- OUTER_PUNCT = (
34
- ".,!?;:\"'()[]{}<>*~`^\u201c\u201d\u2018\u2019\u2014\u2013\u2026"
35
- "\u2665\u2764\u2661\u2606\u2605\u2726\u2727\u2729\u272a"
36
- "\ufe0e\ufe0f"
37
- )
38
- ALPHA_APOSTROPHE_RE = re.compile(r"^[A-Za-z'\u2019]+$")
39
-
40
-
41
- def clean_outer(word: str) -> str:
42
- """Strip quote/sentence punctuation from both ends only."""
43
- return word.strip(OUTER_PUNCT)
44
-
45
-
46
- def _is_plain_ascii_alpha(c: str) -> bool:
47
- """True only for plain a-z/A-Z letters (excludes accented letters
48
- like รฉ, รบ, รฑ, รผ -- those are treated as 'suspicious' below)."""
49
- return c.isalpha() and c.isascii()
50
-
51
-
52
- def _segment_is_clean(segment: str) -> bool:
53
- """A hyphen-joined chunk is 'clean' if it's pure plain letters
54
- (+apostrophe) or pure digits or empty (e.g. '36', 'year', 'old')."""
55
- if segment == "":
56
- return True
57
- if segment.isdigit():
58
- return True
59
- if ALPHA_APOSTROPHE_RE.match(segment):
60
- return True
61
- return False
62
-
63
-
64
- def is_mixup_word(word: str):
65
- """
66
- Returns (is_mixup: bool, cleaned_word: str)
67
-
68
- Smarter rules:
69
- - Strip outer quote/sentence punctuation ("Conquer", once?, enjoyment:)
70
- - Pure digits, pure special chars, or empty -> skip
71
- - Pure alphabetic words, including contractions/possessives
72
- (can't, who's) -> skip
73
- - Hyphenated words where every hyphen-separated chunk is clean on
74
- its own (36-year-old, well-known) -> skip
75
- - Anything else that mixes letters with digits/symbols WITHIN the
76
- same chunk (M0M, G@ng, f1ght, y3ars-0ld) -> flagged as mixup
77
- """
78
- cleaned = clean_outer(word)
79
-
80
- if cleaned == "":
81
- return False, cleaned
82
-
83
- if cleaned.isdigit(): # pure number -> skip
84
- return False, cleaned
85
-
86
- if ALPHA_APOSTROPHE_RE.match(cleaned): # normal word/contraction -> skip
87
- return False, cleaned
88
-
89
- has_letter_overall = any(c.isalpha() for c in cleaned)
90
- if not has_letter_overall: # pure special chars -> skip
91
- return False, cleaned
92
-
93
- if "-" in cleaned:
94
- segments = cleaned.split("-")
95
- if all(_segment_is_clean(s) for s in segments):
96
- return False, cleaned # legit hyphenated word -> skip
97
- return True, cleaned # a chunk is disguised -> flag
98
-
99
- # No hyphen: flag if plain letters are mixed with digits/symbols/
100
- # accented look-alike letters (รฉ, รบ, รฑ, รผ...) anywhere in the word
101
- has_other = any(not _is_plain_ascii_alpha(c) for c in cleaned)
102
- if has_other:
103
- return True, cleaned
104
-
105
- return False, cleaned
106
-
107
-
108
- def scan_text(text: str):
109
- """
110
- Scans text, returns (results, stats)
111
- results: list of dicts [{"word": original_token, "cleaned": cleaned}]
112
- in order of first appearance, duplicates removed (case-sensitive).
113
- stats: dict with counts.
114
- """
115
- tokens = re.findall(r"\S+", text)
116
-
117
- seen = set()
118
- results = []
119
- total_flagged_occurrences = 0
120
-
121
- for token in tokens:
122
- flagged, cleaned = is_mixup_word(token)
123
- if flagged:
124
- total_flagged_occurrences += 1
125
- key = cleaned # dedupe on the cleaned form, case-sensitive
126
- if key not in seen:
127
- seen.add(key)
128
- results.append({"word": token, "cleaned": cleaned})
129
-
130
- stats = {
131
- "total_words_scanned": len(tokens),
132
- "total_flagged_occurrences": total_flagged_occurrences,
133
- "unique_flagged_words": len(results),
134
- }
135
- return results, stats
136
-
137
-
138
- def results_to_txt(results):
139
- return "\n".join(r["cleaned"] for r in results)
140
-
141
-
142
- def results_to_csv(results):
143
- buf = io.StringIO()
144
- writer = csv.writer(buf)
145
- writer.writerow(["word"])
146
- for r in results:
147
- writer.writerow([r["cleaned"]])
148
- return buf.getvalue()
149
-
150
-
151
- # --------------------------------------------------------------------------
152
- # CLI mode
153
- # --------------------------------------------------------------------------
154
- def run_cli(path: str):
155
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
156
  text = f.read()
157
 
158
- results, stats = scan_text(text)
159
-
160
- print(f"Scanned {stats['total_words_scanned']} words.")
161
- print(f"Found {stats['unique_flagged_words']} unique mixed-alphabet words "
162
- f"({stats['total_flagged_occurrences']} total occurrences):\n")
163
-
164
- for r in results:
165
- print(r["cleaned"])
166
-
167
- out_path = "flagged_words.txt"
168
- with open(out_path, "w", encoding="utf-8") as f:
169
- f.write(results_to_txt(results))
170
- print(f"\nSaved list to {out_path}")
171
-
172
-
173
- # --------------------------------------------------------------------------
174
- # Gradio web app (used on Hugging Face Spaces)
175
- # --------------------------------------------------------------------------
176
- def build_gradio_app():
177
- import gradio as gr
178
-
179
- def process(file_obj):
180
- if file_obj is None:
181
- return "Please upload a .txt file.", None, None
182
-
183
- # gr.File gives a filepath (str) in recent Gradio versions
184
- path = file_obj if isinstance(file_obj, str) else file_obj.name
185
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
186
- text = f.read()
187
-
188
- results, stats = scan_text(text)
189
-
190
- summary = (
191
- f"Scanned {stats['total_words_scanned']} words.\n"
192
- f"Unique flagged words: {stats['unique_flagged_words']}\n"
193
- f"Total flagged occurrences: {stats['total_flagged_occurrences']}"
194
- )
195
-
196
- list_display = "\n".join(r["cleaned"] for r in results) or "(none found)"
197
-
198
- # Write downloadable output file
199
- out_path = "/tmp/flagged_words.txt"
200
- with open(out_path, "w", encoding="utf-8") as f:
201
- f.write(results_to_txt(results))
202
-
203
- return summary, list_display, out_path
204
-
205
- with gr.Blocks(title="Mixed-Alphabet Word Scanner") as demo:
206
- gr.Markdown(
207
- "## Mixed-Alphabet Word Scanner\n"
208
- "Upload a `.txt` file. This tool flags words that mix letters "
209
- "with digits/symbols (e.g. `M0M`, `G@ng`, `f1ght`), while "
210
- "skipping pure numbers, pure symbols, normal words, and words "
211
- "with only a trailing comma/period."
212
- )
213
- file_input = gr.File(label="Upload .txt file", file_types=[".txt"])
214
- run_btn = gr.Button("Scan File", variant="primary")
215
- summary_out = gr.Textbox(label="Summary", interactive=False)
216
- list_out = gr.Textbox(label="Flagged Words", lines=15, interactive=False)
217
- download_out = gr.File(label="Download flagged_words.txt")
218
-
219
- run_btn.click(
220
- fn=process,
221
- inputs=file_input,
222
- outputs=[summary_out, list_out, download_out],
223
- )
224
-
225
- return demo
226
-
227
-
228
- if __name__ == "__main__":
229
- # If a filepath is passed as an argument, run in CLI mode.
230
- if len(sys.argv) > 1:
231
- run_cli(sys.argv[1])
232
- else:
233
- # Otherwise launch the Gradio web app (Hugging Face Spaces will
234
- # call this file directly with no args).
235
- app = build_gradio_app()
236
- app.launch()
 
1
+ import gradio as gr
2
+ import tempfile
3
+ import os
4
+
5
+ # ----------------------------
6
+ # Load replacement dictionary
7
+ # ----------------------------
8
+ def load_dictionary(dictionary_file):
9
+ replacements = []
10
+
11
+ with open(dictionary_file.name, "r", encoding="utf-8") as f:
12
+ for line in f:
13
+ line = line.strip()
14
+
15
+ if not line:
16
+ continue
17
+
18
+ if "->" not in line:
19
+ continue
20
+
21
+ wrong, correct = line.split("->", 1)
22
+
23
+ wrong = wrong.strip()
24
+ correct = correct.strip()
25
+
26
+ replacements.append((wrong, correct))
27
+
28
+ # Longest words first
29
+ replacements.sort(key=lambda x: len(x[0]), reverse=True)
30
+
31
+ return replacements
32
+
33
+
34
+ # ----------------------------
35
+ # Replace text
36
+ # ----------------------------
37
+ def correct_text(txt_file, dictionary_file):
38
+
39
+ replacements = load_dictionary(dictionary_file)
40
+
41
+ with open(txt_file.name, "r", encoding="utf-8") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  text = f.read()
43
 
44
+ total = 0
45
+
46
+ for wrong, correct in replacements:
47
+
48
+ count = text.count(wrong)
49
+
50
+ if count:
51
+ text = text.replace(wrong, correct)
52
+ total += count
53
+
54
+ output_path = os.path.join(
55
+ tempfile.gettempdir(),
56
+ "corrected_text.txt"
57
+ )
58
+
59
+ with open(output_path, "w", encoding="utf-8") as f:
60
+ f.write(text)
61
+
62
+ return output_path, f"Done!\n\nTotal replacements: {total}"
63
+
64
+
65
+ # ----------------------------
66
+ # Interface
67
+ # ----------------------------
68
+ demo = gr.Interface(
69
+ fn=correct_text,
70
+ inputs=[
71
+ gr.File(label="Upload TXT File"),
72
+ gr.File(label="Upload Correction Dictionary"),
73
+ ],
74
+ outputs=[
75
+ gr.File(label="Download Corrected TXT"),
76
+ gr.Textbox(label="Status"),
77
+ ],
78
+ title="Novel Word Corrector",
79
+ description="Upload a TXT file and a dictionary in the format 'wrong -> correct'."
80
+ )
81
+
82
+ demo.launch()