GameSentenceMiner 2.0.0__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.
@@ -0,0 +1,803 @@
1
+ import tkinter as tk
2
+ from tkinter import filedialog, messagebox, simpledialog
3
+
4
+ import ttkbootstrap as ttk
5
+
6
+ from . import configuration
7
+ from . import obs
8
+ from .configuration import *
9
+
10
+ TOML_CONFIG_FILE = '../../config.toml'
11
+ CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
12
+ settings_saved = False
13
+ on_save = []
14
+
15
+
16
+ def new_tab(func):
17
+ def wrapper(self, *args, **kwargs):
18
+ self.current_row = 0 # Resetting row for the new tab
19
+ # Perform any other pre-initialization tasks here if needed
20
+ return func(self, *args, **kwargs)
21
+
22
+ return wrapper
23
+
24
+
25
+ class HoverInfoWidget:
26
+ def __init__(self, parent, text, row, column, padx=5, pady=2):
27
+ self.info_icon = ttk.Label(parent, text="ⓘ", foreground="blue", cursor="hand2")
28
+ self.info_icon.grid(row=row, column=column, padx=padx, pady=pady)
29
+ self.info_icon.bind("<Enter>", lambda e: self.show_info_box(text))
30
+ self.info_icon.bind("<Leave>", lambda e: self.hide_info_box())
31
+ self.tooltip = None
32
+
33
+ def show_info_box(self, text):
34
+ x, y, _, _ = self.info_icon.bbox("insert")
35
+ x += self.info_icon.winfo_rootx() + 25
36
+ y += self.info_icon.winfo_rooty() + 20
37
+ self.tooltip = tk.Toplevel(self.info_icon)
38
+ self.tooltip.wm_overrideredirect(True)
39
+ self.tooltip.wm_geometry(f"+{x}+{y}")
40
+ label = tk.Label(self.tooltip, text=text, background="yellow", relief="solid", borderwidth=1,
41
+ font=("tahoma", "8", "normal"))
42
+ label.pack(ipadx=1)
43
+
44
+ def hide_info_box(self):
45
+ if self.tooltip:
46
+ self.tooltip.destroy()
47
+ self.tooltip = None
48
+
49
+
50
+ class ConfigApp:
51
+ def __init__(self):
52
+ self.window = ttk.Window(themename='darkly')
53
+ self.window.title('GameSentenceMiner Configuration')
54
+ self.window.protocol("WM_DELETE_WINDOW", self.hide)
55
+
56
+ self.current_row = 0
57
+
58
+ self.master_config: Config = configuration.load_config()
59
+
60
+ self.settings = self.master_config.get_config()
61
+
62
+ self.notebook = ttk.Notebook(self.window)
63
+ self.notebook.pack(pady=10, expand=True)
64
+
65
+ self.general_frame = self.create_general_tab()
66
+ self.create_paths_tab()
67
+ self.create_anki_tab()
68
+ self.create_vad_tab()
69
+ self.create_features_tab()
70
+ self.create_screenshot_tab()
71
+ self.create_audio_tab()
72
+ self.create_obs_tab()
73
+ self.create_hotkeys_tab()
74
+ self.create_profiles_tab()
75
+
76
+ ttk.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
77
+
78
+ self.window.withdraw()
79
+
80
+
81
+
82
+ def show(self):
83
+ obs.update_current_game()
84
+ self.reload_settings()
85
+ if self.window is not None:
86
+ self.window.deiconify()
87
+ self.window.lift()
88
+ return
89
+
90
+ def hide(self):
91
+ if self.window is not None:
92
+ self.window.withdraw()
93
+
94
+ def add_save_hook(self, func):
95
+ on_save.append(func)
96
+
97
+ def save_settings(self, profile_change=False):
98
+ global settings_saved
99
+
100
+ # Create a new Config instance
101
+ config = ProfileConfig(
102
+ general=General(
103
+ use_websocket=self.websocket_enabled.get(),
104
+ websocket_uri=self.websocket_uri.get(),
105
+ open_config_on_startup=self.open_config_on_startup.get()
106
+ ),
107
+ paths=Paths(
108
+ folder_to_watch=self.folder_to_watch.get(),
109
+ audio_destination=self.audio_destination.get(),
110
+ screenshot_destination=self.screenshot_destination.get(),
111
+ remove_video=self.remove_video.get(),
112
+ remove_audio=self.remove_audio.get(),
113
+ remove_screenshot=self.remove_screenshot.get()
114
+ ),
115
+ anki=Anki(
116
+ update_anki=self.update_anki.get(),
117
+ url=self.anki_url.get(),
118
+ sentence_field=self.sentence_field.get(),
119
+ sentence_audio_field=self.sentence_audio_field.get(),
120
+ picture_field=self.picture_field.get(),
121
+ word_field=self.word_field.get(),
122
+ previous_sentence_field=self.previous_sentence_field.get(),
123
+ custom_tags=[tag.strip() for tag in self.custom_tags.get().split(',') if tag.strip()],
124
+ tags_to_check=[tag.strip().lower() for tag in self.tags_to_check.get().split(',') if tag.strip()],
125
+ add_game_tag=self.add_game_tag.get(),
126
+ polling_rate=int(self.polling_rate.get()),
127
+ overwrite_audio=self.overwrite_audio.get(),
128
+ overwrite_picture=self.overwrite_picture.get(),
129
+ anki_custom_fields={
130
+ key_entry.get(): value_entry.get() for key_entry, value_entry, delete_button in
131
+ self.custom_field_entries if key_entry.get()
132
+ }
133
+ ),
134
+ features=Features(
135
+ full_auto=self.full_auto.get(),
136
+ notify_on_update=self.notify_on_update.get(),
137
+ open_anki_edit=self.open_anki_edit.get(),
138
+ backfill_audio=self.backfill_audio.get()
139
+ ),
140
+ screenshot=Screenshot(
141
+ width=self.screenshot_width.get(),
142
+ height=self.screenshot_height.get(),
143
+ quality=self.screenshot_quality.get(),
144
+ extension=self.screenshot_extension.get(),
145
+ custom_ffmpeg_settings=self.screenshot_custom_ffmpeg_settings.get(),
146
+ screenshot_hotkey_updates_anki=self.screenshot_hotkey_update_anki.get(),
147
+ seconds_after_line = self.seconds_after_line.get()
148
+ ),
149
+ audio=Audio(
150
+ extension=self.audio_extension.get(),
151
+ beginning_offset=float(self.beginning_offset.get()),
152
+ end_offset=float(self.end_offset.get()),
153
+ ffmpeg_reencode_options=self.ffmpeg_reencode_options.get(),
154
+ external_tool = self.external_tool.get(),
155
+ anki_media_collection=self.anki_media_collection.get()
156
+ ),
157
+ obs=OBS(
158
+ enabled=self.obs_enabled.get(),
159
+ host=self.obs_host.get(),
160
+ port=int(self.obs_port.get()),
161
+ password=self.obs_password.get(),
162
+ start_buffer=self.obs_start_buffer.get(),
163
+ get_game_from_scene=self.get_game_from_scene_name.get(),
164
+ minimum_replay_size=int(self.minimum_replay_size.get())
165
+ ),
166
+ hotkeys=Hotkeys(
167
+ reset_line=self.reset_line_hotkey.get(),
168
+ take_screenshot=self.take_screenshot_hotkey.get()
169
+ ),
170
+ vad=VAD(
171
+ whisper_model=self.whisper_model.get(),
172
+ do_vad_postprocessing=self.do_vad_postprocessing.get(),
173
+ vosk_url='https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' if self.vosk_url.get() == VOSK_BASE else "https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip",
174
+ selected_vad_model=self.selected_vad_model.get(),
175
+ backup_vad_model=self.backup_vad_model.get(),
176
+ trim_beginning=self.vad_trim_beginning.get()
177
+ )
178
+ )
179
+
180
+ current_profile = self.profile_combobox.get()
181
+
182
+ if profile_change:
183
+ self.master_config.current_profile = current_profile
184
+ else:
185
+ self.master_config.current_profile = current_profile
186
+ self.master_config.set_config_for_profile(current_profile, config)
187
+
188
+ # Serialize the config instance to JSON
189
+ with open('../../config.json', 'w') as file:
190
+ file.write(self.master_config.to_json(indent=4))
191
+
192
+ print("Settings saved successfully!")
193
+ settings_saved = True
194
+ configuration.reload_config()
195
+ for func in on_save:
196
+ func()
197
+
198
+
199
+ def reload_settings(self):
200
+ new_config = configuration.load_config()
201
+ current_config = new_config.get_config()
202
+
203
+ self.window.title("GameSentenceMiner Configuration - " + current_config.name)
204
+
205
+ if current_config.name != self.settings.name:
206
+ logger.info("Profile changed, reloading settings.")
207
+ self.master_config = new_config
208
+ self.settings = current_config
209
+ for frame in self.notebook.winfo_children():
210
+ frame.destroy()
211
+
212
+ self.general_frame = self.create_general_tab()
213
+ self.create_paths_tab()
214
+ self.create_anki_tab()
215
+ self.create_vad_tab()
216
+ self.create_features_tab()
217
+ self.create_screenshot_tab()
218
+ self.create_audio_tab()
219
+ self.create_obs_tab()
220
+ self.create_hotkeys_tab()
221
+ self.create_profiles_tab()
222
+
223
+
224
+ def increment_row(self):
225
+ """Increment the current row index and return the new value."""
226
+ self.current_row += 1
227
+ return self.current_row
228
+
229
+ def add_label_and_increment_row(self, root, label, row=0, column=0):
230
+ HoverInfoWidget(root, label, row=self.current_row, column=column)
231
+ self.increment_row()
232
+
233
+ @new_tab
234
+ def create_general_tab(self):
235
+ general_frame = ttk.Frame(self.notebook)
236
+ self.notebook.add(general_frame, text='General')
237
+
238
+ ttk.Label(general_frame, text="Websocket Enabled:").grid(row=self.current_row, column=0, sticky='W')
239
+ self.websocket_enabled = tk.BooleanVar(value=self.settings.general.use_websocket)
240
+ ttk.Checkbutton(general_frame, variable=self.websocket_enabled).grid(row=self.current_row, column=1,
241
+ sticky='W')
242
+ self.add_label_and_increment_row(general_frame, "Enable or disable WebSocket communication.",
243
+ row=self.current_row, column=2)
244
+
245
+ ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
246
+ self.websocket_uri = ttk.Entry(general_frame)
247
+ self.websocket_uri.insert(0, self.settings.general.websocket_uri)
248
+ self.websocket_uri.grid(row=self.current_row, column=1)
249
+ self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting.", row=self.current_row,
250
+ column=2)
251
+
252
+ ttk.Label(general_frame, text="Open Config on Startup:").grid(row=self.current_row, column=0, sticky='W')
253
+ self.open_config_on_startup = tk.BooleanVar(value=self.settings.general.open_config_on_startup)
254
+ ttk.Checkbutton(general_frame, variable=self.open_config_on_startup).grid(row=self.current_row, column=1,
255
+ sticky='W')
256
+ self.add_label_and_increment_row(general_frame, "Whether to open config when the script starts.",
257
+ row=self.current_row, column=2)
258
+
259
+ # ttk.Label(general_frame, text="Per Scene Config:").grid(row=self.current_row, column=0, sticky='W')
260
+ # self.per_scene_config = tk.BooleanVar(value=self.master_config.per_scene_config)
261
+ # ttk.Checkbutton(general_frame, variable=self.per_scene_config).grid(row=self.current_row, column=1,
262
+ # sticky='W')
263
+ # self.add_label_and_increment_row(general_frame, "Enable Per-Scene Config, REQUIRES RESTART. Disable to edit the DEFAULT Config.",
264
+ # row=self.current_row, column=2)
265
+
266
+ return general_frame
267
+
268
+ @new_tab
269
+ def create_vad_tab(self):
270
+ vad_frame = ttk.Frame(self.notebook)
271
+ self.notebook.add(vad_frame, text='VAD')
272
+
273
+ ttk.Label(vad_frame, text="Voice Detection Postprocessing:").grid(row=self.current_row, column=0, sticky='W')
274
+ self.do_vad_postprocessing = tk.BooleanVar(
275
+ value=self.settings.vad.do_vad_postprocessing)
276
+ ttk.Checkbutton(vad_frame, variable=self.do_vad_postprocessing).grid(row=self.current_row, column=1, sticky='W')
277
+ self.add_label_and_increment_row(vad_frame, "Enable post-processing of audio to trim just the voiceline.",
278
+ row=self.current_row, column=2)
279
+
280
+ ttk.Label(vad_frame, text="Whisper Model:").grid(row=self.current_row, column=0, sticky='W')
281
+ self.whisper_model = ttk.Combobox(vad_frame, values=[WHISPER_TINY, WHISPER_BASE, WHISPER_SMALL, WHISPER_MEDIUM,
282
+ WHSIPER_LARGE])
283
+ self.whisper_model.set(self.settings.vad.whisper_model)
284
+ self.whisper_model.grid(row=self.current_row, column=1)
285
+ self.add_label_and_increment_row(vad_frame, "Select the Whisper model size for VAD.", row=self.current_row,
286
+ column=2)
287
+
288
+ ttk.Label(vad_frame, text="Vosk URL:").grid(row=self.current_row, column=0, sticky='W')
289
+ self.vosk_url = ttk.Combobox(vad_frame, values=[VOSK_BASE, VOSK_SMALL])
290
+ self.vosk_url.insert(0,
291
+ VOSK_BASE if self.settings.vad.vosk_url == 'https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip' else VOSK_SMALL)
292
+ self.vosk_url.grid(row=self.current_row, column=1)
293
+ self.add_label_and_increment_row(vad_frame, "URL for connecting to the Vosk server.", row=self.current_row,
294
+ column=2)
295
+
296
+ ttk.Label(vad_frame, text="Select VAD Model:").grid(row=self.current_row, column=0, sticky='W')
297
+ self.selected_vad_model = ttk.Combobox(vad_frame, values=[VOSK, SILERO, WHISPER])
298
+ self.selected_vad_model.set(self.settings.vad.selected_vad_model)
299
+ self.selected_vad_model.grid(row=self.current_row, column=1)
300
+ self.add_label_and_increment_row(vad_frame, "Select which VAD model to use.", row=self.current_row, column=2)
301
+
302
+ ttk.Label(vad_frame, text="Backup VAD Model:").grid(row=self.current_row, column=0, sticky='W')
303
+ self.backup_vad_model = ttk.Combobox(vad_frame, values=[OFF, VOSK, SILERO, WHISPER])
304
+ self.backup_vad_model.set(self.settings.vad.backup_vad_model)
305
+ self.backup_vad_model.grid(row=self.current_row, column=1)
306
+ self.add_label_and_increment_row(vad_frame, "Select which model to use as a backup if no audio is found.",
307
+ row=self.current_row, column=2)
308
+
309
+ ttk.Label(vad_frame, text="Trim Beginning:").grid(row=self.current_row, column=0, sticky='W')
310
+ self.vad_trim_beginning = tk.BooleanVar(
311
+ value=self.settings.vad.trim_beginning)
312
+ ttk.Checkbutton(vad_frame, variable=self.vad_trim_beginning).grid(row=self.current_row, column=1, sticky='W')
313
+ self.add_label_and_increment_row(vad_frame, "Trim the beginning of the audio based on Voice Detection Results",
314
+ row=self.current_row, column=2)
315
+
316
+
317
+ @new_tab
318
+ def create_paths_tab(self):
319
+ paths_frame = ttk.Frame(self.notebook)
320
+ self.notebook.add(paths_frame, text='Paths')
321
+
322
+ ttk.Label(paths_frame, text="Folder to Watch:").grid(row=self.current_row, column=0, sticky='W')
323
+ self.folder_to_watch = ttk.Entry(paths_frame, width=50)
324
+ self.folder_to_watch.insert(0, self.settings.paths.folder_to_watch)
325
+ self.folder_to_watch.grid(row=self.current_row, column=1)
326
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.folder_to_watch)).grid(
327
+ row=self.current_row,
328
+ column=2)
329
+ self.add_label_and_increment_row(paths_frame, "Path where the OBS Replays will be saved.", row=self.current_row,
330
+ column=3)
331
+
332
+ ttk.Label(paths_frame, text="Audio Destination:").grid(row=self.current_row, column=0, sticky='W')
333
+ self.audio_destination = ttk.Entry(paths_frame, width=50)
334
+ self.audio_destination.insert(0, self.settings.paths.audio_destination)
335
+ self.audio_destination.grid(row=self.current_row, column=1)
336
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.audio_destination)).grid(
337
+ row=self.current_row,
338
+ column=2)
339
+ self.add_label_and_increment_row(paths_frame, "Path where the cut Audio will be saved.", row=self.current_row,
340
+ column=3)
341
+
342
+ ttk.Label(paths_frame, text="Screenshot Destination:").grid(row=self.current_row, column=0, sticky='W')
343
+ self.screenshot_destination = ttk.Entry(paths_frame, width=50)
344
+ self.screenshot_destination.insert(0, self.settings.paths.screenshot_destination)
345
+ self.screenshot_destination.grid(row=self.current_row, column=1)
346
+ ttk.Button(paths_frame, text="Browse", command=lambda: self.browse_folder(self.screenshot_destination)).grid(
347
+ row=self.current_row, column=2)
348
+ self.add_label_and_increment_row(paths_frame, "Path where the Screenshot will be saved.", row=self.current_row,
349
+ column=3)
350
+
351
+ ttk.Label(paths_frame, text="Remove Video:").grid(row=self.current_row, column=0, sticky='W')
352
+ self.remove_video = tk.BooleanVar(value=self.settings.paths.remove_video)
353
+ ttk.Checkbutton(paths_frame, variable=self.remove_video).grid(row=self.current_row, column=1, sticky='W')
354
+ self.add_label_and_increment_row(paths_frame, "Remove video from the output.", row=self.current_row, column=2)
355
+
356
+ ttk.Label(paths_frame, text="Remove Audio:").grid(row=self.current_row, column=0, sticky='W')
357
+ self.remove_audio = tk.BooleanVar(value=self.settings.paths.remove_audio)
358
+ ttk.Checkbutton(paths_frame, variable=self.remove_audio).grid(row=self.current_row, column=1, sticky='W')
359
+ self.add_label_and_increment_row(paths_frame, "Remove audio from the output.", row=self.current_row, column=2)
360
+
361
+ ttk.Label(paths_frame, text="Remove Screenshot:").grid(row=self.current_row, column=0, sticky='W')
362
+ self.remove_screenshot = tk.BooleanVar(value=self.settings.paths.remove_screenshot)
363
+ ttk.Checkbutton(paths_frame, variable=self.remove_screenshot).grid(row=self.current_row, column=1, sticky='W')
364
+ self.add_label_and_increment_row(paths_frame, "Remove screenshots after processing.", row=self.current_row,
365
+ column=2)
366
+
367
+ return paths_frame
368
+
369
+ def browse_folder(self, entry_widget):
370
+ folder_selected = filedialog.askdirectory()
371
+ if folder_selected:
372
+ entry_widget.delete(0, tk.END)
373
+ entry_widget.insert(0, folder_selected)
374
+
375
+ @new_tab
376
+ def create_anki_tab(self):
377
+ anki_frame = ttk.Frame(self.notebook)
378
+ self.notebook.add(anki_frame, text='Anki')
379
+
380
+ ttk.Label(anki_frame, text="Update Anki:").grid(row=self.current_row, column=0, sticky='W')
381
+ self.update_anki = tk.BooleanVar(value=self.settings.anki.update_anki)
382
+ ttk.Checkbutton(anki_frame, variable=self.update_anki).grid(row=self.current_row, column=1, sticky='W')
383
+ self.add_label_and_increment_row(anki_frame, "Automatically update Anki with new data.", row=self.current_row,
384
+ column=2)
385
+
386
+ ttk.Label(anki_frame, text="Anki URL:").grid(row=self.current_row, column=0, sticky='W')
387
+ self.anki_url = ttk.Entry(anki_frame, width=50)
388
+ self.anki_url.insert(0, self.settings.anki.url)
389
+ self.anki_url.grid(row=self.current_row, column=1)
390
+ self.add_label_and_increment_row(anki_frame, "The URL to connect to your Anki instance.", row=self.current_row,
391
+ column=2)
392
+
393
+ ttk.Label(anki_frame, text="Sentence Field:").grid(row=self.current_row, column=0, sticky='W')
394
+ self.sentence_field = ttk.Entry(anki_frame)
395
+ self.sentence_field.insert(0, self.settings.anki.sentence_field)
396
+ self.sentence_field.grid(row=self.current_row, column=1)
397
+ self.add_label_and_increment_row(anki_frame, "Field in Anki for the main sentence.", row=self.current_row,
398
+ column=2)
399
+
400
+ ttk.Label(anki_frame, text="Sentence Audio Field:").grid(row=self.current_row, column=0, sticky='W')
401
+ self.sentence_audio_field = ttk.Entry(anki_frame)
402
+ self.sentence_audio_field.insert(0, self.settings.anki.sentence_audio_field)
403
+ self.sentence_audio_field.grid(row=self.current_row, column=1)
404
+ self.add_label_and_increment_row(anki_frame,
405
+ "Field in Anki for audio associated with the sentence. Leave Blank to Disable Audio Processing.",
406
+ row=self.current_row, column=2)
407
+
408
+ ttk.Label(anki_frame, text="Picture Field:").grid(row=self.current_row, column=0, sticky='W')
409
+ self.picture_field = ttk.Entry(anki_frame)
410
+ self.picture_field.insert(0, self.settings.anki.picture_field)
411
+ self.picture_field.grid(row=self.current_row, column=1)
412
+ self.add_label_and_increment_row(anki_frame, "Field in Anki for associated pictures.", row=self.current_row,
413
+ column=2)
414
+
415
+ ttk.Label(anki_frame, text="Word Field:").grid(row=self.current_row, column=0, sticky='W')
416
+ self.word_field = ttk.Entry(anki_frame)
417
+ self.word_field.insert(0, self.settings.anki.word_field)
418
+ self.word_field.grid(row=self.current_row, column=1)
419
+ self.add_label_and_increment_row(anki_frame, "Field in Anki for individual words.", row=self.current_row,
420
+ column=2)
421
+
422
+ ttk.Label(anki_frame, text="Previous Sentence Field:").grid(row=self.current_row, column=0, sticky='W')
423
+ self.previous_sentence_field = ttk.Entry(anki_frame)
424
+ self.previous_sentence_field.insert(0, self.settings.anki.previous_sentence_field)
425
+ self.previous_sentence_field.grid(row=self.current_row, column=1)
426
+ self.add_label_and_increment_row(anki_frame,
427
+ "Field in Anki for the previous line of dialogue. If Empty, will not populate",
428
+ row=self.current_row,
429
+ column=2)
430
+
431
+ ttk.Label(anki_frame, text="Custom Tags:").grid(row=self.current_row, column=0, sticky='W')
432
+ self.custom_tags = ttk.Entry(anki_frame)
433
+ self.custom_tags.insert(0, ', '.join(self.settings.anki.custom_tags))
434
+ self.custom_tags.grid(row=self.current_row, column=1)
435
+ self.add_label_and_increment_row(anki_frame, "Comma-separated custom tags for the Anki cards.",
436
+ row=self.current_row, column=2)
437
+
438
+ ttk.Label(anki_frame, text="Tags to work on:").grid(row=self.current_row, column=0, sticky='W')
439
+ self.tags_to_check = ttk.Entry(anki_frame)
440
+ self.tags_to_check.insert(0, ', '.join(self.settings.anki.tags_to_check))
441
+ self.tags_to_check.grid(row=self.current_row, column=1)
442
+ self.add_label_and_increment_row(anki_frame,
443
+ "Comma-separated Tags, script will only do 1-click on cards with these tags (Recommend keep empty, or use Yomitan Profile to add custom tag from texthooker page)",
444
+ row=self.current_row, column=2)
445
+
446
+ ttk.Label(anki_frame, text="Add Game Tag:").grid(row=self.current_row, column=0, sticky='W')
447
+ self.add_game_tag = tk.BooleanVar(value=self.settings.anki.add_game_tag)
448
+ ttk.Checkbutton(anki_frame, variable=self.add_game_tag).grid(row=self.current_row, column=1, sticky='W')
449
+ self.add_label_and_increment_row(anki_frame, "Include a tag for the game on the Anki card.",
450
+ row=self.current_row, column=2)
451
+
452
+ ttk.Label(anki_frame, text="Polling Rate:").grid(row=self.current_row, column=0, sticky='W')
453
+ self.polling_rate = ttk.Entry(anki_frame)
454
+ self.polling_rate.insert(0, str(self.settings.anki.polling_rate))
455
+ self.polling_rate.grid(row=self.current_row, column=1)
456
+ self.add_label_and_increment_row(anki_frame, "Rate at which Anki will check for updates (in milliseconds).",
457
+ row=self.current_row, column=2)
458
+
459
+ ttk.Label(anki_frame, text="Overwrite Audio:").grid(row=self.current_row, column=0, sticky='W')
460
+ self.overwrite_audio = tk.BooleanVar(
461
+ value=self.settings.anki.overwrite_audio)
462
+ ttk.Checkbutton(anki_frame, variable=self.overwrite_audio).grid(row=self.current_row, column=1, sticky='W')
463
+ self.add_label_and_increment_row(anki_frame, "Overwrite existing audio in Anki cards.", row=self.current_row,
464
+ column=2)
465
+
466
+ ttk.Label(anki_frame, text="Overwrite Picture:").grid(row=self.current_row, column=0, sticky='W')
467
+ self.overwrite_picture = tk.BooleanVar(
468
+ value=self.settings.anki.overwrite_picture)
469
+ ttk.Checkbutton(anki_frame, variable=self.overwrite_picture).grid(row=self.current_row, column=1, sticky='W')
470
+ self.add_label_and_increment_row(anki_frame, "Overwrite existing pictures in Anki cards.", row=self.current_row,
471
+ column=2)
472
+
473
+ self.anki_custom_fields = self.settings.anki.anki_custom_fields
474
+ self.custom_field_entries = []
475
+
476
+ row_at_the_time = self.current_row + 1
477
+
478
+ ttk.Button(anki_frame, text="Add Field",
479
+ command=lambda: self.add_custom_field(anki_frame, row_at_the_time)).grid(row=self.current_row,
480
+ column=0, pady=5)
481
+ self.add_label_and_increment_row(anki_frame, "Add a new custom field for Anki cards.", row=self.current_row,
482
+ column=2)
483
+ self.display_custom_fields(anki_frame, self.current_row)
484
+
485
+ return anki_frame
486
+
487
+ def add_custom_field(self, frame, start_row):
488
+ row = len(self.custom_field_entries) + 1 + start_row
489
+
490
+ key_entry = ttk.Entry(frame)
491
+ key_entry.grid(row=row, column=0, padx=5, pady=2, sticky='W')
492
+ value_entry = ttk.Entry(frame)
493
+ value_entry.grid(row=row, column=1, padx=5, pady=2, sticky='W')
494
+
495
+ # Create a delete button for this custom field
496
+ delete_button = ttk.Button(frame, text="X",
497
+ command=lambda: self.delete_custom_field(row, key_entry, value_entry, delete_button))
498
+ delete_button.grid(row=row, column=2, padx=5, pady=2)
499
+
500
+ self.custom_field_entries.append((key_entry, value_entry, delete_button))
501
+
502
+ def display_custom_fields(self, frame, start_row):
503
+ for row, (key, value) in enumerate(self.anki_custom_fields.items()):
504
+ key_entry = ttk.Entry(frame)
505
+ key_entry.insert(0, key)
506
+ key_entry.grid(row=row + start_row, column=0, padx=5, pady=2, sticky='W')
507
+
508
+ value_entry = ttk.Entry(frame)
509
+ value_entry.insert(0, value)
510
+ value_entry.grid(row=row + start_row, column=1, padx=5, pady=2, sticky='W')
511
+
512
+ # Create a delete button for each existing custom field
513
+ delete_button = ttk.Button(frame, text="X",
514
+ command=lambda: self.delete_custom_field(row + start_row, key_entry, value_entry,
515
+ delete_button))
516
+ delete_button.grid(row=row + start_row, column=2, padx=5, pady=2)
517
+
518
+ self.custom_field_entries.append((key_entry, value_entry, delete_button))
519
+
520
+ def delete_custom_field(self, row, key_entry, value_entry, delete_button):
521
+ # Remove the entry from the GUI
522
+ key_entry.destroy()
523
+ value_entry.destroy()
524
+ delete_button.destroy()
525
+
526
+ # Remove the entry from the custom field entries list
527
+ self.custom_field_entries.remove((key_entry, value_entry, delete_button))
528
+
529
+ # Update the GUI rows below to fill the gap if necessary
530
+ # for (ke, ve, db) in self.custom_field_entries:
531
+ # if self.custom_field_entries.index((ke, ve, db)) > self.custom_field_entries.index(
532
+ # (key_entry, value_entry, delete_button)):
533
+ # ke.grid_configure(row=ke.grid_info()['row'] - 1)
534
+ # ve.grid_configure(row=ve.grid_info()['row'] - 1)
535
+ # db.grid_configure(row=db.grid_info()['row'] - 1)
536
+
537
+ @new_tab
538
+ def create_features_tab(self):
539
+ features_frame = ttk.Frame(self.notebook)
540
+ self.notebook.add(features_frame, text='Features')
541
+
542
+ ttk.Label(features_frame, text="Notify on Update:").grid(row=self.current_row, column=0, sticky='W')
543
+ self.notify_on_update = tk.BooleanVar(value=self.settings.features.notify_on_update)
544
+ ttk.Checkbutton(features_frame, variable=self.notify_on_update).grid(row=self.current_row, column=1, sticky='W')
545
+ self.add_label_and_increment_row(features_frame, "Notify the user when an update occurs.", row=self.current_row,
546
+ column=2)
547
+
548
+ ttk.Label(features_frame, text="Open Anki Edit:").grid(row=self.current_row, column=0, sticky='W')
549
+ self.open_anki_edit = tk.BooleanVar(value=self.settings.features.open_anki_edit)
550
+ ttk.Checkbutton(features_frame, variable=self.open_anki_edit).grid(row=self.current_row, column=1, sticky='W')
551
+ self.add_label_and_increment_row(features_frame, "Automatically open Anki for editing after updating.",
552
+ row=self.current_row, column=2)
553
+
554
+ ttk.Label(features_frame, text="Backfill Audio:").grid(row=self.current_row, column=0, sticky='W')
555
+ self.backfill_audio = tk.BooleanVar(value=self.settings.features.backfill_audio)
556
+ ttk.Checkbutton(features_frame, variable=self.backfill_audio).grid(row=self.current_row, column=1, sticky='W')
557
+ self.add_label_and_increment_row(features_frame, "Fill in audio data for existing entries.",
558
+ row=self.current_row, column=2)
559
+
560
+ ttk.Label(features_frame, text="Full Auto Mode:").grid(row=self.current_row, column=0, sticky='W')
561
+ self.full_auto = tk.BooleanVar(
562
+ value=self.settings.features.full_auto)
563
+ ttk.Checkbutton(features_frame, variable=self.full_auto).grid(row=self.current_row, column=1, sticky='W')
564
+ self.add_label_and_increment_row(features_frame, "Yomitan 1-click anki card creation.", row=self.current_row,
565
+ column=2)
566
+
567
+ @new_tab
568
+ def create_screenshot_tab(self):
569
+ screenshot_frame = ttk.Frame(self.notebook)
570
+ self.notebook.add(screenshot_frame, text='Screenshot')
571
+
572
+ ttk.Label(screenshot_frame, text="Width:").grid(row=self.current_row, column=0, sticky='W')
573
+ self.screenshot_width = ttk.Entry(screenshot_frame)
574
+ self.screenshot_width.insert(0, str(self.settings.screenshot.width))
575
+ self.screenshot_width.grid(row=self.current_row, column=1)
576
+ self.add_label_and_increment_row(screenshot_frame, "Width of the screenshot in pixels.", row=self.current_row,
577
+ column=2)
578
+
579
+ ttk.Label(screenshot_frame, text="Height:").grid(row=self.current_row, column=0, sticky='W')
580
+ self.screenshot_height = ttk.Entry(screenshot_frame)
581
+ self.screenshot_height.insert(0, str(self.settings.screenshot.height))
582
+ self.screenshot_height.grid(row=self.current_row, column=1)
583
+ self.add_label_and_increment_row(screenshot_frame, "Height of the screenshot in pixels.", row=self.current_row,
584
+ column=2)
585
+
586
+ ttk.Label(screenshot_frame, text="Quality:").grid(row=self.current_row, column=0, sticky='W')
587
+ self.screenshot_quality = ttk.Entry(screenshot_frame)
588
+ self.screenshot_quality.insert(0, str(self.settings.screenshot.quality))
589
+ self.screenshot_quality.grid(row=self.current_row, column=1)
590
+ self.add_label_and_increment_row(screenshot_frame, "Quality of the screenshot (0-100).", row=self.current_row,
591
+ column=2)
592
+
593
+ ttk.Label(screenshot_frame, text="Extension:").grid(row=self.current_row, column=0, sticky='W')
594
+ self.screenshot_extension = ttk.Combobox(screenshot_frame, values=['webp', 'avif', 'png', 'jpeg'])
595
+ self.screenshot_extension.insert(0, self.settings.screenshot.extension)
596
+ self.screenshot_extension.grid(row=self.current_row, column=1)
597
+ self.add_label_and_increment_row(screenshot_frame, "File extension for the screenshot format.",
598
+ row=self.current_row, column=2)
599
+
600
+ ttk.Label(screenshot_frame, text="FFmpeg Reencode Options:").grid(row=self.current_row, column=0, sticky='W')
601
+ self.screenshot_custom_ffmpeg_settings = ttk.Entry(screenshot_frame, width=50)
602
+ self.screenshot_custom_ffmpeg_settings.insert(0, self.settings.screenshot.custom_ffmpeg_settings)
603
+ self.screenshot_custom_ffmpeg_settings.grid(row=self.current_row, column=1)
604
+ self.add_label_and_increment_row(screenshot_frame, "Custom FFmpeg options for re-encoding screenshots.",
605
+ row=self.current_row, column=2)
606
+
607
+ ttk.Label(screenshot_frame, text="Screenshot Hotkey Updates Anki:").grid(row=self.current_row, column=0, sticky='W')
608
+ self.screenshot_hotkey_update_anki = tk.BooleanVar(value=self.settings.screenshot.screenshot_hotkey_updates_anki)
609
+ ttk.Checkbutton(screenshot_frame, variable=self.screenshot_hotkey_update_anki).grid(row=self.current_row, column=1, sticky='W')
610
+ self.add_label_and_increment_row(screenshot_frame, "Enable to allow Screenshot hotkey/button to update the latest anki card.", row=self.current_row,
611
+ column=2)
612
+
613
+ ttk.Label(screenshot_frame, text="Seconds After Line to SS:").grid(row=self.current_row, column=0, sticky='W')
614
+ self.seconds_after_line = ttk.Entry(screenshot_frame)
615
+ self.seconds_after_line.insert(0, str(self.settings.screenshot.seconds_after_line))
616
+ self.seconds_after_line.grid(row=self.current_row, column=1)
617
+ self.add_label_and_increment_row(screenshot_frame, "This is only used for mining from lines from history (not current line)", row=self.current_row,
618
+ column=2)
619
+
620
+ @new_tab
621
+ def create_audio_tab(self):
622
+ audio_frame = ttk.Frame(self.notebook)
623
+ self.notebook.add(audio_frame, text='Audio')
624
+
625
+ ttk.Label(audio_frame, text="Audio Extension:").grid(row=self.current_row, column=0, sticky='W')
626
+ self.audio_extension = ttk.Combobox(audio_frame, values=['opus', 'mp3', 'ogg', 'aac', 'm4a'])
627
+ self.audio_extension.insert(0, self.settings.audio.extension)
628
+ self.audio_extension.grid(row=self.current_row, column=1)
629
+ self.add_label_and_increment_row(audio_frame, "File extension for audio files.", row=self.current_row, column=2)
630
+
631
+ ttk.Label(audio_frame, text="Beginning Offset:").grid(row=self.current_row, column=0, sticky='W')
632
+ self.beginning_offset = ttk.Entry(audio_frame)
633
+ self.beginning_offset.insert(0, str(self.settings.audio.beginning_offset))
634
+ self.beginning_offset.grid(row=self.current_row, column=1)
635
+ self.add_label_and_increment_row(audio_frame, "Offset in seconds to start audio processing.",
636
+ row=self.current_row, column=2)
637
+
638
+ ttk.Label(audio_frame, text="End Offset:").grid(row=self.current_row, column=0, sticky='W')
639
+ self.end_offset = ttk.Entry(audio_frame)
640
+ self.end_offset.insert(0, str(self.settings.audio.end_offset))
641
+ self.end_offset.grid(row=self.current_row, column=1)
642
+ self.add_label_and_increment_row(audio_frame, "Offset in seconds to end audio processing.",
643
+ row=self.current_row, column=2)
644
+
645
+ ttk.Label(audio_frame, text="FFmpeg Reencode Options:").grid(row=self.current_row, column=0, sticky='W')
646
+ self.ffmpeg_reencode_options = ttk.Entry(audio_frame, width=50)
647
+ self.ffmpeg_reencode_options.insert(0, self.settings.audio.ffmpeg_reencode_options)
648
+ self.ffmpeg_reencode_options.grid(row=self.current_row, column=1)
649
+ self.add_label_and_increment_row(audio_frame, "Custom FFmpeg options for re-encoding audio files.",
650
+ row=self.current_row, column=2)
651
+
652
+ ttk.Label(audio_frame, text="Anki Media Collection:").grid(row=self.current_row, column=0, sticky='W')
653
+ self.anki_media_collection = ttk.Entry(audio_frame)
654
+ self.anki_media_collection.insert(0, self.settings.audio.anki_media_collection)
655
+ self.anki_media_collection.grid(row=self.current_row, column=1)
656
+ self.add_label_and_increment_row(audio_frame,
657
+ "Path of the Anki Media Collection, used for external Trimming tool. NO TRAILING SLASH",
658
+ row=self.current_row,
659
+ column=2)
660
+
661
+ ttk.Label(audio_frame, text="External Audio Editing Tool:").grid(row=self.current_row, column=0, sticky='W')
662
+ self.external_tool = ttk.Entry(audio_frame)
663
+ self.external_tool.insert(0, self.settings.audio.external_tool)
664
+ self.external_tool.grid(row=self.current_row, column=1)
665
+ self.add_label_and_increment_row(audio_frame,
666
+ "Path to External tool that opens the audio up for manual trimming. I recommend OcenAudio for in-place Editing.",
667
+ row=self.current_row,
668
+ column=2)
669
+
670
+ @new_tab
671
+ def create_obs_tab(self):
672
+ obs_frame = ttk.Frame(self.notebook)
673
+ self.notebook.add(obs_frame, text='OBS')
674
+
675
+ ttk.Label(obs_frame, text="Enabled:").grid(row=self.current_row, column=0, sticky='W')
676
+ self.obs_enabled = tk.BooleanVar(value=self.settings.obs.enabled)
677
+ ttk.Checkbutton(obs_frame, variable=self.obs_enabled).grid(row=self.current_row, column=1, sticky='W')
678
+ self.add_label_and_increment_row(obs_frame, "Enable or disable OBS integration.", row=self.current_row,
679
+ column=2)
680
+
681
+ ttk.Label(obs_frame, text="Host:").grid(row=self.current_row, column=0, sticky='W')
682
+ self.obs_host = ttk.Entry(obs_frame)
683
+ self.obs_host.insert(0, self.settings.obs.host)
684
+ self.obs_host.grid(row=self.current_row, column=1)
685
+ self.add_label_and_increment_row(obs_frame, "Host address for the OBS WebSocket server.", row=self.current_row,
686
+ column=2)
687
+
688
+ ttk.Label(obs_frame, text="Port:").grid(row=self.current_row, column=0, sticky='W')
689
+ self.obs_port = ttk.Entry(obs_frame)
690
+ self.obs_port.insert(0, str(self.settings.obs.port))
691
+ self.obs_port.grid(row=self.current_row, column=1)
692
+ self.add_label_and_increment_row(obs_frame, "Port number for the OBS WebSocket server.", row=self.current_row,
693
+ column=2)
694
+
695
+ ttk.Label(obs_frame, text="Password:").grid(row=self.current_row, column=0, sticky='W')
696
+ self.obs_password = ttk.Entry(obs_frame)
697
+ self.obs_password.insert(0, self.settings.obs.password)
698
+ self.obs_password.grid(row=self.current_row, column=1)
699
+ self.add_label_and_increment_row(obs_frame, "Password for the OBS WebSocket server.", row=self.current_row,
700
+ column=2)
701
+
702
+ ttk.Label(obs_frame, text="Start/Stop Buffer:").grid(row=self.current_row, column=0, sticky='W')
703
+ self.obs_start_buffer = tk.BooleanVar(value=self.settings.obs.start_buffer)
704
+ ttk.Checkbutton(obs_frame, variable=self.obs_start_buffer).grid(row=self.current_row, column=1, sticky='W')
705
+ self.add_label_and_increment_row(obs_frame, "Start and Stop the Buffer when Script runs.", row=self.current_row,
706
+ column=2)
707
+
708
+ ttk.Label(obs_frame, text="Get Game From Scene Name:").grid(row=self.current_row, column=0, sticky='W')
709
+ self.get_game_from_scene_name = tk.BooleanVar(value=self.settings.obs.get_game_from_scene)
710
+ ttk.Checkbutton(obs_frame, variable=self.get_game_from_scene_name).grid(row=self.current_row, column=1,
711
+ sticky='W')
712
+ self.add_label_and_increment_row(obs_frame, "Changes Current Game to Scene Name", row=self.current_row,
713
+ column=2)
714
+
715
+ ttk.Label(obs_frame, text="Minimum Replay Size (KB):").grid(row=self.current_row, column=0, sticky='W')
716
+ self.minimum_replay_size = ttk.Entry(obs_frame)
717
+ self.minimum_replay_size.insert(0, str(self.settings.obs.minimum_replay_size))
718
+ self.minimum_replay_size.grid(row=self.current_row, column=1)
719
+ self.add_label_and_increment_row(obs_frame, "Minimum Replay Size for OBS Replays in KB. If Replay is Under this, "
720
+ "Audio/Screenshot Will not be grabbed.", row=self.current_row,
721
+ column=2)
722
+
723
+ @new_tab
724
+ def create_hotkeys_tab(self):
725
+ hotkeys_frame = ttk.Frame(self.notebook)
726
+ self.notebook.add(hotkeys_frame, text='Hotkeys')
727
+
728
+ ttk.Label(hotkeys_frame, text="Reset Line Hotkey:").grid(row=self.current_row, column=0, sticky='W')
729
+ self.reset_line_hotkey = ttk.Entry(hotkeys_frame)
730
+ self.reset_line_hotkey.insert(0, self.settings.hotkeys.reset_line)
731
+ self.reset_line_hotkey.grid(row=self.current_row, column=1)
732
+ self.add_label_and_increment_row(hotkeys_frame, "Hotkey to reset the current line of dialogue.",
733
+ row=self.current_row, column=2)
734
+
735
+ ttk.Label(hotkeys_frame, text="Take Screenshot Hotkey:").grid(row=self.current_row, column=0, sticky='W')
736
+ self.take_screenshot_hotkey = ttk.Entry(hotkeys_frame)
737
+ self.take_screenshot_hotkey.insert(0, self.settings.hotkeys.take_screenshot)
738
+ self.take_screenshot_hotkey.grid(row=self.current_row, column=1)
739
+ self.add_label_and_increment_row(hotkeys_frame, "Hotkey to take a screenshot.", row=self.current_row, column=2)
740
+
741
+
742
+ @new_tab
743
+ def create_profiles_tab(self):
744
+ profiles_frame = ttk.Frame(self.notebook)
745
+ self.notebook.add(profiles_frame, text='Profiles')
746
+
747
+ ttk.Label(profiles_frame, text="Select Profile:").grid(row=self.current_row, column=0, sticky='W')
748
+ self.profile_var = tk.StringVar(value=self.settings.name)
749
+ self.profile_combobox = ttk.Combobox(profiles_frame, textvariable=self.profile_var, values=list(self.master_config.configs.keys()))
750
+ self.profile_combobox.grid(row=self.current_row, column=1)
751
+ self.profile_combobox.bind("<<ComboboxSelected>>", self.on_profile_change)
752
+ self.add_label_and_increment_row(profiles_frame, "Select a profile to load its settings.", row=self.current_row, column=2)
753
+
754
+ ttk.Button(profiles_frame, text="Add Profile", command=self.add_profile).grid(row=self.current_row, column=0, pady=5)
755
+ ttk.Button(profiles_frame, text="Copy Profile", command=self.copy_profile).grid(row=self.current_row, column=1, pady=5)
756
+ if self.master_config.current_profile != DEFAULT_CONFIG:
757
+ ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile).grid(row=self.current_row, column=2, pady=5)
758
+
759
+
760
+ def on_profile_change(self, event):
761
+ print("profile Changed!")
762
+ self.save_settings(profile_change=True)
763
+ self.reload_settings()
764
+
765
+ def add_profile(self):
766
+ new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
767
+ if new_profile_name:
768
+ self.master_config.configs[new_profile_name] = self.master_config.default_config
769
+ self.profile_combobox['values'] = list(self.master_config.configs.keys())
770
+ self.profile_combobox.set(new_profile_name)
771
+ self.save_settings()
772
+ self.reload_settings()
773
+
774
+ def copy_profile(self):
775
+ source_profile = self.profile_combobox.get()
776
+ new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
777
+ if new_profile_name and source_profile in self.master_config.configs:
778
+ self.master_config.configs[new_profile_name] = self.master_config.configs[source_profile]
779
+ self.profile_combobox['values'] = list(self.master_config.configs.keys())
780
+ self.profile_combobox.set(new_profile_name)
781
+ self.save_settings()
782
+ self.reload_settings()
783
+
784
+ def delete_profile(self):
785
+ profile_to_delete = self.profile_combobox.get()
786
+ if profile_to_delete == "Default":
787
+ messagebox.showerror("Error", "Cannot delete the Default profile.")
788
+ return
789
+
790
+ if profile_to_delete and profile_to_delete in self.master_config.configs:
791
+ confirm = messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete the profile '{profile_to_delete}'?")
792
+ if confirm:
793
+ del self.master_config.configs[profile_to_delete]
794
+ self.profile_combobox['values'] = list(self.master_config.configs.keys())
795
+ self.profile_combobox.set("Default")
796
+ self.save_settings()
797
+ self.reload_settings()
798
+
799
+
800
+ if __name__ == '__main__':
801
+ window = ConfigApp()
802
+ window.show()
803
+ window.window.mainloop()