GameSentenceMiner 2.7.17__py3-none-any.whl → 2.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/anki.py +7 -8
- GameSentenceMiner/config_gui.py +19 -3
- GameSentenceMiner/configuration.py +8 -1
- GameSentenceMiner/downloader/oneocr_dl.py +243 -0
- GameSentenceMiner/ffmpeg.py +1 -3
- GameSentenceMiner/gametext.py +16 -155
- GameSentenceMiner/gsm.py +28 -29
- GameSentenceMiner/obs.py +0 -3
- GameSentenceMiner/ocr/ocrconfig.py +0 -1
- GameSentenceMiner/ocr/owocr_area_selector.py +0 -1
- GameSentenceMiner/ocr/owocr_helper.py +25 -26
- GameSentenceMiner/text_log.py +186 -0
- GameSentenceMiner/util.py +60 -3
- GameSentenceMiner/web/__init__.py +0 -0
- GameSentenceMiner/web/static/__init__.py +0 -0
- GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- GameSentenceMiner/web/static/favicon.ico +0 -0
- GameSentenceMiner/web/static/favicon.svg +3 -0
- GameSentenceMiner/web/static/site.webmanifest +21 -0
- GameSentenceMiner/web/static/style.css +292 -0
- GameSentenceMiner/web/static/text_replacements.html +238 -0
- GameSentenceMiner/web/static/utility.html +313 -0
- GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- GameSentenceMiner/web/texthooking_page.py +234 -0
- {gamesentenceminer-2.7.17.dist-info → gamesentenceminer-2.8.1.dist-info}/METADATA +2 -1
- gamesentenceminer-2.8.1.dist-info/RECORD +58 -0
- {gamesentenceminer-2.7.17.dist-info → gamesentenceminer-2.8.1.dist-info}/WHEEL +1 -1
- GameSentenceMiner/utility_gui.py +0 -204
- gamesentenceminer-2.7.17.dist-info/RECORD +0 -44
- {gamesentenceminer-2.7.17.dist-info → gamesentenceminer-2.8.1.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.7.17.dist-info → gamesentenceminer-2.8.1.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.7.17.dist-info → gamesentenceminer-2.8.1.dist-info}/top_level.txt +0 -0
GameSentenceMiner/gsm.py
CHANGED
@@ -10,7 +10,6 @@ from pystray import Icon, Menu, MenuItem
|
|
10
10
|
from watchdog.events import FileSystemEventHandler
|
11
11
|
from watchdog.observers import Observer
|
12
12
|
|
13
|
-
import time
|
14
13
|
|
15
14
|
from GameSentenceMiner import anki
|
16
15
|
from GameSentenceMiner import config_gui
|
@@ -27,10 +26,11 @@ from GameSentenceMiner.communication.websocket import connect_websocket, registe
|
|
27
26
|
from GameSentenceMiner.configuration import *
|
28
27
|
from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
|
29
28
|
from GameSentenceMiner.ffmpeg import get_audio_and_trim, get_video_timings
|
30
|
-
from GameSentenceMiner.gametext import get_text_event, get_mined_line, GameLine
|
31
29
|
from GameSentenceMiner.obs import check_obs_folder_is_correct
|
30
|
+
from GameSentenceMiner.text_log import GameLine, get_text_event, get_mined_line, get_all_lines
|
32
31
|
from GameSentenceMiner.util import *
|
33
|
-
from GameSentenceMiner.
|
32
|
+
from GameSentenceMiner.web import texthooking_page
|
33
|
+
from GameSentenceMiner.web.texthooking_page import start_web_server
|
34
34
|
|
35
35
|
if is_windows():
|
36
36
|
import win32api
|
@@ -57,9 +57,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
57
57
|
@staticmethod
|
58
58
|
def convert_to_audio(video_path):
|
59
59
|
try:
|
60
|
-
if
|
61
|
-
line: GameLine =
|
62
|
-
|
60
|
+
if texthooking_page.event_manager.line_for_audio:
|
61
|
+
line: GameLine = texthooking_page.event_manager.line_for_audio
|
62
|
+
texthooking_page.event_manager.line_for_audio = None
|
63
63
|
if get_config().advanced.audio_player_path:
|
64
64
|
audio = VideoToAudioHandler.get_audio(line, line.next.time if line.next else None, video_path, temporary=True)
|
65
65
|
play_audio_in_external(audio)
|
@@ -67,9 +67,9 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
67
67
|
elif get_config().advanced.video_player_path:
|
68
68
|
play_video_in_external(line, video_path)
|
69
69
|
return
|
70
|
-
if
|
71
|
-
line: GameLine =
|
72
|
-
|
70
|
+
if texthooking_page.event_manager.line_for_screenshot:
|
71
|
+
line: GameLine = texthooking_page.event_manager.line_for_screenshot
|
72
|
+
texthooking_page.event_manager.line_for_screenshot = None
|
73
73
|
screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
|
74
74
|
os.startfile(screenshot)
|
75
75
|
os.remove(video_path)
|
@@ -109,18 +109,18 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
109
109
|
if mined_line.next:
|
110
110
|
line_cutoff = mined_line.next.time
|
111
111
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
112
|
+
selected_lines = []
|
113
|
+
if texthooking_page.are_lines_selected():
|
114
|
+
selected_lines = texthooking_page.get_selected_lines()
|
115
|
+
start_line = selected_lines[0]
|
116
|
+
mined_line = get_mined_line(last_note, selected_lines)
|
117
|
+
line_cutoff = selected_lines[-1].get_next_time()
|
117
118
|
|
118
119
|
if last_note:
|
119
120
|
logger.debug(last_note.to_json())
|
120
|
-
selected_lines = get_utility_window().get_selected_lines()
|
121
121
|
note = anki.get_initial_card_info(last_note, selected_lines)
|
122
122
|
tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
|
123
|
-
|
123
|
+
texthooking_page.reset_checked_lines()
|
124
124
|
|
125
125
|
if get_config().anki.sentence_audio_field and get_config().audio.enabled:
|
126
126
|
logger.debug("Attempting to get audio from video")
|
@@ -268,10 +268,10 @@ def register_hotkeys():
|
|
268
268
|
keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
|
269
269
|
if get_config().hotkeys.take_screenshot:
|
270
270
|
keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
|
271
|
-
if get_config().hotkeys.open_utility:
|
272
|
-
keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
|
273
271
|
if get_config().hotkeys.play_latest_audio:
|
274
272
|
keyboard.add_hotkey(get_config().hotkeys.play_latest_audio, play_most_recent_audio)
|
273
|
+
if get_config().hotkeys.open_utility:
|
274
|
+
keyboard.add_hotkey(get_config().hotkeys.open_utility, texthooking_page.open_texthooker)
|
275
275
|
|
276
276
|
|
277
277
|
def get_screenshot():
|
@@ -322,13 +322,10 @@ def open_settings():
|
|
322
322
|
settings_window.show()
|
323
323
|
|
324
324
|
|
325
|
-
def open_multimine():
|
326
|
-
obs.update_current_game()
|
327
|
-
get_utility_window().show()
|
328
|
-
|
329
325
|
def play_most_recent_audio():
|
330
|
-
if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(
|
331
|
-
|
326
|
+
if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(
|
327
|
+
get_all_lines()) > 0:
|
328
|
+
texthooking_page.event_manager.line_for_audio = get_all_lines()[-1]
|
332
329
|
obs.save_replay_buffer()
|
333
330
|
else:
|
334
331
|
logger.error("Feature Disabled. No audio or video player path set in config!")
|
@@ -368,6 +365,10 @@ def play_pause(icon, item):
|
|
368
365
|
update_icon()
|
369
366
|
|
370
367
|
|
368
|
+
def open_multimine(icon, item):
|
369
|
+
texthooking_page.open_texthooker()
|
370
|
+
|
371
|
+
|
371
372
|
def update_icon():
|
372
373
|
global menu, icon
|
373
374
|
# Recreate the menu with the updated button text
|
@@ -416,7 +417,7 @@ def run_tray():
|
|
416
417
|
|
417
418
|
menu = Menu(
|
418
419
|
MenuItem("Open Settings", open_settings),
|
419
|
-
MenuItem("Open
|
420
|
+
MenuItem("Open Texthooker", texthooking_page.open_texthooker),
|
420
421
|
MenuItem("Open Log", open_log),
|
421
422
|
MenuItem("Toggle Replay Buffer", play_pause),
|
422
423
|
MenuItem("Restart OBS", restart_obs),
|
@@ -554,6 +555,7 @@ def post_init():
|
|
554
555
|
whisper_helper.initialize_whisper_model()
|
555
556
|
if get_config().vad.is_silero():
|
556
557
|
from GameSentenceMiner.vad import silero_trim
|
558
|
+
start_web_server()
|
557
559
|
|
558
560
|
util.run_new_thread(do_post_init)
|
559
561
|
|
@@ -576,7 +578,6 @@ def main(reloading=False):
|
|
576
578
|
logger.info("Script started.")
|
577
579
|
root = ttk.Window(themename='darkly')
|
578
580
|
settings_window = config_gui.ConfigApp(root)
|
579
|
-
init_utility_window(root)
|
580
581
|
initialize(reloading)
|
581
582
|
initialize_async()
|
582
583
|
observer = Observer()
|
@@ -594,9 +595,7 @@ def main(reloading=False):
|
|
594
595
|
try:
|
595
596
|
# if get_config().general.open_config_on_startup:
|
596
597
|
# root.after(0, settings_window.show)
|
597
|
-
|
598
|
-
root.after(0, get_utility_window().show)
|
599
|
-
root.after(0, post_init)
|
598
|
+
root.after(50, post_init)
|
600
599
|
settings_window.add_save_hook(update_icon)
|
601
600
|
settings_window.on_exit = exit_program
|
602
601
|
root.mainloop()
|
GameSentenceMiner/obs.py
CHANGED
@@ -7,7 +7,6 @@ import mss
|
|
7
7
|
from PIL import Image, ImageTk, ImageDraw
|
8
8
|
|
9
9
|
from GameSentenceMiner import obs # Import your actual obs module
|
10
|
-
from GameSentenceMiner.ocr.owocr_helper import get_ocr_config
|
11
10
|
from GameSentenceMiner.util import sanitize_filename # Import your actual util module
|
12
11
|
|
13
12
|
try:
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import asyncio
|
2
|
-
import difflib
|
3
2
|
import json
|
4
3
|
import logging
|
5
4
|
import os
|
6
5
|
import queue
|
6
|
+
import re
|
7
7
|
import threading
|
8
8
|
import time
|
9
9
|
from datetime import datetime
|
@@ -14,17 +14,14 @@ from tkinter import messagebox
|
|
14
14
|
import mss
|
15
15
|
import websockets
|
16
16
|
from rapidfuzz import fuzz
|
17
|
-
from PIL import Image, ImageDraw
|
18
17
|
|
19
18
|
from GameSentenceMiner import obs, util
|
20
|
-
from GameSentenceMiner.configuration import get_config, get_app_directory
|
19
|
+
from GameSentenceMiner.configuration import get_config, get_app_directory, get_temporary_directory
|
21
20
|
from GameSentenceMiner.electron_config import get_ocr_scan_rate, get_requires_open_window
|
22
21
|
from GameSentenceMiner.ocr.gsm_ocr_config import OCRConfig, Rectangle
|
23
22
|
from GameSentenceMiner.owocr.owocr import screen_coordinate_picker, run
|
24
23
|
from GameSentenceMiner.owocr.owocr.run import TextFiltering
|
25
|
-
|
26
|
-
from dataclasses import dataclass
|
27
|
-
from typing import List, Optional
|
24
|
+
from GameSentenceMiner.util import do_text_replacements, OCR_REPLACEMENTS_FILE
|
28
25
|
|
29
26
|
CONFIG_FILE = Path("ocr_config.json")
|
30
27
|
DEFAULT_IMAGE_PATH = r"C:\Users\Beangate\Pictures\msedge_acbl8GL7Ax.jpg" # CHANGE THIS
|
@@ -80,6 +77,7 @@ def get_ocr_config() -> OCRConfig:
|
|
80
77
|
"""Loads and updates screen capture areas from the corresponding JSON file."""
|
81
78
|
app_dir = Path.home() / "AppData" / "Roaming" / "GameSentenceMiner"
|
82
79
|
ocr_config_dir = app_dir / "ocr_config"
|
80
|
+
os.makedirs(ocr_config_dir, exist_ok=True)
|
83
81
|
obs.connect_to_obs()
|
84
82
|
scene = util.sanitize_filename(obs.get_current_scene())
|
85
83
|
config_path = ocr_config_dir / f"{scene}.json"
|
@@ -217,63 +215,62 @@ def do_second_ocr(ocr1_text, rectangle_index, time, img):
|
|
217
215
|
if fuzz.ratio(previous_ocr2_text, text) >= 80:
|
218
216
|
logger.info("Seems like the same text from previous ocr2 result, not sending")
|
219
217
|
return
|
220
|
-
img.save(os.path.join(
|
218
|
+
img.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
|
221
219
|
last_ocr2_results[rectangle_index] = text
|
222
|
-
|
223
|
-
import pyperclip
|
224
|
-
pyperclip.copy(text)
|
225
|
-
websocket_server_thread.send_text(text, time)
|
220
|
+
send_result(text, time)
|
226
221
|
except json.JSONDecodeError:
|
227
222
|
print("Invalid JSON received.")
|
228
223
|
except Exception as e:
|
229
224
|
logger.exception(e)
|
230
225
|
print(f"Error processing message: {e}")
|
231
226
|
|
227
|
+
def send_result(text, time):
|
228
|
+
if text:
|
229
|
+
text = do_text_replacements(text, OCR_REPLACEMENTS_FILE)
|
230
|
+
if get_config().advanced.ocr_sends_to_clipboard:
|
231
|
+
import pyperclip
|
232
|
+
pyperclip.copy(text)
|
233
|
+
websocket_server_thread.send_text(text, time)
|
234
|
+
|
232
235
|
|
233
236
|
last_oneocr_results_to_check = {} # Store last OCR result for each rectangle
|
234
237
|
last_oneocr_times = {} # Store last OCR time for each rectangle
|
235
238
|
text_stable_start_times = {} # Store the start time when text becomes stable for each rectangle
|
239
|
+
previous_imgs = {}
|
236
240
|
orig_text_results = {} # Store original text results for each rectangle
|
237
241
|
TEXT_APPEARENCE_DELAY = get_ocr_scan_rate() * 1000 + 500 # Adjust as needed
|
238
242
|
|
239
243
|
def text_callback(text, orig_text, rectangle_index, time, img=None):
|
240
244
|
global twopassocr, ocr2, last_oneocr_results_to_check, last_oneocr_times, text_stable_start_times, orig_text_results
|
241
245
|
orig_text_string = ''.join([item for item in orig_text if item is not None]) if orig_text else ""
|
246
|
+
# logger.debug(orig_text_string)
|
242
247
|
|
243
248
|
current_time = time if time else datetime.now()
|
244
249
|
|
245
|
-
previous_text = last_oneocr_results_to_check.
|
250
|
+
previous_text = last_oneocr_results_to_check.pop(rectangle_index, "").strip()
|
246
251
|
previous_orig_text = orig_text_results.get(rectangle_index, "").strip()
|
247
252
|
|
248
253
|
# print(previous_orig_text)
|
249
254
|
# if orig_text:
|
250
255
|
# print(orig_text_string)
|
251
|
-
|
256
|
+
if not twopassocr:
|
257
|
+
img.save(os.path.join(get_temporary_directory(), "last_successful_ocr.png"))
|
258
|
+
send_result(text, time)
|
252
259
|
if not text:
|
253
260
|
if previous_text:
|
254
261
|
if rectangle_index in text_stable_start_times:
|
255
|
-
stable_time = text_stable_start_times
|
262
|
+
stable_time = text_stable_start_times.pop(rectangle_index)
|
263
|
+
previous_img = previous_imgs.pop(rectangle_index)
|
256
264
|
previous_result = last_ocr1_results[rectangle_index]
|
257
265
|
if previous_result and fuzz.ratio(previous_result, previous_text) >= 80:
|
258
266
|
logger.info("Seems like the same text, not " + "doing second OCR" if twopassocr else "sending")
|
259
|
-
del last_oneocr_results_to_check[rectangle_index]
|
260
267
|
return
|
261
268
|
if previous_orig_text and fuzz.ratio(orig_text_string, previous_orig_text) >= 80:
|
262
269
|
logger.info("Seems like Text we already sent, not doing anything.")
|
263
|
-
del last_oneocr_results_to_check[rectangle_index]
|
264
270
|
return
|
265
271
|
orig_text_results[rectangle_index] = orig_text_string
|
266
|
-
|
267
|
-
do_second_ocr(previous_text, rectangle_index, time, img)
|
268
|
-
else:
|
269
|
-
if get_config().advanced.ocr_sends_to_clipboard:
|
270
|
-
import pyperclip
|
271
|
-
pyperclip.copy(text)
|
272
|
-
websocket_server_thread.send_text(previous_text, stable_time)
|
273
|
-
img.save(os.path.join(get_app_directory(), "temp", "last_successful_ocr.png"))
|
272
|
+
do_second_ocr(previous_text, rectangle_index, stable_time, previous_img)
|
274
273
|
last_ocr1_results[rectangle_index] = previous_text
|
275
|
-
del text_stable_start_times[rectangle_index]
|
276
|
-
del last_oneocr_results_to_check[rectangle_index]
|
277
274
|
return
|
278
275
|
return
|
279
276
|
|
@@ -281,6 +278,7 @@ def text_callback(text, orig_text, rectangle_index, time, img=None):
|
|
281
278
|
last_oneocr_results_to_check[rectangle_index] = text
|
282
279
|
last_oneocr_times[rectangle_index] = current_time
|
283
280
|
text_stable_start_times[rectangle_index] = current_time
|
281
|
+
previous_imgs[rectangle_index] = img
|
284
282
|
return
|
285
283
|
|
286
284
|
stable = text_stable_start_times.get(rectangle_index)
|
@@ -294,6 +292,7 @@ def text_callback(text, orig_text, rectangle_index, time, img=None):
|
|
294
292
|
else:
|
295
293
|
last_oneocr_results_to_check[rectangle_index] = text
|
296
294
|
last_oneocr_times[rectangle_index] = current_time
|
295
|
+
previous_imgs[rectangle_index] = img
|
297
296
|
|
298
297
|
done = False
|
299
298
|
|
@@ -0,0 +1,186 @@
|
|
1
|
+
import uuid
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from datetime import datetime
|
4
|
+
from difflib import SequenceMatcher
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
from GameSentenceMiner.configuration import logger, get_config
|
8
|
+
from GameSentenceMiner.model import AnkiCard
|
9
|
+
from GameSentenceMiner.util import remove_html_and_cloze_tags
|
10
|
+
|
11
|
+
initial_time = datetime.now()
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class GameLine:
|
16
|
+
id: str
|
17
|
+
text: str
|
18
|
+
time: datetime
|
19
|
+
prev: 'GameLine | None'
|
20
|
+
next: 'GameLine | None'
|
21
|
+
index: int = 0
|
22
|
+
|
23
|
+
def get_previous_time(self):
|
24
|
+
if self.prev:
|
25
|
+
return self.prev.time
|
26
|
+
return initial_time
|
27
|
+
|
28
|
+
def get_next_time(self):
|
29
|
+
if self.next:
|
30
|
+
return self.next.time
|
31
|
+
return 0
|
32
|
+
|
33
|
+
def __str__(self):
|
34
|
+
return str({"text": self.text, "time": self.time})
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class GameText:
|
39
|
+
values: list[GameLine]
|
40
|
+
values_dict: dict[str, GameLine]
|
41
|
+
game_line_index = 0
|
42
|
+
|
43
|
+
def __init__(self):
|
44
|
+
self.values = []
|
45
|
+
self.values_dict = {}
|
46
|
+
|
47
|
+
def __getitem__(self, key):
|
48
|
+
return self.values[key]
|
49
|
+
|
50
|
+
def get_by_id(self, line_id: str) -> Optional[GameLine]:
|
51
|
+
if not self.values_dict:
|
52
|
+
return None
|
53
|
+
return self.values_dict.get(line_id)
|
54
|
+
|
55
|
+
def get_time(self, line_text: str, occurrence: int = -1) -> datetime:
|
56
|
+
matches = [line for line in self.values if line.text == line_text]
|
57
|
+
if matches:
|
58
|
+
return matches[occurrence].time # Default to latest
|
59
|
+
return initial_time
|
60
|
+
|
61
|
+
def get_event(self, line_text: str, occurrence: int = -1) -> GameLine | None:
|
62
|
+
matches = [line for line in self.values if line.text == line_text]
|
63
|
+
if matches:
|
64
|
+
return matches[occurrence]
|
65
|
+
return None
|
66
|
+
|
67
|
+
def add_line(self, line_text, line_time=None):
|
68
|
+
if not line_text:
|
69
|
+
return
|
70
|
+
line_id = str(uuid.uuid1())
|
71
|
+
new_line = GameLine(
|
72
|
+
id=line_id, # Time-based UUID as an integer
|
73
|
+
text=line_text,
|
74
|
+
time=line_time if line_time else datetime.now(),
|
75
|
+
prev=self.values[-1] if self.values else None,
|
76
|
+
next=None,
|
77
|
+
index=self.game_line_index
|
78
|
+
)
|
79
|
+
self.values_dict[line_id] = new_line
|
80
|
+
logger.debug(f"Adding line: {new_line}")
|
81
|
+
self.game_line_index += 1
|
82
|
+
if self.values:
|
83
|
+
self.values[-1].next = new_line
|
84
|
+
self.values.append(new_line)
|
85
|
+
# self.remove_old_events(datetime.now() - timedelta(minutes=10))
|
86
|
+
|
87
|
+
def has_line(self, line_text) -> bool:
|
88
|
+
for game_line in self.values:
|
89
|
+
if game_line.text == line_text:
|
90
|
+
return True
|
91
|
+
return False
|
92
|
+
|
93
|
+
|
94
|
+
text_log = GameText()
|
95
|
+
|
96
|
+
|
97
|
+
def similar(a, b):
|
98
|
+
return SequenceMatcher(None, a, b).ratio()
|
99
|
+
|
100
|
+
|
101
|
+
def one_contains_the_other(a, b):
|
102
|
+
return a in b or b in a
|
103
|
+
|
104
|
+
|
105
|
+
def lines_match(a, b):
|
106
|
+
similarity = similar(a, b)
|
107
|
+
logger.debug(f"Comparing: {a} with {b} - Similarity: {similarity}, Or One contains the other: {one_contains_the_other(a, b)}")
|
108
|
+
return similar(a, b) >= 0.60 or one_contains_the_other(a, b)
|
109
|
+
|
110
|
+
|
111
|
+
def get_text_event(last_note) -> GameLine:
|
112
|
+
lines = text_log.values
|
113
|
+
|
114
|
+
if not lines:
|
115
|
+
raise Exception("No lines in history. Text is required from either clipboard or websocket for GSM to work. Please check your setup/config.")
|
116
|
+
|
117
|
+
if not last_note:
|
118
|
+
return lines[-1]
|
119
|
+
|
120
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
121
|
+
if not sentence:
|
122
|
+
return lines[-1]
|
123
|
+
|
124
|
+
for line in reversed(lines):
|
125
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
|
126
|
+
return line
|
127
|
+
|
128
|
+
logger.debug("Couldn't find a match in history, using last event")
|
129
|
+
return lines[-1]
|
130
|
+
|
131
|
+
|
132
|
+
def get_line_and_future_lines(last_note):
|
133
|
+
if not last_note:
|
134
|
+
return []
|
135
|
+
|
136
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
137
|
+
found_lines = []
|
138
|
+
if sentence:
|
139
|
+
found = False
|
140
|
+
for line in text_log.values:
|
141
|
+
if found:
|
142
|
+
found_lines.append(line.text)
|
143
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
|
144
|
+
found = True
|
145
|
+
found_lines.append(line.text)
|
146
|
+
return found_lines
|
147
|
+
|
148
|
+
|
149
|
+
def get_mined_line(last_note: AnkiCard, lines):
|
150
|
+
if not last_note:
|
151
|
+
return lines[-1]
|
152
|
+
if not lines:
|
153
|
+
lines = get_all_lines()
|
154
|
+
|
155
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
156
|
+
for line in lines:
|
157
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
|
158
|
+
return line
|
159
|
+
return lines[-1]
|
160
|
+
|
161
|
+
|
162
|
+
def get_time_of_line(line):
|
163
|
+
return text_log.get_time(line)
|
164
|
+
|
165
|
+
|
166
|
+
def get_all_lines():
|
167
|
+
return text_log.values
|
168
|
+
|
169
|
+
|
170
|
+
def get_text_log() -> GameText:
|
171
|
+
return text_log
|
172
|
+
|
173
|
+
def add_line(current_line_after_regex, line_time):
|
174
|
+
text_log.add_line(current_line_after_regex, line_time)
|
175
|
+
|
176
|
+
def get_line_by_id(line_id: str) -> Optional[GameLine]:
|
177
|
+
"""
|
178
|
+
Retrieve a GameLine by its unique ID.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
line_id (str): The unique identifier of the GameLine.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Optional[GameLine]: The GameLine object if found, otherwise None.
|
185
|
+
"""
|
186
|
+
return text_log.get_by_id(line_id)
|
GameSentenceMiner/util.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
import
|
1
|
+
import json
|
2
2
|
import os
|
3
3
|
import random
|
4
4
|
import re
|
5
5
|
import string
|
6
6
|
import subprocess
|
7
|
-
import sys
|
8
7
|
import threading
|
9
8
|
import time
|
10
9
|
from datetime import datetime
|
@@ -206,4 +205,62 @@ def import_vad_models():
|
|
206
205
|
from GameSentenceMiner.vad import whisper_helper
|
207
206
|
if get_config().vad.is_vosk():
|
208
207
|
from GameSentenceMiner.vad import vosk_helper
|
209
|
-
return silero_trim, whisper_helper, vosk_helper
|
208
|
+
return silero_trim, whisper_helper, vosk_helper
|
209
|
+
|
210
|
+
|
211
|
+
def isascii(s: str):
|
212
|
+
try:
|
213
|
+
return s.isascii()
|
214
|
+
except:
|
215
|
+
try:
|
216
|
+
s.encode("ascii")
|
217
|
+
return True
|
218
|
+
except:
|
219
|
+
return False
|
220
|
+
|
221
|
+
def do_text_replacements(text, replacements_json):
|
222
|
+
if not text:
|
223
|
+
return text
|
224
|
+
|
225
|
+
replacements = {}
|
226
|
+
if os.path.exists(replacements_json):
|
227
|
+
with open(replacements_json, 'r', encoding='utf-8') as f:
|
228
|
+
replacements.update(json.load(f))
|
229
|
+
|
230
|
+
if replacements.get("enabled", False):
|
231
|
+
orig_text = text
|
232
|
+
filters = replacements.get("args", {}).get("replacements", {})
|
233
|
+
for fil, replacement in filters.items():
|
234
|
+
if not fil:
|
235
|
+
continue
|
236
|
+
if fil.startswith("re:"):
|
237
|
+
pattern = fil[3:]
|
238
|
+
try:
|
239
|
+
text = re.sub(pattern, replacement, text)
|
240
|
+
except Exception:
|
241
|
+
logger.error(f"Invalid regex pattern: {pattern}")
|
242
|
+
continue
|
243
|
+
if isascii(fil):
|
244
|
+
text = re.sub(r"\b{}\b".format(re.escape(fil)), replacement, text)
|
245
|
+
else:
|
246
|
+
text = text.replace(fil, replacement)
|
247
|
+
if text != orig_text:
|
248
|
+
logger.info(f"Text replaced: '{orig_text}' -> '{text}' using replacements.")
|
249
|
+
return text
|
250
|
+
|
251
|
+
|
252
|
+
TEXT_REPLACEMENTS_FILE = os.path.join(os.getenv('APPDATA'), 'GameSentenceMiner', 'config', 'text_replacements.json')
|
253
|
+
OCR_REPLACEMENTS_FILE = os.path.join(os.getenv('APPDATA'), 'GameSentenceMiner', 'config', 'ocr_replacements.json')
|
254
|
+
os.makedirs(os.path.dirname(TEXT_REPLACEMENTS_FILE), exist_ok=True)
|
255
|
+
|
256
|
+
import urllib.request
|
257
|
+
|
258
|
+
if not os.path.exists(TEXT_REPLACEMENTS_FILE):
|
259
|
+
url = "https://raw.githubusercontent.com/bpwhelan/GameSentenceMiner/refs/heads/main/electron-src/assets/ocr_replacements.json"
|
260
|
+
try:
|
261
|
+
with urllib.request.urlopen(url) as response:
|
262
|
+
data = response.read().decode('utf-8')
|
263
|
+
with open(TEXT_REPLACEMENTS_FILE, 'w', encoding='utf-8') as f:
|
264
|
+
f.write(data)
|
265
|
+
except Exception as e:
|
266
|
+
logger.error(f"Failed to fetch JSON from {url}: {e}")
|
File without changes
|
File without changes
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,3 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="67" height="64" viewBox="0 0 67 64"><image width="67" height="64" xlink:href="data:image/png;base64,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"></image><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
2
|
+
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
3
|
+
</style></svg>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"name": "MyWebSite",
|
3
|
+
"short_name": "MySite",
|
4
|
+
"icons": [
|
5
|
+
{
|
6
|
+
"src": "/web-app-manifest-192x192.png",
|
7
|
+
"sizes": "192x192",
|
8
|
+
"type": "image/png",
|
9
|
+
"purpose": "maskable"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"src": "/web-app-manifest-512x512.png",
|
13
|
+
"sizes": "512x512",
|
14
|
+
"type": "image/png",
|
15
|
+
"purpose": "maskable"
|
16
|
+
}
|
17
|
+
],
|
18
|
+
"theme_color": "#ffffff",
|
19
|
+
"background_color": "#ffffff",
|
20
|
+
"display": "standalone"
|
21
|
+
}
|