mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 08:40:29 +00:00
Sort word lists by Google Books frequency (most common first). Add vocabulary dropdown (Common/Standard/Extended/Full) to word mode UI that controls how many words are searched. Co-Authored-By: Claude Opus 4.6 <[email protected]>
212 lines
7.0 KiB
Python
212 lines
7.0 KiB
Python
"""Build word list files (nouns, plurals, adjectives) from WordNet via NLTK.
|
|
|
|
Usage:
|
|
python scripts/build_word_lists.py [--data-dir PATH]
|
|
|
|
Extracts nouns and adjectives from WordNet, generates plural forms using
|
|
irregular mappings from Wiktionary/GitHub and standard English rules, then
|
|
sorts all lists by word frequency (Google Books corpus) and writes three
|
|
files to unredact/data/:
|
|
- nouns.txt (singular nouns, sorted by frequency)
|
|
- nouns_plural.txt (corresponding plural for each noun, same order)
|
|
- adjectives.txt (adjectives, sorted by frequency)
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import io
|
|
import re
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
import nltk
|
|
|
|
OUTPUT_DIR = Path(__file__).parent.parent / "unredact" / "data"
|
|
|
|
IRREGULAR_PLURALS_URL = (
|
|
"https://raw.githubusercontent.com/djstrong/nouns-with-plurals/master/noun.csv"
|
|
)
|
|
FREQUENCY_URL = (
|
|
"https://raw.githubusercontent.com/hackerb9/gwordlist/master/frequency-alpha-alldicts.txt"
|
|
)
|
|
|
|
# Regex: only lowercase alpha, no digits/hyphens/underscores/spaces
|
|
VALID_WORD = re.compile(r"^[a-z]+$")
|
|
|
|
|
|
def extract_wordnet_words(pos_tags: list[str]) -> list[str]:
|
|
"""Extract unique lowercase words from WordNet for given POS tags.
|
|
|
|
Only words matching [a-z]+ are included.
|
|
"""
|
|
from nltk.corpus import wordnet as wn
|
|
|
|
words: set[str] = set()
|
|
for pos in pos_tags:
|
|
for lemma in wn.all_lemma_names(pos=pos):
|
|
word = lemma.lower()
|
|
if VALID_WORD.match(word):
|
|
words.add(word)
|
|
return sorted(words)
|
|
|
|
|
|
def download_irregular_plurals() -> dict[str, str]:
|
|
"""Download irregular plural mappings from GitHub.
|
|
|
|
Returns: {singular: plural} dict, filtered to [a-z]+ words only.
|
|
"""
|
|
print(f"Downloading irregular plurals from {IRREGULAR_PLURALS_URL}...")
|
|
try:
|
|
response = urllib.request.urlopen(IRREGULAR_PLURALS_URL)
|
|
except (urllib.error.URLError, OSError) as exc:
|
|
print(f" WARNING: failed to download irregular plurals: {exc}")
|
|
print(" Continuing with rule-based pluralization only.")
|
|
return {}
|
|
text = response.read().decode("utf-8")
|
|
|
|
mapping: dict[str, str] = {}
|
|
reader = csv.reader(io.StringIO(text))
|
|
for row in reader:
|
|
if len(row) < 2:
|
|
continue
|
|
singular = row[0].strip().lower()
|
|
plural = row[1].strip().lower()
|
|
if VALID_WORD.match(singular) and VALID_WORD.match(plural):
|
|
mapping[singular] = plural
|
|
|
|
print(f" -> loaded {len(mapping)} irregular plural mappings")
|
|
return mapping
|
|
|
|
|
|
def pluralize(word: str) -> str:
|
|
"""Apply standard English pluralization rules to a word."""
|
|
if word.endswith("fe"):
|
|
return word[:-2] + "ves"
|
|
if word.endswith("f"):
|
|
return word[:-1] + "ves"
|
|
if word.endswith(("s", "x", "z")) or word.endswith(("sh", "ch")):
|
|
return word + "es"
|
|
if word.endswith("y") and len(word) >= 2 and word[-2] not in "aeiou":
|
|
return word[:-1] + "ies"
|
|
return word + "s"
|
|
|
|
|
|
def generate_plurals(nouns: list[str], irregulars: dict[str, str]) -> list[str]:
|
|
"""Generate plural form for each noun.
|
|
|
|
Uses irregular mapping if available, otherwise applies rules.
|
|
"""
|
|
plurals: list[str] = []
|
|
for noun in nouns:
|
|
if noun in irregulars:
|
|
plurals.append(irregulars[noun])
|
|
else:
|
|
plurals.append(pluralize(noun))
|
|
return plurals
|
|
|
|
|
|
def download_frequency_ranks() -> dict[str, int]:
|
|
"""Download word frequency rankings from Google Books corpus.
|
|
|
|
Returns: {word: rank} dict where rank 1 = most common.
|
|
"""
|
|
print(f"Downloading frequency data from hackerb9/gwordlist...")
|
|
try:
|
|
response = urllib.request.urlopen(FREQUENCY_URL)
|
|
except (urllib.error.URLError, OSError) as exc:
|
|
print(f" WARNING: failed to download frequency data: {exc}")
|
|
print(" Words will be sorted alphabetically instead.")
|
|
return {}
|
|
text = response.read().decode("utf-8")
|
|
|
|
ranks: dict[str, int] = {}
|
|
for line in text.splitlines():
|
|
if line.startswith("#") or not line.strip():
|
|
continue
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
rank = int(parts[0])
|
|
word = parts[1].lower()
|
|
if word not in ranks:
|
|
ranks[word] = rank
|
|
|
|
print(f" -> loaded {len(ranks)} word frequency rankings")
|
|
return ranks
|
|
|
|
|
|
def sort_by_frequency(words: list[str], freq_ranks: dict[str, int]) -> list[str]:
|
|
"""Sort words by frequency rank. Unranked words go to the end."""
|
|
max_rank = len(freq_ranks) + 1
|
|
return sorted(words, key=lambda w: freq_ranks.get(w, max_rank))
|
|
|
|
|
|
def write_word_list(path: Path, words: list[str]) -> None:
|
|
"""Write a word list, one word per line."""
|
|
path.write_text("\n".join(words) + "\n")
|
|
print(f"Wrote {len(words)} words to {path}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Build word list files from WordNet (nouns, adjectives, plurals)"
|
|
)
|
|
parser.add_argument(
|
|
"--data-dir",
|
|
type=Path,
|
|
default=OUTPUT_DIR,
|
|
help="Directory to write output files (default: unredact/data/)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
args.data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Ensure WordNet corpus is available
|
|
nltk.download("wordnet", quiet=True)
|
|
|
|
# Extract nouns (POS 'n')
|
|
print("Extracting nouns from WordNet...")
|
|
nouns = extract_wordnet_words(["n"])
|
|
print(f" -> {len(nouns)} nouns")
|
|
|
|
# Extract adjectives (POS 'a' for adjective, 's' for satellite adjective)
|
|
print("Extracting adjectives from WordNet...")
|
|
adjectives = extract_wordnet_words(["a", "s"])
|
|
print(f" -> {len(adjectives)} adjectives")
|
|
|
|
# Download irregular plural mappings
|
|
irregulars = download_irregular_plurals()
|
|
|
|
# Download frequency data
|
|
freq_ranks = download_frequency_ranks()
|
|
|
|
# Sort by frequency (most common first)
|
|
if freq_ranks:
|
|
print("Sorting by frequency...")
|
|
nouns_sorted = sort_by_frequency(nouns, freq_ranks)
|
|
adjectives_sorted = sort_by_frequency(adjectives, freq_ranks)
|
|
|
|
# Build noun->plural mapping, then reorder plurals to match sorted nouns
|
|
noun_to_plural = dict(zip(nouns, generate_plurals(nouns, irregulars)))
|
|
plurals_sorted = [noun_to_plural[n] for n in nouns_sorted]
|
|
|
|
ranked_nouns = sum(1 for n in nouns_sorted if n in freq_ranks)
|
|
ranked_adj = sum(1 for a in adjectives_sorted if a in freq_ranks)
|
|
print(f" -> {ranked_nouns}/{len(nouns_sorted)} nouns have frequency data")
|
|
print(f" -> {ranked_adj}/{len(adjectives_sorted)} adjectives have frequency data")
|
|
else:
|
|
nouns_sorted = nouns
|
|
adjectives_sorted = adjectives
|
|
plurals_sorted = generate_plurals(nouns, irregulars)
|
|
|
|
# Write output files
|
|
write_word_list(args.data_dir / "nouns.txt", nouns_sorted)
|
|
write_word_list(args.data_dir / "nouns_plural.txt", plurals_sorted)
|
|
write_word_list(args.data_dir / "adjectives.txt", adjectives_sorted)
|
|
|
|
print("Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|