GameSentenceMiner 2.9.5__py3-none-any.whl → 2.9.6__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 +0 -1
- GameSentenceMiner/util/__init__.py +0 -0
- GameSentenceMiner/util/configuration.py +653 -0
- GameSentenceMiner/util/electron_config.py +315 -0
- GameSentenceMiner/util/ffmpeg.py +449 -0
- GameSentenceMiner/util/gsm_utils.py +235 -0
- GameSentenceMiner/util/model.py +177 -0
- GameSentenceMiner/util/notification.py +124 -0
- GameSentenceMiner/util/package.py +37 -0
- GameSentenceMiner/util/ss_selector.py +122 -0
- GameSentenceMiner/util/text_log.py +186 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/RECORD +17 -7
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.5.dist-info → gamesentenceminer-2.9.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
from dataclasses_json import dataclass_json
|
5
|
+
|
6
|
+
from GameSentenceMiner.util.configuration import get_config, logger, save_current_config
|
7
|
+
|
8
|
+
|
9
|
+
# OBS
|
10
|
+
@dataclass_json
|
11
|
+
@dataclass
|
12
|
+
class SceneInfo:
|
13
|
+
currentProgramSceneName: str
|
14
|
+
currentProgramSceneUuid: str
|
15
|
+
sceneName: str
|
16
|
+
sceneUuid: str
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass_json
|
20
|
+
@dataclass
|
21
|
+
class SceneItemTransform:
|
22
|
+
alignment: int
|
23
|
+
boundsAlignment: int
|
24
|
+
boundsHeight: float
|
25
|
+
boundsType: str
|
26
|
+
boundsWidth: float
|
27
|
+
cropBottom: int
|
28
|
+
cropLeft: int
|
29
|
+
cropRight: int
|
30
|
+
cropToBounds: bool
|
31
|
+
cropTop: int
|
32
|
+
height: float
|
33
|
+
positionX: float
|
34
|
+
positionY: float
|
35
|
+
rotation: float
|
36
|
+
scaleX: float
|
37
|
+
scaleY: float
|
38
|
+
sourceHeight: float
|
39
|
+
sourceWidth: float
|
40
|
+
width: float
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass_json
|
44
|
+
@dataclass
|
45
|
+
class SceneItem:
|
46
|
+
inputKind: str
|
47
|
+
isGroup: Optional[bool]
|
48
|
+
sceneItemBlendMode: str
|
49
|
+
sceneItemEnabled: bool
|
50
|
+
sceneItemId: int
|
51
|
+
sceneItemIndex: int
|
52
|
+
sceneItemLocked: bool
|
53
|
+
sceneItemTransform: SceneItemTransform
|
54
|
+
sourceName: str
|
55
|
+
sourceType: str
|
56
|
+
sourceUuid: str
|
57
|
+
|
58
|
+
# def __init__(self, **kwargs):
|
59
|
+
# self.inputKind = kwargs['inputKind']
|
60
|
+
# self.isGroup = kwargs['isGroup']
|
61
|
+
# self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
|
62
|
+
# self.sceneItemEnabled = kwargs['sceneItemEnabled']
|
63
|
+
# self.sceneItemId = kwargs['sceneItemId']
|
64
|
+
# self.sceneItemIndex = kwargs['sceneItemIndex']
|
65
|
+
# self.sceneItemLocked = kwargs['sceneItemLocked']
|
66
|
+
# self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
|
67
|
+
# self.sourceName = kwargs['sourceName']
|
68
|
+
# self.sourceType = kwargs['sourceType']
|
69
|
+
# self.sourceUuid = kwargs['sourceUuid']
|
70
|
+
|
71
|
+
|
72
|
+
@dataclass_json
|
73
|
+
@dataclass
|
74
|
+
class SceneItemsResponse:
|
75
|
+
sceneItems: List[SceneItem]
|
76
|
+
|
77
|
+
# def __init__(self, **kwargs):
|
78
|
+
# self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
|
79
|
+
|
80
|
+
|
81
|
+
@dataclass_json
|
82
|
+
@dataclass
|
83
|
+
class RecordDirectory:
|
84
|
+
recordDirectory: str
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass_json
|
88
|
+
@dataclass
|
89
|
+
class SceneItemInfo:
|
90
|
+
sceneIndex: int
|
91
|
+
sceneName: str
|
92
|
+
sceneUuid: str
|
93
|
+
|
94
|
+
|
95
|
+
@dataclass_json
|
96
|
+
@dataclass
|
97
|
+
class SceneListResponse:
|
98
|
+
scenes: List[SceneItemInfo]
|
99
|
+
currentProgramSceneName: Optional[str] = None
|
100
|
+
currentProgramSceneUuid: Optional[str] = None
|
101
|
+
currentPreviewSceneName: Optional[str] = None
|
102
|
+
currentPreviewSceneUuid: Optional[str] = None
|
103
|
+
|
104
|
+
#
|
105
|
+
# @dataclass_json
|
106
|
+
# @dataclass
|
107
|
+
# class SourceActive:
|
108
|
+
# videoActive: bool
|
109
|
+
# videoShowing: bool
|
110
|
+
|
111
|
+
@dataclass_json
|
112
|
+
@dataclass
|
113
|
+
class AnkiCard:
|
114
|
+
noteId: int
|
115
|
+
tags: list[str]
|
116
|
+
fields: dict[str, dict[str, str]]
|
117
|
+
cards: list[int]
|
118
|
+
alternatives = {
|
119
|
+
"word_field": ["Front", "Word", "TargetWord", "Expression"],
|
120
|
+
"sentence_field": ["Example", "Context", "Back", "Sentence"],
|
121
|
+
"picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
|
122
|
+
"sentence_audio_field": ["SentenceAudio"]
|
123
|
+
}
|
124
|
+
|
125
|
+
def get_field(self, field_name: str) -> str:
|
126
|
+
if self.has_field(field_name):
|
127
|
+
return self.fields[field_name]['value']
|
128
|
+
else:
|
129
|
+
raise ValueError(f"Field '{field_name}' not found in AnkiCard. Please make sure your Anki Field Settings in GSM Match your fields in your Anki Note!")
|
130
|
+
|
131
|
+
def has_field (self, field_name: str) -> bool:
|
132
|
+
return field_name in self.fields
|
133
|
+
|
134
|
+
def __post_init__(self):
|
135
|
+
config = get_config()
|
136
|
+
changes_found = False
|
137
|
+
if not self.has_field(config.anki.word_field):
|
138
|
+
found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
|
139
|
+
if found_alternative_field:
|
140
|
+
logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
|
141
|
+
config.anki.word_field = field
|
142
|
+
changes_found = True
|
143
|
+
|
144
|
+
if not self.has_field(config.anki.sentence_field):
|
145
|
+
found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
|
146
|
+
if found_alternative_field:
|
147
|
+
logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
|
148
|
+
config.anki.sentence_field = field
|
149
|
+
changes_found = True
|
150
|
+
|
151
|
+
if not self.has_field(config.anki.picture_field):
|
152
|
+
found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
|
153
|
+
if found_alternative_field:
|
154
|
+
logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
|
155
|
+
config.anki.picture_field = field
|
156
|
+
changes_found = True
|
157
|
+
|
158
|
+
if not self.has_field(config.anki.sentence_audio_field):
|
159
|
+
found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
|
160
|
+
if found_alternative_field:
|
161
|
+
logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
|
162
|
+
config.anki.sentence_audio_field = field
|
163
|
+
changes_found = True
|
164
|
+
|
165
|
+
if changes_found:
|
166
|
+
save_current_config(config)
|
167
|
+
|
168
|
+
def find_field(self, field, field_type):
|
169
|
+
if field in self.fields:
|
170
|
+
return False, field
|
171
|
+
|
172
|
+
for alt_field in self.alternatives[field_type]:
|
173
|
+
for key in self.fields:
|
174
|
+
if alt_field.lower() == key.lower():
|
175
|
+
return True, key
|
176
|
+
|
177
|
+
return False, None
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import requests
|
2
|
+
from plyer import notification
|
3
|
+
from GameSentenceMiner.util.configuration import logger, is_windows
|
4
|
+
|
5
|
+
if is_windows():
|
6
|
+
from win10toast import ToastNotifier
|
7
|
+
|
8
|
+
if is_windows():
|
9
|
+
class MyToastNotifier(ToastNotifier):
|
10
|
+
def __init__(self):
|
11
|
+
super().__init__()
|
12
|
+
|
13
|
+
def on_destroy(self, hwnd, msg, wparam, lparam):
|
14
|
+
super().on_destroy(hwnd, msg, wparam, lparam)
|
15
|
+
return 0
|
16
|
+
|
17
|
+
if is_windows():
|
18
|
+
notifier = MyToastNotifier()
|
19
|
+
else:
|
20
|
+
notifier = notification
|
21
|
+
|
22
|
+
def open_browser_window(note_id, query=None):
|
23
|
+
url = "http://localhost:8765"
|
24
|
+
headers = {'Content-Type': 'application/json'}
|
25
|
+
|
26
|
+
data = {
|
27
|
+
"action": "guiBrowse",
|
28
|
+
"version": 6,
|
29
|
+
"params": {
|
30
|
+
"query": f"nid:{note_id}" if not query else query,
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
try:
|
35
|
+
response = requests.post(url, json=data, headers=headers)
|
36
|
+
if response.status_code == 200:
|
37
|
+
logger.info(f"Opened Anki note with ID {note_id}")
|
38
|
+
else:
|
39
|
+
logger.error(f"Failed to open Anki note with ID {note_id}")
|
40
|
+
except Exception as e:
|
41
|
+
logger.info(f"Error connecting to AnkiConnect: {e}")
|
42
|
+
|
43
|
+
|
44
|
+
def open_anki_card(note_id):
|
45
|
+
url = "http://localhost:8765"
|
46
|
+
headers = {'Content-Type': 'application/json'}
|
47
|
+
|
48
|
+
data = {
|
49
|
+
"action": "guiEditNote",
|
50
|
+
"version": 6,
|
51
|
+
"params": {
|
52
|
+
"note": note_id
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
try:
|
57
|
+
response = requests.post(url, json=data, headers=headers)
|
58
|
+
if response.status_code == 200:
|
59
|
+
logger.info(f"Opened Anki note with ID {note_id}")
|
60
|
+
else:
|
61
|
+
logger.error(f"Failed to open Anki note with ID {note_id}")
|
62
|
+
except Exception as e:
|
63
|
+
logger.info(f"Error connecting to AnkiConnect: {e}")
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
def send_notification(title, message, timeout):
|
68
|
+
if is_windows():
|
69
|
+
notifier.show_toast(title, message, duration=timeout, threaded=True)
|
70
|
+
else:
|
71
|
+
notification.notify(
|
72
|
+
title=title,
|
73
|
+
message=message,
|
74
|
+
app_name="GameSentenceMiner",
|
75
|
+
timeout=timeout # Notification disappears after 5 seconds
|
76
|
+
)
|
77
|
+
|
78
|
+
def send_note_updated(tango):
|
79
|
+
send_notification(
|
80
|
+
title="Anki Card Updated",
|
81
|
+
message=f"Audio and/or Screenshot added to note: {tango}",
|
82
|
+
timeout=5 # Notification disappears after 5 seconds
|
83
|
+
)
|
84
|
+
|
85
|
+
def send_screenshot_updated(tango):
|
86
|
+
send_notification(
|
87
|
+
title="Anki Card Updated",
|
88
|
+
message=f"Screenshot updated on note: {tango}",
|
89
|
+
timeout=5 # Notification disappears after 5 seconds
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
def send_screenshot_saved(path):
|
94
|
+
send_notification(
|
95
|
+
title="Screenshot Saved",
|
96
|
+
message=f"Screenshot saved to : {path}",
|
97
|
+
timeout=5 # Notification disappears after 5 seconds
|
98
|
+
)
|
99
|
+
|
100
|
+
|
101
|
+
def send_audio_generated_notification(audio_path):
|
102
|
+
send_notification(
|
103
|
+
title="Audio Trimmed",
|
104
|
+
message=f"Audio Trimmed and placed at {audio_path}",
|
105
|
+
timeout=5 # Notification disappears after 5 seconds
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
def send_check_obs_notification(reason):
|
110
|
+
send_notification(
|
111
|
+
title="OBS Replay Invalid",
|
112
|
+
message=f"Check OBS Settings! Reason: {reason}",
|
113
|
+
timeout=5 # Notification disappears after 5 seconds
|
114
|
+
)
|
115
|
+
|
116
|
+
|
117
|
+
def send_error_no_anki_update():
|
118
|
+
send_notification(
|
119
|
+
title="Error",
|
120
|
+
message=f"Anki Card not updated, Check Console for Reason!",
|
121
|
+
timeout=5 # Notification disappears after 5 seconds
|
122
|
+
)
|
123
|
+
|
124
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from importlib import metadata
|
2
|
+
|
3
|
+
import requests
|
4
|
+
|
5
|
+
from GameSentenceMiner.util.configuration import logger
|
6
|
+
|
7
|
+
PACKAGE_NAME = "GameSentenceMiner"
|
8
|
+
|
9
|
+
def get_current_version():
|
10
|
+
try:
|
11
|
+
version = metadata.version(PACKAGE_NAME)
|
12
|
+
return version
|
13
|
+
except metadata.PackageNotFoundError:
|
14
|
+
return None
|
15
|
+
|
16
|
+
def get_latest_version():
|
17
|
+
try:
|
18
|
+
response = requests.get(f"https://pypi.org/pypi/{PACKAGE_NAME}/json")
|
19
|
+
latest_version = response.json()["info"]["version"]
|
20
|
+
return latest_version
|
21
|
+
except Exception as e:
|
22
|
+
logger.error(f"Error fetching latest version: {e}")
|
23
|
+
return None
|
24
|
+
|
25
|
+
def check_for_updates(force=False):
|
26
|
+
try:
|
27
|
+
installed_version = get_current_version()
|
28
|
+
latest_version = get_latest_version()
|
29
|
+
|
30
|
+
if installed_version != latest_version or force:
|
31
|
+
logger.info(f"Update available: {installed_version} -> {latest_version}")
|
32
|
+
return True, latest_version
|
33
|
+
else:
|
34
|
+
logger.info("You are already using the latest version.")
|
35
|
+
return False, latest_version
|
36
|
+
except Exception as e:
|
37
|
+
logger.error(f"Error checking for updates: {e}")
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import tkinter as tk
|
2
|
+
from PIL import Image, ImageTk
|
3
|
+
import subprocess
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
|
7
|
+
from GameSentenceMiner.util.gsm_utils import sanitize_filename
|
8
|
+
from GameSentenceMiner.util.configuration import get_temporary_directory, logger
|
9
|
+
from GameSentenceMiner.util.ffmpeg import ffmpeg_base_command_list
|
10
|
+
from GameSentenceMiner.util import ffmpeg
|
11
|
+
|
12
|
+
|
13
|
+
def extract_frames(video_path, timestamp, temp_dir, mode):
|
14
|
+
frame_paths = []
|
15
|
+
timestamp_number = float(timestamp)
|
16
|
+
golden_frame_index = 1 # Default to the first frame
|
17
|
+
golden_frame = None
|
18
|
+
video_duration = ffmpeg.get_video_duration(video_path)
|
19
|
+
|
20
|
+
if mode == 'middle':
|
21
|
+
timestamp_number = max(0.0, timestamp_number - 2.5)
|
22
|
+
elif mode == 'end':
|
23
|
+
timestamp_number = max(0.0, timestamp_number - 5.0)
|
24
|
+
|
25
|
+
if video_duration is not None and timestamp_number > video_duration:
|
26
|
+
logger.debug(f"Timestamp {timestamp_number} exceeds video duration {video_duration}.")
|
27
|
+
return None
|
28
|
+
|
29
|
+
try:
|
30
|
+
command = ffmpeg_base_command_list + [
|
31
|
+
"-y",
|
32
|
+
"-ss", str(timestamp_number),
|
33
|
+
"-i", video_path,
|
34
|
+
"-vf", f"fps=1/{0.25}",
|
35
|
+
"-vframes", "20",
|
36
|
+
os.path.join(temp_dir, "frame_%02d.png")
|
37
|
+
]
|
38
|
+
subprocess.run(command, check=True, capture_output=True)
|
39
|
+
for i in range(1, 21):
|
40
|
+
if os.path.exists(os.path.join(temp_dir, f"frame_{i:02d}.png")):
|
41
|
+
frame_paths.append(os.path.join(temp_dir, f"frame_{i:02d}.png"))
|
42
|
+
|
43
|
+
if mode == "beginning":
|
44
|
+
golden_frame = frame_paths[0]
|
45
|
+
if mode == "middle":
|
46
|
+
golden_frame = frame_paths[len(frame_paths) // 2]
|
47
|
+
if mode == "end":
|
48
|
+
golden_frame = frame_paths[-1]
|
49
|
+
except subprocess.CalledProcessError as e:
|
50
|
+
logger.debug(f"Error extracting frames: {e}")
|
51
|
+
logger.debug(f"Command was: {' '.join(command)}")
|
52
|
+
logger.debug(f"FFmpeg output:\n{e.stderr.decode()}")
|
53
|
+
return None
|
54
|
+
except Exception as e:
|
55
|
+
logger.debug(f"An error occurred: {e}")
|
56
|
+
return None
|
57
|
+
return frame_paths, golden_frame
|
58
|
+
|
59
|
+
def timestamp_to_seconds(timestamp):
|
60
|
+
hours, minutes, seconds = map(int, timestamp.split(':'))
|
61
|
+
return hours * 3600 + minutes * 60 + seconds
|
62
|
+
|
63
|
+
def display_images(image_paths, golden_frame):
|
64
|
+
window = tk.Tk()
|
65
|
+
window.configure(bg="black") # Set the background color to black
|
66
|
+
window.title("Image Selector")
|
67
|
+
selected_path = tk.StringVar()
|
68
|
+
image_widgets = []
|
69
|
+
|
70
|
+
def on_image_click(event):
|
71
|
+
widget = event.widget
|
72
|
+
index = image_widgets.index(widget)
|
73
|
+
selected_path.set(image_paths[index])
|
74
|
+
window.quit()
|
75
|
+
|
76
|
+
for i, path in enumerate(image_paths):
|
77
|
+
img = Image.open(path)
|
78
|
+
img.thumbnail((450, 450))
|
79
|
+
img_tk = ImageTk.PhotoImage(img)
|
80
|
+
if golden_frame and path == golden_frame:
|
81
|
+
label = tk.Label(window, image=img_tk, borderwidth=5, relief="solid")
|
82
|
+
label.config(highlightbackground="yellow", highlightthickness=5)
|
83
|
+
else:
|
84
|
+
label = tk.Label(window, image=img_tk)
|
85
|
+
label.image = img_tk
|
86
|
+
label.grid(row=i // 5, column=i % 5, padx=5, pady=5)
|
87
|
+
label.bind("<Button-1>", on_image_click) # Bind click event to the label
|
88
|
+
image_widgets.append(label)
|
89
|
+
|
90
|
+
window.attributes("-topmost", True)
|
91
|
+
window.mainloop()
|
92
|
+
return selected_path.get()
|
93
|
+
|
94
|
+
def run_extraction_and_display(video_path, timestamp_str, mode):
|
95
|
+
temp_dir = os.path.join(get_temporary_directory(False), "screenshot_frames", sanitize_filename(os.path.splitext(os.path.basename(video_path))[0]))
|
96
|
+
os.makedirs(temp_dir, exist_ok=True)
|
97
|
+
image_paths, golden_frame = extract_frames(video_path, timestamp_str, temp_dir, mode)
|
98
|
+
if image_paths:
|
99
|
+
selected_image_path = display_images(image_paths, golden_frame)
|
100
|
+
if selected_image_path:
|
101
|
+
print(selected_image_path)
|
102
|
+
else:
|
103
|
+
logger.debug("No image was selected.")
|
104
|
+
else:
|
105
|
+
logger.debug("Frame extraction failed.")
|
106
|
+
|
107
|
+
def main():
|
108
|
+
# if len(sys.argv) != 3:
|
109
|
+
# print("Usage: python script.py <video_path> <timestamp>")
|
110
|
+
# sys.exit(1)
|
111
|
+
try:
|
112
|
+
video_path = sys.argv[1]
|
113
|
+
timestamp_str = sys.argv[2]
|
114
|
+
mode = sys.argv[3] if len(sys.argv) > 3 else "beginning"
|
115
|
+
run_extraction_and_display(video_path, timestamp_str, mode)
|
116
|
+
except Exception as e:
|
117
|
+
logger.debug(e)
|
118
|
+
sys.exit(1)
|
119
|
+
|
120
|
+
|
121
|
+
if __name__ == "__main__":
|
122
|
+
main()
|
@@ -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.util.gsm_utils import remove_html_and_cloze_tags
|
8
|
+
from GameSentenceMiner.util.configuration import logger, get_config
|
9
|
+
from GameSentenceMiner.util.model import AnkiCard
|
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, index):
|
48
|
+
return self.values[index]
|
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.info("Could not find matching sentence from GSM's history. Using the latest line.")
|
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)
|
@@ -2,7 +2,7 @@ GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
2
2
|
GameSentenceMiner/anki.py,sha256=CuzVqzuFtZnbMbU2Zk-sxNGwSgyCpv5RLL7lOOX0Meg,14972
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=r-ASCXVNS4Io6Ej3svwC8aJEWc9Rc7u-pzfsAwD4ru8,82079
|
4
4
|
GameSentenceMiner/gametext.py,sha256=mM-gw1d7c2EEvMUznaAevTQFLswNZavCuxMXhA9pV4g,6251
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
5
|
+
GameSentenceMiner/gsm.py,sha256=SHvT3JZlYpZgKeJnVXrtk8ve4ubiM7YPv-9FDF7rVM4,27724
|
6
6
|
GameSentenceMiner/obs.py,sha256=jdAKQFnXlviMupRUKBuK68Q1u8yEZNKBgFnvIq1hhnc,14810
|
7
7
|
GameSentenceMiner/vad.py,sha256=Gk_VthD7mDp3-wM_S6bEv8ykGmqzCDbbcRiaEBzAE_o,14835
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -27,6 +27,16 @@ GameSentenceMiner/owocr/owocr/lens_betterproto.py,sha256=oNoISsPilVVRBBPVDtb4-ro
|
|
27
27
|
GameSentenceMiner/owocr/owocr/ocr.py,sha256=y8RHHaJw8M4BG4CbbtIw0DrV8KP9RjbJNJxjM5v91oU,42236
|
28
28
|
GameSentenceMiner/owocr/owocr/run.py,sha256=jFN7gYYriHgfqORJiBTz8mPkQsDJ6ZugA0_ATWUxk-U,54750
|
29
29
|
GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
|
30
|
+
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
|
+
GameSentenceMiner/util/configuration.py,sha256=Qk5V4HA2FJbdMTa9jYZfNVQ_4rzvjbUtJgPejwQ3vwk,25856
|
32
|
+
GameSentenceMiner/util/electron_config.py,sha256=ZZf54QifdNHbII-JDsMZmdT8nTyrq-7gVvalyLRecfw,9792
|
33
|
+
GameSentenceMiner/util/ffmpeg.py,sha256=qaCXkfK2fd-1NRqbm7burrdBYgnGx07kBuyenee8Mtk,18697
|
34
|
+
GameSentenceMiner/util/gsm_utils.py,sha256=RoOTvWCVpmfYA7fLDdIPcgH1c6TZK4jDZq98BectPhg,8272
|
35
|
+
GameSentenceMiner/util/model.py,sha256=iDtLTfR6D-ZC0gCiDqYno6-gA6Z07PZTM4B5MAA6xZI,5704
|
36
|
+
GameSentenceMiner/util/notification.py,sha256=euTnnNDJm0izr0Z5AhZGV2wrrioCASeKUtm5aZFO5zU,3462
|
37
|
+
GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
|
38
|
+
GameSentenceMiner/util/ss_selector.py,sha256=ATgwDXi4-TLv0hB21NV79FZnXgidiM0z7TgvO7eBnhw,4472
|
39
|
+
GameSentenceMiner/util/text_log.py,sha256=XOq8tpJUpNa-mKJPui40P5aUTX2yzMHPnHgJ2obagw0,5201
|
30
40
|
GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
|
31
41
|
GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
|
32
42
|
GameSentenceMiner/util/communication/websocket.py,sha256=gPgxA2R2U6QZJjPqbUgODC87gtacPhmuC8lCprIkSmA,3287
|
@@ -49,9 +59,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
49
59
|
GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
|
50
60
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
51
61
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
52
|
-
gamesentenceminer-2.9.
|
53
|
-
gamesentenceminer-2.9.
|
54
|
-
gamesentenceminer-2.9.
|
55
|
-
gamesentenceminer-2.9.
|
56
|
-
gamesentenceminer-2.9.
|
57
|
-
gamesentenceminer-2.9.
|
62
|
+
gamesentenceminer-2.9.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
63
|
+
gamesentenceminer-2.9.6.dist-info/METADATA,sha256=dXyoIXRhD6Qg7iCyRhGgoWjgR9PbS40yCmltnqTJZAA,7250
|
64
|
+
gamesentenceminer-2.9.6.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
65
|
+
gamesentenceminer-2.9.6.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
66
|
+
gamesentenceminer-2.9.6.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
67
|
+
gamesentenceminer-2.9.6.dist-info/RECORD,,
|