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 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(),
@@ -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
- 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()
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-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'])
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.save_settings()
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
+
@@ -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():