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.
- GameSentenceMiner/ai/ai_prompting.py +3 -3
- GameSentenceMiner/anki.py +17 -11
- GameSentenceMiner/assets/icon.png +0 -0
- GameSentenceMiner/assets/icon128.png +0 -0
- GameSentenceMiner/assets/icon256.png +0 -0
- GameSentenceMiner/assets/icon32.png +0 -0
- GameSentenceMiner/assets/icon512.png +0 -0
- GameSentenceMiner/assets/icon64.png +0 -0
- GameSentenceMiner/assets/pickaxe.png +0 -0
- GameSentenceMiner/config_gui.py +22 -7
- GameSentenceMiner/gametext.py +5 -5
- GameSentenceMiner/gsm.py +26 -67
- GameSentenceMiner/obs.py +7 -9
- GameSentenceMiner/ocr/owocr_area_selector.py +1 -1
- GameSentenceMiner/ocr/owocr_helper.py +30 -13
- GameSentenceMiner/owocr/owocr/ocr.py +0 -2
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/{communication → util/communication}/__init__.py +1 -1
- GameSentenceMiner/{communication → util/communication}/send.py +1 -1
- GameSentenceMiner/{communication → util/communication}/websocket.py +2 -2
- GameSentenceMiner/{downloader → util/downloader}/download_tools.py +3 -3
- GameSentenceMiner/vad.py +344 -0
- GameSentenceMiner/web/texthooking_page.py +78 -55
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/METADATA +2 -3
- gamesentenceminer-2.9.5.dist-info/RECORD +57 -0
- GameSentenceMiner/configuration.py +0 -647
- GameSentenceMiner/electron_config.py +0 -315
- GameSentenceMiner/ffmpeg.py +0 -441
- GameSentenceMiner/model.py +0 -177
- GameSentenceMiner/notification.py +0 -105
- GameSentenceMiner/package.py +0 -39
- GameSentenceMiner/ss_selector.py +0 -121
- GameSentenceMiner/text_log.py +0 -186
- GameSentenceMiner/util.py +0 -262
- GameSentenceMiner/vad/groq_trim.py +0 -82
- GameSentenceMiner/vad/result.py +0 -21
- GameSentenceMiner/vad/silero_trim.py +0 -52
- GameSentenceMiner/vad/vad_utils.py +0 -13
- GameSentenceMiner/vad/vosk_helper.py +0 -158
- GameSentenceMiner/vad/whisper_helper.py +0 -105
- gamesentenceminer-2.9.3.dist-info/RECORD +0 -64
- /GameSentenceMiner/{downloader → assets}/__init__.py +0 -0
- /GameSentenceMiner/{downloader → util/downloader}/Untitled_json.py +0 -0
- /GameSentenceMiner/{vad → util/downloader}/__init__.py +0 -0
- /GameSentenceMiner/{downloader → util/downloader}/oneocr_dl.py +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/top_level.txt +0 -0
GameSentenceMiner/vad.py
ADDED
@@ -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.
|
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
|
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
|
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
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
+
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,,
|