GameSentenceMiner 2.11.2__py3-none-any.whl → 2.11.4__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 +8 -1
- GameSentenceMiner/ocr/gsm_ocr_config.py +30 -4
- GameSentenceMiner/ocr/owocr_area_selector.py +207 -117
- GameSentenceMiner/ocr/owocr_helper.py +25 -12
- GameSentenceMiner/owocr/owocr/ocr.py +0 -1
- GameSentenceMiner/owocr/owocr/run.py +113 -8
- GameSentenceMiner/util/text_log.py +6 -3
- GameSentenceMiner/util/window_transparency.py +168 -0
- GameSentenceMiner/vad.py +13 -11
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/RECORD +15 -14
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.11.2.dist-info → gamesentenceminer-2.11.4.dist-info}/top_level.txt +0 -0
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
|
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
|
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
|
-
#
|
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
|
39
|
-
raise RuntimeError("tkinter
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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.
|
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.
|
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,
|
217
|
-
"""Creates a
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
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
|
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
|
-
|
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("
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
331
|
-
|
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
|
-
|
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
|
@@ -35,6 +35,9 @@ class GameLine:
|
|
35
35
|
def set_TL(self, tl: str):
|
36
36
|
self.TL = tl
|
37
37
|
|
38
|
+
def get_stripped_text(self):
|
39
|
+
return self.text.replace('\n', '').strip()
|
40
|
+
|
38
41
|
def __str__(self):
|
39
42
|
return str({"text": self.text, "time": self.time})
|
40
43
|
|
@@ -151,10 +154,10 @@ def get_line_and_future_lines(last_note):
|
|
151
154
|
found = False
|
152
155
|
for line in game_log.values:
|
153
156
|
if found:
|
154
|
-
found_lines.append(line
|
157
|
+
found_lines.append(line)
|
155
158
|
if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
|
156
159
|
found = True
|
157
|
-
found_lines.append(line
|
160
|
+
found_lines.append(line)
|
158
161
|
return found_lines
|
159
162
|
|
160
163
|
|
@@ -168,7 +171,7 @@ def get_mined_line(last_note: AnkiCard, lines=None):
|
|
168
171
|
|
169
172
|
sentence = last_note.get_field(get_config().anki.sentence_field)
|
170
173
|
for line in reversed(lines):
|
171
|
-
if lines_match(line.
|
174
|
+
if lines_match(line.get_stripped_text(), remove_html_and_cloze_tags(sentence)):
|
172
175
|
return line
|
173
176
|
return lines[-1]
|
174
177
|
|
@@ -0,0 +1,168 @@
|
|
1
|
+
import win32gui
|
2
|
+
import win32con
|
3
|
+
import win32api
|
4
|
+
import keyboard
|
5
|
+
import time
|
6
|
+
import threading
|
7
|
+
|
8
|
+
from GameSentenceMiner.util.configuration import logger
|
9
|
+
|
10
|
+
# --- Configuration (equivalent to AHK top-level variables) ---
|
11
|
+
TRANSPARENT_LEVEL = 1 # Almost invisible (0-255 scale)
|
12
|
+
OPAQUE_LEVEL = 255 # Fully opaque
|
13
|
+
HOTKEY = 'ctrl+alt+y'
|
14
|
+
|
15
|
+
# --- Global State Variables (equivalent to AHK global variables) ---
|
16
|
+
is_toggled = False
|
17
|
+
target_hwnd = None
|
18
|
+
# A lock to prevent race conditions when accessing global state from different threads
|
19
|
+
state_lock = threading.Lock()
|
20
|
+
|
21
|
+
# --- Core Functions (equivalent to AHK functions) ---
|
22
|
+
|
23
|
+
def set_window_transparency(hwnd, transparency):
|
24
|
+
"""
|
25
|
+
Sets the transparency of a window.
|
26
|
+
This is the Python equivalent of WinSetTransparent.
|
27
|
+
"""
|
28
|
+
if not hwnd or not win32gui.IsWindow(hwnd):
|
29
|
+
return
|
30
|
+
try:
|
31
|
+
# Get the current window style
|
32
|
+
style = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
|
33
|
+
# Add the WS_EX_LAYERED style, which is required for transparency
|
34
|
+
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, style | win32con.WS_EX_LAYERED)
|
35
|
+
# Set the transparency
|
36
|
+
win32gui.SetLayeredWindowAttributes(hwnd, 0, transparency, win32con.LWA_ALPHA)
|
37
|
+
except Exception as e:
|
38
|
+
# Some windows (like system or elevated ones) might deny permission
|
39
|
+
# logger.info(f"Error setting transparency for HWND {hwnd}: {e}")
|
40
|
+
pass
|
41
|
+
|
42
|
+
def set_always_on_top(hwnd, is_on_top):
|
43
|
+
"""
|
44
|
+
Sets or removes the "Always on Top" status for a window.
|
45
|
+
This is the Python equivalent of WinSetAlwaysOnTop.
|
46
|
+
"""
|
47
|
+
if not hwnd or not win32gui.IsWindow(hwnd):
|
48
|
+
return
|
49
|
+
try:
|
50
|
+
rect = win32gui.GetWindowRect(hwnd)
|
51
|
+
position = win32con.HWND_TOPMOST if is_on_top else win32con.HWND_NOTOPMOST
|
52
|
+
# Set the window position without moving or resizing it
|
53
|
+
win32gui.SetWindowPos(hwnd, position, rect[0], rect[1], 0, 0,
|
54
|
+
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
|
55
|
+
except Exception as e:
|
56
|
+
# logger.info(f"Error setting always-on-top for HWND {hwnd}: {e}")
|
57
|
+
pass
|
58
|
+
|
59
|
+
def reset_window_state(hwnd):
|
60
|
+
"""A helper to reset a window to its default state."""
|
61
|
+
set_window_transparency(hwnd, OPAQUE_LEVEL)
|
62
|
+
set_always_on_top(hwnd, False)
|
63
|
+
|
64
|
+
# --- Hotkey Callback (equivalent to AHK ^!y::) ---
|
65
|
+
|
66
|
+
def toggle_functionality():
|
67
|
+
"""
|
68
|
+
This function is called when the hotkey is pressed.
|
69
|
+
It manages the toggling logic.
|
70
|
+
"""
|
71
|
+
global is_toggled, target_hwnd
|
72
|
+
|
73
|
+
# Get the currently focused window (equivalent to WinGetID("A"))
|
74
|
+
current_hwnd = win32gui.GetForegroundWindow()
|
75
|
+
if not current_hwnd:
|
76
|
+
logger.info("No window is currently active!")
|
77
|
+
return
|
78
|
+
|
79
|
+
with state_lock:
|
80
|
+
# Case 1: The hotkey is pressed on the currently toggled window to disable it.
|
81
|
+
if is_toggled and target_hwnd == current_hwnd:
|
82
|
+
logger.info(f"Disabling functionality for window: {win32gui.GetWindowText(current_hwnd)}")
|
83
|
+
reset_window_state(current_hwnd)
|
84
|
+
is_toggled = False
|
85
|
+
target_hwnd = None
|
86
|
+
# Case 2: Enable functionality for a new window, or switch to a new one.
|
87
|
+
else:
|
88
|
+
# If another window was already toggled, reset it first.
|
89
|
+
if is_toggled and target_hwnd is not None:
|
90
|
+
logger.info(f"Resetting old window: {win32gui.GetWindowText(target_hwnd)}")
|
91
|
+
reset_window_state(target_hwnd)
|
92
|
+
|
93
|
+
# Enable functionality for the new window.
|
94
|
+
logger.info(f"Enabling functionality for window: {win32gui.GetWindowText(current_hwnd)}")
|
95
|
+
is_toggled = True
|
96
|
+
target_hwnd = current_hwnd
|
97
|
+
set_always_on_top(target_hwnd, True)
|
98
|
+
# The mouse_monitor_loop will handle setting the initial transparency
|
99
|
+
|
100
|
+
# --- Mouse Monitoring (equivalent to AHK Loop) ---
|
101
|
+
|
102
|
+
def mouse_monitor_loop():
|
103
|
+
"""
|
104
|
+
A loop that runs in a separate thread to monitor the mouse position.
|
105
|
+
"""
|
106
|
+
global is_toggled, target_hwnd
|
107
|
+
|
108
|
+
while True:
|
109
|
+
# We check the state without a lock first for performance,
|
110
|
+
# then use the lock when we need to read the shared variable.
|
111
|
+
if is_toggled:
|
112
|
+
with state_lock:
|
113
|
+
# Make a local copy of the target handle to work with
|
114
|
+
monitored_hwnd = target_hwnd
|
115
|
+
|
116
|
+
if monitored_hwnd:
|
117
|
+
# Get mouse position and the window handle under the cursor
|
118
|
+
pos = win32gui.GetCursorPos()
|
119
|
+
hwnd_under_mouse = win32gui.WindowFromPoint(pos)
|
120
|
+
|
121
|
+
# WindowFromPoint can return a child window (like a button).
|
122
|
+
# We need to walk up the parent chain to see if it belongs to our target window.
|
123
|
+
is_mouse_over_target = False
|
124
|
+
current_hwnd = hwnd_under_mouse
|
125
|
+
while current_hwnd != 0:
|
126
|
+
if current_hwnd == monitored_hwnd:
|
127
|
+
is_mouse_over_target = True
|
128
|
+
break
|
129
|
+
current_hwnd = win32gui.GetParent(current_hwnd)
|
130
|
+
|
131
|
+
# Apply transparency based on mouse position
|
132
|
+
if is_mouse_over_target:
|
133
|
+
set_window_transparency(monitored_hwnd, OPAQUE_LEVEL)
|
134
|
+
else:
|
135
|
+
set_window_transparency(monitored_hwnd, TRANSPARENT_LEVEL)
|
136
|
+
|
137
|
+
# A small delay to reduce CPU usage
|
138
|
+
time.sleep(0.1)
|
139
|
+
|
140
|
+
# --- Main Execution Block ---
|
141
|
+
|
142
|
+
if __name__ == "__main__":
|
143
|
+
import argparse
|
144
|
+
# Start the mouse monitor in a separate, non-blocking thread.
|
145
|
+
# daemon=True ensures the thread will exit when the main script does.
|
146
|
+
monitor_thread = threading.Thread(target=mouse_monitor_loop, daemon=True)
|
147
|
+
monitor_thread.start()
|
148
|
+
|
149
|
+
# get hotkey from args
|
150
|
+
parser = argparse.ArgumentParser(description="Window Transparency Toggle Script")
|
151
|
+
parser.add_argument('--hotkey', type=str, default=HOTKEY, help='Hotkey to toggle transparency (default: ctrl+alt+y)')
|
152
|
+
|
153
|
+
hotkey = parser.parse_args().hotkey.lower()
|
154
|
+
|
155
|
+
# Register the global hotkey
|
156
|
+
keyboard.add_hotkey(hotkey, toggle_functionality)
|
157
|
+
|
158
|
+
logger.info(f"Script running. Press '{hotkey}' on a window to toggle transparency.")
|
159
|
+
logger.info("Press Ctrl+C in this console to exit.")
|
160
|
+
|
161
|
+
# Keep the script running to listen for the hotkey.
|
162
|
+
# keyboard.wait() is a blocking call that waits indefinitely.
|
163
|
+
try:
|
164
|
+
keyboard.wait()
|
165
|
+
except KeyboardInterrupt:
|
166
|
+
if is_toggled and target_hwnd:
|
167
|
+
reset_window_state(target_hwnd)
|
168
|
+
logger.info("\nScript terminated by user.")
|
GameSentenceMiner/vad.py
CHANGED
@@ -53,18 +53,10 @@ class VADSystem:
|
|
53
53
|
match model:
|
54
54
|
case configuration.OFF:
|
55
55
|
return VADResult(False, 0, 0, "OFF")
|
56
|
-
# case configuration.GROQ:
|
57
|
-
# if not self.groq:
|
58
|
-
# self.groq = GroqVADProcessor()
|
59
|
-
# return self.groq.process_audio(input_audio, output_audio, game_line)
|
60
56
|
case configuration.SILERO:
|
61
57
|
if not self.silero:
|
62
58
|
self.silero = SileroVADProcessor()
|
63
59
|
return self.silero.process_audio(input_audio, output_audio, game_line)
|
64
|
-
# case configuration.VOSK:
|
65
|
-
# if not self.vosk:
|
66
|
-
# self.vosk = VoskVADProcessor()
|
67
|
-
# return self.vosk.process_audio(input_audio, output_audio, game_line)
|
68
60
|
case configuration.WHISPER:
|
69
61
|
if not self.whisper:
|
70
62
|
self.whisper = WhisperVADProcessor()
|
@@ -121,8 +113,6 @@ class VADProcessor(ABC):
|
|
121
113
|
logger.info("No voice activity detected in the audio.")
|
122
114
|
return VADResult(False, 0, 0, self.vad_system_name)
|
123
115
|
|
124
|
-
print(voice_activity)
|
125
|
-
|
126
116
|
start_time = voice_activity[0]['start'] if voice_activity else 0
|
127
117
|
end_time = voice_activity[-1]['end'] if voice_activity else 0
|
128
118
|
|
@@ -132,6 +122,17 @@ class VADProcessor(ABC):
|
|
132
122
|
if 0 > audio_length - voice_activity[-1]['start'] + get_config().audio.beginning_offset:
|
133
123
|
end_time = voice_activity[-2]['end']
|
134
124
|
|
125
|
+
# if detected text is much shorter than game_line.text, if no text, guess based on length
|
126
|
+
if 'text' in voice_activity[0]:
|
127
|
+
dectected_text = ''.join([item['text'] for item in voice_activity])
|
128
|
+
if game_line and game_line.text and len(dectected_text) < len(game_line.text) / 2:
|
129
|
+
logger.info(f"Detected text '{dectected_text}' is much shorter than expected '{game_line.text}', skipping.")
|
130
|
+
return VADResult(False, 0, 0, self.vad_system_name)
|
131
|
+
else:
|
132
|
+
if game_line and game_line.text and (end_time - start_time) < max(0.5, len(game_line.text) * 0.05):
|
133
|
+
logger.info(f"Detected audio length {end_time - start_time} is much shorter than expected for text '{game_line.text}', skipping.")
|
134
|
+
return VADResult(False, 0, 0, self.vad_system_name)
|
135
|
+
|
135
136
|
if get_config().vad.cut_and_splice_segments:
|
136
137
|
self.extract_audio_and_combine_segments(input_audio, voice_activity, output_audio, padding=get_config().vad.splice_padding)
|
137
138
|
else:
|
@@ -186,7 +187,7 @@ class WhisperVADProcessor(VADProcessor):
|
|
186
187
|
|
187
188
|
# Process the segments to extract tokens, timestamps, and confidence
|
188
189
|
for i, segment in enumerate(result.segments):
|
189
|
-
if len(segment.text)
|
190
|
+
if len(segment.text) <= 2 and ((i > 1 and segment.start - result.segments[i - 1].end > 1.0) or (i < len(result.segments) - 1 and result.segments[i + 1].start - segment.end > 1.0)):
|
190
191
|
if segment.text in ['えー', 'ん']:
|
191
192
|
logger.debug(f"Skipping filler segment: {segment.text} at {segment.start}-{segment.end}")
|
192
193
|
continue
|
@@ -194,6 +195,7 @@ class WhisperVADProcessor(VADProcessor):
|
|
194
195
|
logger.info(
|
195
196
|
"Unknown single character segment, not skipping, but logging, please report if this is a mistake: " + segment.text)
|
196
197
|
|
198
|
+
|
197
199
|
logger.debug(segment.to_dict())
|
198
200
|
voice_activity.append({
|
199
201
|
'text': segment.text,
|
@@ -3,8 +3,8 @@ 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=
|
7
|
-
GameSentenceMiner/vad.py,sha256=
|
6
|
+
GameSentenceMiner/obs.py,sha256=rapxY9PTDczGr7e8_41hVuD5VoRExe3IFFbSWZcYDsQ,15470
|
7
|
+
GameSentenceMiner/vad.py,sha256=Xj_9TM0fiaz9K8JcmW0QqGYASFnPEmYepsTHQrxP38c,18711
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
GameSentenceMiner/ai/ai_prompting.py,sha256=ojp7i_xg2YB1zALgFbivwtXPMVkThnSbPoUiAs-nz_g,25892
|
10
10
|
GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -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=
|
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=
|
22
|
-
GameSentenceMiner/ocr/owocr_helper.py,sha256=
|
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=
|
29
|
-
GameSentenceMiner/owocr/owocr/run.py,sha256=
|
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
|
@@ -38,7 +38,8 @@ GameSentenceMiner/util/model.py,sha256=AaOzgqSbaN7yks_rr1dQpLQR45FpBYdoLebMbrIYm
|
|
38
38
|
GameSentenceMiner/util/notification.py,sha256=0OnEYjn3DUEZ6c6OtPjdVZe-DG-QSoMAl9fetjjCvNU,3874
|
39
39
|
GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
|
40
40
|
GameSentenceMiner/util/ss_selector.py,sha256=cbjMxiKOCuOfbRvLR_PCRlykBrGtm1LXd6u5czPqkmc,4793
|
41
|
-
GameSentenceMiner/util/text_log.py,sha256=
|
41
|
+
GameSentenceMiner/util/text_log.py,sha256=jhG7ny8-DAilMAAPauN5HLoBNSIJ-cXAm68NLBxGNT8,5997
|
42
|
+
GameSentenceMiner/util/window_transparency.py,sha256=eQZausQ8A7-2Vd5cbBEJrJMKhaEPkLjJEa16kcnK6Ec,6592
|
42
43
|
GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
|
43
44
|
GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
|
44
45
|
GameSentenceMiner/util/communication/websocket.py,sha256=TbphRGmxVrgEupS7tNdifsmQfWDfIp0Hio2cSiUKgsk,3317
|
@@ -62,9 +63,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
62
63
|
GameSentenceMiner/web/templates/index.html,sha256=Gv3CJvNnhAzIVV_QxhNq4OD-pXDt1vKCu9k6WdHSXuA,215343
|
63
64
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
64
65
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
65
|
-
gamesentenceminer-2.11.
|
66
|
-
gamesentenceminer-2.11.
|
67
|
-
gamesentenceminer-2.11.
|
68
|
-
gamesentenceminer-2.11.
|
69
|
-
gamesentenceminer-2.11.
|
70
|
-
gamesentenceminer-2.11.
|
66
|
+
gamesentenceminer-2.11.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
67
|
+
gamesentenceminer-2.11.4.dist-info/METADATA,sha256=YP4JZMGhOyWAZVLztniBajXelAKy9biY_ZoRH0CHXXM,7319
|
68
|
+
gamesentenceminer-2.11.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
69
|
+
gamesentenceminer-2.11.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
70
|
+
gamesentenceminer-2.11.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
71
|
+
gamesentenceminer-2.11.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|