GameSentenceMiner 2.18.14__tar.gz → 2.18.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/anki.py +8 -53
  2. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/obs.py +1 -2
  3. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ui/anki_confirmation.py +16 -2
  4. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/db.py +11 -7
  5. gamesentenceminer-2.18.16/GameSentenceMiner/util/games_table.py +320 -0
  6. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/vad.py +3 -3
  7. gamesentenceminer-2.18.16/GameSentenceMiner/web/anki_api_endpoints.py +506 -0
  8. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/database_api.py +239 -117
  9. gamesentenceminer-2.18.16/GameSentenceMiner/web/static/css/loading-skeleton.css +41 -0
  10. gamesentenceminer-2.18.16/GameSentenceMiner/web/static/css/search.css +68 -0
  11. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/stats.css +76 -0
  12. gamesentenceminer-2.18.16/GameSentenceMiner/web/static/js/anki_stats.js +424 -0
  13. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/database.js +44 -7
  14. gamesentenceminer-2.18.16/GameSentenceMiner/web/static/js/heatmap.js +326 -0
  15. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/overview.js +20 -224
  16. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/search.js +190 -23
  17. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/stats.js +371 -1
  18. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/stats.py +188 -0
  19. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/anki_stats.html +145 -58
  20. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/components/date-range.html +19 -0
  21. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/components/html-head.html +45 -0
  22. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/components/js-config.html +37 -0
  23. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/components/popups.html +15 -0
  24. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/components/settings-modal.html +233 -0
  25. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/database.html +13 -3
  26. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/goals.html +9 -31
  27. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/overview.html +247 -0
  28. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/search.html +46 -0
  29. gamesentenceminer-2.18.16/GameSentenceMiner/web/templates/stats.html +137 -0
  30. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/texthooking_page.py +4 -66
  31. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  32. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/SOURCES.txt +9 -0
  33. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/PKG-INFO +1 -1
  34. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/pyproject.toml +1 -1
  35. gamesentenceminer-2.18.14/GameSentenceMiner/web/static/css/search.css +0 -14
  36. gamesentenceminer-2.18.14/GameSentenceMiner/web/static/js/anki_stats.js +0 -170
  37. gamesentenceminer-2.18.14/GameSentenceMiner/web/templates/overview.html +0 -454
  38. gamesentenceminer-2.18.14/GameSentenceMiner/web/templates/stats.html +0 -399
  39. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/__init__.py +0 -0
  40. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ai/__init__.py +0 -0
  41. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  42. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/__init__.py +0 -0
  43. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon.png +0 -0
  44. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon128.png +0 -0
  45. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon256.png +0 -0
  46. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon32.png +0 -0
  47. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon512.png +0 -0
  48. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/icon64.png +0 -0
  49. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/assets/pickaxe.png +0 -0
  50. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/gametext.py +0 -0
  51. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/gsm.py +0 -0
  52. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/locales/en_us.json +0 -0
  53. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/locales/ja_jp.json +0 -0
  54. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/locales/zh_cn.json +0 -0
  55. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/__init__.py +0 -0
  56. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  57. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  58. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  59. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  60. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  61. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  62. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  63. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  64. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  65. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  66. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  67. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  68. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/tools/__init__.py +0 -0
  69. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  70. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/tools/furigana_filter_preview.py +0 -0
  71. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/tools/ss_selector.py +0 -0
  72. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/tools/window_transparency.py +0 -0
  73. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ui/__init__.py +0 -0
  74. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ui/config_gui.py +0 -0
  75. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ui/furigana_filter_preview.py +0 -0
  76. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/ui/screenshot_selector.py +0 -0
  77. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/__init__.py +0 -0
  78. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/audio_player.py +0 -0
  79. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/communication/__init__.py +0 -0
  80. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/communication/send.py +0 -0
  81. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/communication/websocket.py +0 -0
  82. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/configuration.py +0 -0
  83. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  84. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  85. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  86. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  87. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/electron_config.py +0 -0
  88. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/ffmpeg.py +0 -0
  89. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  90. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/gsm_utils.py +0 -0
  91. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/model.py +0 -0
  92. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/notification.py +0 -0
  93. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/text_log.py +0 -0
  94. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  95. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  96. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/__init__.py +0 -0
  97. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/events.py +0 -0
  98. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/gsm_websocket.py +0 -0
  99. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/service.py +0 -0
  100. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/__init__.py +0 -0
  101. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  102. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/dashboard-shared.css +0 -0
  103. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/kanji-grid.css +0 -0
  104. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/overview.css +0 -0
  105. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/popups-shared.css +0 -0
  106. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/css/shared.css +0 -0
  107. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  108. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/favicon.ico +0 -0
  109. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/favicon.svg +0 -0
  110. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/goals.js +0 -0
  111. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/kanji-grid.js +0 -0
  112. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/js/shared.js +0 -0
  113. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  114. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/style.css +0 -0
  115. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  116. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  117. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +0 -0
  118. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +0 -0
  119. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/grade.json +0 -0
  120. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +0 -0
  121. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +0 -0
  122. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +0 -0
  123. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +0 -0
  124. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +0 -0
  125. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +0 -0
  126. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +0 -0
  127. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +0 -0
  128. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +0 -0
  129. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +0 -0
  130. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +0 -0
  131. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +0 -0
  132. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +0 -0
  133. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +0 -0
  134. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +0 -0
  135. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +0 -0
  136. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +0 -0
  137. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +0 -0
  138. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +0 -0
  139. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +0 -0
  140. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +0 -0
  141. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/navigation.html +0 -0
  142. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/components/theme-styles.html +0 -0
  143. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/index.html +0 -0
  144. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/web/templates/utility.html +0 -0
  145. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner/wip/__init___.py +0 -0
  146. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  147. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  148. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/requires.txt +0 -0
  149. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  150. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/LICENSE +0 -0
  151. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/README.md +0 -0
  152. {gamesentenceminer-2.18.14 → gamesentenceminer-2.18.16}/setup.cfg +0 -0
@@ -233,12 +233,16 @@ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path
233
233
  config_app: 'ConfigApp' = gsm_state.config_app
234
234
  sentence = note['fields'].get(config.anki.sentence_field, last_note.get_field(config.anki.sentence_field))
235
235
 
236
- use_voice, sentence, translation, new_ss_path = config_app.show_anki_confirmation_dialog(
236
+ use_voice, sentence, translation, new_ss_path, add_nsfw_tag = config_app.show_anki_confirmation_dialog(
237
237
  tango, sentence, assets.screenshot_path, assets.audio_path if update_audio_flag else None, translation, ss_time
238
238
  )
239
239
  note['fields'][config.anki.sentence_field] = sentence
240
240
  note['fields'][config.ai.anki_field] = translation
241
241
  assets.screenshot_path = new_ss_path or assets.screenshot_path
242
+
243
+ # Add NSFW tag if checkbox was selected
244
+ if add_nsfw_tag:
245
+ tags.append("NSFW")
242
246
 
243
247
  # 5. If creating new media, store files in Anki's collection. Then update note fields.
244
248
  if not use_existing_files:
@@ -643,59 +647,10 @@ def start_monitoring_anki():
643
647
  obs_thread.start()
644
648
 
645
649
 
646
- # --- Anki Stats Kanji Extraction Utilities ---
650
+ # --- Anki Stats Utilities ---
651
+ # Note: Individual query functions have been removed in favor of the combined endpoint
652
+ # All Anki statistics are now fetched through /api/anki_stats_combined
647
653
 
648
- def get_anki_earliest_date():
649
- """
650
- Fetches the earliest Anki card ID.
651
- """
652
- try:
653
- note_ids = invoke("findCards", query="")
654
- if not note_ids:
655
- return 0
656
-
657
- # Return the first card ID as the "earliest"
658
- # return note_ids[0]
659
- return min(note_ids)
660
-
661
- except Exception as e:
662
- logger.error(f"Failed to fetch kanji from Anki: {e}")
663
- return 0
664
-
665
- def get_all_anki_first_field_kanji(start_timestamp = None, end_timestamp = None):
666
- """
667
- Fetch all notes from Anki and extract unique kanji from the first field of each note.
668
- Returns a set of kanji characters.
669
- Optional filtering by start_timestamp and end_timestamp on note IDs.
670
- """
671
- from GameSentenceMiner.web.stats import is_kanji
672
- try:
673
- note_ids = invoke("findNotes", query="")
674
- if not note_ids:
675
- return set()
676
-
677
- # Filter note IDs by start and end timestamps if provided
678
- if (start_timestamp and end_timestamp):
679
- note_ids = [nid for nid in note_ids if int(start_timestamp) <= nid <= int(end_timestamp)]
680
- if not note_ids:
681
- return set()
682
- kanji_set = set()
683
- batch_size = 1000
684
- for i in range(0, len(note_ids), batch_size):
685
- batch_ids = note_ids[i:i+batch_size]
686
- notes_info = invoke("notesInfo", notes=batch_ids)
687
- for note in notes_info:
688
- fields = note.get("fields", {})
689
- first_field = next(iter(fields.values()), None)
690
- if first_field and "value" in first_field:
691
- first_field_value = first_field["value"]
692
- for char in first_field_value:
693
- if is_kanji(char):
694
- kanji_set.add(char)
695
- return kanji_set
696
- except Exception as e:
697
- logger.error(f"Failed to fetch kanji from Anki: {e}")
698
- return set()
699
654
 
700
655
 
701
656
  if __name__ == "__main__":
@@ -561,8 +561,7 @@ def get_replay_buffer_max_time_seconds():
561
561
  if settings and 'max_time_sec' in settings:
562
562
  return settings['max_time_sec']
563
563
  else:
564
- logger.warning("Replay buffer settings received, but 'max_time_sec' key was not found.")
565
- return 0
564
+ return 300
566
565
  else:
567
566
  logger.warning(f"get_output_settings for replay_buffer failed: {response.status}")
568
567
  return 0
@@ -36,6 +36,9 @@ class AnkiConfirmationDialog(tk.Toplevel):
36
36
  self.audio_path_label = None # Store reference to audio path label
37
37
  self.tts_button = None # Store reference to TTS button
38
38
  self.tts_status_label = None # Store reference to TTS status label
39
+
40
+ # NSFW tag option
41
+ self.nsfw_tag_var = tk.BooleanVar(value=False)
39
42
 
40
43
  self.title("Confirm Anki Card Details")
41
44
  self.result = None # This will store the user's choice
@@ -152,6 +155,17 @@ class AnkiConfirmationDialog(tk.Toplevel):
152
155
 
153
156
  row += 1
154
157
 
158
+ # NSFW Tag Option
159
+ nsfw_frame = ttk.Frame(main_frame)
160
+ nsfw_frame.grid(row=row, column=0, columnspan=2, pady=10)
161
+ ttk.Checkbutton(
162
+ nsfw_frame,
163
+ text="Add NSFW tag?",
164
+ variable=self.nsfw_tag_var,
165
+ bootstyle="round-toggle"
166
+ ).pack(side="left", padx=5)
167
+ row += 1
168
+
155
169
  # Action Buttons
156
170
  button_frame = ttk.Frame(main_frame)
157
171
  button_frame.grid(row=row, column=0, columnspan=2, pady=15)
@@ -310,13 +324,13 @@ class AnkiConfirmationDialog(tk.Toplevel):
310
324
  # Clean up audio before closing
311
325
  self._cleanup_audio()
312
326
  # The screenshot_path is now correctly updated if the user chose a new one
313
- self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
327
+ self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path, self.nsfw_tag_var.get())
314
328
  self.destroy()
315
329
 
316
330
  def _on_no_voice(self):
317
331
  # Clean up audio before closing
318
332
  self._cleanup_audio()
319
- self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
333
+ self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path, self.nsfw_tag_var.get())
320
334
  self.destroy()
321
335
 
322
336
  def _on_cancel(self):
@@ -451,9 +451,9 @@ class AIModelsTable(SQLiteDBTable):
451
451
  class GameLinesTable(SQLiteDBTable):
452
452
  _table = 'game_lines'
453
453
  _fields = ['game_name', 'line_text', 'screenshot_in_anki',
454
- 'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation', 'timestamp', 'original_game_name']
454
+ 'audio_in_anki', 'screenshot_path', 'audio_path', 'replay_path', 'translation', 'timestamp', 'original_game_name', 'game_id']
455
455
  _types = [str, # Includes primary key type
456
- str, str, str, str, str, str, str, str, float, str]
456
+ str, str, str, str, str, str, str, str, float, str, str]
457
457
  _pk = 'id'
458
458
  _auto_increment = False # Use string IDs
459
459
 
@@ -468,7 +468,8 @@ class GameLinesTable(SQLiteDBTable):
468
468
  audio_path: Optional[str] = None,
469
469
  replay_path: Optional[str] = None,
470
470
  translation: Optional[str] = None,
471
- original_game_name: Optional[str] = None):
471
+ original_game_name: Optional[str] = None,
472
+ game_id: Optional[str] = None):
472
473
  self.id = id
473
474
  self.game_name = game_name
474
475
  self.line_text = line_text
@@ -481,6 +482,7 @@ class GameLinesTable(SQLiteDBTable):
481
482
  self.replay_path = replay_path if replay_path is not None else ''
482
483
  self.translation = translation if translation is not None else ''
483
484
  self.original_game_name = original_game_name if original_game_name is not None else ''
485
+ self.game_id = game_id if game_id is not None else ''
484
486
 
485
487
  @classmethod
486
488
  def get_all_lines_for_scene(cls, game_name: str) -> List['GameLinesTable']:
@@ -605,7 +607,7 @@ class StatsRollupTable(SQLiteDBTable):
605
607
  stats.anki_cards_created += anki_cards_created
606
608
  stats.time_spent_mining += time_spent_mining
607
609
  stats.save()
608
-
610
+
609
611
  # Ensure database directory exists and return path
610
612
  def get_db_directory(test=False, delete_test=False) -> str:
611
613
  if platform == 'win32': # Windows
@@ -659,7 +661,10 @@ if os.path.exists(db_path):
659
661
 
660
662
  gsm_db = SQLiteDB(db_path)
661
663
 
662
- for cls in [AIModelsTable, GameLinesTable]:
664
+ # Import GamesTable after gsm_db is created to avoid circular import
665
+ from GameSentenceMiner.util.games_table import GamesTable
666
+
667
+ for cls in [AIModelsTable, GameLinesTable, GamesTable]:
663
668
  cls.set_db(gsm_db)
664
669
  # Uncomment to start fresh every time
665
670
  # cls.drop()
@@ -680,8 +685,7 @@ def check_and_run_migrations():
680
685
  # Copy and cast data from old column to new column
681
686
  GameLinesTable.alter_column_type('timestamp_old', 'timestamp', 'REAL')
682
687
  logger.info("Migrated 'timestamp' column to REAL type in GameLinesTable.")
683
-
684
-
688
+
685
689
  migrate_timestamp()
686
690
 
687
691
  check_and_run_migrations()
@@ -0,0 +1,320 @@
1
+ import uuid
2
+ from typing import Optional, List, Dict
3
+
4
+ from GameSentenceMiner.util.db import SQLiteDBTable
5
+ from GameSentenceMiner.util.configuration import logger
6
+
7
+
8
+ class GamesTable(SQLiteDBTable):
9
+ _table = 'games'
10
+ _fields = [
11
+ 'deck_id', 'title_original', 'title_romaji', 'title_english',
12
+ 'type',
13
+ 'description', 'image', 'character_count', 'difficulty', 'links', 'completed',
14
+ 'manual_overrides'
15
+ ]
16
+ _types = [
17
+ str, # id (primary key)
18
+ int, # deck_id
19
+ str, # title_original
20
+ str, # title_romaji
21
+ str, # title_english
22
+ str, # type (string)
23
+ str, # description
24
+ str, # image (base64)
25
+ int, # character_count
26
+ int, # difficulty
27
+ list, # links (stored as JSON)
28
+ bool, # completed
29
+ list # manual_overrides (stored as JSON)
30
+ ]
31
+ _pk = 'id'
32
+ _auto_increment = False # UUID-based primary key
33
+
34
+ def __init__(
35
+ self,
36
+ id: Optional[str] = None,
37
+ deck_id: Optional[int] = None,
38
+ title_original: Optional[str] = None,
39
+ title_romaji: Optional[str] = None,
40
+ title_english: Optional[str] = None,
41
+ type: Optional[str] = None,
42
+ description: Optional[str] = None,
43
+ image: Optional[str] = None,
44
+ character_count: int = 0,
45
+ difficulty: Optional[int] = None,
46
+ links: Optional[List[Dict]] = None,
47
+ completed: bool = False,
48
+ manual_overrides: Optional[List[str]] = None
49
+ ):
50
+ self.id = id if id else str(uuid.uuid4())
51
+ self.deck_id = deck_id
52
+ self.title_original = title_original if title_original else ''
53
+ self.title_romaji = title_romaji if title_romaji else ''
54
+ self.title_english = title_english if title_english else ''
55
+ self.type = type if type else ''
56
+ self.description = description if description else ''
57
+ self.image = image if image else ''
58
+ self.character_count = character_count
59
+ self.difficulty = difficulty
60
+ self.links = links if links else []
61
+ self.completed = completed
62
+ self.manual_overrides = manual_overrides if manual_overrides else []
63
+
64
+ @classmethod
65
+ def get_by_deck_id(cls, deck_id: int) -> Optional['GamesTable']:
66
+ """Get a game by its jiten.moe deck ID."""
67
+ row = cls._db.fetchone(
68
+ f"SELECT * FROM {cls._table} WHERE deck_id=?", (deck_id,))
69
+ return cls.from_row(row) if row else None
70
+
71
+ @classmethod
72
+ def get_by_title(cls, title_original: str) -> Optional['GamesTable']:
73
+ """Get a game by its original title."""
74
+ row = cls._db.fetchone(
75
+ f"SELECT * FROM {cls._table} WHERE title_original=?", (title_original,))
76
+ return cls.from_row(row) if row else None
77
+
78
+ @classmethod
79
+ def get_or_create_by_name(cls, game_name: str) -> 'GamesTable':
80
+ """
81
+ Get an existing game by name, or create a new one if it doesn't exist.
82
+ This is the primary method for automatically linking game_lines to games.
83
+
84
+ Args:
85
+ game_name: The original game name (from game_lines.game_name)
86
+
87
+ Returns:
88
+ GamesTable: The existing or newly created game record
89
+ """
90
+ # Try to find existing game
91
+ existing = cls.get_by_title(game_name)
92
+ if existing:
93
+ return existing
94
+
95
+ # Create new game with minimal info
96
+ new_game = cls(
97
+ title_original=game_name,
98
+ title_romaji='',
99
+ title_english='',
100
+ description='',
101
+ difficulty=None,
102
+ completed=False
103
+ )
104
+ new_game.save()
105
+ logger.info(f"Auto-created new game record: {game_name} (id={new_game.id})")
106
+ return new_game
107
+
108
+ @classmethod
109
+ def get_all_completed(cls) -> List['GamesTable']:
110
+ """Get all completed games."""
111
+ rows = cls._db.fetchall(
112
+ f"SELECT * FROM {cls._table} WHERE completed=1")
113
+ return [cls.from_row(row) for row in rows]
114
+
115
+ @classmethod
116
+ def get_all_in_progress(cls) -> List['GamesTable']:
117
+ """Get all games that are in progress (not completed)."""
118
+ rows = cls._db.fetchall(
119
+ f"SELECT * FROM {cls._table} WHERE completed=0")
120
+ return [cls.from_row(row) for row in rows]
121
+
122
+ @classmethod
123
+ def get_start_date(cls, game_id: str) -> Optional[float]:
124
+ """
125
+ Get the start date (timestamp of first line) for a game.
126
+ Returns Unix timestamp (float) or None if no lines exist.
127
+ """
128
+ from GameSentenceMiner.util.db import GameLinesTable
129
+ result = GameLinesTable._db.fetchone(
130
+ f"SELECT MIN(timestamp) FROM {GameLinesTable._table} WHERE game_id=?",
131
+ (game_id,)
132
+ )
133
+ return result[0] if result and result[0] else None
134
+
135
+ @classmethod
136
+ def get_last_played_date(cls, game_id: str) -> Optional[float]:
137
+ """
138
+ Get the last played date (timestamp of most recent line) for a game.
139
+ Returns Unix timestamp (float) or None if no lines exist.
140
+ """
141
+ from GameSentenceMiner.util.db import GameLinesTable
142
+ result = GameLinesTable._db.fetchone(
143
+ f"SELECT MAX(timestamp) FROM {GameLinesTable._table} WHERE game_id=?",
144
+ (game_id,)
145
+ )
146
+ return result[0] if result and result[0] else None
147
+
148
+ def is_field_manual(self, field_name: str) -> bool:
149
+ """
150
+ Check if a field has been manually edited and should not be auto-updated.
151
+
152
+ Args:
153
+ field_name: The name of the field to check
154
+
155
+ Returns:
156
+ True if the field is manually overridden, False otherwise
157
+ """
158
+ return field_name in self.manual_overrides
159
+
160
+ def mark_field_manual(self, field_name: str):
161
+ """
162
+ Mark a field as manually edited so it won't be auto-updated.
163
+
164
+ Args:
165
+ field_name: The name of the field to mark as manual
166
+ """
167
+ if field_name not in self.manual_overrides and field_name in self._fields:
168
+ self.manual_overrides.append(field_name)
169
+ logger.debug(f"Marked field '{field_name}' as manually overridden for game {self.id}")
170
+
171
+ def update_all_fields_manual(
172
+ self,
173
+ deck_id: Optional[int] = None,
174
+ title_original: Optional[str] = None,
175
+ title_romaji: Optional[str] = None,
176
+ title_english: Optional[str] = None,
177
+ type: Optional[str] = None,
178
+ description: Optional[str] = None,
179
+ image: Optional[str] = None,
180
+ character_count: Optional[int] = None,
181
+ difficulty: Optional[int] = None,
182
+ links: Optional[List[Dict]] = None,
183
+ completed: Optional[bool] = None
184
+ ):
185
+ """
186
+ Update all fields of the game at once. Only provided fields will be updated.
187
+ Fields that are updated will be automatically marked as manually overridden.
188
+
189
+ Args:
190
+ deck_id: jiten.moe deck ID
191
+ title_original: Original Japanese title
192
+ title_romaji: Romanized title
193
+ title_english: English translated title
194
+ description: Game description
195
+ image: Base64-encoded image data
196
+ character_count: Total character count
197
+ difficulty: Difficulty rating
198
+ links: List of link objects
199
+ completed: Whether the game is completed
200
+ """
201
+ if deck_id is not None:
202
+ self.deck_id = deck_id
203
+ self.mark_field_manual('deck_id')
204
+ if title_original is not None:
205
+ self.title_original = title_original
206
+ self.mark_field_manual('title_original')
207
+ if title_romaji is not None:
208
+ self.title_romaji = title_romaji
209
+ self.mark_field_manual('title_romaji')
210
+ if title_english is not None:
211
+ self.title_english = title_english
212
+ self.mark_field_manual('title_english')
213
+ if type is not None:
214
+ self.type = type
215
+ self.mark_field_manual('type')
216
+ if description is not None:
217
+ self.description = description
218
+ self.mark_field_manual('description')
219
+ if image is not None:
220
+ self.image = image
221
+ self.mark_field_manual('image')
222
+ if character_count is not None:
223
+ self.character_count = character_count
224
+ self.mark_field_manual('character_count')
225
+ if difficulty is not None:
226
+ self.difficulty = difficulty
227
+ self.mark_field_manual('difficulty')
228
+ if links is not None:
229
+ self.links = links
230
+ self.mark_field_manual('links')
231
+ if completed is not None:
232
+ self.completed = completed
233
+ self.mark_field_manual('completed')
234
+
235
+ self.save()
236
+ logger.info(f"Updated game {self.id} ({self.title_original})")
237
+
238
+ def update_all_fields_from_jiten(
239
+ self,
240
+ deck_id: Optional[int] = None,
241
+ title_original: Optional[str] = None,
242
+ title_romaji: Optional[str] = None,
243
+ title_english: Optional[str] = None,
244
+ type: Optional[str] = None,
245
+ description: Optional[str] = None,
246
+ image: Optional[str] = None,
247
+ character_count: Optional[int] = None,
248
+ difficulty: Optional[int] = None,
249
+ links: Optional[List[Dict]] = None,
250
+ completed: Optional[bool] = None
251
+ ):
252
+ """
253
+ Update all fields of the game at once. Only provided fields will be updated.
254
+
255
+ Args:
256
+ deck_id: jiten.moe deck ID
257
+ title_original: Original Japanese title
258
+ title_romaji: Romanized title
259
+ title_english: English translated title
260
+ description: Game description
261
+ image: Base64-encoded image data
262
+ character_count: Total character count
263
+ difficulty: Difficulty rating
264
+ links: List of link objects
265
+ completed: Whether the game is completed
266
+ """
267
+ if deck_id is not None:
268
+ self.deck_id = deck_id
269
+ if title_original is not None:
270
+ self.title_original = title_original
271
+ if title_romaji is not None:
272
+ self.title_romaji = title_romaji
273
+ if title_english is not None:
274
+ self.title_english = title_english
275
+ if type is not None:
276
+ self.type = type
277
+ if description is not None:
278
+ self.description = description
279
+ if image is not None:
280
+ self.image = image
281
+ if character_count is not None:
282
+ self.character_count = character_count
283
+ if difficulty is not None:
284
+ self.difficulty = difficulty
285
+ if links is not None:
286
+ self.links = links
287
+ if completed is not None:
288
+ self.completed = completed
289
+ self.save()
290
+ logger.info(f"Updated game {self.id} ({self.title_original})")
291
+
292
+ def add_link(self, link_type: int, url: str, link_id: Optional[int] = None):
293
+ """
294
+ Add a link to the game's links array and persist to database.
295
+
296
+ Args:
297
+ link_type: Type of link (e.g., 4 for AniList, 5 for MyAnimeList)
298
+ url: URL of the link
299
+ link_id: Optional link ID
300
+
301
+ Note:
302
+ Changes are automatically saved to the database.
303
+ """
304
+ new_link = {
305
+ 'linkType': link_type,
306
+ 'url': url,
307
+ 'deckId': self.deck_id
308
+ }
309
+ if link_id is not None:
310
+ new_link['linkId'] = link_id
311
+
312
+ self.links.append(new_link)
313
+ self.save()
314
+
315
+ def get_lines(self) -> List:
316
+ """Get all lines associated with this game."""
317
+ from GameSentenceMiner.util.db import GameLinesTable
318
+ rows = GameLinesTable._db.fetchall(
319
+ f"SELECT * FROM {GameLinesTable._table} WHERE game_id=?", (self.id,))
320
+ return [GameLinesTable.from_row(row) for row in rows]
@@ -195,11 +195,11 @@ class WhisperVADProcessor(VADProcessor):
195
195
  # If both mined text and Whisper transcription are available, compare their similarity
196
196
  if text_mined and text:
197
197
  from rapidfuzz import fuzz
198
- similarity = fuzz.partial_ratio(text_mined, text)
199
- logger.info(f"Whisper transcription: '{text}' | Mined text: '{text_mined}' | Partial similarity: {similarity:.1f}")
198
+ similarity = fuzz.ratio(text_mined, text)
199
+ logger.info(f"Whisper transcription: '{text}' | Mined text: '{text_mined}' | Full similarity: {similarity:.1f}")
200
200
  text_similarity = similarity
201
201
  if similarity < 20:
202
- logger.info(f"Partial similarity {similarity:.1f} is below threshold, skipping voice activity.")
202
+ logger.info(f"Full similarity {similarity:.1f} is below threshold, skipping voice activity.")
203
203
  return []
204
204
 
205
205
  # Process the segments to extract tokens, timestamps, and confidence