generate-banks-1000.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python3
  2. """Generate word-bank.json and word-bank-ru.json with 1000 targets each.
  3. Targets are the first 1000 words from the frequency-sorted guessable list
  4. (most common words first). IDs are sequential 1..1000.
  5. Uses:
  6. - Datamuse API + WordNet for English synonyms
  7. - Abramov's Russian synonym dictionary for Russian synonyms
  8. """
  9. import json
  10. import sys
  11. import time
  12. from pathlib import Path
  13. RAW_DIR = Path(__file__).parent
  14. DATA_DIR = RAW_DIR.parent / "data"
  15. TARGET_COUNT = 1000
  16. # ---- English hint overrides (curated for quality) ----
  17. EN_HINT_OVERRIDES: dict[str, str] = {
  18. "about": "around", "there": "present", "which": "that",
  19. "other": "different", "would": "could", "these": "those",
  20. "while": "during", "being": "living", "where": "location",
  21. "still": "quiet", "never": "not ever", "every": "each",
  22. "since": "because", "those": "these", "among": "between",
  23. "money": "cash", "local": "nearby", "least": "fewest",
  24. "month": "april", "river": "stream", "david": "giant",
  25. "steve": "stephen", "didnt": "did not", "three": "trio",
  26. "years": "decade", "going": "moving",
  27. "state": "condition", "games": "sport", "found": "located",
  28. "often": "frequent", "south": "southern", "small": "little",
  29. "north": "northern", "third": "tertiary", "party": "group",
  30. "began": "started", "young": "youth", "final": "last",
  31. "given": "provided", "using": "employ", "field": "area",
  32. "teams": "squad", "thing": "object", "white": "pale",
  33. "march": "parade", "black": "dark", "built": "constructed",
  34. "moved": "shifted", "added": "extra", "needs": "requires",
  35. "short": "brief", "front": "forward", "seven": "seventh",
  36. "title": "name", "comes": "arrives", "hours": "time",
  37. "eight": "eighth", "green": "color", "round": "circular",
  38. "union": "group", "lives": "exists", "radio": "broadcast",
  39. "words": "phrase", "model": "design", "terms": "words",
  40. "score": "points", "ready": "prepared", "grand": "large",
  41. }
  42. # ---- Russian hint overrides (curated for the most common words) ----
  43. RU_HINT_OVERRIDES: dict[str, str] = {
  44. "жизнь": "бытие", "слово": "речь", "часть": "доля", "город": "посёлок",
  45. "земля": "почва", "право": "закон", "образ": "облик", "закон": "правило",
  46. "война": "битва", "голос": "звук", "книга": "роман", "число": "цифра",
  47. "народ": "люди", "форма": "фигура", "улица": "дорога", "вечер": "закат",
  48. "мысль": "идея", "месяц": "луна", "школа": "учёба", "театр": "сцена",
  49. "смысл": "суть", "рынок": "базар", "центр": "середина", "ответ": "отклик",
  50. "автор": "писатель", "совет": "наказ", "глава": "лидер", "наука": "знание",
  51. "плечо": "рука", "точка": "пятно", "палец": "рука", "метод": "способ",
  52. "фильм": "кино", "гость": "визитёр", "район": "зона", "кровь": "рана",
  53. "армия": "войско", "класс": "группа", "герой": "смельчак", "спина": "тело",
  54. "берег": "пляж", "фирма": "компания", "завод": "фабрика", "повод": "причина",
  55. "выход": "уход", "текст": "письмо", "пункт": "место", "линия": "черта",
  56. "среда": "окружение", "волос": "прядь", "ветер": "буря", "огонь": "пламя",
  57. "страх": "ужас", "сфера": "область", "немец": "германец", "выбор": "отбор",
  58. "масса": "вес", "кухня": "готовка", "товар": "продукт", "вывод": "итог",
  59. "норма": "правило", "рамка": "граница", "прием": "встреча", "режим": "порядок",
  60. "целое": "полное", "доход": "прибыль", "карта": "схема", "акция": "дело",
  61. "фраза": "выражение", "толпа": "скопление", "птица": "пернатый", "запах": "аромат",
  62. "поезд": "состав", "адрес": "жильё", "лидер": "вождь", "весна": "сезон",
  63. "музей": "галерея", "сутки": "день", "еврей": "иудей", "сотня": "сто",
  64. "труба": "дымоход", "масло": "жир", "экран": "дисплей", "вагон": "поезд",
  65. "сезон": "пора", "длина": "размер", "доска": "дерево", "лодка": "судно",
  66. "серия": "ряд", "кулак": "рука", "нефть": "топливо", "кость": "скелет",
  67. "взрыв": "удар", "почва": "земля", "хвост": "конец", "строй": "порядок",
  68. "отчет": "доклад", "забор": "стена", "пожар": "огонь", "ножка": "нога",
  69. }
  70. def load_dictionary(path: Path) -> list[str]:
  71. """Read frequency-sorted word list (one word per line)."""
  72. return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
  73. def load_abramov_dict() -> dict[str, str]:
  74. """Load Abramov's Russian synonym dictionary, returning {word: best_synonym}.
  75. Downloads from GitHub if not already cached locally."""
  76. path = RAW_DIR / "abramov-ru-synonyms.json"
  77. if not path.exists():
  78. print(" Downloading Abramov Russian synonym dictionary...")
  79. import requests
  80. url = "https://raw.githubusercontent.com/egorkaru/synonym_dictionary/master/dictionary.json"
  81. resp = requests.get(url, timeout=30)
  82. resp.raise_for_status()
  83. path.write_text(resp.text, encoding="utf-8")
  84. data = json.loads(path.read_text(encoding="utf-8"))
  85. result: dict[str, str] = {}
  86. for entry in data["wordlist"]:
  87. name = entry["name"].lower()
  88. synonyms = entry.get("synonyms", [])
  89. if not synonyms:
  90. continue
  91. # Pick the best single-word synonym
  92. for syn in synonyms:
  93. # Clean: remove multi-word alternatives (split by ";"), pick first single word
  94. parts = syn.replace(";", ",").split(",")
  95. for part in parts:
  96. part = part.strip().lower()
  97. # Skip multi-word phrases, the word itself, and single letters
  98. if " " in part or part == name or len(part) <= 1:
  99. continue
  100. result[name] = part
  101. break
  102. if name in result:
  103. break
  104. print(f" Loaded {len(result)} Russian synonyms from Abramov dictionary")
  105. return result
  106. def get_english_synonyms_datamuse(word: str) -> list[str]:
  107. """Get related words from Datamuse API."""
  108. import requests
  109. try:
  110. resp = requests.get(
  111. "https://api.datamuse.com/words",
  112. params={"ml": word, "max": 8},
  113. timeout=5,
  114. )
  115. resp.raise_for_status()
  116. results = resp.json()
  117. return [r["word"].lower() for r in results if " " not in r["word"] and r["word"].lower() != word]
  118. except Exception as e:
  119. print(f" Datamuse error for '{word}': {e}", file=sys.stderr)
  120. return []
  121. def get_english_synonyms_wordnet(word: str) -> list[str]:
  122. """Get synonyms from WordNet."""
  123. from nltk.corpus import wordnet as wn
  124. syns = wn.synsets(word)
  125. if not syns:
  126. return []
  127. lemmas = []
  128. for s in syns[:10]:
  129. for l in s.lemmas():
  130. name = l.name().lower()
  131. if "_" not in name and name != word and name not in lemmas:
  132. lemmas.append(name)
  133. return lemmas
  134. def pick_best_synonym(word: str, candidates: list[str]) -> str | None:
  135. """Pick the best synonym from candidates."""
  136. good = [c for c in candidates if len(c) <= 10 and "_" not in c]
  137. if not good:
  138. return None
  139. good.sort(key=len)
  140. return good[0]
  141. def generate_english_hints(targets: list[dict]) -> int:
  142. """Add hints to English targets. Returns count of missing hints."""
  143. missing = 0
  144. for i, target in enumerate(targets):
  145. word = target["word"]
  146. # Check manual override first
  147. if word in EN_HINT_OVERRIDES:
  148. target["hint"] = EN_HINT_OVERRIDES[word]
  149. continue
  150. # Try Datamuse
  151. candidates = get_english_synonyms_datamuse(word)
  152. if not candidates:
  153. candidates = get_english_synonyms_wordnet(word)
  154. hint = pick_best_synonym(word, candidates)
  155. if hint:
  156. target["hint"] = hint
  157. else:
  158. missing += 1
  159. if missing <= 10:
  160. print(f" MISSING: {word} (index={i})")
  161. if (i + 1) % 50 == 0:
  162. print(f" Progress: {i + 1}/{len(targets)}")
  163. time.sleep(0.5)
  164. return missing
  165. def generate_russian_hints(targets: list[dict], abramov: dict[str, str]) -> int:
  166. """Add hints to Russian targets from Abramov dictionary, with curated overrides."""
  167. missing = 0
  168. missing_words: list[str] = []
  169. for target in targets:
  170. word = target["word"]
  171. # Check curated override first
  172. if word in RU_HINT_OVERRIDES:
  173. target["hint"] = RU_HINT_OVERRIDES[word]
  174. continue
  175. hint = abramov.get(word)
  176. if hint:
  177. target["hint"] = hint
  178. else:
  179. missing_words.append(word)
  180. missing += 1
  181. if missing <= 5:
  182. idx = targets.index(target)
  183. print(f" MISSING: {word} (index={idx})")
  184. return missing
  185. def generate_russian_hints_translation(targets: list[dict], missing_words: list[str]) -> int:
  186. """Fallback: translate RU→EN, find English synonym, translate back to RU."""
  187. from deep_translator import GoogleTranslator
  188. from nltk.corpus import wordnet as wn
  189. import requests as req
  190. target_map = {t["word"]: t for t in targets}
  191. still_missing = 0
  192. for i, word in enumerate(missing_words):
  193. try:
  194. # Step 1: Translate RU → EN
  195. en = GoogleTranslator(source='ru', target='en').translate(word).lower()
  196. time.sleep(0.15)
  197. # Step 2: Find English synonym (WordNet primary synset first, then Datamuse)
  198. synonym = None
  199. syns = wn.synsets(en)
  200. if syns:
  201. primary = syns[0]
  202. for l in primary.lemmas():
  203. name = l.name().lower()
  204. if name != en and '_' not in name:
  205. synonym = name
  206. break
  207. if not synonym:
  208. # Try Datamuse as fallback
  209. try:
  210. resp = req.get("https://api.datamuse.com/words",
  211. params={"ml": en, "max": 5}, timeout=5)
  212. for r in resp.json():
  213. dm_word = r["word"]
  214. if " " not in dm_word and dm_word != en:
  215. synonym = dm_word
  216. break
  217. except Exception:
  218. pass
  219. if synonym:
  220. # Step 3: Translate back EN → RU
  221. ru_syn = GoogleTranslator(source='en', target='ru').translate(synonym).lower()
  222. time.sleep(0.15)
  223. # Quality filters: skip if same as input, multi-word, or too long
  224. if ru_syn != word and ' ' not in ru_syn and len(ru_syn) <= 15 and ru_syn != en:
  225. target_map[word]["hint"] = ru_syn
  226. else:
  227. still_missing += 1
  228. else:
  229. still_missing += 1
  230. except Exception:
  231. still_missing += 1
  232. if (i + 1) % 50 == 0:
  233. print(f" Translation progress: {i + 1}/{len(missing_words)} (missing: {still_missing})")
  234. return still_missing
  235. def build_bank(dict_path: Path, output_path: Path, language: str, abramov: dict[str, str] | None = None) -> None:
  236. """Build a word bank with 1000 targets and hints."""
  237. guessable = load_dictionary(dict_path)
  238. if len(guessable) < TARGET_COUNT:
  239. print(f" WARNING: {dict_path} has only {len(guessable)} words, using all")
  240. count = len(guessable)
  241. else:
  242. count = TARGET_COUNT
  243. targets = [{"word": guessable[i]} for i in range(count)]
  244. print(f" Generating hints for {len(targets)} targets ({language})...")
  245. if language == "en":
  246. missing = generate_english_hints(targets)
  247. else:
  248. missing = generate_russian_hints(targets, abramov or {})
  249. bank = {"guessable": guessable, "targets": targets}
  250. output_path.write_text(
  251. json.dumps(bank, ensure_ascii=False, indent=2) + "\n",
  252. encoding="utf-8",
  253. )
  254. print(f" Wrote {output_path}: {len(guessable)} guessable, {len(targets)} targets, {len(targets) - missing} with hints, {missing} missing")
  255. def main():
  256. print("Loading Abramov Russian synonym dictionary...")
  257. abramov = load_abramov_dict()
  258. print("\n=== English word bank ===")
  259. build_bank(
  260. RAW_DIR / "english-5letter-freq-sortfreq-wordsonly.txt",
  261. DATA_DIR / "word-bank.json",
  262. "en",
  263. )
  264. print("\n=== Russian word bank ===")
  265. build_bank(
  266. RAW_DIR / "russian-5letter-freq-sortfreq-wordsonly.txt",
  267. DATA_DIR / "word-bank-ru.json",
  268. "ru",
  269. abramov,
  270. )
  271. print("\nDone. Run 'npm run solve' to validate solvability.")
  272. if __name__ == "__main__":
  273. main()