GameSentenceMiner 2.9.0__py3-none-any.whl → 2.9.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.
- GameSentenceMiner/anki.py +1 -1
- GameSentenceMiner/communication/websocket.py +7 -0
- GameSentenceMiner/config_gui.py +54 -19
- GameSentenceMiner/configuration.py +67 -3
- GameSentenceMiner/ffmpeg.py +2 -2
- GameSentenceMiner/gametext.py +71 -62
- GameSentenceMiner/gsm.py +103 -51
- GameSentenceMiner/obs.py +2 -2
- GameSentenceMiner/ocr/owocr_helper.py +17 -25
- GameSentenceMiner/owocr/owocr/ocr.py +4 -3
- GameSentenceMiner/text_log.py +1 -1
- GameSentenceMiner/vad/groq_trim.py +82 -0
- GameSentenceMiner/vad/result.py +15 -2
- GameSentenceMiner/vad/silero_trim.py +14 -10
- GameSentenceMiner/vad/vosk_helper.py +2 -2
- GameSentenceMiner/vad/whisper_helper.py +8 -7
- GameSentenceMiner/web/texthooking_page.py +41 -26
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/METADATA +4 -1
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/RECORD +23 -22
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/WHEEL +1 -1
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.0.dist-info → gamesentenceminer-2.9.1.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -75,7 +75,7 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
75
75
|
logger.info(f"AI prompt Result: {translation}")
|
76
76
|
note['fields'][get_config().ai.anki_field] = translation
|
77
77
|
|
78
|
-
if prev_screenshot_in_anki:
|
78
|
+
if prev_screenshot_in_anki and get_config().anki.previous_image_field != get_config().anki.picture_field:
|
79
79
|
note['fields'][get_config().anki.previous_image_field] = prev_screenshot_html
|
80
80
|
|
81
81
|
if get_config().anki.anki_custom_fields:
|
@@ -21,6 +21,13 @@ class FunctionName(Enum):
|
|
21
21
|
STOP = "stop"
|
22
22
|
QUIT_OBS = "quit_obs"
|
23
23
|
START_OBS = "start_obs"
|
24
|
+
OPEN_SETTINGS = "open_settings"
|
25
|
+
OPEN_TEXTHOOKER = "open_texthooker"
|
26
|
+
OPEN_LOG = "open_log"
|
27
|
+
TOGGLE_REPLAY_BUFFER = "toggle_replay_buffer"
|
28
|
+
RESTART_OBS = "restart_obs"
|
29
|
+
EXIT = "exit"
|
30
|
+
|
24
31
|
|
25
32
|
async def do_websocket_connection(port):
|
26
33
|
"""
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -41,7 +41,7 @@ class HoverInfoWidget:
|
|
41
41
|
self.tooltip.wm_overrideredirect(True)
|
42
42
|
self.tooltip.wm_geometry(f"+{x}+{y}")
|
43
43
|
label = tk.Label(self.tooltip, text=text, background="yellow", relief="solid", borderwidth=1,
|
44
|
-
font=("tahoma", "
|
44
|
+
font=("tahoma", "12", "normal"))
|
45
45
|
label.pack(ipadx=1)
|
46
46
|
|
47
47
|
def hide_info_box(self):
|
@@ -58,6 +58,7 @@ class ConfigApp:
|
|
58
58
|
# self.window = ttk.Window(themename='darkly')
|
59
59
|
self.window.title('GameSentenceMiner Configuration')
|
60
60
|
self.window.protocol("WM_DELETE_WINDOW", self.hide)
|
61
|
+
self.obs_scene_listbox_changed = False
|
61
62
|
|
62
63
|
self.current_row = 0
|
63
64
|
|
@@ -83,7 +84,11 @@ class ConfigApp:
|
|
83
84
|
|
84
85
|
self.notebook.bind("<<NotebookTabChanged>>", self.on_profiles_tab_selected)
|
85
86
|
|
86
|
-
ttk.
|
87
|
+
button_frame = ttk.Frame(self.window)
|
88
|
+
button_frame.pack(side="top", pady=20, anchor="center")
|
89
|
+
|
90
|
+
ttk.Button(button_frame, text="Save Settings", command=self.save_settings).grid(row=0, column=0, padx=10)
|
91
|
+
ttk.Button(button_frame, text="Save and Sync Changes", command=lambda: self.save_settings(profile_change=False, sync_changes=True)).grid(row=0, column=1, padx=10)
|
87
92
|
|
88
93
|
self.window.withdraw()
|
89
94
|
|
@@ -118,12 +123,12 @@ class ConfigApp:
|
|
118
123
|
if self.window is not None:
|
119
124
|
self.window.withdraw()
|
120
125
|
|
121
|
-
def save_settings(self, profile_change=False):
|
126
|
+
def save_settings(self, profile_change=False, sync_changes=False):
|
122
127
|
global settings_saved
|
123
128
|
|
124
129
|
# Create a new Config instance
|
125
130
|
config = ProfileConfig(
|
126
|
-
scenes=[self.obs_scene_listbox.get(i) for i in self.obs_scene_listbox.curselection()],
|
131
|
+
scenes=[self.obs_scene_listbox.get(i) for i in self.obs_scene_listbox.curselection()] if self.obs_scene_listbox_changed else self.settings.scenes,
|
127
132
|
general=General(
|
128
133
|
use_websocket=self.websocket_enabled.get(),
|
129
134
|
use_clipboard=self.clipboard_enabled.get(),
|
@@ -221,11 +226,12 @@ class ConfigApp:
|
|
221
226
|
advanced=Advanced(
|
222
227
|
audio_player_path=self.audio_player_path.get(),
|
223
228
|
video_player_path=self.video_player_path.get(),
|
224
|
-
# show_screenshot_buttons=self.show_screenshot_button.get(),
|
225
229
|
multi_line_line_break=self.multi_line_line_break.get(),
|
226
230
|
multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
|
227
231
|
ocr_sends_to_clipboard=self.ocr_sends_to_clipboard.get(),
|
228
232
|
use_anki_note_creation_time=self.use_anki_note_creation_time.get(),
|
233
|
+
ocr_websocket_port=int(self.ocr_websocket_port.get()),
|
234
|
+
texthooker_communication_websocket_port=int(self.texthooker_communication_websocket_port.get()),
|
229
235
|
),
|
230
236
|
ai=Ai(
|
231
237
|
enabled=self.ai_enabled.get(),
|
@@ -262,10 +268,11 @@ class ConfigApp:
|
|
262
268
|
self.master_config.current_profile = current_profile
|
263
269
|
self.master_config.set_config_for_profile(current_profile, config)
|
264
270
|
|
265
|
-
|
266
|
-
|
267
271
|
self.master_config = self.master_config.sync_shared_fields()
|
268
272
|
|
273
|
+
if sync_changes:
|
274
|
+
self.master_config.sync_changed_fields(prev_config)
|
275
|
+
|
269
276
|
# Serialize the config instance to JSON
|
270
277
|
with open(get_config_path(), 'w') as file:
|
271
278
|
file.write(self.master_config.to_json(indent=4))
|
@@ -349,11 +356,11 @@ class ConfigApp:
|
|
349
356
|
self.add_label_and_increment_row(general_frame, "Enable to allow GSM to accept both clipboard and websocket input at the same time.",
|
350
357
|
row=self.current_row, column=2)
|
351
358
|
|
352
|
-
ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
|
353
|
-
self.websocket_uri = ttk.Entry(general_frame)
|
359
|
+
ttk.Label(general_frame, text="Websocket URI(s):").grid(row=self.current_row, column=0, sticky='W')
|
360
|
+
self.websocket_uri = ttk.Entry(general_frame, width=50)
|
354
361
|
self.websocket_uri.insert(0, self.settings.general.websocket_uri)
|
355
362
|
self.websocket_uri.grid(row=self.current_row, column=1)
|
356
|
-
self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting.", row=self.current_row,
|
363
|
+
self.add_label_and_increment_row(general_frame, "WebSocket URI for connecting. Allows Comma Seperated Values for Connecting Multiple.", row=self.current_row,
|
357
364
|
column=2)
|
358
365
|
|
359
366
|
ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
|
@@ -425,6 +432,13 @@ class ConfigApp:
|
|
425
432
|
self.add_label_and_increment_row(vad_frame, "Enable post-processing of audio to trim just the voiceline.",
|
426
433
|
row=self.current_row, column=2)
|
427
434
|
|
435
|
+
ttk.Label(vad_frame, text="Language:").grid(row=self.current_row, column=0, sticky='W')
|
436
|
+
self.language = ttk.Combobox(vad_frame, values=AVAILABLE_LANGUAGES)
|
437
|
+
self.language.set(self.settings.vad.language)
|
438
|
+
self.language.grid(row=self.current_row, column=1)
|
439
|
+
self.add_label_and_increment_row(vad_frame, "Select the language for VAD. This is used for Whisper and Groq (if i implemented it)", row=self.current_row,
|
440
|
+
column=2)
|
441
|
+
|
428
442
|
ttk.Label(vad_frame, text="Whisper Model:").grid(row=self.current_row, column=0, sticky='W')
|
429
443
|
self.whisper_model = ttk.Combobox(vad_frame, values=[WHISPER_TINY, WHISPER_BASE, WHISPER_SMALL, WHISPER_MEDIUM,
|
430
444
|
WHSIPER_LARGE])
|
@@ -442,13 +456,13 @@ class ConfigApp:
|
|
442
456
|
column=2)
|
443
457
|
|
444
458
|
ttk.Label(vad_frame, text="Select VAD Model:").grid(row=self.current_row, column=0, sticky='W')
|
445
|
-
self.selected_vad_model = ttk.Combobox(vad_frame, values=[VOSK, SILERO, WHISPER])
|
459
|
+
self.selected_vad_model = ttk.Combobox(vad_frame, values=[VOSK, SILERO, WHISPER, GROQ])
|
446
460
|
self.selected_vad_model.set(self.settings.vad.selected_vad_model)
|
447
461
|
self.selected_vad_model.grid(row=self.current_row, column=1)
|
448
462
|
self.add_label_and_increment_row(vad_frame, "Select which VAD model to use.", row=self.current_row, column=2)
|
449
463
|
|
450
464
|
ttk.Label(vad_frame, text="Backup VAD Model:").grid(row=self.current_row, column=0, sticky='W')
|
451
|
-
self.backup_vad_model = ttk.Combobox(vad_frame, values=[OFF, VOSK, SILERO, WHISPER])
|
465
|
+
self.backup_vad_model = ttk.Combobox(vad_frame, values=[OFF, VOSK, SILERO, WHISPER, GROQ])
|
452
466
|
self.backup_vad_model.set(self.settings.vad.backup_vad_model)
|
453
467
|
self.backup_vad_model.grid(row=self.current_row, column=1)
|
454
468
|
self.add_label_and_increment_row(vad_frame, "Select which model to use as a backup if no audio is found.",
|
@@ -603,14 +617,14 @@ class ConfigApp:
|
|
603
617
|
column=2)
|
604
618
|
|
605
619
|
ttk.Label(anki_frame, text="Custom Tags:").grid(row=self.current_row, column=0, sticky='W')
|
606
|
-
self.custom_tags = ttk.Entry(anki_frame)
|
620
|
+
self.custom_tags = ttk.Entry(anki_frame, width=50)
|
607
621
|
self.custom_tags.insert(0, ', '.join(self.settings.anki.custom_tags))
|
608
622
|
self.custom_tags.grid(row=self.current_row, column=1)
|
609
623
|
self.add_label_and_increment_row(anki_frame, "Comma-separated custom tags for the Anki cards.",
|
610
624
|
row=self.current_row, column=2)
|
611
625
|
|
612
626
|
ttk.Label(anki_frame, text="Tags to work on:").grid(row=self.current_row, column=0, sticky='W')
|
613
|
-
self.tags_to_check = ttk.Entry(anki_frame)
|
627
|
+
self.tags_to_check = ttk.Entry(anki_frame, width=50)
|
614
628
|
self.tags_to_check.insert(0, ', '.join(self.settings.anki.tags_to_check))
|
615
629
|
self.tags_to_check.grid(row=self.current_row, column=1)
|
616
630
|
self.add_label_and_increment_row(anki_frame,
|
@@ -1022,14 +1036,18 @@ class ConfigApp:
|
|
1022
1036
|
|
1023
1037
|
ttk.Button(profiles_frame, text="Add Profile", command=self.add_profile).grid(row=self.current_row, column=0, pady=5)
|
1024
1038
|
ttk.Button(profiles_frame, text="Copy Profile", command=self.copy_profile).grid(row=self.current_row, column=1, pady=5)
|
1039
|
+
self.delete_profile_button = ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile)
|
1025
1040
|
if self.master_config.current_profile != DEFAULT_CONFIG:
|
1026
|
-
|
1041
|
+
self.delete_profile_button.grid(row=1, column=2, pady=5)
|
1042
|
+
else:
|
1043
|
+
self.delete_profile_button.grid_remove()
|
1027
1044
|
self.current_row += 1
|
1028
1045
|
|
1029
1046
|
ttk.Label(profiles_frame, text="OBS Scene:").grid(row=self.current_row, column=0, sticky='W')
|
1030
1047
|
self.obs_scene_var = tk.StringVar(value="")
|
1031
|
-
self.obs_scene_listbox = tk.Listbox(profiles_frame, listvariable=self.obs_scene_var, selectmode=tk.MULTIPLE, height=10)
|
1048
|
+
self.obs_scene_listbox = tk.Listbox(profiles_frame, listvariable=self.obs_scene_var, selectmode=tk.MULTIPLE, height=10, width=50)
|
1032
1049
|
self.obs_scene_listbox.grid(row=self.current_row, column=1)
|
1050
|
+
self.obs_scene_listbox.bind("<<ListboxSelect>>", self.on_obs_scene_select)
|
1033
1051
|
ttk.Button(profiles_frame, text="Refresh Scenes", command=self.refresh_obs_scenes).grid(row=self.current_row, column=2, pady=5)
|
1034
1052
|
self.add_label_and_increment_row(profiles_frame, "Select an OBS scene to associate with this profile. (Optional)", row=self.current_row, column=3)
|
1035
1053
|
|
@@ -1038,6 +1056,8 @@ class ConfigApp:
|
|
1038
1056
|
ttk.Checkbutton(profiles_frame, variable=self.switch_to_default_if_not_found).grid(row=self.current_row, column=1, sticky='W')
|
1039
1057
|
self.add_label_and_increment_row(profiles_frame, "Enable to switch to the default profile if the selected OBS scene is not found.", row=self.current_row, column=2)
|
1040
1058
|
|
1059
|
+
def on_obs_scene_select(self, event):
|
1060
|
+
self.obs_scene_listbox_changed = True
|
1041
1061
|
|
1042
1062
|
def refresh_obs_scenes(self):
|
1043
1063
|
scenes = obs.get_obs_scenes()
|
@@ -1096,6 +1116,17 @@ class ConfigApp:
|
|
1096
1116
|
ttk.Checkbutton(advanced_frame, variable=self.ocr_sends_to_clipboard).grid(row=self.current_row, column=1, sticky='W')
|
1097
1117
|
self.add_label_and_increment_row(advanced_frame, "Enable to send OCR results to clipboard.", row=self.current_row, column=2)
|
1098
1118
|
|
1119
|
+
self.ocr_websocket_port = ttk.Entry(advanced_frame)
|
1120
|
+
self.ocr_websocket_port.insert(0, str(self.settings.advanced.ocr_websocket_port))
|
1121
|
+
self.ocr_websocket_port.grid(row=self.current_row, column=1)
|
1122
|
+
self.add_label_and_increment_row(advanced_frame, "Port for OCR WebSocket communication. GSM will also listen on this port", row=self.current_row, column=2)
|
1123
|
+
|
1124
|
+
ttk.Label(advanced_frame, text="Texthooker Communication WebSocket Port:").grid(row=self.current_row, column=0, sticky='W')
|
1125
|
+
self.texthooker_communication_websocket_port = ttk.Entry(advanced_frame)
|
1126
|
+
self.texthooker_communication_websocket_port.insert(0, str(self.settings.advanced.texthooker_communication_websocket_port))
|
1127
|
+
self.texthooker_communication_websocket_port.grid(row=self.current_row, column=1)
|
1128
|
+
self.add_label_and_increment_row(advanced_frame, "Port for GSM Texthooker WebSocket communication. Does nothing right now, hardcoded to 55001", row=self.current_row, column=2)
|
1129
|
+
|
1099
1130
|
|
1100
1131
|
ttk.Label(advanced_frame, text="Use Anki Creation Date for Audio Timing:").grid(row=self.current_row, column=0, sticky='W')
|
1101
1132
|
self.use_anki_note_creation_time = tk.BooleanVar(value=self.settings.advanced.use_anki_note_creation_time)
|
@@ -1121,7 +1152,7 @@ class ConfigApp:
|
|
1121
1152
|
self.add_label_and_increment_row(ai_frame, "Select the AI provider.", row=self.current_row, column=2)
|
1122
1153
|
|
1123
1154
|
ttk.Label(ai_frame, text="Gemini AI Model:").grid(row=self.current_row, column=0, sticky='W')
|
1124
|
-
self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-pro-preview-
|
1155
|
+
self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-pro-preview-05-06', 'gemini-2.5-flash-preview-05-20'])
|
1125
1156
|
self.gemini_model.set(self.settings.ai.gemini_model)
|
1126
1157
|
self.gemini_model.grid(row=self.current_row, column=1)
|
1127
1158
|
self.add_label_and_increment_row(ai_frame, "Select the AI model to use.", row=self.current_row, column=2)
|
@@ -1188,10 +1219,13 @@ class ConfigApp:
|
|
1188
1219
|
|
1189
1220
|
|
1190
1221
|
def on_profile_change(self, event):
|
1191
|
-
print("profile Changed!")
|
1192
1222
|
self.save_settings(profile_change=True)
|
1193
1223
|
self.reload_settings()
|
1194
1224
|
self.refresh_obs_scenes()
|
1225
|
+
if self.master_config.current_profile != DEFAULT_CONFIG:
|
1226
|
+
self.delete_profile_button.grid(row=1, column=2, pady=5)
|
1227
|
+
else:
|
1228
|
+
self.delete_profile_button.grid_remove()
|
1195
1229
|
|
1196
1230
|
def add_profile(self):
|
1197
1231
|
new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
|
@@ -1224,7 +1258,8 @@ class ConfigApp:
|
|
1224
1258
|
del self.master_config.configs[profile_to_delete]
|
1225
1259
|
self.profile_combobox['values'] = list(self.master_config.configs.keys())
|
1226
1260
|
self.profile_combobox.set("Default")
|
1227
|
-
self.
|
1261
|
+
self.master_config.current_profile = "Default"
|
1262
|
+
save_full_config(self.master_config)
|
1228
1263
|
self.reload_settings()
|
1229
1264
|
|
1230
1265
|
|
@@ -1,23 +1,24 @@
|
|
1
|
+
import dataclasses
|
1
2
|
import json
|
2
3
|
import logging
|
3
4
|
import os
|
4
5
|
import shutil
|
5
|
-
import socket
|
6
6
|
from dataclasses import dataclass, field
|
7
7
|
from logging.handlers import RotatingFileHandler
|
8
8
|
from os.path import expanduser
|
9
9
|
from sys import platform
|
10
10
|
from typing import List, Dict
|
11
11
|
import sys
|
12
|
+
from enum import Enum
|
12
13
|
|
13
14
|
import toml
|
14
15
|
from dataclasses_json import dataclass_json
|
15
16
|
|
16
|
-
|
17
17
|
OFF = 'OFF'
|
18
18
|
VOSK = 'VOSK'
|
19
19
|
SILERO = 'SILERO'
|
20
20
|
WHISPER = 'WHISPER'
|
21
|
+
GROQ = 'GROQ'
|
21
22
|
|
22
23
|
VOSK_BASE = 'BASE'
|
23
24
|
VOSK_SMALL = 'SMALL'
|
@@ -38,13 +39,31 @@ DEFAULT_CONFIG = 'Default'
|
|
38
39
|
|
39
40
|
current_game = ''
|
40
41
|
|
42
|
+
|
43
|
+
class Language(Enum):
|
44
|
+
JAPANESE = "ja"
|
45
|
+
ENGLISH = "en"
|
46
|
+
KOREAN = "ko"
|
47
|
+
CHINESE = "zh"
|
48
|
+
SPANISH = "es"
|
49
|
+
FRENCH = "fr"
|
50
|
+
GERMAN = "de"
|
51
|
+
ITALIAN = "it"
|
52
|
+
RUSSIAN = "ru"
|
53
|
+
PORTUGUESE = "pt"
|
54
|
+
HINDI = "hi"
|
55
|
+
ARABIC = "ar"
|
56
|
+
|
57
|
+
AVAILABLE_LANGUAGES = [lang.value for lang in Language]
|
58
|
+
AVAILABLE_LANGUAGES_DICT = {lang.value: lang for lang in Language}
|
59
|
+
|
41
60
|
@dataclass_json
|
42
61
|
@dataclass
|
43
62
|
class General:
|
44
63
|
use_websocket: bool = True
|
45
64
|
use_clipboard: bool = True
|
46
65
|
use_both_clipboard_and_websocket: bool = False
|
47
|
-
websocket_uri: str = 'localhost:6677'
|
66
|
+
websocket_uri: str = 'localhost:6677,localhost:9001,localhost:2333'
|
48
67
|
open_config_on_startup: bool = False
|
49
68
|
open_multimine_on_startup: bool = True
|
50
69
|
texthook_replacement_regex: str = ""
|
@@ -168,6 +187,7 @@ class Hotkeys:
|
|
168
187
|
class VAD:
|
169
188
|
whisper_model: str = WHISPER_BASE
|
170
189
|
do_vad_postprocessing: bool = True
|
190
|
+
language: str = 'ja'
|
171
191
|
vosk_url: str = VOSK_BASE
|
172
192
|
selected_vad_model: str = SILERO
|
173
193
|
backup_vad_model: str = OFF
|
@@ -184,6 +204,9 @@ class VAD:
|
|
184
204
|
def is_vosk(self):
|
185
205
|
return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
|
186
206
|
|
207
|
+
def is_groq(self):
|
208
|
+
return self.selected_vad_model == GROQ or self.backup_vad_model == GROQ
|
209
|
+
|
187
210
|
|
188
211
|
@dataclass_json
|
189
212
|
@dataclass
|
@@ -194,6 +217,8 @@ class Advanced:
|
|
194
217
|
multi_line_line_break: str = '<br>'
|
195
218
|
multi_line_sentence_storage_field: str = ''
|
196
219
|
ocr_sends_to_clipboard: bool = True
|
220
|
+
ocr_websocket_port: int = 9002
|
221
|
+
texthooker_communication_websocket_port: int = 55001
|
197
222
|
use_anki_note_creation_time: bool = False
|
198
223
|
|
199
224
|
@dataclass_json
|
@@ -347,6 +372,9 @@ class Config:
|
|
347
372
|
return self
|
348
373
|
|
349
374
|
def get_config(self) -> ProfileConfig:
|
375
|
+
if self.current_profile not in self.configs:
|
376
|
+
logger.warning(f"Profile '{self.current_profile}' not found. Switching to default profile.")
|
377
|
+
self.current_profile = DEFAULT_CONFIG
|
350
378
|
return self.configs[self.current_profile]
|
351
379
|
|
352
380
|
def set_config_for_profile(self, profile: str, config: ProfileConfig):
|
@@ -362,6 +390,27 @@ class Config:
|
|
362
390
|
def get_default_config(self):
|
363
391
|
return self.configs[DEFAULT_CONFIG]
|
364
392
|
|
393
|
+
def sync_changed_fields(self, previous_config: ProfileConfig):
|
394
|
+
current_config = self.get_config()
|
395
|
+
|
396
|
+
for section in current_config.to_dict():
|
397
|
+
if dataclasses.is_dataclass(getattr(current_config, section, None)):
|
398
|
+
for field_name in getattr(current_config, section, None).to_dict():
|
399
|
+
config_section = getattr(current_config, section, None)
|
400
|
+
previous_config_section = getattr(previous_config, section, None)
|
401
|
+
current_value = getattr(config_section, field_name, None)
|
402
|
+
previous_value = getattr(previous_config_section, field_name, None)
|
403
|
+
if str(current_value).strip() != str(previous_value).strip():
|
404
|
+
logger.info(f"Syncing changed field '{field_name}' from '{previous_value}' to '{current_value}'")
|
405
|
+
for profile in self.configs.values():
|
406
|
+
if profile != current_config:
|
407
|
+
profile_section = getattr(profile, section, None)
|
408
|
+
if profile_section:
|
409
|
+
setattr(profile_section, field_name, current_value)
|
410
|
+
logger.info(f"Updated '{field_name}' in profile '{profile.name}'")
|
411
|
+
|
412
|
+
return self
|
413
|
+
|
365
414
|
def sync_shared_fields(self):
|
366
415
|
config = self.get_config()
|
367
416
|
for profile in self.configs.values():
|
@@ -390,6 +439,9 @@ class Config:
|
|
390
439
|
self.sync_shared_field(config.general, profile.general, "use_old_texthooker")
|
391
440
|
self.sync_shared_field(config.audio, profile.audio, "external_tool")
|
392
441
|
self.sync_shared_field(config.audio, profile.audio, "anki_media_collection")
|
442
|
+
self.sync_shared_field(config.audio, profile.audio, "external_tool_enabled")
|
443
|
+
self.sync_shared_field(config.audio, profile.audio, "custom_encode_settings")
|
444
|
+
self.sync_shared_field(config.screenshot, profile.screenshot, "custom_ffmpeg_settings")
|
393
445
|
self.sync_shared_field(config, profile, "advanced")
|
394
446
|
self.sync_shared_field(config, profile, "paths")
|
395
447
|
self.sync_shared_field(config, profile, "obs")
|
@@ -575,3 +627,15 @@ if 'gsm' in sys.argv[0]:
|
|
575
627
|
|
576
628
|
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
577
629
|
|
630
|
+
class GsmAppState:
|
631
|
+
def __init__(self):
|
632
|
+
self.line_for_audio = None
|
633
|
+
self.line_for_screenshot = None
|
634
|
+
self.previous_line_for_audio = None
|
635
|
+
self.previous_line_for_screenshot = None
|
636
|
+
self.previous_audio = None
|
637
|
+
self.previous_screenshot = None
|
638
|
+
self.previous_replay = None
|
639
|
+
|
640
|
+
gsm_state = GsmAppState()
|
641
|
+
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -396,8 +396,8 @@ def trim_audio_by_end_time(input_audio, end_time, output_audio):
|
|
396
396
|
def convert_audio_to_wav(input_audio, output_wav):
|
397
397
|
command = ffmpeg_base_command_list + [
|
398
398
|
"-i", input_audio,
|
399
|
-
"-ar", "16000",
|
400
|
-
"-ac", "1",
|
399
|
+
"-ar", "16000", # Resample to 16kHz
|
400
|
+
"-ac", "1", # Convert to mono
|
401
401
|
"-af", "afftdn,dialoguenhance" if not util.is_linux() else "afftdn",
|
402
402
|
output_wav
|
403
403
|
]
|
GameSentenceMiner/gametext.py
CHANGED
@@ -17,82 +17,91 @@ current_line_after_regex = ''
|
|
17
17
|
current_line_time = datetime.now()
|
18
18
|
|
19
19
|
reconnecting = False
|
20
|
-
websocket_connected =
|
21
|
-
|
22
|
-
# def remove_old_events(self, cutoff_time: datetime):
|
23
|
-
# self.values = [line for line in self.values if line.time >= cutoff_time]
|
20
|
+
websocket_connected = {}
|
24
21
|
|
25
22
|
async def monitor_clipboard():
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
skip_next_clipboard = False
|
23
|
+
global current_line
|
24
|
+
current_line = pyperclip.paste()
|
25
|
+
send_message_on_resume = False
|
30
26
|
while True:
|
31
27
|
if not get_config().general.use_clipboard:
|
32
|
-
await asyncio.sleep(
|
28
|
+
await asyncio.sleep(5)
|
33
29
|
continue
|
34
|
-
if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
|
30
|
+
if not get_config().general.use_both_clipboard_and_websocket and any(websocket_connected.values()):
|
35
31
|
await asyncio.sleep(1)
|
36
|
-
|
32
|
+
send_message_on_resume = True
|
37
33
|
continue
|
34
|
+
elif send_message_on_resume:
|
35
|
+
logger.info("No Websocket Connections, resuming Clipboard Monitoring.")
|
36
|
+
send_message_on_resume = False
|
38
37
|
current_clipboard = pyperclip.paste()
|
39
38
|
|
40
|
-
if current_clipboard and current_clipboard != current_line
|
39
|
+
if current_clipboard and current_clipboard != current_line:
|
41
40
|
await handle_new_text_event(current_clipboard)
|
42
|
-
skip_next_clipboard = False
|
43
41
|
|
44
42
|
await asyncio.sleep(0.05)
|
45
43
|
|
46
44
|
|
47
|
-
async def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
websocket_url = f'ws://{
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
logger.info(f"
|
62
|
-
reconnecting
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
while True:
|
67
|
-
message = await websocket.recv()
|
68
|
-
if not message:
|
69
|
-
continue
|
70
|
-
logger.debug(message)
|
71
|
-
try:
|
72
|
-
data = json.loads(message)
|
73
|
-
if "sentence" in data:
|
74
|
-
current_clipboard = data["sentence"]
|
75
|
-
if "time" in data:
|
76
|
-
line_time = datetime.fromisoformat(data["time"])
|
77
|
-
except json.JSONDecodeError or TypeError:
|
78
|
-
current_clipboard = message
|
79
|
-
if current_clipboard != current_line:
|
80
|
-
await handle_new_text_event(current_clipboard, line_time if line_time else None)
|
81
|
-
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
|
82
|
-
if isinstance(e, InvalidStatus):
|
83
|
-
e: InvalidStatus
|
84
|
-
if e.response.status_code == 404:
|
85
|
-
logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
|
45
|
+
async def listen_websockets():
|
46
|
+
async def listen_on_websocket(uri):
|
47
|
+
global current_line, current_line_time, reconnecting, websocket_connected
|
48
|
+
try_other = False
|
49
|
+
websocket_connected[uri] = False
|
50
|
+
while True:
|
51
|
+
if not get_config().general.use_websocket:
|
52
|
+
await asyncio.sleep(1)
|
53
|
+
continue
|
54
|
+
websocket_url = f'ws://{uri}'
|
55
|
+
if try_other:
|
56
|
+
websocket_url = f'ws://{uri}/api/ws/text/origin'
|
57
|
+
try:
|
58
|
+
async with websockets.connect(websocket_url, ping_interval=None) as websocket:
|
59
|
+
logger.info(f"TextHooker Websocket {uri} Connected!")
|
60
|
+
if reconnecting:
|
61
|
+
logger.info(f"Texthooker WebSocket {uri} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
|
62
|
+
reconnecting = False
|
63
|
+
websocket_connected[uri] = True
|
86
64
|
try_other = True
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
65
|
+
line_time = None
|
66
|
+
while True:
|
67
|
+
message = await websocket.recv()
|
68
|
+
if not message:
|
69
|
+
continue
|
70
|
+
logger.debug(message)
|
71
|
+
try:
|
72
|
+
data = json.loads(message)
|
73
|
+
if "sentence" in data:
|
74
|
+
current_clipboard = data["sentence"]
|
75
|
+
if "time" in data:
|
76
|
+
line_time = datetime.fromisoformat(data["time"])
|
77
|
+
except json.JSONDecodeError or TypeError:
|
78
|
+
current_clipboard = message
|
79
|
+
if current_clipboard != current_line:
|
80
|
+
await handle_new_text_event(current_clipboard, line_time if line_time else None)
|
81
|
+
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
|
82
|
+
if isinstance(e, InvalidStatus):
|
83
|
+
e: InvalidStatus
|
84
|
+
if e.response.status_code == 404:
|
85
|
+
logger.info(f"Texthooker WebSocket: {uri} connection failed. Attempting some fixes...")
|
86
|
+
try_other = True
|
87
|
+
else:
|
88
|
+
if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
|
89
|
+
logger.error(f"Unexpected error in Texthooker WebSocket {uri} connection: {e}")
|
90
|
+
if websocket_connected[uri]:
|
91
|
+
logger.warning(f"Texthooker WebSocket {uri} disconnected. Attempting to reconnect...")
|
92
|
+
websocket_connected[uri] = False
|
93
|
+
await asyncio.sleep(1)
|
94
|
+
|
95
|
+
websocket_tasks = []
|
96
|
+
if ',' in get_config().general.websocket_uri:
|
97
|
+
for uri in get_config().general.websocket_uri.split(','):
|
98
|
+
websocket_tasks.append(listen_on_websocket(uri))
|
99
|
+
else:
|
100
|
+
websocket_tasks.append(listen_on_websocket(get_config().general.websocket_uri))
|
101
|
+
|
102
|
+
websocket_tasks.append(listen_on_websocket(f"localhost:{get_config().advanced.ocr_websocket_port}"))
|
103
|
+
|
104
|
+
await asyncio.gather(*websocket_tasks)
|
96
105
|
|
97
106
|
async def handle_new_text_event(current_clipboard, line_time=None):
|
98
107
|
global current_line, current_line_time, current_line_after_regex
|
@@ -116,7 +125,7 @@ def reset_line_hotkey_pressed():
|
|
116
125
|
|
117
126
|
|
118
127
|
def run_websocket_listener():
|
119
|
-
asyncio.run(
|
128
|
+
asyncio.run(listen_websockets())
|
120
129
|
|
121
130
|
|
122
131
|
async def start_text_monitor():
|