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 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
  """
@@ -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", "8", "normal"))
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.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
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
- ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile).grid(row=self.current_row, column=2, pady=5)
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-03-25', 'gemini-2.5-flash-preview-04-17'])
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.save_settings()
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
+
@@ -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
  ]
@@ -17,82 +17,91 @@ current_line_after_regex = ''
17
17
  current_line_time = datetime.now()
18
18
 
19
19
  reconnecting = False
20
- websocket_connected = False
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
- # Initial clipboard content
27
- current_clipboard = pyperclip.paste()
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(1)
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
- skip_next_clipboard = True
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 and not skip_next_clipboard:
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 listen_websocket():
48
- global current_line, current_line_time, reconnecting, websocket_connected
49
- try_other = False
50
- while True:
51
- if not get_config().general.use_websocket:
52
- await asyncio.sleep(1)
53
- continue
54
- websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
55
- if try_other:
56
- websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
57
- try:
58
- async with websockets.connect(websocket_url, ping_interval=None) as websocket:
59
- logger.info("TextHooker Websocket Connected!")
60
- if reconnecting:
61
- logger.info(f"Texthooker WebSocket 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 = True
64
- try_other = True
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("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
- await asyncio.sleep(0.1)
88
- else:
89
- if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
90
- logger.error(f"Unexpected error in Texthooker WebSocket connection: {e}")
91
- websocket_connected = False
92
- if not reconnecting:
93
- logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
94
- reconnecting = True
95
- await asyncio.sleep(5)
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(listen_websocket())
128
+ asyncio.run(listen_websockets())
120
129
 
121
130
 
122
131
  async def start_text_monitor():