GameSentenceMiner 2.13.9__py3-none-any.whl → 2.13.11__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/gsm.py +12 -14
- GameSentenceMiner/ocr/gsm_ocr_config.py +3 -1
- GameSentenceMiner/ocr/owocr_area_selector.py +31 -16
- GameSentenceMiner/ocr/owocr_helper.py +14 -2
- GameSentenceMiner/owocr/owocr/run.py +3 -15
- GameSentenceMiner/util/configuration.py +1 -1
- GameSentenceMiner/util/ffmpeg.py +12 -12
- GameSentenceMiner/web/service.py +6 -1
- GameSentenceMiner/web/texthooking_page.py +60 -18
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/RECORD +15 -15
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.13.9.dist-info → gamesentenceminer-2.13.11.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gsm.py
CHANGED
@@ -53,7 +53,7 @@ try:
|
|
53
53
|
from GameSentenceMiner.util.text_log import GameLine, get_text_event, get_mined_line, get_all_lines, game_log
|
54
54
|
from GameSentenceMiner.util import *
|
55
55
|
from GameSentenceMiner.web import texthooking_page
|
56
|
-
from GameSentenceMiner.web.service import handle_texthooker_button
|
56
|
+
from GameSentenceMiner.web.service import handle_texthooker_button, set_get_audio_from_video_callback
|
57
57
|
from GameSentenceMiner.web.texthooking_page import run_text_hooker_page
|
58
58
|
except Exception as e:
|
59
59
|
from GameSentenceMiner.util.configuration import logger, is_linux, is_windows
|
@@ -91,10 +91,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
91
91
|
selected_lines = []
|
92
92
|
anki_card_creation_time = None
|
93
93
|
mined_line = None
|
94
|
-
gsm_state.previous_replay = video_path
|
95
94
|
if gsm_state.line_for_audio or gsm_state.line_for_screenshot:
|
96
|
-
handle_texthooker_button(
|
97
|
-
video_path, get_audio_from_video=VideoToAudioHandler.get_audio)
|
95
|
+
handle_texthooker_button(video_path)
|
98
96
|
return
|
99
97
|
try:
|
100
98
|
if anki.card_queue and len(anki.card_queue) > 0:
|
@@ -201,16 +199,14 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
201
199
|
logger.debug(
|
202
200
|
f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
|
203
201
|
notification.send_error_no_anki_update()
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
f"Error removing video file {video_path}: {e}", exc_info=True)
|
213
|
-
pass
|
202
|
+
if get_config().paths.remove_video and video_path and not skip_delete:
|
203
|
+
try:
|
204
|
+
if os.path.exists(video_path):
|
205
|
+
logger.debug(f"Removing video: {video_path}")
|
206
|
+
os.remove(video_path)
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(
|
209
|
+
f"Error removing video file {video_path}: {e}", exc_info=True)
|
214
210
|
|
215
211
|
@staticmethod
|
216
212
|
def get_audio(game_line, next_line_time, video_path, anki_card_creation_time=None, temporary=False, timing_only=False, mined_line=None):
|
@@ -525,6 +521,7 @@ def handle_exit():
|
|
525
521
|
def initialize(reloading=False):
|
526
522
|
global obs_process
|
527
523
|
if not reloading:
|
524
|
+
get_temporary_directory(delete=True)
|
528
525
|
if is_windows():
|
529
526
|
download_obs_if_needed()
|
530
527
|
download_ffmpeg_if_needed()
|
@@ -538,6 +535,7 @@ def initialize(reloading=False):
|
|
538
535
|
# gametext.start_text_monitor()
|
539
536
|
os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
|
540
537
|
os.makedirs(get_config().paths.output_folder, exist_ok=True)
|
538
|
+
set_get_audio_from_video_callback(VideoToAudioHandler.get_audio)
|
541
539
|
initial_checks()
|
542
540
|
register_websocket_message_handler(handle_websocket_message)
|
543
541
|
# if get_config().vad.do_vad_postprocessing:
|
@@ -33,6 +33,7 @@ class Rectangle:
|
|
33
33
|
monitor: Monitor
|
34
34
|
coordinates: List[Union[float, int]]
|
35
35
|
is_excluded: bool
|
36
|
+
is_secondary: bool = False
|
36
37
|
|
37
38
|
@dataclass_json
|
38
39
|
@dataclass
|
@@ -41,7 +42,8 @@ class WindowGeometry:
|
|
41
42
|
top: int
|
42
43
|
width: int
|
43
44
|
height: int
|
44
|
-
|
45
|
+
|
46
|
+
|
45
47
|
@dataclass_json
|
46
48
|
@dataclass
|
47
49
|
class OCRConfig:
|
@@ -34,7 +34,7 @@ class ScreenSelector:
|
|
34
34
|
def __init__(self, result, window_name, use_window_as_config, use_obs_screenshot=False):
|
35
35
|
if not selector_available:
|
36
36
|
raise RuntimeError("tkinter is not available.")
|
37
|
-
if not window_name:
|
37
|
+
if not window_name and not use_obs_screenshot:
|
38
38
|
raise ValueError("A target window name is required for configuration.")
|
39
39
|
|
40
40
|
obs.connect_to_obs_sync()
|
@@ -185,7 +185,7 @@ class ScreenSelector:
|
|
185
185
|
monitor_index = rect_data["monitor"]['index']
|
186
186
|
target_monitor = next((m for m in self.monitors if m['index'] == monitor_index), None)
|
187
187
|
if target_monitor:
|
188
|
-
self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"]))
|
188
|
+
self.rectangles.append((target_monitor, abs_coords, rect_data["is_excluded"], rect_data.get("is_secondary", False)))
|
189
189
|
loaded_count += 1
|
190
190
|
except (KeyError, ValueError, TypeError) as e:
|
191
191
|
print(f"Skipping malformed rectangle data: {rect_data}, Error: {e}")
|
@@ -204,7 +204,7 @@ class ScreenSelector:
|
|
204
204
|
print(f"Saving rectangles to: {config_path} relative to window: {win_geom}")
|
205
205
|
|
206
206
|
serializable_rects = []
|
207
|
-
for monitor_dict, abs_coords, is_excluded in self.rectangles:
|
207
|
+
for monitor_dict, abs_coords, is_excluded, is_secondary in self.rectangles:
|
208
208
|
x_abs, y_abs, w_abs, h_abs = abs_coords
|
209
209
|
|
210
210
|
# Convert absolute pixel coordinates to percentages
|
@@ -217,7 +217,8 @@ class ScreenSelector:
|
|
217
217
|
serializable_rects.append({
|
218
218
|
"monitor": {'index': monitor_dict['index']},
|
219
219
|
"coordinates": coords_to_save,
|
220
|
-
"is_excluded": is_excluded
|
220
|
+
"is_excluded": is_excluded,
|
221
|
+
"is_secondary": is_secondary
|
221
222
|
})
|
222
223
|
|
223
224
|
save_data = {
|
@@ -254,13 +255,14 @@ class ScreenSelector:
|
|
254
255
|
|
255
256
|
def redo_last_rect(self, event=None):
|
256
257
|
if not self.redo_stack: return
|
257
|
-
monitor, abs_coords, is_excluded, old_rect_id = self.redo_stack.pop()
|
258
|
+
monitor, abs_coords, is_excluded, is_secondary, old_rect_id = self.redo_stack.pop()
|
258
259
|
canvas = event.widget.winfo_toplevel().winfo_children()[0]
|
259
260
|
x_abs, y_abs, w_abs, h_abs = abs_coords
|
260
261
|
canvas_x, canvas_y = x_abs - self.bounding_box['left'], y_abs - self.bounding_box['top']
|
262
|
+
outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
|
261
263
|
new_rect_id = canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
262
|
-
outline=
|
263
|
-
self.rectangles.append((monitor, abs_coords, is_excluded))
|
264
|
+
outline=outline_color, width=2)
|
265
|
+
self.rectangles.append((monitor, abs_coords, is_excluded, is_secondary))
|
264
266
|
self.drawn_rect_ids.append(new_rect_id)
|
265
267
|
print("Redo: Restored rectangle.")
|
266
268
|
|
@@ -291,6 +293,7 @@ class ScreenSelector:
|
|
291
293
|
"How to Use:\n"
|
292
294
|
"• Left Click + Drag: Create a capture area (green).\n"
|
293
295
|
"• Shift + Left Click + Drag: Create an exclusion area (orange).\n"
|
296
|
+
"• Ctrl + Left Click + Drag: Create a secondary (menu) area (purple).\n"
|
294
297
|
"• Right-Click on a box: Delete it."
|
295
298
|
)
|
296
299
|
tk.Label(main_frame, text=instructions_text, justify=tk.LEFT, anchor="w").pack(pady=(0, 10), fill=tk.X)
|
@@ -426,17 +429,25 @@ class ScreenSelector:
|
|
426
429
|
# --- END MODIFICATION ---
|
427
430
|
|
428
431
|
# Draw existing rectangles (which were converted to absolute pixels on load)
|
429
|
-
for _, abs_coords, is_excluded in self.rectangles:
|
432
|
+
for _, abs_coords, is_excluded, is_secondary in self.rectangles:
|
430
433
|
x_abs, y_abs, w_abs, h_abs = abs_coords
|
431
434
|
canvas_x = x_abs - self.bounding_box['left']
|
432
435
|
canvas_y = y_abs - self.bounding_box['top']
|
436
|
+
outline_color = 'purple' if is_secondary else ('orange' if is_excluded else 'green')
|
433
437
|
rect_id = self.canvas.create_rectangle(canvas_x, canvas_y, canvas_x + w_abs, canvas_y + h_abs,
|
434
|
-
outline=
|
438
|
+
outline=outline_color, width=2)
|
435
439
|
self.drawn_rect_ids.append(rect_id)
|
436
440
|
|
437
441
|
def on_click(event):
|
438
442
|
self.start_x, self.start_y = event.x, event.y
|
439
|
-
|
443
|
+
ctrl_held = bool(event.state & 0x0004)
|
444
|
+
shift_held = bool(event.state & 0x0001)
|
445
|
+
if ctrl_held:
|
446
|
+
outline = 'purple'
|
447
|
+
elif shift_held:
|
448
|
+
outline = 'orange'
|
449
|
+
else:
|
450
|
+
outline = 'green'
|
440
451
|
self.current_rect_id = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y,
|
441
452
|
outline=outline, width=2)
|
442
453
|
|
@@ -451,8 +462,12 @@ class ScreenSelector:
|
|
451
462
|
w, h = int(abs(coords[2] - coords[0])), int(abs(coords[3] - coords[1]))
|
452
463
|
|
453
464
|
if w >= MIN_RECT_WIDTH and h >= MIN_RECT_HEIGHT:
|
454
|
-
|
455
|
-
|
465
|
+
ctrl_held = bool(event.state & 0x0004)
|
466
|
+
shift_held = bool(event.state & 0x0001)
|
467
|
+
is_excl = shift_held
|
468
|
+
is_secondary = ctrl_held
|
469
|
+
outline_color = 'purple' if is_secondary else ('orange' if is_excl else 'green')
|
470
|
+
self.canvas.itemconfig(self.current_rect_id, outline=outline_color)
|
456
471
|
|
457
472
|
center_x, center_y = x_abs + w / 2, y_abs + h / 2
|
458
473
|
target_mon = self.monitors[0]
|
@@ -462,7 +477,7 @@ class ScreenSelector:
|
|
462
477
|
target_mon = mon
|
463
478
|
break
|
464
479
|
|
465
|
-
self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl))
|
480
|
+
self.rectangles.append((target_mon, (x_abs, y_abs, w, h), is_excl, is_secondary))
|
466
481
|
self.drawn_rect_ids.append(self.current_rect_id)
|
467
482
|
self.redo_stack.clear()
|
468
483
|
else:
|
@@ -472,7 +487,7 @@ class ScreenSelector:
|
|
472
487
|
def on_right_click(event):
|
473
488
|
# Iterate through our rectangles in reverse to find the topmost one.
|
474
489
|
for i in range(len(self.rectangles) - 1, -1, -1):
|
475
|
-
_monitor, abs_coords, _is_excluded = self.rectangles[i]
|
490
|
+
_monitor, abs_coords, _is_excluded, _is_secondary = self.rectangles[i]
|
476
491
|
x_abs, y_abs, w_abs, h_abs = abs_coords
|
477
492
|
canvas_x1 = x_abs - self.bounding_box['left']
|
478
493
|
canvas_y1 = y_abs - self.bounding_box['top']
|
@@ -540,7 +555,7 @@ def run_screen_selector(result_dict, window_name, use_window_as_config, use_obs_
|
|
540
555
|
|
541
556
|
def get_screen_selection(window_name, use_window_as_config=False, use_obs_screenshot=False):
|
542
557
|
if not selector_available: return None
|
543
|
-
if not window_name:
|
558
|
+
if not window_name and not use_obs_screenshot:
|
544
559
|
print("Error: A target window name must be provided.", file=sys.stderr)
|
545
560
|
return None
|
546
561
|
|
@@ -567,7 +582,7 @@ if __name__ == "__main__":
|
|
567
582
|
|
568
583
|
parser = argparse.ArgumentParser(description="Screen Selector Arguments")
|
569
584
|
parser.add_argument("window_title", nargs="?", default="", help="Target window title")
|
570
|
-
parser.add_argument("--obs_ocr", action="store_true", help="Use OBS screenshot")
|
585
|
+
parser.add_argument("--obs_ocr", action="store_true", default=True, help="Use OBS screenshot")
|
571
586
|
parser.add_argument("--use_window_for_config", action="store_true", help="Use window for config")
|
572
587
|
args = parser.parse_args()
|
573
588
|
|
@@ -52,7 +52,6 @@ logger.addHandler(console_handler)
|
|
52
52
|
def get_ocr_config(window=None, use_window_for_config=False) -> OCRConfig:
|
53
53
|
"""Loads and updates screen capture areas from the corresponding JSON file."""
|
54
54
|
ocr_config_dir = get_ocr_config_path()
|
55
|
-
obs.connect_to_obs_sync(retry=0)
|
56
55
|
obs.update_current_game()
|
57
56
|
if use_window_for_config and window:
|
58
57
|
scene = sanitize_filename(window)
|
@@ -202,7 +201,7 @@ def do_second_ocr(ocr1_text, time, img, filtering, pre_crop_image=None, ignore_f
|
|
202
201
|
try:
|
203
202
|
orig_text, text = run.process_and_write_results(img, None, last_ocr2_result if not ignore_previous_result else None, filtering, None,
|
204
203
|
engine=get_ocr_ocr2(), furigana_filter_sensitivity=furigana_filter_sensitivity if not ignore_furigana_filter else 0)
|
205
|
-
|
204
|
+
|
206
205
|
if compare_ocr_results(last_sent_result, text, threshold=80):
|
207
206
|
if text:
|
208
207
|
logger.info("Seems like Text we already sent, not doing anything.")
|
@@ -447,6 +446,17 @@ def run_oneocr(ocr_config: OCRConfig, rectangles, config_check_thread):
|
|
447
446
|
|
448
447
|
def add_ss_hotkey(ss_hotkey="ctrl+shift+g"):
|
449
448
|
import keyboard
|
449
|
+
|
450
|
+
def ocr_secondary_rectangles():
|
451
|
+
logger.info("Running secondary OCR rectangles...")
|
452
|
+
ocr_config = get_ocr_config()
|
453
|
+
img = obs.get_screenshot_PIL(compression=80, img_format="jpg")
|
454
|
+
ocr_config.scale_to_custom_size(img.width, img.height)
|
455
|
+
img = run.apply_ocr_config_to_image(img, ocr_config, is_secondary=True)
|
456
|
+
do_second_ocr("", datetime.now(), img, TextFiltering(lang=get_ocr_language()), ignore_furigana_filter=True, ignore_previous_result=True)
|
457
|
+
|
458
|
+
if not manual:
|
459
|
+
keyboard.add_hotkey(get_ocr_manual_ocr_hotkey().lower(), ocr_secondary_rectangles)
|
450
460
|
secret_ss_hotkey = "F14"
|
451
461
|
filtering = TextFiltering(lang=get_ocr_language())
|
452
462
|
cropper = ScreenCropper()
|
@@ -544,6 +554,8 @@ if __name__ == "__main__":
|
|
544
554
|
use_window_for_config = args.use_window_for_config
|
545
555
|
keep_newline = args.keep_newline
|
546
556
|
obs_ocr = args.obs_ocr
|
557
|
+
|
558
|
+
obs.connect_to_obs_sync(retry=0)
|
547
559
|
|
548
560
|
# Start config change checker thread
|
549
561
|
config_check_thread = ConfigChangeCheckThread()
|
@@ -832,7 +832,6 @@ class OBSScreenshotThread(threading.Thread):
|
|
832
832
|
super().__init__(daemon=True)
|
833
833
|
self.ocr_config = ocr_config
|
834
834
|
self.interval = interval
|
835
|
-
self.obs_client = None
|
836
835
|
self.websocket = None
|
837
836
|
self.current_source = None
|
838
837
|
self.current_source_name = None
|
@@ -930,16 +929,6 @@ class OBSScreenshotThread(threading.Thread):
|
|
930
929
|
init_config()
|
931
930
|
start = time.time()
|
932
931
|
while not terminated:
|
933
|
-
if time.time() - start > 5:
|
934
|
-
if not self.obs_client:
|
935
|
-
self.connect_obs()
|
936
|
-
else:
|
937
|
-
try:
|
938
|
-
self.obs_client.get_version()
|
939
|
-
except Exception as e:
|
940
|
-
logger.error(f"Lost connection to OBS: {e}")
|
941
|
-
self.obs_client = None
|
942
|
-
self.connect_obs()
|
943
932
|
if not screenshot_event.wait(timeout=0.1):
|
944
933
|
continue
|
945
934
|
|
@@ -990,15 +979,14 @@ class OBSScreenshotThread(threading.Thread):
|
|
990
979
|
continue
|
991
980
|
|
992
981
|
|
993
|
-
def apply_ocr_config_to_image(img, ocr_config):
|
982
|
+
def apply_ocr_config_to_image(img, ocr_config, is_secondary=False):
|
994
983
|
for rectangle in ocr_config.rectangles:
|
995
984
|
if rectangle.is_excluded:
|
996
985
|
left, top, width, height = rectangle.coordinates
|
997
986
|
draw = ImageDraw.Draw(img)
|
998
|
-
draw.rectangle((left, top, left + width, top +
|
999
|
-
height), fill=(0, 0, 0, 0))
|
987
|
+
draw.rectangle((left, top, left + width, top + height), fill=(0, 0, 0, 0))
|
1000
988
|
|
1001
|
-
rectangles = [r for r in ocr_config.rectangles if not r.is_excluded]
|
989
|
+
rectangles = [r for r in ocr_config.rectangles if not r.is_excluded and r.is_secondary == is_secondary]
|
1002
990
|
|
1003
991
|
# Sort top to bottom
|
1004
992
|
if rectangles:
|
@@ -843,7 +843,7 @@ def get_log_path():
|
|
843
843
|
|
844
844
|
temp_directory = ''
|
845
845
|
|
846
|
-
def get_temporary_directory(delete=
|
846
|
+
def get_temporary_directory(delete=False):
|
847
847
|
global temp_directory
|
848
848
|
if not temp_directory:
|
849
849
|
temp_directory = os.path.join(get_app_directory(), 'temp')
|
GameSentenceMiner/util/ffmpeg.py
CHANGED
@@ -117,7 +117,7 @@ def get_screenshot(video_file, screenshot_timing, try_selector=False):
|
|
117
117
|
get_temporary_directory(),
|
118
118
|
f"{obs.get_current_game(sanitize=True)}.png"))
|
119
119
|
ffmpeg_command = ffmpeg_base_command_list + [
|
120
|
-
"-ss", f"{screenshot_timing}",
|
120
|
+
"-ss", f"{screenshot_timing}",
|
121
121
|
"-i", video_file,
|
122
122
|
"-vframes", "1",
|
123
123
|
output_image
|
@@ -222,7 +222,7 @@ def process_image(image_file):
|
|
222
222
|
break
|
223
223
|
except Exception as e:
|
224
224
|
logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
|
225
|
-
output_image = make_unique_file_name(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.png")
|
225
|
+
output_image = make_unique_file_name(os.path.join(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.png"))
|
226
226
|
shutil.move(image_file, output_image)
|
227
227
|
|
228
228
|
logger.info(f"Processed image saved to: {output_image}")
|
@@ -303,21 +303,21 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
|
|
303
303
|
suffix=f".{get_config().audio.extension}").name
|
304
304
|
start_trim_time, total_seconds, total_seconds_after_offset, file_length = get_video_timings(video_path, game_line, anki_card_creation_time)
|
305
305
|
end_trim_time = 0
|
306
|
+
end_trim_seconds = 0
|
306
307
|
|
307
308
|
ffmpeg_command = ffmpeg_base_command_list + [
|
308
309
|
"-i", untrimmed_audio,
|
309
310
|
"-ss", str(start_trim_time)]
|
310
311
|
if next_line and next_line > game_line.time:
|
311
|
-
|
312
|
-
end_trim_time = f"{
|
312
|
+
end_trim_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
|
313
|
+
end_trim_time = f"{end_trim_seconds:.3f}"
|
313
314
|
ffmpeg_command.extend(['-to', end_trim_time])
|
314
315
|
logger.debug(
|
315
316
|
f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time} seconds")
|
316
317
|
elif get_config().audio.pre_vad_end_offset and get_config().audio.pre_vad_end_offset < 0:
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
logger.debug(f"Trimming end of audio to {end_trim_time} seconds due to pre-vad end offset")
|
318
|
+
end_trim_seconds = file_length + get_config().audio.pre_vad_end_offset
|
319
|
+
ffmpeg_command.extend(['-to', str(end_trim_seconds)])
|
320
|
+
logger.debug(f"Trimming end of audio to {end_trim_seconds} seconds due to pre-vad end offset")
|
321
321
|
|
322
322
|
ffmpeg_command.extend([
|
323
323
|
"-c", "copy", # Using copy to avoid re-encoding, adjust if needed
|
@@ -326,18 +326,18 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
|
|
326
326
|
|
327
327
|
logger.debug(" ".join(ffmpeg_command))
|
328
328
|
subprocess.run(ffmpeg_command)
|
329
|
-
gsm_state.previous_trim_args = (untrimmed_audio, start_trim_time,
|
329
|
+
gsm_state.previous_trim_args = (untrimmed_audio, start_trim_time, end_trim_seconds)
|
330
330
|
|
331
331
|
logger.debug(f"{total_seconds_after_offset} trimmed off of beginning")
|
332
332
|
|
333
|
-
if
|
334
|
-
logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {
|
333
|
+
if end_trim_seconds:
|
334
|
+
logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {end_trim_seconds} seconds")
|
335
335
|
else:
|
336
336
|
logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds")
|
337
337
|
|
338
338
|
|
339
339
|
logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
|
340
|
-
return trimmed_audio, start_trim_time,
|
340
|
+
return trimmed_audio, start_trim_time, end_trim_seconds
|
341
341
|
|
342
342
|
def get_video_timings(video_path, game_line, anki_card_creation_time=None):
|
343
343
|
if anki_card_creation_time:
|
GameSentenceMiner/web/service.py
CHANGED
@@ -11,7 +11,12 @@ from GameSentenceMiner.util.ffmpeg import get_video_timings
|
|
11
11
|
from GameSentenceMiner.util.text_log import GameLine
|
12
12
|
|
13
13
|
|
14
|
-
def
|
14
|
+
def set_get_audio_from_video_callback(func):
|
15
|
+
global get_audio_from_video
|
16
|
+
get_audio_from_video = func
|
17
|
+
|
18
|
+
|
19
|
+
def handle_texthooker_button(video_path=''):
|
15
20
|
try:
|
16
21
|
if gsm_state.line_for_audio:
|
17
22
|
line: GameLine = gsm_state.line_for_audio
|
@@ -52,6 +52,7 @@ class EventItem:
|
|
52
52
|
'history': self.history,
|
53
53
|
}
|
54
54
|
|
55
|
+
|
55
56
|
class EventManager:
|
56
57
|
events: list[EventItem]
|
57
58
|
events_dict: dict[str, EventItem] = {}
|
@@ -87,7 +88,8 @@ class EventManager:
|
|
87
88
|
event_id, line_id, text, timestamp = row
|
88
89
|
timestamp = datetime.datetime.fromisoformat(timestamp)
|
89
90
|
line = GameLine(line_id, text, timestamp, None, None, 0)
|
90
|
-
event = EventItem(line, event_id, text, timestamp,
|
91
|
+
event = EventItem(line, event_id, text, timestamp,
|
92
|
+
False, timestamp < initial_time)
|
91
93
|
self.events.append(event)
|
92
94
|
self.ids.append(event_id)
|
93
95
|
self.events_dict[event_id] = event
|
@@ -99,7 +101,8 @@ class EventManager:
|
|
99
101
|
self.events = new_events
|
100
102
|
|
101
103
|
def add_gameline(self, line: GameLine):
|
102
|
-
new_event = EventItem(line, line.id, line.text,
|
104
|
+
new_event = EventItem(line, line.id, line.text,
|
105
|
+
line.time, False, False)
|
103
106
|
self.events_dict[line.id] = new_event
|
104
107
|
self.ids.append(line.id)
|
105
108
|
self.events.append(new_event)
|
@@ -130,12 +133,16 @@ class EventManager:
|
|
130
133
|
self.conn.close()
|
131
134
|
|
132
135
|
def clear_history(self):
|
133
|
-
self.cursor.execute("DELETE FROM events WHERE time < ?",
|
136
|
+
self.cursor.execute("DELETE FROM events WHERE time < ?",
|
137
|
+
(initial_time.isoformat(),))
|
134
138
|
logger.info(f"Cleared history before {initial_time.isoformat()}")
|
135
139
|
self.conn.commit()
|
136
140
|
# Clear the in-memory events as well
|
137
|
-
event_manager.events = [
|
138
|
-
|
141
|
+
event_manager.events = [
|
142
|
+
event for event in event_manager if not event.history]
|
143
|
+
event_manager.events_dict = {
|
144
|
+
event.id: event for event in event_manager.events}
|
145
|
+
|
139
146
|
|
140
147
|
class EventProcessor(threading.Thread):
|
141
148
|
def __init__(self, event_queue, db_path):
|
@@ -173,6 +180,7 @@ class EventProcessor(threading.Thread):
|
|
173
180
|
if self.conn:
|
174
181
|
self.conn.close()
|
175
182
|
|
183
|
+
|
176
184
|
event_manager = EventManager()
|
177
185
|
event_queue = queue.Queue()
|
178
186
|
|
@@ -185,6 +193,8 @@ server_start_time = datetime.datetime.now().timestamp()
|
|
185
193
|
app = flask.Flask(__name__)
|
186
194
|
|
187
195
|
# Load data from the JSON file
|
196
|
+
|
197
|
+
|
188
198
|
def load_data_from_file():
|
189
199
|
if os.path.exists(TEXT_REPLACEMENTS_FILE):
|
190
200
|
with open(TEXT_REPLACEMENTS_FILE, 'r', encoding='utf-8') as file:
|
@@ -192,10 +202,13 @@ def load_data_from_file():
|
|
192
202
|
return {"enabled": True, "args": {"replacements": {}}}
|
193
203
|
|
194
204
|
# Save data to the JSON file
|
205
|
+
|
206
|
+
|
195
207
|
def save_data_to_file(data):
|
196
208
|
with open(TEXT_REPLACEMENTS_FILE, 'w', encoding='utf-8') as file:
|
197
209
|
json.dump(data, file, indent=4, ensure_ascii=False)
|
198
210
|
|
211
|
+
|
199
212
|
@app.route('/load-data', methods=['GET'])
|
200
213
|
def load_data():
|
201
214
|
try:
|
@@ -204,6 +217,7 @@ def load_data():
|
|
204
217
|
except Exception as e:
|
205
218
|
return jsonify({"error": f"Failed to load data: {str(e)}"}), 500
|
206
219
|
|
220
|
+
|
207
221
|
@app.route('/save-data', methods=['POST'])
|
208
222
|
def save_data():
|
209
223
|
try:
|
@@ -217,40 +231,49 @@ def save_data():
|
|
217
231
|
except Exception as e:
|
218
232
|
return jsonify({"error": f"Failed to save data: {str(e)}"}), 500
|
219
233
|
|
234
|
+
|
220
235
|
def inject_server_start_time(html_content, timestamp):
|
221
236
|
placeholder = '<script>'
|
222
237
|
replacement = f'<script>const serverStartTime = {timestamp};'
|
223
238
|
return html_content.replace(placeholder, replacement)
|
224
239
|
|
240
|
+
|
225
241
|
@app.route('/favicon.ico')
|
226
242
|
def favicon():
|
227
243
|
return send_from_directory(os.path.join(app.root_path, 'static'),
|
228
244
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
229
245
|
|
246
|
+
|
230
247
|
@app.route('/<path:filename>')
|
231
248
|
def serve_static(filename):
|
232
249
|
return send_from_directory('pages', filename)
|
233
250
|
|
251
|
+
|
234
252
|
@app.route('/')
|
235
253
|
def index():
|
236
254
|
return send_from_directory('templates', 'index.html')
|
237
255
|
|
256
|
+
|
238
257
|
@app.route('/texthooker')
|
239
258
|
def texthooker():
|
240
259
|
return send_from_directory('templates', 'index.html')
|
241
260
|
|
261
|
+
|
242
262
|
@app.route('/textreplacements')
|
243
263
|
def textreplacements():
|
244
264
|
return flask.render_template('text_replacements.html')
|
245
265
|
|
266
|
+
|
246
267
|
@app.route('/data', methods=['GET'])
|
247
268
|
def get_data():
|
248
269
|
return jsonify([event.to_dict() for event in event_manager])
|
249
270
|
|
271
|
+
|
250
272
|
@app.route('/get_ids', methods=['GET'])
|
251
273
|
def get_ids():
|
252
274
|
return jsonify(event_manager.get_ids())
|
253
275
|
|
276
|
+
|
254
277
|
@app.route('/clear_history', methods=['POST'])
|
255
278
|
def clear_history():
|
256
279
|
temp_em = EventManager()
|
@@ -268,8 +291,8 @@ async def add_event_to_texthooker(line: GameLine):
|
|
268
291
|
})
|
269
292
|
if get_config().advanced.plaintext_websocket_port:
|
270
293
|
await plaintext_websocket_server_thread.send_text(line.text)
|
271
|
-
|
272
|
-
|
294
|
+
|
295
|
+
|
273
296
|
async def send_word_coordinates_to_overlay(boxes):
|
274
297
|
if boxes and len(boxes) > 0 and overlay_server_thread:
|
275
298
|
await overlay_server_thread.send_text(boxes)
|
@@ -286,6 +309,7 @@ def update_event():
|
|
286
309
|
event_manager.get(event_id).checked = not event.checked
|
287
310
|
return jsonify({'message': 'Event updated successfully'}), 200
|
288
311
|
|
312
|
+
|
289
313
|
@app.route('/get-screenshot', methods=['Post'])
|
290
314
|
def get_screenshot():
|
291
315
|
"""Endpoint to get a screenshot of the current game screen."""
|
@@ -294,12 +318,13 @@ def get_screenshot():
|
|
294
318
|
if event_id is None:
|
295
319
|
return jsonify({'error': 'Missing id'}), 400
|
296
320
|
gsm_state.line_for_screenshot = get_line_by_id(event_id)
|
297
|
-
if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot
|
321
|
+
if gsm_state.previous_line_for_screenshot and gsm_state.line_for_screenshot == gsm_state.previous_line_for_screenshot or gsm_state.previous_line_for_audio and gsm_state.line_for_screenshot == gsm_state.previous_line_for_audio:
|
298
322
|
handle_texthooker_button(gsm_state.previous_replay)
|
299
323
|
else:
|
300
324
|
obs.save_replay_buffer()
|
301
325
|
return jsonify({}), 200
|
302
326
|
|
327
|
+
|
303
328
|
@app.route('/play-audio', methods=['POST'])
|
304
329
|
def play_audio():
|
305
330
|
"""Endpoint to play audio for a specific event."""
|
@@ -307,13 +332,16 @@ def play_audio():
|
|
307
332
|
event_id = data.get('id')
|
308
333
|
if event_id is None:
|
309
334
|
return jsonify({'error': 'Missing id'}), 400
|
335
|
+
print(f"Playing audio for event ID: {event_id}")
|
310
336
|
gsm_state.line_for_audio = get_line_by_id(event_id)
|
311
|
-
|
337
|
+
print(f"gsm_state.line_for_audio: {gsm_state.line_for_audio}")
|
338
|
+
if gsm_state.previous_line_for_audio and gsm_state.line_for_audio == gsm_state.previous_line_for_audio or gsm_state.previous_line_for_screenshot and gsm_state.line_for_audio == gsm_state.previous_line_for_screenshot:
|
312
339
|
handle_texthooker_button(gsm_state.previous_replay)
|
313
340
|
else:
|
314
341
|
obs.save_replay_buffer()
|
315
342
|
return jsonify({}), 200
|
316
343
|
|
344
|
+
|
317
345
|
@app.route("/translate-line", methods=['POST'])
|
318
346
|
def translate_line():
|
319
347
|
data = request.get_json()
|
@@ -362,9 +390,11 @@ def get_status():
|
|
362
390
|
def get_selected_lines():
|
363
391
|
return [item.line for item in event_manager if item.checked]
|
364
392
|
|
393
|
+
|
365
394
|
def are_lines_selected():
|
366
395
|
return any(item.checked for item in event_manager)
|
367
396
|
|
397
|
+
|
368
398
|
def reset_checked_lines():
|
369
399
|
async def send_reset_message():
|
370
400
|
await websocket_server_thread.send_text({
|
@@ -373,9 +403,11 @@ def reset_checked_lines():
|
|
373
403
|
event_manager.reset_checked_lines()
|
374
404
|
asyncio.run(send_reset_message())
|
375
405
|
|
406
|
+
|
376
407
|
def open_texthooker():
|
377
408
|
webbrowser.open(url + '/texthooker')
|
378
409
|
|
410
|
+
|
379
411
|
def start_web_server():
|
380
412
|
logger.debug("Starting web server...")
|
381
413
|
import logging
|
@@ -386,7 +418,8 @@ def start_web_server():
|
|
386
418
|
if get_config().general.open_multimine_on_startup:
|
387
419
|
open_texthooker()
|
388
420
|
|
389
|
-
|
421
|
+
# debug=True provides helpful error messages during development
|
422
|
+
app.run(host='0.0.0.0', port=port, debug=False)
|
390
423
|
|
391
424
|
|
392
425
|
websocket_queue = queue.Queue()
|
@@ -445,7 +478,7 @@ class WebsocketServerThread(threading.Thread):
|
|
445
478
|
text = json.dumps(text)
|
446
479
|
return asyncio.run_coroutine_threadsafe(
|
447
480
|
self.send_text_coroutine(text), self.loop)
|
448
|
-
|
481
|
+
|
449
482
|
def has_clients(self):
|
450
483
|
return len(self.clients) > 0
|
451
484
|
|
@@ -467,24 +500,30 @@ class WebsocketServerThread(threading.Thread):
|
|
467
500
|
await stop_event.wait()
|
468
501
|
return
|
469
502
|
except Exception as e:
|
470
|
-
logger.warning(
|
503
|
+
logger.warning(
|
504
|
+
f"WebSocket server encountered an error: {e}. Retrying...")
|
471
505
|
await asyncio.sleep(1)
|
472
506
|
|
473
507
|
asyncio.run(main())
|
474
508
|
|
509
|
+
|
475
510
|
def handle_exit_signal(loop):
|
476
511
|
logger.info("Received exit signal. Shutting down...")
|
477
512
|
for task in asyncio.all_tasks(loop):
|
478
513
|
task.cancel()
|
479
|
-
|
480
|
-
|
514
|
+
|
515
|
+
|
516
|
+
websocket_server_thread = WebsocketServerThread(read=True, get_ws_port_func=lambda: get_config(
|
517
|
+
).get_field_value('advanced', 'texthooker_communication_websocket_port'))
|
481
518
|
websocket_server_thread.start()
|
482
519
|
|
483
520
|
if get_config().advanced.plaintext_websocket_port:
|
484
|
-
plaintext_websocket_server_thread = WebsocketServerThread(
|
521
|
+
plaintext_websocket_server_thread = WebsocketServerThread(
|
522
|
+
read=False, get_ws_port_func=lambda: get_config().get_field_value('advanced', 'plaintext_websocket_port'))
|
485
523
|
plaintext_websocket_server_thread.start()
|
486
|
-
|
487
|
-
overlay_server_thread = WebsocketServerThread(
|
524
|
+
|
525
|
+
overlay_server_thread = WebsocketServerThread(
|
526
|
+
read=False, get_ws_port_func=lambda: get_config().get_field_value('wip', 'overlay_websocket_port'))
|
488
527
|
overlay_server_thread.start()
|
489
528
|
|
490
529
|
websocket_server_threads = [
|
@@ -493,6 +532,7 @@ websocket_server_threads = [
|
|
493
532
|
overlay_server_thread
|
494
533
|
]
|
495
534
|
|
535
|
+
|
496
536
|
async def texthooker_page_coro():
|
497
537
|
global websocket_server_thread, plaintext_websocket_server_thread, overlay_server_thread
|
498
538
|
# Run the WebSocket server in the asyncio event loop
|
@@ -502,11 +542,13 @@ async def texthooker_page_coro():
|
|
502
542
|
|
503
543
|
# Keep the main asyncio event loop running (for the WebSocket server)
|
504
544
|
|
545
|
+
|
505
546
|
def run_text_hooker_page():
|
506
547
|
try:
|
507
548
|
asyncio.run(texthooker_page_coro())
|
508
549
|
except KeyboardInterrupt:
|
509
550
|
logger.info("Shutting down due to KeyboardInterrupt.")
|
510
551
|
|
552
|
+
|
511
553
|
if __name__ == '__main__':
|
512
|
-
asyncio.run(texthooker_page_coro())
|
554
|
+
asyncio.run(texthooker_page_coro())
|
@@ -2,7 +2,7 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
2
2
|
GameSentenceMiner/anki.py,sha256=zBvKvu-LBaUmG1bUNLq9QXmVoPidqZCxJn4UPw6pUug,21041
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=BeI5Mz9CqDsW08fn1BNzZNUegEmHZsxkDmST8rY2jqU,125738
|
4
4
|
GameSentenceMiner/gametext.py,sha256=h6IOGesK79X8IlvqqMmSzRkSVtkPAXDMHrkQsBwEV1E,10879
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
5
|
+
GameSentenceMiner/gsm.py,sha256=MTopfkJB_URJOenXeX_ZAtOoMsM7GFazawNA2QdUc_Q,28254
|
6
6
|
GameSentenceMiner/obs.py,sha256=smlP_BFuuMkASVDEPG3DjxJ6p617kZXuNTeQ0edtH64,18703
|
7
7
|
GameSentenceMiner/vad.py,sha256=zFReBMvNEEaQ_YEozCTCaMdV-o40FwtlxYRb17cYZio,19125
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -19,23 +19,23 @@ GameSentenceMiner/locales/en_us.json,sha256=9aFJw4vls269_ufnVx03uRLXG8-E-hdG9hJ9
|
|
19
19
|
GameSentenceMiner/locales/ja_jp.json,sha256=zx5R_DLsdUw9Szg7RrOkRprMcnumM5Bnl4QhsfOj-qM,27190
|
20
20
|
GameSentenceMiner/locales/zh_cn.json,sha256=fwP5GQh0WkftdpXuyyt9IiaEIPK_ZXtEa_3kmoeQMAk,24198
|
21
21
|
GameSentenceMiner/ocr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
-
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=
|
22
|
+
GameSentenceMiner/ocr/gsm_ocr_config.py,sha256=7h_nfC7TRQQXW-zkYT0YLvupGBCxlIp62NDOwAASxiI,5369
|
23
23
|
GameSentenceMiner/ocr/ocrconfig.py,sha256=_tY8mjnzHMJrLS8E5pHqYXZjMuLoGKYgJwdhYgN-ny4,6466
|
24
|
-
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=
|
25
|
-
GameSentenceMiner/ocr/owocr_helper.py,sha256=
|
24
|
+
GameSentenceMiner/ocr/owocr_area_selector.py,sha256=Y505tm7KT18flaIyFUe5DfBcBo99e9P3nQDgF4NTmbI,28209
|
25
|
+
GameSentenceMiner/ocr/owocr_helper.py,sha256=_FHwiSNsKvRGWwu9huAepbr2Hpij1ewNxSQC4Gwrd7o,27156
|
26
26
|
GameSentenceMiner/ocr/ss_picker.py,sha256=0IhxUdaKruFpZyBL-8SpxWg7bPrlGpy3lhTcMMZ5rwo,5224
|
27
27
|
GameSentenceMiner/owocr/owocr/__init__.py,sha256=87hfN5u_PbL_onLfMACbc0F5j4KyIK9lKnRCj6oZgR0,49
|
28
28
|
GameSentenceMiner/owocr/owocr/__main__.py,sha256=XQaqZY99EKoCpU-gWQjNbTs7Kg17HvBVE7JY8LqIE0o,157
|
29
29
|
GameSentenceMiner/owocr/owocr/config.py,sha256=qM7kISHdUhuygGXOxmgU6Ef2nwBShrZtdqu4InDCViE,8103
|
30
30
|
GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-roJtAhp8ZAuFTci3TGXtMc,39141
|
31
31
|
GameSentenceMiner/owocr/owocr/ocr.py,sha256=L_Dhy5qoSmVNtzBGVHM8aCJtfFsgqbSdTqXd1Obh1EM,62531
|
32
|
-
GameSentenceMiner/owocr/owocr/run.py,sha256=
|
32
|
+
GameSentenceMiner/owocr/owocr/run.py,sha256=0rsxeIlUys_bpwKDTfBwjER2FpgXKbfzM6lOxgFzzWU,68086
|
33
33
|
GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
|
34
34
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
35
35
|
GameSentenceMiner/util/audio_offset_selector.py,sha256=8Stk3BP-XVIuzRv9nl9Eqd2D-1yD3JrgU-CamBywJmY,8542
|
36
|
-
GameSentenceMiner/util/configuration.py,sha256=
|
36
|
+
GameSentenceMiner/util/configuration.py,sha256=p0ZnrPa0UTFzj59y__pNy0aWOxjlHz1fURZI33qWu74,37184
|
37
37
|
GameSentenceMiner/util/electron_config.py,sha256=9CA27nzEFlxezzDqOPHxeD4BdJ093AnSJ9DJTcwWPsM,8762
|
38
|
-
GameSentenceMiner/util/ffmpeg.py,sha256=
|
38
|
+
GameSentenceMiner/util/ffmpeg.py,sha256=HI2H_prbiZtLtLQgYOj1QNu0Ik_8Du_7gtoHftaz1g4,23520
|
39
39
|
GameSentenceMiner/util/gsm_utils.py,sha256=Piwv88Q9av2LBeN7M6QDi0Mp0_R2lNbkcI6ekK5hd2o,11851
|
40
40
|
GameSentenceMiner/util/model.py,sha256=hmA_seopP2bK40v9T4ulua9TrAeWtbkdCv-sTBPBQDk,6660
|
41
41
|
GameSentenceMiner/util/notification.py,sha256=-qk3kTKEERzmMxx5XMh084HCyFmbfqz0XjY1hTKhCeQ,4202
|
@@ -51,8 +51,8 @@ GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
|
|
51
51
|
GameSentenceMiner/util/downloader/download_tools.py,sha256=zR-aEHiFVkyo-9oPoSx6nQ2K-_J8WBHLZyLoOhypsW4,8458
|
52
52
|
GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UTZo4H-Q-zb-JREkMLI,10439
|
53
53
|
GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
54
|
-
GameSentenceMiner/web/service.py,sha256=
|
55
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
54
|
+
GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
|
55
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=qCNtWtWZAiC91uPrHD2LUWdHOufVuzDhTyTH8Z4oXXk,17927
|
56
56
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
57
57
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
58
58
|
GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
|
@@ -67,9 +67,9 @@ GameSentenceMiner/web/templates/index.html,sha256=Gv3CJvNnhAzIVV_QxhNq4OD-pXDt1v
|
|
67
67
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
68
68
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
69
69
|
GameSentenceMiner/wip/get_overlay_coords.py,sha256=nJRytHJwUBToXeAIkf45HP7Yv42YO-ILbP5h8GVeE2Q,19791
|
70
|
-
gamesentenceminer-2.13.
|
71
|
-
gamesentenceminer-2.13.
|
72
|
-
gamesentenceminer-2.13.
|
73
|
-
gamesentenceminer-2.13.
|
74
|
-
gamesentenceminer-2.13.
|
75
|
-
gamesentenceminer-2.13.
|
70
|
+
gamesentenceminer-2.13.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
71
|
+
gamesentenceminer-2.13.11.dist-info/METADATA,sha256=Vie4cbRkyravU04SbKb0ra2P-2t2-QZTrmnCdrtMQ8g,1464
|
72
|
+
gamesentenceminer-2.13.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
73
|
+
gamesentenceminer-2.13.11.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
74
|
+
gamesentenceminer-2.13.11.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
75
|
+
gamesentenceminer-2.13.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|