GameSentenceMiner 2.14.9__py3-none-any.whl → 2.14.10__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/ai/__init__.py +0 -0
- GameSentenceMiner/ai/ai_prompting.py +473 -0
- GameSentenceMiner/ocr/__init__.py +0 -0
- GameSentenceMiner/ocr/gsm_ocr_config.py +174 -0
- GameSentenceMiner/ocr/ocrconfig.py +129 -0
- GameSentenceMiner/ocr/owocr_area_selector.py +629 -0
- GameSentenceMiner/ocr/owocr_helper.py +638 -0
- GameSentenceMiner/ocr/ss_picker.py +140 -0
- GameSentenceMiner/owocr/owocr/__init__.py +1 -0
- GameSentenceMiner/owocr/owocr/__main__.py +9 -0
- GameSentenceMiner/owocr/owocr/config.py +148 -0
- GameSentenceMiner/owocr/owocr/lens_betterproto.py +1238 -0
- GameSentenceMiner/owocr/owocr/ocr.py +1690 -0
- GameSentenceMiner/owocr/owocr/run.py +1818 -0
- GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +109 -0
- GameSentenceMiner/tools/__init__.py +0 -0
- GameSentenceMiner/tools/audio_offset_selector.py +215 -0
- GameSentenceMiner/tools/ss_selector.py +135 -0
- GameSentenceMiner/tools/window_transparency.py +214 -0
- GameSentenceMiner/util/__init__.py +0 -0
- GameSentenceMiner/util/communication/__init__.py +22 -0
- GameSentenceMiner/util/communication/send.py +7 -0
- GameSentenceMiner/util/communication/websocket.py +94 -0
- GameSentenceMiner/util/configuration.py +1199 -0
- GameSentenceMiner/util/db.py +408 -0
- GameSentenceMiner/util/downloader/Untitled_json.py +472 -0
- GameSentenceMiner/util/downloader/__init__.py +0 -0
- GameSentenceMiner/util/downloader/download_tools.py +194 -0
- GameSentenceMiner/util/downloader/oneocr_dl.py +250 -0
- GameSentenceMiner/util/electron_config.py +259 -0
- GameSentenceMiner/util/ffmpeg.py +571 -0
- GameSentenceMiner/util/get_overlay_coords.py +366 -0
- GameSentenceMiner/util/gsm_utils.py +323 -0
- GameSentenceMiner/util/model.py +206 -0
- GameSentenceMiner/util/notification.py +157 -0
- GameSentenceMiner/util/text_log.py +214 -0
- GameSentenceMiner/util/win10toast/__init__.py +154 -0
- GameSentenceMiner/util/win10toast/__main__.py +22 -0
- GameSentenceMiner/web/__init__.py +0 -0
- GameSentenceMiner/web/service.py +132 -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/web-app-manifest-192x192.png +0 -0
- GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- GameSentenceMiner/web/templates/__init__.py +0 -0
- GameSentenceMiner/web/templates/index.html +50 -0
- GameSentenceMiner/web/templates/text_replacements.html +238 -0
- GameSentenceMiner/web/templates/utility.html +483 -0
- GameSentenceMiner/web/texthooking_page.py +584 -0
- GameSentenceMiner/wip/__init___.py +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/METADATA +1 -1
- gamesentenceminer-2.14.10.dist-info/RECORD +79 -0
- gamesentenceminer-2.14.9.dist-info/RECORD +0 -24
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.14.9.dist-info → gamesentenceminer-2.14.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Optional, List
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
from dataclasses_json import dataclass_json
|
6
|
+
|
7
|
+
from GameSentenceMiner.util.configuration import get_config, logger, save_current_config
|
8
|
+
|
9
|
+
|
10
|
+
# OBS
|
11
|
+
@dataclass_json
|
12
|
+
@dataclass
|
13
|
+
class SceneInfo:
|
14
|
+
currentProgramSceneName: str
|
15
|
+
currentProgramSceneUuid: str
|
16
|
+
sceneName: str
|
17
|
+
sceneUuid: str
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass_json
|
21
|
+
@dataclass
|
22
|
+
class SceneItemTransform:
|
23
|
+
alignment: int
|
24
|
+
boundsAlignment: int
|
25
|
+
boundsHeight: float
|
26
|
+
boundsType: str
|
27
|
+
boundsWidth: float
|
28
|
+
cropBottom: int
|
29
|
+
cropLeft: int
|
30
|
+
cropRight: int
|
31
|
+
cropToBounds: bool
|
32
|
+
cropTop: int
|
33
|
+
height: float
|
34
|
+
positionX: float
|
35
|
+
positionY: float
|
36
|
+
rotation: float
|
37
|
+
scaleX: float
|
38
|
+
scaleY: float
|
39
|
+
sourceHeight: float
|
40
|
+
sourceWidth: float
|
41
|
+
width: float
|
42
|
+
|
43
|
+
|
44
|
+
@dataclass_json
|
45
|
+
@dataclass
|
46
|
+
class SceneItem:
|
47
|
+
inputKind: str
|
48
|
+
isGroup: Optional[bool]
|
49
|
+
sceneItemBlendMode: str
|
50
|
+
sceneItemEnabled: bool
|
51
|
+
sceneItemId: int
|
52
|
+
sceneItemIndex: int
|
53
|
+
sceneItemLocked: bool
|
54
|
+
sceneItemTransform: SceneItemTransform
|
55
|
+
sourceName: str
|
56
|
+
sourceType: str
|
57
|
+
sourceUuid: str
|
58
|
+
|
59
|
+
# def __init__(self, **kwargs):
|
60
|
+
# self.inputKind = kwargs['inputKind']
|
61
|
+
# self.isGroup = kwargs['isGroup']
|
62
|
+
# self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
|
63
|
+
# self.sceneItemEnabled = kwargs['sceneItemEnabled']
|
64
|
+
# self.sceneItemId = kwargs['sceneItemId']
|
65
|
+
# self.sceneItemIndex = kwargs['sceneItemIndex']
|
66
|
+
# self.sceneItemLocked = kwargs['sceneItemLocked']
|
67
|
+
# self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
|
68
|
+
# self.sourceName = kwargs['sourceName']
|
69
|
+
# self.sourceType = kwargs['sourceType']
|
70
|
+
# self.sourceUuid = kwargs['sourceUuid']
|
71
|
+
|
72
|
+
|
73
|
+
@dataclass_json
|
74
|
+
@dataclass
|
75
|
+
class SceneItemsResponse:
|
76
|
+
sceneItems: List[SceneItem]
|
77
|
+
|
78
|
+
# def __init__(self, **kwargs):
|
79
|
+
# self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
|
80
|
+
|
81
|
+
|
82
|
+
@dataclass_json
|
83
|
+
@dataclass
|
84
|
+
class RecordDirectory:
|
85
|
+
recordDirectory: str
|
86
|
+
|
87
|
+
|
88
|
+
@dataclass_json
|
89
|
+
@dataclass
|
90
|
+
class SceneItemInfo:
|
91
|
+
sceneIndex: int
|
92
|
+
sceneName: str
|
93
|
+
sceneUuid: str
|
94
|
+
|
95
|
+
|
96
|
+
@dataclass_json
|
97
|
+
@dataclass
|
98
|
+
class SceneListResponse:
|
99
|
+
scenes: List[SceneItemInfo]
|
100
|
+
currentProgramSceneName: Optional[str] = None
|
101
|
+
currentProgramSceneUuid: Optional[str] = None
|
102
|
+
currentPreviewSceneName: Optional[str] = None
|
103
|
+
currentPreviewSceneUuid: Optional[str] = None
|
104
|
+
|
105
|
+
#
|
106
|
+
# @dataclass_json
|
107
|
+
# @dataclass
|
108
|
+
# class SourceActive:
|
109
|
+
# videoActive: bool
|
110
|
+
# videoShowing: bool
|
111
|
+
|
112
|
+
@dataclass_json
|
113
|
+
@dataclass
|
114
|
+
class AnkiField:
|
115
|
+
value: str
|
116
|
+
order: int
|
117
|
+
|
118
|
+
@dataclass_json
|
119
|
+
@dataclass
|
120
|
+
class AnkiCard:
|
121
|
+
noteId: int
|
122
|
+
tags: list[str]
|
123
|
+
fields: dict[str, AnkiField]
|
124
|
+
cards: list[int]
|
125
|
+
alternatives = {
|
126
|
+
"word_field": ["Front", "Word", "TargetWord", "Expression"],
|
127
|
+
"sentence_field": ["Example", "Context", "Back", "Sentence"],
|
128
|
+
"picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
|
129
|
+
"sentence_audio_field": ["SentenceAudio"]
|
130
|
+
}
|
131
|
+
|
132
|
+
def get_field(self, field_name: str) -> str:
|
133
|
+
if self.has_field(field_name):
|
134
|
+
return self.fields[field_name].value
|
135
|
+
else:
|
136
|
+
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!")
|
137
|
+
|
138
|
+
def has_field (self, field_name: str) -> bool:
|
139
|
+
return field_name in self.fields
|
140
|
+
|
141
|
+
def __post_init__(self):
|
142
|
+
config = get_config()
|
143
|
+
changes_found = False
|
144
|
+
if not self.has_field(config.anki.word_field):
|
145
|
+
found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
|
146
|
+
if found_alternative_field:
|
147
|
+
logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
|
148
|
+
config.anki.word_field = field
|
149
|
+
changes_found = True
|
150
|
+
|
151
|
+
if not self.has_field(config.anki.sentence_field):
|
152
|
+
found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
|
153
|
+
if found_alternative_field:
|
154
|
+
logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
|
155
|
+
config.anki.sentence_field = field
|
156
|
+
changes_found = True
|
157
|
+
|
158
|
+
if not self.has_field(config.anki.picture_field):
|
159
|
+
found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
|
160
|
+
if found_alternative_field:
|
161
|
+
logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
|
162
|
+
config.anki.picture_field = field
|
163
|
+
changes_found = True
|
164
|
+
|
165
|
+
if not self.has_field(config.anki.sentence_audio_field):
|
166
|
+
found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
|
167
|
+
if found_alternative_field:
|
168
|
+
logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
|
169
|
+
config.anki.sentence_audio_field = field
|
170
|
+
changes_found = True
|
171
|
+
|
172
|
+
if changes_found:
|
173
|
+
save_current_config(config)
|
174
|
+
|
175
|
+
def find_field(self, field, field_type):
|
176
|
+
if field in self.fields:
|
177
|
+
return False, field
|
178
|
+
|
179
|
+
for alt_field in self.alternatives[field_type]:
|
180
|
+
for key in self.fields:
|
181
|
+
if alt_field.lower() == key.lower():
|
182
|
+
return True, key
|
183
|
+
|
184
|
+
return False, None
|
185
|
+
|
186
|
+
|
187
|
+
class VADResult:
|
188
|
+
def __init__(self, success: bool, start: float, end: float, model: str, segments: list = None, output_audio: str = None):
|
189
|
+
self.success = success
|
190
|
+
self.start = start
|
191
|
+
self.end = end
|
192
|
+
self.model = model
|
193
|
+
self.segments = segments if segments is not None else []
|
194
|
+
self.output_audio = output_audio
|
195
|
+
|
196
|
+
def __repr__(self):
|
197
|
+
return f"VADResult(success={self.success}, start={self.start}, end={self.end}, model={self.model}, output_audio={self.output_audio})"
|
198
|
+
|
199
|
+
def trim_successful_string(self):
|
200
|
+
if self.success:
|
201
|
+
if get_config().vad.trim_beginning:
|
202
|
+
return f"Trimmed audio from {self.start:.2f} to {self.end:.2f} seconds using {self.model}."
|
203
|
+
else:
|
204
|
+
return f"Trimmed end of audio to {self.end:.2f} seconds using {self.model}."
|
205
|
+
else:
|
206
|
+
return f"Failed to trim audio using {self.model}."
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import requests
|
2
|
+
from plyer import notification
|
3
|
+
from GameSentenceMiner.util.configuration import logger, is_windows
|
4
|
+
|
5
|
+
if is_windows():
|
6
|
+
from GameSentenceMiner.util.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
|
+
|
23
|
+
def open_browser_window(note_id, query=None):
|
24
|
+
url = "http://localhost:8765"
|
25
|
+
headers = {'Content-Type': 'application/json'}
|
26
|
+
|
27
|
+
data = {
|
28
|
+
"action": "guiBrowse",
|
29
|
+
"version": 6,
|
30
|
+
"params": {
|
31
|
+
"query": f"nid:{note_id}" if not query else query,
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
try:
|
36
|
+
if query:
|
37
|
+
blank_req_data = {
|
38
|
+
"action": "guiBrowse",
|
39
|
+
"version": 6,
|
40
|
+
"params": {
|
41
|
+
"query": "nid:1",
|
42
|
+
}
|
43
|
+
}
|
44
|
+
requests.post(url, json=blank_req_data, headers=headers)
|
45
|
+
response = requests.post(url, json=data, headers=headers)
|
46
|
+
if response.status_code == 200:
|
47
|
+
if query:
|
48
|
+
logger.info(f"Opened Anki browser with query: {query}")
|
49
|
+
else:
|
50
|
+
logger.info(f"Opened Anki note in browser with ID {note_id}")
|
51
|
+
else:
|
52
|
+
logger.error(f"Failed to open Anki note with ID {note_id}")
|
53
|
+
except Exception as e:
|
54
|
+
logger.info(f"Error connecting to AnkiConnect: {e}")
|
55
|
+
|
56
|
+
|
57
|
+
def open_anki_card(note_id):
|
58
|
+
url = "http://localhost:8765"
|
59
|
+
headers = {'Content-Type': 'application/json'}
|
60
|
+
|
61
|
+
data = {
|
62
|
+
"action": "guiEditNote",
|
63
|
+
"version": 6,
|
64
|
+
"params": {
|
65
|
+
"note": note_id
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
try:
|
70
|
+
response = requests.post(url, json=data, headers=headers)
|
71
|
+
if response.status_code == 200:
|
72
|
+
logger.info(f"Opened Anki note with ID {note_id}")
|
73
|
+
else:
|
74
|
+
logger.error(f"Failed to open Anki note with ID {note_id}")
|
75
|
+
except Exception as e:
|
76
|
+
logger.info(f"Error connecting to AnkiConnect: {e}")
|
77
|
+
|
78
|
+
|
79
|
+
def send_notification(title, message, timeout):
|
80
|
+
try:
|
81
|
+
if is_windows():
|
82
|
+
notifier.show_toast(
|
83
|
+
title, message, duration=timeout, threaded=True)
|
84
|
+
else:
|
85
|
+
notification.notify(
|
86
|
+
title=title,
|
87
|
+
message=message,
|
88
|
+
app_name="GameSentenceMiner",
|
89
|
+
timeout=timeout # Notification disappears after 5 seconds
|
90
|
+
)
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"Failed to send notification: {e}")
|
93
|
+
|
94
|
+
|
95
|
+
def send_note_updated(tango):
|
96
|
+
send_notification(
|
97
|
+
title="Anki Card Updated",
|
98
|
+
message=f"Audio and/or Screenshot added to note: {tango}",
|
99
|
+
timeout=5 # Notification disappears after 5 seconds
|
100
|
+
)
|
101
|
+
|
102
|
+
|
103
|
+
def send_screenshot_updated(tango):
|
104
|
+
send_notification(
|
105
|
+
title="Anki Card Updated",
|
106
|
+
message=f"Screenshot updated on note: {tango}",
|
107
|
+
timeout=5 # Notification disappears after 5 seconds
|
108
|
+
)
|
109
|
+
|
110
|
+
|
111
|
+
def send_screenshot_saved(path):
|
112
|
+
send_notification(
|
113
|
+
title="Screenshot Saved",
|
114
|
+
message=f"Screenshot saved to : {path}",
|
115
|
+
timeout=5 # Notification disappears after 5 seconds
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
def send_audio_generated_notification(audio_path):
|
120
|
+
send_notification(
|
121
|
+
title="Audio Trimmed",
|
122
|
+
message=f"Audio Trimmed and placed at {audio_path}",
|
123
|
+
timeout=5 # Notification disappears after 5 seconds
|
124
|
+
)
|
125
|
+
|
126
|
+
|
127
|
+
def send_check_obs_notification(reason):
|
128
|
+
send_notification(
|
129
|
+
title="OBS Replay Invalid",
|
130
|
+
message=f"Check OBS Settings! Reason: {reason}",
|
131
|
+
timeout=5 # Notification disappears after 5 seconds
|
132
|
+
)
|
133
|
+
|
134
|
+
|
135
|
+
def send_error_no_anki_update():
|
136
|
+
send_notification(
|
137
|
+
title="Error",
|
138
|
+
message=f"Anki Card not updated, Check Console for Reason!",
|
139
|
+
timeout=5 # Notification disappears after 5 seconds
|
140
|
+
)
|
141
|
+
|
142
|
+
def send_error_notification(message):
|
143
|
+
send_notification(
|
144
|
+
title="Error",
|
145
|
+
message=message,
|
146
|
+
timeout=5 # Notification disappears after 5 seconds
|
147
|
+
)
|
148
|
+
|
149
|
+
|
150
|
+
if __name__ == "__main__":
|
151
|
+
send_note_updated("TestTango")
|
152
|
+
send_screenshot_updated("TestTango")
|
153
|
+
send_screenshot_saved("C:/Screenshots/test.png")
|
154
|
+
send_audio_generated_notification("C:/Audio/test.mp3")
|
155
|
+
send_check_obs_notification("Replay buffer not active")
|
156
|
+
send_error_no_anki_update()
|
157
|
+
send_error_notification("Custom error message for testing")
|
@@ -0,0 +1,214 @@
|
|
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
|
+
import rapidfuzz
|
8
|
+
|
9
|
+
from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
|
10
|
+
from GameSentenceMiner.util.configuration import logger, get_config, gsm_state
|
11
|
+
from GameSentenceMiner.util.model import AnkiCard
|
12
|
+
import re
|
13
|
+
|
14
|
+
initial_time = datetime.now()
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class GameLine:
|
19
|
+
id: str
|
20
|
+
text: str
|
21
|
+
time: datetime
|
22
|
+
prev: 'GameLine | None'
|
23
|
+
next: 'GameLine | None'
|
24
|
+
index: int = 0
|
25
|
+
scene: str = ""
|
26
|
+
TL: str = ""
|
27
|
+
|
28
|
+
def get_previous_time(self):
|
29
|
+
if self.prev:
|
30
|
+
return self.prev.time
|
31
|
+
return initial_time
|
32
|
+
|
33
|
+
def get_next_time(self):
|
34
|
+
if self.next:
|
35
|
+
return self.next.time
|
36
|
+
return 0
|
37
|
+
|
38
|
+
def set_TL(self, tl: str):
|
39
|
+
self.TL = tl
|
40
|
+
|
41
|
+
def get_stripped_text(self):
|
42
|
+
return self.text.replace('\n', '').strip()
|
43
|
+
|
44
|
+
def __str__(self):
|
45
|
+
return str({"text": self.text, "time": self.time})
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class GameText:
|
50
|
+
values: list[GameLine]
|
51
|
+
values_dict: dict[str, GameLine]
|
52
|
+
game_line_index = 0
|
53
|
+
|
54
|
+
def __init__(self):
|
55
|
+
self.values = []
|
56
|
+
self.values_dict = {}
|
57
|
+
|
58
|
+
def __getitem__(self, index):
|
59
|
+
return self.values[index]
|
60
|
+
|
61
|
+
def get_by_id(self, line_id: str) -> Optional[GameLine]:
|
62
|
+
if not self.values_dict:
|
63
|
+
return None
|
64
|
+
return self.values_dict.get(line_id)
|
65
|
+
|
66
|
+
def get_time(self, line_text: str, occurrence: int = -1) -> datetime:
|
67
|
+
matches = [line for line in self.values if line.text == line_text]
|
68
|
+
if matches:
|
69
|
+
return matches[occurrence].time # Default to latest
|
70
|
+
return initial_time
|
71
|
+
|
72
|
+
def get_event(self, line_text: str, occurrence: int = -1) -> GameLine | None:
|
73
|
+
matches = [line for line in self.values if line.text == line_text]
|
74
|
+
if matches:
|
75
|
+
return matches[occurrence]
|
76
|
+
return None
|
77
|
+
|
78
|
+
def add_line(self, line_text, line_time=None):
|
79
|
+
if not line_text:
|
80
|
+
return
|
81
|
+
line_id = str(uuid.uuid1())
|
82
|
+
new_line = GameLine(
|
83
|
+
id=line_id, # Time-based UUID as an integer
|
84
|
+
text=line_text,
|
85
|
+
time=line_time or datetime.now(),
|
86
|
+
prev=self.values[-1] if self.values else None,
|
87
|
+
next=None,
|
88
|
+
index=self.game_line_index,
|
89
|
+
scene=gsm_state.current_game or ""
|
90
|
+
)
|
91
|
+
self.values_dict[line_id] = new_line
|
92
|
+
logger.debug(f"Adding line: {new_line}")
|
93
|
+
self.game_line_index += 1
|
94
|
+
if self.values:
|
95
|
+
self.values[-1].next = new_line
|
96
|
+
self.values.append(new_line)
|
97
|
+
# self.remove_old_events(datetime.now() - timedelta(minutes=10))
|
98
|
+
|
99
|
+
def has_line(self, line_text) -> bool:
|
100
|
+
for game_line in self.values:
|
101
|
+
if game_line.text == line_text:
|
102
|
+
return True
|
103
|
+
return False
|
104
|
+
|
105
|
+
def get_last_line(self):
|
106
|
+
if self.values:
|
107
|
+
return self.values[-1]
|
108
|
+
return None
|
109
|
+
|
110
|
+
|
111
|
+
game_log = GameText()
|
112
|
+
|
113
|
+
def strip_whitespace_and_punctuation(text: str) -> str:
|
114
|
+
"""
|
115
|
+
Strips whitespace and punctuation from the given text.
|
116
|
+
"""
|
117
|
+
# Remove all whitespace and specified punctuation using regex
|
118
|
+
# Includes Japanese and common punctuation
|
119
|
+
return re.sub(r'[\s 、。「」【】《》., ]', '', text).strip()
|
120
|
+
|
121
|
+
|
122
|
+
def lines_match(texthooker_sentence, anki_sentence):
|
123
|
+
# Replace newlines, spaces, other whitespace characters, AND japanese punctuation
|
124
|
+
texthooker_sentence = strip_whitespace_and_punctuation(texthooker_sentence)
|
125
|
+
anki_sentence = strip_whitespace_and_punctuation(anki_sentence)
|
126
|
+
similarity = rapidfuzz.fuzz.ratio(texthooker_sentence, anki_sentence)
|
127
|
+
logger.debug(f"Comparing sentences: '{texthooker_sentence}' and '{anki_sentence}' - Similarity: {similarity}")
|
128
|
+
if texthooker_sentence in anki_sentence:
|
129
|
+
logger.debug(f"One contains the other: {texthooker_sentence} in {anki_sentence} - Similarity: {similarity}")
|
130
|
+
elif anki_sentence in texthooker_sentence:
|
131
|
+
logger.debug(f"One contains the other: {anki_sentence} in {texthooker_sentence} - Similarity: {similarity}")
|
132
|
+
return (anki_sentence in texthooker_sentence) or (texthooker_sentence in anki_sentence) or (similarity >= 80)
|
133
|
+
|
134
|
+
|
135
|
+
def get_text_event(last_note) -> GameLine:
|
136
|
+
lines = game_log.values
|
137
|
+
|
138
|
+
if not lines:
|
139
|
+
raise Exception("No voicelines in GSM. GSM can only do work on text that has been sent to it since it started. If you are not getting any text into GSM, please check your setup/config.")
|
140
|
+
|
141
|
+
if not last_note:
|
142
|
+
return lines[-1]
|
143
|
+
|
144
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
145
|
+
if not sentence:
|
146
|
+
return lines[-1]
|
147
|
+
|
148
|
+
for line in reversed(lines):
|
149
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)):
|
150
|
+
return line
|
151
|
+
|
152
|
+
logger.info("Could not find matching sentence from GSM's history. Using the latest line.")
|
153
|
+
return lines[-1]
|
154
|
+
|
155
|
+
|
156
|
+
def get_line_and_future_lines(last_note):
|
157
|
+
if not last_note:
|
158
|
+
return []
|
159
|
+
|
160
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
161
|
+
found_lines = []
|
162
|
+
if sentence:
|
163
|
+
found = False
|
164
|
+
for line in game_log.values:
|
165
|
+
if found:
|
166
|
+
found_lines.append(line)
|
167
|
+
if lines_match(line.text, remove_html_and_cloze_tags(sentence)): # 80% similarity threshold
|
168
|
+
found = True
|
169
|
+
found_lines.append(line)
|
170
|
+
return found_lines
|
171
|
+
|
172
|
+
|
173
|
+
def get_mined_line(last_note: AnkiCard, lines=None):
|
174
|
+
if lines is None:
|
175
|
+
lines = []
|
176
|
+
if not last_note:
|
177
|
+
return lines[-1]
|
178
|
+
if not lines:
|
179
|
+
lines = get_all_lines()
|
180
|
+
if not lines:
|
181
|
+
raise Exception("No voicelines in GSM. GSM can only do work on text that has been sent to it since it started. If you are not getting any text into GSM, please check your setup/config.")
|
182
|
+
|
183
|
+
sentence = last_note.get_field(get_config().anki.sentence_field)
|
184
|
+
for line in reversed(lines):
|
185
|
+
if lines_match(line.get_stripped_text(), remove_html_and_cloze_tags(sentence)):
|
186
|
+
return line
|
187
|
+
return lines[-1]
|
188
|
+
|
189
|
+
|
190
|
+
def get_time_of_line(line):
|
191
|
+
return game_log.get_time(line)
|
192
|
+
|
193
|
+
|
194
|
+
def get_all_lines():
|
195
|
+
return game_log.values
|
196
|
+
|
197
|
+
|
198
|
+
def get_text_log() -> GameText:
|
199
|
+
return game_log
|
200
|
+
|
201
|
+
def add_line(current_line_after_regex, line_time):
|
202
|
+
game_log.add_line(current_line_after_regex, line_time)
|
203
|
+
|
204
|
+
def get_line_by_id(line_id: str) -> Optional[GameLine]:
|
205
|
+
"""
|
206
|
+
Retrieve a GameLine by its unique ID.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
line_id (str): The unique identifier of the GameLine.
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
Optional[GameLine]: The GameLine object if found, otherwise None.
|
213
|
+
"""
|
214
|
+
return game_log.get_by_id(line_id)
|