GameSentenceMiner 2.16.4__tar.gz → 2.16.6__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 (103) hide show
  1. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/anki.py +7 -2
  2. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/config_gui.py +12 -0
  3. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/locales/en_us.json +5 -1
  4. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/locales/ja_jp.json +4 -0
  5. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/locales/zh_cn.json +4 -0
  6. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/configuration.py +35 -0
  7. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/ffmpeg.py +3 -3
  8. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/database_api.py +203 -76
  9. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/css/stats.css +243 -0
  10. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/search.js +46 -22
  11. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/shared.js +60 -12
  12. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/stats.js +363 -20
  13. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/stats.py +3 -3
  14. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/search.html +6 -1
  15. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/stats.html +163 -12
  16. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/texthooking_page.py +1 -1
  17. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  18. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/SOURCES.txt +1 -1
  19. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/PKG-INFO +1 -1
  20. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/pyproject.toml +1 -1
  21. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/__init__.py +0 -0
  22. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ai/__init__.py +0 -0
  23. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  24. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/__init__.py +0 -0
  25. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon.png +0 -0
  26. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon128.png +0 -0
  27. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon256.png +0 -0
  28. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon32.png +0 -0
  29. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon512.png +0 -0
  30. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/icon64.png +0 -0
  31. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/assets/pickaxe.png +0 -0
  32. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/gametext.py +0 -0
  33. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/gsm.py +0 -0
  34. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/obs.py +0 -0
  35. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/__init__.py +0 -0
  36. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  37. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  38. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  39. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  40. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  41. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  42. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  43. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  44. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  45. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  46. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  47. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  48. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/tools/__init__.py +0 -0
  49. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  50. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/tools/furigana_filter_preview.py +0 -0
  51. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/tools/ss_selector.py +0 -0
  52. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/tools/window_transparency.py +0 -0
  53. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/__init__.py +0 -0
  54. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/communication/__init__.py +0 -0
  55. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/communication/send.py +0 -0
  56. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/communication/websocket.py +0 -0
  57. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/db.py +0 -0
  58. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  59. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  60. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  61. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  62. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/electron_config.py +0 -0
  63. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  64. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/gsm_utils.py +0 -0
  65. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/model.py +0 -0
  66. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/notification.py +0 -0
  67. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/text_log.py +0 -0
  68. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  69. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  70. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/vad.py +0 -0
  71. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/__init__.py +0 -0
  72. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/events.py +0 -0
  73. /gamesentenceminer-2.16.4/GameSentenceMiner/web/websockets.py → /gamesentenceminer-2.16.6/GameSentenceMiner/web/gsm_websocket.py +0 -0
  74. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/service.py +0 -0
  75. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/__init__.py +0 -0
  76. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  77. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/css/kanji-grid.css +0 -0
  78. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/css/search.css +0 -0
  79. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/css/shared.css +0 -0
  80. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  81. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/favicon.ico +0 -0
  82. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/favicon.svg +0 -0
  83. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/anki_stats.js +0 -0
  84. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/database.js +0 -0
  85. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/js/kanji-grid.js +0 -0
  86. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  87. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/style.css +0 -0
  88. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  89. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  90. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/anki_stats.html +0 -0
  91. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/components/navigation.html +0 -0
  92. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/components/theme-styles.html +0 -0
  93. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/database.html +0 -0
  94. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/index.html +0 -0
  95. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/web/templates/utility.html +0 -0
  96. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner/wip/__init___.py +0 -0
  97. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  98. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  99. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/requires.txt +0 -0
  100. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  101. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/LICENSE +0 -0
  102. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/README.md +0 -0
  103. {gamesentenceminer-2.16.4 → gamesentenceminer-2.16.6}/setup.cfg +0 -0
@@ -87,13 +87,18 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
87
87
 
88
88
  if update_picture and screenshot_in_anki:
89
89
  note['fields'][get_config().anki.picture_field] = image_html
90
-
90
+
91
91
  if video_in_anki:
92
92
  note['fields'][get_config().anki.video_field] = video_in_anki
93
-
93
+
94
94
  if not get_config().screenshot.enabled:
95
95
  logger.info("Skipping Adding Screenshot to Anki, Screenshot is disabled in settings")
96
96
 
97
+ # Add game name to field if configured
98
+ game_name_field = get_config().anki.game_name_field
99
+ if note and 'fields' in note and game_name_field:
100
+ note['fields'][game_name_field] = get_current_game()
101
+
97
102
  if note and 'fields' in note and get_config().ai.enabled:
98
103
  sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
99
104
  sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
@@ -307,6 +307,7 @@ class ConfigApp:
307
307
  self.word_field_value = tk.StringVar(value=self.settings.anki.word_field)
308
308
  self.previous_sentence_field_value = tk.StringVar(value=self.settings.anki.previous_sentence_field)
309
309
  self.previous_image_field_value = tk.StringVar(value=self.settings.anki.previous_image_field)
310
+ self.game_name_field_value = tk.StringVar(value=self.settings.anki.game_name_field)
310
311
  self.video_field_value = tk.StringVar(value=self.settings.anki.video_field)
311
312
  self.custom_tags_value = tk.StringVar(value=', '.join(self.settings.anki.custom_tags))
312
313
  self.tags_to_check_value = tk.StringVar(value=', '.join(self.settings.anki.tags_to_check))
@@ -440,6 +441,7 @@ class ConfigApp:
440
441
  default_category_config = getattr(self.default_settings, category)
441
442
 
442
443
  setattr(self.settings, category, default_category_config)
444
+ self.create_vars() # Recreate variables to reflect default values
443
445
  recreate_tab()
444
446
  self.save_settings(profile_change=False)
445
447
  self.reload_settings()
@@ -527,6 +529,7 @@ class ConfigApp:
527
529
  previous_sentence_field=self.previous_sentence_field_value.get(),
528
530
  previous_image_field=self.previous_image_field_value.get(),
529
531
  video_field=self.video_field_value.get(),
532
+ game_name_field=self.game_name_field_value.get(),
530
533
  custom_tags=[tag.strip() for tag in self.custom_tags_value.get().split(',') if tag.strip()],
531
534
  tags_to_check=[tag.strip().lower() for tag in self.tags_to_check_value.get().split(',') if tag.strip()],
532
535
  add_game_tag=self.add_game_tag_value.get(),
@@ -1146,6 +1149,9 @@ class ConfigApp:
1146
1149
  HoverInfoLabelWidget(vad_frame, text=use_cpu_i18n.get('label', 'Force CPU'), tooltip=use_cpu_i18n.get('tooltip', 'Even if CUDA is installed, use CPU for Whisper'), row=self.current_row, column=0)
1147
1150
  ttk.Checkbutton(vad_frame, variable=self.use_cpu_for_inference_value, bootstyle="round-toggle").grid(row=self.current_row, column=1, sticky='W', pady=2)
1148
1151
  self.current_row += 1
1152
+
1153
+ # Add Reset Button
1154
+ self.add_reset_button(vad_frame, "vad", self.current_row, column=0, recreate_tab=self.create_vad_tab)
1149
1155
 
1150
1156
  @new_tab
1151
1157
  def create_paths_tab(self):
@@ -1319,6 +1325,12 @@ class ConfigApp:
1319
1325
  row=self.current_row, column=0)
1320
1326
  ttk.Entry(anki_frame, textvariable=self.video_field_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1321
1327
  self.current_row += 1
1328
+
1329
+ game_name_field_i18n = anki_i18n.get('game_name_field', {})
1330
+ HoverInfoLabelWidget(anki_frame, text=game_name_field_i18n.get('label', 'Game Name Field:'),
1331
+ tooltip=game_name_field_i18n.get('tooltip', 'Field in Anki for the game name.'), row=self.current_row, column=0)
1332
+ ttk.Entry(anki_frame, textvariable=self.game_name_field_value).grid(row=self.current_row, column=1, columnspan=3, sticky='EW', pady=2)
1333
+ self.current_row += 1
1322
1334
 
1323
1335
  tags_i18n = anki_i18n.get('custom_tags', {})
1324
1336
  HoverInfoLabelWidget(anki_frame, text=tags_i18n.get('label', '...'), tooltip=tags_i18n.get('tooltip', '...'),
@@ -184,12 +184,16 @@
184
184
  },
185
185
  "video_field": {
186
186
  "label": "Video Field:",
187
- "tooltip": "Field in Anki for associated videos. This will be AV1 encoded video of the VAD Trimmed Voiceline, if no Voice found, this will be empty."
187
+ "tooltip": "Field in Anki for associated videos. This will be AV1 encoded video of the VAD Trimmed Voiceline, if no Voice found, this will be empty. (OPTIONAL)"
188
188
  },
189
189
  "custom_tags": {
190
190
  "label": "Add Tags:",
191
191
  "tooltip": "Comma-separated custom tags for the Anki cards."
192
192
  },
193
+ "game_name_field": {
194
+ "label": "Game Name Field:",
195
+ "tooltip": "Field in Anki for the game name. If empty, game name will not be added as a field. (OPTIONAL)"
196
+ },
193
197
  "tags_to_check": {
194
198
  "label": "Tags to work on:",
195
199
  "tooltip": "Comma-separated Tags, script will only do 1-click on cards with these tags (Recommend keep empty, or use Yomitan Profile to add custom tag from texthooker page)"
@@ -189,6 +189,10 @@
189
189
  "label": "追加タグ:",
190
190
  "tooltip": "Ankiカードに追加するカスタムタグ(カンマ区切り)。"
191
191
  },
192
+ "game_name_field": {
193
+ "label": "ゲーム名フィールド:",
194
+ "tooltip": "Ankiのゲーム名用フィールド。空欄の場合は追加されません。"
195
+ },
192
196
  "tags_to_check": {
193
197
  "label": "対象タグ:",
194
198
  "tooltip": "これらのタグを持つカードのみワンクリック対象になります(通常は空を推奨)。"
@@ -190,6 +190,10 @@
190
190
  "label": "添加标签:",
191
191
  "tooltip": "Anki 卡片的自定义标签(以逗号分隔)。"
192
192
  },
193
+ "game_name_field": {
194
+ "label": "游戏名称字段:",
195
+ "tooltip": "Anki 中用于游戏名称的字段。如果为空,则不会添加游戏名称。"
196
+ },
193
197
  "tags_to_check": {
194
198
  "label": "处理的标签:",
195
199
  "tooltip": "脚本将只对带有这些标签的卡片进行一键操作(建议留空)。"
@@ -441,6 +441,7 @@ class Anki:
441
441
  custom_tags: List[str] = None
442
442
  tags_to_check: List[str] = None
443
443
  add_game_tag: bool = True
444
+ game_name_field: str = ''
444
445
  polling_rate: int = 200
445
446
  overwrite_audio: bool = False
446
447
  overwrite_picture: bool = True
@@ -780,6 +781,15 @@ class ProfileConfig:
780
781
  def config_changed(self, new: 'ProfileConfig') -> bool:
781
782
  return self != new
782
783
 
784
+ @dataclass_json
785
+ @dataclass
786
+ class StatsConfig:
787
+ afk_timer_seconds: int = 120
788
+ session_gap_seconds: int = 3600
789
+ streak_requirement_hours: float = 0.01 # 1 second required per day to keep your streak by default
790
+ reading_hours_target: int = 1500 # Target reading hours based on TMW N1 achievement data
791
+ character_count_target: int = 25000000 # Target character count (25M) inspired by Discord server milestones
792
+ games_target: int = 100 # Target VNs/games completed based on Refold community standards
783
793
 
784
794
  @dataclass_json
785
795
  @dataclass
@@ -788,6 +798,7 @@ class Config:
788
798
  current_profile: str = DEFAULT_CONFIG
789
799
  switch_to_default_if_not_found: bool = True
790
800
  locale: str = Locale.English.value
801
+ stats: StatsConfig = field(default_factory=StatsConfig)
791
802
 
792
803
  @classmethod
793
804
  def new(cls):
@@ -812,6 +823,18 @@ class Config:
812
823
  return cls.from_dict(data)
813
824
  else:
814
825
  return cls.new()
826
+
827
+ def __post_init__(self):
828
+ # Move Stats to global config if found in profiles for legacy support
829
+ default_stats = StatsConfig()
830
+ for profile in self.configs.values():
831
+ if profile.advanced:
832
+ if profile.advanced.afk_timer_seconds != default_stats.afk_timer_seconds:
833
+ self.stats.afk_timer_seconds = profile.advanced.afk_timer_seconds
834
+ if profile.advanced.session_gap_seconds != default_stats.session_gap_seconds:
835
+ self.stats.session_gap_seconds = profile.advanced.session_gap_seconds
836
+ if profile.advanced.streak_requirement_hours != default_stats.streak_requirement_hours:
837
+ self.stats.streak_requirement_hours = profile.advanced.streak_requirement_hours
815
838
 
816
839
  def save(self):
817
840
  with open(get_config_path(), 'w') as file:
@@ -1069,6 +1092,12 @@ def reload_config():
1069
1092
  logger.warning(
1070
1093
  "Backfill is enabled, but full auto is also enabled. Disabling backfill...")
1071
1094
  config.features.backfill_audio = False
1095
+
1096
+ def get_stats_config():
1097
+ global config_instance
1098
+ if config_instance is None:
1099
+ config_instance = load_config()
1100
+ return config_instance.stats
1072
1101
 
1073
1102
 
1074
1103
  def get_master_config():
@@ -1085,6 +1114,12 @@ def save_current_config(config):
1085
1114
  config_instance.set_config_for_profile(
1086
1115
  config_instance.current_profile, config)
1087
1116
  save_full_config(config_instance)
1117
+
1118
+
1119
+ def save_stats_config(stats_config):
1120
+ global config_instance
1121
+ config_instance.stats = stats_config
1122
+ save_full_config(config_instance)
1088
1123
 
1089
1124
 
1090
1125
  def switch_profile_and_save(profile_name):
@@ -453,7 +453,7 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
453
453
  ffmpeg_command = ffmpeg_base_command_list + [
454
454
  "-i", untrimmed_audio,
455
455
  "-ss", str(start_trim_time)]
456
- if next_line and next_line > game_line.time:
456
+ if next_line and next_line > game_line.time and total_seconds:
457
457
  end_trim_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
458
458
  end_trim_time = f"{end_trim_seconds:.3f}"
459
459
  ffmpeg_command.extend(['-to', end_trim_time])
@@ -495,9 +495,9 @@ def get_video_timings(video_path, game_line, anki_card_creation_time=None):
495
495
  total_seconds = file_length - time_delta.total_seconds()
496
496
  total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
497
497
  if total_seconds < 0 or total_seconds >= file_length:
498
- logger.error("Line mined is outside of the replay buffer! Defaulting to the beginning of the replay buffer. ")
498
+ logger.error("Line mined is outside of the replay buffer! Defaulting to the last 30 seconds of the replay buffer.")
499
499
  logger.info("Recommend either increasing replay buffer length in OBS Settings or mining faster.")
500
- return 0, 0, 0, file_length
500
+ return max(file_length - 30, 0), 0, max(file_length - 30, 0), file_length
501
501
 
502
502
  return total_seconds_after_offset, total_seconds, total_seconds_after_offset, file_length
503
503
 
@@ -1,14 +1,17 @@
1
+ import copy
1
2
  import datetime
2
3
  import re
3
4
  import csv
4
5
  import io
5
6
  from collections import defaultdict
7
+ import time
6
8
 
7
9
  import flask
8
10
  from flask import request, jsonify
11
+ import regex
9
12
 
10
13
  from GameSentenceMiner.util.db import GameLinesTable
11
- from GameSentenceMiner.util.configuration import logger, get_config, save_current_config
14
+ from GameSentenceMiner.util.configuration import get_stats_config, logger, get_config, save_current_config, save_stats_config
12
15
  from GameSentenceMiner.web.stats import (
13
16
  calculate_kanji_frequency, calculate_heatmap_data, calculate_total_chars_per_game,
14
17
  calculate_reading_time_per_game, calculate_reading_speed_per_game,
@@ -32,6 +35,7 @@ def register_database_api_routes(app):
32
35
  sort_by = request.args.get('sort', 'relevance')
33
36
  page = int(request.args.get('page', 1))
34
37
  page_size = int(request.args.get('page_size', 20))
38
+ use_regex = request.args.get('use_regex', 'false').lower() == 'true'
35
39
 
36
40
  # Validate parameters
37
41
  if not query:
@@ -41,65 +45,129 @@ def register_database_api_routes(app):
41
45
  page = 1
42
46
  if page_size < 1 or page_size > 100:
43
47
  page_size = 20
44
-
45
- # Build the SQL query
46
- base_query = f"SELECT * FROM {GameLinesTable._table} WHERE line_text LIKE ?"
47
- params = [f'%{query}%']
48
-
49
- # Add game filter if specified
50
- if game_filter:
51
- base_query += " AND game_name = ?"
52
- params.append(game_filter)
53
-
54
- # Add sorting
55
- if sort_by == 'date_desc':
56
- base_query += " ORDER BY timestamp DESC"
57
- elif sort_by == 'date_asc':
58
- base_query += " ORDER BY timestamp ASC"
59
- elif sort_by == 'game_name':
60
- base_query += " ORDER BY game_name, timestamp DESC"
61
- else: # relevance - could be enhanced with proper scoring
62
- base_query += " ORDER BY timestamp DESC"
63
-
64
- # Get total count for pagination
65
- count_query = f"SELECT COUNT(*) FROM {GameLinesTable._table} WHERE line_text LIKE ?"
66
- count_params = [f'%{query}%']
67
- if game_filter:
68
- count_query += " AND game_name = ?"
69
- count_params.append(game_filter)
70
-
71
- total_results = GameLinesTable._db.fetchone(count_query, count_params)[0]
72
-
73
- # Add pagination
74
- offset = (page - 1) * page_size
75
- base_query += f" LIMIT ? OFFSET ?"
76
- params.extend([page_size, offset])
77
-
78
- # Execute search query
79
- rows = GameLinesTable._db.fetchall(base_query, params)
80
-
81
- # Format results
82
- results = []
83
- for row in rows:
84
- game_line = GameLinesTable.from_row(row)
85
- if game_line:
86
- results.append({
87
- 'id': game_line.id,
88
- 'sentence': game_line.line_text or '',
89
- 'game_name': game_line.game_name or 'Unknown Game',
90
- 'timestamp': float(game_line.timestamp) if game_line.timestamp else 0,
91
- 'translation': game_line.translation or None,
92
- 'has_audio': bool(game_line.audio_path),
93
- 'has_screenshot': bool(game_line.screenshot_path)
94
- })
95
-
96
- return jsonify({
97
- 'results': results,
98
- 'total': total_results,
99
- 'page': page,
100
- 'page_size': page_size,
101
- 'total_pages': (total_results + page_size - 1) // page_size
102
- }), 200
48
+
49
+ if use_regex:
50
+ # Regex search: fetch all candidate rows, filter in Python
51
+ try:
52
+ # Ensure query is a string
53
+ if not isinstance(query, str):
54
+ return jsonify({'error': 'Invalid query parameter type'}), 400
55
+
56
+ all_lines = GameLinesTable.all()
57
+ if game_filter:
58
+ all_lines = [line for line in all_lines if line.game_name == game_filter]
59
+
60
+ # Compile regex pattern with proper error handling
61
+ try:
62
+ pattern = re.compile(query, re.IGNORECASE)
63
+ except re.error as regex_err:
64
+ return jsonify({'error': f'Invalid regex pattern: {str(regex_err)}'}), 400
65
+
66
+ # Filter lines using regex
67
+ filtered_lines = []
68
+ for line in all_lines:
69
+ if line.line_text and isinstance(line.line_text, str):
70
+ try:
71
+ if pattern.search(line.line_text):
72
+ filtered_lines.append(line)
73
+ except Exception as search_err:
74
+ # Log but continue with other lines
75
+ logger.warning(f"Regex search error on line {line.id}: {search_err}")
76
+ continue
77
+
78
+ # Sorting (default: timestamp DESC, or as specified)
79
+ if sort_by == 'date_asc':
80
+ filtered_lines.sort(key=lambda l: float(l.timestamp) if l.timestamp else 0)
81
+ elif sort_by == 'game_name':
82
+ filtered_lines.sort(key=lambda l: (l.game_name or '', -(float(l.timestamp) if l.timestamp else 0)))
83
+ else: # date_desc or relevance
84
+ filtered_lines.sort(key=lambda l: -(float(l.timestamp) if l.timestamp else 0))
85
+
86
+ total_results = len(filtered_lines)
87
+ # Pagination
88
+ start = (page - 1) * page_size
89
+ end = start + page_size
90
+ paged_lines = filtered_lines[start:end]
91
+ results = []
92
+ for line in paged_lines:
93
+ results.append({
94
+ 'id': line.id,
95
+ 'sentence': line.line_text or '',
96
+ 'game_name': line.game_name or 'Unknown Game',
97
+ 'timestamp': float(line.timestamp) if line.timestamp else 0,
98
+ 'translation': line.translation or None,
99
+ 'has_audio': bool(getattr(line, 'audio_path', None)),
100
+ 'has_screenshot': bool(getattr(line, 'screenshot_path', None))
101
+ })
102
+ return jsonify({
103
+ 'results': results,
104
+ 'total': total_results,
105
+ 'page': page,
106
+ 'page_size': page_size,
107
+ 'total_pages': (total_results + page_size - 1) // page_size
108
+ }), 200
109
+ except Exception as e:
110
+ logger.error(f"Regex search failed: {e}")
111
+ return jsonify({'error': f'Search failed: {str(e)}'}), 500
112
+ else:
113
+ # Build the SQL query
114
+ base_query = f"SELECT * FROM {GameLinesTable._table} WHERE line_text LIKE ?"
115
+ params = [f'%{query}%']
116
+
117
+ # Add game filter if specified
118
+ if game_filter:
119
+ base_query += " AND game_name = ?"
120
+ params.append(game_filter)
121
+
122
+ # Add sorting
123
+ if sort_by == 'date_desc':
124
+ base_query += " ORDER BY timestamp DESC"
125
+ elif sort_by == 'date_asc':
126
+ base_query += " ORDER BY timestamp ASC"
127
+ elif sort_by == 'game_name':
128
+ base_query += " ORDER BY game_name, timestamp DESC"
129
+ else: # relevance - could be enhanced with proper scoring
130
+ base_query += " ORDER BY timestamp DESC"
131
+
132
+ # Get total count for pagination
133
+ count_query = f"SELECT COUNT(*) FROM {GameLinesTable._table} WHERE line_text LIKE ?"
134
+ count_params = [f'%{query}%']
135
+ if game_filter:
136
+ count_query += " AND game_name = ?"
137
+ count_params.append(game_filter)
138
+
139
+ total_results = GameLinesTable._db.fetchone(count_query, count_params)[0]
140
+
141
+ # Add pagination
142
+ offset = (page - 1) * page_size
143
+ base_query += f" LIMIT ? OFFSET ?"
144
+ params.extend([page_size, offset])
145
+
146
+ # Execute search query
147
+ rows = GameLinesTable._db.fetchall(base_query, params)
148
+
149
+ # Format results
150
+ results = []
151
+ for row in rows:
152
+ game_line = GameLinesTable.from_row(row)
153
+ if game_line:
154
+ results.append({
155
+ 'id': game_line.id,
156
+ 'sentence': game_line.line_text or '',
157
+ 'game_name': game_line.game_name or 'Unknown Game',
158
+ 'timestamp': float(game_line.timestamp) if game_line.timestamp else 0,
159
+ 'translation': game_line.translation or None,
160
+ 'has_audio': bool(game_line.audio_path),
161
+ 'has_screenshot': bool(game_line.screenshot_path)
162
+ })
163
+
164
+ return jsonify({
165
+ 'results': results,
166
+ 'total': total_results,
167
+ 'page': page,
168
+ 'page_size': page_size,
169
+ 'total_pages': (total_results + page_size - 1) // page_size
170
+ }), 200
103
171
 
104
172
  except ValueError as e:
105
173
  return jsonify({'error': 'Invalid pagination parameters'}), 400
@@ -226,14 +294,17 @@ def register_database_api_routes(app):
226
294
  @app.route('/api/settings', methods=['GET'])
227
295
  def api_get_settings():
228
296
  """
229
- Get current AFK timer, session gap, and streak requirement settings.
297
+ Get current AFK timer, session gap, streak requirement, and goal settings.
230
298
  """
231
299
  try:
232
- config = get_config()
300
+ config = get_stats_config()
233
301
  return jsonify({
234
- 'afk_timer_seconds': config.advanced.afk_timer_seconds,
235
- 'session_gap_seconds': config.advanced.session_gap_seconds,
236
- 'streak_requirement_hours': getattr(config.advanced, 'streak_requirement_hours', 1.0)
302
+ 'afk_timer_seconds': config.afk_timer_seconds,
303
+ 'session_gap_seconds': config.session_gap_seconds,
304
+ 'streak_requirement_hours': config.streak_requirement_hours,
305
+ 'reading_hours_target': config.reading_hours_target,
306
+ 'character_count_target': config.character_count_target,
307
+ 'games_target': config.games_target
237
308
  }), 200
238
309
  except Exception as e:
239
310
  logger.error(f"Error getting settings: {e}")
@@ -242,7 +313,7 @@ def register_database_api_routes(app):
242
313
  @app.route('/api/settings', methods=['POST'])
243
314
  def api_save_settings():
244
315
  """
245
- Save/update AFK timer, session gap, and streak requirement settings.
316
+ Save/update AFK timer, session gap, streak requirement, and goal settings.
246
317
  """
247
318
  try:
248
319
  data = request.get_json()
@@ -253,6 +324,9 @@ def register_database_api_routes(app):
253
324
  afk_timer = data.get('afk_timer_seconds')
254
325
  session_gap = data.get('session_gap_seconds')
255
326
  streak_requirement = data.get('streak_requirement_hours')
327
+ reading_hours_target = data.get('reading_hours_target')
328
+ character_count_target = data.get('character_count_target')
329
+ games_target = data.get('games_target')
256
330
 
257
331
  # Validate input - only require the settings that are provided
258
332
  settings_to_update = {}
@@ -284,22 +358,54 @@ def register_database_api_routes(app):
284
358
  except (ValueError, TypeError):
285
359
  return jsonify({'error': 'Streak requirement must be a valid number'}), 400
286
360
 
361
+ if reading_hours_target is not None:
362
+ try:
363
+ reading_hours_target = int(reading_hours_target)
364
+ if reading_hours_target < 1 or reading_hours_target > 10000:
365
+ return jsonify({'error': 'Reading hours target must be between 1 and 10,000 hours'}), 400
366
+ settings_to_update['reading_hours_target'] = reading_hours_target
367
+ except (ValueError, TypeError):
368
+ return jsonify({'error': 'Reading hours target must be a valid integer'}), 400
369
+
370
+ if character_count_target is not None:
371
+ try:
372
+ character_count_target = int(character_count_target)
373
+ if character_count_target < 1000 or character_count_target > 1000000000:
374
+ return jsonify({'error': 'Character count target must be between 1,000 and 1,000,000,000 characters'}), 400
375
+ settings_to_update['character_count_target'] = character_count_target
376
+ except (ValueError, TypeError):
377
+ return jsonify({'error': 'Character count target must be a valid integer'}), 400
378
+
379
+ if games_target is not None:
380
+ try:
381
+ games_target = int(games_target)
382
+ if games_target < 1 or games_target > 1000:
383
+ return jsonify({'error': 'Games target must be between 1 and 1,000'}), 400
384
+ settings_to_update['games_target'] = games_target
385
+ except (ValueError, TypeError):
386
+ return jsonify({'error': 'Games target must be a valid integer'}), 400
387
+
287
388
  if not settings_to_update:
288
389
  return jsonify({'error': 'No valid settings provided'}), 400
289
390
 
290
391
  # Update configuration
291
- config = get_config()
392
+ config = get_stats_config()
292
393
 
293
394
  if 'afk_timer_seconds' in settings_to_update:
294
- config.advanced.afk_timer_seconds = settings_to_update['afk_timer_seconds']
395
+ config.afk_timer_seconds = settings_to_update['afk_timer_seconds']
295
396
  if 'session_gap_seconds' in settings_to_update:
296
- config.advanced.session_gap_seconds = settings_to_update['session_gap_seconds']
397
+ config.session_gap_seconds = settings_to_update['session_gap_seconds']
297
398
  if 'streak_requirement_hours' in settings_to_update:
298
- setattr(config.advanced, 'streak_requirement_hours', settings_to_update['streak_requirement_hours'])
299
-
300
- # Save configuration
301
- save_current_config(config)
302
-
399
+ config.streak_requirement_hours = settings_to_update['streak_requirement_hours']
400
+ if 'reading_hours_target' in settings_to_update:
401
+ config.reading_hours_target = settings_to_update['reading_hours_target']
402
+ if 'character_count_target' in settings_to_update:
403
+ config.character_count_target = settings_to_update['character_count_target']
404
+ if 'games_target' in settings_to_update:
405
+ config.games_target = settings_to_update['games_target']
406
+
407
+ save_stats_config(config)
408
+
303
409
  logger.info(f"Settings updated: {settings_to_update}")
304
410
 
305
411
  response_data = {'message': 'Settings saved successfully'}
@@ -685,6 +791,7 @@ def register_database_api_routes(app):
685
791
  Provides aggregated, cumulative stats for charting.
686
792
  Accepts optional 'year' parameter to filter heatmap data.
687
793
  """
794
+ punctionation_regex = regex.compile(r'[\p{P}\p{S}\p{Z}]')
688
795
  # Get optional year filter parameter
689
796
  filter_year = request.args.get('year', None)
690
797
 
@@ -697,13 +804,33 @@ def register_database_api_routes(app):
697
804
  # 2. Process data into daily totals for each game
698
805
  # Structure: daily_data[date_str][game_name] = {'lines': N, 'chars': N}
699
806
  daily_data = defaultdict(lambda: defaultdict(lambda: {'lines': 0, 'chars': 0}))
807
+
808
+
809
+ # start_time = time.perf_counter()
810
+ # for line in all_lines:
811
+ # day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
812
+ # game = line.game_name or "Unknown Game"
813
+ # daily_data[day_str][game]['lines'] += 1
814
+ # daily_data[day_str][game]['chars'] += len(line.line_text) if line.line_text else 0
815
+ # end_time = time.perf_counter()
816
+ # logger.info(f"Without Punctuation removal and daily aggregation took {end_time - start_time:.4f} seconds for {len(all_lines)} lines")
700
817
 
818
+ # start_time = time.perf_counter()
819
+ wrong_instance_found = False
701
820
  for line in all_lines:
702
821
  day_str = datetime.date.fromtimestamp(float(line.timestamp)).strftime('%Y-%m-%d')
703
822
  game = line.game_name or "Unknown Game"
704
-
823
+ # Remove punctuation and symbols from line text before counting characters
824
+ clean_text = punctionation_regex.sub('', str(line.line_text)) if line.line_text else ''
825
+ if not isinstance(clean_text, str) and not wrong_instance_found:
826
+ logger.info(f"Non-string line_text encountered: {clean_text} (type: {type(clean_text)})")
827
+ wrong_instance_found = True
828
+
829
+ line.line_text = clean_text # Update line text to cleaned version for future use
705
830
  daily_data[day_str][game]['lines'] += 1
706
- daily_data[day_str][game]['chars'] += len(line.line_text) if line.line_text else 0
831
+ daily_data[day_str][game]['chars'] += len(clean_text)
832
+ # end_time = time.perf_counter()
833
+ # logger.info(f"With Punctuation removal and daily aggregation took {end_time - start_time:.4f} seconds for {len(all_lines)} lines")
707
834
 
708
835
  # 3. Create cumulative datasets for Chart.js
709
836
  sorted_days = sorted(daily_data.keys())