GameSentenceMiner 2.17.6__tar.gz → 2.18.0__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.
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ai/ai_prompting.py +51 -51
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/anki.py +236 -152
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/gametext.py +7 -4
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/gsm.py +49 -10
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/locales/en_us.json +7 -3
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/locales/ja_jp.json +8 -4
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/locales/zh_cn.json +8 -4
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/obs.py +238 -59
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/owocr_helper.py +1 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/tools/ss_selector.py +7 -8
- gamesentenceminer-2.18.0/GameSentenceMiner/ui/anki_confirmation.py +187 -0
- {gamesentenceminer-2.17.6/GameSentenceMiner → gamesentenceminer-2.18.0/GameSentenceMiner/ui}/config_gui.py +102 -37
- gamesentenceminer-2.18.0/GameSentenceMiner/ui/screenshot_selector.py +215 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/configuration.py +124 -22
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/db.py +22 -13
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/downloader/download_tools.py +2 -2
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/ffmpeg.py +24 -30
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/get_overlay_coords.py +34 -34
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/gsm_utils.py +31 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/text_log.py +11 -9
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/vad.py +31 -12
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/database_api.py +742 -123
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
- gamesentenceminer-2.17.6/GameSentenceMiner/web/static/css/stats.css → gamesentenceminer-2.18.0/GameSentenceMiner/web/static/css/overview.css +250 -637
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/css/popups-shared.css +126 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/css/shared.css +97 -0
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/css/stats.css +832 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/js/anki_stats.js +6 -4
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/js/database.js +209 -5
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/js/goals.js +610 -0
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/js/kanji-grid.js +466 -0
- gamesentenceminer-2.17.6/GameSentenceMiner/web/static/js/stats.js → gamesentenceminer-2.18.0/GameSentenceMiner/web/static/js/overview.js +7 -1038
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/js/shared.js +25 -0
- gamesentenceminer-2.18.0/GameSentenceMiner/web/static/js/stats.js +902 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/stats.py +2 -2
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/anki_stats.html +5 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/components/navigation.html +3 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/database.html +73 -1
- gamesentenceminer-2.18.0/GameSentenceMiner/web/templates/goals.html +376 -0
- gamesentenceminer-2.18.0/GameSentenceMiner/web/templates/index.html +51 -0
- gamesentenceminer-2.17.6/GameSentenceMiner/web/templates/stats.html → gamesentenceminer-2.18.0/GameSentenceMiner/web/templates/overview.html +10 -198
- gamesentenceminer-2.18.0/GameSentenceMiner/web/templates/stats.html +399 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/texthooking_page.py +18 -0
- gamesentenceminer-2.18.0/GameSentenceMiner/wip/__init___.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/PKG-INFO +5 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/SOURCES.txt +11 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/PKG-INFO +5 -1
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/README.md +4 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/pyproject.toml +1 -1
- gamesentenceminer-2.17.6/GameSentenceMiner/web/static/js/kanji-grid.js +0 -203
- gamesentenceminer-2.17.6/GameSentenceMiner/web/templates/index.html +0 -49
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon128.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon256.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon32.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon512.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/icon64.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/assets/pickaxe.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/ocr/ss_picker.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/run.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/tools/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/tools/furigana_filter_preview.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/tools/window_transparency.py +0 -0
- {gamesentenceminer-2.17.6/GameSentenceMiner/util → gamesentenceminer-2.18.0/GameSentenceMiner/ui}/__init__.py +0 -0
- {gamesentenceminer-2.17.6/GameSentenceMiner/util/downloader → gamesentenceminer-2.18.0/GameSentenceMiner/util}/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/communication/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/communication/send.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/communication/websocket.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.17.6/GameSentenceMiner/web → gamesentenceminer-2.18.0/GameSentenceMiner/util/downloader}/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/electron_config.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/model.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/notification.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
- {gamesentenceminer-2.17.6/GameSentenceMiner/web/static → gamesentenceminer-2.18.0/GameSentenceMiner/web}/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/events.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/gsm_websocket.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/service.py +0 -0
- /gamesentenceminer-2.17.6/GameSentenceMiner/wip/__init___.py → /gamesentenceminer-2.18.0/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/css/search.css +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/js/search.js +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/components/theme-styles.html +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/search.html +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner/web/templates/utility.html +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/requires.txt +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/LICENSE +0 -0
- {gamesentenceminer-2.17.6 → gamesentenceminer-2.18.0}/setup.cfg +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}")
|
|
@@ -252,7 +252,7 @@ class GeminiAI(AIManager):
|
|
|
252
252
|
)
|
|
253
253
|
if "2.5" in self.model_name:
|
|
254
254
|
self.generation_config.thinking_config = types.ThinkingConfig(
|
|
255
|
-
thinking_budget=-1
|
|
255
|
+
thinking_budget=-1 if '2.5-pro' in self.model_name else 0,
|
|
256
256
|
)
|
|
257
257
|
self.logger.debug(
|
|
258
258
|
f"GeminiAIManager initialized with model: {self.model_name}")
|
|
@@ -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}")
|
|
@@ -497,59 +497,59 @@ if __name__ == '__main__':
|
|
|
497
497
|
|
|
498
498
|
# Completely neutral Japanese sentences
|
|
499
499
|
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
500
|
+
lines = [
|
|
501
|
+
GameLine(index=0, text="今日はいい天気ですね。", id=None,
|
|
502
|
+
time=None, prev=None, next=None),
|
|
503
|
+
GameLine(index=1, text="おはようございます。", id=None,
|
|
504
|
+
time=None, prev=None, next=None),
|
|
505
|
+
GameLine(index=2, text="お元気ですか?", id=None,
|
|
506
|
+
time=None, prev=None, next=None),
|
|
507
|
+
GameLine(index=3, text="これはペンです。", id=None,
|
|
508
|
+
time=None, prev=None, next=None),
|
|
509
|
+
GameLine(index=4, text="私は学生です。", id=None,
|
|
510
|
+
time=None, prev=None, next=None),
|
|
511
|
+
GameLine(index=5, text="東京は日本の首都です。", id=None,
|
|
512
|
+
time=None, prev=None, next=None),
|
|
513
|
+
GameLine(index=6, text="こんにちは、元気ですか?", id=None,
|
|
514
|
+
time=None, prev=None, next=None),
|
|
515
|
+
GameLine(index=7, text="さようなら。また会いましょう。", id=None,
|
|
516
|
+
time=None, prev=None, next=None),
|
|
517
|
+
GameLine(index=8, text="ありがとう。助かりました。", id=None,
|
|
518
|
+
time=None, prev=None, next=None),
|
|
519
|
+
GameLine(index=9, text="すみません、道に迷いました。", id=None,
|
|
520
|
+
time=None, prev=None, next=None),
|
|
521
|
+
GameLine(index=10, text="これは本です。", id=None,
|
|
522
|
+
time=None, prev=None, next=None),
|
|
523
|
+
GameLine(index=11, text="私は先生です。", id=None,
|
|
524
|
+
time=None, prev=None, next=None),
|
|
525
|
+
]
|
|
526
526
|
|
|
527
|
-
sentence = "
|
|
527
|
+
sentence = "おはようございます。"
|
|
528
528
|
current_line = lines[3]
|
|
529
529
|
game_title = "Corrupted Reality"
|
|
530
530
|
|
|
531
531
|
results = []
|
|
532
532
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
533
|
+
get_config().ai.provider = AIType.GEMINI.value
|
|
534
|
+
models = [
|
|
535
|
+
'gemini-2.5-flash']
|
|
536
536
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
537
|
+
for model in models:
|
|
538
|
+
get_config().ai.gemini_model = model
|
|
539
|
+
start_time = time.time()
|
|
540
|
+
result = get_ai_prompt_result(lines, sentence, current_line, game_title, True)
|
|
541
|
+
results.append({"model": model, "response": result, "time": time.time() - start_time, "iteration": 1})
|
|
542
542
|
|
|
543
|
-
get_config().ai.provider = AIType.OPENAI.value
|
|
544
|
-
get_config().ai.open_ai_url = "https://api.openai.com/v1"
|
|
545
|
-
get_config().ai.open_ai_model = "gpt-5-nano-2025-08-07"
|
|
543
|
+
# get_config().ai.provider = AIType.OPENAI.value
|
|
544
|
+
# get_config().ai.open_ai_url = "https://api.openai.com/v1"
|
|
545
|
+
# get_config().ai.open_ai_model = "gpt-5-nano-2025-08-07"
|
|
546
546
|
|
|
547
|
-
for i in range(5):
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
547
|
+
# for i in range(5):
|
|
548
|
+
# start_time = time.time()
|
|
549
|
+
# result = get_ai_prompt_result(
|
|
550
|
+
# lines, sentence, current_line, game_title, True)
|
|
551
|
+
# results.append({"model": get_config().ai.open_ai_model,
|
|
552
|
+
# "response": result, "time": time.time() - start_time, "iteration": i})
|
|
553
553
|
|
|
554
554
|
# get_config().ai.provider = AIType.OPENAI.value
|
|
555
555
|
# models = [
|
|
@@ -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()
|