GameSentenceMiner 2.9.3__py3-none-any.whl → 2.9.5__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.
Files changed (49) hide show
  1. GameSentenceMiner/ai/ai_prompting.py +3 -3
  2. GameSentenceMiner/anki.py +17 -11
  3. GameSentenceMiner/assets/icon.png +0 -0
  4. GameSentenceMiner/assets/icon128.png +0 -0
  5. GameSentenceMiner/assets/icon256.png +0 -0
  6. GameSentenceMiner/assets/icon32.png +0 -0
  7. GameSentenceMiner/assets/icon512.png +0 -0
  8. GameSentenceMiner/assets/icon64.png +0 -0
  9. GameSentenceMiner/assets/pickaxe.png +0 -0
  10. GameSentenceMiner/config_gui.py +22 -7
  11. GameSentenceMiner/gametext.py +5 -5
  12. GameSentenceMiner/gsm.py +26 -67
  13. GameSentenceMiner/obs.py +7 -9
  14. GameSentenceMiner/ocr/owocr_area_selector.py +1 -1
  15. GameSentenceMiner/ocr/owocr_helper.py +30 -13
  16. GameSentenceMiner/owocr/owocr/ocr.py +0 -2
  17. GameSentenceMiner/owocr/owocr/run.py +1 -1
  18. GameSentenceMiner/{communication → util/communication}/__init__.py +1 -1
  19. GameSentenceMiner/{communication → util/communication}/send.py +1 -1
  20. GameSentenceMiner/{communication → util/communication}/websocket.py +2 -2
  21. GameSentenceMiner/{downloader → util/downloader}/download_tools.py +3 -3
  22. GameSentenceMiner/vad.py +344 -0
  23. GameSentenceMiner/web/texthooking_page.py +78 -55
  24. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/METADATA +2 -3
  25. gamesentenceminer-2.9.5.dist-info/RECORD +57 -0
  26. GameSentenceMiner/configuration.py +0 -647
  27. GameSentenceMiner/electron_config.py +0 -315
  28. GameSentenceMiner/ffmpeg.py +0 -441
  29. GameSentenceMiner/model.py +0 -177
  30. GameSentenceMiner/notification.py +0 -105
  31. GameSentenceMiner/package.py +0 -39
  32. GameSentenceMiner/ss_selector.py +0 -121
  33. GameSentenceMiner/text_log.py +0 -186
  34. GameSentenceMiner/util.py +0 -262
  35. GameSentenceMiner/vad/groq_trim.py +0 -82
  36. GameSentenceMiner/vad/result.py +0 -21
  37. GameSentenceMiner/vad/silero_trim.py +0 -52
  38. GameSentenceMiner/vad/vad_utils.py +0 -13
  39. GameSentenceMiner/vad/vosk_helper.py +0 -158
  40. GameSentenceMiner/vad/whisper_helper.py +0 -105
  41. gamesentenceminer-2.9.3.dist-info/RECORD +0 -64
  42. /GameSentenceMiner/{downloader → assets}/__init__.py +0 -0
  43. /GameSentenceMiner/{downloader → util/downloader}/Untitled_json.py +0 -0
  44. /GameSentenceMiner/{vad → util/downloader}/__init__.py +0 -0
  45. /GameSentenceMiner/{downloader → util/downloader}/oneocr_dl.py +0 -0
  46. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/WHEEL +0 -0
  47. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/entry_points.txt +0 -0
  48. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/licenses/LICENSE +0 -0
  49. {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,344 @@
1
+ import subprocess
2
+ import tempfile
3
+ import warnings
4
+ from abc import abstractmethod, ABC
5
+
6
+ from GameSentenceMiner.util import configuration, ffmpeg
7
+ from GameSentenceMiner.util.configuration import *
8
+ from GameSentenceMiner.util.ffmpeg import get_ffprobe_path
9
+
10
+
11
+ def get_audio_length(path):
12
+ result = subprocess.run(
13
+ [get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path],
14
+ stdout=subprocess.PIPE,
15
+ stderr=subprocess.PIPE,
16
+ text=True
17
+ )
18
+ return float(result.stdout.strip())
19
+
20
+ class VADResult:
21
+ def __init__(self, success: bool, start: float, end: float, model: str, output_audio: str = None):
22
+ self.success = success
23
+ self.start = start
24
+ self.end = end
25
+ self.model = model
26
+ self.output_audio = None
27
+
28
+ def __repr__(self):
29
+ return f"VADResult(success={self.success}, start={self.start}, end={self.end}, model={self.model}, output_audio={self.output_audio})"
30
+
31
+ def trim_successful_string(self):
32
+ if self.success:
33
+ if get_config().vad.trim_beginning:
34
+ return f"Trimmed audio from {self.start:.2f} to {self.end:.2f} seconds using {self.model}."
35
+ else:
36
+ return f"Trimmed end of audio to {self.end:.2f} seconds using {self.model}."
37
+ else:
38
+ return f"Failed to trim audio using {self.model}."
39
+
40
+ class VADSystem:
41
+ def __init__(self):
42
+ self.silero = None
43
+ self.whisper = None
44
+ self.vosk = None
45
+ self.groq = None
46
+
47
+ def init(self):
48
+ if get_config().vad.is_whisper():
49
+ if not self.whisper:
50
+ self.whisper = WhisperVADProcessor()
51
+ if get_config().vad.is_silero():
52
+ if not self.silero:
53
+ self.silero = SileroVADProcessor()
54
+ if get_config().vad.is_vosk():
55
+ if not self.vosk:
56
+ self.vosk = VoskVADProcessor()
57
+ if get_config().vad.is_groq():
58
+ if not self.groq:
59
+ self.groq = GroqVADProcessor()
60
+
61
+ def trim_audio_with_vad(self, input_audio, output_audio, game_line):
62
+ if get_config().vad.do_vad_postprocessing:
63
+ result = self._do_vad_processing(get_config().vad.selected_vad_model, input_audio, output_audio, game_line)
64
+ if not result.success and get_config().vad.backup_vad_model != configuration.OFF:
65
+ logger.info("No voice activity detected, using backup VAD model.")
66
+ result = self._do_vad_processing(get_config().vad.backup_vad_model, input_audio, output_audio, game_line)
67
+ if not result.success:
68
+ if get_config().vad.add_audio_on_no_results:
69
+ logger.info("No voice activity detected, using full audio.")
70
+ result.output_audio = input_audio
71
+ else:
72
+ logger.info("No voice activity detected.")
73
+ return result
74
+ else:
75
+ logger.info(result.trim_successful_string())
76
+ return result
77
+
78
+
79
+ def _do_vad_processing(self, model, input_audio, output_audio, game_line):
80
+ match model:
81
+ case configuration.OFF:
82
+ return VADResult(False, 0, 0, "OFF")
83
+ case configuration.GROQ:
84
+ if not self.groq:
85
+ self.groq = GroqVADProcessor()
86
+ return self.groq.process_audio(input_audio, output_audio, game_line)
87
+ case configuration.SILERO:
88
+ if not self.silero:
89
+ self.silero = SileroVADProcessor()
90
+ return self.silero.process_audio(input_audio, output_audio, game_line)
91
+ case configuration.VOSK:
92
+ if not self.vosk:
93
+ self.vosk = VoskVADProcessor()
94
+ return self.vosk.process_audio(input_audio, output_audio, game_line)
95
+ case configuration.WHISPER:
96
+ if not self.whisper:
97
+ self.whisper = WhisperVADProcessor()
98
+ return self.whisper.process_audio(input_audio, output_audio, game_line)
99
+
100
+ # Base class for VAD systems
101
+ class VADProcessor(ABC):
102
+ def __init__(self):
103
+ self.vad_model = None
104
+ self.vad_system_name = None
105
+
106
+ @abstractmethod
107
+ def _detect_voice_activity(self, input_audio):
108
+ pass
109
+
110
+ def process_audio(self, input_audio, output_audio, game_line):
111
+ voice_activity = self._detect_voice_activity(input_audio)
112
+
113
+ if not voice_activity:
114
+ logger.info("No voice activity detected in the audio.")
115
+ return VADResult(False, 0, 0, self.vad_system_name)
116
+
117
+ start_time = voice_activity[0]['start'] if voice_activity else 0
118
+ end_time = voice_activity[-1]['end'] if voice_activity else 0
119
+
120
+ # Attempt to fix the end time if the last segment is too short
121
+ if game_line and game_line.next and len(voice_activity) > 1:
122
+ audio_length = get_audio_length(input_audio)
123
+ if 0 > audio_length - voice_activity[-1]['start'] + get_config().audio.beginning_offset:
124
+ end_time = voice_activity[-2]['end']
125
+
126
+ ffmpeg.trim_audio(input_audio, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, output_audio)
127
+ return VADResult(True, start_time + get_config().vad.beginning_offset, end_time + get_config().audio.end_offset, self.vad_system_name, output_audio)
128
+
129
+ class SileroVADProcessor(VADProcessor):
130
+ def __init__(self):
131
+ super().__init__()
132
+ from silero_vad import load_silero_vad
133
+ self.vad_model = load_silero_vad()
134
+ self.vad_system_name = SILERO
135
+
136
+ def _detect_voice_activity(self, input_audio):
137
+ from silero_vad import read_audio, get_speech_timestamps
138
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
139
+ ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
140
+ wav = read_audio(temp_wav)
141
+ speech_timestamps = get_speech_timestamps(wav, self.vad_model, return_seconds=True)
142
+ logger.debug(speech_timestamps)
143
+ return speech_timestamps
144
+
145
+ class WhisperVADProcessor(VADProcessor):
146
+ def __init__(self):
147
+ super().__init__()
148
+ self.vad_model = self.load_whisper_model()
149
+ self.vad_system_name = WHISPER
150
+
151
+ def load_whisper_model(self):
152
+ import stable_whisper as whisper
153
+ if not self.vad_model:
154
+ with warnings.catch_warnings(action="ignore"):
155
+ self.vad_model = whisper.load_model(get_config().vad.whisper_model)
156
+ logger.info(f"Whisper model '{get_config().vad.whisper_model}' loaded.")
157
+ return self.vad_model
158
+
159
+ def _detect_voice_activity(self, input_audio):
160
+ from stable_whisper import WhisperResult
161
+ # Convert the audio to 16kHz mono WAV
162
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
163
+ ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
164
+
165
+ logger.info('transcribing audio...')
166
+
167
+ # Transcribe the audio using Whisper
168
+ with warnings.catch_warnings(action="ignore"):
169
+ result: WhisperResult = self.vad_model.transcribe(temp_wav, vad=True, language=get_config().vad.language,
170
+ temperature=0.0)
171
+ voice_activity = []
172
+
173
+ logger.debug(result.to_dict())
174
+
175
+ # Process the segments to extract tokens, timestamps, and confidence
176
+ for segment in result.segments:
177
+ logger.debug(segment.to_dict())
178
+ for word in segment.words:
179
+ logger.debug(word.to_dict())
180
+ confidence = word.probability
181
+ if confidence > .1:
182
+ logger.debug(word)
183
+ voice_activity.append({
184
+ 'text': word.word,
185
+ 'start': word.start,
186
+ 'end': word.end,
187
+ 'confidence': word.probability
188
+ })
189
+
190
+ # Analyze the detected words to decide whether to use the audio
191
+ should_use = False
192
+ unique_words = set(word['text'] for word in voice_activity)
193
+ if len(unique_words) > 1 or not all(item in ['えー', 'ん'] for item in unique_words):
194
+ should_use = True
195
+
196
+ if not should_use:
197
+ return None
198
+
199
+ # Return the detected voice activity and the total duration
200
+ return voice_activity
201
+
202
+ # Add a new class for Vosk-based VAD
203
+ class VoskVADProcessor(VADProcessor):
204
+ def __init__(self):
205
+ super().__init__()
206
+ self.vad_model = self._load_vosk_model()
207
+ self.vad_system_name = VOSK
208
+
209
+ def _load_vosk_model(self):
210
+ if not self.vad_model:
211
+ import vosk
212
+ vosk_model_path = self._download_and_cache_vosk_model()
213
+ self.vad_model = vosk.Model(vosk_model_path)
214
+ logger.info(f"Vosk model loaded from {vosk_model_path}")
215
+ return self.vad_model
216
+
217
+ def _download_and_cache_vosk_model(self, model_dir="vosk_model_cache"):
218
+ # Ensure the cache directory exists
219
+ import requests
220
+ import zipfile
221
+ import tarfile
222
+ if not os.path.exists(os.path.join(get_app_directory(), model_dir)):
223
+ os.makedirs(os.path.join(get_app_directory(), model_dir))
224
+
225
+ # Extract the model name from the URL
226
+ model_filename = get_config().vad.vosk_url.split("/")[-1]
227
+ model_path = os.path.join(get_app_directory(), model_dir, model_filename)
228
+
229
+ # If the model is already downloaded, skip the download
230
+ if not os.path.exists(model_path):
231
+ logger.info(
232
+ f"Downloading the Vosk model from {get_config().vad.vosk_url}... This will take a while if using large model, ~1G")
233
+ response = requests.get(get_config().vad.vosk_url, stream=True)
234
+ with open(model_path, "wb") as file:
235
+ for chunk in response.iter_content(chunk_size=8192):
236
+ if chunk:
237
+ file.write(chunk)
238
+ logger.info("Download complete.")
239
+
240
+ # Extract the model if it's a zip or tar file
241
+ model_extract_path = os.path.join(get_app_directory(), model_dir, "vosk_model")
242
+ if not os.path.exists(model_extract_path):
243
+ logger.info("Extracting the Vosk model...")
244
+ if model_filename.endswith(".zip"):
245
+ with zipfile.ZipFile(model_path, "r") as zip_ref:
246
+ zip_ref.extractall(model_extract_path)
247
+ elif model_filename.endswith(".tar.gz"):
248
+ with tarfile.open(model_path, "r:gz") as tar_ref:
249
+ tar_ref.extractall(model_extract_path)
250
+ else:
251
+ logger.info("Unknown archive format. Model extraction skipped.")
252
+ logger.info(f"Model extracted to {model_extract_path}.")
253
+ else:
254
+ logger.info(f"Model already extracted at {model_extract_path}.")
255
+
256
+ # Return the path to the actual model folder inside the extraction directory
257
+ extracted_folders = os.listdir(model_extract_path)
258
+ if extracted_folders:
259
+ actual_model_folder = os.path.join(model_extract_path,
260
+ extracted_folders[0]) # Assuming the first folder is the model
261
+ return actual_model_folder
262
+ else:
263
+ return model_extract_path # In case there's no subfolder, return the extraction path directly
264
+
265
+ def _detect_voice_activity(self, input_audio):
266
+ import soundfile as sf
267
+ import vosk
268
+ import numpy as np
269
+ # Convert the audio to 16kHz mono WAV
270
+ temp_wav = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(), suffix='.wav').name
271
+ ffmpeg.convert_audio_to_wav(input_audio, temp_wav)
272
+
273
+ # Initialize recognizer
274
+ with sf.SoundFile(temp_wav) as audio_file:
275
+ recognizer = vosk.KaldiRecognizer(self.vad_model, audio_file.samplerate)
276
+ voice_activity = []
277
+
278
+ recognizer.SetWords(True)
279
+
280
+ # Process audio in chunks
281
+ while True:
282
+ data = audio_file.buffer_read(4000, dtype='int16')
283
+ if len(data) == 0:
284
+ break
285
+
286
+ # Convert buffer to bytes using NumPy
287
+ data_bytes = np.frombuffer(data, dtype='int16').tobytes()
288
+
289
+ if recognizer.AcceptWaveform(data_bytes):
290
+ pass
291
+
292
+ final_result = json.loads(recognizer.FinalResult())
293
+ if 'result' in final_result:
294
+ for word in final_result['result']:
295
+ if word['conf'] >= 0.90:
296
+ voice_activity.append({
297
+ 'text': word['word'],
298
+ 'start': word['start'],
299
+ 'end': word['end']
300
+ })
301
+
302
+ # Return the detected voice activity
303
+ return voice_activity
304
+
305
+ class GroqVADProcessor(VADProcessor):
306
+ def __init__(self):
307
+ super().__init__()
308
+ from groq import Groq
309
+ self.client = Groq(api_key=get_config().ai.groq_api_key)
310
+ self.vad_model = self.load_groq_model()
311
+ self.vad_system_name = GROQ
312
+
313
+ def load_groq_model(self):
314
+ if not self.vad_model:
315
+ from groq import Groq
316
+ self.vad_model = Groq()
317
+ logger.info("Groq model loaded.")
318
+ return self.vad_model
319
+
320
+ def _detect_voice_activity(self, input_audio):
321
+ try:
322
+ with open(input_audio, "rb") as file:
323
+ transcription = self.client.audio.transcriptions.create(
324
+ file=(os.path.basename(input_audio), file.read()),
325
+ model="whisper-large-v3-turbo",
326
+ response_format="verbose_json",
327
+ language=get_config().vad.language,
328
+ temperature=0.0,
329
+ timestamp_granularities=["segment"],
330
+ prompt=f"Start detecting speech from the first spoken word. If there is music or background noise, ignore it completely. Be very careful to not hallucinate on silence. If the transcription is anything but language:{get_config().vad.language}, ignore it completely. If the end of the audio seems like the start of a new sentence, ignore it completely.",
331
+ )
332
+
333
+ logger.debug(transcription)
334
+ speech_segments = []
335
+ if hasattr(transcription, 'segments'):
336
+ speech_segments = transcription.segments
337
+ elif hasattr(transcription, 'words'):
338
+ speech_segments = transcription.words
339
+ return speech_segments
340
+ except Exception as e:
341
+ logger.error(f"Error detecting voice with Groq: {e}")
342
+ return [], 0.0
343
+
344
+ vad_processor = VADSystem()
@@ -10,12 +10,12 @@ from dataclasses import dataclass
10
10
  import flask
11
11
  import websockets
12
12
 
13
- from GameSentenceMiner.text_log import GameLine, get_line_by_id, initial_time, get_all_lines
13
+ from GameSentenceMiner.util.gsm_utils import TEXT_REPLACEMENTS_FILE
14
+ from GameSentenceMiner.util.text_log import GameLine, get_line_by_id, initial_time
14
15
  from flask import request, jsonify, send_from_directory
15
16
  import webbrowser
16
17
  from GameSentenceMiner import obs
17
- from GameSentenceMiner.configuration import logger, get_config, DB_PATH, gsm_state
18
- from GameSentenceMiner.util import TEXT_REPLACEMENTS_FILE
18
+ from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state
19
19
 
20
20
  port = get_config().general.texthooker_port
21
21
  url = f"http://localhost:{port}"
@@ -247,7 +247,7 @@ def clear_history():
247
247
 
248
248
  async def add_event_to_texthooker(line: GameLine):
249
249
  new_event = event_manager.add_gameline(line)
250
- await broadcast_message({
250
+ await websocket_server_thread.send_text({
251
251
  'event': 'text_received',
252
252
  'sentence': line.text,
253
253
  'data': new_event.to_serializable()
@@ -294,37 +294,6 @@ def play_audio():
294
294
  return jsonify({}), 200
295
295
 
296
296
 
297
- connected_clients = set()
298
-
299
- async def websocket_handler(websocket):
300
- logger.debug(f"Client connected: {websocket.remote_address}")
301
- connected_clients.add(websocket)
302
- try:
303
- async for message in websocket:
304
- try:
305
- data = json.loads(message)
306
- if 'type' in data and data['type'] == 'get_events':
307
- initial_events = [{'id': 1, 'text': 'Initial event from WebSocket'}, {'id': 2, 'text': 'Another initial event'}]
308
- await websocket.send(json.dumps({'event': 'initial_events', 'payload': initial_events}))
309
- elif 'update_checkbox' in data:
310
- print(f"Received checkbox update: {data}")
311
- # Handle checkbox update logic
312
- pass
313
- await websocket.send(json.dumps({'response': f'Server received: {message}'}))
314
- except json.JSONDecodeError:
315
- await websocket.send(json.dumps({'error': 'Invalid JSON format'}))
316
- except websockets.exceptions.ConnectionClosedError:
317
- print(f"Client disconnected abruptly: {websocket.remote_address}")
318
- except websockets.exceptions.ConnectionClosedOK:
319
- print(f"Client disconnected gracefully: {websocket.remote_address}")
320
- finally:
321
- connected_clients.discard(websocket)
322
-
323
- async def broadcast_message(message):
324
- if connected_clients:
325
- for client in connected_clients:
326
- await client.send(json.dumps(message))
327
-
328
297
  # async def main():
329
298
  # async with websockets.serve(websocket_handler, "localhost", 8765): # Choose a port for WebSocket
330
299
  # print("WebSocket server started on ws://localhost:8765/ws (adjust as needed)")
@@ -360,7 +329,7 @@ def are_lines_selected():
360
329
 
361
330
  def reset_checked_lines():
362
331
  async def send_reset_message():
363
- await broadcast_message({
332
+ await websocket_server_thread.send_text({
364
333
  'event': 'reset_checkboxes',
365
334
  })
366
335
  event_manager.reset_checked_lines()
@@ -381,25 +350,76 @@ def start_web_server():
381
350
 
382
351
  app.run(port=port, debug=False) # debug=True provides helpful error messages during development
383
352
 
384
- import signal
385
353
 
386
- async def run_websocket_server(host="0.0.0.0"):
387
- global websocket_port
388
- websocket = None
389
- try:
390
- websocket_port = get_config().advanced.texthooker_communication_websocket_port
391
- websocket = await websockets.serve(websocket_handler, host, websocket_port)
392
- logger.debug(f"WebSocket server started at ws://{host}:{websocket_port}/")
393
- await asyncio.Future() # Keep the server running
394
- except asyncio.CancelledError:
395
- logger.info("WebSocket server shutting down...")
396
- except OSError as e:
397
- logger.error(f"TextHooker WebSocket server failed to start on port {websocket_port}: {e}")
398
- logger.info("You may need to try a different port in GSM's advanced config, and then update that in the Texthooker's settings.")
399
- finally:
400
- if websocket:
401
- websocket.close()
402
- await asyncio.sleep(1) # Wait before retrying
354
+ websocket_server_thread = None
355
+ websocket_queue = queue.Queue()
356
+ paused = False
357
+
358
+
359
+ class WebsocketServerThread(threading.Thread):
360
+ def __init__(self, read):
361
+ super().__init__(daemon=True)
362
+ self._loop = None
363
+ self.read = read
364
+ self.clients = set()
365
+ self._event = threading.Event()
366
+
367
+ @property
368
+ def loop(self):
369
+ self._event.wait()
370
+ return self._loop
371
+
372
+ async def send_text_coroutine(self, message):
373
+ for client in self.clients:
374
+ await client.send(message)
375
+
376
+ async def server_handler(self, websocket):
377
+ self.clients.add(websocket)
378
+ try:
379
+ async for message in websocket:
380
+ if self.read and not paused:
381
+ websocket_queue.put(message)
382
+ try:
383
+ await websocket.send('True')
384
+ except websockets.exceptions.ConnectionClosedOK:
385
+ pass
386
+ else:
387
+ try:
388
+ await websocket.send('False')
389
+ except websockets.exceptions.ConnectionClosedOK:
390
+ pass
391
+ except websockets.exceptions.ConnectionClosedError:
392
+ pass
393
+ finally:
394
+ self.clients.remove(websocket)
395
+
396
+ async def send_text(self, text):
397
+ if text:
398
+ return asyncio.run_coroutine_threadsafe(
399
+ self.send_text_coroutine(json.dumps(text)), self.loop)
400
+
401
+ def stop_server(self):
402
+ self.loop.call_soon_threadsafe(self._stop_event.set)
403
+
404
+ def run(self):
405
+ async def main():
406
+ self._loop = asyncio.get_running_loop()
407
+ self._stop_event = stop_event = asyncio.Event()
408
+ self._event.set()
409
+ while True:
410
+ try:
411
+ self.server = start_server = websockets.serve(self.server_handler,
412
+ "0.0.0.0",
413
+ get_config().advanced.texthooker_communication_websocket_port,
414
+ max_size=1000000000)
415
+ async with start_server:
416
+ await stop_event.wait()
417
+ return
418
+ except Exception as e:
419
+ logger.warning(f"WebSocket server encountered an error: {e}. Retrying...")
420
+ await asyncio.sleep(1)
421
+
422
+ asyncio.run(main())
403
423
 
404
424
  def handle_exit_signal(loop):
405
425
  logger.info("Received exit signal. Shutting down...")
@@ -407,13 +427,16 @@ def handle_exit_signal(loop):
407
427
  task.cancel()
408
428
 
409
429
  async def texthooker_page_coro():
430
+ global websocket_server_thread
410
431
  # Run the WebSocket server in the asyncio event loop
411
432
  flask_thread = threading.Thread(target=start_web_server)
412
433
  flask_thread.daemon = True
413
434
  flask_thread.start()
414
435
 
436
+ websocket_server_thread = WebsocketServerThread(read=True)
437
+ websocket_server_thread.start()
438
+
415
439
  # Keep the main asyncio event loop running (for the WebSocket server)
416
- await run_websocket_server()
417
440
 
418
441
  def run_text_hooker_page():
419
442
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.9.3
3
+ Version: 2.9.5
4
4
  Summary: A tool for mining sentences from games.
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -21,7 +21,6 @@ Requires-Dist: soundfile~=0.12.1
21
21
  Requires-Dist: toml~=0.10.2
22
22
  Requires-Dist: psutil~=6.0.0
23
23
  Requires-Dist: rapidfuzz~=3.9.7
24
- Requires-Dist: obs-websocket-py~=1.0
25
24
  Requires-Dist: plyer~=2.1.0
26
25
  Requires-Dist: keyboard~=0.13.5
27
26
  Requires-Dist: websockets~=15.0.1
@@ -37,7 +36,7 @@ Requires-Dist: google-generativeai
37
36
  Requires-Dist: pygetwindow; sys_platform == "win32"
38
37
  Requires-Dist: flask
39
38
  Requires-Dist: groq
40
- Requires-Dist: obsws-python
39
+ Requires-Dist: obsws-python~=1.7.2
41
40
  Requires-Dist: Flask-SocketIO
42
41
  Dynamic: license-file
43
42
 
@@ -0,0 +1,57 @@
1
+ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ GameSentenceMiner/anki.py,sha256=CuzVqzuFtZnbMbU2Zk-sxNGwSgyCpv5RLL7lOOX0Meg,14972
3
+ GameSentenceMiner/config_gui.py,sha256=r-ASCXVNS4Io6Ej3svwC8aJEWc9Rc7u-pzfsAwD4ru8,82079
4
+ GameSentenceMiner/gametext.py,sha256=mM-gw1d7c2EEvMUznaAevTQFLswNZavCuxMXhA9pV4g,6251
5
+ GameSentenceMiner/gsm.py,sha256=t5rW4njIzWKi9zANU6W2JZJUbxWBOsQak-irHr7a85Q,27741
6
+ GameSentenceMiner/obs.py,sha256=jdAKQFnXlviMupRUKBuK68Q1u8yEZNKBgFnvIq1hhnc,14810
7
+ GameSentenceMiner/vad.py,sha256=Gk_VthD7mDp3-wM_S6bEv8ykGmqzCDbbcRiaEBzAE_o,14835
8
+ GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ GameSentenceMiner/ai/ai_prompting.py,sha256=tPDiTHlrfZul0hlvEFgZS4V_6oaHkVb-4v79Sd4gtlM,10018
10
+ GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ GameSentenceMiner/assets/icon.png,sha256=9GRL8uXUAgkUSlvbm9Pv9o2poFVRGdW6s2ub_DeUD9M,937624
12
+ GameSentenceMiner/assets/icon128.png,sha256=l90j7biwdz5ahwOd5wZ-406ryEV9Pan93dquJQ3e1CI,18395
13
+ GameSentenceMiner/assets/icon256.png,sha256=JEW46wOrG1KR-907rvFaEdNbPtj5gu0HJmG7qUnIHxQ,51874
14
+ GameSentenceMiner/assets/icon32.png,sha256=Kww0hU_qke9_22wBuO_Nq0Dv2SfnOLwMhCyGgbgXdg8,6089
15
+ GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB_7W4sqs,192487
16
+ GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
17
+ GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
18
+ GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=fEQ2o2NXksGRHpueO8c4TfAp75GEdAtAr1ngTFOsdpg,2257
20
+ GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
21
+ GameSentenceMiner/ocr/owocr_area_selector.py,sha256=71trzwz9Isyy-kN9mLS8vIX-giC8Lkin4slLXaxudac,47162
22
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=FQXk5PSCS9gWtcgoIFsPxjVELUwA4Dg1hEX83902K0Q,18114
23
+ GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
24
+ GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
25
+ GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
26
+ GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
27
+ GameSentenceMiner/owocr/owocr/ocr.py,sha256=y8RHHaJw8M4BG4CbbtIw0DrV8KP9RjbJNJxjM5v91oU,42236
28
+ GameSentenceMiner/owocr/owocr/run.py,sha256=jFN7gYYriHgfqORJiBTz8mPkQsDJ6ZugA0_ATWUxk-U,54750
29
+ GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
30
+ GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
31
+ GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
32
+ GameSentenceMiner/util/communication/websocket.py,sha256=gPgxA2R2U6QZJjPqbUgODC87gtacPhmuC8lCprIkSmA,3287
33
+ GameSentenceMiner/util/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
34
+ GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ GameSentenceMiner/util/downloader/download_tools.py,sha256=mvnOjDHFlV1AbjHaNI7mdnC5_CH5k3N4n1ezqzzbzGA,8139
36
+ GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
37
+ GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ GameSentenceMiner/web/texthooking_page.py,sha256=RR70Vgde3wNHarQHbB-LBbEP-z95vRD5rtlW0GgdjmQ,15037
39
+ GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
41
+ GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
42
+ GameSentenceMiner/web/static/favicon.ico,sha256=7d25r_FBqRSNsAoEHpSzNoT7zyVt2DJRLNDNq_HYoX8,15086
43
+ GameSentenceMiner/web/static/favicon.svg,sha256=x305AP6WlXGtrXIZlaQspdLmwteoFYUoe5FyJ9MYlJ8,11517
44
+ GameSentenceMiner/web/static/site.webmanifest,sha256=kaeNT-FjFt-T7JGzOhXH7YSqsrDeiplZ2kDxCN_CFU4,436
45
+ GameSentenceMiner/web/static/style.css,sha256=bPZK0NVMuyRl5NNDuT7ZTzVLKlvSsdmeVHmAW4y5FM0,7001
46
+ GameSentenceMiner/web/static/web-app-manifest-192x192.png,sha256=EfSNnBmsSaLfESbkGfYwbKzcjKOdzuWo18ABADfN974,51117
47
+ GameSentenceMiner/web/static/web-app-manifest-512x512.png,sha256=wyqgCWCrLEUxSRXmaA3iJEESd-vM-ZmlTtZFBY4V8Pk,230819
48
+ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
50
+ GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
51
+ GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
52
+ gamesentenceminer-2.9.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
53
+ gamesentenceminer-2.9.5.dist-info/METADATA,sha256=_SsLMuzhBNnvzZALCJLBX0besj87Wxr6TOR3xpV3h1Q,7250
54
+ gamesentenceminer-2.9.5.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
55
+ gamesentenceminer-2.9.5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
56
+ gamesentenceminer-2.9.5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
57
+ gamesentenceminer-2.9.5.dist-info/RECORD,,