GameSentenceMiner 2.8.5__py3-none-any.whl → 2.8.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ import logging
2
+ import textwrap
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import List, Any, Optional
7
+
8
+ import google.generativeai as genai
9
+ from groq import Groq
10
+
11
+ from GameSentenceMiner.configuration import get_config, Ai, logger
12
+ from GameSentenceMiner.text_log import GameLine
13
+
14
+ # Suppress debug logs from httpcore
15
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
16
+ logging.getLogger("httpx").setLevel(logging.WARNING)
17
+ logging.getLogger("groq._base_client").setLevel(logging.WARNING)
18
+
19
+
20
+ TRANSLATION_PROMPT = textwrap.dedent(f"""Translate the following Japanese dialogue from this game into natural, context-aware English. Focus on preserving the tone, intent, and emotional nuance of the original text, paying close attention to the context provided by surrounding lines. The dialogue may include slang, idioms, implied meanings, or game-specific terminology that should be adapted naturally for English-speaking players. Ensure the translation feels immersive and aligns with the game's narrative style and character voices.
21
+ Translate only the specified line below, providing a single result. Do not include additional text, explanations, alternatives, or other lines unless explicitly requested. If there are alternatives, choose the best one. Allow expletives if more natural. Allow HTML tags for emphasis, italics, and other formatting as needed. Please also try to preserve existing HTML tags from the specified sentence if appropriate. Answer with nothing but the best translation, no alternatives or explanations.
22
+
23
+ Line to Translate:
24
+ """)
25
+
26
+ CONTEXT_PROMPT = textwrap.dedent(f"""Provide a very brief summary of the scene in English based on the provided Japanese dialogue and context. Focus on the characters' actions and the immediate situation being described.
27
+
28
+ Current Sentence:
29
+ """)
30
+
31
+ class AIType(Enum):
32
+ GEMINI = "Gemini"
33
+ GROQ = "Groq"
34
+
35
+ @dataclass
36
+ class AIConfig:
37
+ api_key: str
38
+ model: str
39
+ api_url: Optional[str]
40
+ type: 'AIType'
41
+
42
+ @dataclass
43
+ class GeminiAIConfig(AIConfig):
44
+ def __init__(self, api_key: str, model: str = "gemini-2.0-flash"):
45
+ super().__init__(api_key=api_key, model=model, api_url=None, type=AIType.GEMINI)
46
+
47
+ @dataclass
48
+ class GroqAiConfig(AIConfig):
49
+ def __init__(self, api_key: str, model: str = "meta-llama/llama-4-scout-17b-16e-instruct"):
50
+ super().__init__(api_key=api_key, model=model, api_url=None, type=AIType.GROQ)
51
+
52
+
53
+ class AIManager(ABC):
54
+ def __init__(self, ai_config: AIConfig, logger: Optional[logging.Logger] = None):
55
+ self.ai_config = ai_config
56
+ self.logger = logger
57
+
58
+ @abstractmethod
59
+ def process(self, lines: List[GameLine], sentence: str, current_line_index: int, game_title: str = "") -> str:
60
+ pass
61
+
62
+ @abstractmethod
63
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
64
+ start_index = max(0, current_line.index - 10)
65
+ end_index = min(len(lines), current_line.index + 11)
66
+
67
+ context_lines_text = []
68
+ for i in range(start_index, end_index):
69
+ if i < len(lines):
70
+ context_lines_text.append(lines[i].text)
71
+
72
+ dialogue_context = "\n".join(context_lines_text)
73
+
74
+ if get_config().ai.use_canned_translation_prompt:
75
+ prompt_to_use = TRANSLATION_PROMPT
76
+ elif get_config().ai.use_canned_context_prompt:
77
+ prompt_to_use = CONTEXT_PROMPT
78
+ else:
79
+ prompt_to_use = getattr(self.ai_config, 'custom_prompt', "")
80
+
81
+ full_prompt = textwrap.dedent(f"""
82
+ Dialogue Context:
83
+
84
+ {dialogue_context}
85
+
86
+ I am playing the game {game_title}. With that, and the above dialogue context in mind, answer the following prompt.
87
+
88
+ {prompt_to_use}
89
+
90
+ {sentence}
91
+ """)
92
+ return full_prompt
93
+
94
+
95
+ class GeminiAI(AIManager):
96
+ def __init__(self, model, api_key, logger: Optional[logging.Logger] = None):
97
+ super().__init__(GeminiAIConfig(model=model, api_key=api_key), logger)
98
+ try:
99
+ genai.configure(api_key=self.ai_config.api_key)
100
+ model_name = self.ai_config.model
101
+ self.model = genai.GenerativeModel(model_name)
102
+ self.logger.info(f"GeminiAIManager initialized with model: {model_name}")
103
+ except Exception as e:
104
+ self.logger.error(f"Failed to initialize Gemini API: {e}")
105
+ self.model = None
106
+
107
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
108
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title)
109
+ return prompt
110
+
111
+ def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
112
+ if self.model is None:
113
+ return "Processing failed: AI model not initialized."
114
+
115
+ if not lines or not current_line:
116
+ self.logger.warning(f"Invalid input for process: lines={len(lines)}, current_line={current_line.index}")
117
+ return "Invalid input."
118
+
119
+ try:
120
+ prompt = self._build_prompt(lines, sentence, current_line, game_title)
121
+ self.logger.debug(f"Generated prompt:\n{prompt}")
122
+ response = self.model.generate_content(prompt)
123
+ result = response.text.strip()
124
+ self.logger.debug(f"Received response:\n{result}")
125
+ return result
126
+ except Exception as e:
127
+ self.logger.error(f"Gemini processing failed: {e}")
128
+ return f"Processing failed: {e}"
129
+
130
+ class GroqAI(AIManager):
131
+ def __init__(self, model, api_key, logger: Optional[logging.Logger] = None):
132
+ super().__init__(GroqAiConfig(model=model, api_key=api_key), logger)
133
+ self.api_key = self.ai_config.api_key
134
+ self.model_name = self.ai_config.model
135
+ try:
136
+ self.client = Groq(api_key=self.api_key)
137
+ self.logger.info(f"GroqAIManager initialized with model: {self.model_name}")
138
+ except Exception as e:
139
+ self.logger.error(f"Failed to initialize Groq client: {e}")
140
+ self.client = None
141
+
142
+ def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
143
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title)
144
+ return prompt
145
+
146
+ def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
147
+ if self.client is None:
148
+ return "Processing failed: Groq client not initialized."
149
+
150
+ if not lines or not current_line:
151
+ self.logger.warning(f"Invalid input for process: lines={len(lines)}, current_line={current_line.index}")
152
+ return "Invalid input."
153
+
154
+ try:
155
+ prompt = self._build_prompt(lines, sentence, current_line, game_title)
156
+ self.logger.debug(f"Generated prompt:\n{prompt}")
157
+ completion = self.client.chat.completions.create(
158
+ model=self.model_name,
159
+ messages=[{"role": "user", "content": prompt}],
160
+ temperature=.5,
161
+ max_completion_tokens=1024,
162
+ top_p=1,
163
+ stream=False,
164
+ stop=None,
165
+ )
166
+ result = completion.choices[0].message.content.strip()
167
+ self.logger.debug(f"Received response:\n{result}")
168
+ return result
169
+ except Exception as e:
170
+ self.logger.error(f"Groq processing failed: {e}")
171
+ return f"Processing failed: {e}"
172
+
173
+ ai_manager: AIManager | None = None
174
+ current_ai_config: Ai | None = None
175
+
176
+ def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = ""):
177
+ global ai_manager, current_ai_config
178
+ if not ai_manager or get_config().ai != current_ai_config:
179
+ if get_config().ai.provider == AIType.GEMINI.value:
180
+ ai_manager = GeminiAI(model=get_config().ai.gemini_model, api_key=get_config().ai.gemini_api_key, logger=logger)
181
+ elif get_config().ai.provider == AIType.GROQ.value:
182
+ ai_manager = GroqAI(model=get_config().ai.groq_model, api_key=get_config().ai.groq_api_key, logger=logger)
183
+ current_ai_config = get_config().ai
184
+ return ai_manager.process(lines, sentence, current_line, game_title)
185
+
186
+ if __name__ == '__main__':
187
+ lines = [
188
+ GameLine(index=0, text="こんにちは、元気ですか?", id=None, time=None, prev=None, next=None),
189
+ GameLine(index=1, text="今日はいい天気ですね。",id=None, time=None, prev=None, next=None),
190
+ GameLine(index=2, text="ゲームを始めましょう!",id=None, time=None, prev=None, next=None),
191
+ ]
192
+ sentence = "ゲームを始めましょう!"
193
+ current_line = lines[2]
194
+ game_title = "Test Game"
195
+
196
+ # Set up logging
197
+ logging.basicConfig(level=logging.DEBUG)
198
+
199
+ # Test the function
200
+ result = get_ai_prompt_result(lines, sentence, current_line, game_title)
201
+ print("AI Prompt Result:", result)
GameSentenceMiner/anki.py CHANGED
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
8
8
  from requests import post
9
9
 
10
10
  from GameSentenceMiner import obs, util, notification, ffmpeg
11
- from GameSentenceMiner.ai.gemini import translate_with_context
11
+ from GameSentenceMiner.ai.ai_prompting import GeminiAI, get_ai_prompt_result
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.configuration import get_config
14
14
  from GameSentenceMiner.model import AnkiCard
@@ -67,8 +67,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
67
67
  sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
68
68
  sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
69
69
  get_config().anki.sentence_field)
70
- translation = translate_with_context(get_all_lines(), sentence_to_translate,
71
- game_line.index, get_current_game())
70
+ translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate,
71
+ game_line, get_current_game())
72
72
  logger.info(translation)
73
73
  note['fields'][get_config().ai.anki_field] = translation
74
74
 
@@ -286,6 +286,7 @@ def update_new_card():
286
286
  try:
287
287
  obs.save_replay_buffer()
288
288
  except Exception as e:
289
+ card_queue.pop(0)
289
290
  logger.error(f"Error saving replay buffer: {e}")
290
291
  return
291
292
 
@@ -206,9 +206,12 @@ class ConfigApp:
206
206
  ),
207
207
  ai=Ai(
208
208
  enabled=self.ai_enabled.get(),
209
- # provider=self.provider.get(),
209
+ provider=self.ai_provider.get(),
210
+ gemini_model=self.gemini_model.get(),
211
+ groq_model=self.groq_model.get(),
212
+ gemini_api_key=self.gemini_api_key.get(),
213
+ groq_api_key=self.groq_api_key.get(),
210
214
  anki_field=self.ai_anki_field.get(),
211
- api_key=self.ai_api_key.get(),
212
215
  use_canned_translation_prompt=self.use_canned_translation_prompt.get(),
213
216
  use_canned_context_prompt=self.use_canned_context_prompt.get(),
214
217
  custom_prompt=self.custom_prompt.get("1.0", tk.END)
@@ -1004,12 +1007,18 @@ class ConfigApp:
1004
1007
  ttk.Checkbutton(ai_frame, variable=self.ai_enabled).grid(row=self.current_row, column=1, sticky='W')
1005
1008
  self.add_label_and_increment_row(ai_frame, "Enable or disable AI integration.", row=self.current_row, column=2)
1006
1009
 
1007
- ttk.Label(ai_frame, text="Anki Field:").grid(row=self.current_row, column=0, sticky='W')
1008
- self.ai_anki_field = ttk.Entry(ai_frame)
1009
- self.ai_anki_field.insert(0, self.settings.ai.anki_field)
1010
- self.ai_anki_field.grid(row=self.current_row, column=1)
1011
- self.add_label_and_increment_row(ai_frame, "Field in Anki for AI-generated content.", row=self.current_row,
1012
- column=2)
1010
+ ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
1011
+ self.ai_provider = ttk.Combobox(ai_frame, values=['Gemini', 'Groq'])
1012
+ self.ai_provider.set(self.settings.ai.provider)
1013
+ self.ai_provider.grid(row=self.current_row, column=1)
1014
+ self.add_label_and_increment_row(ai_frame, "Select the AI provider.", row=self.current_row, column=2)
1015
+
1016
+ ttk.Label(ai_frame, text="Gemini AI Model:").grid(row=self.current_row, column=0, sticky='W')
1017
+ 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'])
1018
+ self.gemini_model.set(self.settings.ai.gemini_model)
1019
+ self.gemini_model.grid(row=self.current_row, column=1)
1020
+ self.add_label_and_increment_row(ai_frame, "Select the AI model to use.", row=self.current_row, column=2)
1021
+
1013
1022
 
1014
1023
  # ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
1015
1024
  # self.provider = ttk.Combobox(ai_frame,
@@ -1018,13 +1027,34 @@ class ConfigApp:
1018
1027
  # self.provider.grid(row=self.current_row, column=1)
1019
1028
  # self.add_label_and_increment_row(ai_frame, "Select the AI provider. Currently only Gemini is supported.", row=self.current_row, column=2)
1020
1029
 
1021
- ttk.Label(ai_frame, text="API Key:").grid(row=self.current_row, column=0, sticky='W')
1022
- self.ai_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1023
- self.ai_api_key.insert(0, self.settings.ai.api_key)
1024
- self.ai_api_key.grid(row=self.current_row, column=1)
1030
+ ttk.Label(ai_frame, text="Gemini API Key:").grid(row=self.current_row, column=0, sticky='W')
1031
+ self.gemini_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1032
+ self.gemini_api_key.insert(0, self.settings.ai.gemini_api_key)
1033
+ self.gemini_api_key.grid(row=self.current_row, column=1)
1025
1034
  self.add_label_and_increment_row(ai_frame, "API key for the selected AI provider (Gemini only currently).", row=self.current_row,
1026
1035
  column=2)
1027
1036
 
1037
+ ttk.Label(ai_frame, text="Groq AI Model:").grid(row=self.current_row, column=0, sticky='W')
1038
+ self.groq_model = ttk.Combobox(ai_frame, values=['meta-llama/llama-4-maverick-17b-128e-instruct', 'meta-llama/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b-instant'])
1039
+ self.groq_model.set(self.settings.ai.groq_model)
1040
+ self.groq_model.grid(row=self.current_row, column=1)
1041
+ self.add_label_and_increment_row(ai_frame, "Select the Groq AI model to use.", row=self.current_row, column=2)
1042
+
1043
+
1044
+ ttk.Label(ai_frame, text="Groq API Key:").grid(row=self.current_row, column=0, sticky='W')
1045
+ self.groq_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
1046
+ self.groq_api_key.insert(0, self.settings.ai.groq_api_key)
1047
+ self.groq_api_key.grid(row=self.current_row, column=1)
1048
+ self.add_label_and_increment_row(ai_frame, "API key for Groq AI provider.", row=self.current_row,
1049
+ column=2)
1050
+
1051
+ ttk.Label(ai_frame, text="Anki Field:").grid(row=self.current_row, column=0, sticky='W')
1052
+ self.ai_anki_field = ttk.Entry(ai_frame)
1053
+ self.ai_anki_field.insert(0, self.settings.ai.anki_field)
1054
+ self.ai_anki_field.grid(row=self.current_row, column=1)
1055
+ self.add_label_and_increment_row(ai_frame, "Field in Anki for AI-generated content.", row=self.current_row,
1056
+ column=2)
1057
+
1028
1058
  ttk.Label(ai_frame, text="Use Canned Translation Prompt:").grid(row=self.current_row, column=0, sticky='W')
1029
1059
  self.use_canned_translation_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_translation_prompt)
1030
1060
  ttk.Checkbutton(ai_frame, variable=self.use_canned_translation_prompt).grid(row=self.current_row, column=1,
@@ -28,7 +28,8 @@ WHISPER_SMALL = 'small'
28
28
  WHISPER_MEDIUM = 'medium'
29
29
  WHSIPER_LARGE = 'large'
30
30
 
31
- AI_GEMINI = 'gemini'
31
+ AI_GEMINI = 'Gemini'
32
+ AI_GROQ = 'Groq'
32
33
 
33
34
  INFO = 'INFO'
34
35
  DEBUG = 'DEBUG'
@@ -134,7 +135,7 @@ class OBS:
134
135
  open_obs: bool = True
135
136
  close_obs: bool = True
136
137
  host: str = "127.0.0.1"
137
- port: int = 4455
138
+ port: int = 7274
138
139
  password: str = "your_password"
139
140
  get_game_from_scene: bool = True
140
141
  minimum_replay_size: int = 0
@@ -187,11 +188,18 @@ class Ai:
187
188
  enabled: bool = False
188
189
  anki_field: str = ''
189
190
  provider: str = AI_GEMINI
190
- api_key: str = ''
191
+ gemini_model: str = 'gemini-2.0-flash'
192
+ groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
193
+ api_key: str = '' # Deprecated
194
+ gemini_api_key: str = ''
195
+ groq_api_key: str = ''
191
196
  use_canned_translation_prompt: bool = True
192
197
  use_canned_context_prompt: bool = False
193
198
  custom_prompt: str = ''
194
199
 
200
+ def __post_init__(self):
201
+ self.gemini_api_key = self.api_key
202
+
195
203
 
196
204
  @dataclass_json
197
205
  @dataclass
@@ -306,6 +314,21 @@ class Config:
306
314
  instance = cls(configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
307
315
  return instance
308
316
 
317
+ @classmethod
318
+ def load(cls):
319
+ config_path = get_config_path()
320
+ if os.path.exists(config_path):
321
+ with open(config_path, 'r') as file:
322
+ data = json.load(file)
323
+ return cls.from_dict(data)
324
+ else:
325
+ return cls.new()
326
+
327
+ def save(self):
328
+ with open(get_config_path(), 'w') as file:
329
+ json.dump(self.to_dict(), file, indent=4)
330
+ return self
331
+
309
332
  def get_config(self) -> ProfileConfig:
310
333
  return self.configs[self.current_profile]
311
334
 
@@ -355,7 +378,8 @@ class Config:
355
378
  self.sync_shared_field(config.ai, profile.ai, "anki_field")
356
379
  self.sync_shared_field(config.ai, profile.ai, "provider")
357
380
  self.sync_shared_field(config.ai, profile.ai, "api_key")
358
-
381
+ self.sync_shared_field(config.ai, profile.ai, "gemini_api_key")
382
+ self.sync_shared_field(config.ai, profile.ai, "groq_api_key")
359
383
 
360
384
  return self
361
385
 
@@ -513,22 +537,22 @@ sys.stderr.reconfigure(encoding='utf-8')
513
537
 
514
538
  logger = logging.getLogger("GameSentenceMiner")
515
539
  logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
540
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
516
541
 
517
542
  # Create console handler with level INFO
518
543
  console_handler = logging.StreamHandler(sys.stdout)
519
544
  console_handler.setLevel(logging.INFO)
520
545
 
521
- # Create rotating file handler with level DEBUG
522
- file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
523
- file_handler.setLevel(logging.DEBUG)
524
-
525
- # Create a formatter
526
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
527
-
528
- # Add formatter to handlers
529
546
  console_handler.setFormatter(formatter)
530
- file_handler.setFormatter(formatter)
531
547
 
532
- # Add handlers to the logger
533
548
  logger.addHandler(console_handler)
534
- logger.addHandler(file_handler)
549
+
550
+ # Create rotating file handler with level DEBUG
551
+ if 'gsm' in sys.argv[0]:
552
+ file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
553
+ file_handler.setLevel(logging.DEBUG)
554
+ file_handler.setFormatter(formatter)
555
+ logger.addHandler(file_handler)
556
+
557
+ DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
558
+
@@ -1,7 +1,5 @@
1
1
  import asyncio
2
2
  import re
3
- import threading
4
- import time
5
3
 
6
4
  import pyperclip
7
5
  import websockets
@@ -24,31 +22,23 @@ websocket_connected = False
24
22
  # def remove_old_events(self, cutoff_time: datetime):
25
23
  # self.values = [line for line in self.values if line.time >= cutoff_time]
26
24
 
27
- class ClipboardMonitor(threading.Thread):
25
+ async def monitor_clipboard():
26
+ # Initial clipboard content
27
+ current_clipboard = pyperclip.paste()
28
28
 
29
- def __init__(self):
30
- threading.Thread.__init__(self)
31
- self.daemon = True
32
-
33
- def run(self):
34
- global current_line_time, current_line
35
-
36
- # Initial clipboard content
37
- current_line = pyperclip.paste()
38
-
39
- skip_next_clipboard = False
40
- while True:
41
- if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
42
- time.sleep(1)
43
- skip_next_clipboard = True
44
- continue
45
- current_clipboard = pyperclip.paste()
29
+ skip_next_clipboard = False
30
+ while True:
31
+ if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
32
+ await asyncio.sleep(1)
33
+ skip_next_clipboard = True
34
+ continue
35
+ current_clipboard = pyperclip.paste()
46
36
 
47
- if current_clipboard != current_line and not skip_next_clipboard:
48
- handle_new_text_event(current_clipboard)
49
- skip_next_clipboard = False
37
+ if current_clipboard and current_clipboard != current_line and not skip_next_clipboard:
38
+ await handle_new_text_event(current_clipboard)
39
+ skip_next_clipboard = False
50
40
 
51
- time.sleep(0.05)
41
+ await asyncio.sleep(0.05)
52
42
 
53
43
 
54
44
  async def listen_websocket():
@@ -69,6 +59,8 @@ async def listen_websocket():
69
59
  line_time = None
70
60
  while True:
71
61
  message = await websocket.recv()
62
+ if not message:
63
+ continue
72
64
  logger.debug(message)
73
65
  try:
74
66
  data = json.loads(message)
@@ -76,26 +68,25 @@ async def listen_websocket():
76
68
  current_clipboard = data["sentence"]
77
69
  if "time" in data:
78
70
  line_time = datetime.fromisoformat(data["time"])
79
- print(line_time)
80
71
  except json.JSONDecodeError or TypeError:
81
72
  current_clipboard = message
82
73
  if current_clipboard != current_line:
83
- handle_new_text_event(current_clipboard, line_time if line_time else None)
84
- except (websockets.ConnectionClosed, ConnectionError, InvalidStatus) as e:
74
+ await handle_new_text_event(current_clipboard, line_time if line_time else None)
75
+ except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
85
76
  if isinstance(e, InvalidStatus):
86
77
  e: InvalidStatus
87
78
  if e.response.status_code == 404:
88
79
  logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
89
80
  try_other = True
90
-
91
- logger.error(f"Texthooker WebSocket connection failed. Please check if the Texthooker is running and the WebSocket URI is correct.")
81
+ if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
82
+ logger.error(f"Unexpected error in Texthooker WebSocket connection: {e}")
92
83
  websocket_connected = False
93
84
  if not reconnecting:
94
85
  logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
95
86
  reconnecting = True
96
87
  await asyncio.sleep(5)
97
88
 
98
- def handle_new_text_event(current_clipboard, line_time=None):
89
+ async def handle_new_text_event(current_clipboard, line_time=None):
99
90
  global current_line, current_line_time, current_line_after_regex
100
91
  current_line = current_clipboard
101
92
  if get_config().general.texthook_replacement_regex:
@@ -106,7 +97,7 @@ def handle_new_text_event(current_clipboard, line_time=None):
106
97
  logger.info(f"Line Received: {current_line_after_regex}")
107
98
  current_line_time = line_time if line_time else datetime.now()
108
99
  add_line(current_line_after_regex, line_time)
109
- add_event_to_texthooker(get_text_log()[-1])
100
+ await add_event_to_texthooker(get_text_log()[-1])
110
101
 
111
102
  def reset_line_hotkey_pressed():
112
103
  global current_line_time
@@ -119,13 +110,14 @@ def run_websocket_listener():
119
110
  asyncio.run(listen_websocket())
120
111
 
121
112
 
122
- def start_text_monitor():
113
+ async def start_text_monitor():
123
114
  if get_config().general.use_websocket:
124
- threading.Thread(target=run_websocket_listener, daemon=True).start()
115
+ util.run_new_thread(run_websocket_listener)
125
116
  if get_config().general.use_clipboard:
126
117
  if get_config().general.use_websocket:
127
118
  if get_config().general.use_both_clipboard_and_websocket:
128
119
  logger.info("Listening for Text on both WebSocket and Clipboard.")
129
120
  else:
130
121
  logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
131
- ClipboardMonitor().start()
122
+ await monitor_clipboard()
123
+ await asyncio.sleep(1)