GameSentenceMiner 2.9.15__py3-none-any.whl → 2.9.16__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 +37 -15
- GameSentenceMiner/gametext.py +8 -1
- GameSentenceMiner/gsm.py +87 -81
- GameSentenceMiner/obs.py +6 -4
- GameSentenceMiner/util/communication/websocket.py +1 -0
- GameSentenceMiner/util/configuration.py +44 -2
- GameSentenceMiner/util/notification.py +1 -1
- GameSentenceMiner/util/text_log.py +6 -3
- GameSentenceMiner/vad.py +12 -11
- GameSentenceMiner/web/texthooking_page.py +7 -1
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/METADATA +1 -1
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/RECORD +16 -16
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.15.dist-info → gamesentenceminer-2.9.16.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import copy
|
1
2
|
import queue
|
2
3
|
import time
|
3
4
|
|
@@ -19,10 +20,6 @@ from GameSentenceMiner.util.text_log import get_all_lines, get_text_event, get_m
|
|
19
20
|
from GameSentenceMiner.obs import get_current_game
|
20
21
|
from GameSentenceMiner.web import texthooking_page
|
21
22
|
|
22
|
-
audio_in_anki = None
|
23
|
-
screenshot_in_anki = None
|
24
|
-
prev_screenshot_in_anki = None
|
25
|
-
|
26
23
|
# Global variables to track state
|
27
24
|
previous_note_ids = set()
|
28
25
|
first_run = True
|
@@ -31,14 +28,21 @@ card_queue = []
|
|
31
28
|
|
32
29
|
def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='', tango='', reuse_audio=False,
|
33
30
|
should_update_audio=True, ss_time=0, game_line=None, selected_lines=None, prev_ss_timing=0):
|
34
|
-
global audio_in_anki, screenshot_in_anki, prev_screenshot_in_anki
|
35
31
|
update_audio = should_update_audio and (get_config().anki.sentence_audio_field and not
|
36
32
|
last_note.get_field(get_config().anki.sentence_audio_field) or get_config().anki.overwrite_audio)
|
37
33
|
update_picture = (get_config().anki.picture_field and get_config().screenshot.enabled
|
38
34
|
and (get_config().anki.overwrite_picture or not last_note.get_field(get_config().anki.picture_field)))
|
39
35
|
|
40
|
-
|
41
|
-
|
36
|
+
audio_in_anki = ''
|
37
|
+
screenshot_in_anki = ''
|
38
|
+
prev_screenshot_in_anki = ''
|
39
|
+
if reuse_audio:
|
40
|
+
logger.info("Reusing Audio from last note")
|
41
|
+
anki_result = anki_results[game_line.id]
|
42
|
+
audio_in_anki = anki_result.audio_in_anki
|
43
|
+
screenshot_in_anki = anki_result.screenshot_in_anki
|
44
|
+
prev_screenshot_in_anki = anki_result.prev_screenshot_in_anki
|
45
|
+
else:
|
42
46
|
if update_audio:
|
43
47
|
audio_in_anki = store_media_file(audio_path)
|
44
48
|
if get_config().audio.external_tool and get_config().audio.external_tool_enabled:
|
@@ -96,6 +100,16 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
96
100
|
tag_string = " ".join(tags)
|
97
101
|
invoke("addTags", tags=tag_string, notes=[last_note.noteId])
|
98
102
|
|
103
|
+
if not reuse_audio:
|
104
|
+
anki_results[game_line.id] = AnkiUpdateResult(
|
105
|
+
success=True,
|
106
|
+
audio_in_anki=audio_in_anki,
|
107
|
+
screenshot_in_anki=screenshot_in_anki,
|
108
|
+
prev_screenshot_in_anki=prev_screenshot_in_anki,
|
109
|
+
sentence_in_anki=game_line.text if game_line else '',
|
110
|
+
multi_line=bool(selected_lines and len(selected_lines) > 1)
|
111
|
+
)
|
112
|
+
|
99
113
|
run_new_thread(lambda: check_and_update_note(last_note, note, tags))
|
100
114
|
|
101
115
|
def check_and_update_note(last_note, note, tags=[]):
|
@@ -111,6 +125,7 @@ def check_and_update_note(last_note, note, tags=[]):
|
|
111
125
|
notification.open_anki_card(last_note.noteId)
|
112
126
|
if get_config().features.notify_on_update:
|
113
127
|
notification.send_note_updated(last_note.noteId)
|
128
|
+
gsm_status.remove_word_being_processed(last_note.get_field(get_config().anki.word_field))
|
114
129
|
|
115
130
|
|
116
131
|
def add_image_to_card(last_note: AnkiCard, image_path):
|
@@ -262,7 +277,9 @@ def check_for_new_cards():
|
|
262
277
|
current_note_ids = set()
|
263
278
|
try:
|
264
279
|
current_note_ids = get_note_ids()
|
280
|
+
gsm_status.anki_connected = True
|
265
281
|
except Exception as e:
|
282
|
+
gsm_status.anki_connected = False
|
266
283
|
if datetime.now() - last_connection_error > timedelta(seconds=10):
|
267
284
|
logger.error(f"Error fetching Anki notes, Make sure Anki is running, ankiconnect add-on is installed, and url/port is configured correctly in GSM Settings")
|
268
285
|
last_connection_error = datetime.now()
|
@@ -270,10 +287,7 @@ def check_for_new_cards():
|
|
270
287
|
new_card_ids = current_note_ids - previous_note_ids
|
271
288
|
if new_card_ids and not first_run:
|
272
289
|
try:
|
273
|
-
|
274
|
-
run_new_thread(update_new_card)
|
275
|
-
else:
|
276
|
-
update_new_card()
|
290
|
+
update_new_card()
|
277
291
|
except Exception as e:
|
278
292
|
logger.error("Error updating new card, Reason:", e)
|
279
293
|
first_run = False
|
@@ -283,6 +297,7 @@ def update_new_card():
|
|
283
297
|
last_card = get_last_anki_card()
|
284
298
|
if not last_card or not check_tags_for_should_update(last_card):
|
285
299
|
return
|
300
|
+
gsm_status.add_word_being_processed(last_card.get_field(get_config().anki.word_field))
|
286
301
|
use_prev_audio = sentence_is_same_as_previous(last_card)
|
287
302
|
logger.debug(f"last mined line: {gsm_state.last_mined_line}, current sentence: {get_sentence(last_card)}")
|
288
303
|
logger.info(f"New card using previous audio: {use_prev_audio}")
|
@@ -290,8 +305,7 @@ def update_new_card():
|
|
290
305
|
obs.update_current_game()
|
291
306
|
if use_prev_audio:
|
292
307
|
lines = texthooking_page.get_selected_lines()
|
293
|
-
|
294
|
-
update_anki_card(last_card, note=get_initial_card_info(last_card, lines), game_line=get_mined_line(last_card, lines), reuse_audio=True)
|
308
|
+
run_new_thread(lambda: update_card_from_same_sentence(last_card, lines=lines, game_line=get_mined_line(last_card, lines)))
|
295
309
|
texthooking_page.reset_checked_lines()
|
296
310
|
else:
|
297
311
|
logger.info("New card(s) detected! Added to Processing Queue!")
|
@@ -303,8 +317,16 @@ def update_new_card():
|
|
303
317
|
logger.error(f"Error saving replay buffer: {e}")
|
304
318
|
return
|
305
319
|
|
306
|
-
|
307
|
-
|
320
|
+
def update_card_from_same_sentence(last_card, lines, game_line):
|
321
|
+
while game_line.id not in anki_results:
|
322
|
+
time.sleep(0.5)
|
323
|
+
anki_result = anki_results[game_line.id]
|
324
|
+
if anki_result.success:
|
325
|
+
update_anki_card(last_card, note=get_initial_card_info(last_card, lines),
|
326
|
+
game_line=get_mined_line(last_card, lines), reuse_audio=True)
|
327
|
+
else:
|
328
|
+
logger.error(f"Anki update failed for card {last_card.noteId}")
|
329
|
+
notification.send_error_no_anki_update()
|
308
330
|
|
309
331
|
|
310
332
|
def sentence_is_same_as_previous(last_card):
|
GameSentenceMiner/gametext.py
CHANGED
@@ -25,15 +25,18 @@ async def monitor_clipboard():
|
|
25
25
|
send_message_on_resume = False
|
26
26
|
while True:
|
27
27
|
if not get_config().general.use_clipboard:
|
28
|
+
gsm_status.clipboard_enabled = False
|
28
29
|
await asyncio.sleep(5)
|
29
30
|
continue
|
30
31
|
if not get_config().general.use_both_clipboard_and_websocket and any(websocket_connected.values()):
|
32
|
+
gsm_status.clipboard_enabled = False
|
31
33
|
await asyncio.sleep(1)
|
32
34
|
send_message_on_resume = True
|
33
35
|
continue
|
34
36
|
elif send_message_on_resume:
|
35
37
|
logger.info("No Websocket Connections, resuming Clipboard Monitoring.")
|
36
38
|
send_message_on_resume = False
|
39
|
+
gsm_status.clipboard_enabled = True
|
37
40
|
current_clipboard = pyperclip.paste()
|
38
41
|
|
39
42
|
if current_clipboard and current_clipboard != current_line:
|
@@ -57,6 +60,7 @@ async def listen_websockets():
|
|
57
60
|
try:
|
58
61
|
async with websockets.connect(websocket_url, ping_interval=None) as websocket:
|
59
62
|
logger.info(f"TextHooker Websocket {uri} Connected!")
|
63
|
+
gsm_status.websockets_connected.append(websocket_url)
|
60
64
|
if reconnecting:
|
61
65
|
logger.info(f"Texthooker WebSocket {uri} connected Successfully!" + " Disabling Clipboard Monitor." if (get_config().general.use_clipboard and not get_config().general.use_both_clipboard_and_websocket) else "")
|
62
66
|
reconnecting = False
|
@@ -79,6 +83,8 @@ async def listen_websockets():
|
|
79
83
|
if current_clipboard != current_line:
|
80
84
|
await handle_new_text_event(current_clipboard, line_time if line_time else None)
|
81
85
|
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
|
86
|
+
if websocket_url in gsm_status.websockets_connected:
|
87
|
+
gsm_status.websockets_connected.remove(websocket_url)
|
82
88
|
if isinstance(e, InvalidStatus):
|
83
89
|
e: InvalidStatus
|
84
90
|
if e.response.status_code == 404:
|
@@ -113,6 +119,7 @@ async def handle_new_text_event(current_clipboard, line_time=None):
|
|
113
119
|
current_line_after_regex = do_text_replacements(current_line, TEXT_REPLACEMENTS_FILE)
|
114
120
|
logger.info(f"Line Received: {current_line_after_regex}")
|
115
121
|
current_line_time = line_time if line_time else datetime.now()
|
122
|
+
gsm_status.last_line_received = current_line_time.strftime("%Y-%m-%d %H:%M:%S")
|
116
123
|
add_line(current_line_after_regex, line_time)
|
117
124
|
if len(get_text_log().values) > 0:
|
118
125
|
await add_event_to_texthooker(get_text_log()[-1])
|
@@ -121,7 +128,7 @@ def reset_line_hotkey_pressed():
|
|
121
128
|
global current_line_time
|
122
129
|
logger.info("LINE RESET HOTKEY PRESSED")
|
123
130
|
current_line_time = datetime.now()
|
124
|
-
gsm_state.last_mined_line =
|
131
|
+
gsm_state.last_mined_line = None
|
125
132
|
|
126
133
|
|
127
134
|
def run_websocket_listener():
|
GameSentenceMiner/gsm.py
CHANGED
@@ -76,6 +76,7 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
76
76
|
|
77
77
|
def process_replay(self, video_path):
|
78
78
|
vad_trimmed_audio = ''
|
79
|
+
skip_delete = False
|
79
80
|
if "previous.mkv" in video_path:
|
80
81
|
os.remove(video_path)
|
81
82
|
video_path = gsm_state.previous_replay
|
@@ -89,92 +90,95 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
89
90
|
last_note, anki_card_creation_time = anki.card_queue.pop(0)
|
90
91
|
else:
|
91
92
|
logger.info("Replay buffer initiated externally. Skipping processing.")
|
93
|
+
skip_delete = True
|
92
94
|
return
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
95
|
+
|
96
|
+
mined_line = get_text_event(last_note)
|
97
|
+
gsm_state.last_mined_line = mined_line
|
98
|
+
if os.path.exists(video_path) and os.access(video_path, os.R_OK):
|
99
|
+
logger.debug(f"Video found and is readable: {video_path}")
|
100
|
+
if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
|
101
|
+
get_config().obs.minimum_replay_size):
|
102
|
+
logger.debug("Checking if video is big enough")
|
103
|
+
notification.send_check_obs_notification(reason="Video may be empty, check scene in OBS.")
|
104
|
+
logger.error(
|
105
|
+
f"Video was unusually small, potentially empty! Check OBS for Correct Scene Settings! Path: {video_path}")
|
106
|
+
return
|
107
|
+
if not last_note:
|
108
|
+
logger.debug("Attempting to get last anki card")
|
109
|
+
if get_config().anki.update_anki:
|
110
|
+
last_note = anki.get_last_anki_card()
|
111
|
+
if get_config().features.backfill_audio:
|
112
|
+
last_note = anki.get_cards_by_sentence(gametext.current_line_after_regex)
|
113
|
+
line_cutoff = None
|
114
|
+
start_line = None
|
115
|
+
if mined_line:
|
116
|
+
start_line = mined_line
|
117
|
+
if mined_line.next:
|
118
|
+
line_cutoff = mined_line.next.time
|
119
|
+
|
120
|
+
selected_lines = []
|
121
|
+
if texthooking_page.are_lines_selected():
|
122
|
+
selected_lines = texthooking_page.get_selected_lines()
|
123
|
+
start_line = selected_lines[0]
|
124
|
+
mined_line = get_mined_line(last_note, selected_lines)
|
125
|
+
line_cutoff = selected_lines[-1].get_next_time()
|
126
|
+
|
127
|
+
if last_note:
|
128
|
+
logger.debug(last_note.to_json())
|
129
|
+
note = anki.get_initial_card_info(last_note, selected_lines)
|
130
|
+
tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
|
131
|
+
texthooking_page.reset_checked_lines()
|
132
|
+
|
133
|
+
if get_config().anki.sentence_audio_field and get_config().audio.enabled:
|
134
|
+
logger.debug("Attempting to get audio from video")
|
135
|
+
final_audio_output, vad_result, vad_trimmed_audio = VideoToAudioHandler.get_audio(
|
136
|
+
start_line,
|
137
|
+
line_cutoff,
|
138
|
+
video_path,
|
139
|
+
anki_card_creation_time,
|
140
|
+
mined_line=mined_line)
|
141
|
+
else:
|
142
|
+
final_audio_output = ""
|
143
|
+
vad_result = VADResult(False, 0, 0, '')
|
144
|
+
vad_trimmed_audio = ""
|
145
|
+
if not get_config().audio.enabled:
|
146
|
+
logger.info("Audio is disabled in config, skipping audio processing!")
|
147
|
+
elif not get_config().anki.sentence_audio_field:
|
148
|
+
logger.info("No SentenceAudio Field in config, skipping audio processing!")
|
149
|
+
|
150
|
+
ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line, vad_result=vad_result, doing_multi_line=bool(selected_lines), anki_card_creation_time=anki_card_creation_time)
|
151
|
+
# prev_ss_timing = 0
|
152
|
+
# if get_config().anki.previous_image_field and get_config().vad.do_vad_postprocessing:
|
153
|
+
# prev_ss_timing = ffmpeg.get_screenshot_time(video_path, mined_line.prev,
|
154
|
+
# vad_result=VideoToAudioHandler.get_audio(game_line=mined_line.prev,
|
155
|
+
# next_line_time=mined_line.time,
|
156
|
+
# video_path=video_path,
|
157
|
+
# anki_card_creation_time=anki_card_creation_time,
|
158
|
+
# timing_only=True) ,doing_multi_line=bool(selected_lines), previous_line=True)
|
159
|
+
|
160
|
+
if get_config().anki.update_anki and last_note:
|
161
|
+
anki.update_anki_card(
|
162
|
+
last_note, note, audio_path=final_audio_output, video_path=video_path,
|
163
|
+
tango=tango,
|
164
|
+
should_update_audio=vad_result.success,
|
165
|
+
ss_time=ss_timing,
|
166
|
+
game_line=start_line,
|
167
|
+
selected_lines=selected_lines
|
168
|
+
)
|
169
|
+
elif get_config().features.notify_on_update and vad_result.success:
|
170
|
+
notification.send_audio_generated_notification(vad_trimmed_audio)
|
169
171
|
except Exception as e:
|
172
|
+
anki_results[mined_line.id] = AnkiUpdateResult.failure()
|
170
173
|
logger.error(f"Failed Processing and/or adding to Anki: Reason {e}")
|
171
174
|
logger.debug(f"Some error was hit catching to allow further work to be done: {e}", exc_info=True)
|
172
175
|
notification.send_error_no_anki_update()
|
173
176
|
finally:
|
174
|
-
if
|
175
|
-
os.
|
176
|
-
|
177
|
-
os.
|
177
|
+
if not skip_delete:
|
178
|
+
if video_path and get_config().paths.remove_video and os.path.exists(video_path):
|
179
|
+
os.remove(video_path)
|
180
|
+
if vad_trimmed_audio and get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
181
|
+
os.remove(vad_trimmed_audio)
|
178
182
|
|
179
183
|
def handle_texthooker_button(self, video_path):
|
180
184
|
try:
|
@@ -643,6 +647,7 @@ def async_loop():
|
|
643
647
|
async def register_scene_switcher_callback():
|
644
648
|
def scene_switcher_callback(scene):
|
645
649
|
logger.info(f"Scene changed to: {scene}")
|
650
|
+
gsm_state.current_game = obs.get_current_game(sanitize=True)
|
646
651
|
all_configured_scenes = [config.scenes for config in get_master_config().configs.values()]
|
647
652
|
print(all_configured_scenes)
|
648
653
|
matching_configs = [name.strip() for name, config in config_instance.configs.items() if scene.strip() in config.scenes]
|
@@ -691,7 +696,8 @@ async def async_main(reloading=False):
|
|
691
696
|
if is_windows():
|
692
697
|
win32api.SetConsoleCtrlHandler(handle_exit())
|
693
698
|
|
694
|
-
|
699
|
+
gsm_status.ready = True
|
700
|
+
gsm_status.status = "Ready"
|
695
701
|
try:
|
696
702
|
if get_config().general.open_config_on_startup:
|
697
703
|
root.after(50, settings_window.show)
|
GameSentenceMiner/obs.py
CHANGED
@@ -31,6 +31,7 @@ class OBSConnectionManager(threading.Thread):
|
|
31
31
|
try:
|
32
32
|
if not client or not client.get_version() and not connecting:
|
33
33
|
logger.info("OBS WebSocket not connected. Attempting to reconnect...")
|
34
|
+
gsm_status.obs_connected = False
|
34
35
|
asyncio.run(connect_to_obs())
|
35
36
|
except Exception as e:
|
36
37
|
logger.debug(f"Error checking OBS connection: {e}")
|
@@ -157,6 +158,7 @@ async def connect_to_obs(retry_count=0):
|
|
157
158
|
password=get_config().obs.password,
|
158
159
|
timeout=1,
|
159
160
|
)
|
161
|
+
gsm_status.obs_connected = True
|
160
162
|
logger.info("Connected to OBS WebSocket.")
|
161
163
|
if not obs_connection_manager:
|
162
164
|
obs_connection_manager = OBSConnectionManager()
|
@@ -357,15 +359,15 @@ def get_screenshot_base64():
|
|
357
359
|
return None
|
358
360
|
|
359
361
|
def update_current_game():
|
360
|
-
|
362
|
+
gsm_state.current_game = get_current_scene()
|
361
363
|
|
362
364
|
def get_current_game(sanitize=False):
|
363
|
-
if not
|
365
|
+
if not gsm_state.current_game:
|
364
366
|
update_current_game()
|
365
367
|
|
366
368
|
if sanitize:
|
367
|
-
return sanitize_filename(
|
368
|
-
return
|
369
|
+
return sanitize_filename(gsm_state.current_game)
|
370
|
+
return gsm_state.current_game
|
369
371
|
|
370
372
|
|
371
373
|
def main():
|
@@ -633,8 +633,11 @@ logger.addHandler(console_handler)
|
|
633
633
|
|
634
634
|
# Create rotating file handler with level DEBUG
|
635
635
|
file_path = get_log_path()
|
636
|
-
|
637
|
-
|
636
|
+
try:
|
637
|
+
if os.path.exists(file_path) and os.path.getsize(file_path) > 10 * 1024 * 1024 and os.access(file_path, os.W_OK):
|
638
|
+
shutil.move(file_path, os.path.join(os.path.dirname(file_path), "gamesentenceminer_old.log"))
|
639
|
+
except Exception as e:
|
640
|
+
logger.debug("Error rotating log, probably because the file is being written to by another process.")
|
638
641
|
|
639
642
|
file_handler = logging.FileHandler(file_path, encoding='utf-8')
|
640
643
|
file_handler.setLevel(logging.DEBUG)
|
@@ -658,4 +661,43 @@ class GsmAppState:
|
|
658
661
|
self.keep_running = True
|
659
662
|
self.current_game = ''
|
660
663
|
|
664
|
+
@dataclass_json
|
665
|
+
@dataclass
|
666
|
+
class AnkiUpdateResult:
|
667
|
+
success: bool = False
|
668
|
+
audio_in_anki: str = ''
|
669
|
+
screenshot_in_anki: str = ''
|
670
|
+
prev_screenshot_in_anki: str = ''
|
671
|
+
sentence_in_anki: str = ''
|
672
|
+
multi_line: bool = False
|
673
|
+
|
674
|
+
@staticmethod
|
675
|
+
def failure():
|
676
|
+
return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False)
|
677
|
+
|
678
|
+
|
679
|
+
@dataclass_json
|
680
|
+
@dataclass
|
681
|
+
class GsmStatus:
|
682
|
+
ready: bool = False
|
683
|
+
status: bool = "Initializing"
|
684
|
+
cards_created: int = 0
|
685
|
+
websockets_connected: List[str] = field(default_factory=list)
|
686
|
+
obs_connected: bool = False
|
687
|
+
anki_connected: bool = False
|
688
|
+
last_line_received: str = None
|
689
|
+
words_being_processed: List[str] = field(default_factory=list)
|
690
|
+
clipboard_enabled: bool = True
|
691
|
+
|
692
|
+
def add_word_being_processed(self, word: str):
|
693
|
+
if word not in self.words_being_processed:
|
694
|
+
self.words_being_processed.append(word)
|
695
|
+
|
696
|
+
def remove_word_being_processed(self, word: str):
|
697
|
+
if word in self.words_being_processed:
|
698
|
+
self.words_being_processed.remove(word)
|
699
|
+
|
700
|
+
|
701
|
+
gsm_status = GsmStatus()
|
702
|
+
anki_results = {}
|
661
703
|
gsm_state = GsmAppState()
|
@@ -4,8 +4,9 @@ from datetime import datetime
|
|
4
4
|
from difflib import SequenceMatcher
|
5
5
|
from typing import Optional
|
6
6
|
|
7
|
+
from GameSentenceMiner.obs import get_current_game
|
7
8
|
from GameSentenceMiner.util.gsm_utils import remove_html_and_cloze_tags
|
8
|
-
from GameSentenceMiner.util.configuration import logger, get_config
|
9
|
+
from GameSentenceMiner.util.configuration import logger, get_config, gsm_state
|
9
10
|
from GameSentenceMiner.util.model import AnkiCard
|
10
11
|
|
11
12
|
initial_time = datetime.now()
|
@@ -19,6 +20,7 @@ class GameLine:
|
|
19
20
|
prev: 'GameLine | None'
|
20
21
|
next: 'GameLine | None'
|
21
22
|
index: int = 0
|
23
|
+
scene: str = ""
|
22
24
|
|
23
25
|
def get_previous_time(self):
|
24
26
|
if self.prev:
|
@@ -71,10 +73,11 @@ class GameText:
|
|
71
73
|
new_line = GameLine(
|
72
74
|
id=line_id, # Time-based UUID as an integer
|
73
75
|
text=line_text,
|
74
|
-
time=line_time
|
76
|
+
time=line_time or datetime.now(),
|
75
77
|
prev=self.values[-1] if self.values else None,
|
76
78
|
next=None,
|
77
|
-
index=self.game_line_index
|
79
|
+
index=self.game_line_index,
|
80
|
+
scene=gsm_state.current_game or ""
|
78
81
|
)
|
79
82
|
self.values_dict[line_id] = new_line
|
80
83
|
logger.debug(f"Adding line: {new_line}")
|
GameSentenceMiner/vad.py
CHANGED
@@ -7,16 +7,6 @@ from GameSentenceMiner.util import configuration, ffmpeg
|
|
7
7
|
from GameSentenceMiner.util.configuration import *
|
8
8
|
from GameSentenceMiner.util.ffmpeg import get_ffprobe_path
|
9
9
|
|
10
|
-
|
11
|
-
def get_audio_length(path):
|
12
|
-
result = subprocess.run(
|
13
|
-
[get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path],
|
14
|
-
stdout=subprocess.PIPE,
|
15
|
-
stderr=subprocess.PIPE,
|
16
|
-
text=True
|
17
|
-
)
|
18
|
-
return float(result.stdout.strip())
|
19
|
-
|
20
10
|
class VADResult:
|
21
11
|
def __init__(self, success: bool, start: float, end: float, model: str, output_audio: str = None):
|
22
12
|
self.success = success
|
@@ -107,6 +97,17 @@ class VADProcessor(ABC):
|
|
107
97
|
def _detect_voice_activity(self, input_audio):
|
108
98
|
pass
|
109
99
|
|
100
|
+
@staticmethod
|
101
|
+
def get_audio_length(path):
|
102
|
+
result = subprocess.run(
|
103
|
+
[get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of",
|
104
|
+
"default=noprint_wrappers=1:nokey=1", path],
|
105
|
+
stdout=subprocess.PIPE,
|
106
|
+
stderr=subprocess.PIPE,
|
107
|
+
text=True
|
108
|
+
)
|
109
|
+
return float(result.stdout.strip())
|
110
|
+
|
110
111
|
def process_audio(self, input_audio, output_audio, game_line):
|
111
112
|
voice_activity = self._detect_voice_activity(input_audio)
|
112
113
|
|
@@ -119,7 +120,7 @@ class VADProcessor(ABC):
|
|
119
120
|
|
120
121
|
# Attempt to fix the end time if the last segment is too short
|
121
122
|
if game_line and game_line.next and len(voice_activity) > 1:
|
122
|
-
audio_length = get_audio_length(input_audio)
|
123
|
+
audio_length = self.get_audio_length(input_audio)
|
123
124
|
if 0 > audio_length - voice_activity[-1]['start'] + get_config().audio.beginning_offset:
|
124
125
|
end_time = voice_activity[-2]['end']
|
125
126
|
|
@@ -15,7 +15,7 @@ from GameSentenceMiner.util.text_log import GameLine, get_line_by_id, initial_ti
|
|
15
15
|
from flask import request, jsonify, send_from_directory
|
16
16
|
import webbrowser
|
17
17
|
from GameSentenceMiner import obs
|
18
|
-
from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state
|
18
|
+
from GameSentenceMiner.util.configuration import logger, get_config, DB_PATH, gsm_state, gsm_status
|
19
19
|
|
20
20
|
port = get_config().general.texthooker_port
|
21
21
|
url = f"http://localhost:{port}"
|
@@ -294,6 +294,12 @@ def play_audio():
|
|
294
294
|
return jsonify({}), 200
|
295
295
|
|
296
296
|
|
297
|
+
@app.route('/get_status', methods=['GET'])
|
298
|
+
def get_status():
|
299
|
+
gsm_status.clipboard_enabled
|
300
|
+
return jsonify(gsm_status.to_dict()), 200
|
301
|
+
|
302
|
+
|
297
303
|
# async def main():
|
298
304
|
# async with websockets.serve(websocket_handler, "localhost", 8765): # Choose a port for WebSocket
|
299
305
|
# print("WebSocket server started on ws://localhost:8765/ws (adjust as needed)")
|
@@ -1,10 +1,10 @@
|
|
1
1
|
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=
|
2
|
+
GameSentenceMiner/anki.py,sha256=hNHBIoJRrsWIhLe0sehOYPXTWzPREeXl4gYCPHUCaiE,16331
|
3
3
|
GameSentenceMiner/config_gui.py,sha256=iAOLD47sQW67kzBcZKSQ0Dwctc1ngZK1lwSVIaLpQPI,83559
|
4
|
-
GameSentenceMiner/gametext.py,sha256=
|
5
|
-
GameSentenceMiner/gsm.py,sha256=
|
6
|
-
GameSentenceMiner/obs.py,sha256=
|
7
|
-
GameSentenceMiner/vad.py,sha256=
|
4
|
+
GameSentenceMiner/gametext.py,sha256=iO1o2980XBzBc2nsgBVr_ZaKHRLotebVpDhwGqBCK9k,6696
|
5
|
+
GameSentenceMiner/gsm.py,sha256=j6Y4Nd79Qi09r-Pgt2y3THxeP-yBAfZSFbbsISE9Ah8,29251
|
6
|
+
GameSentenceMiner/obs.py,sha256=21GQ4DzjccLc-m4rU5Tfm4E3n1R1U4vRwmIWfKqcjmw,14805
|
7
|
+
GameSentenceMiner/vad.py,sha256=TbP3NVdjfB1TFJeB0QpOXZysgo_UHHKLdx95pYmM0JI,14902
|
8
8
|
GameSentenceMiner/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
GameSentenceMiner/ai/ai_prompting.py,sha256=0jBAnngNwmc3dqJiVWe_QRy4Syr-muV-ML2rq0FiUtU,10215
|
10
10
|
GameSentenceMiner/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -28,24 +28,24 @@ GameSentenceMiner/owocr/owocr/ocr.py,sha256=vZR3du1fGg5-3cmPvYKTO4PFk7Lxyf6-BrIy
|
|
28
28
|
GameSentenceMiner/owocr/owocr/run.py,sha256=U6VIfCvsNPADG3twhp4SQVX1xhihSAGGrBnQj2x0C2c,54964
|
29
29
|
GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py,sha256=Na6XStbQBtpQUSdbN3QhEswtKuU1JjReFk_K8t5ezQE,3395
|
30
30
|
GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
|
-
GameSentenceMiner/util/configuration.py,sha256=
|
31
|
+
GameSentenceMiner/util/configuration.py,sha256=Wgr1UAf_JoXBlp9h_f3-d2DAmIWnR1FtCmMX6mCfMNM,27461
|
32
32
|
GameSentenceMiner/util/electron_config.py,sha256=3VmIrcXhC-wIMMc4uqV85NrNenRl4ZUbnQfSjWEwuig,9852
|
33
33
|
GameSentenceMiner/util/ffmpeg.py,sha256=daItJprEqi5PQe-aFb836rls3tBHNqIQKz61vlJK07M,19276
|
34
34
|
GameSentenceMiner/util/gsm_utils.py,sha256=Z_Lu4jSIfUaM2VljIJXQkSJD0UsyJ5hMB46H2NS0gZo,8819
|
35
35
|
GameSentenceMiner/util/model.py,sha256=iDtLTfR6D-ZC0gCiDqYno6-gA6Z07PZTM4B5MAA6xZI,5704
|
36
|
-
GameSentenceMiner/util/notification.py,sha256=
|
36
|
+
GameSentenceMiner/util/notification.py,sha256=0OnEYjn3DUEZ6c6OtPjdVZe-DG-QSoMAl9fetjjCvNU,3874
|
37
37
|
GameSentenceMiner/util/package.py,sha256=u1ym5z869lw5EHvIviC9h9uH97bzUXSXXA8KIn8rUvk,1157
|
38
38
|
GameSentenceMiner/util/ss_selector.py,sha256=oCzmDbpEGvVselF-oDPIrBcQktGIZT0Zt16uDLDAHMQ,4493
|
39
|
-
GameSentenceMiner/util/text_log.py,sha256=
|
39
|
+
GameSentenceMiner/util/text_log.py,sha256=VhdiF2UIlGYW42CFurkaeKa6W8x7Z5klXN9GhY1JLIA,5471
|
40
40
|
GameSentenceMiner/util/communication/__init__.py,sha256=xh__yn2MhzXi9eLi89PeZWlJPn-cbBSjskhi1BRraXg,643
|
41
41
|
GameSentenceMiner/util/communication/send.py,sha256=Wki9qIY2CgYnuHbmnyKVIYkcKAN_oYS4up93XMikBaI,222
|
42
|
-
GameSentenceMiner/util/communication/websocket.py,sha256=
|
42
|
+
GameSentenceMiner/util/communication/websocket.py,sha256=TbphRGmxVrgEupS7tNdifsmQfWDfIp0Hio2cSiUKgsk,3317
|
43
43
|
GameSentenceMiner/util/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
44
44
|
GameSentenceMiner/util/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
45
|
GameSentenceMiner/util/downloader/download_tools.py,sha256=mvnOjDHFlV1AbjHaNI7mdnC5_CH5k3N4n1ezqzzbzGA,8139
|
46
46
|
GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=o3ANp5IodEQoQ8GPcJdg9Y8JzA_lictwnebFPwwUZVk,10144
|
47
47
|
GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
|
-
GameSentenceMiner/web/texthooking_page.py,sha256=
|
48
|
+
GameSentenceMiner/web/texthooking_page.py,sha256=kDRNeTz6eIzc9rqrnupAqEmdVF3GuhB1PSgv9azEaAI,15191
|
49
49
|
GameSentenceMiner/web/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
50
|
GameSentenceMiner/web/static/apple-touch-icon.png,sha256=OcMI8af_68DA_tweOsQ5LytTyMwm7-hPW07IfrOVgEs,46132
|
51
51
|
GameSentenceMiner/web/static/favicon-96x96.png,sha256=lOePzjiKl1JY2J1kT_PMdyEnrlJmi5GWbmXJunM12B4,16502
|
@@ -59,9 +59,9 @@ GameSentenceMiner/web/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
|
|
59
59
|
GameSentenceMiner/web/templates/index.html,sha256=HZKiIjiGJV8PGQ9T2aLDUNSfJn71qOwbYCjbRuSIjpY,213583
|
60
60
|
GameSentenceMiner/web/templates/text_replacements.html,sha256=tV5c8mCaWSt_vKuUpbdbLAzXZ3ATZeDvQ9PnnAfqY0M,8598
|
61
61
|
GameSentenceMiner/web/templates/utility.html,sha256=3flZinKNqUJ7pvrZk6xu__v67z44rXnaK7UTZ303R-8,16946
|
62
|
-
gamesentenceminer-2.9.
|
63
|
-
gamesentenceminer-2.9.
|
64
|
-
gamesentenceminer-2.9.
|
65
|
-
gamesentenceminer-2.9.
|
66
|
-
gamesentenceminer-2.9.
|
67
|
-
gamesentenceminer-2.9.
|
62
|
+
gamesentenceminer-2.9.16.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
63
|
+
gamesentenceminer-2.9.16.dist-info/METADATA,sha256=ybDsHKiVvTzpQUq5v7g8XQa8NKmf0Vl0Pcsuw18DXIo,7221
|
64
|
+
gamesentenceminer-2.9.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
65
|
+
gamesentenceminer-2.9.16.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
66
|
+
gamesentenceminer-2.9.16.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
67
|
+
gamesentenceminer-2.9.16.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|