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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (76) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +6 -6
  2. GameSentenceMiner/anki.py +236 -152
  3. GameSentenceMiner/gametext.py +7 -4
  4. GameSentenceMiner/gsm.py +49 -10
  5. GameSentenceMiner/locales/en_us.json +7 -3
  6. GameSentenceMiner/locales/ja_jp.json +8 -4
  7. GameSentenceMiner/locales/zh_cn.json +8 -4
  8. GameSentenceMiner/obs.py +238 -59
  9. GameSentenceMiner/ocr/owocr_helper.py +1 -1
  10. GameSentenceMiner/tools/ss_selector.py +7 -8
  11. GameSentenceMiner/ui/__init__.py +0 -0
  12. GameSentenceMiner/ui/anki_confirmation.py +187 -0
  13. GameSentenceMiner/{config_gui.py → ui/config_gui.py} +100 -35
  14. GameSentenceMiner/ui/screenshot_selector.py +215 -0
  15. GameSentenceMiner/util/configuration.py +124 -22
  16. GameSentenceMiner/util/db.py +22 -13
  17. GameSentenceMiner/util/downloader/download_tools.py +2 -2
  18. GameSentenceMiner/util/ffmpeg.py +24 -30
  19. GameSentenceMiner/util/get_overlay_coords.py +34 -34
  20. GameSentenceMiner/util/gsm_utils.py +31 -1
  21. GameSentenceMiner/util/text_log.py +11 -9
  22. GameSentenceMiner/vad.py +31 -12
  23. GameSentenceMiner/web/database_api.py +742 -123
  24. GameSentenceMiner/web/static/css/dashboard-shared.css +241 -0
  25. GameSentenceMiner/web/static/css/kanji-grid.css +94 -2
  26. GameSentenceMiner/web/static/css/overview.css +850 -0
  27. GameSentenceMiner/web/static/css/popups-shared.css +126 -0
  28. GameSentenceMiner/web/static/css/shared.css +97 -0
  29. GameSentenceMiner/web/static/css/stats.css +192 -597
  30. GameSentenceMiner/web/static/js/anki_stats.js +6 -4
  31. GameSentenceMiner/web/static/js/database.js +209 -5
  32. GameSentenceMiner/web/static/js/goals.js +610 -0
  33. GameSentenceMiner/web/static/js/kanji-grid.js +267 -4
  34. GameSentenceMiner/web/static/js/overview.js +1176 -0
  35. GameSentenceMiner/web/static/js/shared.js +25 -0
  36. GameSentenceMiner/web/static/js/stats.js +154 -1459
  37. GameSentenceMiner/web/stats.py +2 -2
  38. GameSentenceMiner/web/templates/anki_stats.html +5 -0
  39. GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +17 -0
  40. GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +29 -0
  41. GameSentenceMiner/web/templates/components/kanji_grid/grade.json +17 -0
  42. GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +17 -0
  43. GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +13 -0
  44. GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +33 -0
  45. GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +41 -0
  46. GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +25 -0
  47. GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +29 -0
  48. GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +37 -0
  49. GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +161 -0
  50. GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +13 -0
  51. GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +37 -0
  52. GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +61 -0
  53. GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +33 -0
  54. GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +47 -0
  55. GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +25 -0
  56. GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +25 -0
  57. GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +25 -0
  58. GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +21 -0
  59. GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +45 -0
  60. GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +13 -0
  61. GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +249 -0
  62. GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +33 -0
  63. GameSentenceMiner/web/templates/components/navigation.html +3 -1
  64. GameSentenceMiner/web/templates/database.html +73 -1
  65. GameSentenceMiner/web/templates/goals.html +376 -0
  66. GameSentenceMiner/web/templates/index.html +13 -11
  67. GameSentenceMiner/web/templates/overview.html +416 -0
  68. GameSentenceMiner/web/templates/stats.html +46 -251
  69. GameSentenceMiner/web/texthooking_page.py +18 -0
  70. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/METADATA +5 -1
  71. gamesentenceminer-2.18.1.dist-info/RECORD +132 -0
  72. gamesentenceminer-2.17.7.dist-info/RECORD +0 -98
  73. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/WHEEL +0 -0
  74. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/entry_points.txt +0 -0
  75. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/licenses/LICENSE +0 -0
  76. {gamesentenceminer-2.17.7.dist-info → gamesentenceminer-2.18.1.dist-info}/top_level.txt +0 -0
@@ -38,7 +38,7 @@ logger.setLevel(logging.DEBUG)
38
38
  # Create a file handler for logging
39
39
  log_file = os.path.join(get_app_directory(), "logs", "ocr_log.txt")
40
40
  os.makedirs(os.path.join(get_app_directory(), "logs"), exist_ok=True)
41
- file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
41
+ file_handler = RotatingFileHandler(log_file, maxBytes=1024 * 1024, backupCount=2, encoding='utf-8')
42
42
  file_handler.setLevel(logging.DEBUG)
43
43
  # Create a formatter and set it for the handler
44
44
  formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
@@ -1,22 +1,21 @@
1
-
1
+ # TODO REMOVE THIS, DEPRECATED
2
2
 
3
3
  import os
4
4
  import sys
5
5
  import subprocess
6
6
 
7
+ import tkinter as tk
8
+ from PIL import Image, ImageTk
9
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
10
+ from GameSentenceMiner.util.configuration import get_temporary_directory, logger, ffmpeg_base_command_list
11
+ from GameSentenceMiner.util import ffmpeg
12
+
7
13
  # Suppress stdout and stderr during imports
8
14
  sys_stdout = sys.stdout
9
15
  sys_stderr = sys.stderr
10
16
  sys.stdout = open(os.devnull, 'w')
11
17
  sys.stderr = open(os.devnull, 'w')
12
18
 
13
- import tkinter as tk
14
- from PIL import Image, ImageTk
15
- from GameSentenceMiner.util.gsm_utils import sanitize_filename
16
- from GameSentenceMiner.util.configuration import get_temporary_directory, logger
17
- from GameSentenceMiner.util.ffmpeg import ffmpeg_base_command_list
18
- from GameSentenceMiner.util import ffmpeg
19
-
20
19
  def extract_frames(video_path, timestamp, temp_dir, mode):
21
20
  frame_paths = []
22
21
  timestamp_number = float(timestamp)
File without changes
@@ -0,0 +1,187 @@
1
+ import tkinter as tk
2
+ from tkinter import scrolledtext
3
+ from PIL import Image, ImageTk
4
+
5
+ import ttkbootstrap as ttk
6
+ from GameSentenceMiner.util.configuration import get_config, logger, gsm_state
7
+
8
+ import platform
9
+ import subprocess
10
+ import os
11
+
12
+ class AnkiConfirmationDialog(tk.Toplevel):
13
+ """
14
+ A modal dialog to confirm Anki card details and choose an audio option.
15
+ """
16
+ def __init__(self, parent, config_app, expression, sentence, screenshot_path, audio_path, translation, screenshot_timestamp):
17
+ super().__init__(parent)
18
+ self.config_app = config_app
19
+ self.screenshot_timestamp = screenshot_timestamp
20
+
21
+ # Initialize screenshot_path here, will be updated by button if needed
22
+ self.screenshot_path = screenshot_path
23
+
24
+ self.title("Confirm Anki Card Details")
25
+ self.result = None # This will store the user's choice
26
+
27
+ # This makes the dialog block interaction with other windows.
28
+ self.grab_set()
29
+
30
+ # --- Create and lay out widgets ---
31
+ self._create_widgets(expression, sentence, screenshot_path, audio_path, translation)
32
+
33
+ # --- Smarter Centering Logic ---
34
+ self.update_idletasks()
35
+
36
+ if parent.state() == 'withdrawn':
37
+ screen_width = self.winfo_screenwidth()
38
+ screen_height = self.winfo_screenheight()
39
+ dialog_width = self.winfo_width()
40
+ dialog_height = self.winfo_height()
41
+ x = (screen_width // 2) - (dialog_width // 2)
42
+ y = (screen_height // 2) - (dialog_height // 2)
43
+ self.geometry(f'+{x}+{y}')
44
+ else:
45
+ self.transient(parent)
46
+ parent_x = parent.winfo_x()
47
+ parent_y = parent.winfo_y()
48
+ parent_width = parent.winfo_width()
49
+ parent_height = parent.winfo_height()
50
+ dialog_width = self.winfo_width()
51
+ dialog_height = self.winfo_height()
52
+ x = parent_x + (parent_width // 2) - (dialog_width // 2)
53
+ y = parent_y + (parent_height // 2) - (dialog_height // 2)
54
+ self.geometry(f'+{x}+{y}')
55
+
56
+ self.protocol("WM_DELETE_WINDOW", self._on_cancel)
57
+ self.attributes('-topmost', True)
58
+ self.wait_window(self)
59
+
60
+ def _create_widgets(self, expression, sentence, screenshot_path, audio_path, translation):
61
+ main_frame = ttk.Frame(self, padding=20)
62
+ main_frame.pack(expand=True, fill="both")
63
+
64
+ row = 0
65
+
66
+ # Expression
67
+ ttk.Label(main_frame, text=f"{get_config().anki.word_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
68
+ ttk.Label(main_frame, text=expression, wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
69
+ row += 1
70
+
71
+ # Sentence
72
+ ttk.Label(main_frame, text=f"{get_config().anki.sentence_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
73
+ sentence_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
74
+ sentence_text.insert(tk.END, sentence)
75
+ sentence_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
76
+ self.sentence_text = sentence_text
77
+ row += 1
78
+
79
+ # Translation
80
+ ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
81
+ translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
82
+ translation_text.insert(tk.END, translation)
83
+ translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
84
+ self.translation_text = translation_text
85
+ row += 1
86
+
87
+ # Screenshot
88
+ ttk.Label(main_frame, text=f"{get_config().anki.picture_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
89
+
90
+ # <<< CHANGED: Step 1 - Create and store the label for the image
91
+ self.image_label = ttk.Label(main_frame)
92
+ self.image_label.grid(row=row, column=1, sticky="w", padx=5, pady=2)
93
+
94
+ try:
95
+ img = Image.open(screenshot_path)
96
+ img.thumbnail((400, 300))
97
+ self.photo_image = ImageTk.PhotoImage(img)
98
+ # Configure the label we just created
99
+ self.image_label.config(image=self.photo_image)
100
+ # Keep a reference on the widget itself to prevent garbage collection!
101
+ self.image_label.image = self.photo_image
102
+ except Exception as e:
103
+ # Configure the label to show an error message
104
+ self.image_label.config(text=f"Could not load image:\n{screenshot_path}\n{e}", foreground="red")
105
+
106
+ # Open Screenshot Selector button
107
+ ttk.Button(main_frame, text="Open Screenshot Selector", command=self._get_different_screenshot).grid(row=row, column=2, sticky="w", padx=5, pady=2)
108
+
109
+ row += 1
110
+
111
+ # Audio Path
112
+ ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
113
+ ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
114
+ if audio_path and os.path.isfile(audio_path):
115
+ ttk.Button(main_frame, text="Play Audio", command=lambda: self._play_audio(audio_path)).grid(row=row, column=2, sticky="w", padx=5, pady=2)
116
+
117
+ row += 1
118
+
119
+ # Action Buttons
120
+ button_frame = ttk.Frame(main_frame)
121
+ button_frame.grid(row=row, column=0, columnspan=2, pady=15)
122
+ if audio_path and os.path.isfile(audio_path):
123
+ ttk.Button(button_frame, text="Voice", command=self._on_voice, bootstyle="success").pack(side="left", padx=10)
124
+ ttk.Button(button_frame, text="NO Voice", command=self._on_no_voice, bootstyle="danger").pack(side="left", padx=10)
125
+ else:
126
+ ttk.Button(button_frame, text="Confirm", command=self._on_no_voice, bootstyle="primary").pack(side="left", padx=10)
127
+
128
+
129
+ def _get_different_screenshot(self):
130
+ video_path = gsm_state.current_replay
131
+ new_screenshot_path = self.config_app.show_screenshot_selector(
132
+ video_path, self.screenshot_timestamp, mode=get_config().screenshot.screenshot_timing_setting
133
+ )
134
+
135
+ # If the user cancels the selector, it might return None or an empty string
136
+ if not new_screenshot_path:
137
+ return
138
+
139
+ self.screenshot_path = new_screenshot_path # Update the path to be returned later
140
+
141
+ try:
142
+ img = Image.open(self.screenshot_path)
143
+ img.thumbnail((400, 300))
144
+ # Create the new image object
145
+ self.photo_image = ImageTk.PhotoImage(img)
146
+
147
+ # <<< CHANGED: Step 2 - Update the label with the new image
148
+ self.image_label.config(image=self.photo_image, text="") # Clear any previous error text
149
+
150
+ # This is crucial! Keep a reference to the new image object on the widget
151
+ # itself, so it doesn't get garbage-collected.
152
+ self.image_label.image = self.photo_image
153
+
154
+ except Exception as e:
155
+ # Handle cases where the newly selected file is invalid
156
+ self.image_label.config(image=None, text=f"Could not load new image:\n{e}", foreground="red")
157
+ self.image_label.image = None # Clear old image reference
158
+
159
+
160
+ def _play_audio(self, audio_path):
161
+ if not os.path.isfile(audio_path):
162
+ print(f"Audio file does not exist: {audio_path}")
163
+ return
164
+ try:
165
+ if platform.system() == "Windows":
166
+ os.startfile(audio_path)
167
+ elif platform.system() == "Darwin":
168
+ subprocess.run(["open", audio_path])
169
+ else:
170
+ subprocess.run(["xdg-open", audio_path])
171
+ except Exception as e:
172
+ print(f"Failed to play audio: {e}")
173
+
174
+ def _on_voice(self):
175
+ # The screenshot_path is now correctly updated if the user chose a new one
176
+ self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
177
+ self.destroy()
178
+
179
+ def _on_no_voice(self):
180
+ self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
181
+ self.destroy()
182
+
183
+ def _on_cancel(self):
184
+ # We block the cancel button, but if you wanted to enable it:
185
+ # self.result = None
186
+ # self.destroy()
187
+ pass
@@ -7,11 +7,14 @@ import sys
7
7
  import time
8
8
  import tkinter as tk
9
9
  from tkinter import filedialog, messagebox, simpledialog, scrolledtext, font
10
+ from PIL import Image, ImageTk
10
11
 
11
12
  import pyperclip
12
13
  import ttkbootstrap as ttk
13
14
 
14
15
  from GameSentenceMiner import obs
16
+ from GameSentenceMiner.ui.anki_confirmation import AnkiConfirmationDialog
17
+ from GameSentenceMiner.ui.screenshot_selector import ScreenshotSelectorDialog
15
18
  from GameSentenceMiner.util import configuration
16
19
  from GameSentenceMiner.util.communication.send import send_restart_signal
17
20
  from GameSentenceMiner.util.configuration import Config, Locale, logger, CommonLanguages, ProfileConfig, General, Paths, \
@@ -40,7 +43,7 @@ def load_localization(locale=Locale.English):
40
43
  try:
41
44
  # Use a path relative to this script file
42
45
  script_dir = os.path.dirname(os.path.abspath(__file__))
43
- lang_file = os.path.join(script_dir, 'locales', f'{locale.value}.json')
46
+ lang_file = os.path.join(script_dir, '..', 'locales', f'{locale.value}.json')
44
47
  with open(lang_file, 'r', encoding='utf-8') as f:
45
48
  return json.load(f)['python']['config']
46
49
  except (FileNotFoundError, json.JSONDecodeError) as e:
@@ -167,7 +170,6 @@ class ResetToDefaultButton(ttk.Button):
167
170
  self.tooltip.destroy()
168
171
  self.tooltip = None
169
172
 
170
-
171
173
  class ConfigApp:
172
174
  def __init__(self, root):
173
175
  self.window = root
@@ -267,6 +269,56 @@ class ConfigApp:
267
269
  def set_test_func(self, func):
268
270
  self.test_func = func
269
271
 
272
+ def show_anki_confirmation_dialog(self, expression, sentence, screenshot_path, audio_path, translation, ss_timestamp):
273
+ """
274
+ Displays a modal dialog for the user to confirm Anki card details and
275
+ choose whether to include audio.
276
+
277
+ Args:
278
+ expression (str): The target word or expression.
279
+ sentence (str): The full sentence.
280
+ screenshot_path (str): The file path to the screenshot image.
281
+ audio_path (str): The file path to the audio clip.
282
+ translation (str): The translation or definition.
283
+
284
+ Returns:
285
+ str: 'voice' if the user chooses to add with voice,
286
+ 'no_voice' if they choose to add without voice,
287
+ or None if they cancel.
288
+ """
289
+ dialog = AnkiConfirmationDialog(self.window,
290
+ self,
291
+ expression=expression,
292
+ sentence=sentence,
293
+ screenshot_path=screenshot_path,
294
+ audio_path=audio_path,
295
+ translation=translation,
296
+ screenshot_timestamp=ss_timestamp)
297
+ return dialog.result
298
+
299
+ def show_screenshot_selector(self, video_path, timestamp, mode='beginning'):
300
+ """
301
+ Displays a modal dialog for the user to select the best screenshot from
302
+ a series of extracted video frames.
303
+
304
+ Args:
305
+ video_path (str): The file path to the source video.
306
+ timestamp (str or float): The timestamp (in seconds) around which to extract frames.
307
+ mode (str): 'beginning', 'middle', or 'end'. Determines the time offset
308
+ and which frame is highlighted as the "golden" frame.
309
+
310
+ Returns:
311
+ str: The file path of the image the user selected, or None if they canceled.
312
+ """
313
+ dialog = ScreenshotSelectorDialog(self.window,
314
+ self,
315
+ video_path=video_path,
316
+ timestamp=str(timestamp), # Ensure it's a string for ffmpeg
317
+ mode=mode)
318
+
319
+ print(dialog.selected_path)
320
+ return dialog.selected_path
321
+
270
322
  def create_vars(self):
271
323
  """
272
324
  Initializes all the tkinter variables used in the configuration GUI.
@@ -290,9 +342,8 @@ class ConfigApp:
290
342
  self.obs_password_value = tk.StringVar(value=self.settings.obs.password)
291
343
  self.obs_open_obs_value = tk.BooleanVar(value=self.settings.obs.open_obs)
292
344
  self.obs_close_obs_value = tk.BooleanVar(value=self.settings.obs.close_obs)
293
- self.obs_get_game_from_scene_name_value = tk.BooleanVar(value=self.settings.obs.get_game_from_scene)
294
345
  self.obs_minimum_replay_size_value = tk.StringVar(value=str(self.settings.obs.minimum_replay_size))
295
- self.obs_turn_off_output_check_value = tk.BooleanVar(value=self.settings.obs.turn_off_output_check)
346
+ self.automatically_manage_replay_buffer_value = tk.BooleanVar(value=self.settings.obs.automatically_manage_replay_buffer)
296
347
 
297
348
  # Paths Settings
298
349
  self.folder_to_watch_value = tk.StringVar(value=self.settings.paths.folder_to_watch)
@@ -306,6 +357,7 @@ class ConfigApp:
306
357
 
307
358
  # Anki Settings
308
359
  self.update_anki_value = tk.BooleanVar(value=self.settings.anki.update_anki)
360
+ self.show_update_confirmation_dialog_value = tk.BooleanVar(value=self.settings.anki.show_update_confirmation_dialog)
309
361
  self.anki_url_value = tk.StringVar(value=self.settings.anki.url)
310
362
  self.sentence_field_value = tk.StringVar(value=self.settings.anki.sentence_field)
311
363
  self.sentence_audio_field_value = tk.StringVar(value=self.settings.anki.sentence_audio_field)
@@ -331,6 +383,7 @@ class ConfigApp:
331
383
  self.open_anki_browser_value = tk.BooleanVar(value=self.settings.features.open_anki_in_browser)
332
384
  self.backfill_audio_value = tk.BooleanVar(value=self.settings.features.backfill_audio)
333
385
  self.browser_query_value = tk.StringVar(value=self.settings.features.browser_query)
386
+ self.generate_longplay_value = tk.BooleanVar(value=self.settings.features.generate_longplay)
334
387
 
335
388
  # Screenshot Settings
336
389
  self.screenshot_enabled_value = tk.BooleanVar(value=self.settings.screenshot.enabled)
@@ -534,6 +587,7 @@ class ConfigApp:
534
587
  ),
535
588
  anki=Anki(
536
589
  update_anki=self.update_anki_value.get(),
590
+ show_update_confirmation_dialog=self.show_update_confirmation_dialog_value.get(),
537
591
  url=self.anki_url_value.get(),
538
592
  sentence_field=self.sentence_field_value.get(),
539
593
  sentence_audio_field=self.sentence_audio_field_value.get(),
@@ -559,6 +613,7 @@ class ConfigApp:
559
613
  open_anki_in_browser=self.open_anki_browser_value.get(),
560
614
  backfill_audio=self.backfill_audio_value.get(),
561
615
  browser_query=self.browser_query_value.get(),
616
+ generate_longplay=self.generate_longplay_value.get(),
562
617
  ),
563
618
  screenshot=Screenshot(
564
619
  enabled=self.screenshot_enabled_value.get(),
@@ -590,9 +645,8 @@ class ConfigApp:
590
645
  host=self.obs_host_value.get(),
591
646
  port=int(self.obs_port_value.get()),
592
647
  password=self.obs_password_value.get(),
593
- get_game_from_scene=self.obs_get_game_from_scene_name_value.get(),
594
648
  minimum_replay_size=int(self.obs_minimum_replay_size_value.get()),
595
- turn_off_output_check=self.obs_turn_off_output_check_value.get()
649
+ automatically_manage_replay_buffer=self.automatically_manage_replay_buffer_value.get()
596
650
  ),
597
651
  hotkeys=Hotkeys(
598
652
  reset_line=self.reset_line_hotkey_value.get(),
@@ -838,21 +892,6 @@ class ConfigApp:
838
892
  row=self.current_row, column=0)
839
893
  ttk.Combobox(self.general_tab, textvariable=self.native_language_value, values=CommonLanguages.get_all_names_pretty(), state="readonly").grid(row=self.current_row, column=1, sticky='EW', pady=2)
840
894
  self.current_row += 1
841
-
842
- legend_i18n = general_i18n.get('legend', {})
843
- ttk.Label(self.general_tab, text=legend_i18n.get('important', '...'), foreground="dark orange",
844
- font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
845
- self.current_row += 1
846
- ttk.Label(self.general_tab, text=legend_i18n.get('advanced', '...'), foreground="red",
847
- font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
848
- self.current_row += 1
849
- ttk.Label(self.general_tab, text=legend_i18n.get('recommended', '...'), foreground="green",
850
- font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
851
- self.current_row += 1
852
- ttk.Label(self.general_tab,
853
- text=legend_i18n.get('tooltip_info', '...'),
854
- font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
855
- self.current_row += 1
856
895
 
857
896
  if is_beangate:
858
897
  ttk.Button(self.general_tab, text=self.i18n.get('buttons', {}).get('run_function', 'Run Function'), command=self.test_func, bootstyle="info").grid(
@@ -1040,6 +1079,26 @@ class ConfigApp:
1040
1079
  row=self.current_row, column=3, sticky='W', pady=2)
1041
1080
  self.current_row += 1
1042
1081
 
1082
+ # Add Horizontal Separator
1083
+ sep = ttk.Separator(feature_frame, orient='horizontal')
1084
+ sep.grid(row=self.current_row, column=0, columnspan=5, sticky='EW', pady=10)
1085
+ self.current_row += 1
1086
+
1087
+ legend_i18n = general_i18n.get('legend', {})
1088
+ ttk.Label(feature_frame,
1089
+ text=legend_i18n.get('tooltip_info', '...'),
1090
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
1091
+ self.current_row += 1
1092
+ ttk.Label(feature_frame, text=legend_i18n.get('important', '...'), foreground="dark orange",
1093
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
1094
+ self.current_row += 1
1095
+ ttk.Label(feature_frame, text=legend_i18n.get('advanced', '...'), foreground="red",
1096
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
1097
+ self.current_row += 1
1098
+ ttk.Label(feature_frame, text=legend_i18n.get('recommended', '...'), foreground="green",
1099
+ font=("Helvetica", 10, "bold")).grid(row=self.current_row, column=0, columnspan=2, sticky='W', pady=2)
1100
+ self.current_row += 1
1101
+
1043
1102
  # screenshot_i18n = simple_i18n.get('screenshot_enabled', {})
1044
1103
  # HoverInfoLabelWidget(feature_frame, text=screenshot_i18n.get('label', '...'),
1045
1104
  # tooltip=screenshot_i18n.get('tooltip', '...'), row=self.current_row, column=0)
@@ -1318,6 +1377,13 @@ class ConfigApp:
1318
1377
  column=1, sticky='W', pady=2)
1319
1378
  self.current_row += 1
1320
1379
 
1380
+ show_confirmation_i18n = anki_i18n.get('show_update_confirmation_dialog', {})
1381
+ HoverInfoLabelWidget(anki_frame, text=show_confirmation_i18n.get('label', '...'), tooltip=show_confirmation_i18n.get('tooltip', '...'),
1382
+ foreground="red", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
1383
+ ttk.Checkbutton(anki_frame, variable=self.show_update_confirmation_dialog_value, bootstyle="round-toggle").grid(row=self.current_row,
1384
+ column=1, sticky='W', pady=2)
1385
+ self.current_row += 1
1386
+
1321
1387
  url_i18n = anki_i18n.get('url', {})
1322
1388
  HoverInfoLabelWidget(anki_frame, text=url_i18n.get('label', '...'), tooltip=url_i18n.get('tooltip', '...'),
1323
1389
  foreground="dark orange", font=("Helvetica", 10, "bold"), row=self.current_row, column=0)
@@ -1492,6 +1558,12 @@ class ConfigApp:
1492
1558
  row=self.current_row, column=1, sticky='W', pady=2)
1493
1559
  self.current_row += 1
1494
1560
 
1561
+ HoverInfoLabelWidget(features_frame, text="Generate LongPlay", tooltip="Generate a LongPlay video using OBS recording, and write to a .srt file with all the text coming into gsm. RESTART REQUIRED FOR SETTING TO TAKE EFFECT.",
1562
+ row=self.current_row, column=0)
1563
+ ttk.Checkbutton(features_frame, variable=self.generate_longplay_value, bootstyle="round-toggle").grid(
1564
+ row=self.current_row, column=1, sticky='W', pady=2)
1565
+ self.current_row += 1
1566
+
1495
1567
  self.add_reset_button(features_frame, "features", self.current_row, 0, self.create_features_tab)
1496
1568
 
1497
1569
  for col in range(3):
@@ -1797,13 +1869,6 @@ class ConfigApp:
1797
1869
  ttk.Entry(obs_frame, show="*", textvariable=self.obs_password_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1798
1870
  self.current_row += 1
1799
1871
 
1800
- game_scene_i18n = obs_i18n.get('game_from_scene', {})
1801
- HoverInfoLabelWidget(obs_frame, text=game_scene_i18n.get('label', '...'), tooltip=game_scene_i18n.get('tooltip', '...'),
1802
- row=self.current_row, column=0)
1803
- ttk.Checkbutton(obs_frame, variable=self.obs_get_game_from_scene_name_value, bootstyle="round-toggle").grid(
1804
- row=self.current_row, column=1, sticky='W', pady=2)
1805
- self.current_row += 1
1806
-
1807
1872
  min_size_i18n = obs_i18n.get('min_replay_size', {})
1808
1873
  HoverInfoLabelWidget(obs_frame, text=min_size_i18n.get('label', '...'),
1809
1874
  tooltip=min_size_i18n.get('tooltip', '...'),
@@ -1811,13 +1876,13 @@ class ConfigApp:
1811
1876
  ttk.Entry(obs_frame, textvariable=self.obs_minimum_replay_size_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
1812
1877
  self.current_row += 1
1813
1878
 
1814
- turn_off_output_check_i18n = obs_i18n.get('turn_off_output_check', {})
1815
- HoverInfoLabelWidget(obs_frame, text=turn_off_output_check_i18n.get('label', '...'),
1816
- tooltip=turn_off_output_check_i18n.get('tooltip', '...'),
1817
- row=self.current_row, column=0)
1818
- ttk.Checkbutton(obs_frame, variable=self.obs_turn_off_output_check_value, bootstyle="round-toggle").grid(
1819
- row=self.current_row, column=1, sticky='W', pady=2)
1820
- self.current_row += 1
1879
+ # turn_off_output_check_i18n = obs_i18n.get('turn_off_replay_buffer_management', {})
1880
+ # HoverInfoLabelWidget(obs_frame, text=turn_off_output_check_i18n.get('label', '...'),
1881
+ # tooltip=turn_off_output_check_i18n.get('tooltip', '...'),
1882
+ # row=self.current_row, column=0)
1883
+ # ttk.Checkbutton(obs_frame, variable=self.obs_turn_off_output_check_value, bootstyle="round-toggle").grid(
1884
+ # row=self.current_row, column=1, sticky='W', pady=2)
1885
+ # self.current_row += 1
1821
1886
 
1822
1887
  self.add_reset_button(obs_frame, "obs", self.current_row, 0, self.create_obs_tab)
1823
1888