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.
- GameSentenceMiner/ai/ai_prompting.py +6 -6
- GameSentenceMiner/anki.py +236 -152
- GameSentenceMiner/gametext.py +7 -4
- GameSentenceMiner/gsm.py +49 -10
- GameSentenceMiner/locales/en_us.json +7 -3
- GameSentenceMiner/locales/ja_jp.json +8 -4
- GameSentenceMiner/locales/zh_cn.json +8 -4
- GameSentenceMiner/obs.py +238 -59
- GameSentenceMiner/ocr/owocr_helper.py +1 -1
- GameSentenceMiner/tools/ss_selector.py +7 -8
- GameSentenceMiner/ui/__init__.py +0 -0
- GameSentenceMiner/ui/anki_confirmation.py +187 -0
- GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
- GameSentenceMiner/ui/screenshot_selector.py +215 -0
- GameSentenceMiner/util/configuration.py +124 -22
- GameSentenceMiner/util/db.py +22 -13
- GameSentenceMiner/util/downloader/download_tools.py +2 -2
- GameSentenceMiner/util/ffmpeg.py +24 -30
- GameSentenceMiner/util/get_overlay_coords.py +34 -34
- GameSentenceMiner/util/gsm_utils.py +31 -1
- GameSentenceMiner/util/text_log.py +11 -9
- GameSentenceMiner/vad.py +31 -12
- GameSentenceMiner/web/database_api.py +742 -123
- GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- GameSentenceMiner/web/static/css/overview.css +850 -0
- GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- GameSentenceMiner/web/static/css/shared.css +97 -0
- GameSentenceMiner/web/static/css/stats.css +192 -597
- GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- GameSentenceMiner/web/static/js/database.js +209 -5
- GameSentenceMiner/web/static/js/goals.js +610 -0
- GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
- GameSentenceMiner/web/static/js/overview.js +1176 -0
- GameSentenceMiner/web/static/js/shared.js +25 -0
- GameSentenceMiner/web/static/js/stats.js +154 -1459
- GameSentenceMiner/web/stats.py +2 -2
- GameSentenceMiner/web/templates/anki_stats.html +5 -0
- GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
- GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
- GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
- GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
- GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
- GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
- GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
- GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
- GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
- GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
- GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
- GameSentenceMiner/web/templates/components/navigation.html +3 -1
- GameSentenceMiner/web/templates/database.html +73 -1
- GameSentenceMiner/web/templates/goals.html +376 -0
- GameSentenceMiner/web/templates/index.html +13 -11
- GameSentenceMiner/web/templates/overview.html +416 -0
- GameSentenceMiner/web/templates/stats.html +46 -251
- GameSentenceMiner/web/texthooking_page.py +18 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
- gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
- gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
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
|
-
|
|
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
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
sentence_field = note['fields'].get(
|
|
108
|
-
sentence_to_translate = sentence_field
|
|
109
|
-
|
|
110
|
-
|
|
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'][
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
152
|
+
if config.anki.add_game_tag:
|
|
122
153
|
game = get_current_game().replace(" ", "").replace("::", "")
|
|
123
|
-
if
|
|
124
|
-
game = f"{
|
|
154
|
+
if config.anki.parent_tag:
|
|
155
|
+
game = f"{config.anki.parent_tag}::{game}"
|
|
125
156
|
tags.append(game)
|
|
126
|
-
if
|
|
127
|
-
tags.extend(
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
os.
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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()
|
GameSentenceMiner/gametext.py
CHANGED
|
@@ -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
|
-
|
|
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")
|