GameSentenceMiner 2.17.7__py3-none-any.whl → 2.18.1__py3-none-any.whl

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.

Potentially problematic release.


This version of GameSentenceMiner might be problematic. Click here for more details.

Files changed (76) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +6 -6
  2. GameSentenceMiner/anki.py +236 -152
  3. GameSentenceMiner/gametext.py +7 -4
  4. GameSentenceMiner/gsm.py +49 -10
  5. GameSentenceMiner/locales/en_us.json +7 -3
  6. GameSentenceMiner/locales/ja_jp.json +8 -4
  7. GameSentenceMiner/locales/zh_cn.json +8 -4
  8. GameSentenceMiner/obs.py +238 -59
  9. GameSentenceMiner/ocr/owocr_helper.py +1 -1
  10. GameSentenceMiner/tools/ss_selector.py +7 -8
  11. GameSentenceMiner/ui/__init__.py +0 -0
  12. GameSentenceMiner/ui/anki_confirmation.py +187 -0
  13. GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
  14. GameSentenceMiner/ui/screenshot_selector.py +215 -0
  15. GameSentenceMiner/util/configuration.py +124 -22
  16. GameSentenceMiner/util/db.py +22 -13
  17. GameSentenceMiner/util/downloader/download_tools.py +2 -2
  18. GameSentenceMiner/util/ffmpeg.py +24 -30
  19. GameSentenceMiner/util/get_overlay_coords.py +34 -34
  20. GameSentenceMiner/util/gsm_utils.py +31 -1
  21. GameSentenceMiner/util/text_log.py +11 -9
  22. GameSentenceMiner/vad.py +31 -12
  23. GameSentenceMiner/web/database_api.py +742 -123
  24. GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
  25. GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
  26. GameSentenceMiner/web/static/css/overview.css +850 -0
  27. GameSentenceMiner/web/static/css/popups-shared.css +126 -0
  28. GameSentenceMiner/web/static/css/shared.css +97 -0
  29. GameSentenceMiner/web/static/css/stats.css +192 -597
  30. GameSentenceMiner/web/static/js/anki_stats.js +6 -4
  31. GameSentenceMiner/web/static/js/database.js +209 -5
  32. GameSentenceMiner/web/static/js/goals.js +610 -0
  33. GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
  34. GameSentenceMiner/web/static/js/overview.js +1176 -0
  35. GameSentenceMiner/web/static/js/shared.js +25 -0
  36. GameSentenceMiner/web/static/js/stats.js +154 -1459
  37. GameSentenceMiner/web/stats.py +2 -2
  38. GameSentenceMiner/web/templates/anki_stats.html +5 -0
  39. GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
  40. GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
  41. GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
  42. GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
  43. GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
  44. GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
  45. GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
  46. GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
  47. GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
  48. GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
  49. GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
  50. GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
  51. GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
  52. GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
  53. GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
  54. GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
  55. GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
  56. GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
  57. GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
  58. GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
  59. GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
  60. GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
  61. GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
  62. GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
  63. GameSentenceMiner/web/templates/components/navigation.html +3 -1
  64. GameSentenceMiner/web/templates/database.html +73 -1
  65. GameSentenceMiner/web/templates/goals.html +376 -0
  66. GameSentenceMiner/web/templates/index.html +13 -11
  67. GameSentenceMiner/web/templates/overview.html +416 -0
  68. GameSentenceMiner/web/templates/stats.html +46 -251
  69. GameSentenceMiner/web/texthooking_page.py +18 -0
  70. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
  71. gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
  72. gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
  73. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
  74. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
  75. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
  76. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/top_level.txt +0 -0
@@ -178,7 +178,7 @@ class OpenAIManager(AIManager):
178
178
  try:
179
179
  prompt = self._build_prompt(
180
180
  lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
181
- self.logger.debug(f"Generated prompt:\n{prompt}")
181
+ # self.logger.debug(f"Generated prompt:\n{prompt}")
182
182
  # Try with full parameters first, fallback to basic parameters if model doesn't support them
183
183
  if self.extra_params_allowed:
184
184
  try:
@@ -216,7 +216,7 @@ class OpenAIManager(AIManager):
216
216
  json_output = text_output[text_output.find(
217
217
  "{"):text_output.rfind("}")+1]
218
218
  text_output = json.loads(json_output)['output']
219
- self.logger.debug(f"Received response:\n{text_output}")
219
+ # self.logger.debug(f"Received response:\n{text_output}")
220
220
  return text_output
221
221
  except Exception as e:
222
222
  self.logger.error(f"OpenAI processing failed: {e}")
@@ -263,6 +263,7 @@ class GeminiAI(AIManager):
263
263
  def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str, custom_prompt=None) -> str:
264
264
  prompt = super()._build_prompt(lines, sentence, current_line,
265
265
  game_title, custom_prompt=custom_prompt)
266
+ # self.logger.debug(f"Built prompt:\n{prompt}")
266
267
  return prompt
267
268
 
268
269
  def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", custom_prompt=None) -> str:
@@ -285,13 +286,12 @@ class GeminiAI(AIManager):
285
286
  ],
286
287
  ),
287
288
  ]
288
- self.logger.debug(f"Generated prompt:\n{prompt}")
289
289
  response = self.client.models.generate_content(
290
290
  model=self.model_name,
291
291
  contents=contents,
292
292
  config=self.generation_config
293
293
  )
294
- self.logger.debug(f"Full response: {response}")
294
+ # self.logger.debug(f"Full response: {response}")
295
295
  result = response.text.strip()
296
296
  self.logger.debug(f"Received response:\n{result}")
297
297
  return result
@@ -304,6 +304,7 @@ class GroqAI(AIManager):
304
304
  def __init__(self, model, api_key, logger: Optional[logging.Logger] = None):
305
305
  super().__init__(GroqAiConfig(model=model, api_key=api_key), logger)
306
306
  self.api_key = self.ai_config.api_key
307
+ self.model_name = model
307
308
  try:
308
309
  self.client = Groq(api_key=self.api_key)
309
310
  self.logger.debug(
@@ -329,7 +330,6 @@ class GroqAI(AIManager):
329
330
  try:
330
331
  prompt = self._build_prompt(
331
332
  lines, sentence, current_line, game_title, custom_prompt=custom_prompt)
332
- self.logger.debug(f"Generated prompt:\n{prompt}")
333
333
  completion = self.client.chat.completions.create(
334
334
  model=self.model_name,
335
335
  messages=[{"role": "user", "content": prompt}],
@@ -340,7 +340,7 @@ class GroqAI(AIManager):
340
340
  stop=None,
341
341
  )
342
342
  result = completion.choices[0].message.content.strip()
343
- self.logger.debug(f"Received response:\n{result}")
343
+ # self.logger.debug(f"Received response:\n{result}")
344
344
  return result
345
345
  except Exception as e:
346
346
  self.logger.error(f"Groq processing failed: {e}")
GameSentenceMiner/anki.py CHANGED
@@ -1,10 +1,8 @@
1
- import copy
2
1
  import json
3
2
  import os
4
3
  import shutil
5
4
  import threading
6
5
  from pathlib import Path
7
- import queue
8
6
  import time
9
7
 
10
8
  import base64
@@ -22,189 +20,181 @@ from GameSentenceMiner.util import ffmpeg, notification
22
20
  from GameSentenceMiner.util.configuration import get_config, AnkiUpdateResult, logger, anki_results, gsm_status, \
23
21
  gsm_state
24
22
  from GameSentenceMiner.util.model import AnkiCard
25
- from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_mined_line, lines_match
23
+ from GameSentenceMiner.util.text_log import GameLine, get_all_lines, get_text_event, get_mined_line, lines_match
26
24
  from GameSentenceMiner.obs import get_current_game
27
25
  from GameSentenceMiner.web import texthooking_page
26
+ from GameSentenceMiner.ui.config_gui import ConfigApp
28
27
  import re
29
28
  import platform
30
29
  import sys
31
30
 
31
+ from dataclasses import dataclass
32
+ from typing import Dict, Any, List
33
+
32
34
  # Global variables to track state
33
35
  previous_note_ids = set()
34
36
  first_run = True
35
37
  card_queue = []
36
38
 
37
39
 
38
- def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
39
- should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0, start_time=None, end_time=None, vad_result=None):
40
- update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
41
- last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
42
- update_picture = (get_config().anki.picture_field and get_config().screenshot.enabled
43
- and (get_config().anki.overwrite_picture or not last_note.get_field(get_config().anki.picture_field)))
44
-
45
- audio_in_anki = ''
46
- screenshot_in_anki = ''
47
- prev_screenshot_in_anki = ''
48
- video_in_anki = ''
49
- video = ''
50
- screenshot = ''
51
- prev_screenshot = ''
52
- if reuse_audio:
53
- logger.info("Reusing Audio from last note")
54
- anki_result: AnkiUpdateResult = anki_results[game_line.id]
55
- audio_in_anki = anki_result.audio_in_anki
56
- screenshot_in_anki = anki_result.screenshot_in_anki
57
- prev_screenshot_in_anki = anki_result.prev_screenshot_in_anki
58
- video_in_anki = anki_result.video_in_anki
59
- else:
60
- if update_audio:
61
- audio_in_anki = store_media_file(audio_path)
62
- if get_config().audio.external_tool and get_config().audio.external_tool_enabled:
63
- open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
64
- if update_picture:
65
- logger.info("Getting Screenshot...")
66
- if get_config().screenshot.animated:
67
- screenshot = ffmpeg.get_anki_compatible_video(video_path, start_time, vad_result.start, vad_result.end, codec='avif', quality=10, fps=12, audio=False)
68
- else:
69
- screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
70
- wait_for_stable_file(screenshot)
71
- screenshot_in_anki = store_media_file(screenshot)
72
- if get_config().anki.video_field:
73
- if vad_result:
74
- video = ffmpeg.get_anki_compatible_video(video_path, start_time, vad_result.start, vad_result.end, codec='avif', quality=10, fps=12, audio=True)
75
- video_in_anki = store_media_file(video)
76
- if get_config().anki.previous_image_field and game_line.prev:
77
- prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev, try_selector=get_config().screenshot.use_screenshot_selector)
78
- wait_for_stable_file(prev_screenshot)
79
- prev_screenshot_in_anki = store_media_file(prev_screenshot)
80
- if get_config().paths.remove_screenshot:
81
- os.remove(prev_screenshot)
82
- audio_html = f"[sound:{audio_in_anki}]"
83
- image_html = f"<img src=\"{screenshot_in_anki}\">"
84
- prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
40
+ @dataclass
41
+ class MediaAssets:
42
+ """A simple container for media file paths and their Anki names."""
43
+ # Local temporary paths
44
+ audio_path: str = ''
45
+ screenshot_path: str = ''
46
+ prev_screenshot_path: str = ''
47
+ video_path: str = ''
48
+
49
+ # Filenames after being stored in Anki's media collection
50
+ audio_in_anki: str = ''
51
+ screenshot_in_anki: str = ''
52
+ prev_screenshot_in_anki: str = ''
53
+ video_in_anki: str = ''
54
+
55
+ # Paths after being copied to the final output folder
56
+ final_audio_path: str = ''
57
+ final_screenshot_path: str = ''
58
+ final_prev_screenshot_path: str = ''
59
+ final_video_path: str = ''
60
+
61
+
62
+ def _determine_update_conditions(last_note: 'AnkiCard') -> (bool, bool):
63
+ """Determine if audio and picture fields should be updated."""
64
+ config = get_config()
65
+ update_audio = (config.anki.sentence_audio_field and
66
+ (not last_note.get_field(config.anki.sentence_audio_field) or config.anki.overwrite_audio))
67
+
68
+ update_picture = (config.anki.picture_field and config.screenshot.enabled and
69
+ (not last_note.get_field(config.anki.picture_field) or config.anki.overwrite_picture))
70
+
71
+ return update_audio, update_picture
85
72
 
86
73
 
87
- # note = {'id': last_note.noteId, 'fields': {}}
74
+ def _generate_media_files(reuse_audio: bool, game_line: 'GameLine', video_path: str, ss_time: float, start_time: float, vad_result: Any, selected_lines: List['GameLine']) -> MediaAssets:
75
+ """Generates or retrieves paths for all media assets (audio, video, screenshots)."""
76
+ assets = MediaAssets()
77
+ config = get_config()
88
78
 
89
- if update_audio and audio_in_anki:
90
- note['fields'][get_config().anki.sentence_audio_field] = audio_html
79
+ if reuse_audio:
80
+ logger.info("Reusing media from last note")
81
+ anki_result: 'AnkiUpdateResult' = anki_results[game_line.id]
82
+ assets.audio_in_anki = anki_result.audio_in_anki
83
+ assets.screenshot_in_anki = anki_result.screenshot_in_anki
84
+ assets.prev_screenshot_in_anki = anki_result.prev_screenshot_in_anki
85
+ assets.video_in_anki = anki_result.video_in_anki
86
+ return assets
87
+
88
+ # --- Generate new media files ---
89
+ if config.anki.picture_field and config.screenshot.enabled:
90
+ logger.info("Getting Screenshot...")
91
+ if config.screenshot.animated:
92
+ assets.screenshot_path = ffmpeg.get_anki_compatible_video(
93
+ video_path, start_time, vad_result.start, vad_result.end,
94
+ codec='avif', quality=10, fps=12, audio=False
95
+ )
96
+ else:
97
+ assets.screenshot_path = ffmpeg.get_screenshot(
98
+ video_path, ss_time, try_selector=config.screenshot.use_screenshot_selector
99
+ )
100
+ wait_for_stable_file(assets.screenshot_path)
101
+
102
+ if config.anki.video_field and vad_result:
103
+ assets.video_path = ffmpeg.get_anki_compatible_video(
104
+ video_path, start_time, vad_result.start, vad_result.end,
105
+ codec='avif', quality=10, fps=12, audio=True
106
+ )
107
+
108
+ if config.anki.previous_image_field and game_line.prev:
109
+ if anki_results.get(game_line.prev.id):
110
+ assets.prev_screenshot_in_anki = anki_results.get(game_line.prev.id).screenshot_in_anki
111
+ else:
112
+ line_for_prev_ss = selected_lines[0].prev if selected_lines else game_line.prev
113
+ assets.prev_screenshot_path = ffmpeg.get_screenshot_for_line(
114
+ video_path, line_for_prev_ss, try_selector=config.screenshot.use_screenshot_selector
115
+ )
116
+ wait_for_stable_file(assets.prev_screenshot_path)
117
+ assets.prev_screenshot_in_anki = store_media_file(assets.prev_screenshot_path)
118
+ if config.paths.remove_screenshot:
119
+ os.remove(assets.prev_screenshot_path)
120
+
121
+ return assets
91
122
 
92
- if update_picture and screenshot_in_anki:
93
- note['fields'][get_config().anki.picture_field] = image_html
94
123
 
95
- if video_in_anki:
96
- note['fields'][get_config().anki.video_field] = video_in_anki
124
+ def _prepare_anki_note_fields(note: Dict, last_note: 'AnkiCard', assets: MediaAssets, game_line: 'GameLine') -> Dict:
125
+ """Populates the fields of the Anki note dictionary."""
126
+ config = get_config()
127
+
128
+ if assets.video_in_anki:
129
+ note['fields'][config.anki.video_field] = assets.video_in_anki
97
130
 
98
- if not get_config().screenshot.enabled:
99
- logger.info("Skipping Adding Screenshot to Anki, Screenshot is disabled in settings")
131
+ if assets.prev_screenshot_in_anki and config.anki.previous_image_field != config.anki.picture_field:
132
+ note['fields'][config.anki.previous_image_field] = f"<img src=\"{assets.prev_screenshot_in_anki}\">"
100
133
 
101
- # Add game name to field if configured
102
- game_name_field = get_config().anki.game_name_field
103
- if note and 'fields' in note and game_name_field:
134
+ if game_name_field := config.anki.game_name_field:
104
135
  note['fields'][game_name_field] = get_current_game()
105
136
 
106
- if note and 'fields' in note and get_config().ai.enabled:
107
- sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
108
- sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
109
- get_config().anki.sentence_field)
110
- translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate,
111
- game_line, get_current_game())
112
- game_line.TL = translation
137
+ if config.ai.enabled:
138
+ sentence_field = note['fields'].get(config.anki.sentence_field, {})
139
+ sentence_to_translate = sentence_field or last_note.get_field(config.anki.sentence_field)
140
+ translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate, game_line, get_current_game())
141
+ game_line.TL = translation # Side-effect: updates game_line object
113
142
  logger.info(f"AI prompt Result: {translation}")
114
- note['fields'][get_config().ai.anki_field] = translation
115
-
116
- if prev_screenshot_in_anki and get_config().anki.previous_image_field != get_config().anki.picture_field:
117
- note['fields'][get_config().anki.previous_image_field] = prev_screenshot_html
143
+ note['fields'][config.ai.anki_field] = translation
144
+
145
+ return note
118
146
 
119
147
 
148
+ def _prepare_anki_tags() -> List[str]:
149
+ """Generates a list of tags to be added to the Anki note."""
150
+ config = get_config()
120
151
  tags = []
121
- if get_config().anki.add_game_tag:
152
+ if config.anki.add_game_tag:
122
153
  game = get_current_game().replace(" ", "").replace("::", "")
123
- if get_config().anki.parent_tag:
124
- game = f"{get_config().anki.parent_tag}::{game}"
154
+ if config.anki.parent_tag:
155
+ game = f"{config.anki.parent_tag}::{game}"
125
156
  tags.append(game)
126
- if get_config().anki.custom_tags:
127
- tags.extend(get_config().anki.custom_tags)
128
- if tags:
129
- tag_string = " ".join(tags)
130
- invoke("addTags", tags=tag_string, notes=[last_note.noteId])
131
-
132
- run_new_thread(lambda: check_and_update_note(last_note, note, tags))
157
+ if config.anki.custom_tags:
158
+ tags.extend(config.anki.custom_tags)
159
+ return tags
133
160
 
134
- word_path = os.path.join(get_config().paths.output_folder, sanitize_filename(tango)) if get_config().paths.output_folder else ''
135
- if not reuse_audio:
136
- anki_results[game_line.id] = AnkiUpdateResult(
137
- success=True,
138
- audio_in_anki=audio_in_anki,
139
- screenshot_in_anki=screenshot_in_anki,
140
- prev_screenshot_in_anki=prev_screenshot_in_anki,
141
- sentence_in_anki=game_line.text if game_line else '',
142
- multi_line=bool(selected_lines and len(selected_lines) > 1),
143
- video_in_anki=video_in_anki or '',
144
- word_path=word_path
145
- )
146
- # Update GameLine in DB
161
+
162
+ def _handle_file_management(tango: str, reuse_audio: bool, game_line: 'GameLine', assets: MediaAssets, video_path: str, start_time: float, end_time: float):
163
+ """Copies temporary media files to the final output folder if configured."""
164
+ config = get_config()
165
+ if not config.paths.output_folder:
166
+ return
167
+
168
+ word_path = os.path.join(config.paths.output_folder, sanitize_filename(tango))
169
+ os.makedirs(word_path, exist_ok=True)
147
170
 
148
- # Vars for DB update
149
- new_audio_path = ''
150
- new_screenshot_path = ''
151
- new_prev_screenshot_path = ''
152
- new_video_path = ''
153
- translation = ''
154
- anki_audio_path = ''
155
- anki_screenshot_path = ''
156
- # Move files to output folder if configured
157
- if get_config().paths.output_folder and reuse_audio:
158
- anki_result: AnkiUpdateResult = anki_results[game_line.id]
171
+ if reuse_audio:
172
+ # If reusing, copy all files from the original word's folder
173
+ anki_result: 'AnkiUpdateResult' = anki_results[game_line.id]
159
174
  previous_word_path = anki_result.word_path
160
175
  if previous_word_path and os.path.exists(previous_word_path):
161
- os.makedirs(word_path, exist_ok=True)
162
- # Copy all files from previous_word_path to word_path
163
- for item in os.listdir(previous_word_path):
164
- s = os.path.join(previous_word_path, item)
165
- d = os.path.join(word_path, item)
166
- if os.path.isdir(s):
167
- shutil.copytree(s, d, False, None)
168
- else:
169
- shutil.copy2(s, d)
170
- elif get_config().paths.output_folder and get_config().paths.copy_temp_files_to_output_folder:
171
- os.makedirs(word_path, exist_ok=True)
172
- if audio_path:
173
- audio_filename = Path(audio_path).name
174
- new_audio_path = os.path.join(word_path, audio_filename)
175
- if os.path.exists(audio_path):
176
- shutil.copy(audio_path, new_audio_path)
177
- if screenshot:
178
- screenshot_filename = Path(screenshot).name
179
- new_screenshot_path = os.path.join(word_path, screenshot_filename)
180
- if os.path.exists(screenshot):
181
- shutil.copy(screenshot, new_screenshot_path)
182
- if prev_screenshot:
183
- prev_screenshot_filename = Path(prev_screenshot).name
184
- new_prev_screenshot_path = os.path.join(word_path, "prev_" + prev_screenshot_filename)
185
- if os.path.exists(prev_screenshot):
186
- shutil.copy(prev_screenshot, new_prev_screenshot_path)
187
-
188
- if video_path and get_config().paths.copy_trimmed_replay_to_output_folder:
176
+ shutil.copytree(previous_word_path, word_path, dirs_exist_ok=True)
177
+ elif config.paths.copy_temp_files_to_output_folder:
178
+ # If creating new, copy generated files to the new word's folder
179
+ if assets.audio_path and os.path.exists(assets.audio_path):
180
+ assets.final_audio_path = shutil.copy(assets.audio_path, word_path)
181
+ if assets.screenshot_path and os.path.exists(assets.screenshot_path):
182
+ assets.final_screenshot_path = shutil.copy(assets.screenshot_path, word_path)
183
+ if assets.prev_screenshot_path and os.path.exists(assets.prev_screenshot_path):
184
+ dest_name = "prev_" + Path(assets.prev_screenshot_path).name
185
+ assets.final_prev_screenshot_path = shutil.copy(assets.prev_screenshot_path, os.path.join(word_path, dest_name))
186
+ if assets.video_path and os.path.exists(assets.video_path):
187
+ assets.final_video_path = shutil.copy(assets.video_path, word_path)
188
+ elif video_path and config.paths.copy_trimmed_replay_to_output_folder:
189
189
  trimmed_video = ffmpeg.trim_replay_for_gameline(video_path, start_time, end_time, accurate=True)
190
- new_video_path = os.path.join(word_path, Path(trimmed_video).name)
191
190
  if os.path.exists(trimmed_video):
192
- shutil.copy(trimmed_video, new_video_path)
193
-
194
- if video:
195
- new_video_path = os.path.join(word_path, Path(video).name)
196
- if os.path.exists(video):
197
- shutil.copy(video, new_video_path)
191
+ assets.final_video_path = shutil.copy(trimmed_video, word_path)
198
192
 
199
- if get_config().audio.anki_media_collection:
200
- anki_audio_path = os.path.join(get_config().audio.anki_media_collection, audio_in_anki)
201
- anki_screenshot_path = os.path.join(get_config().audio.anki_media_collection, screenshot_in_anki)
202
-
203
- # Open to word_path if configured
204
- if get_config().paths.open_output_folder_on_card_creation:
193
+ # Open folder if configured
194
+ if config.paths.open_output_folder_on_card_creation:
205
195
  try:
206
196
  if platform.system() == "Windows":
207
- subprocess.Popen(f'explorer "{word_path}"')
197
+ os.startfile(word_path)
208
198
  elif platform.system() == "Darwin":
209
199
  subprocess.Popen(["open", word_path])
210
200
  else:
@@ -212,15 +202,109 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
212
202
  except Exception as e:
213
203
  logger.error(f"Error opening output folder: {e}")
214
204
 
215
- logger.info(f"Adding {game_line.id} to Anki Results Dict...")
205
+ # Return word_path for storing in AnkiUpdateResult
206
+ return word_path
207
+
208
+
209
+ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path='', tango='', use_existing_files=False,
210
+ should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0, start_time=None, end_time=None, vad_result=None):
211
+ """
212
+ Main function to handle the entire process of updating an Anki card with new media and data.
213
+ """
214
+ config = get_config()
215
+
216
+ # 1. Decide what to update based on config and existing note state
217
+ update_audio_flag, update_picture_flag = _determine_update_conditions(last_note)
218
+ update_audio_flag = update_audio_flag and should_update_audio
219
+
220
+ # 2. Generate or retrieve all necessary media files
221
+ assets = _generate_media_files(use_existing_files, game_line, video_path, ss_time, start_time, vad_result, selected_lines)
222
+ assets.audio_path = audio_path # Assign the passed audio path
223
+
224
+ # 3. Prepare the basic structure of the Anki note and its tags
225
+ note = note or {'id': last_note.noteId, 'fields': {}}
226
+ note = _prepare_anki_note_fields(note, last_note, assets, game_line)
227
+ tags = _prepare_anki_tags()
228
+
229
+ # 4. (Optional) Show confirmation dialog to the user, which may alter media
230
+ use_voice = True
231
+ translation = game_line.TL if hasattr(game_line, 'TL') else ''
232
+ if config.anki.show_update_confirmation_dialog and not use_existing_files:
233
+ config_app: 'ConfigApp' = gsm_state.config_app
234
+ sentence = note['fields'].get(config.anki.sentence_field, last_note.get_field(config.anki.sentence_field))
235
+
236
+ use_voice, sentence, translation, new_ss_path = config_app.show_anki_confirmation_dialog(
237
+ tango, sentence, assets.screenshot_path, assets.audio_path, translation, ss_time
238
+ )
239
+ note['fields'][config.anki.sentence_field] = sentence
240
+ note['fields'][config.ai.anki_field] = translation
241
+ assets.screenshot_path = new_ss_path or assets.screenshot_path
242
+
243
+ # 5. If creating new media, store files in Anki's collection. Then update note fields.
244
+ if not use_existing_files:
245
+ # Only store new files in Anki if we are not reusing existing ones.
246
+ if assets.video_path:
247
+ assets.video_in_anki = store_media_file(assets.video_path)
248
+ if assets.screenshot_path:
249
+ assets.screenshot_in_anki = store_media_file(assets.screenshot_path)
250
+ if update_audio_flag and use_voice and assets.audio_path:
251
+ assets.audio_in_anki = store_media_file(assets.audio_path)
252
+
253
+ # Now, update the note fields using the Anki filenames (either from cache or newly stored)
254
+ if assets.video_in_anki:
255
+ note['fields'][config.anki.video_field] = assets.video_in_anki
256
+
257
+ if update_picture_flag and assets.screenshot_in_anki:
258
+ note['fields'][config.anki.picture_field] = f"<img src=\"{assets.screenshot_in_anki}\">"
216
259
 
217
- GameLinesTable.update(line_id=game_line.id, screenshot_path=new_screenshot_path, audio_path=new_audio_path, replay_path=new_video_path, audio_in_anki=anki_audio_path, screenshot_in_anki=anki_screenshot_path, translation=translation)
260
+ if update_audio_flag and use_voice and assets.audio_in_anki:
261
+ note['fields'][config.anki.sentence_audio_field] = f"[sound:{assets.audio_in_anki}]"
262
+ if config.audio.external_tool and config.audio.external_tool_enabled:
263
+ anki_media_audio_path = os.path.join(config.audio.anki_media_collection, assets.audio_in_anki)
264
+ open_audio_in_external(anki_media_audio_path)
218
265
 
266
+ # 6. Asynchronously update the note in Anki
267
+ run_new_thread(lambda: check_and_update_note(last_note, note, tags))
268
+
269
+ # 7. Handle post-creation file management (copying to output folder)
270
+ word_path = _handle_file_management(tango, use_existing_files, game_line, assets, video_path, start_time, end_time)
271
+
272
+ # 8. Cache the result for potential reuse (e.g., for 'previous screenshot')
273
+ if not use_existing_files:
274
+ anki_results[game_line.id] = AnkiUpdateResult(
275
+ success=True,
276
+ audio_in_anki=assets.audio_in_anki,
277
+ screenshot_in_anki=assets.screenshot_in_anki,
278
+ prev_screenshot_in_anki=assets.prev_screenshot_in_anki,
279
+ sentence_in_anki=game_line.text if game_line else '',
280
+ multi_line=bool(selected_lines and len(selected_lines) > 1),
281
+ video_in_anki=assets.video_in_anki or '',
282
+ word_path=word_path
283
+ )
284
+
285
+ # 9. Update the local application database with final paths
286
+ anki_audio_path = os.path.join(config.audio.anki_media_collection, assets.audio_in_anki) if assets.audio_in_anki else ''
287
+ anki_screenshot_path = os.path.join(config.audio.anki_media_collection, assets.screenshot_in_anki) if assets.screenshot_in_anki else ''
288
+
289
+ GameLinesTable.update(
290
+ line_id=game_line.id,
291
+ screenshot_path=assets.final_screenshot_path,
292
+ audio_path=assets.final_audio_path,
293
+ replay_path=assets.final_video_path,
294
+ audio_in_anki=anki_audio_path,
295
+ screenshot_in_anki=anki_screenshot_path,
296
+ translation=translation
297
+ )
298
+
219
299
  def check_and_update_note(last_note, note, tags=[]):
220
300
  selected_notes = invoke("guiSelectedNotes")
221
301
  if last_note.noteId in selected_notes:
222
302
  notification.open_browser_window(1)
223
303
  invoke("updateNoteFields", note=note)
304
+
305
+ if tags:
306
+ tag_string = " ".join(tags)
307
+ invoke("addTags", tags=tag_string, notes=[last_note.noteId])
224
308
 
225
309
  logger.info(f"UPDATED ANKI CARD FOR {last_note.noteId}")
226
310
  if last_note.noteId in selected_notes or get_config().features.open_anki_in_browser:
@@ -500,7 +584,7 @@ def update_card_from_same_sentence(last_card, lines, game_line):
500
584
  note, last_card = get_initial_card_info(last_card, lines)
501
585
  tango = last_card.get_field(get_config().anki.word_field)
502
586
  update_anki_card(last_card, note=note,
503
- game_line=get_mined_line(last_card, lines), reuse_audio=True, tango=tango)
587
+ game_line=get_mined_line(last_card, lines), use_existing_files=True, tango=tango)
504
588
  else:
505
589
  logger.error(f"Anki update failed for card {last_card.noteId}")
506
590
  notification.send_error_no_anki_update()
@@ -9,10 +9,11 @@ import websockets
9
9
  from websockets import InvalidStatus
10
10
  from rapidfuzz import fuzz
11
11
 
12
- from GameSentenceMiner.util.configuration import get_config, gsm_status, logger, gsm_state
12
+ from GameSentenceMiner.util.configuration import get_config, gsm_status, logger, gsm_state, is_dev
13
13
  from GameSentenceMiner.util.db import GameLinesTable
14
14
  from GameSentenceMiner.util.gsm_utils import do_text_replacements, TEXT_REPLACEMENTS_FILE, run_new_thread
15
15
  from GameSentenceMiner import obs
16
+ from GameSentenceMiner.util.gsm_utils import add_srt_line
16
17
  from GameSentenceMiner.util.text_log import add_line, get_text_log
17
18
  from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker, overlay_server_thread
18
19
 
@@ -87,7 +88,8 @@ async def listen_websockets():
87
88
  message_received_time = datetime.now()
88
89
  if not message:
89
90
  continue
90
- logger.debug(message)
91
+ if is_dev:
92
+ logger.debug(message)
91
93
  try:
92
94
  data = json.loads(message)
93
95
  if "sentence" in data:
@@ -183,6 +185,7 @@ async def handle_new_text_event(current_clipboard, line_time=None):
183
185
 
184
186
  async def add_line_to_text_log(line, line_time=None):
185
187
  global overlay_processor
188
+
186
189
  if get_config().general.texthook_replacement_regex:
187
190
  current_line_after_regex = re.sub(get_config().general.texthook_replacement_regex, '', line)
188
191
  else:
@@ -191,7 +194,7 @@ async def add_line_to_text_log(line, line_time=None):
191
194
  logger.info(f"Line Received: {current_line_after_regex}")
192
195
  current_line_time = line_time if line_time else datetime.now()
193
196
  gsm_status.last_line_received = current_line_time.strftime("%Y-%m-%d %H:%M:%S")
194
- add_line(current_line_after_regex, line_time if line_time else datetime.now())
197
+ new_line = add_line(current_line_after_regex, line_time if line_time else datetime.now())
195
198
  if len(get_text_log().values) > 0:
196
199
  await add_event_to_texthooker(get_text_log()[-1])
197
200
  if get_config().overlay.websocket_port and overlay_server_thread.has_clients():
@@ -199,9 +202,9 @@ async def add_line_to_text_log(line, line_time=None):
199
202
  overlay_processor = OverlayProcessor()
200
203
  if overlay_processor.ready:
201
204
  await overlay_processor.find_box_and_send_to_overlay(current_line_after_regex)
205
+ add_srt_line(line_time, new_line)
202
206
  GameLinesTable.add_line(get_text_log()[-1])
203
207
 
204
-
205
208
  def reset_line_hotkey_pressed():
206
209
  global current_line_time
207
210
  logger.info("LINE RESET HOTKEY PRESSED")