GameSentenceMiner 2.8.54__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 +63 -20
- GameSentenceMiner/configuration.py +69 -3
- GameSentenceMiner/ffmpeg.py +2 -2
- GameSentenceMiner/gametext.py +71 -62
- GameSentenceMiner/gsm.py +108 -55
- 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/vad_utils.py +13 -0
- GameSentenceMiner/vad/vosk_helper.py +2 -2
- GameSentenceMiner/vad/whisper_helper.py +8 -7
- GameSentenceMiner/web/templates/index.html +49 -0
- GameSentenceMiner/web/templates/utility.html +2 -2
- GameSentenceMiner/web/texthooking_page.py +54 -32
- {gamesentenceminer-2.8.54.dist-info → gamesentenceminer-2.9.1.dist-info}/METADATA +4 -1
- {gamesentenceminer-2.8.54.dist-info → gamesentenceminer-2.9.1.dist-info}/RECORD +26 -23
- {gamesentenceminer-2.8.54.dist-info → gamesentenceminer-2.9.1.dist-info}/WHEEL +1 -1
- {gamesentenceminer-2.8.54.dist-info → gamesentenceminer-2.9.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.8.54.dist-info → gamesentenceminer-2.9.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.8.54.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(),
|
@@ -131,7 +136,8 @@ class ConfigApp:
|
|
131
136
|
open_config_on_startup=self.open_config_on_startup.get(),
|
132
137
|
open_multimine_on_startup=self.open_multimine_on_startup.get(),
|
133
138
|
texthook_replacement_regex=self.texthook_replacement_regex.get(),
|
134
|
-
use_both_clipboard_and_websocket=self.use_both_clipboard_and_websocket.get()
|
139
|
+
use_both_clipboard_and_websocket=self.use_both_clipboard_and_websocket.get(),
|
140
|
+
use_old_texthooker=self.use_old_texthooker.get()
|
135
141
|
),
|
136
142
|
paths=Paths(
|
137
143
|
folder_to_watch=self.folder_to_watch.get(),
|
@@ -220,11 +226,12 @@ class ConfigApp:
|
|
220
226
|
advanced=Advanced(
|
221
227
|
audio_player_path=self.audio_player_path.get(),
|
222
228
|
video_player_path=self.video_player_path.get(),
|
223
|
-
# show_screenshot_buttons=self.show_screenshot_button.get(),
|
224
229
|
multi_line_line_break=self.multi_line_line_break.get(),
|
225
230
|
multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
|
226
231
|
ocr_sends_to_clipboard=self.ocr_sends_to_clipboard.get(),
|
227
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()),
|
228
235
|
),
|
229
236
|
ai=Ai(
|
230
237
|
enabled=self.ai_enabled.get(),
|
@@ -261,10 +268,11 @@ class ConfigApp:
|
|
261
268
|
self.master_config.current_profile = current_profile
|
262
269
|
self.master_config.set_config_for_profile(current_profile, config)
|
263
270
|
|
264
|
-
|
265
|
-
|
266
271
|
self.master_config = self.master_config.sync_shared_fields()
|
267
272
|
|
273
|
+
if sync_changes:
|
274
|
+
self.master_config.sync_changed_fields(prev_config)
|
275
|
+
|
268
276
|
# Serialize the config instance to JSON
|
269
277
|
with open(get_config_path(), 'w') as file:
|
270
278
|
file.write(self.master_config.to_json(indent=4))
|
@@ -348,11 +356,11 @@ class ConfigApp:
|
|
348
356
|
self.add_label_and_increment_row(general_frame, "Enable to allow GSM to accept both clipboard and websocket input at the same time.",
|
349
357
|
row=self.current_row, column=2)
|
350
358
|
|
351
|
-
ttk.Label(general_frame, text="Websocket URI:").grid(row=self.current_row, column=0, sticky='W')
|
352
|
-
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)
|
353
361
|
self.websocket_uri.insert(0, self.settings.general.websocket_uri)
|
354
362
|
self.websocket_uri.grid(row=self.current_row, column=1)
|
355
|
-
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,
|
356
364
|
column=2)
|
357
365
|
|
358
366
|
ttk.Label(general_frame, text="TextHook Replacement Regex:").grid(row=self.current_row, column=0, sticky='W')
|
@@ -383,6 +391,13 @@ class ConfigApp:
|
|
383
391
|
self.add_label_and_increment_row(general_frame, "Port for the Texthooker to run on. Only change if you know what you are doing", row=self.current_row,
|
384
392
|
column=2)
|
385
393
|
|
394
|
+
ttk.Label(general_frame, text="Use Old Texthooker").grid(row=self.current_row, column=0, sticky='W')
|
395
|
+
self.use_old_texthooker = tk.BooleanVar(value=self.settings.general.use_old_texthooker)
|
396
|
+
ttk.Checkbutton(general_frame, variable=self.use_old_texthooker).grid(row=self.current_row, column=1,
|
397
|
+
sticky='W')
|
398
|
+
self.add_label_and_increment_row(general_frame, "Use the old Texthooker page, this option will be removed at a later date.", row=self.current_row,
|
399
|
+
column=2)
|
400
|
+
|
386
401
|
|
387
402
|
ttk.Label(general_frame, text="Current Version:").grid(row=self.current_row, column=0, sticky='W')
|
388
403
|
self.current_version = ttk.Label(general_frame, text=get_current_version())
|
@@ -417,6 +432,13 @@ class ConfigApp:
|
|
417
432
|
self.add_label_and_increment_row(vad_frame, "Enable post-processing of audio to trim just the voiceline.",
|
418
433
|
row=self.current_row, column=2)
|
419
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
|
+
|
420
442
|
ttk.Label(vad_frame, text="Whisper Model:").grid(row=self.current_row, column=0, sticky='W')
|
421
443
|
self.whisper_model = ttk.Combobox(vad_frame, values=[WHISPER_TINY, WHISPER_BASE, WHISPER_SMALL, WHISPER_MEDIUM,
|
422
444
|
WHSIPER_LARGE])
|
@@ -434,13 +456,13 @@ class ConfigApp:
|
|
434
456
|
column=2)
|
435
457
|
|
436
458
|
ttk.Label(vad_frame, text="Select VAD Model:").grid(row=self.current_row, column=0, sticky='W')
|
437
|
-
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])
|
438
460
|
self.selected_vad_model.set(self.settings.vad.selected_vad_model)
|
439
461
|
self.selected_vad_model.grid(row=self.current_row, column=1)
|
440
462
|
self.add_label_and_increment_row(vad_frame, "Select which VAD model to use.", row=self.current_row, column=2)
|
441
463
|
|
442
464
|
ttk.Label(vad_frame, text="Backup VAD Model:").grid(row=self.current_row, column=0, sticky='W')
|
443
|
-
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])
|
444
466
|
self.backup_vad_model.set(self.settings.vad.backup_vad_model)
|
445
467
|
self.backup_vad_model.grid(row=self.current_row, column=1)
|
446
468
|
self.add_label_and_increment_row(vad_frame, "Select which model to use as a backup if no audio is found.",
|
@@ -595,14 +617,14 @@ class ConfigApp:
|
|
595
617
|
column=2)
|
596
618
|
|
597
619
|
ttk.Label(anki_frame, text="Custom Tags:").grid(row=self.current_row, column=0, sticky='W')
|
598
|
-
self.custom_tags = ttk.Entry(anki_frame)
|
620
|
+
self.custom_tags = ttk.Entry(anki_frame, width=50)
|
599
621
|
self.custom_tags.insert(0, ', '.join(self.settings.anki.custom_tags))
|
600
622
|
self.custom_tags.grid(row=self.current_row, column=1)
|
601
623
|
self.add_label_and_increment_row(anki_frame, "Comma-separated custom tags for the Anki cards.",
|
602
624
|
row=self.current_row, column=2)
|
603
625
|
|
604
626
|
ttk.Label(anki_frame, text="Tags to work on:").grid(row=self.current_row, column=0, sticky='W')
|
605
|
-
self.tags_to_check = ttk.Entry(anki_frame)
|
627
|
+
self.tags_to_check = ttk.Entry(anki_frame, width=50)
|
606
628
|
self.tags_to_check.insert(0, ', '.join(self.settings.anki.tags_to_check))
|
607
629
|
self.tags_to_check.grid(row=self.current_row, column=1)
|
608
630
|
self.add_label_and_increment_row(anki_frame,
|
@@ -1014,14 +1036,18 @@ class ConfigApp:
|
|
1014
1036
|
|
1015
1037
|
ttk.Button(profiles_frame, text="Add Profile", command=self.add_profile).grid(row=self.current_row, column=0, pady=5)
|
1016
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)
|
1017
1040
|
if self.master_config.current_profile != DEFAULT_CONFIG:
|
1018
|
-
|
1041
|
+
self.delete_profile_button.grid(row=1, column=2, pady=5)
|
1042
|
+
else:
|
1043
|
+
self.delete_profile_button.grid_remove()
|
1019
1044
|
self.current_row += 1
|
1020
1045
|
|
1021
1046
|
ttk.Label(profiles_frame, text="OBS Scene:").grid(row=self.current_row, column=0, sticky='W')
|
1022
1047
|
self.obs_scene_var = tk.StringVar(value="")
|
1023
|
-
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)
|
1024
1049
|
self.obs_scene_listbox.grid(row=self.current_row, column=1)
|
1050
|
+
self.obs_scene_listbox.bind("<<ListboxSelect>>", self.on_obs_scene_select)
|
1025
1051
|
ttk.Button(profiles_frame, text="Refresh Scenes", command=self.refresh_obs_scenes).grid(row=self.current_row, column=2, pady=5)
|
1026
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)
|
1027
1053
|
|
@@ -1030,6 +1056,8 @@ class ConfigApp:
|
|
1030
1056
|
ttk.Checkbutton(profiles_frame, variable=self.switch_to_default_if_not_found).grid(row=self.current_row, column=1, sticky='W')
|
1031
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)
|
1032
1058
|
|
1059
|
+
def on_obs_scene_select(self, event):
|
1060
|
+
self.obs_scene_listbox_changed = True
|
1033
1061
|
|
1034
1062
|
def refresh_obs_scenes(self):
|
1035
1063
|
scenes = obs.get_obs_scenes()
|
@@ -1088,6 +1116,17 @@ class ConfigApp:
|
|
1088
1116
|
ttk.Checkbutton(advanced_frame, variable=self.ocr_sends_to_clipboard).grid(row=self.current_row, column=1, sticky='W')
|
1089
1117
|
self.add_label_and_increment_row(advanced_frame, "Enable to send OCR results to clipboard.", row=self.current_row, column=2)
|
1090
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
|
+
|
1091
1130
|
|
1092
1131
|
ttk.Label(advanced_frame, text="Use Anki Creation Date for Audio Timing:").grid(row=self.current_row, column=0, sticky='W')
|
1093
1132
|
self.use_anki_note_creation_time = tk.BooleanVar(value=self.settings.advanced.use_anki_note_creation_time)
|
@@ -1113,7 +1152,7 @@ class ConfigApp:
|
|
1113
1152
|
self.add_label_and_increment_row(ai_frame, "Select the AI provider.", row=self.current_row, column=2)
|
1114
1153
|
|
1115
1154
|
ttk.Label(ai_frame, text="Gemini AI Model:").grid(row=self.current_row, column=0, sticky='W')
|
1116
|
-
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'])
|
1117
1156
|
self.gemini_model.set(self.settings.ai.gemini_model)
|
1118
1157
|
self.gemini_model.grid(row=self.current_row, column=1)
|
1119
1158
|
self.add_label_and_increment_row(ai_frame, "Select the AI model to use.", row=self.current_row, column=2)
|
@@ -1180,10 +1219,13 @@ class ConfigApp:
|
|
1180
1219
|
|
1181
1220
|
|
1182
1221
|
def on_profile_change(self, event):
|
1183
|
-
print("profile Changed!")
|
1184
1222
|
self.save_settings(profile_change=True)
|
1185
1223
|
self.reload_settings()
|
1186
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()
|
1187
1229
|
|
1188
1230
|
def add_profile(self):
|
1189
1231
|
new_profile_name = simpledialog.askstring("Input", "Enter new profile name:")
|
@@ -1216,7 +1258,8 @@ class ConfigApp:
|
|
1216
1258
|
del self.master_config.configs[profile_to_delete]
|
1217
1259
|
self.profile_combobox['values'] = list(self.master_config.configs.keys())
|
1218
1260
|
self.profile_combobox.set("Default")
|
1219
|
-
self.
|
1261
|
+
self.master_config.current_profile = "Default"
|
1262
|
+
save_full_config(self.master_config)
|
1220
1263
|
self.reload_settings()
|
1221
1264
|
|
1222
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,17 +39,36 @@ 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 = ""
|
51
70
|
texthooker_port: int = 55000
|
71
|
+
use_old_texthooker: bool = False
|
52
72
|
|
53
73
|
|
54
74
|
@dataclass_json
|
@@ -167,6 +187,7 @@ class Hotkeys:
|
|
167
187
|
class VAD:
|
168
188
|
whisper_model: str = WHISPER_BASE
|
169
189
|
do_vad_postprocessing: bool = True
|
190
|
+
language: str = 'ja'
|
170
191
|
vosk_url: str = VOSK_BASE
|
171
192
|
selected_vad_model: str = SILERO
|
172
193
|
backup_vad_model: str = OFF
|
@@ -183,6 +204,9 @@ class VAD:
|
|
183
204
|
def is_vosk(self):
|
184
205
|
return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
|
185
206
|
|
207
|
+
def is_groq(self):
|
208
|
+
return self.selected_vad_model == GROQ or self.backup_vad_model == GROQ
|
209
|
+
|
186
210
|
|
187
211
|
@dataclass_json
|
188
212
|
@dataclass
|
@@ -193,6 +217,8 @@ class Advanced:
|
|
193
217
|
multi_line_line_break: str = '<br>'
|
194
218
|
multi_line_sentence_storage_field: str = ''
|
195
219
|
ocr_sends_to_clipboard: bool = True
|
220
|
+
ocr_websocket_port: int = 9002
|
221
|
+
texthooker_communication_websocket_port: int = 55001
|
196
222
|
use_anki_note_creation_time: bool = False
|
197
223
|
|
198
224
|
@dataclass_json
|
@@ -346,6 +372,9 @@ class Config:
|
|
346
372
|
return self
|
347
373
|
|
348
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
|
349
378
|
return self.configs[self.current_profile]
|
350
379
|
|
351
380
|
def set_config_for_profile(self, profile: str, config: ProfileConfig):
|
@@ -361,6 +390,27 @@ class Config:
|
|
361
390
|
def get_default_config(self):
|
362
391
|
return self.configs[DEFAULT_CONFIG]
|
363
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
|
+
|
364
414
|
def sync_shared_fields(self):
|
365
415
|
config = self.get_config()
|
366
416
|
for profile in self.configs.values():
|
@@ -386,8 +436,12 @@ class Config:
|
|
386
436
|
self.sync_shared_field(config.general, profile.general, "open_multimine_on_startup")
|
387
437
|
self.sync_shared_field(config.general, profile.general, "websocket_uri")
|
388
438
|
self.sync_shared_field(config.general, profile.general, "texthooker_port")
|
439
|
+
self.sync_shared_field(config.general, profile.general, "use_old_texthooker")
|
389
440
|
self.sync_shared_field(config.audio, profile.audio, "external_tool")
|
390
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")
|
391
445
|
self.sync_shared_field(config, profile, "advanced")
|
392
446
|
self.sync_shared_field(config, profile, "paths")
|
393
447
|
self.sync_shared_field(config, profile, "obs")
|
@@ -573,3 +627,15 @@ if 'gsm' in sys.argv[0]:
|
|
573
627
|
|
574
628
|
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
575
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():
|