GameSentenceMiner 2.14.16__tar.gz → 2.14.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ai/ai_prompting.py +1 -1
  2. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/anki.py +107 -60
  3. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/config_gui.py +25 -7
  4. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/gsm.py +30 -24
  5. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/locales/en_us.json +8 -0
  6. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/locales/ja_jp.json +8 -0
  7. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/locales/zh_cn.json +8 -0
  8. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/obs.py +72 -10
  9. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/communication/websocket.py +1 -0
  10. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/configuration.py +4 -4
  11. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/ffmpeg.py +153 -0
  12. gamesentenceminer-2.14.18/GameSentenceMiner/web/templates/index.html +50 -0
  13. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  14. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/PKG-INFO +1 -1
  15. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/pyproject.toml +1 -1
  16. gamesentenceminer-2.14.16/GameSentenceMiner/web/templates/index.html +0 -50
  17. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/__init__.py +0 -0
  18. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ai/__init__.py +0 -0
  19. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/__init__.py +0 -0
  20. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon.png +0 -0
  21. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon128.png +0 -0
  22. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon256.png +0 -0
  23. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon32.png +0 -0
  24. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon512.png +0 -0
  25. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/icon64.png +0 -0
  26. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/assets/pickaxe.png +0 -0
  27. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/gametext.py +0 -0
  28. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/__init__.py +0 -0
  29. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  30. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  31. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  32. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  33. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  34. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  35. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  36. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  37. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  38. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  39. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  40. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  41. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/tools/__init__.py +0 -0
  42. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
  43. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/tools/ss_selector.py +0 -0
  44. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/tools/window_transparency.py +0 -0
  45. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/__init__.py +0 -0
  46. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/communication/__init__.py +0 -0
  47. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/communication/send.py +0 -0
  48. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/db.py +0 -0
  49. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  50. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  51. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  52. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  53. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/electron_config.py +0 -0
  54. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/get_overlay_coords.py +0 -0
  55. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/gsm_utils.py +0 -0
  56. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/model.py +0 -0
  57. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/notification.py +0 -0
  58. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/text_log.py +0 -0
  59. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
  60. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
  61. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/vad.py +0 -0
  62. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/__init__.py +0 -0
  63. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/service.py +0 -0
  64. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/__init__.py +0 -0
  65. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  66. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  67. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/favicon.ico +0 -0
  68. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/favicon.svg +0 -0
  69. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  70. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/style.css +0 -0
  71. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  72. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  73. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/templates/__init__.py +0 -0
  74. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  75. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/templates/utility.html +0 -0
  76. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/web/texthooking_page.py +0 -0
  77. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner/wip/__init___.py +0 -0
  78. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  79. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  80. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  81. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/requires.txt +0 -0
  82. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  83. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/LICENSE +0 -0
  84. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/README.md +0 -0
  85. {gamesentenceminer-2.14.16 → gamesentenceminer-2.14.18}/setup.cfg +0 -0
@@ -29,7 +29,7 @@ Translate ONLY the provided line of game dialogue specified below into natural-s
29
29
  **Output Requirements:**
30
30
  - Provide only the single, best {get_config().general.get_native_language_name()} translation.
31
31
  - Use expletives if they are natural for the context and enhance the translation's impact, but do not over-exaggerate.
32
- - Carryover all HTML tags present in the original text to HTML tags surrounding their corresponding words in the translation. DO NOT CONVERT TO MARKDOWN.
32
+ - Carryover all HTML tags present in the original text to HTML tags surrounding their corresponding translated words in the translation. Look for the equivalent word, not the equivalent location. DO NOT CONVERT TO MARKDOWN.
33
33
  - If there are no HTML tags present in the original text, do not add any in the translation whatsoever.
34
34
  - Do not include notes, alternatives, explanations, or any other surrounding text. Absolutely nothing but the translated line.
35
35
 
@@ -32,7 +32,7 @@ card_queue = []
32
32
 
33
33
 
34
34
  def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
35
- should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0, start_time=None, end_time=None):
35
+ 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):
36
36
  update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
37
37
  last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
38
38
  update_picture = (get_config().anki.picture_field and get_config().screenshot.enabled
@@ -41,14 +41,17 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
41
41
  audio_in_anki = ''
42
42
  screenshot_in_anki = ''
43
43
  prev_screenshot_in_anki = ''
44
+ video_in_anki = ''
45
+ video = ''
44
46
  screenshot = ''
45
47
  prev_screenshot = ''
46
48
  if reuse_audio:
47
49
  logger.info("Reusing Audio from last note")
48
- anki_result = anki_results[game_line.id]
50
+ anki_result: AnkiUpdateResult = anki_results[game_line.id]
49
51
  audio_in_anki = anki_result.audio_in_anki
50
52
  screenshot_in_anki = anki_result.screenshot_in_anki
51
53
  prev_screenshot_in_anki = anki_result.prev_screenshot_in_anki
54
+ video_in_anki = anki_result.video_in_anki
52
55
  else:
53
56
  if update_audio:
54
57
  audio_in_anki = store_media_file(audio_path)
@@ -56,9 +59,16 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
56
59
  open_audio_in_external(f"{get_config().audio.anki_media_collection}/{audio_in_anki}")
57
60
  if update_picture:
58
61
  logger.info("Getting Screenshot...")
59
- screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
62
+ if get_config().screenshot.animated:
63
+ screenshot = ffmpeg.get_anki_compatible_video(video_path, start_time, vad_result.start, vad_result.end, codec='avif', quality=10, fps=12, audio=False)
64
+ else:
65
+ screenshot = ffmpeg.get_screenshot(video_path, ss_time, try_selector=get_config().screenshot.use_screenshot_selector)
60
66
  wait_for_stable_file(screenshot)
61
67
  screenshot_in_anki = store_media_file(screenshot)
68
+ if get_config().anki.video_field:
69
+ if vad_result:
70
+ video = ffmpeg.get_anki_compatible_video(video_path, start_time, vad_result.start, vad_result.end, codec='avif', quality=10, fps=12, audio=True)
71
+ video_in_anki = store_media_file(video)
62
72
  if get_config().anki.previous_image_field and game_line.prev:
63
73
  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)
64
74
  wait_for_stable_file(prev_screenshot)
@@ -68,58 +78,6 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
68
78
  audio_html = f"[sound:{audio_in_anki}]"
69
79
  image_html = f"<img src=\"{screenshot_in_anki}\">"
70
80
  prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
71
-
72
-
73
- # Vars for DB update
74
- new_audio_path = ''
75
- new_screenshot_path = ''
76
- new_prev_screenshot_path = ''
77
- new_video_path = ''
78
- translation = ''
79
- anki_audio_path = ''
80
- anki_screenshot_path = ''
81
- # Move files to output folder if configured
82
- if get_config().paths.output_folder and get_config().paths.copy_temp_files_to_output_folder:
83
- word_path = os.path.join(get_config().paths.output_folder, sanitize_filename(tango))
84
- os.makedirs(word_path, exist_ok=True)
85
-
86
- if audio_path:
87
- audio_filename = Path(audio_path).name
88
- new_audio_path = os.path.join(word_path, audio_filename)
89
- if os.path.exists(audio_path):
90
- shutil.copy(audio_path, new_audio_path)
91
- if screenshot:
92
- screenshot_filename = Path(screenshot).name
93
- new_screenshot_path = os.path.join(word_path, screenshot_filename)
94
- if os.path.exists(screenshot):
95
- shutil.copy(screenshot, new_screenshot_path)
96
- if prev_screenshot:
97
- prev_screenshot_filename = Path(prev_screenshot).name
98
- new_prev_screenshot_path = os.path.join(word_path, "prev_" + prev_screenshot_filename)
99
- if os.path.exists(prev_screenshot):
100
- shutil.copy(prev_screenshot, new_prev_screenshot_path)
101
-
102
- if video_path and get_config().paths.copy_trimmed_replay_to_output_folder:
103
- trimmed_video = ffmpeg.trim_replay_for_gameline(video_path, start_time, end_time, accurate=True)
104
- new_video_path = os.path.join(word_path, Path(trimmed_video).name)
105
- if os.path.exists(trimmed_video):
106
- shutil.copy(trimmed_video, new_video_path)
107
-
108
- if get_config().audio.anki_media_collection:
109
- anki_audio_path = os.path.join(get_config().audio.anki_media_collection, audio_in_anki)
110
- anki_screenshot_path = os.path.join(get_config().audio.anki_media_collection, screenshot_in_anki)
111
-
112
- # Open to word_path if configured
113
- if get_config().paths.open_output_folder_on_card_creation:
114
- try:
115
- if platform.system() == "Windows":
116
- subprocess.Popen(f'explorer "{word_path}"')
117
- elif platform.system() == "Darwin":
118
- subprocess.Popen(["open", word_path])
119
- else:
120
- subprocess.Popen(["xdg-open", word_path])
121
- except Exception as e:
122
- logger.error(f"Error opening output folder: {e}")
123
81
 
124
82
 
125
83
  # note = {'id': last_note.noteId, 'fields': {}}
@@ -129,6 +87,10 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
129
87
 
130
88
  if update_picture and screenshot_in_anki:
131
89
  note['fields'][get_config().anki.picture_field] = image_html
90
+
91
+ if video_in_anki:
92
+ note['fields'][get_config().anki.video_field] = video_in_anki
93
+
132
94
  if not get_config().screenshot.enabled:
133
95
  logger.info("Skipping Adding Screenshot to Anki, Screenshot is disabled in settings")
134
96
 
@@ -158,9 +120,9 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
158
120
  tag_string = " ".join(tags)
159
121
  invoke("addTags", tags=tag_string, notes=[last_note.noteId])
160
122
 
161
- # Update GameLine in DB
123
+ run_new_thread(lambda: check_and_update_note(last_note, note, tags))
162
124
 
163
- logger.info(f"Adding {game_line.id} to Anki Results Dict...")
125
+ word_path = os.path.join(get_config().paths.output_folder, sanitize_filename(tango)) if get_config().paths.output_folder else ''
164
126
  if not reuse_audio:
165
127
  anki_results[game_line.id] = AnkiUpdateResult(
166
128
  success=True,
@@ -168,10 +130,81 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
168
130
  screenshot_in_anki=screenshot_in_anki,
169
131
  prev_screenshot_in_anki=prev_screenshot_in_anki,
170
132
  sentence_in_anki=game_line.text if game_line else '',
171
- multi_line=bool(selected_lines and len(selected_lines) > 1)
133
+ multi_line=bool(selected_lines and len(selected_lines) > 1),
134
+ video_in_anki=video_in_anki or '',
135
+ word_path=word_path
172
136
  )
137
+ # Update GameLine in DB
138
+
139
+ # Vars for DB update
140
+ new_audio_path = ''
141
+ new_screenshot_path = ''
142
+ new_prev_screenshot_path = ''
143
+ new_video_path = ''
144
+ translation = ''
145
+ anki_audio_path = ''
146
+ anki_screenshot_path = ''
147
+ # Move files to output folder if configured
148
+ if get_config().paths.output_folder and reuse_audio:
149
+ anki_result: AnkiUpdateResult = anki_results[game_line.id]
150
+ previous_word_path = anki_result.word_path
151
+ if previous_word_path and os.path.exists(previous_word_path):
152
+ os.makedirs(word_path, exist_ok=True)
153
+ # Copy all files from previous_word_path to word_path
154
+ for item in os.listdir(previous_word_path):
155
+ s = os.path.join(previous_word_path, item)
156
+ d = os.path.join(word_path, item)
157
+ if os.path.isdir(s):
158
+ shutil.copytree(s, d, False, None)
159
+ else:
160
+ shutil.copy2(s, d)
161
+ elif get_config().paths.output_folder and get_config().paths.copy_temp_files_to_output_folder:
162
+ os.makedirs(word_path, exist_ok=True)
163
+ if audio_path:
164
+ audio_filename = Path(audio_path).name
165
+ new_audio_path = os.path.join(word_path, audio_filename)
166
+ if os.path.exists(audio_path):
167
+ shutil.copy(audio_path, new_audio_path)
168
+ if screenshot:
169
+ screenshot_filename = Path(screenshot).name
170
+ new_screenshot_path = os.path.join(word_path, screenshot_filename)
171
+ if os.path.exists(screenshot):
172
+ shutil.copy(screenshot, new_screenshot_path)
173
+ if prev_screenshot:
174
+ prev_screenshot_filename = Path(prev_screenshot).name
175
+ new_prev_screenshot_path = os.path.join(word_path, "prev_" + prev_screenshot_filename)
176
+ if os.path.exists(prev_screenshot):
177
+ shutil.copy(prev_screenshot, new_prev_screenshot_path)
178
+
179
+ if video_path and get_config().paths.copy_trimmed_replay_to_output_folder:
180
+ trimmed_video = ffmpeg.trim_replay_for_gameline(video_path, start_time, end_time, accurate=True)
181
+ new_video_path = os.path.join(word_path, Path(trimmed_video).name)
182
+ if os.path.exists(trimmed_video):
183
+ shutil.copy(trimmed_video, new_video_path)
184
+
185
+ if video:
186
+ new_video_path = os.path.join(word_path, Path(video).name)
187
+ if os.path.exists(video):
188
+ shutil.copy(video, new_video_path)
173
189
 
174
- run_new_thread(lambda: check_and_update_note(last_note, note, tags))
190
+ if get_config().audio.anki_media_collection:
191
+ anki_audio_path = os.path.join(get_config().audio.anki_media_collection, audio_in_anki)
192
+ anki_screenshot_path = os.path.join(get_config().audio.anki_media_collection, screenshot_in_anki)
193
+
194
+ # Open to word_path if configured
195
+ if get_config().paths.open_output_folder_on_card_creation:
196
+ try:
197
+ if platform.system() == "Windows":
198
+ subprocess.Popen(f'explorer "{word_path}"')
199
+ elif platform.system() == "Darwin":
200
+ subprocess.Popen(["open", word_path])
201
+ else:
202
+ subprocess.Popen(["xdg-open", word_path])
203
+ except Exception as e:
204
+ logger.error(f"Error opening output folder: {e}")
205
+
206
+ logger.info(f"Adding {game_line.id} to Anki Results Dict...")
207
+
175
208
  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)
176
209
 
177
210
  def check_and_update_note(last_note, note, tags=[]):
@@ -428,6 +461,7 @@ def update_new_card():
428
461
  texthooking_page.reset_checked_lines()
429
462
  else:
430
463
  logger.info("New card(s) detected! Added to Processing Queue!")
464
+ gsm_state.last_mined_line = game_line
431
465
  card_queue.append((last_card, datetime.now(), lines))
432
466
  texthooking_page.reset_checked_lines()
433
467
  try:
@@ -438,13 +472,26 @@ def update_new_card():
438
472
  return
439
473
 
440
474
  def update_card_from_same_sentence(last_card, lines, game_line):
475
+ time_elapsed = 0
441
476
  while game_line.id not in anki_results:
442
477
  time.sleep(0.5)
478
+ time_elapsed += 0.5
479
+ if time_elapsed > 15:
480
+ logger.info(f"Timed out waiting for Anki update for card {last_card.noteId}, retrieving new audio")
481
+ card_queue.append((last_card, datetime.now(), lines))
482
+ texthooking_page.reset_checked_lines()
483
+ try:
484
+ obs.save_replay_buffer()
485
+ except Exception as e:
486
+ card_queue.pop(0)
487
+ logger.error(f"Error saving replay buffer: {e}")
488
+ return
443
489
  anki_result = anki_results[game_line.id]
444
490
  if anki_result.success:
445
491
  note, last_card = get_initial_card_info(last_card, lines)
492
+ tango = last_card.get_field(get_config().anki.word_field)
446
493
  update_anki_card(last_card, note=note,
447
- game_line=get_mined_line(last_card, lines), reuse_audio=True)
494
+ game_line=get_mined_line(last_card, lines), reuse_audio=True, tango=tango)
448
495
  else:
449
496
  logger.error(f"Anki update failed for card {last_card.noteId}")
450
497
  notification.send_error_no_anki_update()
@@ -307,6 +307,7 @@ class ConfigApp:
307
307
  self.word_field_value = tk.StringVar(value=self.settings.anki.word_field)
308
308
  self.previous_sentence_field_value = tk.StringVar(value=self.settings.anki.previous_sentence_field)
309
309
  self.previous_image_field_value = tk.StringVar(value=self.settings.anki.previous_image_field)
310
+ self.video_field_value = tk.StringVar(value=self.settings.anki.video_field)
310
311
  self.custom_tags_value = tk.StringVar(value=', '.join(self.settings.anki.custom_tags))
311
312
  self.tags_to_check_value = tk.StringVar(value=', '.join(self.settings.anki.tags_to_check))
312
313
  self.add_game_tag_value = tk.BooleanVar(value=self.settings.anki.add_game_tag)
@@ -335,7 +336,8 @@ class ConfigApp:
335
336
  self.seconds_after_line_value = tk.StringVar(value=str(self.settings.screenshot.seconds_after_line))
336
337
  self.screenshot_timing_value = tk.StringVar(value=self.settings.screenshot.screenshot_timing_setting)
337
338
  self.use_screenshot_selector_value = tk.BooleanVar(value=self.settings.screenshot.use_screenshot_selector)
338
-
339
+ self.animated_screenshot_value = tk.BooleanVar(value=self.settings.screenshot.animated)
340
+
339
341
  # Audio Settings
340
342
  self.audio_enabled_value = tk.BooleanVar(value=self.settings.audio.enabled)
341
343
  self.audio_extension_value = tk.StringVar(value=self.settings.audio.extension)
@@ -522,6 +524,7 @@ class ConfigApp:
522
524
  word_field=self.word_field_value.get(),
523
525
  previous_sentence_field=self.previous_sentence_field_value.get(),
524
526
  previous_image_field=self.previous_image_field_value.get(),
527
+ video_field=self.video_field_value.get(),
525
528
  custom_tags=[tag.strip() for tag in self.custom_tags_value.get().split(',') if tag.strip()],
526
529
  tags_to_check=[tag.strip().lower() for tag in self.tags_to_check_value.get().split(',') if tag.strip()],
527
530
  add_game_tag=self.add_game_tag_value.get(),
@@ -541,6 +544,7 @@ class ConfigApp:
541
544
  ),
542
545
  screenshot=Screenshot(
543
546
  enabled=self.screenshot_enabled_value.get(),
547
+ animated=self.animated_screenshot_value.get(),
544
548
  width=self.screenshot_width_value.get(),
545
549
  height=self.screenshot_height_value.get(),
546
550
  quality=self.screenshot_quality_value.get(),
@@ -1304,6 +1308,13 @@ class ConfigApp:
1304
1308
  ttk.Entry(anki_frame, textvariable=self.previous_image_field_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1305
1309
  self.current_row += 1
1306
1310
 
1311
+ video_img_i18n = anki_i18n.get('video_field', {})
1312
+ HoverInfoLabelWidget(anki_frame, text=video_img_i18n.get('label', '...'),
1313
+ tooltip=video_img_i18n.get('tooltip', '...'),
1314
+ row=self.current_row, column=0)
1315
+ ttk.Entry(anki_frame, textvariable=self.video_field_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1316
+ self.current_row += 1
1317
+
1307
1318
  tags_i18n = anki_i18n.get('custom_tags', {})
1308
1319
  HoverInfoLabelWidget(anki_frame, text=tags_i18n.get('label', '...'), tooltip=tags_i18n.get('tooltip', '...'),
1309
1320
  row=self.current_row, column=0)
@@ -1469,6 +1480,13 @@ class ConfigApp:
1469
1480
  ttk.Combobox(screenshot_frame, textvariable=self.screenshot_extension_value, values=['webp', 'avif', 'png', 'jpeg'],
1470
1481
  state="readonly").grid(row=self.current_row, column=1, sticky='EW', pady=2)
1471
1482
  self.current_row += 1
1483
+
1484
+ animated_i18n = ss_i18n.get('animated', {})
1485
+ HoverInfoLabelWidget(screenshot_frame, text=animated_i18n.get('label', '...'), tooltip=animated_i18n.get('tooltip', '...'),
1486
+ row=self.current_row, column=0)
1487
+ ttk.Checkbutton(screenshot_frame, variable=self.animated_screenshot_value, bootstyle="round-toggle").grid(
1488
+ row=self.current_row, column=1, sticky='W', pady=2)
1489
+ self.current_row += 1
1472
1490
 
1473
1491
  ffmpeg_i18n = ss_i18n.get('ffmpeg_options', {})
1474
1492
  HoverInfoLabelWidget(screenshot_frame, text=ffmpeg_i18n.get('label', '...'),
@@ -1577,10 +1595,10 @@ class ConfigApp:
1577
1595
  preset_options_i18n = ffmpeg_preset_i18n.get('options', {})
1578
1596
  self.ffmpeg_audio_preset_options_map = {
1579
1597
  preset_options_i18n.get('no_reencode', "No Re-encode"): "",
1580
- preset_options_i18n.get('fade_in', "Simple Fade-in..."): "-c:a {encoder} -f {format} -af \"afade=t=in:d=0.10\"",
1581
- preset_options_i18n.get('loudness_norm', "Simple loudness..."): "-c:a {encoder} -f {format} -af \"loudnorm=I=-23:TP=-2,afade=t=in:d=0.10\"",
1582
- preset_options_i18n.get('downmix_norm', "Downmix to mono..."): "-c:a {encoder} -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
1583
- preset_options_i18n.get('downmix_norm_low_bitrate', "Downmix to mono, 30kbps..."): "-c:a {encoder} -b:a 30k -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.10\"",
1598
+ preset_options_i18n.get('fade_in', "Simple Fade-in..."): "-c:a {encoder} -f {format} -af \"afade=t=in:d=0.005\"",
1599
+ preset_options_i18n.get('loudness_norm', "Simple loudness..."): "-c:a {encoder} -f {format} -af \"loudnorm=I=-23:TP=-2,afade=t=in:d=0.005\"",
1600
+ preset_options_i18n.get('downmix_norm', "Downmix to mono..."): "-c:a {encoder} -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.005\"",
1601
+ preset_options_i18n.get('downmix_norm_low_bitrate', "Downmix to mono, 30kbps..."): "-c:a {encoder} -b:a 30k -ac 1 -f {format} -af \"loudnorm=I=-23:TP=-2:dual_mono=true,afade=t=in:d=0.005\"",
1584
1602
  preset_options_i18n.get('custom', "Custom"): get_config().audio.custom_encode_settings,
1585
1603
  }
1586
1604
 
@@ -2107,12 +2125,12 @@ class ConfigApp:
2107
2125
  if ai_models and ai_models.gemini_models and ai_models.groq_models:
2108
2126
  if time.time() - ai_models.last_updated > 3600 * 6:
2109
2127
  print("AI models are outdated, fetching new ones.")
2110
- threading.Thread(target=get_models, daemon=True).start()
2128
+ self.window.after(100, get_models)
2111
2129
  self.gemini_model_combobox['values'] = ai_models.gemini_models
2112
2130
  self.groq_models_combobox['values'] = ai_models.groq_models
2113
2131
  else:
2114
2132
  print("No AI models found, fetching new ones.")
2115
- threading.Thread(target=get_models, daemon=True).start()
2133
+ self.window.after(100, get_models)
2116
2134
  # get_models()
2117
2135
 
2118
2136
  def update_models_element(self, frame, row):
@@ -247,7 +247,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
247
247
  game_line=mined_line,
248
248
  selected_lines=selected_lines,
249
249
  start_time=start_time,
250
- end_time=end_time
250
+ end_time=end_time,
251
+ vad_result=vad_result
251
252
  )
252
253
  elif get_config().features.notify_on_update and vad_result.success:
253
254
  notification.send_audio_generated_notification(
@@ -616,29 +617,34 @@ def initialize_async():
616
617
 
617
618
 
618
619
  def handle_websocket_message(message: Message):
619
- match FunctionName(message.function):
620
- case FunctionName.QUIT:
621
- cleanup()
622
- sys.exit(0)
623
- case FunctionName.QUIT_OBS:
624
- close_obs()
625
- case FunctionName.START_OBS:
626
- obs.start_obs()
627
- case FunctionName.OPEN_SETTINGS:
628
- open_settings()
629
- case FunctionName.OPEN_TEXTHOOKER:
630
- texthooking_page.open_texthooker()
631
- case FunctionName.OPEN_LOG:
632
- open_log()
633
- case FunctionName.TOGGLE_REPLAY_BUFFER:
634
- play_pause(None, None)
635
- case FunctionName.RESTART_OBS:
636
- restart_obs()
637
- case FunctionName.EXIT:
638
- exit_program(None, None)
639
- case _:
640
- logger.debug(
641
- f"unknown message from electron websocket: {message.to_json()}")
620
+ try:
621
+ match FunctionName(message.function):
622
+ case FunctionName.QUIT:
623
+ cleanup()
624
+ sys.exit(0)
625
+ case FunctionName.QUIT_OBS:
626
+ close_obs()
627
+ case FunctionName.START_OBS:
628
+ obs.start_obs()
629
+ case FunctionName.OPEN_SETTINGS:
630
+ open_settings()
631
+ case FunctionName.OPEN_TEXTHOOKER:
632
+ texthooking_page.open_texthooker()
633
+ case FunctionName.OPEN_LOG:
634
+ open_log()
635
+ case FunctionName.TOGGLE_REPLAY_BUFFER:
636
+ play_pause(None, None)
637
+ case FunctionName.RESTART_OBS:
638
+ restart_obs()
639
+ case FunctionName.EXIT:
640
+ exit_program(None, None)
641
+ case FunctionName.CONNECT:
642
+ logger.debug("Electron WSS connected")
643
+ case _:
644
+ logger.debug(
645
+ f"unknown message from electron websocket: {message.to_json()}")
646
+ except Exception as e:
647
+ logger.debug(f"Error handling websocket message: {e}")
642
648
 
643
649
 
644
650
  def initialize_text_monitor():
@@ -182,6 +182,10 @@
182
182
  "label": "Previous VoiceLine SS Field:",
183
183
  "tooltip": "Field in Anki for the screenshot of previous line. If Empty, will not populate"
184
184
  },
185
+ "video_field": {
186
+ "label": "Video Field:",
187
+ "tooltip": "Field in Anki for associated videos. This will be AV1 encoded video of the VAD Trimmed Voiceline, if no Voice found, this will be empty."
188
+ },
185
189
  "custom_tags": {
186
190
  "label": "Add Tags:",
187
191
  "tooltip": "Comma-separated custom tags for the Anki cards."
@@ -307,6 +311,10 @@
307
311
  "label": "Extension:",
308
312
  "tooltip": "File extension for the screenshot format."
309
313
  },
314
+ "animated": {
315
+ "label": "Animated:",
316
+ "tooltip": "Enable to create an animated screenshot. Will be a webm file encoded with AV1. All settings under this are not applicable with this ON."
317
+ },
310
318
  "ffmpeg_options": {
311
319
  "label": "FFmpeg Reencode Options:",
312
320
  "tooltip": "Custom FFmpeg options for re-encoding screenshots."
@@ -181,6 +181,10 @@
181
181
  "label": "前の画像フィールド:",
182
182
  "tooltip": "前のセリフの画像を保存するフィールド。空欄で無効。"
183
183
  },
184
+ "video_field": {
185
+ "label": "動画フィールド:",
186
+ "tooltip": "Ankiの関連動画フィールド。VADトリミングされたボイスラインのAV1エンコード動画です。音声が見つからない場合は空になります。"
187
+ },
184
188
  "custom_tags": {
185
189
  "label": "追加タグ:",
186
190
  "tooltip": "Ankiカードに追加するカスタムタグ(カンマ区切り)。"
@@ -306,6 +310,10 @@
306
310
  "label": "拡張子:",
307
311
  "tooltip": "スクリーンショットのファイル形式。"
308
312
  },
313
+ "animated": {
314
+ "label": "アニメーション:",
315
+ "tooltip": "アニメーションスクリーンショットを作成する。AV1でエンコードされたwebmファイルになります。この設定がONの場合、以下の設定は適用されません。"
316
+ },
309
317
  "ffmpeg_options": {
310
318
  "label": "FFmpeg再エンコード設定:",
311
319
  "tooltip": "スクリーンショットの再エンコードに使用するカスタムFFmpegオプション。"
@@ -182,6 +182,10 @@
182
182
  "label": "上一句截图字段:",
183
183
  "tooltip": "Anki 中用于上一句截图的字段。如果为空,则不填充。"
184
184
  },
185
+ "video_field": {
186
+ "label": "视频字段:",
187
+ "tooltip": "Anki 中关联视频的字段。这将是VAD修剪语音线的AV1编码视频,如果未找到语音,此字段将为空。"
188
+ },
185
189
  "custom_tags": {
186
190
  "label": "添加标签:",
187
191
  "tooltip": "Anki 卡片的自定义标签(以逗号分隔)。"
@@ -307,6 +311,10 @@
307
311
  "label": "扩展名:",
308
312
  "tooltip": "截图格式的文件扩展名。"
309
313
  },
314
+ "animated": {
315
+ "label": "动画:",
316
+ "tooltip": "启用创建动画截图。将是使用AV1编码的webm文件。启用此选项时,以下所有设置均不适用。"
317
+ },
310
318
  "ffmpeg_options": {
311
319
  "label": "FFmpeg 重编码选项:",
312
320
  "tooltip": "用于重编码截图的自定义 FFmpeg 选项。"
@@ -43,16 +43,21 @@ class OBSConnectionManager(threading.Thread):
43
43
  logger.info(f"OBS WebSocket not connected. Attempting to reconnect... {e}")
44
44
  gsm_status.obs_connected = False
45
45
  asyncio.run(connect_to_obs())
46
- if self.counter % 5 == 0 and not get_config().obs.turn_off_output_check and self.check_output:
47
- replay_buffer_status = get_replay_buffer_status()
48
- if replay_buffer_status and self.said_no_to_replay_buffer:
49
- self.said_no_to_replay_buffer = False
50
- self.counter = 0
51
- if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
52
- try:
53
- self.check_output()
54
- except Exception as e:
55
- pass
46
+ if self.counter % 5 == 0:
47
+ try:
48
+ set_fit_to_screen_for_scene_items(get_current_scene())
49
+ if get_config().obs.turn_off_output_check and self.check_output:
50
+ replay_buffer_status = get_replay_buffer_status()
51
+ if replay_buffer_status and self.said_no_to_replay_buffer:
52
+ self.said_no_to_replay_buffer = False
53
+ self.counter = 0
54
+ if gsm_status.obs_connected and not replay_buffer_status and not self.said_no_to_replay_buffer:
55
+ try:
56
+ self.check_output()
57
+ except Exception as e:
58
+ pass
59
+ except Exception as e:
60
+ logger.error(f"Error when running Extra Utils in OBS Health Check, Keeping ConnectionManager Alive: {e}")
56
61
  self.counter += 1
57
62
 
58
63
  def stop(self):
@@ -463,6 +468,61 @@ def get_current_game(sanitize=False, update=True):
463
468
  return gsm_state.current_game
464
469
 
465
470
 
471
+
472
+ def set_fit_to_screen_for_scene_items(scene_name: str):
473
+ """
474
+ Sets all sources in a given scene to "Fit to Screen" (like Ctrl+F in OBS).
475
+
476
+ This function fetches the canvas dimensions, then iterates through all scene
477
+ items in the specified scene and applies a transform that scales them to
478
+ fit within the canvas while maintaining aspect ratio and centering them.
479
+
480
+ Args:
481
+ scene_name: The name of the scene to modify.
482
+ """
483
+ try:
484
+ # 1. Get the canvas (base) resolution from OBS video settings
485
+ video_settings = client.get_video_settings()
486
+ canvas_width = video_settings.base_width
487
+ canvas_height = video_settings.base_height
488
+
489
+ # 2. Get the list of items in the specified scene
490
+ scene_items_response = client.get_scene_item_list(scene_name)
491
+ items = scene_items_response.scene_items if scene_items_response.scene_items else []
492
+
493
+ if not items:
494
+ logger.warning(f"No items found in scene '{scene_name}'.")
495
+ return
496
+
497
+ # 3. Loop through each item and apply the "Fit to Screen" transform
498
+ for item in items:
499
+ item_id = item['sceneItemId']
500
+ source_name = item['sourceName']
501
+
502
+ # This transform object is the equivalent of "Fit to Screen"
503
+ fit_to_screen_transform = {
504
+ 'boundsType': 'OBS_BOUNDS_SCALE_INNER',
505
+ 'alignment': 5, # 5 = Center alignment (horizontal and vertical)
506
+ 'boundsWidth': canvas_width,
507
+ 'boundsHeight': canvas_height,
508
+ }
509
+
510
+ try:
511
+ client.set_scene_item_transform(
512
+ scene_name=scene_name,
513
+ item_id=item_id,
514
+ transform=fit_to_screen_transform
515
+ )
516
+ except obs.error.OBSSDKError as e:
517
+ logger.error(f"Failed to set transform for source '{source_name}': {e}")
518
+
519
+ except obs.error.OBSSDKError as e:
520
+ # This will catch errors like "scene not found"
521
+ logger.error(f"An OBS error occurred: {e}")
522
+ except Exception as e:
523
+ logger.error(f"An unexpected error occurred: {e}")
524
+
525
+
466
526
  def main():
467
527
  start_obs()
468
528
  connect_to_obs()
@@ -501,6 +561,8 @@ def main():
501
561
  if __name__ == '__main__':
502
562
  from mss import mss
503
563
  logging.basicConfig(level=logging.INFO)
564
+ connect_to_obs_sync()
565
+ set_fit_to_screen_for_scene_items(get_current_scene())
504
566
  # main()
505
567
  # connect_to_obs_sync()
506
568
  # img = get_screenshot_PIL(source_name="Display Capture 2", compression=75, img_format='png', width=1280, height=720)
@@ -28,6 +28,7 @@ class FunctionName(Enum):
28
28
  RESTART_OBS = "restart_obs"
29
29
  EXIT = "exit"
30
30
  GET_STATUS = "get_status"
31
+ CONNECT = "on_connect"
31
32
 
32
33
 
33
34
  async def do_websocket_connection(port):
@@ -504,7 +504,7 @@ class Audio:
504
504
  beginning_offset: float = -0.5
505
505
  end_offset: float = 0.5
506
506
  pre_vad_end_offset: float = 0.0
507
- ffmpeg_reencode_options: str = '-c:a {encoder} -f {format} -af \"afade=t=in:d=0.10\"' if is_windows() else ''
507
+ ffmpeg_reencode_options: str = '-c:a {encoder} -f {format} -af \"afade=t=in:d=0.005\"' if is_windows() else ''
508
508
  ffmpeg_reencode_options_to_use: str = ''
509
509
  external_tool: str = ""
510
510
  anki_media_collection: str = ""
@@ -1149,10 +1149,12 @@ class AnkiUpdateResult:
1149
1149
  prev_screenshot_in_anki: str = ''
1150
1150
  sentence_in_anki: str = ''
1151
1151
  multi_line: bool = False
1152
+ video_in_anki: str = ''
1153
+ word_path: str = ''
1152
1154
 
1153
1155
  @staticmethod
1154
1156
  def failure():
1155
- return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False)
1157
+ return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False, video_in_anki='', word_path='')
1156
1158
 
1157
1159
 
1158
1160
  @dataclass_json
@@ -1199,5 +1201,3 @@ is_beangate = os.path.exists("C:/Users/Beangate")
1199
1201
 
1200
1202
  logger.debug(f"Running in development mode: {is_dev}")
1201
1203
  logger.debug(f"Running on Beangate's PC: {is_beangate}")
1202
-
1203
- logger.info("THE UPDATE WORKED?")