GameSentenceMiner 2.10.14__tar.gz → 2.10.16__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 (75) hide show
  1. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/anki.py +2 -2
  2. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/config_gui.py +267 -85
  3. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/gsm.py +1 -1
  4. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/configuration.py +6 -5
  5. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/downloader/download_tools.py +9 -2
  6. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/gsm_utils.py +15 -10
  7. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/text_log.py +9 -9
  8. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
  9. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/PKG-INFO +1 -1
  10. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/pyproject.toml +1 -1
  11. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/__init__.py +0 -0
  12. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ai/__init__.py +0 -0
  13. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ai/ai_prompting.py +0 -0
  14. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/__init__.py +0 -0
  15. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon.png +0 -0
  16. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon128.png +0 -0
  17. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon256.png +0 -0
  18. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon32.png +0 -0
  19. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon512.png +0 -0
  20. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/icon64.png +0 -0
  21. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/assets/pickaxe.png +0 -0
  22. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/gametext.py +0 -0
  23. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/obs.py +0 -0
  24. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/__init__.py +0 -0
  25. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  26. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  27. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  28. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  29. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  30. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  31. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  32. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  33. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  34. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
  35. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  36. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  37. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/__init__.py +0 -0
  38. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/audio_offset_selector.py +0 -0
  39. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/communication/__init__.py +0 -0
  40. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/communication/send.py +0 -0
  41. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/communication/websocket.py +0 -0
  42. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  43. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/downloader/__init__.py +0 -0
  44. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  45. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/electron_config.py +0 -0
  46. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/ffmpeg.py +0 -0
  47. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/model.py +0 -0
  48. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/notification.py +0 -0
  49. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/package.py +0 -0
  50. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/util/ss_selector.py +0 -0
  51. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/vad.py +0 -0
  52. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/__init__.py +0 -0
  53. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/service.py +0 -0
  54. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/__init__.py +0 -0
  55. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  56. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  57. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/favicon.ico +0 -0
  58. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/favicon.svg +0 -0
  59. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  60. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/style.css +0 -0
  61. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  62. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  63. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/templates/__init__.py +0 -0
  64. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/templates/index.html +0 -0
  65. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  66. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/templates/utility.html +0 -0
  67. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner/web/texthooking_page.py +0 -0
  68. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/SOURCES.txt +0 -0
  69. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  70. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  71. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/requires.txt +0 -0
  72. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  73. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/LICENSE +0 -0
  74. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/README.md +0 -0
  75. {gamesentenceminer-2.10.14 → gamesentenceminer-2.10.16}/setup.cfg +0 -0
@@ -16,7 +16,7 @@ from GameSentenceMiner.util import ffmpeg, notification
16
16
  from GameSentenceMiner.util.configuration import *
17
17
  from GameSentenceMiner.util.configuration import get_config
18
18
  from GameSentenceMiner.util.model import AnkiCard
19
- from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_mined_line
19
+ from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_mined_line, lines_match
20
20
  from GameSentenceMiner.obs import get_current_game
21
21
  from GameSentenceMiner.web import texthooking_page
22
22
 
@@ -162,7 +162,7 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
162
162
  sentence_in_anki = last_note.get_field(get_config().anki.sentence_field)
163
163
  logger.info(f"Attempting Preserve HTML for multi-line")
164
164
  for line in selected_lines:
165
- if remove_html_and_cloze_tags(sentence_in_anki) in line.text:
165
+ if lines_match(line.text, remove_html_and_cloze_tags(sentence_in_anki)):
166
166
  sentences.append(sentence_in_anki)
167
167
  logger.info("Found matching line in Anki, Preserving HTML!")
168
168
  else:
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import subprocess
3
+ import time
3
4
  import tkinter as tk
4
5
  from tkinter import filedialog, messagebox, simpledialog, scrolledtext, font
5
6
 
@@ -51,8 +52,10 @@ class HoverInfoWidget:
51
52
  self.tooltip.destroy()
52
53
  self.tooltip = None
53
54
 
55
+
54
56
  class HoverInfoLabelWidget:
55
- def __init__(self, parent, text, tooltip, row, column, padx=5, pady=2, foreground="white", sticky='W', bootstyle=None, font=("Arial", 10, "normal")):
57
+ def __init__(self, parent, text, tooltip, row, column, padx=5, pady=2, foreground="white", sticky='W',
58
+ bootstyle=None, font=("Arial", 10, "normal")):
56
59
  self.label = ttk.Label(parent, text=text, foreground=foreground, cursor="hand2", bootstyle=bootstyle, font=font)
57
60
  self.label.grid(row=row, column=column, padx=(0, padx), pady=0, sticky=sticky)
58
61
  self.label.bind("<Enter>", lambda e: self.show_info_box(tooltip))
@@ -76,6 +79,31 @@ class HoverInfoLabelWidget:
76
79
  self.tooltip = None
77
80
 
78
81
 
82
+ class ResetToDefaultButton(ttk.Button):
83
+ def __init__(self, parent, command, text="Reset to Default", bootstyle="danger", **kwargs):
84
+ super().__init__(parent, text=text, command=command, bootstyle=bootstyle, **kwargs)
85
+ self.tooltip = None
86
+ self.bind("<Enter>", self.show_tooltip)
87
+ self.bind("<Leave>", self.hide_tooltip)
88
+
89
+ def show_tooltip(self, event):
90
+ if not self.tooltip:
91
+ x = self.winfo_rootx() + 20
92
+ y = self.winfo_rooty() + 20
93
+ self.tooltip = tk.Toplevel(self)
94
+ self.tooltip.wm_overrideredirect(True)
95
+ self.tooltip.wm_geometry(f"+{x}+{y}")
96
+ label = ttk.Label(self.tooltip, text="Reset Current Tab Settings to default values.", relief="solid",
97
+ borderwidth=1,
98
+ font=("tahoma", "12", "normal"))
99
+ label.pack(ipadx=1)
100
+
101
+ def hide_tooltip(self, event):
102
+ if self.tooltip:
103
+ self.tooltip.destroy()
104
+ self.tooltip = None
105
+
106
+
79
107
  class ConfigApp:
80
108
  def __init__(self, root):
81
109
  self.window = root
@@ -92,21 +120,26 @@ class ConfigApp:
92
120
  self.master_config: Config = configuration.load_config()
93
121
 
94
122
  self.settings = self.master_config.get_config()
123
+ self.default_master_settings = Config.new()
124
+ self.default_settings = self.default_master_settings.get_config()
95
125
 
96
126
  self.notebook = ttk.Notebook(self.window)
97
127
  self.notebook.pack(pady=10, expand=True, fill='both')
98
128
 
99
- self.general_frame = self.create_general_tab()
100
- self.create_paths_tab()
101
- self.create_anki_tab()
102
- self.create_vad_tab()
103
- self.create_features_tab()
104
- self.create_screenshot_tab()
105
- self.create_audio_tab()
106
- self.create_obs_tab()
107
- self.create_profiles_tab()
108
- self.create_ai_tab()
109
- self.create_advanced_tab()
129
+ self.general_tab = None
130
+ self.paths_tab = None
131
+ self.anki_tab = None
132
+ self.vad_tab = None
133
+ self.features_tab = None
134
+ self.screenshot_tab = None
135
+ self.audio_tab = None
136
+ self.obs_tab = None
137
+ self.profiles_tab = None
138
+ self.ai_tab = None
139
+ self.advanced_tab = None
140
+
141
+ self.create_tabs()
142
+
110
143
  # self.create_help_tab()
111
144
 
112
145
  self.notebook.bind("<<NotebookTabChanged>>", self.on_profiles_tab_selected)
@@ -127,6 +160,44 @@ class ConfigApp:
127
160
 
128
161
  self.window.withdraw()
129
162
 
163
+ def create_tabs(self):
164
+ self.create_general_tab()
165
+ self.create_paths_tab()
166
+ self.create_anki_tab()
167
+ self.create_vad_tab()
168
+ self.create_features_tab()
169
+ self.create_screenshot_tab()
170
+ self.create_audio_tab()
171
+ self.create_obs_tab()
172
+ self.create_profiles_tab()
173
+ self.create_ai_tab()
174
+ self.create_advanced_tab()
175
+
176
+ def add_reset_button(self, frame, category, row, column=0, recreate_tab=None):
177
+ """
178
+ Adds a reset button to the given frame that resets the settings in the frame to default values.
179
+ """
180
+ reset_button = ResetToDefaultButton(frame, command=lambda: self.reset_to_default(category, recreate_tab),
181
+ text="Reset to Default")
182
+ reset_button.grid(row=row, column=column, sticky='W', padx=5, pady=5)
183
+ return reset_button
184
+
185
+ # Category is the dataclass name of the settings being reset, default is a default instance of that dataclass
186
+ def reset_to_default(self, category, recreate_tab):
187
+ """
188
+ Resets the settings in the current tab to default values.
189
+ """
190
+ if not messagebox.askyesno("Reset to Default",
191
+ "Are you sure you want to reset all settings in this tab to default?"):
192
+ return
193
+
194
+ default_category_config = getattr(self.default_settings, category)
195
+
196
+ setattr(self.settings, category, default_category_config)
197
+ recreate_tab()
198
+ self.save_settings(profile_change=False)
199
+ self.reload_settings()
200
+
130
201
  def show_scene_selection(self, matched_configs):
131
202
  selected_scene = None
132
203
  if matched_configs:
@@ -313,6 +384,14 @@ class ConfigApp:
313
384
  self.master_config.current_profile = current_profile
314
385
  self.master_config.set_config_for_profile(current_profile, config)
315
386
 
387
+
388
+ config_backup_folder = os.path.join(get_app_directory(), "backup", "config")
389
+ os.makedirs(config_backup_folder, exist_ok=True)
390
+ # write a timesstamped backup of the current config before saving
391
+ timestamp = time.strftime("%Y-%m-%d_%H-%M-%S")
392
+ with open(os.path.join(config_backup_folder, f"config_backup_{timestamp}.json"), 'w') as backup_file:
393
+ backup_file.write(self.master_config.to_json(indent=4))
394
+
316
395
  self.master_config = self.master_config.sync_shared_fields()
317
396
 
318
397
  if sync_changes:
@@ -347,17 +426,20 @@ class ConfigApp:
347
426
  for frame in self.notebook.winfo_children():
348
427
  frame.destroy()
349
428
 
350
- self.general_frame = self.create_general_tab()
351
- self.create_paths_tab()
352
- self.create_anki_tab()
353
- self.create_vad_tab()
354
- self.create_features_tab()
355
- self.create_screenshot_tab()
356
- self.create_audio_tab()
357
- self.create_obs_tab()
358
- self.create_profiles_tab()
359
- self.create_advanced_tab()
360
- self.create_ai_tab()
429
+ # Reset tab frames so they are recreated
430
+ self.general_tab = None
431
+ self.paths_tab = None
432
+ self.anki_tab = None
433
+ self.vad_tab = None
434
+ self.features_tab = None
435
+ self.screenshot_tab = None
436
+ self.audio_tab = None
437
+ self.obs_tab = None
438
+ self.profiles_tab = None
439
+ self.ai_tab = None
440
+ self.advanced_tab = None
441
+
442
+ self.create_tabs()
361
443
 
362
444
  def increment_row(self):
363
445
  """Increment the current row index and return the new value."""
@@ -373,111 +455,120 @@ class ConfigApp:
373
455
 
374
456
  @new_tab
375
457
  def create_general_tab(self):
376
- general_frame = ttk.Frame(self.notebook, padding=5)
377
- self.notebook.add(general_frame, text='General')
458
+ if self.general_tab is None:
459
+ self.general_tab = ttk.Frame(self.notebook, padding=15)
460
+ self.notebook.add(self.general_tab, text='General')
461
+ else:
462
+ for widget in self.general_tab.winfo_children():
463
+ widget.destroy()
378
464
 
379
- HoverInfoLabelWidget(general_frame, text="Websocket Enabled:",
465
+ HoverInfoLabelWidget(self.general_tab, text="Websocket Enabled:",
380
466
  foreground="dark orange", font=("Helvetica", 10, "bold"),
381
467
  tooltip="Enable or disable WebSocket communication. Enabling this will disable the clipboard monitor.",
382
468
  row=self.current_row, column=0)
383
469
  self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
384
- ttk.Checkbutton(general_frame, variable=self.websocket_enabled, bootstyle="round-toggle").grid(
470
+ ttk.Checkbutton(self.general_tab, variable=self.websocket_enabled, bootstyle="round-toggle").grid(
385
471
  row=self.current_row, column=1,
386
472
  sticky='W', pady=2)
387
473
  self.current_row += 1
388
474
 
389
- HoverInfoLabelWidget(general_frame, text="Clipboard Enabled:",
475
+ HoverInfoLabelWidget(self.general_tab, text="Clipboard Enabled:",
390
476
  foreground="dark orange", font=("Helvetica", 10, "bold"),
391
477
  tooltip="Enable or disable Clipboard monitoring.", row=self.current_row, column=0)
392
478
  self.clipboard_enabled = tk.BooleanVar(value=self.settings.general.use_clipboard)
393
- ttk.Checkbutton(general_frame, variable=self.clipboard_enabled, bootstyle="round-toggle").grid(
479
+ ttk.Checkbutton(self.general_tab, variable=self.clipboard_enabled, bootstyle="round-toggle").grid(
394
480
  row=self.current_row, column=1,
395
481
  sticky='W', pady=2)
396
482
  self.current_row += 1
397
483
 
398
- HoverInfoLabelWidget(general_frame, text="Allow Both Simultaneously:",
484
+ HoverInfoLabelWidget(self.general_tab, text="Allow Both Simultaneously:",
399
485
  foreground="red", font=("Helvetica", 10, "bold"),
400
486
  tooltip="Enable to allow GSM to accept both clipboard and websocket input at the same time.",
401
487
  row=self.current_row, column=0)
402
488
  self.use_both_clipboard_and_websocket = tk.BooleanVar(
403
489
  value=self.settings.general.use_both_clipboard_and_websocket)
404
- ttk.Checkbutton(general_frame, variable=self.use_both_clipboard_and_websocket, bootstyle="round-toggle").grid(
490
+ ttk.Checkbutton(self.general_tab, variable=self.use_both_clipboard_and_websocket,
491
+ bootstyle="round-toggle").grid(
405
492
  row=self.current_row, column=1,
406
493
  sticky='W', pady=2)
407
494
  self.current_row += 1
408
495
 
409
- HoverInfoLabelWidget(general_frame, text="Websocket URI(s):",
496
+ HoverInfoLabelWidget(self.general_tab, text="Websocket URI(s):",
410
497
  tooltip="WebSocket URI for connecting. Allows Comma Separated Values for Connecting Multiple.",
411
498
  row=self.current_row, column=0)
412
- self.websocket_uri = ttk.Entry(general_frame, width=50)
499
+ self.websocket_uri = ttk.Entry(self.general_tab, width=50)
413
500
  self.websocket_uri.insert(0, self.settings.general.websocket_uri)
414
501
  self.websocket_uri.grid(row=self.current_row, column=1, sticky='EW', pady=2)
415
502
  self.current_row += 1
416
503
 
417
- HoverInfoLabelWidget(general_frame, text="TextHook Replacement Regex:",
504
+ HoverInfoLabelWidget(self.general_tab, text="TextHook Replacement Regex:",
418
505
  tooltip="Regex to run replacement on texthook input, set this to the same as what you may have in your texthook page.",
419
506
  row=self.current_row, column=0)
420
- self.texthook_replacement_regex = ttk.Entry(general_frame)
507
+ self.texthook_replacement_regex = ttk.Entry(self.general_tab)
421
508
  self.texthook_replacement_regex.insert(0, self.settings.general.texthook_replacement_regex)
422
509
  self.texthook_replacement_regex.grid(row=self.current_row, column=1, sticky='EW', pady=2)
423
510
  self.current_row += 1
424
511
 
425
- HoverInfoLabelWidget(general_frame, text="Open Config on Startup:",
512
+ HoverInfoLabelWidget(self.general_tab, text="Open Config on Startup:",
426
513
  tooltip="Whether to open config when the script starts.", row=self.current_row, column=0)
427
514
  self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
428
- ttk.Checkbutton(general_frame, variable=self.open_config_on_startup, bootstyle="round-toggle").grid(
515
+ ttk.Checkbutton(self.general_tab, variable=self.open_config_on_startup, bootstyle="round-toggle").grid(
429
516
  row=self.current_row, column=1,
430
517
  sticky='W', pady=2)
431
518
  self.current_row += 1
432
519
 
433
- HoverInfoLabelWidget(general_frame, text="Open GSM Texthooker on Startup:",
520
+ HoverInfoLabelWidget(self.general_tab, text="Open GSM Texthooker on Startup:",
434
521
  tooltip="Whether to open Texthooking page when the script starts.", row=self.current_row,
435
522
  column=0)
436
523
  self.open_multimine_on_startup = tk.BooleanVar(value=self.settings.general.open_multimine_on_startup)
437
- ttk.Checkbutton(general_frame, variable=self.open_multimine_on_startup, bootstyle="round-toggle").grid(
524
+ ttk.Checkbutton(self.general_tab, variable=self.open_multimine_on_startup, bootstyle="round-toggle").grid(
438
525
  row=self.current_row, column=1,
439
526
  sticky='W', pady=2)
440
527
  self.current_row += 1
441
528
 
442
- HoverInfoLabelWidget(general_frame, text="GSM Texthooker Port:",
529
+ HoverInfoLabelWidget(self.general_tab, text="GSM Texthooker Port:",
443
530
  tooltip="Port for the Texthooker to run on. Only change if you know what you are doing.",
444
531
  row=self.current_row, column=0)
445
- self.texthooker_port = ttk.Entry(general_frame)
532
+ self.texthooker_port = ttk.Entry(self.general_tab)
446
533
  self.texthooker_port.insert(0, str(self.settings.general.texthooker_port))
447
534
  self.texthooker_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
448
535
  self.current_row += 1
449
536
 
450
- HoverInfoLabelWidget(general_frame, text="Current Version:", bootstyle="secondary",
537
+ HoverInfoLabelWidget(self.general_tab, text="Current Version:", bootstyle="secondary",
451
538
  tooltip="The current version of the application.", row=self.current_row, column=0)
452
- self.current_version = ttk.Label(general_frame, text=get_current_version(), bootstyle="secondary")
539
+ self.current_version = ttk.Label(self.general_tab, text=get_current_version(), bootstyle="secondary")
453
540
  self.current_version.grid(row=self.current_row, column=1, sticky='W', pady=2)
454
541
  self.current_row += 1
455
542
 
456
- HoverInfoLabelWidget(general_frame, text="Latest Version:", bootstyle="secondary",
543
+ HoverInfoLabelWidget(self.general_tab, text="Latest Version:", bootstyle="secondary",
457
544
  tooltip="The latest available version of the application.", row=self.current_row, column=0)
458
- self.latest_version = ttk.Label(general_frame, text=get_latest_version(), bootstyle="secondary")
545
+ self.latest_version = ttk.Label(self.general_tab, text=get_latest_version(), bootstyle="secondary")
459
546
  self.latest_version.grid(row=self.current_row, column=1, sticky='W', pady=2)
460
547
  self.current_row += 1
461
548
 
462
- ttk.Label(general_frame, text="Indicates important/required settings.", foreground="dark orange",
549
+ ttk.Label(self.general_tab, text="Indicates important/required settings.", foreground="dark orange",
463
550
  font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
464
551
  self.current_row += 1
465
- ttk.Label(general_frame, text="Highlights Advanced Features that may break things.", foreground="red",
552
+ ttk.Label(self.general_tab, text="Highlights Advanced Features that may break things.", foreground="red",
466
553
  font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
467
554
  self.current_row += 1
468
- ttk.Label(general_frame, text="Indicates Recommended, but completely optional settings.", foreground="green",
555
+ ttk.Label(self.general_tab, text="Indicates Recommended, but completely optional settings.", foreground="green",
469
556
  font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
470
557
  self.current_row += 1
471
- ttk.Label(general_frame, text="Every Label in settings has a tooltip with more information if you hover over them.",
472
- font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
558
+ ttk.Label(self.general_tab,
559
+ text="Every Label in settings has a tooltip with more information if you hover over them.",
560
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
473
561
  self.current_row += 1
474
562
 
475
- general_frame.grid_columnconfigure(0, weight=0) # No expansion for the label column
476
- general_frame.grid_columnconfigure(1, weight=0) # Entry column gets more space
563
+ # Add Reset to Default button
564
+ self.add_reset_button(self.general_tab, "general", self.current_row, column=0, recreate_tab=self.create_general_tab)
565
+
566
+ self.general_tab.grid_columnconfigure(0, weight=0) # No expansion for the label column
567
+ self.general_tab.grid_columnconfigure(1, weight=0) # Entry column gets more space
477
568
  for row in range(self.current_row):
478
- general_frame.grid_rowconfigure(row, minsize=30)
569
+ self.general_tab.grid_rowconfigure(row, minsize=30)
479
570
 
480
- return general_frame
571
+ return self.general_tab
481
572
 
482
573
  @new_tab
483
574
  def create_required_settings_tab(self):
@@ -487,8 +578,14 @@ class ConfigApp:
487
578
 
488
579
  @new_tab
489
580
  def create_vad_tab(self):
490
- vad_frame = ttk.Frame(self.notebook, padding=15)
491
- self.notebook.add(vad_frame, text='VAD')
581
+ if self.vad_tab is None:
582
+ self.vad_tab = ttk.Frame(self.notebook, padding=15)
583
+ self.notebook.add(self.vad_tab, text='VAD')
584
+ else:
585
+ for widget in self.vad_tab.winfo_children():
586
+ widget.destroy()
587
+
588
+ vad_frame = self.vad_tab
492
589
 
493
590
  HoverInfoLabelWidget(vad_frame, text="Voice Detection Postprocessing:",
494
591
  tooltip="Enable post-processing of audio to trim just the voiceline.",
@@ -575,16 +672,26 @@ class ConfigApp:
575
672
  self.splice_padding.grid(row=self.current_row, column=3, sticky='EW', pady=2)
576
673
  self.current_row += 1
577
674
 
675
+ self.add_reset_button(vad_frame, "vad", self.current_row, 0, self.create_vad_tab)
676
+
578
677
  for col in range(5):
579
678
  vad_frame.grid_columnconfigure(col, weight=0)
580
679
 
581
680
  for row in range(self.current_row):
582
681
  vad_frame.grid_rowconfigure(row, minsize=30)
583
682
 
683
+ return vad_frame
684
+
584
685
  @new_tab
585
686
  def create_paths_tab(self):
586
- paths_frame = ttk.Frame(self.notebook, padding=15)
587
- self.notebook.add(paths_frame, text='Paths')
687
+ if self.paths_tab is None:
688
+ self.paths_tab = ttk.Frame(self.notebook, padding=15)
689
+ self.notebook.add(self.paths_tab, text='Paths')
690
+ else:
691
+ for widget in self.paths_tab.winfo_children():
692
+ widget.destroy()
693
+
694
+ paths_frame = self.paths_tab
588
695
 
589
696
  HoverInfoLabelWidget(paths_frame, text="Folder to Watch:", tooltip="Path where the OBS Replays will be saved.",
590
697
  foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
@@ -642,6 +749,8 @@ class ConfigApp:
642
749
  row=self.current_row, column=1, sticky='W', pady=2)
643
750
  self.current_row += 1
644
751
 
752
+ self.add_reset_button(paths_frame, "paths", self.current_row, 0, self.create_paths_tab)
753
+
645
754
  paths_frame.grid_columnconfigure(0, weight=0)
646
755
  paths_frame.grid_columnconfigure(1, weight=0)
647
756
  paths_frame.grid_columnconfigure(2, weight=0)
@@ -665,8 +774,14 @@ class ConfigApp:
665
774
 
666
775
  @new_tab
667
776
  def create_anki_tab(self):
668
- anki_frame = ttk.Frame(self.notebook, padding=15)
669
- self.notebook.add(anki_frame, text='Anki')
777
+ if self.anki_tab is None:
778
+ self.anki_tab = ttk.Frame(self.notebook, padding=15)
779
+ self.notebook.add(self.anki_tab, text='Anki')
780
+ else:
781
+ for widget in self.anki_tab.winfo_children():
782
+ widget.destroy()
783
+
784
+ anki_frame = self.anki_tab
670
785
 
671
786
  HoverInfoLabelWidget(anki_frame, text="Update Anki:", tooltip="Automatically update Anki with new data.",
672
787
  row=self.current_row, column=0)
@@ -761,7 +876,6 @@ class ConfigApp:
761
876
  self.parent_tag.insert(0, self.settings.anki.parent_tag)
762
877
  self.parent_tag.grid(row=self.current_row, column=1, sticky='EW', pady=2)
763
878
 
764
-
765
879
  self.current_row += 1
766
880
 
767
881
  HoverInfoLabelWidget(anki_frame, text="Overwrite Audio:", tooltip="Overwrite existing audio in Anki cards.",
@@ -790,6 +904,8 @@ class ConfigApp:
790
904
  row=self.current_row, column=1, sticky='W', pady=2)
791
905
  self.current_row += 1
792
906
 
907
+ self.add_reset_button(anki_frame, "anki", self.current_row, 0, self.create_anki_tab)
908
+
793
909
  anki_frame.grid_columnconfigure(0, weight=0)
794
910
  anki_frame.grid_columnconfigure(1, weight=0)
795
911
 
@@ -807,8 +923,14 @@ class ConfigApp:
807
923
 
808
924
  @new_tab
809
925
  def create_features_tab(self):
810
- features_frame = ttk.Frame(self.notebook, padding=15)
811
- self.notebook.add(features_frame, text='Features')
926
+ if self.features_tab is None:
927
+ self.features_tab = ttk.Frame(self.notebook, padding=15)
928
+ self.notebook.add(self.features_tab, text='Features')
929
+ else:
930
+ for widget in self.features_tab.winfo_children():
931
+ widget.destroy()
932
+
933
+ features_frame = self.features_tab
812
934
 
813
935
  HoverInfoLabelWidget(features_frame, text="Notify on Update:", tooltip="Notify the user when an update occurs.",
814
936
  row=self.current_row, column=0)
@@ -856,16 +978,26 @@ class ConfigApp:
856
978
  pady=2)
857
979
  self.current_row += 1
858
980
 
981
+ self.add_reset_button(features_frame, "features", self.current_row, 0, self.create_features_tab)
982
+
859
983
  for col in range(3):
860
984
  features_frame.grid_columnconfigure(col, weight=0)
861
985
 
862
986
  for row in range(self.current_row):
863
987
  features_frame.grid_rowconfigure(row, minsize=30)
864
988
 
989
+ return features_frame
990
+
865
991
  @new_tab
866
992
  def create_screenshot_tab(self):
867
- screenshot_frame = ttk.Frame(self.notebook, padding=15)
868
- self.notebook.add(screenshot_frame, text='Screenshot')
993
+ if self.screenshot_tab is None:
994
+ self.screenshot_tab = ttk.Frame(self.notebook, padding=15)
995
+ self.notebook.add(self.screenshot_tab, text='Screenshot')
996
+ else:
997
+ for widget in self.screenshot_tab.winfo_children():
998
+ widget.destroy()
999
+
1000
+ screenshot_frame = self.screenshot_tab
869
1001
 
870
1002
  HoverInfoLabelWidget(screenshot_frame, text="Enabled:", tooltip="Enable or disable screenshot processing.",
871
1003
  row=self.current_row, column=0)
@@ -951,12 +1083,16 @@ class ConfigApp:
951
1083
  row=self.current_row, column=1, sticky='W', pady=2)
952
1084
  self.current_row += 1
953
1085
 
1086
+ self.add_reset_button(screenshot_frame, "screenshot", self.current_row, 0, self.create_screenshot_tab)
1087
+
954
1088
  for col in range(3):
955
1089
  screenshot_frame.grid_columnconfigure(col, weight=0)
956
1090
 
957
1091
  for row in range(self.current_row):
958
1092
  screenshot_frame.grid_rowconfigure(row, minsize=30)
959
1093
 
1094
+ return screenshot_frame
1095
+
960
1096
  def update_audio_ffmpeg_settings(self, event):
961
1097
  selected_option = self.ffmpeg_audio_preset_options.get()
962
1098
  if selected_option in self.ffmpeg_audio_preset_options_map:
@@ -968,8 +1104,14 @@ class ConfigApp:
968
1104
 
969
1105
  @new_tab
970
1106
  def create_audio_tab(self):
971
- audio_frame = ttk.Frame(self.notebook, padding=15)
972
- self.notebook.add(audio_frame, text='Audio')
1107
+ if self.audio_tab is None:
1108
+ self.audio_tab = ttk.Frame(self.notebook, padding=15)
1109
+ self.notebook.add(self.audio_tab, text='Audio')
1110
+ else:
1111
+ for widget in self.audio_tab.winfo_children():
1112
+ widget.destroy()
1113
+
1114
+ audio_frame = self.audio_tab
973
1115
 
974
1116
  HoverInfoLabelWidget(audio_frame, text="Enabled:", tooltip="Enable or disable audio processing.",
975
1117
  row=self.current_row, column=0)
@@ -993,12 +1135,12 @@ class ConfigApp:
993
1135
  self.beginning_offset.insert(0, str(self.settings.audio.beginning_offset))
994
1136
  self.beginning_offset.grid(row=self.current_row, column=1, sticky='EW', pady=2)
995
1137
 
996
- ttk.Button(audio_frame, text="Find Offset (WIP)", command=self.call_audio_offset_selector, bootstyle="info").grid(
1138
+ ttk.Button(audio_frame, text="Find Offset (WIP)", command=self.call_audio_offset_selector,
1139
+ bootstyle="info").grid(
997
1140
  row=self.current_row, column=2, sticky='EW', pady=2, padx=5)
998
1141
 
999
1142
  self.current_row += 1
1000
1143
 
1001
-
1002
1144
  HoverInfoLabelWidget(audio_frame, text="Audio Extraction End Offset:",
1003
1145
  tooltip="Offset in seconds to trim from the end before VAD processing starts. Warning: May Result in lost audio if negative.",
1004
1146
  foreground="red", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
@@ -1069,12 +1211,15 @@ class ConfigApp:
1069
1211
  column=1, pady=5)
1070
1212
  self.current_row += 1
1071
1213
 
1214
+ self.add_reset_button(audio_frame, "audio", self.current_row, 0, self.create_audio_tab)
1215
+
1072
1216
  for col in range(5):
1073
1217
  audio_frame.grid_columnconfigure(col, weight=0)
1074
1218
 
1075
1219
  for row in range(self.current_row):
1076
1220
  audio_frame.grid_rowconfigure(row, minsize=30)
1077
1221
 
1222
+ return audio_frame
1078
1223
 
1079
1224
  def call_audio_offset_selector(self):
1080
1225
  try:
@@ -1120,11 +1265,16 @@ class ConfigApp:
1120
1265
  logger.error(f"An unexpected error occurred: {e}")
1121
1266
  return None
1122
1267
 
1123
-
1124
1268
  @new_tab
1125
1269
  def create_obs_tab(self):
1126
- obs_frame = ttk.Frame(self.notebook, padding=15)
1127
- self.notebook.add(obs_frame, text='OBS')
1270
+ if self.obs_tab is None:
1271
+ self.obs_tab = ttk.Frame(self.notebook, padding=15)
1272
+ self.notebook.add(self.obs_tab, text='OBS')
1273
+ else:
1274
+ for widget in self.obs_tab.winfo_children():
1275
+ widget.destroy()
1276
+
1277
+ obs_frame = self.obs_tab
1128
1278
 
1129
1279
  HoverInfoLabelWidget(obs_frame, text="Enabled:", tooltip="Enable or disable OBS integration.",
1130
1280
  row=self.current_row, column=0)
@@ -1185,16 +1335,26 @@ class ConfigApp:
1185
1335
  self.minimum_replay_size.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1186
1336
  self.current_row += 1
1187
1337
 
1338
+ self.add_reset_button(obs_frame, "obs", self.current_row, 0, self.create_obs_tab)
1339
+
1188
1340
  for col in range(3):
1189
1341
  obs_frame.grid_columnconfigure(col, weight=0)
1190
1342
 
1191
1343
  for row in range(self.current_row):
1192
1344
  obs_frame.grid_rowconfigure(row, minsize=30)
1193
1345
 
1346
+ return obs_frame
1347
+
1194
1348
  @new_tab
1195
1349
  def create_profiles_tab(self):
1196
- profiles_frame = ttk.Frame(self.notebook, padding=15)
1197
- self.notebook.add(profiles_frame, text='Profiles')
1350
+ if self.profiles_tab is None:
1351
+ self.profiles_tab = ttk.Frame(self.notebook, padding=15)
1352
+ self.notebook.add(self.profiles_tab, text='Profiles')
1353
+ else:
1354
+ for widget in self.profiles_tab.winfo_children():
1355
+ widget.destroy()
1356
+
1357
+ profiles_frame = self.profiles_tab
1198
1358
 
1199
1359
  HoverInfoLabelWidget(profiles_frame, text="Select Profile:", tooltip="Select a profile to load its settings.",
1200
1360
  row=self.current_row, column=0)
@@ -1244,9 +1404,11 @@ class ConfigApp:
1244
1404
  for row in range(self.current_row):
1245
1405
  profiles_frame.grid_rowconfigure(row, minsize=30)
1246
1406
 
1407
+ return profiles_frame
1408
+
1247
1409
  def on_obs_scene_select(self, event):
1248
1410
  self.settings.scenes = [self.obs_scene_listbox.get(i) for i in
1249
- self.obs_scene_listbox.curselection()]
1411
+ self.obs_scene_listbox.curselection()]
1250
1412
  self.obs_scene_listbox_changed = True
1251
1413
 
1252
1414
  def refresh_obs_scenes(self):
@@ -1263,8 +1425,14 @@ class ConfigApp:
1263
1425
 
1264
1426
  @new_tab
1265
1427
  def create_advanced_tab(self):
1266
- advanced_frame = ttk.Frame(self.notebook, padding=15)
1267
- self.notebook.add(advanced_frame, text='Advanced')
1428
+ if self.advanced_tab is None:
1429
+ self.advanced_tab = ttk.Frame(self.notebook, padding=15)
1430
+ self.notebook.add(self.advanced_tab, text='Advanced')
1431
+ else:
1432
+ for widget in self.advanced_tab.winfo_children():
1433
+ widget.destroy()
1434
+
1435
+ advanced_frame = self.advanced_tab
1268
1436
 
1269
1437
  ttk.Label(advanced_frame, text="Note: Only one of these will take effect, prioritizing audio.",
1270
1438
  foreground="red", font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=3,
@@ -1339,7 +1507,6 @@ class ConfigApp:
1339
1507
  self.plaintext_websocket_export_port.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1340
1508
  self.current_row += 1
1341
1509
 
1342
-
1343
1510
  # HoverInfoLabelWidget(advanced_frame, text="Use Anki Creation Date for Audio Timing:",
1344
1511
  # tooltip="Use the Anki note creation date for audio timing instead of the OBS replay time.",
1345
1512
  # row=self.current_row, column=0)
@@ -1366,20 +1533,31 @@ class ConfigApp:
1366
1533
  HoverInfoLabelWidget(advanced_frame, text="Vosk URL:", tooltip="URL for connecting to the Vosk server.",
1367
1534
  row=self.current_row, column=0)
1368
1535
  self.vosk_url = ttk.Combobox(advanced_frame, values=[VOSK_BASE, VOSK_SMALL], state="readonly")
1369
- self.vosk_url.set(VOSK_BASE if self.settings.vad.vosk_url == 'https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' else VOSK_SMALL)
1536
+ self.vosk_url.set(
1537
+ VOSK_BASE if self.settings.vad.vosk_url == 'https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' else VOSK_SMALL)
1370
1538
  self.vosk_url.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1371
1539
  self.current_row += 1
1372
1540
 
1541
+ self.add_reset_button(advanced_frame, "advanced", self.current_row, 0, self.create_advanced_tab)
1542
+
1373
1543
  for col in range(4):
1374
1544
  advanced_frame.grid_columnconfigure(col, weight=0)
1375
1545
 
1376
1546
  for row in range(self.current_row):
1377
1547
  advanced_frame.grid_rowconfigure(row, minsize=30)
1378
1548
 
1549
+ return advanced_frame
1550
+
1379
1551
  @new_tab
1380
1552
  def create_ai_tab(self):
1381
- ai_frame = ttk.Frame(self.notebook, padding=15)
1382
- self.notebook.add(ai_frame, text='AI')
1553
+ if self.ai_tab is None:
1554
+ self.ai_tab = ttk.Frame(self.notebook, padding=15)
1555
+ self.notebook.add(self.ai_tab, text='AI')
1556
+ else:
1557
+ for widget in self.ai_tab.winfo_children():
1558
+ widget.destroy()
1559
+
1560
+ ai_frame = self.ai_tab
1383
1561
 
1384
1562
  HoverInfoLabelWidget(ai_frame, text="Enabled:", tooltip="Enable or disable AI integration.",
1385
1563
  row=self.current_row, column=0)
@@ -1397,10 +1575,12 @@ class ConfigApp:
1397
1575
 
1398
1576
  HoverInfoLabelWidget(ai_frame, text="Gemini AI Model:", tooltip="Select the AI model to use.",
1399
1577
  row=self.current_row, column=0)
1400
- self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite',
1401
- 'gemini-2.5-pro-preview-05-06',
1402
- 'gemini-2.5-flash-preview-05-20'], state="readonly")
1403
- self.gemini_model.set(self.settings.ai.gemini_model)
1578
+ self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.5-flash', 'gemini-2.5-pro','gemini-2.0-flash', 'gemini-2.0-flash-lite',
1579
+ 'gemini-2.5-flash-lite-preview-06-17'], state="readonly")
1580
+ try:
1581
+ self.gemini_model.set(self.settings.ai.gemini_model)
1582
+ except Exception:
1583
+ self.gemini_model.set('gemini-2.5-flash')
1404
1584
  self.gemini_model.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1405
1585
  self.current_row += 1
1406
1586
 
@@ -1460,6 +1640,8 @@ class ConfigApp:
1460
1640
  self.custom_prompt.grid(row=self.current_row, column=1, sticky='EW', pady=2)
1461
1641
  self.current_row += 1
1462
1642
 
1643
+ self.add_reset_button(ai_frame, "ai", self.current_row, 0, self.create_ai_tab)
1644
+
1463
1645
  for col in range(3):
1464
1646
  ai_frame.grid_columnconfigure(col, weight=0)
1465
1647
 
@@ -173,7 +173,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
173
173
  except Exception as e:
174
174
  if mined_line:
175
175
  anki_results[mined_line.id] = AnkiUpdateResult.failure()
176
- logger.error(f"Failed Processing and/or adding to Anki: Reason {e}")
176
+ logger.error(f"Failed Processing and/or adding to Anki: Reason {e}", exc_info=True)
177
177
  logger.debug(f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
178
178
  notification.send_error_no_anki_update()
179
179
  finally:
@@ -184,9 +184,10 @@ class Audio:
184
184
 
185
185
  def __post_init__(self):
186
186
  self.ffmpeg_reencode_options_to_use = self.ffmpeg_reencode_options.replace("{format}", self.extension).replace("{encoder}", supported_formats.get(self.extension, ''))
187
-
188
- self.anki_media_collection = os.path.normpath(self.anki_media_collection)
189
- self.external_tool = os.path.normpath(self.external_tool)
187
+ if self.anki_media_collection:
188
+ self.anki_media_collection = os.path.normpath(self.anki_media_collection)
189
+ if self.external_tool:
190
+ self.external_tool = os.path.normpath(self.external_tool)
190
191
 
191
192
 
192
193
 
@@ -220,7 +221,7 @@ class VAD:
220
221
  language: str = 'ja'
221
222
  vosk_url: str = VOSK_BASE
222
223
  selected_vad_model: str = WHISPER
223
- backup_vad_model: str = OFF
224
+ backup_vad_model: str = SILERO
224
225
  trim_beginning: bool = False
225
226
  beginning_offset: float = -0.25
226
227
  add_audio_on_no_results: bool = False
@@ -264,7 +265,7 @@ class Ai:
264
265
  enabled: bool = False
265
266
  anki_field: str = ''
266
267
  provider: str = AI_GEMINI
267
- gemini_model: str = 'gemini-2.0-flash'
268
+ gemini_model: str = 'gemini-2.5-flash'
268
269
  groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
269
270
  api_key: str = '' # Deprecated
270
271
  gemini_api_key: str = ''
@@ -40,12 +40,19 @@ def download_obs_if_needed():
40
40
  logger.info("OBS directory exists but executable is missing. Re-downloading OBS...")
41
41
  shutil.rmtree(obs_path)
42
42
 
43
+ def get_windows_obs_url():
44
+ machine = platform.machine().lower()
45
+ if machine in ['arm64', 'aarch64']:
46
+ return next(asset['browser_download_url'] for asset in latest_release['assets'] if
47
+ asset['name'].endswith('Windows-arm64.zip'))
48
+ return next(asset['browser_download_url'] for asset in latest_release['assets'] if
49
+ asset['name'].endswith('Windows-x64.zip'))
50
+
43
51
  latest_release_url = "https://api.github.com/repos/obsproject/obs-studio/releases/latest"
44
52
  with urllib.request.urlopen(latest_release_url) as response:
45
53
  latest_release = json.load(response)
46
54
  obs_url = {
47
- "Windows": next(asset['browser_download_url'] for asset in latest_release['assets'] if
48
- asset['name'].endswith('Windows.zip')),
55
+ "Windows": get_windows_obs_url(),
49
56
  "Linux": next(asset['browser_download_url'] for asset in latest_release['assets'] if
50
57
  asset['name'].endswith('Ubuntu-24.04-x86_64.deb')),
51
58
  "Darwin": next(asset['browser_download_url'] for asset in latest_release['assets'] if
@@ -157,27 +157,32 @@ def combine_dialogue(dialogue_lines, new_lines=None):
157
157
 
158
158
  return new_lines
159
159
 
160
- def wait_for_stable_file(file_path, timeout=10, check_interval=0.1):
160
+ def wait_for_stable_file(file_path, timeout=10, check_interval=0.5):
161
161
  elapsed_time = 0
162
162
  last_size = -1
163
163
 
164
+ logger.info(f"Waiting for file '{file_path}' to stabilize or become accessible...")
165
+
164
166
  while elapsed_time < timeout:
165
167
  try:
166
168
  current_size = os.path.getsize(file_path)
167
169
  if current_size == last_size:
168
170
  try:
169
- with open(file_path, 'rb') as f:
171
+ with open(file_path, 'rb'):
170
172
  return True
171
- except Exception as e:
172
- time.sleep(check_interval)
173
- elapsed_time += check_interval
173
+ except IOError:
174
+ pass
174
175
  last_size = current_size
175
- time.sleep(check_interval)
176
- elapsed_time += check_interval
176
+ except FileNotFoundError:
177
+ last_size = -1
177
178
  except Exception as e:
178
- logger.warning(f"Error checking file size, will still try updating Anki Card!: {e}")
179
- return False
180
- logger.warning("File size did not stabilize within the timeout period. Continuing...")
179
+ logger.warning(f"Error checking file {file_path}, will retry: {e}")
180
+ last_size = -1
181
+
182
+ time.sleep(check_interval)
183
+ elapsed_time += check_interval
184
+
185
+ logger.warning(f"File '{file_path}' did not stabilize or become accessible within {timeout} seconds. Continuing...")
181
186
  return False
182
187
 
183
188
  def isascii(s: str):
@@ -4,7 +4,6 @@ from datetime import datetime
4
4
  from difflib import SequenceMatcher
5
5
  from typing import Optional
6
6
 
7
- from GameSentenceMiner.obs import get_current_game
8
7
  from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
9
8
  from GameSentenceMiner.util.configuration import logger, get_config, gsm_state
10
9
  from GameSentenceMiner.util.model import AnkiCard
@@ -106,14 +105,15 @@ def similar(a, b):
106
105
  return SequenceMatcher(None, a, b).ratio()
107
106
 
108
107
 
109
- def one_contains_the_other(a, b):
110
- return a in b or b in a
111
-
112
-
113
- def lines_match(a, b):
114
- similarity = similar(a, b)
115
- logger.debug(f"Comparing: {a} with {b} - Similarity: {similarity}, Or One contains the other: {one_contains_the_other(a, b)}")
116
- return similar(a, b) >= 0.80 or one_contains_the_other(a, b)
108
+ def lines_match(texthooker_sentence, anki_sentence):
109
+ texthooker_sentence = texthooker_sentence.replace("\n", "").replace("\r", "").strip()
110
+ anki_sentence = anki_sentence.replace("\n", "").replace("\r", "").strip()
111
+ similarity = similar(texthooker_sentence, anki_sentence)
112
+ if texthooker_sentence in anki_sentence:
113
+ logger.debug(f"One contains the other: {texthooker_sentence} in {anki_sentence} - Similarity: {similarity}")
114
+ elif anki_sentence in texthooker_sentence:
115
+ logger.debug(f"One contains the other: {anki_sentence} in {texthooker_sentence} - Similarity: {similarity}")
116
+ return (anki_sentence in texthooker_sentence) or (texthooker_sentence in anki_sentence and similarity > 0.8)
117
117
 
118
118
 
119
119
  def get_text_event(last_note) -> GameLine:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.14
3
+ Version: 2.10.16
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.10.14
3
+ Version: 2.10.16
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "GameSentenceMiner"
10
- version = "2.10.14"
10
+ version = "2.10.16"
11
11
  description = "A tool for mining sentences from games. Update: Full UI Re-design"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"