GameSentenceMiner 2.6.4__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
GameSentenceMiner/anki.py CHANGED
@@ -7,7 +7,7 @@ import urllib.request
7
7
  from datetime import datetime, timedelta
8
8
  from requests import post
9
9
 
10
- from GameSentenceMiner import obs, util, notification, ffmpeg
10
+ from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
11
11
  from GameSentenceMiner.ai.gemini import translate_with_context
12
12
  from GameSentenceMiner.configuration import *
13
13
  from GameSentenceMiner.configuration import get_config
@@ -279,7 +279,7 @@ def update_new_card():
279
279
  if use_prev_audio:
280
280
  lines = get_utility_window().get_selected_lines()
281
281
  with util.lock:
282
- update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
282
+ update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=gametext.get_mined_line(last_card, lines), reuse_audio=True)
283
283
  get_utility_window().reset_checkboxes()
284
284
  else:
285
285
  logger.info("New card(s) detected! Added to Processing Queue!")
@@ -392,7 +392,9 @@ def get_app_directory():
392
392
 
393
393
 
394
394
  def get_log_path():
395
- return os.path.join(get_app_directory(), 'gamesentenceminer.log')
395
+ path = os.path.join(get_app_directory(), "logs", 'gamesentenceminer.log')
396
+ os.makedirs(os.path.dirname(path), exist_ok=True)
397
+ return path
396
398
 
397
399
  temp_directory = ''
398
400
 
@@ -1,12 +1,13 @@
1
1
  import asyncio
2
+ import difflib
2
3
  import re
3
4
  import threading
4
5
  import time
5
- from datetime import datetime
6
+ from datetime import datetime, timedelta
6
7
 
7
8
  import pyperclip
8
9
  import websockets
9
- from websockets import InvalidStatusCode
10
+ from websockets import InvalidStatus
10
11
 
11
12
  from GameSentenceMiner import util
12
13
  from GameSentenceMiner.model import AnkiCard
@@ -66,12 +67,15 @@ class GameText:
66
67
  return matches[occurrence]
67
68
  return None
68
69
 
69
- def add_line(self, line_text):
70
- new_line = GameLine(line_text, datetime.now(), self.values[-1] if self.values else None, None, self.game_line_index)
70
+ def add_line(self, line_text, line_time=None):
71
+ if not line_text:
72
+ return
73
+ new_line = GameLine(line_text, line_time if line_time else datetime.now(), self.values[-1] if self.values else None, None, self.game_line_index)
71
74
  self.game_line_index += 1
72
75
  if self.values:
73
76
  self.values[-1].next = new_line
74
77
  self.values.append(new_line)
78
+ # self.remove_old_events(datetime.now() - timedelta(minutes=10))
75
79
 
76
80
  def has_line(self, line_text) -> bool:
77
81
  for game_line in self.values:
@@ -79,6 +83,9 @@ class GameText:
79
83
  return True
80
84
  return False
81
85
 
86
+ # def remove_old_events(self, cutoff_time: datetime):
87
+ # self.values = [line for line in self.values if line.time >= cutoff_time]
88
+
82
89
  line_history = GameText()
83
90
 
84
91
  class ClipboardMonitor(threading.Thread):
@@ -111,7 +118,7 @@ class ClipboardMonitor(threading.Thread):
111
118
  async def listen_websocket():
112
119
  global current_line, current_line_time, line_history, reconnecting, websocket_connected
113
120
  try_other = False
114
- websocket_url = f'ws://{get_config().general.websocket_uri}'
121
+ websocket_url = f'ws://{get_config().general.websocket_uri}/gsm'
115
122
  while True:
116
123
  if try_other:
117
124
  websocket_url = f'ws://{get_config().general.websocket_uri}/api/ws/text/origin'
@@ -123,6 +130,7 @@ async def listen_websocket():
123
130
  reconnecting = False
124
131
  websocket_connected = True
125
132
  try_other = True
133
+ line_time = None
126
134
  while True:
127
135
  message = await websocket.recv()
128
136
  logger.debug(message)
@@ -130,14 +138,17 @@ async def listen_websocket():
130
138
  data = json.loads(message)
131
139
  if "sentence" in data:
132
140
  current_clipboard = data["sentence"]
141
+ if "time" in data:
142
+ line_time = datetime.fromisoformat(data["time"])
143
+ print(line_time)
133
144
  except json.JSONDecodeError or TypeError:
134
145
  current_clipboard = message
135
146
  if current_clipboard != current_line:
136
- handle_new_text_event(current_clipboard)
137
- except (websockets.ConnectionClosed, ConnectionError, InvalidStatusCode) as e:
138
- if isinstance(e, InvalidStatusCode):
139
- e: InvalidStatusCode
140
- if e.status_code == 404:
147
+ handle_new_text_event(current_clipboard, line_time if line_time else None)
148
+ except (websockets.ConnectionClosed, ConnectionError, InvalidStatus) as e:
149
+ if isinstance(e, InvalidStatus):
150
+ e: InvalidStatus
151
+ if e.response.status_code == 404:
141
152
  logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
142
153
  try_other = True
143
154
 
@@ -148,7 +159,7 @@ async def listen_websocket():
148
159
  reconnecting = True
149
160
  await asyncio.sleep(5)
150
161
 
151
- def handle_new_text_event(current_clipboard):
162
+ def handle_new_text_event(current_clipboard, line_time=None):
152
163
  global current_line, current_line_time, line_history, current_line_after_regex
153
164
  current_line = current_clipboard
154
165
  if get_config().general.texthook_replacement_regex:
@@ -156,9 +167,10 @@ def handle_new_text_event(current_clipboard):
156
167
  else:
157
168
  current_line_after_regex = current_line
158
169
  logger.info(f"Line Received: {current_line_after_regex}")
159
- current_line_time = datetime.now()
160
- line_history.add_line(current_line_after_regex)
161
- get_utility_window().add_text(line_history[-1])
170
+ current_line_time = line_time if line_time else datetime.now()
171
+ line_history.add_line(current_line_after_regex, line_time)
172
+ if get_utility_window():
173
+ get_utility_window().add_text(line_history[-1])
162
174
 
163
175
 
164
176
  def reset_line_hotkey_pressed():
@@ -232,6 +244,8 @@ def get_line_and_future_lines(last_note):
232
244
  def get_mined_line(last_note: AnkiCard, lines):
233
245
  if not last_note:
234
246
  return lines[-1]
247
+ if not lines:
248
+ lines = get_all_lines()
235
249
 
236
250
  sentence = last_note.get_field(get_config().anki.sentence_field)
237
251
  for line in lines:
GameSentenceMiner/gsm.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  import os.path
3
2
  import signal
4
3
  from subprocess import Popen
@@ -496,6 +495,7 @@ def cleanup():
496
495
  icon.stop()
497
496
 
498
497
  settings_window.window.destroy()
498
+ time.sleep(5)
499
499
  logger.info("Cleanup complete.")
500
500
 
501
501
 
@@ -594,8 +594,8 @@ def main(reloading=False):
594
594
  win32api.SetConsoleCtrlHandler(handle_exit())
595
595
 
596
596
  try:
597
- if get_config().general.open_config_on_startup:
598
- root.after(0, settings_window.show)
597
+ # if get_config().general.open_config_on_startup:
598
+ # root.after(0, settings_window.show)
599
599
  if get_config().general.open_multimine_on_startup:
600
600
  root.after(0, get_utility_window().show)
601
601
  root.after(0, post_init)
@@ -614,4 +614,8 @@ def main(reloading=False):
614
614
 
615
615
  if __name__ == "__main__":
616
616
  logger.info("Starting GSM")
617
- main()
617
+ try:
618
+ main()
619
+ except Exception as e:
620
+ logger.exception(e)
621
+ time.sleep(5)
GameSentenceMiner/obs.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import os.path
3
3
  import subprocess
4
+ import tempfile
4
5
  import time
5
6
 
6
7
  import psutil
@@ -72,20 +73,27 @@ def get_obs_websocket_config_values():
72
73
  if get_config().obs.password == 'your_password':
73
74
  logger.info("OBS WebSocket password is not set. Setting it now...")
74
75
  config = get_master_config()
75
- config.get_config().obs.port = server_port
76
- config.get_config().obs.password = server_password
76
+ config.get_scene_ocr_config().obs.port = server_port
77
+ config.get_scene_ocr_config().obs.password = server_password
77
78
  with open(get_config_path(), 'w') as file:
78
79
  json.dump(config.to_dict(), file, indent=4)
79
80
  reload_config()
80
81
 
81
82
 
83
+ reconnecting = False
84
+
82
85
  def on_connect(obs):
86
+ global reconnecting
83
87
  logger.info("Reconnected to OBS WebSocket.")
84
- start_replay_buffer()
88
+ if reconnecting:
89
+ start_replay_buffer()
90
+ reconnecting = False
85
91
 
86
92
 
87
93
  def on_disconnect(obs):
94
+ global reconnecting
88
95
  logger.error("OBS Connection Lost!")
96
+ reconnecting = True
89
97
 
90
98
 
91
99
  def connect_to_obs():
@@ -113,6 +121,7 @@ def do_obs_call(request, from_dict = None, retry=10):
113
121
  if not client:
114
122
  time.sleep(1)
115
123
  return do_obs_call(request, from_dict, retry - 1)
124
+ logger.debug("Sending obs call: " + str(request))
116
125
  response = client.call(request)
117
126
  if not response.status and retry > 0:
118
127
  time.sleep(1)
@@ -142,8 +151,8 @@ def toggle_replay_buffer():
142
151
  # Start replay buffer
143
152
  def start_replay_buffer(retry=5):
144
153
  try:
145
- do_obs_call(requests.GetVersion())
146
- do_obs_call(requests.StartReplayBuffer())
154
+ if not get_replay_buffer_status()['outputActive']:
155
+ do_obs_call(requests.StartReplayBuffer(), retry=0)
147
156
  except Exception as e:
148
157
  if "socket is already closed" in str(e):
149
158
  if retry > 0:
@@ -152,6 +161,12 @@ def start_replay_buffer(retry=5):
152
161
  else:
153
162
  logger.error(f"Error starting replay buffer: {e}")
154
163
 
164
+ def get_replay_buffer_status():
165
+ try:
166
+ return do_obs_call(requests.GetReplayBufferStatus())
167
+ except Exception as e:
168
+ logger.error(f"Error getting replay buffer status: {e}")
169
+
155
170
 
156
171
  # Stop replay buffer
157
172
  def stop_replay_buffer():
@@ -176,7 +191,7 @@ def save_replay_buffer():
176
191
 
177
192
  def get_current_scene():
178
193
  try:
179
- return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict).sceneName
194
+ return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict, retry=0).sceneName
180
195
  except Exception as e:
181
196
  logger.error(f"Couldn't get scene: {e}")
182
197
  return ''
@@ -197,7 +212,7 @@ def get_record_directory():
197
212
  return ''
198
213
 
199
214
 
200
- def get_screenshot():
215
+ def get_screenshot(compression=-1):
201
216
  try:
202
217
  screenshot = util.make_unique_file_name(os.path.abspath(
203
218
  configuration.get_temporary_directory()) + '/screenshot.png')
@@ -207,11 +222,29 @@ def get_screenshot():
207
222
  if not current_source_name:
208
223
  logger.error("No active scene found.")
209
224
  return
210
- do_obs_call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot))
225
+ start = time.time()
226
+ logger.debug(f"Current source name: {current_source_name}")
227
+ response = client.call(requests.SaveSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageFilePath=screenshot, imageCompressionQuality=compression))
228
+ logger.debug(f"Screenshot response: {response}")
229
+ logger.debug(f"Screenshot took {time.time() - start:.3f} seconds to save")
211
230
  return screenshot
212
231
  except Exception as e:
213
232
  logger.error(f"Error getting screenshot: {e}")
214
233
 
234
+ def get_screenshot_base64():
235
+ try:
236
+ update_current_game()
237
+ current_source = get_source_from_scene(get_current_game())
238
+ current_source_name = current_source.sourceName
239
+ if not current_source_name:
240
+ logger.error("No active scene found.")
241
+ return
242
+ response = do_obs_call(requests.GetSourceScreenshot(sourceName=current_source_name, imageFormat='png', imageCompressionQuality=0))
243
+ with open('screenshot_response.txt', 'wb') as f:
244
+ f.write(str(response).encode())
245
+ return response['imageData']
246
+ except Exception as e:
247
+ logger.error(f"Error getting screenshot: {e}")
215
248
 
216
249
  def update_current_game():
217
250
  configuration.current_game = get_current_scene()
File without changes
@@ -0,0 +1,130 @@
1
+ import configparser
2
+ import os
3
+ import re
4
+
5
+ class OCRConfig:
6
+ def __init__(self, config_file=os.path.expanduser("~/.config/owocr_config.ini")):
7
+ self.config_file = config_file
8
+ self.config = configparser.ConfigParser(allow_no_value=True)
9
+ self.raw_config = {} # Store the raw lines of the config file
10
+ self.load_config()
11
+
12
+ def load_config(self):
13
+ if os.path.exists(self.config_file):
14
+ self.raw_config = self._read_config_with_comments()
15
+ self.config.read_dict(self._parse_config_to_dict())
16
+ else:
17
+ self.create_default_config()
18
+
19
+ def create_default_config(self):
20
+ self.raw_config = {
21
+ "general": [
22
+ ";engines = avision,alivetext,bing,glens,glensweb,gvision,azure,mangaocr,winrtocr,oneocr,easyocr,rapidocr,ocrspace",
23
+ ";engine = glens",
24
+ "read_from = screencapture",
25
+ "write_to = websocket",
26
+ ";note: this specifies an amount of seconds to wait for auto pausing the program after a successful text recognition. Will be ignored when reading with screen capture. 0 to disable.",
27
+ ";auto_pause = 0",
28
+ ";pause_at_startup = False",
29
+ ";logger_format = <green>{time:HH:mm:ss.SSS}</green> | <level>{message}</level>",
30
+ ";engine_color = cyan",
31
+ "websocket_port = 7331",
32
+ ";delay_secs = 0.5",
33
+ ";notifications = False",
34
+ ";ignore_flag = False",
35
+ ";delete_images = False",
36
+ ";note: this specifies a combo to wait on for pausing the program. As an example: <ctrl>+<shift>+p. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
37
+ ";combo_pause = <ctrl>+<shift>+p",
38
+ ";note: this specifies a combo to wait on for switching the OCR engine. As an example: <ctrl>+<shift>+a. To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
39
+ ";combo_engine_switch = <ctrl>+<shift>+a",
40
+ ";note: screen_capture_area can be empty for the coordinate picker, \"screen_N\" (where N is the screen number starting from 1) for an entire screen, have a manual set of coordinates (x,y,width,height) or a window name (the first matching window title will be used).",
41
+ ";screen_capture_area = ",
42
+ ";screen_capture_area = screen_1",
43
+ ";screen_capture_area = 400,200,1500,600",
44
+ ";screen_capture_area = OBS",
45
+ ";note: if screen_capture_area is a window name, this can be changed to capture inactive windows too.",
46
+ ";screen_capture_only_active_windows = True",
47
+ ";screen_capture_delay_secs = 3",
48
+ ";note: this specifies a combo to wait on for taking a screenshot instead of using the delay. As an example: <ctrl>+<shift>+s. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key",
49
+ ";screen_capture_combo = <ctrl>+<shift>+s",
50
+ ],
51
+ "winrtocr": [";url = http://aaa.xxx.yyy.zzz:8000"],
52
+ "oneocr": [";url = http://aaa.xxx.yyy.zzz:8001"],
53
+ "azure": [";api_key = api_key_here", ";endpoint = https://YOURPROJECT.cognitiveservices.azure.com/"],
54
+ "mangaocr": ["pretrained_model_name_or_path = kha-white/manga-ocr-base", "force_cpu = False"],
55
+ "easyocr": ["gpu = True"],
56
+ "ocrspace": [";api_key = api_key_here"],
57
+ }
58
+ self.config.read_dict(self._parse_config_to_dict())
59
+ self.save_config()
60
+
61
+ def _read_config_with_comments(self):
62
+ with open(self.config_file, "r") as f:
63
+ lines = f.readlines()
64
+ config_data = {}
65
+ current_section = None
66
+ for line in lines:
67
+ line = line.strip()
68
+ if line.startswith("[") and line.endswith("]"):
69
+ current_section = line[1:-1]
70
+ config_data[current_section] = []
71
+ elif current_section is not None:
72
+ config_data[current_section].append(line)
73
+ return config_data
74
+
75
+ def _parse_config_to_dict(self):
76
+ parsed_config = {}
77
+ for section, lines in self.raw_config.items():
78
+ parsed_config[section] = {}
79
+ for line in lines:
80
+ if "=" in line and not line.startswith(";"):
81
+ key, value = line.split("=", 1)
82
+ parsed_config[section][key.strip()] = value.strip()
83
+ return parsed_config
84
+
85
+ def save_config(self):
86
+ with open(self.config_file, "w") as f:
87
+ for section, lines in self.raw_config.items():
88
+ f.write(f"[{section}]\n")
89
+ for line in lines:
90
+ f.write(f"{line}\n")
91
+
92
+ def get_value(self, section, key):
93
+ if section in self.config and key in self.config[section]:
94
+ return self.config[section][key]
95
+ return None
96
+
97
+ def set_value(self, section, key, value):
98
+ if section not in self.config:
99
+ self.raw_config[section] = [] #add section if it does not exist.
100
+ if section not in self.config:
101
+ self.config[section] = {}
102
+ self.config[section][key] = str(value)
103
+
104
+ # Update the raw config to keep comments
105
+ found = False
106
+ for i, line in enumerate(self.raw_config[section]):
107
+ if line.startswith(key + " ="):
108
+ self.raw_config[section][i] = f"{key} = {value}"
109
+ found = True
110
+ break
111
+ if not found:
112
+ self.raw_config[section].append(f"{key} = {value}")
113
+
114
+ self.save_config()
115
+
116
+ def get_section(self, section):
117
+ if section in self.config:
118
+ return dict(self.config[section])
119
+ return None
120
+
121
+ def set_screen_capture_area(self, screen_capture_data):
122
+ if not isinstance(screen_capture_data, dict) or "coordinates" not in screen_capture_data:
123
+ raise ValueError("Invalid screen capture data format.")
124
+
125
+ coordinates = screen_capture_data["coordinates"]
126
+ if len(coordinates) != 4:
127
+ raise ValueError("Coordinates must contain four values: x, y, width, height.")
128
+
129
+ x, y, width, height = coordinates
130
+ self.set_value("general", "screen_capture_area", f"{x},{y},{width},{height}")