GameSentenceMiner 2.9.18__py3-none-any.whl → 2.9.19__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/gametext.py +0 -3
- GameSentenceMiner/gsm.py +6 -120
- GameSentenceMiner/ocr/owocr_helper.py +69 -56
- GameSentenceMiner/ocr/ss_picker.py +132 -0
- GameSentenceMiner/util/ffmpeg.py +26 -0
- GameSentenceMiner/web/service.py +135 -0
- GameSentenceMiner/web/texthooking_page.py +15 -7
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/METADATA +2 -2
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/RECORD +13 -11
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.18.dist-info → gamesentenceminer-2.9.19.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gametext.py
CHANGED
@@ -5,11 +5,9 @@ import pyperclip
|
|
5
5
|
import websockets
|
6
6
|
from websockets import InvalidStatus
|
7
7
|
|
8
|
-
from GameSentenceMiner import util
|
9
8
|
from GameSentenceMiner.util.gsm_utils import do_text_replacements, TEXT_REPLACEMENTS_FILE, run_new_thread
|
10
9
|
from GameSentenceMiner.util.configuration import *
|
11
10
|
from GameSentenceMiner.util.text_log import *
|
12
|
-
|
13
11
|
from GameSentenceMiner.web.texthooking_page import add_event_to_texthooker
|
14
12
|
|
15
13
|
current_line = ''
|
@@ -65,7 +63,6 @@ async def listen_websockets():
|
|
65
63
|
logger.info(f"Texthooker WebSocket {uri} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
|
66
64
|
reconnecting = False
|
67
65
|
websocket_connected[uri] = True
|
68
|
-
try_other = True
|
69
66
|
line_time = None
|
70
67
|
while True:
|
71
68
|
message = await websocket.recv()
|
GameSentenceMiner/gsm.py
CHANGED
@@ -4,10 +4,10 @@ import sys
|
|
4
4
|
|
5
5
|
import os
|
6
6
|
|
7
|
+
|
7
8
|
os.environ.pop('TCL_LIBRARY', None)
|
8
9
|
|
9
|
-
from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
|
10
|
-
open_audio_in_external
|
10
|
+
from GameSentenceMiner.util.gsm_utils import wait_for_stable_file, make_unique_file_name, run_new_thread
|
11
11
|
from GameSentenceMiner.util.communication.send import send_restart_signal
|
12
12
|
from GameSentenceMiner.util.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
|
13
13
|
from GameSentenceMiner.vad import vad_processor, VADResult
|
@@ -40,6 +40,7 @@ try:
|
|
40
40
|
from GameSentenceMiner.util.text_log import GameLine, get_text_event, get_mined_line, get_all_lines, game_log
|
41
41
|
from GameSentenceMiner.util import *
|
42
42
|
from GameSentenceMiner.web import texthooking_page
|
43
|
+
from GameSentenceMiner.web.service import handle_texthooker_button
|
43
44
|
from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
|
44
45
|
except Exception as e:
|
45
46
|
from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
|
@@ -77,13 +78,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
77
78
|
def process_replay(self, video_path):
|
78
79
|
vad_trimmed_audio = ''
|
79
80
|
skip_delete = False
|
80
|
-
|
81
|
-
os.remove(video_path)
|
82
|
-
video_path = gsm_state.previous_replay
|
83
|
-
else:
|
84
|
-
gsm_state.previous_replay = video_path
|
81
|
+
gsm_state.previous_replay = video_path
|
85
82
|
if gsm_state.line_for_audio or gsm_state.line_for_screenshot:
|
86
|
-
|
83
|
+
handle_texthooker_button(video_path, get_audio_from_video=VideoToAudioHandler.get_audio)
|
87
84
|
return
|
88
85
|
try:
|
89
86
|
if anki.card_queue and len(anki.card_queue) > 0:
|
@@ -180,66 +177,11 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
180
177
|
if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
181
178
|
os.remove(vad_trimmed_audio)
|
182
179
|
|
183
|
-
def handle_texthooker_button(self, video_path):
|
184
|
-
try:
|
185
|
-
if gsm_state.line_for_audio:
|
186
|
-
line: GameLine = gsm_state.line_for_audio
|
187
|
-
gsm_state.line_for_audio = None
|
188
|
-
if line == gsm_state.previous_line_for_audio:
|
189
|
-
logger.info("Line is the same as the last one, skipping processing.")
|
190
|
-
if get_config().advanced.audio_player_path:
|
191
|
-
play_audio_in_external(gsm_state.previous_audio)
|
192
|
-
elif get_config().advanced.video_player_path:
|
193
|
-
play_video_in_external(line, gsm_state.previous_audio)
|
194
|
-
return
|
195
|
-
gsm_state.previous_line_for_audio = line
|
196
|
-
if get_config().advanced.audio_player_path:
|
197
|
-
audio = VideoToAudioHandler.get_audio(line, line.next.time if line.next else None, video_path,
|
198
|
-
temporary=True)
|
199
|
-
play_audio_in_external(audio)
|
200
|
-
gsm_state.previous_audio = audio
|
201
|
-
elif get_config().advanced.video_player_path:
|
202
|
-
new_video_path = play_video_in_external(line, video_path)
|
203
|
-
gsm_state.previous_audio = new_video_path
|
204
|
-
gsm_state.previous_replay = new_video_path
|
205
|
-
return
|
206
|
-
if gsm_state.line_for_screenshot:
|
207
|
-
line: GameLine = gsm_state.line_for_screenshot
|
208
|
-
gsm_state.line_for_screenshot = None
|
209
|
-
gsm_state.previous_line_for_screenshot = line
|
210
|
-
screenshot = ffmpeg.get_screenshot_for_line(video_path, line, True)
|
211
|
-
if gsm_state.anki_note_for_screenshot:
|
212
|
-
gsm_state.anki_note_for_screenshot = None
|
213
|
-
encoded_image = ffmpeg.process_image(screenshot)
|
214
|
-
if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
|
215
|
-
last_note = anki.get_last_anki_card()
|
216
|
-
if get_config().features.backfill_audio:
|
217
|
-
last_note = anki.get_cards_by_sentence(gametext.current_line)
|
218
|
-
if last_note:
|
219
|
-
anki.add_image_to_card(last_note, encoded_image)
|
220
|
-
notification.send_screenshot_updated(last_note.get_field(get_config().anki.word_field))
|
221
|
-
if get_config().features.open_anki_edit:
|
222
|
-
notification.open_anki_card(last_note.noteId)
|
223
|
-
else:
|
224
|
-
notification.send_screenshot_saved(encoded_image)
|
225
|
-
else:
|
226
|
-
notification.send_screenshot_saved(encoded_image)
|
227
|
-
else:
|
228
|
-
os.startfile(screenshot)
|
229
|
-
return
|
230
|
-
except Exception as e:
|
231
|
-
logger.error(f"Error Playing Audio/Video: {e}")
|
232
|
-
logger.debug(f"Error Playing Audio/Video: {e}", exc_info=True)
|
233
|
-
return
|
234
|
-
finally:
|
235
|
-
if video_path and get_config().paths.remove_video and os.path.exists(video_path):
|
236
|
-
os.remove(video_path)
|
237
|
-
|
238
180
|
@staticmethod
|
239
181
|
def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
|
240
182
|
trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time)
|
241
183
|
if temporary:
|
242
|
-
return trimmed_audio
|
184
|
+
return ffmpeg.convert_audio_to_wav_lossless(trimmed_audio)
|
243
185
|
vad_trimmed_audio = make_unique_file_name(
|
244
186
|
f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
245
187
|
final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
|
@@ -256,62 +198,6 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
256
198
|
return final_audio_output, vad_result, vad_trimmed_audio
|
257
199
|
|
258
200
|
|
259
|
-
def play_audio_in_external(filepath):
|
260
|
-
exe = get_config().advanced.audio_player_path
|
261
|
-
|
262
|
-
filepath = os.path.normpath(filepath)
|
263
|
-
|
264
|
-
command = [exe, "--no-video", filepath]
|
265
|
-
|
266
|
-
try:
|
267
|
-
subprocess.Popen(command)
|
268
|
-
print(f"Opened {filepath} in {exe}.")
|
269
|
-
except Exception as e:
|
270
|
-
print(f"An error occurred: {e}")
|
271
|
-
|
272
|
-
def play_video_in_external(line, filepath):
|
273
|
-
def move_video_when_closed(p, fp):
|
274
|
-
p.wait()
|
275
|
-
os.remove(fp)
|
276
|
-
|
277
|
-
shutil.move(filepath, get_temporary_directory())
|
278
|
-
new_filepath = os.path.join(get_temporary_directory(), os.path.basename(filepath))
|
279
|
-
|
280
|
-
command = [get_config().advanced.video_player_path]
|
281
|
-
|
282
|
-
start, _, _, _ = get_video_timings(new_filepath, line)
|
283
|
-
|
284
|
-
if start:
|
285
|
-
if "vlc" in get_config().advanced.video_player_path:
|
286
|
-
command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
|
287
|
-
else:
|
288
|
-
command.extend(["--start", convert_to_vlc_seconds(start)])
|
289
|
-
command.append(os.path.normpath(new_filepath))
|
290
|
-
|
291
|
-
logger.info(" ".join(command))
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
try:
|
296
|
-
proc = subprocess.Popen(command)
|
297
|
-
print(f"Opened {filepath} in {get_config().advanced.video_player_path}.")
|
298
|
-
threading.Thread(target=move_video_when_closed, args=(proc, filepath)).start()
|
299
|
-
except FileNotFoundError:
|
300
|
-
print("VLC not found. Make sure it's installed and in your PATH.")
|
301
|
-
except Exception as e:
|
302
|
-
print(f"An error occurred: {e}")
|
303
|
-
return new_filepath
|
304
|
-
|
305
|
-
def convert_to_vlc_seconds(time_str):
|
306
|
-
"""Converts HH:MM:SS.milliseconds to VLC-compatible seconds."""
|
307
|
-
try:
|
308
|
-
hours, minutes, seconds_ms = time_str.split(":")
|
309
|
-
seconds, milliseconds = seconds_ms.split(".")
|
310
|
-
total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + int(seconds) + (int(milliseconds) / 1000.0)
|
311
|
-
return str(total_seconds)
|
312
|
-
except ValueError:
|
313
|
-
return "Invalid time format"
|
314
|
-
|
315
201
|
def initial_checks():
|
316
202
|
try:
|
317
203
|
subprocess.run(ffmpeg.ffmpeg_base_command_list)
|
@@ -15,6 +15,8 @@ import websockets
|
|
15
15
|
from rapidfuzz import fuzz
|
16
16
|
|
17
17
|
from GameSentenceMiner import obs
|
18
|
+
from GameSentenceMiner.ocr.ss_picker import ScreenCropper
|
19
|
+
from GameSentenceMiner.owocr.owocr.run import TextFiltering
|
18
20
|
from GameSentenceMiner.util.configuration import get_config, get_app_directory, get_temporary_directory
|
19
21
|
from GameSentenceMiner.util.electron_config import get_ocr_scan_rate, get_requires_open_window
|
20
22
|
from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, set_dpi_awareness
|
@@ -71,7 +73,7 @@ def get_new_game_cords():
|
|
71
73
|
return coords_list
|
72
74
|
|
73
75
|
|
74
|
-
def get_ocr_config() -> OCRConfig:
|
76
|
+
def get_ocr_config(window=None) -> OCRConfig:
|
75
77
|
"""Loads and updates screen capture areas from the corresponding JSON file."""
|
76
78
|
app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
|
77
79
|
ocr_config_dir = app_dir / "ocr_config"
|
@@ -80,8 +82,10 @@ def get_ocr_config() -> OCRConfig:
|
|
80
82
|
scene = sanitize_filename(obs.get_current_scene())
|
81
83
|
config_path = ocr_config_dir / f"{scene}.json"
|
82
84
|
if not config_path.exists():
|
83
|
-
|
84
|
-
|
85
|
+
ocr_config = OCRConfig(scene=scene, window=window, rectangles=[], coordinate_system="")
|
86
|
+
with open(config_path, 'w', encoding="utf-8") as f:
|
87
|
+
json.dump(ocr_config.to_dict(), f, indent=4)
|
88
|
+
return ocr_config
|
85
89
|
try:
|
86
90
|
with open(config_path, 'r', encoding="utf-8") as f:
|
87
91
|
config_data = json.load(f)
|
@@ -204,13 +208,11 @@ all_cords = None
|
|
204
208
|
rectangles = None
|
205
209
|
last_ocr2_result = ""
|
206
210
|
|
207
|
-
def do_second_ocr(ocr1_text, time, img, filtering
|
211
|
+
def do_second_ocr(ocr1_text, time, img, filtering):
|
208
212
|
global twopassocr, ocr2, last_ocr2_result
|
209
213
|
try:
|
210
214
|
orig_text, text = run.process_and_write_results(img, None, last_ocr2_result, filtering, None,
|
211
215
|
engine=ocr2)
|
212
|
-
if scrolling:
|
213
|
-
return text
|
214
216
|
if fuzz.ratio(last_ocr2_result, text) >= 90:
|
215
217
|
logger.info("Seems like the same text from previous ocr2 result, not sending")
|
216
218
|
return
|
@@ -239,11 +241,10 @@ async def send_result(text, time):
|
|
239
241
|
if get_config().advanced.ocr_sends_to_clipboard or ssonly:
|
240
242
|
import pyperclip
|
241
243
|
pyperclip.copy(text)
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
logger.debug(f"Error sending text to websocket: {e}")
|
244
|
+
try:
|
245
|
+
await websocket_server_thread.send_text(text, time)
|
246
|
+
except Exception as e:
|
247
|
+
logger.debug(f"Error sending text to websocket: {e}")
|
247
248
|
|
248
249
|
|
249
250
|
previous_text_list = []
|
@@ -255,10 +256,9 @@ previous_img = None
|
|
255
256
|
previous_orig_text = "" # Store original text result
|
256
257
|
TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
|
257
258
|
force_stable = False
|
258
|
-
scrolling_text_images = []
|
259
259
|
|
260
260
|
def text_callback(text, orig_text, time, img=None, came_from_ss=False, filtering=None, crop_coords=None):
|
261
|
-
global twopassocr, ocr2, previous_text, last_oneocr_time, text_stable_start_time, previous_orig_text, previous_img, force_stable, previous_ocr1_result,
|
261
|
+
global twopassocr, ocr2, previous_text, last_oneocr_time, text_stable_start_time, previous_orig_text, previous_img, force_stable, previous_ocr1_result, previous_text_list
|
262
262
|
orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
|
263
263
|
if came_from_ss:
|
264
264
|
save_result_image(img)
|
@@ -341,8 +341,8 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
|
|
341
341
|
exclusions = list(rect.coordinates for rect in list(filter(lambda x: x.is_excluded, rectangles)))
|
342
342
|
|
343
343
|
run.init_config(False)
|
344
|
-
run.run(read_from="screencapture" if not ssonly else "clipboard",
|
345
|
-
read_from_secondary="clipboard" if not ssonly else None,
|
344
|
+
run.run(read_from="screencapture" if not ssonly else "clipboard" if ss_clipboard else "",
|
345
|
+
read_from_secondary="clipboard" if ss_clipboard and not ssonly else None,
|
346
346
|
write_to="callback",
|
347
347
|
screen_capture_area=screen_area,
|
348
348
|
# screen_capture_monitor=monitor_config['index'],
|
@@ -360,6 +360,20 @@ def run_oneocr(ocr_config: OCRConfig, rectangles):
|
|
360
360
|
done = True
|
361
361
|
|
362
362
|
|
363
|
+
|
364
|
+
def add_ss_hotkey():
|
365
|
+
import keyboard
|
366
|
+
cropper = ScreenCropper()
|
367
|
+
filtering = TextFiltering()
|
368
|
+
def capture():
|
369
|
+
print("Taking screenshot...")
|
370
|
+
img = cropper.run()
|
371
|
+
do_second_ocr("", datetime.now(), img, filtering)
|
372
|
+
|
373
|
+
keyboard.add_hotkey('ctrl+shift+g', capture)
|
374
|
+
print("Press Ctrl+Shift+G to take a screenshot.")
|
375
|
+
|
376
|
+
|
363
377
|
def get_window(window_name):
|
364
378
|
import pygetwindow as gw
|
365
379
|
try:
|
@@ -388,50 +402,48 @@ def set_force_stable_hotkey():
|
|
388
402
|
print("Press Ctrl+Shift+F to toggle force stable mode.")
|
389
403
|
|
390
404
|
if __name__ == "__main__":
|
391
|
-
global ocr1, ocr2, twopassocr, language,
|
405
|
+
global ocr1, ocr2, twopassocr, language, ss_clipboard, ss, ocr_config
|
392
406
|
import sys
|
393
407
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
twopassocr = True
|
415
|
-
|
416
|
-
ssonly = "--ssonly" in args
|
408
|
+
import argparse
|
409
|
+
|
410
|
+
parser = argparse.ArgumentParser(description="OCR Configuration")
|
411
|
+
parser.add_argument("--language", type=str, default="ja", help="Language for OCR (default: ja)")
|
412
|
+
parser.add_argument("--ocr1", type=str, default="oneocr", help="Primary OCR engine (default: oneocr)")
|
413
|
+
parser.add_argument("--ocr2", type=str, default="glens", help="Secondary OCR engine (default: glens)")
|
414
|
+
parser.add_argument("--twopassocr", type=int, choices=[0, 1], default=1, help="Enable two-pass OCR (default: 1)")
|
415
|
+
parser.add_argument("--ssonly", action="store_true", help="Use screenshot-only mode")
|
416
|
+
parser.add_argument("--clipboard", action="store_true", help="Use clipboard for input")
|
417
|
+
parser.add_argument("--window", type=str, help="Specify the window name for OCR")
|
418
|
+
|
419
|
+
args = parser.parse_args()
|
420
|
+
|
421
|
+
language = args.language
|
422
|
+
ocr1 = args.ocr1
|
423
|
+
ocr2 = args.ocr2 if args.ocr2 else None
|
424
|
+
twopassocr = bool(args.twopassocr)
|
425
|
+
ssonly = args.ssonly
|
426
|
+
ss_clipboard = args.clipboard
|
427
|
+
window_name = args.window
|
417
428
|
logger.info(f"Received arguments: ocr1={ocr1}, ocr2={ocr2}, twopassocr={twopassocr}")
|
418
429
|
# set_force_stable_hotkey()
|
419
|
-
global ocr_config
|
420
|
-
ocr_config: OCRConfig = get_ocr_config()
|
421
430
|
set_dpi_awareness()
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
431
|
+
ocr_config = None
|
432
|
+
if not ssonly:
|
433
|
+
ocr_config: OCRConfig = get_ocr_config(window=window_name)
|
434
|
+
if ocr_config:
|
435
|
+
if ocr_config.window:
|
436
|
+
start_time = time.time()
|
437
|
+
while time.time() - start_time < 30:
|
438
|
+
if get_window(ocr_config.window):
|
439
|
+
break
|
440
|
+
logger.info(f"Window: {ocr_config.window} Could not be found, retrying in 1 second...")
|
441
|
+
time.sleep(1)
|
442
|
+
else:
|
443
|
+
logger.error(f"Window '{ocr_config.window}' not found within 30 seconds.")
|
444
|
+
sys.exit(1)
|
445
|
+
logger.info(f"Starting OCR with configuration: Window: {ocr_config.window}, Rectangles: {ocr_config.rectangles}, Engine 1: {ocr1}, Engine 2: {ocr2}, Two-pass OCR: {twopassocr}")
|
446
|
+
if ssonly or ocr_config:
|
435
447
|
rectangles = ocr_config.rectangles if ocr_config and ocr_config.rectangles else []
|
436
448
|
oneocr_threads = []
|
437
449
|
ocr_thread = threading.Thread(target=run_oneocr, args=(ocr_config,rectangles ), daemon=True)
|
@@ -439,8 +451,9 @@ if __name__ == "__main__":
|
|
439
451
|
if not ssonly:
|
440
452
|
worker_thread = threading.Thread(target=process_task_queue, daemon=True)
|
441
453
|
worker_thread.start()
|
442
|
-
|
443
|
-
|
454
|
+
websocket_server_thread = WebsocketServerThread(read=True)
|
455
|
+
websocket_server_thread.start()
|
456
|
+
add_ss_hotkey()
|
444
457
|
try:
|
445
458
|
while not done:
|
446
459
|
time.sleep(1)
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import tkinter as tk
|
2
|
+
from tkinter import Canvas
|
3
|
+
from PIL import Image, ImageTk
|
4
|
+
import mss
|
5
|
+
import mss.tools
|
6
|
+
import io
|
7
|
+
import sys
|
8
|
+
import ctypes
|
9
|
+
|
10
|
+
if sys.platform == "win32":
|
11
|
+
try:
|
12
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
13
|
+
except AttributeError:
|
14
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
15
|
+
|
16
|
+
class ScreenCropper:
|
17
|
+
def __init__(self):
|
18
|
+
self.root = None
|
19
|
+
self.canvas = None
|
20
|
+
self.captured_image = None
|
21
|
+
self.tk_image = None
|
22
|
+
self.start_x = None
|
23
|
+
self.start_y = None
|
24
|
+
self.end_x = None
|
25
|
+
self.end_y = None
|
26
|
+
self.rect_id = None
|
27
|
+
self.cropped_image = None
|
28
|
+
self.monitor_geometry = None
|
29
|
+
|
30
|
+
def grab_all_monitors(self):
|
31
|
+
try:
|
32
|
+
with mss.mss() as sct:
|
33
|
+
all_monitors_bbox = sct.monitors[0]
|
34
|
+
self.monitor_geometry = {
|
35
|
+
'left': all_monitors_bbox['left'],
|
36
|
+
'top': all_monitors_bbox['top'],
|
37
|
+
'width': all_monitors_bbox['width'],
|
38
|
+
'height': all_monitors_bbox['height']
|
39
|
+
}
|
40
|
+
sct_grab = sct.grab(all_monitors_bbox)
|
41
|
+
|
42
|
+
img_bytes = mss.tools.to_png(sct_grab.rgb, sct_grab.size)
|
43
|
+
self.captured_image = Image.open(io.BytesIO(img_bytes))
|
44
|
+
|
45
|
+
print("All monitors captured successfully.")
|
46
|
+
except Exception as e:
|
47
|
+
print(f"An error occurred during screen capture: {e}")
|
48
|
+
self.captured_image = None
|
49
|
+
self.monitor_geometry = None
|
50
|
+
|
51
|
+
def _on_button_press(self, event):
|
52
|
+
self.start_x = self.end_x = event.x
|
53
|
+
self.start_y = self.end_y = event.y
|
54
|
+
|
55
|
+
if self.rect_id:
|
56
|
+
self.canvas.delete(self.rect_id)
|
57
|
+
self.rect_id = self.canvas.create_rectangle(self.start_x, self.start_y,
|
58
|
+
self.end_x, self.end_y,
|
59
|
+
outline="red", width=2)
|
60
|
+
|
61
|
+
def _on_mouse_drag(self, event):
|
62
|
+
self.end_x = event.x
|
63
|
+
self.end_y = event.y
|
64
|
+
self.canvas.coords(self.rect_id, self.start_x, self.start_y,
|
65
|
+
self.end_x, self.end_y)
|
66
|
+
|
67
|
+
def _on_button_release(self, event):
|
68
|
+
self.end_x = event.x
|
69
|
+
self.end_y = event.y
|
70
|
+
|
71
|
+
x1 = min(self.start_x, self.end_x)
|
72
|
+
y1 = min(self.start_y, self.end_y)
|
73
|
+
x2 = max(self.start_x, self.end_x)
|
74
|
+
y2 = max(self.start_y, self.end_y)
|
75
|
+
|
76
|
+
if (x2 - x1) > 0 and (y2 - y1) > 0:
|
77
|
+
self.cropped_image = self.captured_image.crop((x1, y1, x2, y2))
|
78
|
+
print(f"Selection made: ({x1}, {y1}) to ({x2}, {y2})")
|
79
|
+
else:
|
80
|
+
print("No valid selection made (area was too small).")
|
81
|
+
self.cropped_image = None
|
82
|
+
|
83
|
+
self.root.destroy()
|
84
|
+
|
85
|
+
def show_image_and_select_box(self):
|
86
|
+
if self.captured_image is None or self.monitor_geometry is None:
|
87
|
+
print("No image or monitor geometry to display. Capture all monitors first.")
|
88
|
+
return
|
89
|
+
|
90
|
+
self.root = tk.Tk()
|
91
|
+
self.root.attributes('-topmost', True)
|
92
|
+
self.root.overrideredirect(True)
|
93
|
+
|
94
|
+
window_width = self.monitor_geometry['width']
|
95
|
+
window_height = self.monitor_geometry['height']
|
96
|
+
window_x = self.monitor_geometry['left']
|
97
|
+
window_y = self.monitor_geometry['top']
|
98
|
+
|
99
|
+
self.root.geometry(f"{window_width}x{window_height}+{window_x}+{window_y}")
|
100
|
+
|
101
|
+
self.tk_image = ImageTk.PhotoImage(self.captured_image)
|
102
|
+
|
103
|
+
self.canvas = Canvas(self.root, cursor="cross", highlightthickness=0)
|
104
|
+
self.canvas.pack(fill=tk.BOTH, expand=True)
|
105
|
+
|
106
|
+
self.canvas.create_image(0, 0, anchor="nw", image=self.tk_image)
|
107
|
+
|
108
|
+
self.canvas.bind("<Button-1>", self._on_button_press)
|
109
|
+
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
|
110
|
+
self.canvas.bind("<ButtonRelease-1>", self._on_button_release)
|
111
|
+
|
112
|
+
self.root.mainloop()
|
113
|
+
|
114
|
+
def get_cropped_image(self):
|
115
|
+
return self.cropped_image
|
116
|
+
|
117
|
+
def run(self):
|
118
|
+
self.grab_all_monitors()
|
119
|
+
if self.captured_image and self.monitor_geometry:
|
120
|
+
self.show_image_and_select_box()
|
121
|
+
return self.get_cropped_image()
|
122
|
+
return None
|
123
|
+
|
124
|
+
if __name__ == "__main__":
|
125
|
+
cropper = ScreenCropper()
|
126
|
+
cropped_img = cropper.run()
|
127
|
+
|
128
|
+
if cropped_img:
|
129
|
+
print("Image cropped successfully. Displaying cropped image...")
|
130
|
+
cropped_img.show()
|
131
|
+
else:
|
132
|
+
print("No image was cropped.")
|
GameSentenceMiner/util/ffmpeg.py
CHANGED
@@ -500,6 +500,32 @@ def convert_audio_to_wav(input_audio, output_wav):
|
|
500
500
|
logger.debug(" ".join(command))
|
501
501
|
subprocess.run(command)
|
502
502
|
|
503
|
+
def convert_audio_to_wav_lossless(input_audio):
|
504
|
+
output_wav = make_unique_file_name(
|
505
|
+
os.path.join(configuration.get_temporary_directory(), "output.wav")
|
506
|
+
)
|
507
|
+
command = ffmpeg_base_command_list + [
|
508
|
+
"-i", input_audio,
|
509
|
+
output_wav
|
510
|
+
]
|
511
|
+
logger.debug(" ".join(command))
|
512
|
+
subprocess.run(command)
|
513
|
+
return output_wav
|
514
|
+
|
515
|
+
def convert_audio_to_mp3(input_audio):
|
516
|
+
output_mp3 = make_unique_file_name(
|
517
|
+
os.path.join(configuration.get_temporary_directory(), "output.mp3")
|
518
|
+
)
|
519
|
+
command = ffmpeg_base_command_list + [
|
520
|
+
"-i", input_audio,
|
521
|
+
"-codec:a", "libmp3lame",
|
522
|
+
"-qscale:a", "2", # Quality scale for MP3
|
523
|
+
output_mp3
|
524
|
+
]
|
525
|
+
logger.debug(" ".join(command))
|
526
|
+
subprocess.run(command)
|
527
|
+
return output_mp3
|
528
|
+
|
503
529
|
|
504
530
|
# Trim the audio using FFmpeg based on detected speech timestamps
|
505
531
|
def trim_audio(input_audio, start_time, end_time, output_audio):
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import subprocess
|
4
|
+
import threading
|
5
|
+
|
6
|
+
import simpleaudio as sa
|
7
|
+
|
8
|
+
from GameSentenceMiner import anki
|
9
|
+
from GameSentenceMiner.util import ffmpeg, notification
|
10
|
+
from GameSentenceMiner.util.configuration import gsm_state, logger, get_config, get_temporary_directory
|
11
|
+
from GameSentenceMiner.util.ffmpeg import get_video_timings
|
12
|
+
from GameSentenceMiner.util.text_log import GameLine
|
13
|
+
|
14
|
+
|
15
|
+
def handle_texthooker_button(video_path='', get_audio_from_video=None):
|
16
|
+
try:
|
17
|
+
if gsm_state.line_for_audio:
|
18
|
+
line: GameLine = gsm_state.line_for_audio
|
19
|
+
gsm_state.line_for_audio = None
|
20
|
+
if line == gsm_state.previous_line_for_audio:
|
21
|
+
logger.info("Line is the same as the last one, skipping processing.")
|
22
|
+
if get_config().advanced.audio_player_path:
|
23
|
+
play_audio_in_external(gsm_state.previous_audio)
|
24
|
+
elif get_config().advanced.video_player_path:
|
25
|
+
play_video_in_external(line, gsm_state.previous_audio)
|
26
|
+
else:
|
27
|
+
play_obj = gsm_state.previous_audio.play()
|
28
|
+
play_obj.wait_done()
|
29
|
+
return
|
30
|
+
gsm_state.previous_line_for_audio = line
|
31
|
+
if get_config().advanced.audio_player_path:
|
32
|
+
audio = get_audio_from_video(line, line.next.time if line.next else None, video_path,
|
33
|
+
temporary=True)
|
34
|
+
play_audio_in_external(audio)
|
35
|
+
gsm_state.previous_audio = audio
|
36
|
+
elif get_config().advanced.video_player_path:
|
37
|
+
new_video_path = play_video_in_external(line, video_path)
|
38
|
+
gsm_state.previous_audio = new_video_path
|
39
|
+
gsm_state.previous_replay = new_video_path
|
40
|
+
else:
|
41
|
+
audio = get_audio_from_video(line, line.next.time if line.next else None, video_path,
|
42
|
+
temporary=True)
|
43
|
+
wave_obj = sa.WaveObject.from_wave_file(audio)
|
44
|
+
play_obj = wave_obj.play()
|
45
|
+
play_obj.wait_done()
|
46
|
+
gsm_state.previous_audio = wave_obj
|
47
|
+
return
|
48
|
+
if gsm_state.line_for_screenshot:
|
49
|
+
line: GameLine = gsm_state.line_for_screenshot
|
50
|
+
gsm_state.line_for_screenshot = None
|
51
|
+
gsm_state.previous_line_for_screenshot = line
|
52
|
+
screenshot = ffmpeg.get_screenshot_for_line(video_path, line, True)
|
53
|
+
if gsm_state.anki_note_for_screenshot:
|
54
|
+
gsm_state.anki_note_for_screenshot = None
|
55
|
+
encoded_image = ffmpeg.process_image(screenshot)
|
56
|
+
if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
|
57
|
+
last_note = anki.get_last_anki_card()
|
58
|
+
if last_note:
|
59
|
+
anki.add_image_to_card(last_note, encoded_image)
|
60
|
+
notification.send_screenshot_updated(last_note.get_field(get_config().anki.word_field))
|
61
|
+
if get_config().features.open_anki_edit:
|
62
|
+
notification.open_anki_card(last_note.noteId)
|
63
|
+
else:
|
64
|
+
notification.send_screenshot_saved(encoded_image)
|
65
|
+
else:
|
66
|
+
notification.send_screenshot_saved(encoded_image)
|
67
|
+
else:
|
68
|
+
os.startfile(screenshot)
|
69
|
+
return
|
70
|
+
except Exception as e:
|
71
|
+
logger.error(f"Error Playing Audio/Video: {e}")
|
72
|
+
logger.debug(f"Error Playing Audio/Video: {e}", exc_info=True)
|
73
|
+
return
|
74
|
+
finally:
|
75
|
+
if video_path and get_config().paths.remove_video and os.path.exists(video_path):
|
76
|
+
os.remove(video_path)
|
77
|
+
|
78
|
+
|
79
|
+
def play_audio_in_external(filepath):
|
80
|
+
exe = get_config().advanced.audio_player_path
|
81
|
+
|
82
|
+
filepath = os.path.normpath(filepath)
|
83
|
+
|
84
|
+
command = [exe, "--no-video", filepath]
|
85
|
+
|
86
|
+
try:
|
87
|
+
subprocess.Popen(command)
|
88
|
+
print(f"Opened {filepath} in {exe}.")
|
89
|
+
except Exception as e:
|
90
|
+
print(f"An error occurred: {e}")
|
91
|
+
|
92
|
+
|
93
|
+
def play_video_in_external(line, filepath):
|
94
|
+
def move_video_when_closed(p, fp):
|
95
|
+
p.wait()
|
96
|
+
os.remove(fp)
|
97
|
+
|
98
|
+
shutil.move(filepath, get_temporary_directory())
|
99
|
+
new_filepath = os.path.join(get_temporary_directory(), os.path.basename(filepath))
|
100
|
+
|
101
|
+
command = [get_config().advanced.video_player_path]
|
102
|
+
|
103
|
+
start, _, _, _ = get_video_timings(new_filepath, line)
|
104
|
+
|
105
|
+
if start:
|
106
|
+
if "vlc" in get_config().advanced.video_player_path:
|
107
|
+
command.extend(["--start-time", convert_to_vlc_seconds(start), '--one-instance'])
|
108
|
+
else:
|
109
|
+
command.extend(["--start", convert_to_vlc_seconds(start)])
|
110
|
+
command.append(os.path.normpath(new_filepath))
|
111
|
+
|
112
|
+
logger.info(" ".join(command))
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
try:
|
117
|
+
proc = subprocess.Popen(command)
|
118
|
+
print(f"Opened {filepath} in {get_config().advanced.video_player_path}.")
|
119
|
+
threading.Thread(target=move_video_when_closed, args=(proc, filepath)).start()
|
120
|
+
except FileNotFoundError:
|
121
|
+
print("VLC not found. Make sure it's installed and in your PATH.")
|
122
|
+
except Exception as e:
|
123
|
+
print(f"An error occurred: {e}")
|
124
|
+
return new_filepath
|
125
|
+
|
126
|
+
|
127
|
+
def convert_to_vlc_seconds(time_str):
|
128
|
+
"""Converts HH:MM:SS.milliseconds to VLC-compatible seconds."""
|
129
|
+
try:
|
130
|
+
hours, minutes, seconds_ms = time_str.split(":")
|
131
|
+
seconds, milliseconds = seconds_ms.split(".")
|
132
|
+
total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + int(seconds) + (int(milliseconds) / 1000.0)
|
133
|
+
return str(total_seconds)
|
134
|
+
except ValueError:
|
135
|
+
return "Invalid time format"
|
@@ -16,6 +16,7 @@ from flask import request, jsonify, send_from_directory
|
|
16
16
|
import webbrowser
|
17
17
|
from GameSentenceMiner import obs
|
18
18
|
from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state, gsm_status
|
19
|
+
from GameSentenceMiner.web.service import handle_texthooker_button
|
19
20
|
|
20
21
|
port = get_config().general.texthooker_port
|
21
22
|
url = f"http://localhost:{port}"
|
@@ -252,6 +253,7 @@ async def add_event_to_texthooker(line: GameLine):
|
|
252
253
|
'sentence': line.text,
|
253
254
|
'data': new_event.to_serializable()
|
254
255
|
})
|
256
|
+
await plaintext_websocket_server_thread.send_text(line.text)
|
255
257
|
|
256
258
|
|
257
259
|
@app.route('/update_checkbox', methods=['POST'])
|
@@ -274,7 +276,7 @@ def get_screenshot():
|
|
274
276
|
return jsonify({'error': 'Missing id'}), 400
|
275
277
|
gsm_state.line_for_screenshot = get_line_by_id(event_id)
|
276
278
|
if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot.id == gsm_state.previous_line_for_screenshot.id:
|
277
|
-
|
279
|
+
handle_texthooker_button()
|
278
280
|
else:
|
279
281
|
obs.save_replay_buffer()
|
280
282
|
return jsonify({}), 200
|
@@ -288,7 +290,7 @@ def play_audio():
|
|
288
290
|
return jsonify({'error': 'Missing id'}), 400
|
289
291
|
gsm_state.line_for_audio = get_line_by_id(event_id)
|
290
292
|
if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio:
|
291
|
-
|
293
|
+
handle_texthooker_button()
|
292
294
|
else:
|
293
295
|
obs.save_replay_buffer()
|
294
296
|
return jsonify({}), 200
|
@@ -362,12 +364,13 @@ paused = False
|
|
362
364
|
|
363
365
|
|
364
366
|
class WebsocketServerThread(threading.Thread):
|
365
|
-
def __init__(self, read):
|
367
|
+
def __init__(self, read, ws_port):
|
366
368
|
super().__init__(daemon=True)
|
367
369
|
self._loop = None
|
368
370
|
self.read = read
|
369
371
|
self.clients = set()
|
370
372
|
self._event = threading.Event()
|
373
|
+
self.ws_port = ws_port
|
371
374
|
|
372
375
|
@property
|
373
376
|
def loop(self):
|
@@ -400,8 +403,10 @@ class WebsocketServerThread(threading.Thread):
|
|
400
403
|
|
401
404
|
async def send_text(self, text):
|
402
405
|
if text:
|
406
|
+
if isinstance(text, dict):
|
407
|
+
text = json.dumps(text)
|
403
408
|
return asyncio.run_coroutine_threadsafe(
|
404
|
-
self.send_text_coroutine(
|
409
|
+
self.send_text_coroutine(text), self.loop)
|
405
410
|
|
406
411
|
def stop_server(self):
|
407
412
|
self.loop.call_soon_threadsafe(self._stop_event.set)
|
@@ -415,7 +420,7 @@ class WebsocketServerThread(threading.Thread):
|
|
415
420
|
try:
|
416
421
|
self.server = start_server = websockets.serve(self.server_handler,
|
417
422
|
"0.0.0.0",
|
418
|
-
|
423
|
+
self.ws_port,
|
419
424
|
max_size=1000000000)
|
420
425
|
async with start_server:
|
421
426
|
await stop_event.wait()
|
@@ -432,15 +437,18 @@ def handle_exit_signal(loop):
|
|
432
437
|
task.cancel()
|
433
438
|
|
434
439
|
async def texthooker_page_coro():
|
435
|
-
global websocket_server_thread
|
440
|
+
global websocket_server_thread, plaintext_websocket_server_thread
|
436
441
|
# Run the WebSocket server in the asyncio event loop
|
437
442
|
flask_thread = threading.Thread(target=start_web_server)
|
438
443
|
flask_thread.daemon = True
|
439
444
|
flask_thread.start()
|
440
445
|
|
441
|
-
websocket_server_thread = WebsocketServerThread(read=True)
|
446
|
+
websocket_server_thread = WebsocketServerThread(read=True, ws_port=get_config().advanced.texthooker_communication_websocket_port)
|
442
447
|
websocket_server_thread.start()
|
443
448
|
|
449
|
+
plaintext_websocket_server_thread = WebsocketServerThread(read=False, ws_port=get_config().advanced.texthooker_communication_websocket_port + 1)
|
450
|
+
plaintext_websocket_server_thread.start()
|
451
|
+
|
444
452
|
# Keep the main asyncio event loop running (for the WebSocket server)
|
445
453
|
|
446
454
|
def run_text_hooker_page():
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: GameSentenceMiner
|
3
|
-
Version: 2.9.
|
3
|
+
Version: 2.9.19
|
4
4
|
Summary: A tool for mining sentences from games.
|
5
5
|
Author-email: Beangate <bpwhelan95@gmail.com>
|
6
6
|
License: MIT License
|
@@ -155,4 +155,4 @@ If you encounter issues, please ask for help in my [Discord](https://discord.gg/
|
|
155
155
|
|
156
156
|
## Donations
|
157
157
|
|
158
|
-
If you've found this or any of my other projects helpful, please consider supporting my work through [GitHub Sponsors](https://github.com/sponsors/bpwhelan)
|
158
|
+
If you've found this or any of my other projects helpful, please consider supporting my work through [GitHub Sponsors](https://github.com/sponsors/bpwhelan), [Ko-fi](https://ko-fi.com/beangate), or [Patreon](https://www.patreon.com/GameSentenceMiner).
|
@@ -1,8 +1,8 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
GameSentenceMiner/anki.py,sha256=hNHBIoJRrsWIhLe0sehOYPXTWzPREeXl4gYCPHUCaiE,16331
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=iAOLD47sQW67kzBcZKSQ0Dwctc1ngZK1lwSVIaLpQPI,83559
|
4
|
-
GameSentenceMiner/gametext.py,sha256=
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
4
|
+
GameSentenceMiner/gametext.py,sha256=nAwGMQSrmc6sUAw-OAURK2n6MG5Ecm6psJ7YF9q5KTA,6623
|
5
|
+
GameSentenceMiner/gsm.py,sha256=b3hOjaPGHn_3_G_0Ro43bX84oLX8SltfKEX_LtujX0U,24127
|
6
6
|
GameSentenceMiner/obs.py,sha256=YG8LwBf9BTsGbROm_Uq6LhFDSrbf3jgogp78rBbJq94,14728
|
7
7
|
GameSentenceMiner/vad.py,sha256=TbP3NVdjfB1TFJeB0QpOXZysgo_UHHKLdx95pYmM0JI,14902
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -19,7 +19,8 @@ GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
19
19
|
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=fEQ2o2NXksGRHpueO8c4TfAp75GEdAtAr1ngTFOsdpg,2257
|
20
20
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
21
21
|
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=71trzwz9Isyy-kN9mLS8vIX-giC8Lkin4slLXaxudac,47162
|
22
|
-
GameSentenceMiner/ocr/owocr_helper.py,sha256=
|
22
|
+
GameSentenceMiner/ocr/owocr_helper.py,sha256=7XpuLT8ezdrEXiODyXgh1X7iNbW7OI-QrSgEmTBMaqQ,19505
|
23
|
+
GameSentenceMiner/ocr/ss_picker.py,sha256=943tlCJ8536TyAFJFfRt0OGjWwe7QuT-X2UoLl1LyUY,4469
|
23
24
|
GameSentenceMiner/owocr/owocr/__init__.py,sha256=opjBOyGGyEqZCE6YdZPnyt7nVfiwyELHsXA0jAsjm14,25
|
24
25
|
GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
|
25
26
|
GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
|
@@ -30,7 +31,7 @@ GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSd
|
|
30
31
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
32
|
GameSentenceMiner/util/configuration.py,sha256=Wgr1UAf_JoXBlp9h_f3-d2DAmIWnR1FtCmMX6mCfMNM,27461
|
32
33
|
GameSentenceMiner/util/electron_config.py,sha256=3VmIrcXhC-wIMMc4uqV85NrNenRl4ZUbnQfSjWEwuig,9852
|
33
|
-
GameSentenceMiner/util/ffmpeg.py,sha256=
|
34
|
+
GameSentenceMiner/util/ffmpeg.py,sha256=7ZrgnXRa67IH7EYrc93pagkH_2rsO8-QBCDVrMWSg-Y,23398
|
34
35
|
GameSentenceMiner/util/gsm_utils.py,sha256=Z_Lu4jSIfUaM2VljIJXQkSJD0UsyJ5hMB46H2NS0gZo,8819
|
35
36
|
GameSentenceMiner/util/model.py,sha256=iDtLTfR6D-ZC0gCiDqYno6-gA6Z07PZTM4B5MAA6xZI,5704
|
36
37
|
GameSentenceMiner/util/notification.py,sha256=0OnEYjn3DUEZ6c6OtPjdVZe-DG-QSoMAl9fetjjCvNU,3874
|
@@ -45,7 +46,8 @@ GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
|
|
45
46
|
GameSentenceMiner/util/downloader/download_tools.py,sha256=mvnOjDHFlV1AbjHaNI7mdnC5_CH5k3N4n1ezqzzbzGA,8139
|
46
47
|
GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
|
47
48
|
GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
|
-
GameSentenceMiner/web/
|
49
|
+
GameSentenceMiner/web/service.py,sha256=paRUbmdh6msANHplFX-UAyYWfmYL43ltJJhXeuXXr30,5658
|
50
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=v4fh79uAO9jM-ZKoLAlLOBaMTn02GfNGAOuxZUXfs-k,15536
|
49
51
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
52
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
51
53
|
GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
|
@@ -59,9 +61,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
59
61
|
GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
|
60
62
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
61
63
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
62
|
-
gamesentenceminer-2.9.
|
63
|
-
gamesentenceminer-2.9.
|
64
|
-
gamesentenceminer-2.9.
|
65
|
-
gamesentenceminer-2.9.
|
66
|
-
gamesentenceminer-2.9.
|
67
|
-
gamesentenceminer-2.9.
|
64
|
+
gamesentenceminer-2.9.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
65
|
+
gamesentenceminer-2.9.19.dist-info/METADATA,sha256=YuvVpsZ-0To0Ig35-sehLVRH_uIW_vjwMbVvCNPpMQM,7276
|
66
|
+
gamesentenceminer-2.9.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
67
|
+
gamesentenceminer-2.9.19.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
68
|
+
gamesentenceminer-2.9.19.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
69
|
+
gamesentenceminer-2.9.19.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|