GameSentenceMiner 2.11.2__py3-none-any.whl → 2.11.3__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/obs.py CHANGED
@@ -203,6 +203,7 @@ def connect_to_obs_sync(retry=2):
203
203
  obs_connection_manager = OBSConnectionManager()
204
204
  obs_connection_manager.start()
205
205
  update_current_game()
206
+ logger.info("Connected to OBS WebSocket.")
206
207
  break # Exit the loop once connected
207
208
  except Exception as e:
208
209
  if retry <= 0:
@@ -295,6 +296,12 @@ def get_source_from_scene(scene_name):
295
296
  logger.error(f"Error getting source from scene: {e}")
296
297
  return ''
297
298
 
299
+ def get_active_source():
300
+ current_game = get_current_game()
301
+ if not current_game:
302
+ return None
303
+ return get_source_from_scene(current_game)
304
+
298
305
  def get_record_directory():
299
306
  try:
300
307
  response = client.get_record_directory()
@@ -362,7 +369,7 @@ def get_screenshot_base64(compression=0, width=None, height=None):
362
369
  return None
363
370
  response = client.get_source_screenshot(name=current_source_name, img_format='png', quality=compression, width=width, height=height)
364
371
  if response and response.image_data:
365
- return response.image_data
372
+ return response.image_data.split(',', 1)[-1] # Remove data:image/png;base64, prefix if present
366
373
  else:
367
374
  logger.error(f"Error getting base64 screenshot: {response}")
368
375
  return None
@@ -1,12 +1,15 @@
1
- import ctypes
1
+ import os
2
2
  from copy import deepcopy
3
3
  from dataclasses import dataclass
4
4
  from math import floor, ceil
5
+ from pathlib import Path
5
6
 
7
+ from GameSentenceMiner import obs
6
8
  from dataclasses_json import dataclass_json
7
9
  from typing import List, Optional, Union
8
10
 
9
- from GameSentenceMiner.util.configuration import logger
11
+ from GameSentenceMiner.util.configuration import logger, get_app_directory
12
+ from GameSentenceMiner.util.gsm_utils import sanitize_filename
10
13
 
11
14
 
12
15
  @dataclass_json
@@ -50,8 +53,10 @@ class OCRConfig:
50
53
  window: Optional[str] = None
51
54
  language: str = "ja"
52
55
 
53
- def scale_coords(self):
56
+ def __post_init__(self):
54
57
  self.pre_scale_rectangles = deepcopy(self.rectangles)
58
+
59
+ def scale_coords(self):
55
60
  if self.coordinate_system and self.coordinate_system == "percentage" and self.window:
56
61
  import pygetwindow as gw
57
62
  try:
@@ -116,7 +121,28 @@ def get_window(title):
116
121
  return window
117
122
  return ret
118
123
 
119
- # try w10+, fall back to w8.1+
124
+ # if windows, set dpi awareness to per-monitor v2
120
125
  def set_dpi_awareness():
126
+ import sys
127
+ if sys.platform != "win32":
128
+ return
129
+ import ctypes
121
130
  per_monitor_awareness = 2
122
131
  ctypes.windll.shcore.SetProcessDpiAwareness(per_monitor_awareness)
132
+
133
+ def get_scene_ocr_config(use_window_as_config=False, window=""):
134
+ ocr_config_dir = get_ocr_config_path()
135
+ try:
136
+ if use_window_as_config:
137
+ scene = sanitize_filename(window)
138
+ else:
139
+ scene = sanitize_filename(obs.get_current_scene() or "Default")
140
+ except Exception as e:
141
+ print(f"Error getting OBS scene: {e}. Using default config name.")
142
+ scene = "Default"
143
+ return os.path.join(ocr_config_dir, f"{scene}.json")
144
+
145
+ def get_ocr_config_path():
146
+ ocr_config_dir = os.path.join(get_app_directory(), "ocr_config")
147
+ os.makedirs(ocr_config_dir, exist_ok=True)
148
+ return ocr_config_dir
@@ -1,23 +1,19 @@
1
+ import argparse
2
+ import base64
1
3
  import ctypes
4
+ import io
2
5
  import json
3
6
  import sys
4
7
  from multiprocessing import Process, Manager
5
8
  from pathlib import Path
6
9
 
7
- import mss
8
10
  from PIL import Image, ImageTk
9
11
 
10
12
  # Assuming a mock or real obs module exists in this path
11
13
  from GameSentenceMiner import obs
12
- from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window
14
+ from GameSentenceMiner.ocr.gsm_ocr_config import set_dpi_awareness, get_window, get_scene_ocr_config
13
15
  from GameSentenceMiner.util.gsm_utils import sanitize_filename
14
16
 
15
- try:
16
- import pygetwindow as gw
17
- except ImportError:
18
- print("Error: pygetwindow library not found. Please install it: pip install pygetwindow")
19
- gw = None
20
-
21
17
  try:
22
18
  import tkinter as tk
23
19
  from tkinter import font as tkfont # NEW: Import for better font control
@@ -34,30 +30,59 @@ COORD_SYSTEM_PERCENTAGE = "percentage"
34
30
 
35
31
 
36
32
  class ScreenSelector:
37
- def __init__(self, result, window_name, use_window_as_config):
38
- if not selector_available or not gw:
39
- raise RuntimeError("tkinter or pygetwindow is not available.")
33
+ def __init__(self, result, window_name, use_window_as_config, use_obs_screenshot=False):
34
+ if not selector_available:
35
+ raise RuntimeError("tkinter is not available.")
40
36
  if not window_name:
41
- raise ValueError("A target window name is required for percentage-based coordinates.")
37
+ raise ValueError("A target window name is required for configuration.")
42
38
 
43
39
  obs.connect_to_obs_sync()
44
40
  self.window_name = window_name
45
- print(f"Targeting window: '{window_name}'")
46
-
47
- self.sct = mss.mss()
48
- self.monitors = self.sct.monitors[1:]
49
- if not self.monitors:
50
- raise RuntimeError("No monitors found by mss.")
51
- for i, monitor in enumerate(self.monitors):
52
- monitor['index'] = i
53
-
54
- # --- Window Awareness is now critical ---
55
- self.target_window = self._find_target_window()
56
- self.target_window_geometry = self._get_window_geometry(self.target_window)
57
- if not self.target_window_geometry:
58
- raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
59
- print(f"Found target window at: {self.target_window_geometry}")
60
- # ---
41
+ self.use_obs_screenshot = use_obs_screenshot
42
+ self.screenshot_img = None
43
+ try:
44
+ import mss
45
+ self.sct = mss.mss()
46
+ self.monitors = self.sct.monitors[1:]
47
+ if not self.monitors:
48
+ raise RuntimeError("No monitors found by mss.")
49
+ for i, monitor in enumerate(self.monitors):
50
+ monitor['index'] = i
51
+ except ImportError:
52
+ print("Error: mss library not found. Please install it: pip install mss")
53
+ raise RuntimeError("mss is required for screen selection.")
54
+
55
+ if self.use_obs_screenshot:
56
+ print("Using OBS screenshot as target.")
57
+ screenshot_base64 = obs.get_screenshot_base64(compression=75)
58
+ # print(screenshot_base64)
59
+ if not screenshot_base64:
60
+ raise RuntimeError("Failed to get OBS screenshot.")
61
+ try:
62
+ img_data = base64.b64decode(screenshot_base64)
63
+ self.screenshot_img = Image.open(io.BytesIO(img_data))
64
+ # Scale image to 1280x720
65
+ self.screenshot_img = self.screenshot_img.resize((1280, 720), Image.LANCZOS)
66
+ except Exception as e:
67
+ raise RuntimeError(f"Failed to decode or open OBS screenshot: {e}")
68
+
69
+ self.target_window = None
70
+ self.target_window_geometry = {
71
+ "left": 0, "top": 0,
72
+ "width": self.screenshot_img.width,
73
+ "height": self.screenshot_img.height
74
+ }
75
+ print(f"OBS Screenshot dimensions: {self.target_window_geometry}")
76
+ else:
77
+ import pygetwindow as gw
78
+ if not gw:
79
+ raise RuntimeError("pygetwindow is not available for window selection.")
80
+ print(f"Targeting window: '{window_name}'")
81
+ self.target_window = self._find_target_window()
82
+ self.target_window_geometry = self._get_window_geometry(self.target_window)
83
+ if not self.target_window_geometry:
84
+ raise RuntimeError(f"Could not find or get geometry for window '{self.window_name}'.")
85
+ print(f"Found target window at: {self.target_window_geometry}")
61
86
 
62
87
  self.root = None
63
88
  self.scene = ''
@@ -71,6 +96,11 @@ class ScreenSelector:
71
96
  self.redo_stack = []
72
97
  self.bounding_box = {} # Geometry of the single large canvas window
73
98
 
99
+ self.canvas = None
100
+ self.window = None
101
+ self.instructions_widget = None
102
+ self.instructions_window_id = None
103
+
74
104
  self.load_existing_rectangles()
75
105
 
76
106
  def _find_target_window(self):
@@ -91,23 +121,9 @@ class ScreenSelector:
91
121
  return None
92
122
  return None
93
123
 
94
- def get_scene_ocr_config(self):
95
- app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
96
- ocr_config_dir = app_dir / "ocr_config"
97
- ocr_config_dir.mkdir(parents=True, exist_ok=True)
98
- try:
99
- if self.use_window_as_config:
100
- self.scene = sanitize_filename(self.window_name)
101
- else:
102
- self.scene = sanitize_filename(obs.get_current_scene() or "")
103
- except Exception as e:
104
- print(f"Error getting OBS scene: {e}. Using default config name.")
105
- self.scene = ""
106
- return ocr_config_dir / f"{self.scene}.json"
107
-
108
124
  def load_existing_rectangles(self):
109
125
  """Loads rectangles from config, converting from percentage to absolute pixels for use."""
110
- config_path = self.get_scene_ocr_config()
126
+ config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
111
127
  win_geom = self.target_window_geometry # Use current geometry for conversion
112
128
  win_w, win_h, win_l, win_t = win_geom['width'], win_geom['height'], win_geom['left'], win_geom['top']
113
129
 
@@ -152,7 +168,7 @@ class ScreenSelector:
152
168
 
153
169
  def save_rects(self, event=None):
154
170
  """Saves rectangles to config, converting from absolute pixels to percentages."""
155
- config_path = self.get_scene_ocr_config()
171
+ config_path = get_scene_ocr_config(self.use_window_as_config, self.window_name)
156
172
  win_geom = self.target_window_geometry
157
173
  win_l, win_t, win_w, win_h = win_geom['left'], win_geom['top'], win_geom['width'], win_geom['height']
158
174
  print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
@@ -200,6 +216,12 @@ class ScreenSelector:
200
216
  event.widget.winfo_toplevel().winfo_children()[0].delete(last_rect_id)
201
217
  print("Undo: Removed last rectangle.")
202
218
 
219
+ def toggle_image_mode(self, e=None):
220
+ self.image_mode = not self.image_mode
221
+ # Only change alpha of the main window, not the text widget
222
+ self.window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
223
+ print("Toggled background visibility.")
224
+
203
225
  def redo_last_rect(self, event=None):
204
226
  if not self.redo_stack: return
205
227
  monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
@@ -213,8 +235,63 @@ class ScreenSelector:
213
235
  print("Redo: Restored rectangle.")
214
236
 
215
237
  # --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
216
- def _create_instructions_widget(self, canvas):
217
- """Creates a text box with usage instructions on the canvas."""
238
+ def _create_instructions_widget(self, parent_canvas):
239
+ """Creates a separate, persistent window for instructions and control buttons."""
240
+ if self.instructions_widget and self.instructions_widget.winfo_exists():
241
+ self.instructions_widget.lift()
242
+ return
243
+
244
+ self.instructions_widget = tk.Toplevel(parent_canvas)
245
+ self.instructions_widget.title("Controls")
246
+
247
+ # --- Position it near the main window ---
248
+ parent_window = parent_canvas.winfo_toplevel()
249
+ # Make the instructions window transient to the main window to keep it on top
250
+ # self.instructions_widget.transient(parent_window)
251
+ self.instructions_widget.attributes('-topmost', 1)
252
+ # parent_window.update_idletasks() # Ensure dimensions are up-to-date
253
+ pos_x = parent_window.winfo_x() + 50
254
+ pos_y = parent_window.winfo_y() + 50
255
+ self.instructions_widget.geometry(f"+{pos_x}+{pos_y}")
256
+
257
+ main_frame = tk.Frame(self.instructions_widget, padx=10, pady=10)
258
+ main_frame.pack(fill=tk.BOTH, expand=True)
259
+
260
+ instructions_text = (
261
+ "How to Use:\n"
262
+ "• Left Click + Drag: Create a capture area (green).\n"
263
+ "• Shift + Left Click + Drag: Create an exclusion area (orange).\n"
264
+ "• Right-Click on a box: Delete it."
265
+ )
266
+ tk.Label(main_frame, text=instructions_text, justify=tk.LEFT, anchor="w").pack(pady=(0, 10), fill=tk.X)
267
+
268
+ button_frame = tk.Frame(main_frame)
269
+ button_frame.pack(fill=tk.X, pady=5)
270
+
271
+ def canvas_event_wrapper(func):
272
+ class MockEvent:
273
+ def __init__(self, widget):
274
+ self.widget = widget
275
+ return lambda: func(MockEvent(self.canvas))
276
+
277
+ def root_event_wrapper(func):
278
+ return lambda: func(None)
279
+
280
+ tk.Button(button_frame, text="Save and Quit (Ctrl+S)", command=root_event_wrapper(self.save_rects)).pack(fill=tk.X, pady=2)
281
+ tk.Button(button_frame, text="Undo (Ctrl+Z)", command=canvas_event_wrapper(self.undo_last_rect)).pack(fill=tk.X, pady=2)
282
+ tk.Button(button_frame, text="Redo (Ctrl+Y)", command=canvas_event_wrapper(self.redo_last_rect)).pack(fill=tk.X, pady=2)
283
+ tk.Button(button_frame, text="Toggle Background (M)", command=root_event_wrapper(self.toggle_image_mode)).pack(fill=tk.X, pady=2)
284
+ tk.Button(button_frame, text="Quit without Saving (Esc)", command=root_event_wrapper(self.quit_app)).pack(fill=tk.X, pady=2)
285
+
286
+ hotkeys_text = "\n• I: Toggle this instruction panel"
287
+ tk.Label(main_frame, text=hotkeys_text, justify=tk.LEFT, anchor="w").pack(pady=(10, 0), fill=tk.X)
288
+
289
+ self.instructions_widget.protocol("WM_DELETE_WINDOW", self.toggle_instructions)
290
+
291
+
292
+ # --- NEW METHOD TO DISPLAY INSTRUCTIONS ---
293
+ def print_instructions_box(self, canvas):
294
+ """Creates a separate, persistent window for instructions and control buttons."""
218
295
  instructions_text = (
219
296
  "How to Use:\n"
220
297
  " • Left Click + Drag: Create a capture area (green).\n"
@@ -260,44 +337,51 @@ class ScreenSelector:
260
337
  canvas.tag_lower(rect_id, text_id)
261
338
 
262
339
  def toggle_instructions(self, event=None):
263
- canvas = event.widget.winfo_toplevel().winfo_children()[0]
264
- # Find all text and rectangle items (assuming only one of each for instructions)
265
- text_items = [item for item in canvas.find_all() if canvas.type(item) == 'text']
266
- rect_items = [item for item in canvas.find_all() if canvas.type(item) == 'rectangle']
267
-
268
- if text_items and rect_items:
269
- current_state = canvas.itemcget(text_items[0], 'state')
270
- new_state = tk.NORMAL if current_state == tk.HIDDEN else tk.HIDDEN
271
- for item in text_items + rect_items:
272
- canvas.itemconfigure(item, state=new_state)
273
- print("Toggled instructions visibility.")
340
+ if self.instructions_widget and self.instructions_widget.winfo_exists() and self.instructions_widget.state() == "normal":
341
+ self.instructions_widget.withdraw()
342
+ print("Toggled instructions visibility: OFF")
343
+ else:
344
+ self._create_instructions_widget(self.canvas)
345
+ print("Toggled instructions visibility: ON")
274
346
 
275
347
  def start(self):
276
348
  self.root = tk.Tk()
277
349
  self.root.withdraw()
278
350
 
279
- # Calculate bounding box of all monitors
280
- left = min(m['left'] for m in self.monitors)
281
- top = min(m['top'] for m in self.monitors)
282
- right = max(m['left'] + m['width'] for m in self.monitors)
283
- bottom = max(m['top'] + m['height'] for m in self.monitors)
284
- self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
285
-
286
- sct_img = self.sct.grab(self.sct.monitors[0])
287
- img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
288
-
289
- window = tk.Toplevel(self.root)
290
- window.geometry(f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}")
291
- window.overrideredirect(1)
292
- window.attributes('-topmost', 1)
351
+ if self.use_obs_screenshot:
352
+ # Use the pre-loaded OBS screenshot
353
+ img = self.screenshot_img
354
+ self.bounding_box = self.target_window_geometry
355
+ # Center the window on the primary monitor
356
+ primary_monitor = self.sct.monitors[1] if len(self.sct.monitors) > 1 else self.sct.monitors[0]
357
+ win_x = primary_monitor['left'] + (primary_monitor['width'] - img.width) // 2
358
+ win_y = primary_monitor['top'] + (primary_monitor['height'] - img.height) // 2
359
+ window_geometry = f"{img.width}x{img.height}+{int(win_x)}+{int(win_y)}"
360
+ else:
361
+ # Calculate bounding box of all monitors for the overlay
362
+ left = min(m['left'] for m in self.monitors)
363
+ top = min(m['top'] for m in self.monitors)
364
+ right = max(m['left'] + m['width'] for m in self.monitors)
365
+ bottom = max(m['top'] + m['height'] for m in self.monitors)
366
+ self.bounding_box = {'left': left, 'top': top, 'width': right - left, 'height': bottom - top}
367
+
368
+ # Capture the entire desktop area covered by all monitors
369
+ sct_img = self.sct.grab(self.bounding_box)
370
+ img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
371
+ window_geometry = f"{self.bounding_box['width']}x{self.bounding_box['height']}+{left}+{top}"
372
+
373
+ self.window = tk.Toplevel(self.root)
374
+ self.window.geometry(window_geometry)
375
+ self.window.overrideredirect(1)
376
+ self.window.attributes('-topmost', 1)
293
377
 
294
378
  self.photo_image = ImageTk.PhotoImage(img)
295
- canvas = tk.Canvas(window, cursor='cross', highlightthickness=0)
296
- canvas.pack(fill=tk.BOTH, expand=True)
297
- canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
379
+ self.canvas = tk.Canvas(self.window, cursor='cross', highlightthickness=0)
380
+ self.canvas.pack(fill=tk.BOTH, expand=True)
381
+ self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW)
298
382
 
299
383
  # --- MODIFIED: CALL THE INSTRUCTION WIDGET CREATOR ---
300
- self._create_instructions_widget(canvas)
384
+ # self._create_instructions_widget(self.canvas)
301
385
  # --- END MODIFICATION ---
302
386
 
303
387
  # Draw existing rectangles (which were converted to absolute pixels on load)
@@ -305,29 +389,29 @@ class ScreenSelector:
305
389
  x_abs, y_abs, w_abs, h_abs = abs_coords
306
390
  canvas_x = x_abs - self.bounding_box['left']
307
391
  canvas_y = y_abs - self.bounding_box['top']
308
- rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
392
+ rect_id = self.canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
309
393
  outline='orange' if is_excluded else 'green', width=2)
310
394
  self.drawn_rect_ids.append(rect_id)
311
395
 
312
396
  def on_click(event):
313
397
  self.start_x, self.start_y = event.x, event.y
314
398
  outline = 'purple' if bool(event.state & 0x0001) else 'red'
315
- self.current_rect_id = canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
399
+ self.current_rect_id = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
316
400
  outline=outline, width=2)
317
401
 
318
402
  def on_drag(event):
319
- if self.current_rect_id: canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
403
+ if self.current_rect_id: self.canvas.coords(self.current_rect_id, self.start_x, self.start_y, event.x, event.y)
320
404
 
321
405
  def on_release(event):
322
406
  if not self.current_rect_id: return
323
- coords = canvas.coords(self.current_rect_id)
407
+ coords = self.canvas.coords(self.current_rect_id)
324
408
  x_abs = int(min(coords[0], coords[2]) + self.bounding_box['left'])
325
409
  y_abs = int(min(coords[1], coords[3]) + self.bounding_box['top'])
326
410
  w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
327
411
 
328
412
  if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
329
413
  is_excl = bool(event.state & 0x0001)
330
- canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
414
+ self.canvas.itemconfig(self.current_rect_id, outline='orange' if is_excl else 'green')
331
415
 
332
416
  center_x, center_y = x_abs + w / 2, y_abs + h / 2
333
417
  target_mon = self.monitors[0]
@@ -341,7 +425,7 @@ class ScreenSelector:
341
425
  self.drawn_rect_ids.append(self.current_rect_id)
342
426
  self.redo_stack.clear()
343
427
  else:
344
- canvas.delete(self.current_rect_id)
428
+ self.canvas.delete(self.current_rect_id)
345
429
  self.current_rect_id = self.start_x = self.start_y = None
346
430
 
347
431
  def on_right_click(event):
@@ -366,45 +450,45 @@ class ScreenSelector:
366
450
  # Now, perform the deletion
367
451
  del self.rectangles[i]
368
452
  del self.drawn_rect_ids[i]
369
- canvas.delete(item_id_to_del)
453
+ self.canvas.delete(item_id_to_del)
370
454
  print("Deleted rectangle.")
371
455
 
372
456
  break # Stop after deleting the topmost one
373
457
 
374
- def toggle_image_mode(e=None):
375
- self.image_mode = not self.image_mode
376
- # Only change alpha of the main window, not the text widget
377
- window.attributes("-alpha", 1.0 if self.image_mode else 0.25)
378
- print("Toggled background visibility.")
379
-
380
458
  def on_enter(e=None):
381
- canvas.focus_set()
382
-
383
- canvas.bind('<Enter>', on_enter)
384
- canvas.bind('<ButtonPress-1>', on_click)
385
- canvas.bind('<B1-Motion>', on_drag)
386
- canvas.bind('<ButtonRelease-1>', on_release)
387
- canvas.bind('<Button-3>', on_right_click)
388
- canvas.bind('<Control-s>', self.save_rects)
389
- canvas.bind('<Control-y>', self.redo_last_rect)
390
- canvas.bind('<Control-z>', self.undo_last_rect)
391
- canvas.bind("<Escape>", self.quit_app)
392
- canvas.bind("<m>", toggle_image_mode)
393
- canvas.bind("<i>", self.toggle_instructions)
394
-
395
- canvas.focus_set()
459
+ self.canvas.focus_set()
460
+
461
+ self.canvas.bind('<Enter>', on_enter)
462
+ self.canvas.bind('<ButtonPress-1>', on_click)
463
+ self.canvas.bind('<B1-Motion>', on_drag)
464
+ self.canvas.bind('<ButtonRelease-1>', on_release)
465
+ self.canvas.bind('<Button-3>', on_right_click)
466
+ self.canvas.bind('<Control-s>', self.save_rects)
467
+ self.canvas.bind('<Control-y>', self.redo_last_rect)
468
+ self.canvas.bind('<Control-z>', self.undo_last_rect)
469
+ self.canvas.bind("<Escape>", self.quit_app)
470
+ self.canvas.bind("<m>", self.toggle_image_mode)
471
+ self.canvas.bind("<i>", self.toggle_instructions)
472
+
473
+ self.canvas.focus_set()
474
+ self._create_instructions_widget(self.window)
475
+ self.window.winfo_toplevel().update_idletasks()
476
+ self.print_instructions_box(self.canvas)
396
477
  # The print message is now redundant but kept for console feedback
397
478
  print("Starting UI. See on-screen instructions. Press Esc to quit, Ctrl+S to save.")
479
+ # self.canvas.update_idletasks()
398
480
  self.root.mainloop()
399
481
 
400
482
  def quit_app(self, event=None):
483
+ if self.instructions_widget and self.instructions_widget.winfo_exists():
484
+ self.instructions_widget.destroy()
401
485
  if self.root and self.root.winfo_exists(): self.root.destroy()
402
486
  self.root = None
403
487
 
404
488
 
405
- def run_screen_selector(result_dict, window_name, use_window_as_config):
489
+ def run_screen_selector(result_dict, window_name, use_window_as_config, use_obs_screenshot):
406
490
  try:
407
- selector = ScreenSelector(result_dict, window_name, use_window_as_config)
491
+ selector = ScreenSelector(result_dict, window_name, use_window_as_config, use_obs_screenshot)
408
492
  selector.start()
409
493
  except Exception as e:
410
494
  print(f"Error in selector process: {e}", file=sys.stderr)
@@ -413,15 +497,15 @@ def run_screen_selector(result_dict, window_name, use_window_as_config):
413
497
  result_dict['error'] = str(e)
414
498
 
415
499
 
416
- def get_screen_selection(window_name, use_window_as_config=False):
417
- if not selector_available or not gw: return None
500
+ def get_screen_selection(window_name, use_window_as_config=False, use_obs_screenshot=False):
501
+ if not selector_available: return None
418
502
  if not window_name:
419
503
  print("Error: A target window name must be provided.", file=sys.stderr)
420
504
  return None
421
505
 
422
506
  with Manager() as manager:
423
507
  result_data = manager.dict()
424
- process = Process(target=run_screen_selector, args=(result_data, window_name, use_window_as_config))
508
+ process = Process(target=run_screen_selector, args=(result_data, window_name, use_window_as_config, use_obs_screenshot))
425
509
  print(f"Starting ScreenSelector process...")
426
510
  process.start()
427
511
  process.join()
@@ -439,18 +523,24 @@ def get_screen_selection(window_name, use_window_as_config=False):
439
523
 
440
524
  if __name__ == "__main__":
441
525
  set_dpi_awareness()
442
- target_window_title = "YouTube - JP"
443
- use_window_as_config = False
444
- if len(sys.argv) > 1:
445
- target_window_title = sys.argv[1]
446
- if len(sys.argv) > 2:
447
- use_window_as_config = True
448
- target_window_title = sys.argv[1]
449
526
 
450
- selection_result = get_screen_selection(target_window_title, use_window_as_config)
527
+ parser = argparse.ArgumentParser(description="Screen Selector Arguments")
528
+ parser.add_argument("window_title", nargs="?", default="", help="Target window title")
529
+ parser.add_argument("--obs_ocr", action="store_true", help="Use OBS screenshot")
530
+ parser.add_argument("--use_window_for_config", action="store_true", help="Use window for config")
531
+ args = parser.parse_args()
532
+
533
+ target_window_title = args.window_title
534
+ use_obs_screenshot = args.obs_ocr
535
+ use_window_as_config = args.use_window_for_config
536
+
537
+ print(f"Arguments: Window Title='{target_window_title}', Use OBS Screenshot={use_obs_screenshot}, Use Window for Config={use_window_as_config}")
538
+
539
+ # Example of how to call it
540
+ selection_result = get_screen_selection(target_window_title, use_window_as_config, use_obs_screenshot)
451
541
 
452
542
  if selection_result is None:
453
- print("\n--- Screen selection failed. ---")
543
+ print("--- Screen selection failed. ---")
454
544
  elif not selection_result:
455
545
  print("\n--- Screen selection cancelled. ---")
456
546
  elif 'rectangles' in selection_result:
@@ -22,7 +22,7 @@ from GameSentenceMiner.ocr.ss_picker import ScreenCropper
22
22
  from GameSentenceMiner.owocr.owocr.run import TextFiltering
23
23
  from GameSentenceMiner.util.configuration import get_config, get_app_directory, get_temporary_directory
24
24
  from GameSentenceMiner.util.electron_config import get_ocr_scan_rate, get_requires_open_window
25
- from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, set_dpi_awareness, get_window
25
+ from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, set_dpi_awareness, get_window, get_ocr_config_path
26
26
  from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
27
27
  from GameSentenceMiner.util.gsm_utils import sanitize_filename, do_text_replacements, OCR_REPLACEMENTS_FILE
28
28
 
@@ -49,15 +49,13 @@ logger.addHandler(console_handler)
49
49
 
50
50
  def get_ocr_config(window=None, use_window_for_config=False) -> OCRConfig:
51
51
  """Loads and updates screen capture areas from the corresponding JSON file."""
52
- app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
53
- ocr_config_dir = app_dir / "ocr_config"
54
- os.makedirs(ocr_config_dir, exist_ok=True)
52
+ ocr_config_dir = get_ocr_config_path()
55
53
  obs.connect_to_obs_sync(retry=0)
56
54
  if use_window_for_config and window:
57
55
  scene = sanitize_filename(window)
58
56
  else:
59
57
  scene = sanitize_filename(obs.get_current_scene())
60
- config_path = ocr_config_dir / f"{scene}.json"
58
+ config_path = Path(ocr_config_dir) / f"{scene}.json"
61
59
  if not config_path.exists():
62
60
  ocr_config = OCRConfig(scene=scene, window=window, rectangles=[], coordinate_system="percentage")
63
61
  with open(config_path, 'w', encoding="utf-8") as f:
@@ -202,7 +200,8 @@ def do_second_ocr(ocr1_text, time, img, filtering, ignore_furigana_filter=False,
202
200
  engine=ocr2, furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
203
201
 
204
202
  if compare_ocr_results(last_ocr2_result, orig_text):
205
- logger.info("Detected similar text from previous OCR2 result, not sending")
203
+ if text:
204
+ logger.info("Seems like Text we already sent, not doing anything.")
206
205
  return
207
206
  save_result_image(img)
208
207
  last_ocr2_result = orig_text
@@ -257,7 +256,8 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
257
256
 
258
257
  if manual or not twopassocr:
259
258
  if compare_ocr_results(previous_orig_text, orig_text_string):
260
- logger.info("Seems like Text we already sent, not doing anything.")
259
+ if text:
260
+ logger.info("Seems like Text we already sent, not doing anything.")
261
261
  return
262
262
  save_result_image(img)
263
263
  asyncio.run(send_result(text, line_start_time))
@@ -275,7 +275,8 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
275
275
  stable_time = text_stable_start_time
276
276
  previous_img_local = previous_img
277
277
  if compare_ocr_results(previous_orig_text, orig_text_string):
278
- logger.info("Seems like Text we already sent, not doing anything.")
278
+ if text:
279
+ logger.info("Seems like Text we already sent, not doing anything.")
279
280
  previous_text = None
280
281
  return
281
282
  previous_orig_text = orig_text_string
@@ -292,6 +293,10 @@ def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering
292
293
  previous_text = None
293
294
  return
294
295
 
296
+ # Make sure it's an actual new line before starting the timer
297
+ if compare_ocr_results(orig_text_string, previous_orig_text):
298
+ return
299
+
295
300
  if not text_stable_start_time:
296
301
  text_stable_start_time = line_start_time
297
302
  previous_text = text
@@ -327,8 +332,14 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
327
332
 
328
333
  run.init_config(False)
329
334
  try:
330
- run.run(read_from="screencapture" if window else "",
331
- read_from_secondary="clipboard" if ss_clipboard else None,
335
+ read_from = ""
336
+ if obs_ocr:
337
+ read_from = "obs"
338
+ elif window:
339
+ read_from = "screencapture"
340
+ read_from_secondary = "clipboard" if ss_clipboard else None
341
+ run.run(read_from=read_from,
342
+ read_from_secondary=read_from_secondary,
332
343
  write_to="callback",
333
344
  screen_capture_area=screen_area,
334
345
  # screen_capture_monitor=monitor_config['index'],
@@ -405,7 +416,7 @@ def set_force_stable_hotkey():
405
416
 
406
417
  if __name__ == "__main__":
407
418
  try:
408
- global ocr1, ocr2, twopassocr, language, ss_clipboard, ss, ocr_config, furigana_filter_sensitivity, area_select_ocr_hotkey, window, optimize_second_scan, use_window_for_config, keep_newline
419
+ global ocr1, ocr2, twopassocr, language, ss_clipboard, ss, ocr_config, furigana_filter_sensitivity, area_select_ocr_hotkey, window, optimize_second_scan, use_window_for_config, keep_newline, obs_ocr
409
420
  import sys
410
421
 
411
422
  import argparse
@@ -430,6 +441,7 @@ if __name__ == "__main__":
430
441
  parser.add_argument("--use_window_for_config", action="store_true",
431
442
  help="Use the specified window for loading OCR configuration")
432
443
  parser.add_argument("--keep_newline", action="store_true", help="Keep new lines in OCR output")
444
+ parser.add_argument('--obs_ocr', action='store_true', help='Use OBS for Picture Source (not implemented)')
433
445
 
434
446
  args = parser.parse_args()
435
447
 
@@ -449,12 +461,13 @@ if __name__ == "__main__":
449
461
  optimize_second_scan = args.optimize_second_scan
450
462
  use_window_for_config = args.use_window_for_config
451
463
  keep_newline = args.keep_newline
464
+ obs_ocr = args.obs_ocr
452
465
 
453
466
  window = None
454
467
  logger.info(f"Received arguments: {vars(args)}")
455
468
  # set_force_stable_hotkey()
456
469
  ocr_config: OCRConfig = get_ocr_config(window=window_name, use_window_for_config=use_window_for_config)
457
- if ocr_config:
470
+ if ocr_config and not obs_ocr:
458
471
  if ocr_config.window:
459
472
  start_time = time.time()
460
473
  while time.time() - start_time < 30:
@@ -326,7 +326,6 @@ class GoogleLens:
326
326
  # logger.info(f"Vertical space: {vertical_space}, Average height: {avg_height}")
327
327
  # logger.info(avg_height * 2)
328
328
  if vertical_space > avg_height * 2:
329
- logger.info('Adding blank line')
330
329
  res += 'BLANK_LINE'
331
330
  for line in paragraph['lines']:
332
331
  if furigana_filter_sensitivity:
@@ -55,7 +55,7 @@ except ImportError:
55
55
  pass
56
56
  from .config import Config
57
57
  from .screen_coordinate_picker import get_screen_selection
58
- from GameSentenceMiner.util.configuration import get_temporary_directory
58
+ from GameSentenceMiner.util.configuration import get_temporary_directory, get_config
59
59
 
60
60
  config = None
61
61
 
@@ -763,6 +763,100 @@ class ScreenshotThread(threading.Thread):
763
763
  elif self.windows_window_tracker_instance:
764
764
  self.windows_window_tracker_instance.join()
765
765
 
766
+ # Use OBS for Screenshot Source (i.e. Linux)
767
+ class OBSScreenshotThread(threading.Thread):
768
+ def __init__(self, ocr_config, screen_capture_on_combo, width=1280, height=720, interval=1):
769
+ super().__init__(daemon=True)
770
+ self.ocr_config = ocr_config
771
+ self.interval = interval
772
+ self.obs_client = None
773
+ self.websocket = None
774
+ self.width = width
775
+ self.height = height
776
+ self.use_periodic_queue = not screen_capture_on_combo
777
+
778
+ def write_result(self, result):
779
+ if self.use_periodic_queue:
780
+ periodic_screenshot_queue.put(result)
781
+ else:
782
+ image_queue.put((result, True))
783
+
784
+ def connect_obs(self):
785
+ try:
786
+ import obsws_python as obs
787
+ self.obs_client = obs.ReqClient(
788
+ host=get_config().obs.host,
789
+ port=get_config().obs.port,
790
+ password=get_config().obs.password,
791
+ timeout=10
792
+ )
793
+ logger.info("Connected to OBS WebSocket.")
794
+ except Exception as e:
795
+ logger.error(f"Failed to connect to OBS: {e}")
796
+ self.obs_client = None
797
+
798
+ def run(self):
799
+ import base64
800
+ import io
801
+ from PIL import Image
802
+ import GameSentenceMiner.obs as obs
803
+
804
+ loop = asyncio.new_event_loop()
805
+ asyncio.set_event_loop(loop)
806
+
807
+ self.connect_obs()
808
+ self.ocr_config.scale_to_custom_size(self.width, self.height)
809
+ current_source = obs.get_active_source()
810
+ current_source_name = current_source.get('sourceName') if isinstance(current_source, dict) else None
811
+
812
+ while not terminated:
813
+ try:
814
+ response = self.obs_client.get_source_screenshot(
815
+ name=current_source_name,
816
+ img_format='png',
817
+ quality=75,
818
+ width=self.width,
819
+ height=self.height,
820
+ )
821
+
822
+ if response.image_data:
823
+ image_data = base64.b64decode(response.image_data.split(",")[1])
824
+ img = Image.open(io.BytesIO(image_data)).convert("RGBA")
825
+
826
+ for rectangle in self.ocr_config.rectangles:
827
+ if rectangle.is_excluded:
828
+ left, top, width, height = rectangle.coordinates
829
+ draw = ImageDraw.Draw(img)
830
+ draw.rectangle((left, top, left + width, top + height), fill=(0, 0, 0, 0))
831
+
832
+ cropped_sections = []
833
+ for rectangle in [r for r in self.ocr_config.rectangles if not r.is_excluded]:
834
+ area = rectangle.coordinates
835
+ cropped_sections.append(img.crop((area[0], area[1], area[0] + area[2], area[1] + area[3])))
836
+
837
+ if len(cropped_sections) > 1:
838
+ combined_width = max(section.width for section in cropped_sections)
839
+ combined_height = sum(section.height for section in cropped_sections) + (
840
+ len(cropped_sections) - 1) * 10
841
+ combined_img = Image.new("RGBA", (combined_width, combined_height))
842
+ y_offset = 0
843
+ for section in cropped_sections:
844
+ combined_img.paste(section, (0, y_offset))
845
+ y_offset += section.height + 50
846
+ img = combined_img
847
+ elif cropped_sections:
848
+ img = cropped_sections[0]
849
+
850
+ self.write_result(img)
851
+ else:
852
+ logger.error("Failed to get screenshot data from OBS.")
853
+
854
+ except Exception as e:
855
+ logger.error(f"An unexpected error occurred with OBS connection: {e}")
856
+ continue
857
+
858
+ time.sleep(self.interval)
859
+
766
860
  class AutopauseTimer:
767
861
  def __init__(self, timeout):
768
862
  self.stop_event = threading.Event()
@@ -1137,7 +1231,7 @@ def run(read_from=None,
1137
1231
  prefix_to_use = ""
1138
1232
  delay_secs = config.get_general('delay_secs')
1139
1233
 
1140
- non_path_inputs = ('screencapture', 'clipboard', 'websocket', 'unixsocket')
1234
+ non_path_inputs = ('screencapture', 'clipboard', 'websocket', 'unixsocket', 'obs')
1141
1235
  read_from_path = None
1142
1236
  read_from_readable = []
1143
1237
  terminated = False
@@ -1176,22 +1270,33 @@ def run(read_from=None,
1176
1270
  global txt_callback
1177
1271
  txt_callback = text_callback
1178
1272
 
1179
- if 'screencapture' in (read_from, read_from_secondary):
1180
- global take_screenshot
1273
+ if 'screencapture' in (read_from, read_from_secondary) or 'obs' in (read_from, read_from_secondary):
1181
1274
  global screenshot_event
1182
- last_screenshot_time = 0
1183
- last_result = ([], engine_index)
1275
+ global take_screenshot
1184
1276
  if screen_capture_combo != '':
1185
1277
  screen_capture_on_combo = True
1186
1278
  key_combos[screen_capture_combo] = on_screenshot_combo
1187
1279
  else:
1188
1280
  global periodic_screenshot_queue
1189
1281
  periodic_screenshot_queue = queue.Queue()
1282
+
1283
+ if 'screencapture' in (read_from, read_from_secondary):
1284
+ last_screenshot_time = 0
1285
+ last_result = ([], engine_index)
1286
+
1190
1287
  screenshot_event = threading.Event()
1191
1288
  screenshot_thread = ScreenshotThread(screen_capture_area, screen_capture_window, screen_capture_exclusions, screen_capture_only_active_windows, screen_capture_areas, screen_capture_on_combo)
1192
1289
  screenshot_thread.start()
1193
1290
  filtering = TextFiltering()
1194
1291
  read_from_readable.append('screen capture')
1292
+ if 'obs' in (read_from, read_from_secondary):
1293
+ last_screenshot_time = 0
1294
+ last_result = ([], engine_index)
1295
+ screenshot_event = threading.Event()
1296
+ obs_screenshot_thread = OBSScreenshotThread(gsm_ocr_config, screen_capture_on_combo, interval=screen_capture_delay_secs)
1297
+ obs_screenshot_thread.start()
1298
+ filtering = TextFiltering()
1299
+ read_from_readable.append('obs')
1195
1300
  if 'websocket' in (read_from, read_from_secondary):
1196
1301
  read_from_readable.append('websocket')
1197
1302
  if 'unixsocket' in (read_from, read_from_secondary):
@@ -1231,7 +1336,7 @@ def run(read_from=None,
1231
1336
  write_to_readable = f'file {write_to}'
1232
1337
 
1233
1338
  process_queue = (any(i in ('clipboard', 'websocket', 'unixsocket') for i in (read_from, read_from_secondary)) or read_from_path or screen_capture_on_combo)
1234
- process_screenshots = 'screencapture' in (read_from, read_from_secondary) and not screen_capture_on_combo
1339
+ process_screenshots = any(x in ('screencapture', 'obs') for x in (read_from, read_from_secondary)) and not screen_capture_on_combo
1235
1340
  if threading.current_thread() == threading.main_thread():
1236
1341
  signal.signal(signal.SIGINT, signal_handler)
1237
1342
  if (not process_screenshots) and auto_pause != 0:
@@ -1256,7 +1361,7 @@ def run(read_from=None,
1256
1361
  pass
1257
1362
 
1258
1363
  if (not img) and process_screenshots:
1259
- if (not paused) and screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
1364
+ if (not paused) and (not screenshot_thread or (screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible)) and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
1260
1365
  screenshot_event.set()
1261
1366
  img = periodic_screenshot_queue.get()
1262
1367
  filter_img = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.11.2
3
+ Version: 2.11.3
4
4
  Summary: A tool for mining sentences from games. Update: Full UI Re-design
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -3,7 +3,7 @@ GameSentenceMiner/anki.py,sha256=3BVFXAM7tpJAxHMbsMpnMHUoDfyqHQ1JSYJThW18QWA,168
3
3
  GameSentenceMiner/config_gui.py,sha256=QTK1yBDcfHaIUR_JyekkRQY9CVI_rh3Cae0bi7lviIo,99198
4
4
  GameSentenceMiner/gametext.py,sha256=6VkjmBeiuZfPk8T6PHFdIAElBH2Y_oLVYvmcafqN7RM,6747
5
5
  GameSentenceMiner/gsm.py,sha256=wTERcvG37SeDel51TCFusoQqk5B_b11YY4QZMTF0a6s,24954
6
- GameSentenceMiner/obs.py,sha256=o_I6213VZvXqYkZDdUBgUg2KWi9SbnNZZjjUnKnQkK4,15190
6
+ GameSentenceMiner/obs.py,sha256=rapxY9PTDczGr7e8_41hVuD5VoRExe3IFFbSWZcYDsQ,15470
7
7
  GameSentenceMiner/vad.py,sha256=A3CvBQ67w3c7L8s7mTMxo6U_9ZQXlCToIpGUbePotfA,18321
8
8
  GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  GameSentenceMiner/ai/ai_prompting.py,sha256=ojp7i_xg2YB1zALgFbivwtXPMVkThnSbPoUiAs-nz_g,25892
@@ -16,17 +16,17 @@ GameSentenceMiner/assets/icon512.png,sha256=HxUj2GHjyQsk8NV433256UxU9phPhtjCY-YB
16
16
  GameSentenceMiner/assets/icon64.png,sha256=N8xgdZXvhqVQP9QUK3wX5iqxX9LxHljD7c-Bmgim6tM,9301
17
17
  GameSentenceMiner/assets/pickaxe.png,sha256=VfIGyXyIZdzEnVcc4PmG3wszPMO1W4KCT7Q_nFK6eSE,1403829
18
18
  GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=jtTzAWtMAx8GuA1XIJ_BmyNn3aYaO3u_c5Q7m5D4gS8,4056
19
+ GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=76IuoOMsBxNvU8z8lixqz58YSZpenNVugnHjrUXgCf4,4963
20
20
  GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
21
- GameSentenceMiner/ocr/owocr_area_selector.py,sha256=lHMVZuEE_-_wICfDr6jDJNSJIyZd2PnF7dIajknaHCU,20255
22
- GameSentenceMiner/ocr/owocr_helper.py,sha256=OV31PCPGwLUYq3HBFdS6UoFB2hAyJE8yHm1UoDYoY38,22431
21
+ GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Aj6t-cCePPeYNSF-XxQKo2gVNWmWqK3f3qR-0vxdtuE,25523
22
+ GameSentenceMiner/ocr/owocr_helper.py,sha256=sxmZcori9_ujldclwQFpmMwTyfJyflAQ3mn_3BvIdQs,22888
23
23
  GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
24
24
  GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
25
25
  GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
26
26
  GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
27
27
  GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
28
- GameSentenceMiner/owocr/owocr/ocr.py,sha256=S1unC9FShXApl5mxL0NAdvcH3wISB8KcolZcAxOGdnM,59450
29
- GameSentenceMiner/owocr/owocr/run.py,sha256=lwZOj5nmQWgXlQOA_b2FYrMjhGWCPA7ZZCBg-cj57-k,56518
28
+ GameSentenceMiner/owocr/owocr/ocr.py,sha256=xAhqCfVY2xKKvUhskAiAaYiL3yQrAl8oYi5GU46NOgI,59392
29
+ GameSentenceMiner/owocr/owocr/run.py,sha256=824KFS5v3c4ZLx7RYafBOezvFmnB4Idexf4mJAJhfp8,61100
30
30
  GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
31
31
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  GameSentenceMiner/util/audio_offset_selector.py,sha256=8Stk3BP-XVIuzRv9nl9Eqd2D-1yD3JrgU-CamBywJmY,8542
@@ -62,9 +62,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
62
62
  GameSentenceMiner/web/templates/index.html,sha256=Gv3CJvNnhAzIVV_QxhNq4OD-pXDt1vKCu9k6WdHSXuA,215343
63
63
  GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
64
64
  GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
65
- gamesentenceminer-2.11.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
66
- gamesentenceminer-2.11.2.dist-info/METADATA,sha256=zpe9LIoyiK5AyBRjnGH7Blp8z5rksCm3Nrxng5CKhyg,7319
67
- gamesentenceminer-2.11.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
- gamesentenceminer-2.11.2.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
69
- gamesentenceminer-2.11.2.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
70
- gamesentenceminer-2.11.2.dist-info/RECORD,,
65
+ gamesentenceminer-2.11.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
66
+ gamesentenceminer-2.11.3.dist-info/METADATA,sha256=mn5TDp9ibeCZhzxmIdhs4JZnVHwZPWtY0JYJRShzYGQ,7319
67
+ gamesentenceminer-2.11.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
+ gamesentenceminer-2.11.3.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
69
+ gamesentenceminer-2.11.3.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
70
+ gamesentenceminer-2.11.3.dist-info/RECORD,,