GameSentenceMiner 2.3.7__py3-none-any.whl → 2.3.8__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 +11 -13
- GameSentenceMiner/config_gui.py +2 -5
- GameSentenceMiner/ffmpeg.py +14 -7
- GameSentenceMiner/gametext.py +148 -71
- GameSentenceMiner/gsm.py +27 -12
- GameSentenceMiner/utility_gui.py +23 -22
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.3.8.dist-info}/METADATA +1 -1
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.3.8.dist-info}/RECORD +11 -11
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.3.8.dist-info}/WHEEL +1 -1
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.3.8.dist-info}/entry_points.txt +0 -0
- {GameSentenceMiner-2.3.7.dist-info → gamesentenceminer-2.3.8.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import base64
|
2
|
-
import queue
|
3
2
|
import subprocess
|
4
3
|
import threading
|
5
4
|
import time
|
@@ -11,7 +10,7 @@ from GameSentenceMiner import obs, util, notification, ffmpeg, gametext
|
|
11
10
|
|
12
11
|
from GameSentenceMiner.configuration import *
|
13
12
|
from GameSentenceMiner.configuration import get_config
|
14
|
-
from GameSentenceMiner.gametext import
|
13
|
+
from GameSentenceMiner.gametext import get_text_event
|
15
14
|
from GameSentenceMiner.obs import get_current_game
|
16
15
|
from GameSentenceMiner.util import remove_html_tags
|
17
16
|
|
@@ -26,7 +25,7 @@ card_queue = []
|
|
26
25
|
|
27
26
|
|
28
27
|
def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
|
29
|
-
should_update_audio=True, ss_time=0):
|
28
|
+
should_update_audio=True, ss_time=0, game_line=None):
|
30
29
|
global audio_in_anki, screenshot_in_anki, prev_screenshot_in_anki
|
31
30
|
update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
|
32
31
|
last_note['fields'][get_config().anki.sentence_audio_field][
|
@@ -44,12 +43,10 @@ def update_anki_card(last_note, note=None, audio_path='', video_path='', tango='
|
|
44
43
|
if get_config().paths.remove_screenshot:
|
45
44
|
os.remove(screenshot)
|
46
45
|
if get_config().anki.previous_image_field:
|
47
|
-
|
48
|
-
prev_screenshot = ffmpeg.get_screenshot(video_path, ffmpeg.get_screenshot_time(video_path, gametext.get_time_of_line(previous_sentence)))
|
46
|
+
prev_screenshot = ffmpeg.get_screenshot(video_path, ffmpeg.get_screenshot_time(video_path, game_line.prev))
|
49
47
|
prev_screenshot_in_anki = store_media_file(prev_screenshot)
|
50
48
|
if get_config().paths.remove_screenshot:
|
51
49
|
os.remove(prev_screenshot)
|
52
|
-
util.set_last_mined_line(get_sentence(last_note))
|
53
50
|
audio_html = f"[sound:{audio_in_anki}]"
|
54
51
|
image_html = f"<img src=\"{screenshot_in_anki}\">"
|
55
52
|
prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
|
@@ -122,7 +119,7 @@ def get_initial_card_info(last_note, selected_lines):
|
|
122
119
|
note = {'id': last_note['noteId'], 'fields': {}}
|
123
120
|
if not last_note:
|
124
121
|
return note
|
125
|
-
|
122
|
+
game_line = get_text_event(last_note)
|
126
123
|
|
127
124
|
if get_config().audio.mining_from_history_grab_all_audio and get_config().anki.multi_overwrites_sentence:
|
128
125
|
lines = gametext.get_line_and_future_lines(last_note)
|
@@ -130,13 +127,13 @@ def get_initial_card_info(last_note, selected_lines):
|
|
130
127
|
note['fields'][get_config().anki.sentence_field] = "".join(lines)
|
131
128
|
|
132
129
|
if selected_lines and get_config().anki.multi_overwrites_sentence:
|
133
|
-
note['fields'][get_config().anki.sentence_field] = "".join(selected_lines)
|
130
|
+
note['fields'][get_config().anki.sentence_field] = "".join([line.text for line in selected_lines])
|
134
131
|
|
135
|
-
|
136
|
-
f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and previous_line and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
|
137
|
-
if get_config().anki.previous_sentence_field and previous_line and not \
|
132
|
+
if get_config().anki.previous_sentence_field and game_line.prev and not \
|
138
133
|
last_note['fields'][get_config().anki.previous_sentence_field]['value']:
|
139
|
-
|
134
|
+
logger.debug(
|
135
|
+
f"Adding Previous Sentence: {get_config().anki.previous_sentence_field and game_line.prev.text and not last_note['fields'][get_config().anki.previous_sentence_field]['value']}")
|
136
|
+
note['fields'][get_config().anki.previous_sentence_field] = game_line.prev.text
|
140
137
|
return note
|
141
138
|
|
142
139
|
|
@@ -227,7 +224,8 @@ def update_new_card():
|
|
227
224
|
if get_config().obs.get_game_from_scene:
|
228
225
|
obs.update_current_game()
|
229
226
|
if use_prev_audio:
|
230
|
-
|
227
|
+
with util.lock:
|
228
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card, []), reuse_audio=True)
|
231
229
|
else:
|
232
230
|
logger.info("New card(s) detected! Added to Processing Queue!")
|
233
231
|
card_queue.append(last_card)
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -51,6 +51,7 @@ class HoverInfoWidget:
|
|
51
51
|
class ConfigApp:
|
52
52
|
def __init__(self, root):
|
53
53
|
self.window = root
|
54
|
+
self.on_exit = None
|
54
55
|
# self.window = ttk.Window(themename='darkly')
|
55
56
|
self.window.title('GameSentenceMiner Configuration')
|
56
57
|
self.window.protocol("WM_DELETE_WINDOW", self.hide)
|
@@ -82,10 +83,6 @@ class ConfigApp:
|
|
82
83
|
def add_save_hook(self, func):
|
83
84
|
on_save.append(func)
|
84
85
|
|
85
|
-
def add_exit_hook(self, func):
|
86
|
-
global exit_func
|
87
|
-
exit_func = func
|
88
|
-
|
89
86
|
def show(self):
|
90
87
|
obs.update_current_game()
|
91
88
|
self.reload_settings()
|
@@ -103,7 +100,7 @@ class ConfigApp:
|
|
103
100
|
if update_available:
|
104
101
|
messagebox.showinfo("Update", "GSM Will Copy the Update Command to your clipboard, please run it in a terminal.")
|
105
102
|
pyperclip.copy("pip install --upgrade GameSentenceMiner")
|
106
|
-
|
103
|
+
self.on_exit(None, None)
|
107
104
|
else:
|
108
105
|
messagebox.showinfo("No Update Found", "No update found.")
|
109
106
|
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -5,6 +5,8 @@ import time
|
|
5
5
|
from GameSentenceMiner import obs, util, configuration
|
6
6
|
from GameSentenceMiner.configuration import *
|
7
7
|
from GameSentenceMiner.util import *
|
8
|
+
from GameSentenceMiner.gametext import initial_time
|
9
|
+
|
8
10
|
|
9
11
|
def get_ffmpeg_path():
|
10
12
|
return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if util.is_windows() else "ffmpeg"
|
@@ -47,7 +49,12 @@ def get_screenshot(video_file, time_from_end):
|
|
47
49
|
return output_image
|
48
50
|
|
49
51
|
|
50
|
-
def get_screenshot_time(video_path,
|
52
|
+
def get_screenshot_time(video_path, game_line):
|
53
|
+
if game_line:
|
54
|
+
line_time = game_line.time
|
55
|
+
else:
|
56
|
+
line_time = initial_time
|
57
|
+
|
51
58
|
file_length = get_video_duration(video_path)
|
52
59
|
file_mod_time = get_file_modification_time(video_path)
|
53
60
|
|
@@ -115,7 +122,7 @@ def get_audio_codec(video_path):
|
|
115
122
|
return None
|
116
123
|
|
117
124
|
|
118
|
-
def get_audio_and_trim(video_path,
|
125
|
+
def get_audio_and_trim(video_path, game_line, next_line_time):
|
119
126
|
supported_formats = {
|
120
127
|
'opus': 'libopus',
|
121
128
|
'mp3': 'libmp3lame',
|
@@ -149,7 +156,7 @@ def get_audio_and_trim(video_path, line_time, next_line_time):
|
|
149
156
|
|
150
157
|
subprocess.run(command)
|
151
158
|
|
152
|
-
return trim_audio_based_on_last_line(untrimmed_audio, video_path,
|
159
|
+
return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time)
|
153
160
|
|
154
161
|
|
155
162
|
def get_video_duration(file_path):
|
@@ -167,12 +174,12 @@ def get_video_duration(file_path):
|
|
167
174
|
return float(duration_info["format"]["duration"]) # Return the duration in seconds
|
168
175
|
|
169
176
|
|
170
|
-
def trim_audio_based_on_last_line(untrimmed_audio, video_path,
|
177
|
+
def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line):
|
171
178
|
trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
|
172
179
|
suffix=f".{get_config().audio.extension}").name
|
173
180
|
file_mod_time = get_file_modification_time(video_path)
|
174
181
|
file_length = get_video_duration(video_path)
|
175
|
-
time_delta = file_mod_time -
|
182
|
+
time_delta = file_mod_time - game_line.time
|
176
183
|
# Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
|
177
184
|
total_seconds = file_length - time_delta.total_seconds() + get_config().audio.beginning_offset
|
178
185
|
if total_seconds < 0 or total_seconds >= file_length:
|
@@ -186,8 +193,8 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, line_time, next_l
|
|
186
193
|
ffmpeg_command = ffmpeg_base_command_list + [
|
187
194
|
"-i", untrimmed_audio,
|
188
195
|
"-ss", start_trim_time]
|
189
|
-
if next_line and next_line >
|
190
|
-
end_total_seconds = total_seconds + (next_line -
|
196
|
+
if next_line and next_line > game_line.time and not get_config().audio.mining_from_history_grab_all_audio:
|
197
|
+
end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
|
191
198
|
hours, remainder = divmod(end_total_seconds, 3600)
|
192
199
|
minutes, seconds = divmod(remainder, 60)
|
193
200
|
end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
GameSentenceMiner/gametext.py
CHANGED
@@ -2,7 +2,6 @@ import asyncio
|
|
2
2
|
import re
|
3
3
|
import threading
|
4
4
|
import time
|
5
|
-
from collections import OrderedDict
|
6
5
|
from datetime import datetime
|
7
6
|
from typing import Callable
|
8
7
|
|
@@ -21,10 +20,61 @@ current_line = ''
|
|
21
20
|
current_line_after_regex = ''
|
22
21
|
current_line_time = datetime.now()
|
23
22
|
|
24
|
-
line_history = OrderedDict()
|
25
23
|
reconnecting = False
|
26
24
|
multi_mine_event_bus: Callable[[str, datetime], None] = None
|
27
25
|
|
26
|
+
@dataclass
|
27
|
+
class GameLine:
|
28
|
+
text: str
|
29
|
+
time: datetime
|
30
|
+
prev: 'GameLine'
|
31
|
+
next: 'GameLine'
|
32
|
+
|
33
|
+
def get_previous_time(self):
|
34
|
+
if self.prev:
|
35
|
+
return self.prev.time
|
36
|
+
return initial_time
|
37
|
+
|
38
|
+
def get_next_time(self):
|
39
|
+
if self.next:
|
40
|
+
return self.next.time
|
41
|
+
return 0
|
42
|
+
|
43
|
+
@dataclass
|
44
|
+
class GameText:
|
45
|
+
values: list[GameLine]
|
46
|
+
|
47
|
+
def __init__(self):
|
48
|
+
self.values = []
|
49
|
+
|
50
|
+
def __getitem__(self, key):
|
51
|
+
return self.values[key]
|
52
|
+
|
53
|
+
def get_time(self, line_text: str, occurrence: int = -1) -> datetime:
|
54
|
+
matches = [line for line in self.values if line.text == line_text]
|
55
|
+
if matches:
|
56
|
+
return matches[occurrence].time # Default to latest
|
57
|
+
return initial_time
|
58
|
+
|
59
|
+
def get_event(self, line_text: str, occurrence: int = -1) -> GameLine | None:
|
60
|
+
matches = [line for line in self.values if line.text == line_text]
|
61
|
+
if matches:
|
62
|
+
return matches[occurrence]
|
63
|
+
return None
|
64
|
+
|
65
|
+
def add_line(self, line_text):
|
66
|
+
new_line = GameLine(line_text, datetime.now(), self.values[-1] if self.values else None, None)
|
67
|
+
if self.values:
|
68
|
+
self.values[-1].next = new_line
|
69
|
+
self.values.append(new_line)
|
70
|
+
|
71
|
+
def has_line(self, line_text) -> bool:
|
72
|
+
for game_line in self.values:
|
73
|
+
if game_line.text == line_text:
|
74
|
+
return True
|
75
|
+
return False
|
76
|
+
|
77
|
+
line_history = GameText()
|
28
78
|
|
29
79
|
class ClipboardMonitor(threading.Thread):
|
30
80
|
|
@@ -80,8 +130,8 @@ def handle_new_text_event(current_clipboard):
|
|
80
130
|
else:
|
81
131
|
current_line_after_regex = current_line
|
82
132
|
current_line_time = datetime.now()
|
83
|
-
line_history
|
84
|
-
multi_mine_event_bus(
|
133
|
+
line_history.add_line(current_line_after_regex)
|
134
|
+
multi_mine_event_bus(line_history[-1])
|
85
135
|
logger.debug(f"New Line: {current_clipboard}")
|
86
136
|
|
87
137
|
|
@@ -89,7 +139,6 @@ def reset_line_hotkey_pressed():
|
|
89
139
|
global current_line_time
|
90
140
|
logger.info("LINE RESET HOTKEY PRESSED")
|
91
141
|
current_line_time = datetime.now()
|
92
|
-
line_history[current_line_after_regex] = current_line_time
|
93
142
|
util.set_last_mined_line("")
|
94
143
|
|
95
144
|
|
@@ -107,72 +156,91 @@ def start_text_monitor(send_to_mine_event_bus):
|
|
107
156
|
text_thread.start()
|
108
157
|
|
109
158
|
|
110
|
-
def get_line_timing(last_note):
|
111
|
-
|
112
|
-
|
159
|
+
# def get_line_timing(last_note):
|
160
|
+
# def similar(a, b):
|
161
|
+
# return SequenceMatcher(None, a, b).ratio()
|
162
|
+
#
|
163
|
+
# if not last_note:
|
164
|
+
# return current_line_time, 0
|
165
|
+
#
|
166
|
+
# lines = line_history.values
|
167
|
+
#
|
168
|
+
# line_time = current_line_time
|
169
|
+
# next_line = 0
|
170
|
+
# prev_clip_time = 0
|
171
|
+
#
|
172
|
+
# try:
|
173
|
+
# sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
174
|
+
# if sentence:
|
175
|
+
# for line in reversed(lines):
|
176
|
+
# similarity = similar(remove_html_tags(sentence), line.text)
|
177
|
+
# if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
|
178
|
+
# line_time = line.time
|
179
|
+
# next_line = prev_clip_time
|
180
|
+
# break
|
181
|
+
# prev_clip_time = line.time
|
182
|
+
# except Exception as e:
|
183
|
+
# logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
|
184
|
+
#
|
185
|
+
# return line_time, next_line
|
186
|
+
#
|
187
|
+
#
|
188
|
+
# def get_last_two_sentences(last_note) -> (str, str):
|
189
|
+
# def similar(a, b):
|
190
|
+
# return SequenceMatcher(None, a, b).ratio()
|
191
|
+
#
|
192
|
+
# lines = line_history.values
|
193
|
+
#
|
194
|
+
# if not last_note:
|
195
|
+
# return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
|
196
|
+
#
|
197
|
+
# sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
198
|
+
# if not sentence:
|
199
|
+
# return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
|
200
|
+
#
|
201
|
+
# current, previous = "", ""
|
202
|
+
# found = False
|
203
|
+
#
|
204
|
+
# for line in reversed(lines):
|
205
|
+
# similarity = similar(remove_html_tags(sentence), line.text)
|
206
|
+
# logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
|
207
|
+
# if found:
|
208
|
+
# previous = line.text
|
209
|
+
# break
|
210
|
+
# if similarity >= 0.60 or line.text in remove_html_tags(sentence):
|
211
|
+
# found = True
|
212
|
+
# current = line.text
|
213
|
+
#
|
214
|
+
# if not current or not previous:
|
215
|
+
# logger.debug("Couldn't find lines in history, using last two lines")
|
216
|
+
# return lines[-1].text if lines else '', lines[-2].text if len(lines) > 1 else ''
|
217
|
+
#
|
218
|
+
# return current, previous
|
219
|
+
|
220
|
+
def similar(a, b):
|
221
|
+
return SequenceMatcher(None, a, b).ratio()
|
222
|
+
|
223
|
+
def get_text_event(last_note) -> GameLine:
|
224
|
+
lines = line_history.values
|
113
225
|
|
114
226
|
if not last_note:
|
115
|
-
return
|
116
|
-
|
117
|
-
line_time = current_line_time
|
118
|
-
next_line = 0
|
119
|
-
prev_clip_time = 0
|
120
|
-
|
121
|
-
try:
|
122
|
-
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
123
|
-
if sentence:
|
124
|
-
for i, (line, clip_time) in enumerate(reversed(line_history.items())):
|
125
|
-
similarity = similar(remove_html_tags(sentence), line)
|
126
|
-
if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
|
127
|
-
line_time = clip_time
|
128
|
-
next_line = prev_clip_time
|
129
|
-
break
|
130
|
-
prev_clip_time = clip_time
|
131
|
-
except Exception as e:
|
132
|
-
logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
|
133
|
-
|
134
|
-
return line_time, next_line
|
135
|
-
|
136
|
-
|
137
|
-
def get_last_two_sentences(last_note):
|
138
|
-
def similar(a, b):
|
139
|
-
return SequenceMatcher(None, a, b).ratio()
|
140
|
-
lines = list(line_history.items())
|
141
|
-
|
142
|
-
if not last_note:
|
143
|
-
return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
|
144
|
-
|
145
|
-
current = ""
|
146
|
-
previous = ""
|
227
|
+
return lines[-1]
|
147
228
|
|
148
229
|
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
149
|
-
if sentence:
|
150
|
-
|
151
|
-
for i, (line, clip_time) in enumerate(reversed(lines)):
|
152
|
-
similarity = similar(remove_html_tags(sentence), line)
|
153
|
-
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
|
154
|
-
if found:
|
155
|
-
previous = line
|
156
|
-
break
|
157
|
-
if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
|
158
|
-
found = True
|
159
|
-
current = line
|
160
|
-
|
161
|
-
logger.debug(f"Current Line: {current}")
|
162
|
-
logger.debug(f"Previous Line: {previous}")
|
230
|
+
if not sentence:
|
231
|
+
return lines[-1]
|
163
232
|
|
164
|
-
|
165
|
-
|
166
|
-
|
233
|
+
for line in reversed(lines):
|
234
|
+
similarity = similar(remove_html_tags(sentence), line.text)
|
235
|
+
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
|
236
|
+
if similarity >= 0.60 or line.text in remove_html_tags(sentence):
|
237
|
+
return line
|
167
238
|
|
168
|
-
|
239
|
+
logger.debug("Couldn't find a match in history, using last event")
|
240
|
+
return lines[-1]
|
169
241
|
|
170
242
|
|
171
243
|
def get_line_and_future_lines(last_note):
|
172
|
-
def similar(a, b):
|
173
|
-
return SequenceMatcher(None, a, b).ratio()
|
174
|
-
lines = list(line_history.items())
|
175
|
-
|
176
244
|
if not last_note:
|
177
245
|
return []
|
178
246
|
|
@@ -180,18 +248,27 @@ def get_line_and_future_lines(last_note):
|
|
180
248
|
found_lines = []
|
181
249
|
if sentence:
|
182
250
|
found = False
|
183
|
-
for
|
184
|
-
similarity = similar(remove_html_tags(sentence), line)
|
185
|
-
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line} - Similarity: {similarity}")
|
251
|
+
for line in line_history.values:
|
252
|
+
similarity = similar(remove_html_tags(sentence), line.text)
|
253
|
+
logger.debug(f"Comparing: {remove_html_tags(sentence)} with {line.text} - Similarity: {similarity}")
|
186
254
|
if found:
|
187
|
-
found_lines.append(line)
|
188
|
-
if similarity >= 0.60 or line in remove_html_tags(sentence): # 80% similarity threshold
|
255
|
+
found_lines.append(line.text)
|
256
|
+
if similarity >= 0.60 or line.text in remove_html_tags(sentence): # 80% similarity threshold
|
189
257
|
found = True
|
190
|
-
found_lines.append(line)
|
258
|
+
found_lines.append(line.text)
|
191
259
|
return found_lines
|
192
260
|
|
261
|
+
def get_mined_line(last_note, lines):
|
262
|
+
if not last_note:
|
263
|
+
return lines[0]
|
264
|
+
|
265
|
+
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
266
|
+
for line2 in lines:
|
267
|
+
similarity = similar(remove_html_tags(sentence), line2.text)
|
268
|
+
if similarity >= 0.60 or line2.text in remove_html_tags(sentence):
|
269
|
+
return line2
|
270
|
+
return lines[0]
|
271
|
+
|
193
272
|
|
194
273
|
def get_time_of_line(line):
|
195
|
-
|
196
|
-
return line_history[line]
|
197
|
-
return initial_time
|
274
|
+
return line_history.get_time(line)
|
GameSentenceMiner/gsm.py
CHANGED
@@ -25,7 +25,7 @@ from GameSentenceMiner.downloader.download_tools import download_obs_if_needed,
|
|
25
25
|
from GameSentenceMiner.vad import vosk_helper, silero_trim, whisper_helper
|
26
26
|
from GameSentenceMiner.configuration import *
|
27
27
|
from GameSentenceMiner.ffmpeg import get_audio_and_trim
|
28
|
-
from GameSentenceMiner.gametext import
|
28
|
+
from GameSentenceMiner.gametext import get_text_event, get_mined_line
|
29
29
|
from GameSentenceMiner.util import *
|
30
30
|
|
31
31
|
if is_windows():
|
@@ -57,6 +57,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
57
57
|
if anki.card_queue and len(anki.card_queue) > 0:
|
58
58
|
last_note = anki.card_queue.pop(0)
|
59
59
|
with util.lock:
|
60
|
+
util.set_last_mined_line(anki.get_sentence(last_note))
|
60
61
|
if os.path.exists(video_path) and os.access(video_path, os.R_OK):
|
61
62
|
logger.debug(f"Video found and is readable: {video_path}")
|
62
63
|
|
@@ -73,12 +74,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
73
74
|
last_note = anki.get_last_anki_card()
|
74
75
|
if get_config().features.backfill_audio:
|
75
76
|
last_note = anki.get_cards_by_sentence(gametext.current_line)
|
76
|
-
|
77
|
+
line_cutoff = None
|
78
|
+
start_line = None
|
79
|
+
mined_line = get_text_event(last_note)
|
80
|
+
if mined_line:
|
81
|
+
start_line = mined_line
|
82
|
+
if mined_line.next:
|
83
|
+
line_cutoff = mined_line.next.time
|
84
|
+
|
77
85
|
if utility_window.lines_selected():
|
78
|
-
|
86
|
+
lines = utility_window.get_selected_lines()
|
87
|
+
start_line = lines[0]
|
88
|
+
mined_line = get_mined_line(last_note, lines)
|
89
|
+
line_cutoff = utility_window.get_next_line_timing()
|
90
|
+
|
79
91
|
ss_timing = 0
|
80
|
-
if
|
81
|
-
ss_timing = ffmpeg.get_screenshot_time(video_path,
|
92
|
+
if mined_line and line_cutoff or mined_line and get_config().screenshot.use_beginning_of_line_as_screenshot:
|
93
|
+
ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line)
|
82
94
|
if last_note:
|
83
95
|
logger.debug(json.dumps(last_note))
|
84
96
|
|
@@ -89,8 +101,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
89
101
|
if get_config().anki.sentence_audio_field:
|
90
102
|
logger.debug("Attempting to get audio from video")
|
91
103
|
final_audio_output, should_update_audio, vad_trimmed_audio = VideoToAudioHandler.get_audio(
|
92
|
-
|
93
|
-
|
104
|
+
start_line,
|
105
|
+
line_cutoff,
|
94
106
|
video_path)
|
95
107
|
else:
|
96
108
|
final_audio_output = ""
|
@@ -103,7 +115,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
103
115
|
anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
|
104
116
|
tango=tango,
|
105
117
|
should_update_audio=should_update_audio,
|
106
|
-
ss_time=ss_timing
|
118
|
+
ss_time=ss_timing,
|
119
|
+
game_line=start_line)
|
107
120
|
elif get_config().features.notify_on_update and should_update_audio:
|
108
121
|
notification.send_audio_generated_notification(vad_trimmed_audio)
|
109
122
|
except Exception as e:
|
@@ -118,8 +131,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
118
131
|
utility_window.reset_checkboxes()
|
119
132
|
|
120
133
|
@staticmethod
|
121
|
-
def get_audio(
|
122
|
-
trimmed_audio = get_audio_and_trim(video_path,
|
134
|
+
def get_audio(game_line, next_line_time, video_path):
|
135
|
+
trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
|
123
136
|
vad_trimmed_audio = make_unique_file_name(
|
124
137
|
f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
125
138
|
final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}"))
|
@@ -259,10 +272,12 @@ def open_log():
|
|
259
272
|
logger.info("Log opened.")
|
260
273
|
|
261
274
|
|
262
|
-
def exit_program(
|
275
|
+
def exit_program(passed_icon, item):
|
263
276
|
"""Exit the application."""
|
277
|
+
if not passed_icon:
|
278
|
+
passed_icon = icon
|
264
279
|
logger.info("Exiting...")
|
265
|
-
|
280
|
+
passed_icon.stop()
|
266
281
|
cleanup()
|
267
282
|
|
268
283
|
|
GameSentenceMiner/utility_gui.py
CHANGED
@@ -35,16 +35,16 @@ class UtilityApp:
|
|
35
35
|
self.checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
36
36
|
|
37
37
|
# Add existing items
|
38
|
-
for
|
39
|
-
self.add_checkbox_to_gui(
|
38
|
+
for line, var in self.items:
|
39
|
+
self.add_checkbox_to_gui(line, var)
|
40
40
|
else:
|
41
41
|
self.multi_mine_window.deiconify()
|
42
42
|
self.multi_mine_window.lift()
|
43
43
|
|
44
|
-
def add_text(self,
|
45
|
-
if text:
|
44
|
+
def add_text(self, line):
|
45
|
+
if line.text:
|
46
46
|
var = tk.BooleanVar()
|
47
|
-
self.items.append((
|
47
|
+
self.items.append((line, var))
|
48
48
|
|
49
49
|
if len(self.items) > 10:
|
50
50
|
if self.checkboxes:
|
@@ -53,12 +53,12 @@ class UtilityApp:
|
|
53
53
|
self.items.pop(0)
|
54
54
|
|
55
55
|
if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
|
56
|
-
self.add_checkbox_to_gui(
|
56
|
+
self.add_checkbox_to_gui(line, var)
|
57
57
|
|
58
|
-
def add_checkbox_to_gui(self,
|
58
|
+
def add_checkbox_to_gui(self, line, var):
|
59
59
|
""" Add a single checkbox without repainting everything. """
|
60
60
|
if self.checkbox_frame:
|
61
|
-
chk = ttk.Checkbutton(self.checkbox_frame, text=f"{time.strftime('%H:%M:%S')} - {text}", variable=var)
|
61
|
+
chk = ttk.Checkbutton(self.checkbox_frame, text=f"{line.time.strftime('%H:%M:%S')} - {line.text}", variable=var)
|
62
62
|
chk.pack(anchor='w')
|
63
63
|
self.checkboxes.append(chk)
|
64
64
|
|
@@ -73,30 +73,31 @@ class UtilityApp:
|
|
73
73
|
# chk.pack(anchor='w')
|
74
74
|
|
75
75
|
def get_selected_lines(self):
|
76
|
-
filtered_items = [
|
77
|
-
return filtered_items if len(filtered_items)
|
76
|
+
filtered_items = [line for line, var in self.items if var.get()]
|
77
|
+
return filtered_items if len(filtered_items) > 0 else []
|
78
78
|
|
79
|
-
def get_selected_times(self):
|
80
|
-
filtered_times = [time for _, var, time in self.items if var.get()]
|
81
79
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
80
|
+
def get_next_line_timing(self):
|
81
|
+
selected_lines = [line for line, var in self.items if var.get()]
|
82
|
+
|
83
|
+
if len(selected_lines) >= 2:
|
84
|
+
last_checked_index = max(i for i, (_, var) in enumerate(self.items) if var.get())
|
86
85
|
|
87
|
-
# Get the time AFTER the last checked checkbox, if it exists
|
88
86
|
if last_checked_index + 1 < len(self.items):
|
89
|
-
next_time = self.items[last_checked_index + 1][
|
87
|
+
next_time = self.items[last_checked_index + 1][0].time
|
90
88
|
else:
|
91
89
|
next_time = 0
|
92
90
|
|
93
|
-
return
|
91
|
+
return next_time
|
92
|
+
if len(selected_lines) == 1:
|
93
|
+
return selected_lines[0].get_next_time()
|
94
94
|
|
95
95
|
return None
|
96
96
|
|
97
|
+
|
97
98
|
def lines_selected(self):
|
98
|
-
filter_times = [time for
|
99
|
-
if len(filter_times)
|
99
|
+
filter_times = [line.time for line, var in self.items if var.get()]
|
100
|
+
if len(filter_times) > 0:
|
100
101
|
return True
|
101
102
|
return False
|
102
103
|
|
@@ -114,7 +115,7 @@ class UtilityApp:
|
|
114
115
|
# found_unchecked = True
|
115
116
|
|
116
117
|
def reset_checkboxes(self):
|
117
|
-
for _, var
|
118
|
+
for _, var in self.items:
|
118
119
|
var.set(False)
|
119
120
|
# if self.multi_mine_window:
|
120
121
|
# for checkbox in self.checkboxes:
|
@@ -1,16 +1,16 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=
|
3
|
-
GameSentenceMiner/config_gui.py,sha256
|
2
|
+
GameSentenceMiner/anki.py,sha256=QBrA-WVQfK4KjKB2Wm1WDW7BlVy8MF6-1ByTApVzPVA,10576
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=nk0-aFTrqtygbLOqnQMSUqHQFda4bRhFG18Tz3Ansn4,54215
|
4
4
|
GameSentenceMiner/configuration.py,sha256=_nS-9sWNn97Zdv2V4Ypt_hf4PL6oLa5oK2Z7rWrqEmc,14405
|
5
|
-
GameSentenceMiner/ffmpeg.py,sha256=
|
6
|
-
GameSentenceMiner/gametext.py,sha256=
|
7
|
-
GameSentenceMiner/gsm.py,sha256=
|
5
|
+
GameSentenceMiner/ffmpeg.py,sha256=4nyl_kdXi_trzDyhhjSmszN0BlFF-McAFyOghvCyl34,11142
|
6
|
+
GameSentenceMiner/gametext.py,sha256=BsVECzyh91nuQzBn9ZkdWLJXhtIekoPyKRNgsuB_wP4,8917
|
7
|
+
GameSentenceMiner/gsm.py,sha256=dILfQrWQ-cWKRAaKswnzBMNNlBqC7_BEZUSFxe-8PMk,18994
|
8
8
|
GameSentenceMiner/model.py,sha256=oh8VVT8T1UKekbmP6MGNgQ8jIuQ_7Rg4GPzDCn2kJo8,1999
|
9
9
|
GameSentenceMiner/notification.py,sha256=WBaQWoPNhW4XqdPBUmxPBgjk0ngzH_4v9zMQ-XQAKC8,2010
|
10
10
|
GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
|
11
11
|
GameSentenceMiner/package_updater.py,sha256=0uaLAp0WrWqostNTBWRS0laITjI9aN9Yt_6GXosS4NQ,1278
|
12
12
|
GameSentenceMiner/util.py,sha256=MITweiFYaefWQF5nR8tZ9yE6vd_b-fLuP0MP1Y1U4K0,4720
|
13
|
-
GameSentenceMiner/utility_gui.py,sha256=
|
13
|
+
GameSentenceMiner/utility_gui.py,sha256=EtQUnCgTTdzKJE0iCJiHjjc_c6tc7JtI09LRg4_iy8Y,4555
|
14
14
|
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
15
15
|
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
16
|
GameSentenceMiner/downloader/download_tools.py,sha256=M7vLo6_0QMuk1Ji4CsZqk1C2g7Bq6PyM29r7XNoP6Rw,6406
|
@@ -18,8 +18,8 @@ GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
18
18
|
GameSentenceMiner/vad/silero_trim.py,sha256=syDJX_KbFmdyFFtnQqYTD0tICsUCJizYhs-atPgXtxA,1549
|
19
19
|
GameSentenceMiner/vad/vosk_helper.py,sha256=-AAwK0cgOC5rK3_gL0sQgrPJ75E49g_PxZR4d5ckwc4,5826
|
20
20
|
GameSentenceMiner/vad/whisper_helper.py,sha256=bpR1HVnJRn9H5u8XaHBqBJ6JwIjzqn-Fajps8QmQ4zc,3411
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
gamesentenceminer-2.3.8.dist-info/METADATA,sha256=PYvBuu_5hcR_2cEbow_2BhBjM93wjIkCl5gxfGaCGhQ,10120
|
22
|
+
gamesentenceminer-2.3.8.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
23
|
+
gamesentenceminer-2.3.8.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
24
|
+
gamesentenceminer-2.3.8.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
25
|
+
gamesentenceminer-2.3.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|