GameSentenceMiner 2.8.41__py3-none-any.whl → 2.8.43__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.
@@ -119,7 +119,10 @@ class GeminiAI(AIManager):
119
119
  try:
120
120
  prompt = self._build_prompt(lines, sentence, current_line, game_title)
121
121
  self.logger.debug(f"Generated prompt:\n{prompt}")
122
- response = self.model.generate_content(prompt)
122
+ response = self.model.generate_content(
123
+ prompt,
124
+ temperature=0.5,
125
+ )
123
126
  result = response.text.strip()
124
127
  self.logger.debug(f"Received response:\n{result}")
125
128
  return result
GameSentenceMiner/anki.py CHANGED
@@ -14,7 +14,7 @@ from GameSentenceMiner.configuration import get_config
14
14
  from GameSentenceMiner.model import AnkiCard
15
15
  from GameSentenceMiner.text_log import get_all_lines, get_text_event, get_mined_line
16
16
  from GameSentenceMiner.obs import get_current_game
17
- from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue
17
+ from GameSentenceMiner.util import remove_html_and_cloze_tags, combine_dialogue, wait_for_stable_file
18
18
  from GameSentenceMiner.web import texthooking_page
19
19
 
20
20
  audio_in_anki = None
@@ -41,11 +41,13 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
41
41
  audio_in_anki = store_media_file(audio_path)
42
42
  if update_picture:
43
43
  screenshot = ffmpeg.get_screenshot(video_path, ss_time)
44
+ wait_for_stable_file(screenshot)
44
45
  screenshot_in_anki = store_media_file(screenshot)
45
46
  if get_config().paths.remove_screenshot:
46
47
  os.remove(screenshot)
47
48
  if get_config().anki.previous_image_field:
48
49
  prev_screenshot = ffmpeg.get_screenshot_for_line(video_path, selected_lines[0].prev if selected_lines else game_line.prev)
50
+ wait_for_stable_file(prev_screenshot)
49
51
  prev_screenshot_in_anki = store_media_file(prev_screenshot)
50
52
  if get_config().paths.remove_screenshot:
51
53
  os.remove(prev_screenshot)
@@ -178,7 +180,11 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
178
180
 
179
181
 
180
182
  def store_media_file(path):
181
- return invoke('storeMediaFile', filename=path, data=convert_to_base64(path))
183
+ try:
184
+ return invoke('storeMediaFile', filename=path, data=convert_to_base64(path))
185
+ except Exception as e:
186
+ logger.error(f"Error storing media file, check anki card for blank media fields: {e}")
187
+ return "None"
182
188
 
183
189
 
184
190
  def convert_to_base64(file_path):
@@ -1,3 +1,4 @@
1
+ import shutil
1
2
  import tempfile
2
3
 
3
4
  from GameSentenceMiner import obs, util, configuration
@@ -15,13 +16,65 @@ def get_ffprobe_path():
15
16
  ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
16
17
 
17
18
 
18
- def get_screenshot(video_file, screenshot_timing):
19
+ def call_frame_extractor(video_path, timestamp):
20
+ """
21
+ Calls the video frame extractor script and captures the output.
22
+
23
+ Args:
24
+ video_path (str): Path to the video file.
25
+ timestamp (str): Timestamp string (HH:MM:SS).
26
+
27
+ Returns:
28
+ str: The path of the selected image, or None on error.
29
+ """
30
+ try:
31
+ # Get the directory of the current script
32
+ current_dir = os.path.dirname(os.path.abspath(__file__))
33
+ # Construct the path to the frame extractor script
34
+ script_path = os.path.join(current_dir, "ss_selector.py") # Replace with the actual script name if different
35
+
36
+ logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp)]))
37
+
38
+ # Run the script using subprocess.run()
39
+ result = subprocess.run(
40
+ [sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
41
+ capture_output=True,
42
+ text=True, # Get output as text
43
+ check=False # Raise an exception for non-zero exit codes
44
+ )
45
+ if result.returncode != 0:
46
+ logger.error(f"Script failed with return code: {result.returncode}")
47
+ return None
48
+ logger.info(result)
49
+ # Print the standard output
50
+ logger.info(f"Frame extractor script output: {result.stdout.strip()}")
51
+ return result.stdout.strip() # Return the output
52
+
53
+ except subprocess.CalledProcessError as e:
54
+ logger.error(f"Error calling script: {e}")
55
+ logger.error(f"Script output (stderr): {e.stderr.strip()}")
56
+ return None
57
+ except FileNotFoundError:
58
+ logger.error(f"Error: Script not found at {script_path}. Make sure the script name is correct.")
59
+ return None
60
+ except Exception as e:
61
+ logger.error(f"An unexpected error occurred: {e}")
62
+ return None
63
+
64
+ def get_screenshot(video_file, screenshot_timing, try_selector=False):
19
65
  screenshot_timing = screenshot_timing if screenshot_timing else 1
66
+ if try_selector:
67
+ filepath = call_frame_extractor(video_path=video_file, timestamp=screenshot_timing)
68
+ output = process_image(filepath)
69
+ if output:
70
+ return output
71
+ else:
72
+ logger.error("Frame extractor script failed to run or returned no output, defaulting")
20
73
  output_image = make_unique_file_name(os.path.join(
21
74
  get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
22
75
  # FFmpeg command to extract the last frame of the video
23
76
  ffmpeg_command = ffmpeg_base_command_list + [
24
- "-ss", f"{screenshot_timing}", # Seek to 1 second after the beginning
77
+ "-ss", f"{screenshot_timing}",
25
78
  "-i", f"{video_file}",
26
79
  "-vframes", "1" # Extract only one frame
27
80
  ]
@@ -39,15 +92,33 @@ def get_screenshot(video_file, screenshot_timing):
39
92
 
40
93
  logger.debug(f"FFMPEG SS Command: {ffmpeg_command}")
41
94
 
42
- # Run the command
43
- subprocess.run(ffmpeg_command)
95
+ try:
96
+ for i in range(3):
97
+ logger.debug(" ".join(ffmpeg_command))
98
+ result = subprocess.run(ffmpeg_command)
99
+ if result.returncode != 0 and i < 2:
100
+ raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
101
+ else:
102
+ break
103
+ except Exception as e:
104
+ logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
105
+ output_image = make_unique_file_name(os.path.join(
106
+ get_config().paths.screenshot_destination,
107
+ f"{obs.get_current_game(sanitize=True)}.png"))
108
+ ffmpeg_command = ffmpeg_base_command_list + [
109
+ "-ss", f"{screenshot_timing}", # Default to 1 second
110
+ "-i", video_file,
111
+ "-vframes", "1",
112
+ output_image
113
+ ]
114
+ subprocess.run(ffmpeg_command)
44
115
 
45
116
  logger.debug(f"Screenshot saved to: {output_image}")
46
117
 
47
118
  return output_image
48
119
 
49
- def get_screenshot_for_line(video_file, game_line):
50
- return get_screenshot(video_file, get_screenshot_time(video_file, game_line))
120
+ def get_screenshot_for_line(video_file, game_line, try_selector=False):
121
+ return get_screenshot(video_file, get_screenshot_time(video_file, game_line), try_selector)
51
122
 
52
123
 
53
124
  def get_screenshot_time(video_path, game_line, default_beginning=False, vad_result=None, doing_multi_line=False, previous_line=False):
@@ -127,7 +198,18 @@ def process_image(image_file):
127
198
  logger.debug(ffmpeg_command)
128
199
  logger.debug(" ".join(ffmpeg_command))
129
200
  # Run the command
130
- subprocess.run(ffmpeg_command)
201
+ try:
202
+ for i in range(3):
203
+ logger.debug(" ".join(ffmpeg_command))
204
+ result = subprocess.run(ffmpeg_command)
205
+ if result.returncode != 0 and i < 2:
206
+ raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
207
+ else:
208
+ break
209
+ except Exception as e:
210
+ logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
211
+ output_image = make_unique_file_name(os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.png"))
212
+ shutil.move(image_file, output_image)
131
213
 
132
214
  logger.info(f"Processed image saved to: {output_image}")
133
215
 
GameSentenceMiner/gsm.py CHANGED
@@ -82,7 +82,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
82
82
  if texthooking_page.event_manager.line_for_screenshot:
83
83
  line: GameLine = texthooking_page.event_manager.line_for_screenshot
84
84
  texthooking_page.event_manager.line_for_screenshot = None
85
- screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
85
+ screenshot = ffmpeg.get_screenshot_for_line(video_path, line, True)
86
86
  os.startfile(screenshot)
87
87
  os.remove(video_path)
88
88
  return
@@ -92,10 +92,11 @@ class VideoToAudioHandler(FileSystemEventHandler):
92
92
  os.remove(video_path)
93
93
  return
94
94
  try:
95
- last_note = None
96
- anki_card_creation_time = None
97
95
  if anki.card_queue and len(anki.card_queue) > 0:
98
96
  last_note, anki_card_creation_time = anki.card_queue.pop(0)
97
+ else:
98
+ logger.info("Replay buffer initiated externally. Skipping processing.")
99
+ return
99
100
  with util.lock:
100
101
  util.set_last_mined_line(anki.get_sentence(last_note))
101
102
  if os.path.exists(video_path) and os.access(video_path, os.R_OK):
GameSentenceMiner/obs.py CHANGED
@@ -27,8 +27,8 @@ class OBSConnectionManager(threading.Thread):
27
27
 
28
28
  def run(self):
29
29
  while self.running:
30
- time.sleep(5)
31
- if not client.get_version():
30
+ time.sleep(1)
31
+ if not client or not client.get_version():
32
32
  logger.info("OBS WebSocket not connected. Attempting to reconnect...")
33
33
  connect_to_obs()
34
34
 
@@ -0,0 +1,309 @@
1
+ import os.path
2
+ import subprocess
3
+ import threading
4
+ import time
5
+ import psutil
6
+
7
+ from obswebsocket import obsws, requests, events
8
+ from obswebsocket.exceptions import ConnectionFailure
9
+
10
+ from GameSentenceMiner import util, configuration
11
+ from GameSentenceMiner.configuration import *
12
+ from GameSentenceMiner.model import *
13
+
14
+ client: obsws = None
15
+ obs_process_pid = None
16
+ # logging.getLogger('obswebsocket').setLevel(logging.CRITICAL)
17
+ OBS_PID_FILE = os.path.join(configuration.get_app_directory(), 'obs-studio', 'obs_pid.txt')
18
+
19
+ # REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
20
+
21
+
22
+ def get_obs_path():
23
+ return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
24
+
25
+ def is_process_running(pid):
26
+ try:
27
+ process = psutil.Process(pid)
28
+ return 'obs' in process.exe()
29
+ except (psutil.NoSuchProcess, psutil.AccessDenied, OSError):
30
+ if os.path.exists(OBS_PID_FILE):
31
+ os.remove(OBS_PID_FILE)
32
+ return False
33
+
34
+ def start_obs():
35
+ global obs_process_pid
36
+ if os.path.exists(OBS_PID_FILE):
37
+ with open(OBS_PID_FILE, "r") as f:
38
+ try:
39
+ obs_process_pid = int(f.read().strip())
40
+ if is_process_running(obs_process_pid):
41
+ print(f"OBS is already running with PID: {obs_process_pid}")
42
+ connect_to_obs()
43
+ return obs_process_pid
44
+ except ValueError:
45
+ print("Invalid PID found in file. Launching new OBS instance.")
46
+ except OSError:
47
+ print("No process found with the stored PID. Launching new OBS instance.")
48
+
49
+ obs_path = get_obs_path()
50
+ if not os.path.exists(obs_path):
51
+ print(f"OBS not found at {obs_path}. Please install OBS.")
52
+ return None
53
+ try:
54
+ obs_process = subprocess.Popen([obs_path, '--disable-shutdown-check', '--portable', '--startreplaybuffer', ], cwd=os.path.dirname(obs_path))
55
+ obs_process_pid = obs_process.pid
56
+ connect_to_obs()
57
+ with open(OBS_PID_FILE, "w") as f:
58
+ f.write(str(obs_process_pid))
59
+ print(f"OBS launched with PID: {obs_process_pid}")
60
+ return obs_process_pid
61
+ except Exception as e:
62
+ print(f"Error launching OBS: {e}")
63
+ return None
64
+
65
+ def check_obs_folder_is_correct():
66
+ obs_record_directory = get_record_directory()
67
+ if obs_record_directory and os.path.normpath(obs_record_directory) != os.path.normpath(
68
+ get_config().paths.folder_to_watch):
69
+ logger.info("OBS Path Setting wrong, OBS Recording folder in GSM Config")
70
+ get_config().paths.folder_to_watch = os.path.normpath(obs_record_directory)
71
+ get_master_config().sync_shared_fields()
72
+ save_full_config(get_master_config())
73
+
74
+
75
+ def get_obs_websocket_config_values():
76
+ config_path = os.path.join(get_app_directory(), 'obs-studio', 'config', 'obs-studio', 'plugin_config', 'obs-websocket', 'config.json')
77
+
78
+ # Check if config file exists
79
+ if not os.path.isfile(config_path):
80
+ raise FileNotFoundError(f"OBS WebSocket config not found at {config_path}")
81
+
82
+ # Read the JSON configuration
83
+ with open(config_path, 'r') as file:
84
+ config = json.load(file)
85
+
86
+ # Extract values
87
+ server_enabled = config.get("server_enabled", False)
88
+ server_port = config.get("server_port", 7274) # Default to 4455 if not set
89
+ server_password = config.get("server_password", None)
90
+
91
+ if not server_enabled:
92
+ logger.info("OBS WebSocket server is not enabled. Enabling it now... Restart OBS for changes to take effect.")
93
+ config["server_enabled"] = True
94
+
95
+ with open(config_path, 'w') as file:
96
+ json.dump(config, file, indent=4)
97
+
98
+ if get_config().obs.password == 'your_password':
99
+ logger.info("OBS WebSocket password is not set. Setting it now...")
100
+ full_config = get_master_config()
101
+ full_config.get_config().obs.port = server_port
102
+ full_config.get_config().obs.password = server_password
103
+ full_config.sync_shared_fields()
104
+ full_config.save()
105
+ reload_config()
106
+
107
+
108
+ connected = False
109
+
110
+ def on_connect(obs):
111
+ global connected
112
+ logger.info("Reconnected to OBS WebSocket.")
113
+ start_replay_buffer()
114
+ connected = True
115
+
116
+
117
+ def on_disconnect(obs):
118
+ global connected
119
+ logger.error("OBS Connection Lost!")
120
+ connected = False
121
+
122
+
123
+ def connect_to_obs(retry_count=0):
124
+ global client
125
+ if not get_config().obs.enabled or client:
126
+ return
127
+
128
+ if util.is_windows():
129
+ get_obs_websocket_config_values()
130
+
131
+ try:
132
+ client = obsws(
133
+ host=get_config().obs.host,
134
+ port=get_config().obs.port,
135
+ password=get_config().obs.password,
136
+ authreconnect=1,
137
+ on_connect=on_connect,
138
+ on_disconnect=on_disconnect
139
+ )
140
+ client.connect()
141
+ update_current_game()
142
+ except ConnectionFailure as e:
143
+ if retry_count % 5 == 0:
144
+ logger.error(f"Failed to connect to OBS WebSocket: {e}. Retrying...")
145
+ time.sleep(1)
146
+ connect_to_obs(retry_count=retry_count + 1)
147
+
148
+
149
+ # Disconnect from OBS WebSocket
150
+ def disconnect_from_obs():
151
+ global client
152
+ if client:
153
+ client.disconnect()
154
+ client = None
155
+ logger.info("Disconnected from OBS WebSocket.")
156
+
157
+ def do_obs_call(request, from_dict=None, retry=3):
158
+ connect_to_obs()
159
+ for _ in range(retry + 1):
160
+ try:
161
+ response = client.call(request)
162
+ if response and response.status:
163
+ return from_dict(response.datain) if from_dict else response.datain
164
+ time.sleep(0.3)
165
+ except Exception as e:
166
+ logger.error(f"Error calling OBS: {e}")
167
+ if "socket is already closed" in str(e) or "object has no attribute" in str(e):
168
+ time.sleep(0.3)
169
+ else:
170
+ return None
171
+ return None
172
+
173
+ def toggle_replay_buffer():
174
+ try:
175
+ do_obs_call(requests.ToggleReplayBuffer())
176
+ logger.info("Replay buffer Toggled.")
177
+ except Exception as e:
178
+ logger.error(f"Error toggling buffer: {e}")
179
+
180
+
181
+ # Start replay buffer
182
+ def start_replay_buffer(retry=5):
183
+ try:
184
+ if not get_replay_buffer_status()['outputActive']:
185
+ do_obs_call(requests.StartReplayBuffer(), retry=0)
186
+ except Exception as e:
187
+ if "socket is already closed" in str(e):
188
+ if retry > 0:
189
+ time.sleep(1)
190
+ start_replay_buffer(retry - 1)
191
+ else:
192
+ logger.error(f"Error starting replay buffer: {e}")
193
+
194
+ def get_replay_buffer_status():
195
+ try:
196
+ return do_obs_call(requests.GetReplayBufferStatus())
197
+ except Exception as e:
198
+ logger.error(f"Error getting replay buffer status: {e}")
199
+
200
+
201
+ # Stop replay buffer
202
+ def stop_replay_buffer():
203
+ try:
204
+ client.call(requests.StopReplayBuffer())
205
+ logger.error("Replay buffer stopped.")
206
+ except Exception as e:
207
+ logger.error(f"Error stopping replay buffer: {e}")
208
+
209
+ # Save the current replay buffer
210
+ def save_replay_buffer():
211
+ replay_buffer_started = do_obs_call(requests.GetReplayBufferStatus())['outputActive']
212
+ if replay_buffer_started:
213
+ client.call(requests.SaveReplayBuffer())
214
+ logger.info("Replay buffer saved. If your log stops bere, make sure your obs output path matches \"Path To Watch\" in GSM settings.")
215
+ else:
216
+ logger.error("Replay Buffer is not active, could not save Replay Buffer!")
217
+
218
+
219
+ def get_current_scene():
220
+ try:
221
+ return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict, retry=0).sceneName
222
+ except Exception as e:
223
+ logger.debug(f"Couldn't get scene: {e}")
224
+ return ''
225
+
226
+
227
+ def get_source_from_scene(scene_name):
228
+ try:
229
+ return do_obs_call(requests.GetSceneItemList(sceneName=scene_name), SceneItemsResponse.from_dict).sceneItems[0]
230
+ except Exception as e:
231
+ logger.error(f"Error getting source from scene: {e}")
232
+ return ''
233
+
234
+ def get_record_directory():
235
+ try:
236
+ return do_obs_call(requests.GetRecordDirectory(), RecordDirectory.from_dict).recordDirectory
237
+ except Exception as e:
238
+ logger.error(f"Error getting recording folder: {e}")
239
+ return ''
240
+
241
+ def get_obs_scenes():
242
+ try:
243
+ response: SceneListResponse = do_obs_call(requests.GetSceneList(), SceneListResponse.from_dict, retry=0)
244
+ return response.scenes
245
+ except Exception as e:
246
+ logger.error(f"Error getting scenes: {e}")
247
+ return None
248
+
249
+ def register_scene_change_callback(callback):
250
+ global client
251
+ if not client:
252
+ logger.error("OBS client is not connected.")
253
+ return
254
+
255
+ def on_scene_change(data):
256
+ logger.info("Scene changed: " + str(data))
257
+ scene_name = data.getSceneName()
258
+ if scene_name:
259
+ callback(scene_name)
260
+
261
+ client.register(on_scene_change, events.CurrentProgramSceneChanged)
262
+ logger.info("Scene change callback registered.")
263
+
264
+
265
+ def get_screenshot(compression=-1):
266
+ try:
267
+ screenshot = util.make_unique_file_name(os.path.abspath(
268
+ configuration.get_temporary_directory()) + '/screenshot.png')
269
+ update_current_game()
270
+ current_source = get_source_from_scene(get_current_game())
271
+ current_source_name = current_source.sourceName
272
+ if not current_source_name:
273
+ logger.error("No active scene found.")
274
+ return
275
+ start = time.time()
276
+ logger.debug(f"Current source name: {current_source_name}")
277
+ response = client.call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot, imageCompressionQuality=compression))
278
+ logger.debug(f"Screenshot response: {response}")
279
+ logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
280
+ return screenshot
281
+ except Exception as e:
282
+ logger.error(f"Error getting screenshot: {e}")
283
+
284
+ def get_screenshot_base64():
285
+ try:
286
+ update_current_game()
287
+ current_source = get_source_from_scene(get_current_game())
288
+ current_source_name = current_source.sourceName
289
+ if not current_source_name:
290
+ logger.error("No active scene found.")
291
+ return
292
+ response = do_obs_call(requests.GetSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageCompressionQuality=0))
293
+ with open('screenshot_response.txt', 'wb') as f:
294
+ f.write(str(response).encode())
295
+ return response['imageData']
296
+ except Exception as e:
297
+ logger.error(f"Error getting screenshot: {e}")
298
+
299
+ def update_current_game():
300
+ configuration.current_game = get_current_scene()
301
+
302
+
303
+ def get_current_game(sanitize=False):
304
+ if not configuration.current_game:
305
+ update_current_game()
306
+
307
+ if sanitize:
308
+ return util.sanitize_filename(configuration.current_game)
309
+ return configuration.current_game
@@ -205,18 +205,18 @@ class WebsocketServerThread(threading.Thread):
205
205
 
206
206
  all_cords = None
207
207
  rectangles = None
208
+ last_ocr2_result = ""
208
209
 
209
- def do_second_ocr(ocr1_text, rectangle_index, time, img):
210
- global twopassocr, ocr2, last_ocr1_results, last_ocr2_results
210
+ def do_second_ocr(ocr1_text, time, img, filtering):
211
+ global twopassocr, ocr2, last_ocr2_result
211
212
  try:
212
213
  orig_text, text = run.process_and_write_results(img, None, None, None, None,
213
214
  engine=ocr2)
214
- previous_ocr2_text = last_ocr2_results[rectangle_index]
215
- if fuzz.ratio(previous_ocr2_text, text) >= 80:
215
+ if fuzz.ratio(last_ocr2_result, text) >= 80:
216
216
  logger.info("Seems like the same text from previous ocr2 result, not sending")
217
217
  return
218
218
  save_result_image(img)
219
- last_ocr2_results[rectangle_index] = text
219
+ last_ocr2_result = text
220
220
  send_result(text, time)
221
221
  except json.JSONDecodeError:
222
222
  print("Invalid JSON received.")
@@ -243,91 +243,74 @@ def send_result(text, time):
243
243
  websocket_server_thread.send_text(text, time)
244
244
 
245
245
 
246
- last_oneocr_results_to_check = {} # Store last OCR result for each rectangle
247
- last_oneocr_times = {} # Store last OCR time for each rectangle
248
- text_stable_start_times = {} # Store the start time when text becomes stable for each rectangle
249
- previous_imgs = {}
250
- orig_text_results = {} # Store original text results for each rectangle
246
+ previous_text = "" # Store last OCR result
247
+ last_oneocr_time = None # Store last OCR time
248
+ text_stable_start_time = None # Store the start time when text becomes stable
249
+ previous_img = None
250
+ orig_text_result = "" # Store original text result
251
251
  TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
252
252
 
253
- def text_callback(text, orig_text, rectangle_index, time, img=None, came_from_ss=False):
254
- global twopassocr, ocr2, last_oneocr_results_to_check, last_oneocr_times, text_stable_start_times, orig_text_results
253
+ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering=None):
254
+ global twopassocr, ocr2, previous_text, last_oneocr_time, text_stable_start_time, orig_text_result, previous_img
255
255
  orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
256
- # logger.debug(orig_text_string)
257
256
  if came_from_ss:
258
257
  save_result_image(img)
259
258
  send_result(text, time)
260
259
  return
261
260
 
262
- current_time = time if time else datetime.now()
261
+ line_start_time = time if time else datetime.now()
263
262
 
264
- previous_text = last_oneocr_results_to_check.pop(rectangle_index, "").strip()
265
- previous_orig_text = orig_text_results.get(rectangle_index, "").strip()
266
-
267
- # print(previous_orig_text)
268
- # if orig_text:
269
- # print(orig_text_string)
270
263
  if not twopassocr:
271
- if previous_orig_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 80:
264
+ if previous_text and fuzz.ratio(orig_text_string, previous_text) >= 80:
272
265
  logger.info("Seems like Text we already sent, not doing anything.")
273
266
  return
274
267
  save_result_image(img)
275
268
  send_result(text, time)
276
- orig_text_results[rectangle_index] = orig_text_string
277
- last_ocr1_results[rectangle_index] = previous_text
269
+ orig_text_result = orig_text_string
270
+ previous_text = previous_text
271
+ previous_img = None
272
+ text_stable_start_time = None
273
+ last_oneocr_time = None
278
274
  return
279
275
  if not text:
280
276
  if previous_text:
281
- if rectangle_index in text_stable_start_times:
282
- stable_time = text_stable_start_times.pop(rectangle_index)
283
- previous_img = previous_imgs.pop(rectangle_index)
284
- previous_result = last_ocr1_results[rectangle_index]
285
- if previous_result and fuzz.ratio(previous_result, previous_text) >= 80:
286
- logger.info("Seems like the same text, not " + "doing second OCR" if twopassocr else "sending")
287
- return
288
- if previous_orig_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 80:
277
+ if text_stable_start_time:
278
+ stable_time = text_stable_start_time
279
+ previous_img_local = previous_img
280
+ if fuzz.ratio(orig_text_string, previous_text) >= 80:
289
281
  logger.info("Seems like Text we already sent, not doing anything.")
290
282
  return
291
- orig_text_results[rectangle_index] = orig_text_string
292
- last_ocr1_results[rectangle_index] = previous_text
293
- do_second_ocr(previous_text, rectangle_index, stable_time, previous_img)
283
+ orig_text_result = orig_text_string
284
+ previous_text = previous_text
285
+ do_second_ocr(previous_text, stable_time, previous_img_local, filtering)
286
+ previous_img = None
287
+ text_stable_start_time = None
288
+ last_oneocr_time = None
294
289
  return
295
290
  return
296
-
297
- if rectangle_index not in last_oneocr_results_to_check:
298
- last_oneocr_results_to_check[rectangle_index] = text
299
- last_oneocr_times[rectangle_index] = current_time
300
- text_stable_start_times[rectangle_index] = current_time
301
- previous_imgs[rectangle_index] = img
302
- return
303
-
304
- stable = text_stable_start_times.get(rectangle_index)
305
-
306
- if stable:
307
- time_since_stable_ms = int((current_time - stable).total_seconds() * 1000)
308
-
309
- if time_since_stable_ms >= TEXT_APPEARENCE_DELAY:
310
- last_oneocr_results_to_check[rectangle_index] = text
311
- last_oneocr_times[rectangle_index] = current_time
312
- else:
313
- last_oneocr_results_to_check[rectangle_index] = text
314
- last_oneocr_times[rectangle_index] = current_time
315
- previous_imgs[rectangle_index] = img
291
+ if not text_stable_start_time:
292
+ text_stable_start_time = line_start_time
293
+ previous_text = orig_text
294
+ last_oneocr_time = line_start_time
295
+ previous_img = img
316
296
 
317
297
  done = False
318
298
 
319
299
 
320
- def run_oneocr(ocr_config: OCRConfig, i, area=False):
300
+ def run_oneocr(ocr_config: OCRConfig, area=False):
321
301
  global done
302
+ print("Running OneOCR")
322
303
  screen_area = None
323
- if ocr_config.rectangles:
324
- rect_config = ocr_config.rectangles[i]
304
+ screen_areas = []
305
+ for rect_config in ocr_config.rectangles:
325
306
  coords = rect_config.coordinates
326
307
  monitor_config = rect_config.monitor
327
308
  screen_area = ",".join(str(c) for c in coords) if area else None
309
+ if screen_area:
310
+ screen_areas.append(screen_area)
328
311
  exclusions = list(rect.coordinates for rect in list(filter(lambda x: x.is_excluded, ocr_config.rectangles)))
329
312
  run.run(read_from="screencapture",
330
- read_from_secondary="clipboard" if i == 0 else None,
313
+ read_from_secondary="clipboard",
331
314
  write_to="callback",
332
315
  screen_capture_area=screen_area,
333
316
  # screen_capture_monitor=monitor_config['index'],
@@ -336,10 +319,11 @@ def run_oneocr(ocr_config: OCRConfig, i, area=False):
336
319
  screen_capture_delay_secs=get_ocr_scan_rate(), engine=ocr1,
337
320
  text_callback=text_callback,
338
321
  screen_capture_exclusions=exclusions,
339
- rectangle=i,
340
322
  language=language,
341
323
  monitor_index=ocr_config.window,
342
- ocr2=ocr2)
324
+ ocr2=ocr2,
325
+ gsm_ocr_config=ocr_config,
326
+ screen_capture_areas=screen_areas)
343
327
  done = True
344
328
 
345
329
 
@@ -400,17 +384,14 @@ if __name__ == "__main__":
400
384
  logger.info(f"Starting OCR with configuration: Window: {ocr_config.window}, Rectangles: {ocr_config.rectangles}, Engine 1: {ocr1}, Engine 2: {ocr2}, Two-pass OCR: {twopassocr}")
401
385
  if ocr_config:
402
386
  rectangles = list(filter(lambda rect: not rect.is_excluded, ocr_config.rectangles))
403
- last_ocr1_results = [""] * len(rectangles) if rectangles else [""]
404
- last_ocr2_results = [""] * len(rectangles) if rectangles else [""]
405
387
  oneocr_threads = []
406
388
  run.init_config(False)
407
389
  if rectangles:
408
- for i, rectangle in enumerate(rectangles):
409
- thread = threading.Thread(target=run_oneocr, args=(ocr_config, i,True, ), daemon=True)
410
- oneocr_threads.append(thread)
411
- thread.start()
390
+ thread = threading.Thread(target=run_oneocr, args=(ocr_config,True, ), daemon=True)
391
+ oneocr_threads.append(thread)
392
+ thread.start()
412
393
  else:
413
- single_ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config, 0,False, ), daemon=True)
394
+ single_ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config,False, ), daemon=True)
414
395
  oneocr_threads.append(single_ocr_thread)
415
396
  single_ocr_thread.start()
416
397
  websocket_server_thread = WebsocketServerThread(read=True)
@@ -379,7 +379,7 @@ class TextFiltering:
379
379
  else:
380
380
  orig_text_filtered.append(None)
381
381
 
382
- if last_result[1] == engine_index:
382
+ if last_result and last_result[1] == engine_index:
383
383
  last_text = last_result[0]
384
384
  else:
385
385
  last_text = []
@@ -399,6 +399,7 @@ class TextFiltering:
399
399
  break
400
400
  else:
401
401
  for block in new_blocks:
402
+ print(block)
402
403
  if lang not in ["ja", "zh"] or self.classify(block)[0] == lang:
403
404
  final_blocks.append(block)
404
405
 
@@ -407,7 +408,7 @@ class TextFiltering:
407
408
 
408
409
 
409
410
  class ScreenshotClass:
410
- def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows):
411
+ def __init__(self, screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas):
411
412
  self.macos_window_tracker_instance = None
412
413
  self.windows_window_tracker_instance = None
413
414
  self.screencapture_window_active = True
@@ -415,6 +416,7 @@ class ScreenshotClass:
415
416
  self.custom_left = None
416
417
  self.screen_capture_exclusions = screen_capture_exclusions
417
418
  self.screen_capture_window = screen_capture_window
419
+ self.areas = []
418
420
  if screen_capture_area == '':
419
421
  self.screencapture_mode = 0
420
422
  elif screen_capture_area.startswith('screen_'):
@@ -463,9 +465,15 @@ class ScreenshotClass:
463
465
 
464
466
  self.sct_params = {'top': coord_top, 'left': coord_left, 'width': coord_width, 'height': coord_height}
465
467
  logger.opt(ansi=True).info(f'Selected coordinates: {coord_left},{coord_top},{coord_width},{coord_height}')
466
- if len(screen_capture_area.split(',')) == 4:
467
- self.custom_left, self.custom_top, self.custom_width, self.custom_height = [int(c.strip()) for c in
468
- screen_capture_area.split(',')]
468
+ if screen_capture_areas:
469
+ for area in screen_capture_areas:
470
+ if len(area.split(',')) == 4:
471
+ self.areas.append(([int(c.strip()) for c in area.split(',')]))
472
+ else:
473
+ if len(screen_capture_area.split(',')) == 4:
474
+ self.areas.append(([int(c.strip()) for c in screen_capture_area.split(',')]))
475
+
476
+ self.areas.sort(key=lambda rect: (rect[1], rect[0]))
469
477
 
470
478
 
471
479
  if self.screencapture_mode == 2 or self.screen_capture_window:
@@ -724,8 +732,35 @@ class ScreenshotClass:
724
732
  left, top, width, height = exclusion
725
733
  draw.rectangle((left, top, left + width, top + height), fill=(0, 0, 0, 0))
726
734
 
727
- if self.custom_left:
728
- img = img.crop((self.custom_left, self.custom_top, self.custom_left + self.custom_width, self.custom_top + self.custom_height))
735
+ cropped_sections = []
736
+ start = time.time()
737
+ for area in self.areas:
738
+ cropped_sections.append(img.crop((area[0], area[1], area[0] + area[2], area[1] + area[3])))
739
+
740
+ # if len(cropped_sections) > 1:
741
+ # combined_width = sum(section.width for section in cropped_sections)
742
+ # combined_height = max(section.height for section in cropped_sections)
743
+ # combined_img = Image.new("RGBA", (combined_width, combined_height))
744
+ #
745
+ # x_offset = 0
746
+ # for section in cropped_sections:
747
+ # combined_img.paste(section, (x_offset, 0))
748
+ # x_offset += section.width
749
+ #
750
+ # img = combined_img
751
+ if len(cropped_sections) > 1:
752
+ combined_width = max(section.width for section in cropped_sections)
753
+ combined_height = sum(section.height for section in cropped_sections) + (len(cropped_sections) - 1) * 10 # Add space for gaps
754
+ combined_img = Image.new("RGBA", (combined_width, combined_height))
755
+
756
+ y_offset = 0
757
+ for section in cropped_sections:
758
+ combined_img.paste(section, (0, y_offset))
759
+ y_offset += section.height + 50 # Add gap between sections
760
+
761
+ img = combined_img
762
+ elif cropped_sections:
763
+ img = cropped_sections[0]
729
764
 
730
765
  if rand_int == 1:
731
766
  img.save(os.path.join(get_temporary_directory(), 'after_crop.png'), 'PNG')
@@ -858,7 +893,7 @@ def on_window_minimized(minimized):
858
893
  screencapture_window_visible = not minimized
859
894
 
860
895
 
861
- def process_and_write_results(img_or_path, write_to=None, last_result=None, filtering=None, notify=None, engine=None, rectangle=None, ocr_start_time=None):
896
+ def process_and_write_results(img_or_path, write_to=None, last_result=None, filtering=None, notify=None, engine=None, ocr_start_time=None):
862
897
  global engine_index
863
898
  if auto_pause_handler:
864
899
  auto_pause_handler.stop()
@@ -900,7 +935,7 @@ def process_and_write_results(img_or_path, write_to=None, last_result=None, filt
900
935
  elif write_to == 'clipboard':
901
936
  pyperclipfix.copy(text)
902
937
  elif write_to == "callback":
903
- txt_callback(text, orig_text, rectangle, ocr_start_time, img_or_path, bool(engine))
938
+ txt_callback(text, orig_text, ocr_start_time, img_or_path, bool(engine), filtering)
904
939
  elif write_to:
905
940
  with Path(write_to).open('a', encoding='utf-8') as f:
906
941
  f.write(text + '\n')
@@ -937,6 +972,7 @@ def run(read_from=None,
937
972
  combo_pause=None,
938
973
  combo_engine_switch=None,
939
974
  screen_capture_area=None,
975
+ screen_capture_areas=None,
940
976
  screen_capture_exclusions=None,
941
977
  screen_capture_window=None,
942
978
  screen_capture_delay_secs=None,
@@ -944,11 +980,11 @@ def run(read_from=None,
944
980
  screen_capture_combo=None,
945
981
  stop_running_flag=None,
946
982
  screen_capture_event_bus=None,
947
- rectangle=None,
948
983
  text_callback=None,
949
984
  language=None,
950
985
  monitor_index=None,
951
986
  ocr2=None,
987
+ gsm_ocr_config=None,
952
988
  ):
953
989
  """
954
990
  Japanese OCR client
@@ -1004,9 +1040,6 @@ def run(read_from=None,
1004
1040
  if screen_capture_event_bus is None:
1005
1041
  screen_capture_event_bus = config.get_general('screen_capture_event_bus')
1006
1042
 
1007
- if rectangle is None:
1008
- rectangle = config.get_general('rectangle')
1009
-
1010
1043
  if text_callback is None:
1011
1044
  text_callback = config.get_general('text_callback')
1012
1045
 
@@ -1063,8 +1096,8 @@ def run(read_from=None,
1063
1096
  global paused
1064
1097
  global just_unpaused
1065
1098
  global first_pressed
1066
- global notifier
1067
1099
  global auto_pause_handler
1100
+ global notifier
1068
1101
  global websocket_server_thread
1069
1102
  global image_queue
1070
1103
  custom_left = None
@@ -1124,7 +1157,8 @@ def run(read_from=None,
1124
1157
  if screen_capture_combo != '':
1125
1158
  screen_capture_on_combo = True
1126
1159
  key_combos[screen_capture_combo] = on_screenshot_combo
1127
- take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows)
1160
+ take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas)
1161
+ # global_take_screenshot = ScreenshotClass(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, rectangle)
1128
1162
  filtering = TextFiltering()
1129
1163
  read_from_readable.append('screen capture')
1130
1164
  if 'websocket' in (read_from, read_from_secondary):
@@ -1201,11 +1235,11 @@ def run(read_from=None,
1201
1235
  break
1202
1236
  elif img:
1203
1237
  if filter_img:
1204
- res, _ = process_and_write_results(img, write_to, last_result, filtering, notify, rectangle=rectangle, ocr_start_time=ocr_start_time)
1238
+ res, _ = process_and_write_results(img, write_to, last_result, filtering, notify, ocr_start_time=ocr_start_time)
1205
1239
  if res:
1206
1240
  last_result = (res, engine_index)
1207
1241
  else:
1208
- process_and_write_results(img, write_to, None, notify=notify, rectangle=rectangle, ocr_start_time=ocr_start_time, engine=ocr2)
1242
+ process_and_write_results(img, write_to, None, notify=notify, ocr_start_time=ocr_start_time, engine=ocr2)
1209
1243
  if isinstance(img, Path):
1210
1244
  if delete_images:
1211
1245
  Path.unlink(img)
@@ -0,0 +1,121 @@
1
+ import tkinter as tk
2
+ from PIL import Image, ImageTk
3
+ import subprocess
4
+ import os
5
+ import sys
6
+
7
+ from GameSentenceMiner import ffmpeg
8
+ from GameSentenceMiner.configuration import get_temporary_directory, logger
9
+ from GameSentenceMiner.ffmpeg import ffmpeg_base_command_list
10
+ from GameSentenceMiner.util import sanitize_filename
11
+
12
+ def extract_frames(video_path, timestamp, temp_dir, mode):
13
+ frame_paths = []
14
+ timestamp_number = float(timestamp)
15
+ golden_frame_index = 1 # Default to the first frame
16
+ golden_frame = None
17
+ video_duration = ffmpeg.get_video_duration(video_path)
18
+
19
+ if mode == 'middle':
20
+ timestamp_number = max(0.0, timestamp_number - 2.5)
21
+ elif mode == 'end':
22
+ timestamp_number = max(0.0, timestamp_number - 5.0)
23
+
24
+ if video_duration is not None and timestamp_number > video_duration:
25
+ logger.debug(f"Timestamp {timestamp_number} exceeds video duration {video_duration}.")
26
+ return None
27
+
28
+ try:
29
+ command = ffmpeg_base_command_list + [
30
+ "-y",
31
+ "-ss", str(timestamp_number),
32
+ "-i", video_path,
33
+ "-vf", f"fps=1/{0.25}",
34
+ "-vframes", "20",
35
+ os.path.join(temp_dir, "frame_%02d.png")
36
+ ]
37
+ subprocess.run(command, check=True, capture_output=True)
38
+ for i in range(1, 21):
39
+ if os.path.exists(os.path.join(temp_dir, f"frame_{i:02d}.png")):
40
+ frame_paths.append(os.path.join(temp_dir, f"frame_{i:02d}.png"))
41
+
42
+ if mode == "beginning":
43
+ golden_frame = frame_paths[0]
44
+ if mode == "middle":
45
+ golden_frame = frame_paths[len(frame_paths) // 2]
46
+ if mode == "end":
47
+ golden_frame = frame_paths[-1]
48
+ except subprocess.CalledProcessError as e:
49
+ logger.debug(f"Error extracting frames: {e}")
50
+ logger.debug(f"Command was: {' '.join(command)}")
51
+ logger.debug(f"FFmpeg output:\n{e.stderr.decode()}")
52
+ return None
53
+ except Exception as e:
54
+ logger.debug(f"An error occurred: {e}")
55
+ return None
56
+ return frame_paths, golden_frame
57
+
58
+ def timestamp_to_seconds(timestamp):
59
+ hours, minutes, seconds = map(int, timestamp.split(':'))
60
+ return hours * 3600 + minutes * 60 + seconds
61
+
62
+ def display_images(image_paths, golden_frame):
63
+ window = tk.Tk()
64
+ window.configure(bg="black") # Set the background color to black
65
+ window.title("Image Selector")
66
+ selected_path = tk.StringVar()
67
+ image_widgets = []
68
+
69
+ def on_image_click(event):
70
+ widget = event.widget
71
+ index = image_widgets.index(widget)
72
+ selected_path.set(image_paths[index])
73
+ window.quit()
74
+
75
+ for i, path in enumerate(image_paths):
76
+ img = Image.open(path)
77
+ img.thumbnail((450, 450))
78
+ img_tk = ImageTk.PhotoImage(img)
79
+ if golden_frame and path == golden_frame:
80
+ label = tk.Label(window, image=img_tk, borderwidth=5, relief="solid")
81
+ label.config(highlightbackground="yellow", highlightthickness=5)
82
+ else:
83
+ label = tk.Label(window, image=img_tk)
84
+ label.image = img_tk
85
+ label.grid(row=i // 5, column=i % 5, padx=5, pady=5)
86
+ label.bind("<Button-1>", on_image_click) # Bind click event to the label
87
+ image_widgets.append(label)
88
+
89
+ window.attributes("-topmost", True)
90
+ window.mainloop()
91
+ return selected_path.get()
92
+
93
+ def run_extraction_and_display(video_path, timestamp_str, mode):
94
+ temp_dir = os.path.join(get_temporary_directory(), "screenshot_frames", sanitize_filename(os.path.splitext(os.path.basename(video_path))[0]))
95
+ os.makedirs(temp_dir, exist_ok=True)
96
+ image_paths, golden_frame = extract_frames(video_path, timestamp_str, temp_dir, mode)
97
+ if image_paths:
98
+ selected_image_path = display_images(image_paths, golden_frame)
99
+ if selected_image_path:
100
+ print(selected_image_path)
101
+ else:
102
+ logger.debug("No image was selected.")
103
+ else:
104
+ logger.debug("Frame extraction failed.")
105
+
106
+ def main():
107
+ # if len(sys.argv) != 3:
108
+ # print("Usage: python script.py <video_path> <timestamp>")
109
+ # sys.exit(1)
110
+ try:
111
+ video_path = sys.argv[1]
112
+ timestamp_str = sys.argv[2]
113
+ mode = sys.argv[3] if len(sys.argv) > 3 else "beginning"
114
+ run_extraction_and_display(video_path, timestamp_str, mode)
115
+ except Exception as e:
116
+ logger.debug(e)
117
+ sys.exit(1)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
GameSentenceMiner/util.py CHANGED
@@ -186,7 +186,12 @@ def wait_for_stable_file(file_path, timeout=10, check_interval=0.1):
186
186
  try:
187
187
  current_size = os.path.getsize(file_path)
188
188
  if current_size == last_size:
189
- return True
189
+ try:
190
+ with open(file_path, 'rb') as f:
191
+ return True
192
+ except Exception as e:
193
+ time.sleep(check_interval)
194
+ elapsed_time += check_interval
190
195
  last_size = current_size
191
196
  time.sleep(check_interval)
192
197
  elapsed_time += check_interval
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.8.41
3
+ Version: 2.8.43
4
4
  Summary: A tool for mining sentences from games. Update: Multi-Line Mining! Fixed!
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -29,7 +29,7 @@ Requires-Dist: stable-ts~=2.17.5
29
29
  Requires-Dist: silero-vad~=5.1.2
30
30
  Requires-Dist: ttkbootstrap~=1.10.1
31
31
  Requires-Dist: dataclasses_json~=0.6.7
32
- Requires-Dist: win10toast
32
+ Requires-Dist: win10toast; sys_platform == "win32"
33
33
  Requires-Dist: numpy
34
34
  Requires-Dist: pystray
35
35
  Requires-Dist: pywin32; sys_platform == "win32"
@@ -1,19 +1,21 @@
1
1
  GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- GameSentenceMiner/anki.py,sha256=fu59gdyp1en_yKTK1WOVZX5lwGzGFKvhccXffgkoYlY,14190
2
+ GameSentenceMiner/anki.py,sha256=p4z8qleEKkOrMyBe7LeVfXzuFowQtk-ZyHr_7PyYt8U,14464
3
3
  GameSentenceMiner/config_gui.py,sha256=J3R_oh4edAULY9_0UEuEnRhRczakyta_f5hnegpC1uQ,77373
4
4
  GameSentenceMiner/configuration.py,sha256=5XdL7ZBEouj6Rz8pHvWKyTmhdDNs5p6UkdNng589n7Y,22428
5
5
  GameSentenceMiner/electron_config.py,sha256=dGcPYCISPehXubYSzsDuI2Gl092MYK0u3bTnkL9Jh1Y,9787
6
- GameSentenceMiner/ffmpeg.py,sha256=pNgBRaaZ_efvUnqOapMiJbsl8ZbL3eWPwjZPiJH8DhE,14558
6
+ GameSentenceMiner/ffmpeg.py,sha256=sf_Wz4ijFOYKRZI4qhRc32Y9E2zZuPscLv_VU8ZYCHU,18250
7
7
  GameSentenceMiner/gametext.py,sha256=hcyZQ69B7xB5ZG85wLzM5au7ZPKxmeUXsmUD26oyk_0,5660
8
- GameSentenceMiner/gsm.py,sha256=KsFj49e6XtoTDVT3_wTfq_YNkuPqRTkZgWM9PPSnq20,27176
8
+ GameSentenceMiner/gsm.py,sha256=rXWuR6RfJknkwtqHLspkk1qDXKO6Tk6dJ8Nuk-i-HDA,27239
9
9
  GameSentenceMiner/model.py,sha256=1lRyJFf_LND_4O16h8CWVqDfosLgr0ZS6ufBZ3qJHpY,5699
10
10
  GameSentenceMiner/notification.py,sha256=FY39ChSRK0Y8TQ6lBGsLnpZUFPtFpSy2tweeXVoV7kc,2809
11
- GameSentenceMiner/obs.py,sha256=6P6sDjQ1uoFd7TsOgpJBF_nclVGtqkX32IufiEh_fco,14486
11
+ GameSentenceMiner/obs.py,sha256=a-zLd7FrfI8fBiv5IfR-3pZYwqRJI_hUElzlZauaL40,14500
12
+ GameSentenceMiner/obs_back.py,sha256=_N_UV7Nh5cyy3mnH5lOUOzhgZwHMACeFEuBo1Z-bNzg,10894
12
13
  GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
14
+ GameSentenceMiner/ss_selector.py,sha256=hS_p_7VljVkFDg_POsutLfXDv1hdF-NbVnQAW4X5qsQ,4441
13
15
  GameSentenceMiner/text_log.py,sha256=MD7LB5D-v4G0Bnm3uGvZQ0aV38Fcj4E0vgq7mmyQ7_4,5157
14
- GameSentenceMiner/util.py,sha256=LzWGIDZb8NLv-RyrE_d6ycoQEwM1zpaDhWp0LKb6_Zc,8928
16
+ GameSentenceMiner/util.py,sha256=Lvk6sjEVBGhgVkWkalXDus48BZOSSMRuxNA7n3SiBqA,9147
15
17
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- GameSentenceMiner/ai/ai_prompting.py,sha256=O1QBgCL6AkkDyhzxZuW8FPCKgUDfkl_ZlKGcEUfbRnk,9508
18
+ GameSentenceMiner/ai/ai_prompting.py,sha256=_uWvwOg2YROVZV3MOKlx2THvAmg07ScwVyvBCtPrUzs,9572
17
19
  GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
18
20
  GameSentenceMiner/communication/send.py,sha256=X0MytGv5hY-uUvkfvdCqQA_ljZFmV6UkJ6in1TA1bUE,217
19
21
  GameSentenceMiner/communication/websocket.py,sha256=pTcUe_ZZRp9REdSU4qalhPmbT_1DKa7w18j6RfFLELA,3074
@@ -25,13 +27,13 @@ GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
25
27
  GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=WFTGg2J8FAqoILVbJaMt5XWhF-btXeDnj38Iuz-kf8k,2410
26
28
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
27
29
  GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Q8ETMHL7BKMA1mbtjrntDLyqCQB0lZ5T4RCZsodjH7Y,47186
28
- GameSentenceMiner/ocr/owocr_helper.py,sha256=actkNPsLGmhW0n1LGpgqsbiCc98MMRrStXXTsfGSyfI,17871
30
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=mcmljTQYfRHyoqtCVOHjyVAEGWwEkdUAYXY3Z85J4eg,16415
29
31
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
30
32
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
31
33
  GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
32
34
  GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
33
35
  GameSentenceMiner/owocr/owocr/ocr.py,sha256=dPnDmtG-I24kcfxC3iudeRIVgGhLmiWMGyRiMANcYsA,41573
34
- GameSentenceMiner/owocr/owocr/run.py,sha256=upwGML6oFb_SQNwU5P7tP9pe-dlJEbTo3QTp7ISrxUU,53363
36
+ GameSentenceMiner/owocr/owocr/run.py,sha256=MekuzH78OxSa2a79YpXdgJikrRh01oxk3Ydre7bxjTE,54942
35
37
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
36
38
  GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  GameSentenceMiner/vad/result.py,sha256=C08HsYH4qVjTRh_dvrWrskmXHJ950w0GWxPjGx_BfGY,275
@@ -52,9 +54,9 @@ GameSentenceMiner/web/static/web-app-manifest-512x512.png,sha256=wyqgCWCrLEUxSRX
52
54
  GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
55
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
54
56
  GameSentenceMiner/web/templates/utility.html,sha256=P659ZU2j7tcbJ5xPO3p7E_SQpkp3CrrFtSvvXJNNuLI,16330
55
- gamesentenceminer-2.8.41.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
56
- gamesentenceminer-2.8.41.dist-info/METADATA,sha256=2vOAqkGM1PNw_Xjw1FEm73Sq7pswzpxTHgIoDcrPf8Q,7193
57
- gamesentenceminer-2.8.41.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
58
- gamesentenceminer-2.8.41.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
59
- gamesentenceminer-2.8.41.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
60
- gamesentenceminer-2.8.41.dist-info/RECORD,,
57
+ gamesentenceminer-2.8.43.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
58
+ gamesentenceminer-2.8.43.dist-info/METADATA,sha256=IgVqNmHmj_PelgSoht2IKt0U93FyYyCHnDQ9CACTN0M,7218
59
+ gamesentenceminer-2.8.43.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
60
+ gamesentenceminer-2.8.43.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
61
+ gamesentenceminer-2.8.43.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
62
+ gamesentenceminer-2.8.43.dist-info/RECORD,,