| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- #!/usr/bin/env python3
- """Add synonyms (hints) to each target word in word-bank.json and word-bank-ru.json.
- Uses Datamuse API for English (free, no auth). Falls back to WordNet.
- Results are reviewed for quality — known-bad mappings are overridden.
- For Russian, uses a curated synonym dictionary.
- """
- import json
- import sys
- import time
- from pathlib import Path
- DATA_DIR = Path(__file__).parent.parent / "data"
- def get_english_synonyms_datamuse(word: str) -> list[str]:
- """Get related words from Datamuse API (free, no auth)."""
- import requests
- try:
- resp = requests.get(
- "https://api.datamuse.com/words",
- params={"ml": word, "max": 8},
- timeout=5,
- )
- resp.raise_for_status()
- results = resp.json()
- words = []
- for r in results:
- w = r.get("word", "")
- if " " not in w and w.lower() != word.lower():
- words.append(w.lower())
- return words
- except Exception as e:
- print(f" Datamuse error for '{word}': {e}", file=sys.stderr)
- return []
- def get_english_synonyms_wordnet(word: str) -> list[str]:
- """Get synonyms from WordNet."""
- from nltk.corpus import wordnet as wn
- syns = wn.synsets(word)
- if not syns:
- return []
- lemmas = []
- for s in syns[:10]:
- for l in s.lemmas():
- name = l.name().lower()
- if "_" not in name and name != word and name not in lemmas:
- lemmas.append(name)
- return lemmas
- def pick_best_synonym(word: str, candidates: list[str]) -> str | None:
- """Pick the best synonym from candidates. Prefers common, short words."""
- # Filter out multi-word, very long words, and obscure forms
- good = [c for c in candidates if len(c) <= 10 and "_" not in c]
- if not good:
- return None
- # Prefer shorter words (more likely to be common)
- good.sort(key=len)
- return good[0]
- # Manually curated overrides for words where automated approaches give poor results.
- # These are common, intuitive synonyms that help a player guess the target word.
- EN_HINT_OVERRIDES: dict[str, str] = {
- # Function words and words where Datamuse/WordNet produce confusing hints
- "about": "around",
- "there": "present",
- "which": "that",
- "other": "different",
- "would": "could",
- "these": "those",
- "while": "during",
- "being": "living",
- "where": "location",
- "still": "quiet",
- "never": "not ever",
- "every": "each",
- "since": "because",
- "those": "these",
- "among": "between",
- "money": "cash",
- "local": "nearby",
- "least": "fewest",
- "month": "april",
- "river": "stream",
- "david": "giant",
- "steve": "stephen",
- "didnt": "did not",
- "three": "trio",
- "years": "decade",
- "going": "moving",
- # Quality overrides for bad Datamuse/WordNet results
- "state": "condition",
- "games": "sport",
- "found": "located",
- "often": "frequent",
- "south": "southern",
- "small": "little",
- "north": "northern",
- "third": "tertiary",
- "party": "group",
- "began": "started",
- "young": "youth",
- "final": "last",
- "given": "provided",
- "using": "employ",
- "field": "area",
- "teams": "squad",
- "thing": "object",
- "white": "pale",
- "march": "parade",
- "black": "dark",
- "built": "constructed",
- "moved": "shifted",
- "added": "extra",
- "needs": "requires",
- "short": "brief",
- "front": "forward",
- "seven": "seventh",
- "title": "name",
- "comes": "arrives",
- "hours": "time",
- "eight": "eighth",
- "green": "color",
- "round": "circular",
- "union": "group",
- "lives": "exists",
- "radio": "broadcast",
- "words": "phrase",
- "model": "design",
- "terms": "words",
- "score": "points",
- "ready": "prepared",
- "grand": "large",
- }
- # ---- Russian synonym dictionary (curated for the 100 target words) ----
- # Manually selected synonyms or related hints for Russian target words.
- RU_HINTS: dict[str, str] = {
- "жизнь": "бытие",
- "слово": "речь",
- "часть": "доля",
- "город": "посёлок",
- "земля": "почва",
- "право": "закон",
- "образ": "облик",
- "закон": "правило",
- "война": "битва",
- "голос": "звук",
- "книга": "роман",
- "число": "цифра",
- "народ": "люди",
- "форма": "фигура",
- "улица": "дорога",
- "вечер": "закат",
- "мысль": "идея",
- "месяц": "луна",
- "школа": "учёба",
- "театр": "сцена",
- "смысл": "суть",
- "рынок": "базар",
- "центр": "середина",
- "ответ": "отклик",
- "автор": "писатель",
- "совет": "наказ",
- "глава": "лидер",
- "наука": "знание",
- "плечо": "рука",
- "точка": "пятно",
- "палец": "рука",
- "метод": "способ",
- "фильм": "кино",
- "гость": "визитёр",
- "район": "зона",
- "кровь": "рана",
- "армия": "войско",
- "класс": "группа",
- "герой": "смельчак",
- "спина": "тело",
- "берег": "пляж",
- "фирма": "компания",
- "завод": "фабрика",
- "повод": "причина",
- "выход": "уход",
- "текст": "письмо",
- "пункт": "место",
- "линия": "черта",
- "среда": "окружение",
- "волос": "прядь",
- "ветер": "буря",
- "огонь": "пламя",
- "страх": "ужас",
- "сфера": "область",
- "немец": "германец",
- "выбор": "отбор",
- "масса": "вес",
- "кухня": "готовка",
- "товар": "продукт",
- "вывод": "итог",
- "норма": "правило",
- "рамка": "граница",
- "прием": "встреча",
- "режим": "порядок",
- "целое": "полное",
- "доход": "прибыль",
- "карта": "схема",
- "акция": "дело",
- "фраза": "выражение",
- "толпа": "скопление",
- "птица": "пернатый",
- "запах": "аромат",
- "поезд": "состав",
- "адрес": "жильё",
- "лидер": "вождь",
- "весна": "сезон",
- "музей": "галерея",
- "сутки": "день",
- "еврей": "иудей",
- "сотня": "сто",
- "труба": "дымоход",
- "масло": "жир",
- "экран": "дисплей",
- "вагон": "поезд",
- "сезон": "пора",
- "длина": "размер",
- "доска": "дерево",
- "лодка": "судно",
- "серия": "ряд",
- "кулак": "рука",
- "нефть": "топливо",
- "кость": "скелет",
- "взрыв": "удар",
- "почва": "земля",
- "хвост": "конец",
- "строй": "порядок",
- "отчет": "доклад",
- "забор": "стена",
- "пожар": "огонь",
- "ножка": "нога",
- }
- def generate_english_hints(bank: dict) -> int:
- """Add hints to English targets. Returns count of missing hints."""
- targets = bank["targets"]
- missing = 0
- for i, target in enumerate(targets):
- word = target["word"]
- # Check manual override first
- if word in EN_HINT_OVERRIDES:
- target["hint"] = EN_HINT_OVERRIDES[word]
- continue
- # Try Datamuse
- candidates = get_english_synonyms_datamuse(word)
- if not candidates:
- # Fall back to WordNet
- candidates = get_english_synonyms_wordnet(word)
- hint = pick_best_synonym(word, candidates)
- if hint:
- target["hint"] = hint
- else:
- missing += 1
- print(f" MISSING: {word} (id={target['id']})")
- # Be polite to the API
- if i % 20 == 19:
- time.sleep(0.5)
- return missing
- def generate_russian_hints(bank: dict) -> int:
- """Add hints to Russian targets from curated dictionary. Returns count missing."""
- targets = bank["targets"]
- missing = 0
- for target in targets:
- word = target["word"]
- hint = RU_HINTS.get(word)
- if hint:
- target["hint"] = hint
- else:
- missing += 1
- print(f" MISSING: {word} (id={target['id']})")
- return missing
- def main():
- # English
- print("=== English synonyms ===")
- en_path = DATA_DIR / "word-bank.json"
- with open(en_path, encoding="utf-8") as f:
- en_bank = json.load(f)
- en_missing = generate_english_hints(en_bank)
- with open(en_path, "w", encoding="utf-8") as f:
- json.dump(en_bank, f, ensure_ascii=False, indent=2)
- f.write("\n")
- print(f" {len(en_bank['targets'])} targets, {len(en_bank['targets']) - en_missing} with hints, {en_missing} missing")
- # Russian
- print("\n=== Russian synonyms ===")
- ru_path = DATA_DIR / "word-bank-ru.json"
- with open(ru_path, encoding="utf-8") as f:
- ru_bank = json.load(f)
- ru_missing = generate_russian_hints(ru_bank)
- with open(ru_path, "w", encoding="utf-8") as f:
- json.dump(ru_bank, f, ensure_ascii=False, indent=2)
- f.write("\n")
- print(f" {len(ru_bank['targets'])} targets, {len(ru_bank['targets']) - ru_missing} with hints, {ru_missing} missing")
- print("\nDone!")
- if __name__ == "__main__":
- main()
|