GameSentenceMiner 2.0.0__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/__init__.py +0 -0
- GameSentenceMiner/anki.py +265 -0
- GameSentenceMiner/config_gui.py +803 -0
- GameSentenceMiner/configuration.py +359 -0
- GameSentenceMiner/ffmpeg.py +297 -0
- GameSentenceMiner/gametext.py +128 -0
- GameSentenceMiner/gsm.py +385 -0
- GameSentenceMiner/model.py +84 -0
- GameSentenceMiner/notification.py +69 -0
- GameSentenceMiner/obs.py +128 -0
- GameSentenceMiner/util.py +136 -0
- GameSentenceMiner/vad/__init__.py +0 -0
- GameSentenceMiner/vad/silero_trim.py +43 -0
- GameSentenceMiner/vad/vosk_helper.py +152 -0
- GameSentenceMiner/vad/whisper_helper.py +98 -0
- GameSentenceMiner-2.0.0.dist-info/METADATA +346 -0
- GameSentenceMiner-2.0.0.dist-info/RECORD +20 -0
- GameSentenceMiner-2.0.0.dist-info/WHEEL +5 -0
- GameSentenceMiner-2.0.0.dist-info/entry_points.txt +2 -0
- GameSentenceMiner-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
import asyncio
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
from collections import OrderedDict
|
5
|
+
from datetime import datetime
|
6
|
+
|
7
|
+
import pyperclip
|
8
|
+
import websockets
|
9
|
+
|
10
|
+
from . import util
|
11
|
+
from .configuration import *
|
12
|
+
from .configuration import get_config, logger
|
13
|
+
from .util import remove_html_tags
|
14
|
+
from difflib import SequenceMatcher
|
15
|
+
|
16
|
+
|
17
|
+
previous_line = ''
|
18
|
+
previous_line_time = datetime.now()
|
19
|
+
|
20
|
+
line_history = OrderedDict()
|
21
|
+
reconnecting = False
|
22
|
+
|
23
|
+
|
24
|
+
class ClipboardMonitor(threading.Thread):
|
25
|
+
|
26
|
+
def __init__(self):
|
27
|
+
threading.Thread.__init__(self)
|
28
|
+
self.daemon = True
|
29
|
+
|
30
|
+
def run(self):
|
31
|
+
global previous_line_time, previous_line, line_history
|
32
|
+
|
33
|
+
# Initial clipboard content
|
34
|
+
previous_line = pyperclip.paste()
|
35
|
+
|
36
|
+
while True:
|
37
|
+
current_clipboard = pyperclip.paste()
|
38
|
+
|
39
|
+
if current_clipboard != previous_line:
|
40
|
+
previous_line = current_clipboard
|
41
|
+
previous_line_time = datetime.now()
|
42
|
+
line_history[previous_line] = previous_line_time
|
43
|
+
util.use_previous_audio = False
|
44
|
+
|
45
|
+
time.sleep(0.05)
|
46
|
+
|
47
|
+
|
48
|
+
async def listen_websocket():
|
49
|
+
global previous_line, previous_line_time, line_history, reconnecting
|
50
|
+
while True:
|
51
|
+
try:
|
52
|
+
async with websockets.connect(f'ws://{get_config().general.websocket_uri}', ping_interval=None) as websocket:
|
53
|
+
if reconnecting:
|
54
|
+
print(f"Texthooker WebSocket connected Successfully!")
|
55
|
+
reconnecting = False
|
56
|
+
while True:
|
57
|
+
message = await websocket.recv()
|
58
|
+
|
59
|
+
try:
|
60
|
+
data = json.loads(message)
|
61
|
+
if "sentence" in data:
|
62
|
+
current_clipboard = data["sentence"]
|
63
|
+
except json.JSONDecodeError:
|
64
|
+
current_clipboard = message
|
65
|
+
|
66
|
+
if current_clipboard != previous_line:
|
67
|
+
previous_line = current_clipboard
|
68
|
+
previous_line_time = datetime.now()
|
69
|
+
line_history[previous_line] = previous_line_time
|
70
|
+
util.use_previous_audio = False
|
71
|
+
|
72
|
+
except (websockets.ConnectionClosed, ConnectionError) as e:
|
73
|
+
if not reconnecting:
|
74
|
+
print(f"Texthooker WebSocket connection lost: {e}. Attempting to Reconnect...")
|
75
|
+
reconnecting = True
|
76
|
+
await asyncio.sleep(5)
|
77
|
+
|
78
|
+
|
79
|
+
def reset_line_hotkey_pressed():
|
80
|
+
global previous_line_time
|
81
|
+
logger.info("LINE RESET HOTKEY PRESSED")
|
82
|
+
previous_line_time = datetime.now()
|
83
|
+
line_history[previous_line] = previous_line_time
|
84
|
+
util.use_previous_audio = False
|
85
|
+
|
86
|
+
|
87
|
+
def run_websocket_listener():
|
88
|
+
asyncio.run(listen_websocket())
|
89
|
+
|
90
|
+
|
91
|
+
def start_text_monitor():
|
92
|
+
if get_config().general.use_websocket:
|
93
|
+
text_thread = threading.Thread(target=run_websocket_listener, daemon=True)
|
94
|
+
else:
|
95
|
+
text_thread = ClipboardMonitor()
|
96
|
+
text_thread.start()
|
97
|
+
|
98
|
+
|
99
|
+
def get_line_timing(last_note):
|
100
|
+
def similar(a, b):
|
101
|
+
return SequenceMatcher(None, a, b).ratio()
|
102
|
+
|
103
|
+
if not last_note:
|
104
|
+
return previous_line_time, 0
|
105
|
+
|
106
|
+
line_time = previous_line_time
|
107
|
+
next_line = 0
|
108
|
+
prev_clip_time = 0
|
109
|
+
|
110
|
+
try:
|
111
|
+
sentence = last_note['fields'][get_config().anki.sentence_field]['value']
|
112
|
+
if sentence:
|
113
|
+
for i, (line, clip_time) in enumerate(reversed(line_history.items())):
|
114
|
+
similarity = similar(remove_html_tags(sentence), line)
|
115
|
+
if similarity >= 0.60: # 80% similarity threshold
|
116
|
+
line_time = clip_time
|
117
|
+
next_line = prev_clip_time
|
118
|
+
break
|
119
|
+
prev_clip_time = clip_time
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"Using Default clipboard/websocket timing - reason: {e}")
|
122
|
+
|
123
|
+
return line_time, next_line
|
124
|
+
|
125
|
+
|
126
|
+
def get_last_two_sentences():
|
127
|
+
lines = list(line_history.items())
|
128
|
+
return lines[-1][0] if lines else '', lines[-2][0] if len(lines) > 1 else ''
|
GameSentenceMiner/gsm.py
ADDED
@@ -0,0 +1,385 @@
|
|
1
|
+
import shutil
|
2
|
+
import signal
|
3
|
+
import sys
|
4
|
+
import tempfile
|
5
|
+
import time
|
6
|
+
|
7
|
+
import keyboard
|
8
|
+
import psutil
|
9
|
+
import win32api
|
10
|
+
from PIL import Image, ImageDraw
|
11
|
+
from pystray import Icon, Menu, MenuItem
|
12
|
+
from watchdog.events import FileSystemEventHandler
|
13
|
+
from watchdog.observers import Observer
|
14
|
+
|
15
|
+
from . import anki
|
16
|
+
from . import config_gui
|
17
|
+
from . import configuration
|
18
|
+
from . import ffmpeg
|
19
|
+
from . import gametext
|
20
|
+
from . import notification
|
21
|
+
from . import obs
|
22
|
+
from . import util
|
23
|
+
from .vad import vosk_helper, silero_trim, whisper_helper
|
24
|
+
from .configuration import *
|
25
|
+
from .ffmpeg import get_audio_and_trim
|
26
|
+
from .gametext import get_line_timing
|
27
|
+
from .util import *
|
28
|
+
|
29
|
+
config_pids = []
|
30
|
+
settings_window: config_gui.ConfigApp = None
|
31
|
+
obs_paused = False
|
32
|
+
icon: Icon
|
33
|
+
menu: Menu
|
34
|
+
|
35
|
+
|
36
|
+
class VideoToAudioHandler(FileSystemEventHandler):
|
37
|
+
def on_created(self, event):
|
38
|
+
if event.is_directory or "Replay" not in event.src_path:
|
39
|
+
return
|
40
|
+
if event.src_path.endswith(".mkv") or event.src_path.endswith(".mp4"): # Adjust based on your OBS output format
|
41
|
+
logger.info(f"MKV {event.src_path} FOUND, RUNNING LOGIC")
|
42
|
+
time.sleep(.5) # Small Sleep to allow for replay to be fully written
|
43
|
+
self.convert_to_audio(event.src_path)
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def convert_to_audio(video_path):
|
47
|
+
try:
|
48
|
+
with util.lock:
|
49
|
+
if get_config().obs.minimum_replay_size and not ffmpeg.is_video_big_enough(video_path,
|
50
|
+
get_config().obs.minimum_replay_size):
|
51
|
+
notification.send_check_obs_notification(reason="Video may be empty, check scene in OBS.")
|
52
|
+
logger.error(
|
53
|
+
f"Video was unusually small, potentially empty! Check OBS for Correct Scene Settings! Path: {video_path}")
|
54
|
+
return
|
55
|
+
util.use_previous_audio = True
|
56
|
+
last_note = None
|
57
|
+
if get_config().anki.update_anki:
|
58
|
+
last_note = anki.get_last_anki_card()
|
59
|
+
if get_config().features.backfill_audio:
|
60
|
+
last_note = anki.get_cards_by_sentence(gametext.previous_line)
|
61
|
+
line_time, next_line_time = get_line_timing(last_note)
|
62
|
+
ss_timing = 0
|
63
|
+
if line_time and next_line_time:
|
64
|
+
ss_timing = ffmpeg.get_screenshot_time(video_path, line_time)
|
65
|
+
if last_note:
|
66
|
+
logger.debug(json.dumps(last_note))
|
67
|
+
|
68
|
+
note = anki.get_initial_card_info(last_note)
|
69
|
+
|
70
|
+
tango = last_note['fields'][get_config().anki.word_field]['value'] if last_note else ''
|
71
|
+
|
72
|
+
if get_config().anki.sentence_audio_field:
|
73
|
+
final_audio_output, should_update_audio, vad_trimmed_audio = VideoToAudioHandler.get_audio(
|
74
|
+
line_time,
|
75
|
+
next_line_time,
|
76
|
+
video_path)
|
77
|
+
else:
|
78
|
+
final_audio_output = ""
|
79
|
+
should_update_audio = False
|
80
|
+
vad_trimmed_audio = ""
|
81
|
+
logger.info("No SentenceAudio Field in config, skipping audio processing!")
|
82
|
+
try:
|
83
|
+
# Only update sentenceaudio if it's not present. Want to avoid accidentally overwriting sentence audio
|
84
|
+
try:
|
85
|
+
if get_config().anki.update_anki and last_note:
|
86
|
+
anki.update_anki_card(last_note, note, audio_path=final_audio_output, video_path=video_path,
|
87
|
+
tango=tango,
|
88
|
+
should_update_audio=should_update_audio,
|
89
|
+
ss_time=ss_timing)
|
90
|
+
elif get_config().features.notify_on_update and should_update_audio:
|
91
|
+
notification.send_audio_generated_notification(vad_trimmed_audio)
|
92
|
+
except Exception as e:
|
93
|
+
logger.error(f"Card failed to update! Maybe it was removed? {e}")
|
94
|
+
except FileNotFoundError as f:
|
95
|
+
print(f)
|
96
|
+
print("Something went wrong with processing, anki card not updated")
|
97
|
+
except Exception as e:
|
98
|
+
logger.error(f"Some error was hit catching to allow further work to be done: {e}")
|
99
|
+
if get_config().paths.remove_video and os.path.exists(video_path):
|
100
|
+
os.remove(video_path) # Optionally remove the video after conversion
|
101
|
+
if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
102
|
+
os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
|
103
|
+
|
104
|
+
@staticmethod
|
105
|
+
def get_audio(line_time, next_line_time, video_path):
|
106
|
+
trimmed_audio = get_audio_and_trim(video_path, line_time, next_line_time)
|
107
|
+
vad_trimmed_audio = make_unique_file_name(
|
108
|
+
f"{os.path.abspath(configuration.temp_directory)}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
109
|
+
final_audio_output = make_unique_file_name(
|
110
|
+
f"{get_config().paths.audio_destination}{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
111
|
+
should_update_audio = True
|
112
|
+
if get_config().vad.do_vad_postprocessing:
|
113
|
+
match get_config().vad.selected_vad_model:
|
114
|
+
case configuration.SILERO:
|
115
|
+
should_update_audio = silero_trim.process_audio_with_silero(trimmed_audio, vad_trimmed_audio)
|
116
|
+
case configuration.VOSK:
|
117
|
+
should_update_audio = vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio)
|
118
|
+
case configuration.WHISPER:
|
119
|
+
should_update_audio = whisper_helper.process_audio_with_whisper(trimmed_audio,
|
120
|
+
vad_trimmed_audio)
|
121
|
+
if not should_update_audio:
|
122
|
+
match get_config().vad.backup_vad_model:
|
123
|
+
case configuration.OFF:
|
124
|
+
pass
|
125
|
+
case configuration.SILERO:
|
126
|
+
should_update_audio = silero_trim.process_audio_with_silero(trimmed_audio,
|
127
|
+
vad_trimmed_audio)
|
128
|
+
case configuration.VOSK:
|
129
|
+
should_update_audio = vosk_helper.process_audio_with_vosk(trimmed_audio, vad_trimmed_audio)
|
130
|
+
case configuration.WHISPER:
|
131
|
+
should_update_audio = whisper_helper.process_audio_with_whisper(trimmed_audio,
|
132
|
+
vad_trimmed_audio)
|
133
|
+
if get_config().audio.ffmpeg_reencode_options and os.path.exists(vad_trimmed_audio):
|
134
|
+
ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
|
135
|
+
get_config().audio.ffmpeg_reencode_options)
|
136
|
+
elif os.path.exists(vad_trimmed_audio):
|
137
|
+
os.replace(vad_trimmed_audio, final_audio_output)
|
138
|
+
return final_audio_output, should_update_audio, vad_trimmed_audio
|
139
|
+
|
140
|
+
|
141
|
+
def initialize(reloading=False):
|
142
|
+
if not reloading:
|
143
|
+
if get_config().obs.enabled:
|
144
|
+
obs.connect_to_obs(start_replay=True)
|
145
|
+
anki.start_monitoring_anki()
|
146
|
+
if get_config().general.open_config_on_startup:
|
147
|
+
proc = subprocess.Popen([sys.executable, "config_gui.py"])
|
148
|
+
config_pids.append(proc.pid)
|
149
|
+
gametext.start_text_monitor()
|
150
|
+
if not os.path.exists(get_config().paths.folder_to_watch):
|
151
|
+
os.mkdir(get_config().paths.folder_to_watch)
|
152
|
+
if not os.path.exists(get_config().paths.screenshot_destination):
|
153
|
+
os.mkdir(get_config().paths.screenshot_destination)
|
154
|
+
if not os.path.exists(get_config().paths.audio_destination):
|
155
|
+
os.mkdir(get_config().paths.audio_destination)
|
156
|
+
if not os.path.exists("../temp_files"):
|
157
|
+
os.mkdir("../temp_files")
|
158
|
+
else:
|
159
|
+
for filename in os.scandir('../temp_files'):
|
160
|
+
file_path = os.path.join('../temp_files', filename.name)
|
161
|
+
if filename.is_file() or filename.is_symlink():
|
162
|
+
os.remove(file_path)
|
163
|
+
elif filename.is_dir():
|
164
|
+
shutil.rmtree(file_path)
|
165
|
+
if get_config().vad.do_vad_postprocessing:
|
166
|
+
if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
167
|
+
vosk_helper.get_vosk_model()
|
168
|
+
if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
169
|
+
whisper_helper.initialize_whisper_model()
|
170
|
+
|
171
|
+
|
172
|
+
def register_hotkeys():
|
173
|
+
keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
|
174
|
+
keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
|
175
|
+
|
176
|
+
|
177
|
+
def get_screenshot():
|
178
|
+
try:
|
179
|
+
image = obs.get_screenshot()
|
180
|
+
time.sleep(2) # Wait for ss to save
|
181
|
+
if not image:
|
182
|
+
raise Exception("Failed to get Screenshot from OBS")
|
183
|
+
encoded_image = ffmpeg.process_image(image)
|
184
|
+
if get_config().anki.update_anki and get_config().screenshot.screenshot_hotkey_updates_anki:
|
185
|
+
last_note = anki.get_last_anki_card()
|
186
|
+
if last_note:
|
187
|
+
logger.debug(json.dumps(last_note))
|
188
|
+
if get_config().features.backfill_audio:
|
189
|
+
last_note = anki.get_cards_by_sentence(gametext.previous_line)
|
190
|
+
if last_note:
|
191
|
+
anki.add_image_to_card(last_note, encoded_image)
|
192
|
+
notification.send_screenshot_updated(last_note['fields'][get_config().anki.word_field]['value'])
|
193
|
+
if get_config().features.open_anki_edit:
|
194
|
+
notification.open_anki_card(last_note['noteId'])
|
195
|
+
else:
|
196
|
+
notification.send_screenshot_saved(encoded_image)
|
197
|
+
else:
|
198
|
+
notification.send_screenshot_saved(encoded_image)
|
199
|
+
except Exception as e:
|
200
|
+
logger.error(f"Failed to get Screenshot {e}")
|
201
|
+
|
202
|
+
|
203
|
+
def create_image():
|
204
|
+
"""Create a simple pickaxe icon."""
|
205
|
+
width, height = 64, 64
|
206
|
+
image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # Transparent background
|
207
|
+
draw = ImageDraw.Draw(image)
|
208
|
+
|
209
|
+
# Handle (rectangle)
|
210
|
+
handle_color = (139, 69, 19) # Brown color
|
211
|
+
draw.rectangle([(30, 15), (34, 50)], fill=handle_color)
|
212
|
+
|
213
|
+
# Blade (triangle-like shape)
|
214
|
+
blade_color = (192, 192, 192) # Silver color
|
215
|
+
draw.polygon([(15, 15), (49, 15), (32, 5)], fill=blade_color)
|
216
|
+
|
217
|
+
return image
|
218
|
+
|
219
|
+
|
220
|
+
def open_settings():
|
221
|
+
obs.update_current_game()
|
222
|
+
settings_window.show()
|
223
|
+
|
224
|
+
|
225
|
+
def open_log():
|
226
|
+
"""Function to handle opening log."""
|
227
|
+
"""Open log file with the default application."""
|
228
|
+
log_file_path = "../../gamesentenceminer.log"
|
229
|
+
if not os.path.exists(log_file_path):
|
230
|
+
print("Log file not found!")
|
231
|
+
return
|
232
|
+
|
233
|
+
if sys.platform.startswith("win"): # Windows
|
234
|
+
os.startfile(log_file_path)
|
235
|
+
elif sys.platform.startswith("darwin"): # macOS
|
236
|
+
subprocess.call(["open", log_file_path])
|
237
|
+
elif sys.platform.startswith("linux"): # Linux
|
238
|
+
subprocess.call(["xdg-open", log_file_path])
|
239
|
+
else:
|
240
|
+
print("Unsupported platform!")
|
241
|
+
print("Log opened.")
|
242
|
+
|
243
|
+
|
244
|
+
def exit_program(icon, item):
|
245
|
+
"""Exit the application."""
|
246
|
+
print("Exiting...")
|
247
|
+
icon.stop()
|
248
|
+
cleanup()
|
249
|
+
|
250
|
+
|
251
|
+
def play_pause(icon, item):
|
252
|
+
global obs_paused, menu
|
253
|
+
if obs_paused:
|
254
|
+
obs.start_replay_buffer()
|
255
|
+
else:
|
256
|
+
obs.stop_replay_buffer()
|
257
|
+
|
258
|
+
obs_paused = not obs_paused
|
259
|
+
update_icon()
|
260
|
+
|
261
|
+
def get_obs_icon_text():
|
262
|
+
return "Pause OBS" if obs_paused else "Resume OBS"
|
263
|
+
|
264
|
+
|
265
|
+
def update_icon():
|
266
|
+
global menu, icon
|
267
|
+
# Recreate the menu with the updated button text
|
268
|
+
profile_menu = Menu(
|
269
|
+
*[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for profile in
|
270
|
+
get_master_config().get_all_profile_names()]
|
271
|
+
)
|
272
|
+
|
273
|
+
menu = Menu(
|
274
|
+
MenuItem("Open Settings", open_settings),
|
275
|
+
MenuItem("Open Log", open_log),
|
276
|
+
MenuItem(get_obs_icon_text(), play_pause),
|
277
|
+
MenuItem("Switch Profile", profile_menu),
|
278
|
+
MenuItem("Exit", exit_program)
|
279
|
+
)
|
280
|
+
|
281
|
+
icon.menu = menu
|
282
|
+
icon.update_menu()
|
283
|
+
|
284
|
+
def switch_profile(icon, item):
|
285
|
+
if "Active:" in item.text:
|
286
|
+
logger.error("You cannot switch to the currently active profile!")
|
287
|
+
return
|
288
|
+
logger.info(f"Switching to profile: {item.text}")
|
289
|
+
get_master_config().current_profile = item.text
|
290
|
+
switch_profile_and_save(item.text)
|
291
|
+
settings_window.reload_settings()
|
292
|
+
update_icon()
|
293
|
+
|
294
|
+
|
295
|
+
def run_tray():
|
296
|
+
global menu, icon
|
297
|
+
|
298
|
+
profile_menu = Menu(
|
299
|
+
*[MenuItem(("Active: " if profile == get_master_config().current_profile else "") + profile, switch_profile) for
|
300
|
+
profile in
|
301
|
+
get_master_config().get_all_profile_names()]
|
302
|
+
)
|
303
|
+
|
304
|
+
menu = Menu(
|
305
|
+
MenuItem("Open Settings", open_settings),
|
306
|
+
MenuItem("Open Log", open_log),
|
307
|
+
MenuItem(get_obs_icon_text(), play_pause),
|
308
|
+
MenuItem("Switch Profile", profile_menu),
|
309
|
+
MenuItem("Exit", exit_program)
|
310
|
+
)
|
311
|
+
|
312
|
+
icon = Icon("TrayApp", create_image(), "Game Sentence Miner", menu)
|
313
|
+
icon.run()
|
314
|
+
|
315
|
+
|
316
|
+
def cleanup():
|
317
|
+
logger.info("Performing cleanup...")
|
318
|
+
util.keep_running = False
|
319
|
+
|
320
|
+
for pid in config_pids:
|
321
|
+
try:
|
322
|
+
p = psutil.Process(pid)
|
323
|
+
p.terminate() # Gracefully terminate the process
|
324
|
+
except psutil.NoSuchProcess:
|
325
|
+
logger.info("Config process already closed.")
|
326
|
+
except Exception as e:
|
327
|
+
logger.error(f"Error terminating process {pid}: {e}")
|
328
|
+
|
329
|
+
if get_config().obs.enabled:
|
330
|
+
if get_config().obs.start_buffer:
|
331
|
+
obs.stop_replay_buffer()
|
332
|
+
obs.disconnect_from_obs()
|
333
|
+
settings_window.window.destroy()
|
334
|
+
logger.info("Cleanup complete.")
|
335
|
+
|
336
|
+
|
337
|
+
def handle_exit():
|
338
|
+
"""Signal handler for graceful termination."""
|
339
|
+
|
340
|
+
def _handle_exit(signum):
|
341
|
+
logger.info(f"Received signal {signum}. Exiting gracefully...")
|
342
|
+
cleanup()
|
343
|
+
sys.exit(0)
|
344
|
+
|
345
|
+
return _handle_exit
|
346
|
+
|
347
|
+
|
348
|
+
def main(reloading=False, do_config_input=True):
|
349
|
+
global settings_window
|
350
|
+
logger.info("Script started.")
|
351
|
+
initialize(reloading)
|
352
|
+
with tempfile.TemporaryDirectory(dir="../temp_files") as temp_dir:
|
353
|
+
configuration.temp_directory = temp_dir
|
354
|
+
event_handler = VideoToAudioHandler()
|
355
|
+
observer = Observer()
|
356
|
+
observer.schedule(event_handler, get_config().paths.folder_to_watch, recursive=False)
|
357
|
+
observer.start()
|
358
|
+
|
359
|
+
logger.info("Script Initialized. Happy Mining!")
|
360
|
+
if not is_linux():
|
361
|
+
register_hotkeys()
|
362
|
+
|
363
|
+
# Register signal handlers for graceful shutdown
|
364
|
+
signal.signal(signal.SIGTERM, handle_exit()) # Handle `kill` commands
|
365
|
+
signal.signal(signal.SIGINT, handle_exit()) # Handle Ctrl+C
|
366
|
+
win32api.SetConsoleCtrlHandler(handle_exit())
|
367
|
+
|
368
|
+
util.run_new_thread(run_tray)
|
369
|
+
|
370
|
+
try:
|
371
|
+
settings_window = config_gui.ConfigApp()
|
372
|
+
settings_window.add_save_hook(update_icon)
|
373
|
+
settings_window.window.mainloop()
|
374
|
+
except KeyboardInterrupt:
|
375
|
+
cleanup()
|
376
|
+
|
377
|
+
try:
|
378
|
+
observer.stop()
|
379
|
+
observer.join()
|
380
|
+
except Exception as e:
|
381
|
+
logger.error(f"Error stopping observer: {e}")
|
382
|
+
|
383
|
+
|
384
|
+
if __name__ == "__main__":
|
385
|
+
main()
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
from dataclasses_json import dataclass_json
|
5
|
+
|
6
|
+
|
7
|
+
# OBS
|
8
|
+
@dataclass_json
|
9
|
+
@dataclass
|
10
|
+
class SceneInfo:
|
11
|
+
currentProgramSceneName: str
|
12
|
+
currentProgramSceneUuid: str
|
13
|
+
sceneName: str
|
14
|
+
sceneUuid: str
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass_json
|
18
|
+
@dataclass
|
19
|
+
class SceneItemTransform:
|
20
|
+
alignment: int
|
21
|
+
boundsAlignment: int
|
22
|
+
boundsHeight: float
|
23
|
+
boundsType: str
|
24
|
+
boundsWidth: float
|
25
|
+
cropBottom: int
|
26
|
+
cropLeft: int
|
27
|
+
cropRight: int
|
28
|
+
cropToBounds: bool
|
29
|
+
cropTop: int
|
30
|
+
height: float
|
31
|
+
positionX: float
|
32
|
+
positionY: float
|
33
|
+
rotation: float
|
34
|
+
scaleX: float
|
35
|
+
scaleY: float
|
36
|
+
sourceHeight: float
|
37
|
+
sourceWidth: float
|
38
|
+
width: float
|
39
|
+
|
40
|
+
|
41
|
+
@dataclass_json
|
42
|
+
@dataclass
|
43
|
+
class SceneItem:
|
44
|
+
inputKind: str
|
45
|
+
isGroup: Optional[bool]
|
46
|
+
sceneItemBlendMode: str
|
47
|
+
sceneItemEnabled: bool
|
48
|
+
sceneItemId: int
|
49
|
+
sceneItemIndex: int
|
50
|
+
sceneItemLocked: bool
|
51
|
+
sceneItemTransform: SceneItemTransform
|
52
|
+
sourceName: str
|
53
|
+
sourceType: str
|
54
|
+
sourceUuid: str
|
55
|
+
|
56
|
+
# def __init__(self, **kwargs):
|
57
|
+
# self.inputKind = kwargs['inputKind']
|
58
|
+
# self.isGroup = kwargs['isGroup']
|
59
|
+
# self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
|
60
|
+
# self.sceneItemEnabled = kwargs['sceneItemEnabled']
|
61
|
+
# self.sceneItemId = kwargs['sceneItemId']
|
62
|
+
# self.sceneItemIndex = kwargs['sceneItemIndex']
|
63
|
+
# self.sceneItemLocked = kwargs['sceneItemLocked']
|
64
|
+
# self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
|
65
|
+
# self.sourceName = kwargs['sourceName']
|
66
|
+
# self.sourceType = kwargs['sourceType']
|
67
|
+
# self.sourceUuid = kwargs['sourceUuid']
|
68
|
+
|
69
|
+
|
70
|
+
@dataclass_json
|
71
|
+
@dataclass
|
72
|
+
class SceneItemsResponse:
|
73
|
+
sceneItems: List[SceneItem]
|
74
|
+
|
75
|
+
# def __init__(self, **kwargs):
|
76
|
+
# self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
|
77
|
+
|
78
|
+
#
|
79
|
+
# @dataclass_json
|
80
|
+
# @dataclass
|
81
|
+
# class SourceActive:
|
82
|
+
# videoActive: bool
|
83
|
+
# videoShowing: bool
|
84
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import requests
|
2
|
+
from plyer import notification
|
3
|
+
|
4
|
+
|
5
|
+
def open_anki_card(note_id):
|
6
|
+
url = "http://localhost:8765"
|
7
|
+
headers = {'Content-Type': 'application/json'}
|
8
|
+
|
9
|
+
data = {
|
10
|
+
"action": "guiEditNote",
|
11
|
+
"version": 6,
|
12
|
+
"params": {
|
13
|
+
"note": note_id
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
try:
|
18
|
+
response = requests.post(url, json=data, headers=headers)
|
19
|
+
if response.status_code == 200:
|
20
|
+
print(f"Opened Anki note with ID {note_id}")
|
21
|
+
else:
|
22
|
+
print(f"Failed to open Anki note with ID {note_id}")
|
23
|
+
except Exception as e:
|
24
|
+
print(f"Error connecting to AnkiConnect: {e}")
|
25
|
+
|
26
|
+
|
27
|
+
def send_notification(tango):
|
28
|
+
notification.notify(
|
29
|
+
title="Anki Card Updated",
|
30
|
+
message=f"Audio and/or Screenshot added to note: {tango}",
|
31
|
+
app_name="GameSentenceMiner",
|
32
|
+
timeout=5 # Notification disappears after 5 seconds
|
33
|
+
)
|
34
|
+
|
35
|
+
|
36
|
+
def send_screenshot_updated(tango):
|
37
|
+
notification.notify(
|
38
|
+
title="Anki Card Updated",
|
39
|
+
message=f"Screenshot updated on note: {tango}",
|
40
|
+
app_name="GameSentenceMiner",
|
41
|
+
timeout=5 # Notification disappears after 5 seconds
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
def send_screenshot_saved(path):
|
46
|
+
notification.notify(
|
47
|
+
title="Screenshot Saved",
|
48
|
+
message=f"Screenshot saved to : {path}",
|
49
|
+
app_name="GameSentenceMiner",
|
50
|
+
timeout=5 # Notification disappears after 5 seconds
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
def send_audio_generated_notification(audio_path):
|
55
|
+
notification.notify(
|
56
|
+
title="Audio Trimmed",
|
57
|
+
message=f"Audio Trimmed and placed at {audio_path}",
|
58
|
+
app_name="VideoGameMiner",
|
59
|
+
timeout=5 # Notification disappears after 5 seconds
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
def send_check_obs_notification(reason):
|
64
|
+
notification.notify(
|
65
|
+
title="OBS Replay Invalid",
|
66
|
+
message=f"Check OBS Settings! Reason: {reason}",
|
67
|
+
app_name="GameSentenceMiner",
|
68
|
+
timeout=5 # Notification disappears after 5 seconds
|
69
|
+
)
|