GameSentenceMiner 2.5.5__py3-none-any.whl → 2.5.7__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 +14 -8
- GameSentenceMiner/communication/__init__.py +22 -0
- GameSentenceMiner/communication/send.py +7 -0
- GameSentenceMiner/communication/websocket.py +77 -0
- GameSentenceMiner/config_gui.py +64 -4
- GameSentenceMiner/configuration.py +23 -1
- GameSentenceMiner/ffmpeg.py +22 -14
- GameSentenceMiner/gsm.py +146 -59
- GameSentenceMiner/notification.py +1 -1
- GameSentenceMiner/obs.py +36 -14
- GameSentenceMiner/util.py +41 -6
- GameSentenceMiner/utility_gui.py +40 -2
- {gamesentenceminer-2.5.5.dist-info → gamesentenceminer-2.5.7.dist-info}/METADATA +1 -1
- gamesentenceminer-2.5.7.dist-info/RECORD +29 -0
- {gamesentenceminer-2.5.5.dist-info → gamesentenceminer-2.5.7.dist-info}/WHEEL +1 -1
- GameSentenceMiner/electron_messaging.py +0 -6
- gamesentenceminer-2.5.5.dist-info/RECORD +0 -27
- {gamesentenceminer-2.5.5.dist-info → gamesentenceminer-2.5.7.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.5.5.dist-info → gamesentenceminer-2.5.7.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.5.5.dist-info → gamesentenceminer-2.5.7.dist-info}/top_level.txt +0 -0
GameSentenceMiner/anki.py
CHANGED
@@ -120,10 +120,9 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
|
|
120
120
|
if not last_note:
|
121
121
|
return note
|
122
122
|
game_line = get_text_event(last_note)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
sentences_text = ''
|
123
|
+
sentences = []
|
124
|
+
sentences_text = ''
|
125
|
+
if selected_lines:
|
127
126
|
try:
|
128
127
|
sentence_in_anki = last_note.get_field(get_config().anki.sentence_field)
|
129
128
|
logger.info(f"Attempting Preserve HTML for multi-line")
|
@@ -134,11 +133,11 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
|
|
134
133
|
else:
|
135
134
|
sentences.append(line.text)
|
136
135
|
|
137
|
-
logger.
|
138
|
-
logger.
|
136
|
+
logger.debug(f"Attempting to Fix Character Dialogue Format")
|
137
|
+
logger.debug([f"{line}" for line in sentences])
|
139
138
|
try:
|
140
139
|
combined_lines = combine_dialogue(sentences)
|
141
|
-
|
140
|
+
logger.debug(combined_lines)
|
142
141
|
if combined_lines:
|
143
142
|
sentences_text = "".join(combined_lines)
|
144
143
|
except Exception as e:
|
@@ -147,7 +146,13 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
|
|
147
146
|
except Exception as e:
|
148
147
|
logger.debug(f"Error preserving HTML for multi-line: {e}")
|
149
148
|
pass
|
150
|
-
|
149
|
+
multi_line_sentence = sentences_text if sentences_text else get_config().advanced.multi_line_line_break.join(sentences)
|
150
|
+
if get_config().anki.multi_overwrites_sentence:
|
151
|
+
note['fields'][get_config().anki.sentence_field] = multi_line_sentence
|
152
|
+
else:
|
153
|
+
logger.info(f"Configured to not overwrite sentence field, Multi-line Sentence If you want it, Note you need to do ctrl+shift+x in anki to paste properly:\n\n" + (sentences_text if sentences_text else get_config().advanced.multi_line_line_break.join(sentences)) + "\n")
|
154
|
+
if get_config().advanced.multi_line_sentence_storage_field:
|
155
|
+
note['fields'][get_config().advanced.multi_line_sentence_storage_field] = multi_line_sentence
|
151
156
|
|
152
157
|
if get_config().anki.previous_sentence_field and game_line.prev and not \
|
153
158
|
last_note.get_field(get_config().anki.previous_sentence_field):
|
@@ -259,6 +264,7 @@ def update_new_card():
|
|
259
264
|
lines = get_utility_window().get_selected_lines()
|
260
265
|
with util.lock:
|
261
266
|
update_anki_card(last_card, note=get_initial_card_info(last_card, lines), reuse_audio=True)
|
267
|
+
get_utility_window().reset_checkboxes()
|
262
268
|
else:
|
263
269
|
logger.info("New card(s) detected! Added to Processing Queue!")
|
264
270
|
card_queue.append(last_card)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import os.path
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
from typing import Dict, Optional, Any
|
4
|
+
|
5
|
+
from dataclasses_json import dataclass_json, Undefined
|
6
|
+
from websocket import WebSocket
|
7
|
+
|
8
|
+
from GameSentenceMiner.configuration import get_app_directory
|
9
|
+
|
10
|
+
CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
|
11
|
+
websocket: WebSocket = None
|
12
|
+
|
13
|
+
@dataclass_json(undefined=Undefined.RAISE)
|
14
|
+
@dataclass
|
15
|
+
class Message:
|
16
|
+
"""
|
17
|
+
Represents a message for inter-process communication.
|
18
|
+
Mimics the structure of IPC or HTTP calls.
|
19
|
+
"""
|
20
|
+
function: str
|
21
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
22
|
+
id: Optional[str] = None
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os.path
|
3
|
+
|
4
|
+
import websockets
|
5
|
+
import json
|
6
|
+
|
7
|
+
from websocket import WebSocket
|
8
|
+
|
9
|
+
from GameSentenceMiner.communication import Message
|
10
|
+
from GameSentenceMiner.configuration import get_app_directory, logger
|
11
|
+
|
12
|
+
CONFIG_FILE = os.path.join(get_app_directory(), "shared_config.json")
|
13
|
+
websocket: WebSocket = None
|
14
|
+
handle_websocket_message = None
|
15
|
+
|
16
|
+
async def do_websocket_connection(port):
|
17
|
+
"""
|
18
|
+
Connects to the WebSocket server running in the Electron app.
|
19
|
+
"""
|
20
|
+
global websocket
|
21
|
+
|
22
|
+
uri = f"ws://localhost:{port}" # Use the port from Electron
|
23
|
+
logger.debug(f"Electron Communication : Connecting to server at {uri}...")
|
24
|
+
try:
|
25
|
+
async with websockets.connect(uri) as websocket:
|
26
|
+
logger.debug(f"Connected to websocket server at {uri}")
|
27
|
+
|
28
|
+
# Send an initial message
|
29
|
+
message = Message(function="on_connect", data={"message": "Hello from Python!"})
|
30
|
+
await websocket.send(message.to_json())
|
31
|
+
logger.debug(f"> Sent: {message}")
|
32
|
+
|
33
|
+
# Receive messages from the server
|
34
|
+
while True:
|
35
|
+
try:
|
36
|
+
response = await websocket.recv()
|
37
|
+
if response is None:
|
38
|
+
break
|
39
|
+
logger.debug(f"Electron Communication : < Received: {response}")
|
40
|
+
handle_websocket_message(Message.from_json(response))
|
41
|
+
await asyncio.sleep(1) # keep the connection alive
|
42
|
+
except websockets.ConnectionClosedOK:
|
43
|
+
logger.debug("Electron Communication : Connection closed by server")
|
44
|
+
break
|
45
|
+
except websockets.ConnectionClosedError as e:
|
46
|
+
logger.debug(f"Electron Communication : Connection closed with error: {e}")
|
47
|
+
break
|
48
|
+
except ConnectionRefusedError:
|
49
|
+
logger.debug(f"Electron Communication : Error: Could not connect to server at {uri}. Electron App not running..")
|
50
|
+
except Exception as e:
|
51
|
+
logger.debug(f"Electron Communication : An error occurred: {e}")
|
52
|
+
|
53
|
+
def connect_websocket():
|
54
|
+
"""
|
55
|
+
Main function to run the WebSocket client.
|
56
|
+
"""
|
57
|
+
# Load the port from the same config.json the Electron app uses
|
58
|
+
try:
|
59
|
+
with open(CONFIG_FILE, "r") as f:
|
60
|
+
config = json.load(f)
|
61
|
+
port = config["port"]
|
62
|
+
except FileNotFoundError:
|
63
|
+
print("Error: shared_config.json not found. Using default port 8766. Ensure Electron app creates this file.")
|
64
|
+
port = 8766 # Default port, same as in Electron
|
65
|
+
except json.JSONDecodeError:
|
66
|
+
print("Error: shared_config.json was not valid JSON. Using default port 8765.")
|
67
|
+
port = 8766
|
68
|
+
|
69
|
+
asyncio.run(do_websocket_connection(port))
|
70
|
+
|
71
|
+
def register_websocket_message_handler(handler):
|
72
|
+
global handle_websocket_message
|
73
|
+
handle_websocket_message = handler
|
74
|
+
|
75
|
+
|
76
|
+
if __name__ == "__main__":
|
77
|
+
connect_websocket()
|
GameSentenceMiner/config_gui.py
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
import tkinter as tk
|
2
2
|
from tkinter import filedialog, messagebox, simpledialog
|
3
3
|
|
4
|
-
import pyperclip
|
5
4
|
import ttkbootstrap as ttk
|
6
5
|
|
7
6
|
from GameSentenceMiner import obs, configuration
|
7
|
+
from GameSentenceMiner.communication.send import send_restart_signal
|
8
8
|
from GameSentenceMiner.configuration import *
|
9
9
|
from GameSentenceMiner.downloader.download_tools import download_ocenaudio_if_needed
|
10
|
-
from GameSentenceMiner.electron_messaging import signal_restart_settings_change
|
11
10
|
from GameSentenceMiner.package import get_current_version, get_latest_version
|
12
11
|
|
13
12
|
settings_saved = False
|
@@ -77,6 +76,7 @@ class ConfigApp:
|
|
77
76
|
self.create_obs_tab()
|
78
77
|
self.create_hotkeys_tab()
|
79
78
|
self.create_profiles_tab()
|
79
|
+
self.create_advanced_tab()
|
80
80
|
|
81
81
|
ttk.Button(self.window, text="Save Settings", command=self.save_settings).pack(pady=20)
|
82
82
|
|
@@ -178,7 +178,8 @@ class ConfigApp:
|
|
178
178
|
hotkeys=Hotkeys(
|
179
179
|
reset_line=self.reset_line_hotkey.get(),
|
180
180
|
take_screenshot=self.take_screenshot_hotkey.get(),
|
181
|
-
open_utility=self.open_utility_hotkey.get()
|
181
|
+
open_utility=self.open_utility_hotkey.get(),
|
182
|
+
play_latest_audio=self.play_latest_audio_hotkey.get()
|
182
183
|
),
|
183
184
|
vad=VAD(
|
184
185
|
whisper_model=self.whisper_model.get(),
|
@@ -189,6 +190,13 @@ class ConfigApp:
|
|
189
190
|
trim_beginning=self.vad_trim_beginning.get(),
|
190
191
|
beginning_offset=float(self.vad_beginning_offset.get()),
|
191
192
|
add_audio_on_no_results=self.add_audio_on_no_results.get(),
|
193
|
+
),
|
194
|
+
advanced=Advanced(
|
195
|
+
audio_player_path=self.audio_player_path.get(),
|
196
|
+
video_player_path=self.video_player_path.get(),
|
197
|
+
show_screenshot_buttons=self.show_screenshot_button.get(),
|
198
|
+
multi_line_line_break=self.multi_line_line_break.get(),
|
199
|
+
multi_line_sentence_storage_field=self.multi_line_sentence_storage_field.get(),
|
192
200
|
)
|
193
201
|
)
|
194
202
|
|
@@ -216,7 +224,7 @@ class ConfigApp:
|
|
216
224
|
|
217
225
|
if self.master_config.get_config().restart_required(prev_config):
|
218
226
|
logger.info("Restart Required for some settings to take affect!")
|
219
|
-
|
227
|
+
send_restart_signal()
|
220
228
|
settings_saved = True
|
221
229
|
configuration.reload_config()
|
222
230
|
self.settings = get_config()
|
@@ -248,6 +256,7 @@ class ConfigApp:
|
|
248
256
|
self.create_obs_tab()
|
249
257
|
self.create_hotkeys_tab()
|
250
258
|
self.create_profiles_tab()
|
259
|
+
self.create_advanced_tab()
|
251
260
|
|
252
261
|
|
253
262
|
def increment_row(self):
|
@@ -442,6 +451,12 @@ class ConfigApp:
|
|
442
451
|
|
443
452
|
return paths_frame
|
444
453
|
|
454
|
+
def browse_file(self, entry_widget):
|
455
|
+
file_selected = filedialog.askopenfilename()
|
456
|
+
if file_selected:
|
457
|
+
entry_widget.delete(0, tk.END)
|
458
|
+
entry_widget.insert(0, file_selected)
|
459
|
+
|
445
460
|
def browse_folder(self, entry_widget):
|
446
461
|
folder_selected = filedialog.askdirectory()
|
447
462
|
if folder_selected:
|
@@ -880,6 +895,51 @@ class ConfigApp:
|
|
880
895
|
if self.master_config.current_profile != DEFAULT_CONFIG:
|
881
896
|
ttk.Button(profiles_frame, text="Delete Config", command=self.delete_profile).grid(row=self.current_row, column=2, pady=5)
|
882
897
|
|
898
|
+
@new_tab
|
899
|
+
def create_advanced_tab(self):
|
900
|
+
advanced_frame = ttk.Frame(self.notebook)
|
901
|
+
self.notebook.add(advanced_frame, text='Advanced')
|
902
|
+
|
903
|
+
ttk.Label(advanced_frame, text="Note: Only one of these will take effect, prioritizing audio.", foreground="red").grid(row=self.current_row, column=0, columnspan=3, sticky='W')
|
904
|
+
self.current_row += 1
|
905
|
+
|
906
|
+
ttk.Label(advanced_frame, text="Audio Player Path:").grid(row=self.current_row, column=0, sticky='W')
|
907
|
+
self.audio_player_path = ttk.Entry(advanced_frame, width=50)
|
908
|
+
self.audio_player_path.insert(0, self.settings.advanced.audio_player_path)
|
909
|
+
self.audio_player_path.grid(row=self.current_row, column=1)
|
910
|
+
ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.audio_player_path)).grid(row=self.current_row, column=2)
|
911
|
+
self.add_label_and_increment_row(advanced_frame, "Path to the audio player executable. Will open the trimmed Audio", row=self.current_row, column=3)
|
912
|
+
|
913
|
+
ttk.Label(advanced_frame, text="Video Player Path:").grid(row=self.current_row, column=0, sticky='W')
|
914
|
+
self.video_player_path = ttk.Entry(advanced_frame, width=50)
|
915
|
+
self.video_player_path.insert(0, self.settings.advanced.video_player_path)
|
916
|
+
self.video_player_path.grid(row=self.current_row, column=1)
|
917
|
+
ttk.Button(advanced_frame, text="Browse", command=lambda: self.browse_file(self.video_player_path)).grid(row=self.current_row, column=2)
|
918
|
+
self.add_label_and_increment_row(advanced_frame, "Path to the video player executable. Will seek to the location of the line in the replay", row=self.current_row, column=3)
|
919
|
+
|
920
|
+
|
921
|
+
ttk.Label(advanced_frame, text="Play Latest Video/Audio Hotkey:").grid(row=self.current_row, column=0, sticky='W')
|
922
|
+
self.play_latest_audio_hotkey = ttk.Entry(advanced_frame)
|
923
|
+
self.play_latest_audio_hotkey.insert(0, self.settings.hotkeys.play_latest_audio)
|
924
|
+
self.play_latest_audio_hotkey.grid(row=self.current_row, column=1)
|
925
|
+
self.add_label_and_increment_row(advanced_frame, "Hotkey to trim and play the latest audio.", row=self.current_row, column=2)
|
926
|
+
|
927
|
+
ttk.Label(advanced_frame, text="Show Screenshot Button:").grid(row=self.current_row, column=0, sticky='W')
|
928
|
+
self.show_screenshot_button = tk.BooleanVar(value=self.settings.advanced.show_screenshot_buttons)
|
929
|
+
ttk.Checkbutton(advanced_frame, variable=self.show_screenshot_button).grid(row=self.current_row, column=1, sticky='W')
|
930
|
+
self.add_label_and_increment_row(advanced_frame, "Show the screenshot button in the utility gui.", row=self.current_row, column=2)
|
931
|
+
|
932
|
+
ttk.Label(advanced_frame, text="Multi-line Line-Break:").grid(row=self.current_row, column=0, sticky='W')
|
933
|
+
self.multi_line_line_break = ttk.Entry(advanced_frame)
|
934
|
+
self.multi_line_line_break.insert(0, self.settings.advanced.multi_line_line_break)
|
935
|
+
self.multi_line_line_break.grid(row=self.current_row, column=1)
|
936
|
+
self.add_label_and_increment_row(advanced_frame, "Line break for multi-line mining. This goes between each sentence", row=self.current_row, column=2)
|
937
|
+
|
938
|
+
ttk.Label(advanced_frame, text="Multi-Line Sentence Storage Field:").grid(row=self.current_row, column=0, sticky='W')
|
939
|
+
self.multi_line_sentence_storage_field = ttk.Entry(advanced_frame)
|
940
|
+
self.multi_line_sentence_storage_field.insert(0, self.settings.advanced.multi_line_sentence_storage_field)
|
941
|
+
self.multi_line_sentence_storage_field.grid(row=self.current_row, column=1)
|
942
|
+
self.add_label_and_increment_row(advanced_frame, "Field in Anki for storing the multi-line sentence temporarily.", row=self.current_row, column=2)
|
883
943
|
|
884
944
|
def on_profile_change(self, event):
|
885
945
|
print("profile Changed!")
|
@@ -139,6 +139,7 @@ class Hotkeys:
|
|
139
139
|
reset_line: str = 'f5'
|
140
140
|
take_screenshot: str = 'f6'
|
141
141
|
open_utility: str = 'ctrl+m'
|
142
|
+
play_latest_audio: str = 'f7'
|
142
143
|
|
143
144
|
|
144
145
|
@dataclass_json
|
@@ -153,6 +154,26 @@ class VAD:
|
|
153
154
|
beginning_offset: float = -0.25
|
154
155
|
add_audio_on_no_results: bool = False
|
155
156
|
|
157
|
+
def is_silero(self):
|
158
|
+
return self.selected_vad_model == SILERO or self.backup_vad_model == SILERO
|
159
|
+
|
160
|
+
def is_whisper(self):
|
161
|
+
return self.selected_vad_model == WHISPER or self.backup_vad_model == WHISPER
|
162
|
+
|
163
|
+
def is_vosk(self):
|
164
|
+
return self.selected_vad_model == VOSK or self.backup_vad_model == VOSK
|
165
|
+
|
166
|
+
|
167
|
+
@dataclass_json
|
168
|
+
@dataclass
|
169
|
+
class Advanced:
|
170
|
+
audio_player_path: str = ''
|
171
|
+
video_player_path: str = ''
|
172
|
+
show_screenshot_buttons: bool = False
|
173
|
+
multi_line_line_break: str = '<br>'
|
174
|
+
multi_line_sentence_storage_field: str = ''
|
175
|
+
|
176
|
+
|
156
177
|
|
157
178
|
@dataclass_json
|
158
179
|
@dataclass
|
@@ -167,6 +188,7 @@ class ProfileConfig:
|
|
167
188
|
obs: OBS = field(default_factory=OBS)
|
168
189
|
hotkeys: Hotkeys = field(default_factory=Hotkeys)
|
169
190
|
vad: VAD = field(default_factory=VAD)
|
191
|
+
advanced: Advanced = field(default_factory=Advanced)
|
170
192
|
|
171
193
|
|
172
194
|
# This is just for legacy support
|
@@ -421,7 +443,7 @@ console_handler = logging.StreamHandler(sys.stdout)
|
|
421
443
|
console_handler.setLevel(logging.INFO)
|
422
444
|
|
423
445
|
# Create rotating file handler with level DEBUG
|
424
|
-
file_handler = RotatingFileHandler(get_log_path(), maxBytes=
|
446
|
+
file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
|
425
447
|
file_handler.setLevel(logging.DEBUG)
|
426
448
|
|
427
449
|
# Create a formatter
|
GameSentenceMiner/ffmpeg.py
CHANGED
@@ -48,6 +48,10 @@ def get_screenshot(video_file, time_from_end):
|
|
48
48
|
|
49
49
|
return output_image
|
50
50
|
|
51
|
+
def get_screenshot_for_line(video_file, game_line):
|
52
|
+
return get_screenshot(video_file, get_screenshot_time(video_file, game_line))
|
53
|
+
|
54
|
+
|
51
55
|
|
52
56
|
def get_screenshot_time(video_path, game_line, default_beginning=False):
|
53
57
|
if game_line:
|
@@ -183,19 +187,7 @@ def get_video_duration(file_path):
|
|
183
187
|
def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line):
|
184
188
|
trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
|
185
189
|
suffix=f".{get_config().audio.extension}").name
|
186
|
-
|
187
|
-
file_length = get_video_duration(video_path)
|
188
|
-
time_delta = file_mod_time - game_line.time
|
189
|
-
# Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
|
190
|
-
total_seconds = file_length - time_delta.total_seconds()
|
191
|
-
total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
|
192
|
-
if total_seconds < 0 or total_seconds >= file_length:
|
193
|
-
logger.info(f"0 seconds trimmed off of beginning")
|
194
|
-
return untrimmed_audio
|
195
|
-
|
196
|
-
hours, remainder = divmod(total_seconds_after_offset, 3600)
|
197
|
-
minutes, seconds = divmod(remainder, 60)
|
198
|
-
start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
190
|
+
start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line)
|
199
191
|
|
200
192
|
ffmpeg_command = ffmpeg_base_command_list + [
|
201
193
|
"-i", untrimmed_audio,
|
@@ -222,6 +214,22 @@ def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_l
|
|
222
214
|
logger.info(f"Audio trimmed and saved to {trimmed_audio}")
|
223
215
|
return trimmed_audio
|
224
216
|
|
217
|
+
def get_video_timings(video_path, game_line):
|
218
|
+
file_mod_time = get_file_modification_time(video_path)
|
219
|
+
file_length = get_video_duration(video_path)
|
220
|
+
time_delta = file_mod_time - game_line.time
|
221
|
+
# Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
|
222
|
+
total_seconds = file_length - time_delta.total_seconds()
|
223
|
+
total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
|
224
|
+
if total_seconds < 0 or total_seconds >= file_length:
|
225
|
+
logger.info(f"0 seconds trimmed off of beginning")
|
226
|
+
return 0, 0, 0
|
227
|
+
|
228
|
+
hours, remainder = divmod(total_seconds_after_offset, 3600)
|
229
|
+
minutes, seconds = divmod(remainder, 60)
|
230
|
+
start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
231
|
+
return start_trim_time, total_seconds, total_seconds_after_offset
|
232
|
+
|
225
233
|
|
226
234
|
def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
|
227
235
|
logger.info(f"Re-encode running with settings: {user_ffmpeg_options}")
|
@@ -251,7 +259,7 @@ def create_temp_file_with_same_name(input_file: str):
|
|
251
259
|
def replace_file_with_retry(temp_file, input_file, retries=5, delay=1):
|
252
260
|
for attempt in range(retries):
|
253
261
|
try:
|
254
|
-
|
262
|
+
shutil.move(temp_file, input_file)
|
255
263
|
logger.info(f'Re-encode Finished!')
|
256
264
|
return
|
257
265
|
except OSError as e:
|
GameSentenceMiner/gsm.py
CHANGED
@@ -18,19 +18,20 @@ from GameSentenceMiner import gametext
|
|
18
18
|
from GameSentenceMiner import notification
|
19
19
|
from GameSentenceMiner import obs
|
20
20
|
from GameSentenceMiner import util
|
21
|
+
from GameSentenceMiner.communication import Message
|
22
|
+
from GameSentenceMiner.communication.send import send_restart_signal
|
23
|
+
from GameSentenceMiner.communication.websocket import connect_websocket, register_websocket_message_handler
|
21
24
|
from GameSentenceMiner.configuration import *
|
22
25
|
from GameSentenceMiner.downloader.download_tools import download_obs_if_needed, download_ffmpeg_if_needed
|
23
|
-
from GameSentenceMiner.
|
24
|
-
from GameSentenceMiner.
|
25
|
-
from GameSentenceMiner.gametext import get_text_event, get_mined_line
|
26
|
+
from GameSentenceMiner.ffmpeg import get_audio_and_trim, get_video_timings
|
27
|
+
from GameSentenceMiner.gametext import get_text_event, get_mined_line, GameLine
|
26
28
|
from GameSentenceMiner.util import *
|
27
29
|
from GameSentenceMiner.utility_gui import init_utility_window, get_utility_window
|
28
|
-
from GameSentenceMiner.vad import
|
30
|
+
from GameSentenceMiner.vad import silero_trim, whisper_helper, vosk_helper
|
29
31
|
|
30
32
|
if is_windows():
|
31
33
|
import win32api
|
32
34
|
|
33
|
-
obs_process = None
|
34
35
|
procs_to_close = []
|
35
36
|
settings_window: config_gui.ConfigApp = None
|
36
37
|
obs_paused = False
|
@@ -69,6 +70,23 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
69
70
|
|
70
71
|
@staticmethod
|
71
72
|
def convert_to_audio(video_path):
|
73
|
+
if get_utility_window().line_for_audio:
|
74
|
+
line: GameLine = get_utility_window().line_for_audio
|
75
|
+
get_utility_window().line_for_audio = None
|
76
|
+
if get_config().advanced.audio_player_path:
|
77
|
+
audio = VideoToAudioHandler.get_audio(line, line.next.time if line.next else None, video_path, temporary=True)
|
78
|
+
play_audio_in_external(audio)
|
79
|
+
os.remove(video_path)
|
80
|
+
elif get_config().advanced.video_player_path:
|
81
|
+
play_video_in_external(line, video_path)
|
82
|
+
return
|
83
|
+
if get_utility_window().line_for_screenshot:
|
84
|
+
line: GameLine = get_utility_window().line_for_screenshot
|
85
|
+
get_utility_window().line_for_screenshot = None
|
86
|
+
screenshot = ffmpeg.get_screenshot_for_line(video_path, line)
|
87
|
+
os.startfile(screenshot)
|
88
|
+
os.remove(video_path)
|
89
|
+
return
|
72
90
|
try:
|
73
91
|
last_note = None
|
74
92
|
if anki.card_queue and len(anki.card_queue) > 0:
|
@@ -112,8 +130,8 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
112
130
|
logger.debug(last_note.to_json())
|
113
131
|
|
114
132
|
note = anki.get_initial_card_info(last_note, get_utility_window().get_selected_lines())
|
115
|
-
|
116
133
|
tango = last_note.get_field(get_config().anki.word_field) if last_note else ''
|
134
|
+
get_utility_window().reset_checkboxes()
|
117
135
|
|
118
136
|
if get_config().anki.sentence_audio_field:
|
119
137
|
logger.debug("Attempting to get audio from video")
|
@@ -142,11 +160,13 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
142
160
|
os.remove(video_path) # Optionally remove the video after conversion
|
143
161
|
if get_config().paths.remove_audio and os.path.exists(vad_trimmed_audio):
|
144
162
|
os.remove(vad_trimmed_audio) # Optionally remove the screenshot after conversion
|
145
|
-
|
163
|
+
|
146
164
|
|
147
165
|
@staticmethod
|
148
|
-
def get_audio(game_line, next_line_time, video_path):
|
166
|
+
def get_audio(game_line, next_line_time, video_path, temporary=False):
|
149
167
|
trimmed_audio = get_audio_and_trim(video_path, game_line, next_line_time)
|
168
|
+
if temporary:
|
169
|
+
return trimmed_audio
|
150
170
|
vad_trimmed_audio = make_unique_file_name(
|
151
171
|
f"{os.path.abspath(configuration.get_temporary_directory())}/{obs.get_current_game(sanitize=True)}.{get_config().audio.extension}")
|
152
172
|
final_audio_output = make_unique_file_name(os.path.join(get_config().paths.audio_destination,
|
@@ -181,31 +201,54 @@ class VideoToAudioHandler(FileSystemEventHandler):
|
|
181
201
|
ffmpeg.reencode_file_with_user_config(vad_trimmed_audio, final_audio_output,
|
182
202
|
get_config().audio.ffmpeg_reencode_options)
|
183
203
|
elif os.path.exists(vad_trimmed_audio):
|
184
|
-
|
204
|
+
shutil.move(vad_trimmed_audio, final_audio_output)
|
185
205
|
return final_audio_output, should_update_audio, vad_trimmed_audio
|
186
206
|
|
207
|
+
def play_audio_in_external(filepath):
|
208
|
+
exe = get_config().advanced.audio_player_path
|
187
209
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
os.
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
210
|
+
filepath = os.path.normpath(filepath)
|
211
|
+
|
212
|
+
command = [exe, filepath]
|
213
|
+
|
214
|
+
try:
|
215
|
+
subprocess.Popen(command)
|
216
|
+
print(f"Opened {filepath} in {exe}.")
|
217
|
+
except Exception as e:
|
218
|
+
print(f"An error occurred: {e}")
|
219
|
+
|
220
|
+
def play_video_in_external(line, filepath):
|
221
|
+
def remove_video_when_closed(p, fp):
|
222
|
+
p.wait()
|
223
|
+
os.remove(fp)
|
224
|
+
|
225
|
+
command = [get_config().advanced.video_player_path, os.path.normpath(filepath)]
|
226
|
+
|
227
|
+
start, _, _ = get_video_timings(filepath, line)
|
228
|
+
|
229
|
+
print(start)
|
230
|
+
|
231
|
+
if start:
|
232
|
+
command.extend(["--start-time", convert_to_vlc_seconds(start)])
|
233
|
+
|
234
|
+
try:
|
235
|
+
proc = subprocess.Popen(command)
|
236
|
+
print(f"Opened {filepath} in VLC.")
|
237
|
+
threading.Thread(target=remove_video_when_closed, args=(proc, filepath)).start()
|
238
|
+
except FileNotFoundError:
|
239
|
+
print("VLC not found. Make sure it's installed and in your PATH.")
|
240
|
+
except Exception as e:
|
241
|
+
print(f"An error occurred: {e}")
|
208
242
|
|
243
|
+
def convert_to_vlc_seconds(time_str):
|
244
|
+
"""Converts HH:MM:SS.milliseconds to VLC-compatible seconds."""
|
245
|
+
try:
|
246
|
+
hours, minutes, seconds_ms = time_str.split(":")
|
247
|
+
seconds, milliseconds = seconds_ms.split(".")
|
248
|
+
total_seconds = (int(hours) * 3600) + (int(minutes) * 60) + int(seconds) + (int(milliseconds) / 1000.0)
|
249
|
+
return str(total_seconds)
|
250
|
+
except ValueError:
|
251
|
+
return "Invalid time format"
|
209
252
|
|
210
253
|
def initial_checks():
|
211
254
|
try:
|
@@ -220,6 +263,7 @@ def register_hotkeys():
|
|
220
263
|
keyboard.add_hotkey(get_config().hotkeys.reset_line, gametext.reset_line_hotkey_pressed)
|
221
264
|
keyboard.add_hotkey(get_config().hotkeys.take_screenshot, get_screenshot)
|
222
265
|
keyboard.add_hotkey(get_config().hotkeys.open_utility, open_multimine)
|
266
|
+
keyboard.add_hotkey(get_config().hotkeys.play_latest_audio, play_most_recent_audio)
|
223
267
|
|
224
268
|
|
225
269
|
def get_screenshot():
|
@@ -274,6 +318,13 @@ def open_multimine():
|
|
274
318
|
obs.update_current_game()
|
275
319
|
get_utility_window().show()
|
276
320
|
|
321
|
+
def play_most_recent_audio():
|
322
|
+
if get_config().advanced.audio_player_path or get_config().advanced.video_player_path and len(gametext.line_history) > 0:
|
323
|
+
get_utility_window().line_for_audio = gametext.line_history.values[-1]
|
324
|
+
obs.save_replay_buffer()
|
325
|
+
else:
|
326
|
+
logger.error("Feature Disabled. No audio or video player path set in config!")
|
327
|
+
|
277
328
|
|
278
329
|
def open_log():
|
279
330
|
"""Function to handle opening log."""
|
@@ -343,7 +394,7 @@ def switch_profile(icon, item):
|
|
343
394
|
settings_window.reload_settings()
|
344
395
|
update_icon()
|
345
396
|
if get_config().restart_required(prev_config):
|
346
|
-
|
397
|
+
send_restart_signal()
|
347
398
|
|
348
399
|
|
349
400
|
def run_tray():
|
@@ -388,10 +439,10 @@ def run_tray():
|
|
388
439
|
# proc.wait()
|
389
440
|
|
390
441
|
def close_obs():
|
391
|
-
if obs_process:
|
442
|
+
if obs.obs_process:
|
392
443
|
try:
|
393
|
-
subprocess.run(["taskkill", "/PID", str(obs_process), "/F"], check=True, capture_output=True, text=True)
|
394
|
-
print(f"OBS (PID {obs_process}) has been terminated.")
|
444
|
+
subprocess.run(["taskkill", "/PID", str(obs.obs_process.pid), "/F"], check=True, capture_output=True, text=True)
|
445
|
+
print(f"OBS (PID {obs.obs_process.pid}) has been terminated.")
|
395
446
|
except subprocess.CalledProcessError as e:
|
396
447
|
print(f"Error terminating OBS: {e.stderr}")
|
397
448
|
else:
|
@@ -399,12 +450,11 @@ def close_obs():
|
|
399
450
|
|
400
451
|
|
401
452
|
def restart_obs():
|
402
|
-
|
403
|
-
if obs_process:
|
453
|
+
if obs.obs_process:
|
404
454
|
close_obs()
|
405
455
|
time.sleep(2)
|
406
|
-
|
407
|
-
obs.connect_to_obs(
|
456
|
+
obs.start_obs()
|
457
|
+
obs.connect_to_obs()
|
408
458
|
|
409
459
|
|
410
460
|
def cleanup():
|
@@ -438,20 +488,6 @@ def cleanup():
|
|
438
488
|
logger.info("Cleanup complete.")
|
439
489
|
|
440
490
|
|
441
|
-
def check_for_stdin():
|
442
|
-
while True:
|
443
|
-
for line in sys.stdin:
|
444
|
-
logger.info(f"Got stdin: {line}")
|
445
|
-
if "exit" in line:
|
446
|
-
cleanup()
|
447
|
-
sys.exit(0)
|
448
|
-
elif "restart_obs" in line:
|
449
|
-
restart_obs()
|
450
|
-
elif "update" in line:
|
451
|
-
update_icon()
|
452
|
-
sys.stdin.flush()
|
453
|
-
|
454
|
-
|
455
491
|
def handle_exit():
|
456
492
|
"""Signal handler for graceful termination."""
|
457
493
|
|
@@ -462,23 +498,74 @@ def handle_exit():
|
|
462
498
|
|
463
499
|
return _handle_exit
|
464
500
|
|
501
|
+
def initialize(reloading=False):
|
502
|
+
global obs_process
|
503
|
+
if not reloading:
|
504
|
+
if is_windows():
|
505
|
+
download_obs_if_needed()
|
506
|
+
download_ffmpeg_if_needed()
|
507
|
+
# if get_config().obs.enabled:
|
508
|
+
# if get_config().obs.open_obs:
|
509
|
+
# obs_process = obs.start_obs()
|
510
|
+
# obs.connect_to_obs(start_replay=True)
|
511
|
+
# anki.start_monitoring_anki()
|
512
|
+
# gametext.start_text_monitor()
|
513
|
+
os.makedirs(get_config().paths.folder_to_watch, exist_ok=True)
|
514
|
+
os.makedirs(get_config().paths.screenshot_destination, exist_ok=True)
|
515
|
+
os.makedirs(get_config().paths.audio_destination, exist_ok=True)
|
516
|
+
initial_checks()
|
517
|
+
register_websocket_message_handler(handle_websocket_message)
|
518
|
+
# if get_config().vad.do_vad_postprocessing:
|
519
|
+
# if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
520
|
+
# vosk_helper.get_vosk_model()
|
521
|
+
# if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
522
|
+
# whisper_helper.initialize_whisper_model()
|
523
|
+
|
524
|
+
def initialize_async():
|
525
|
+
tasks = [gametext.start_text_monitor, connect_websocket, run_tray]
|
526
|
+
threads = []
|
527
|
+
if get_config().obs.enabled:
|
528
|
+
if get_config().obs.open_obs:
|
529
|
+
tasks.append(obs.start_obs)
|
530
|
+
tasks.append(obs.connect_to_obs)
|
531
|
+
tasks.append(anki.start_monitoring_anki)
|
532
|
+
if get_config().vad.do_vad_postprocessing:
|
533
|
+
if VOSK in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
534
|
+
tasks.append(vosk_helper.get_vosk_model)
|
535
|
+
if WHISPER in (get_config().vad.backup_vad_model, get_config().vad.selected_vad_model):
|
536
|
+
tasks.append(whisper_helper.initialize_whisper_model)
|
537
|
+
for task in tasks:
|
538
|
+
threads.append(util.run_new_thread(task))
|
539
|
+
return threads
|
540
|
+
|
541
|
+
def check_async_init_done(threads):
|
542
|
+
while True:
|
543
|
+
if all(not thread.is_alive() for thread in threads):
|
544
|
+
break
|
545
|
+
time.sleep(0.5)
|
546
|
+
logger.info("Script Fully Initialized. Happy Mining!")
|
547
|
+
|
548
|
+
|
549
|
+
def handle_websocket_message(message: Message):
|
550
|
+
match message.function:
|
551
|
+
case "quit":
|
552
|
+
cleanup()
|
553
|
+
sys.exit(0)
|
554
|
+
case _:
|
555
|
+
logger.debug(f"unknown message from electron websocket: {message.to_json()}")
|
465
556
|
|
466
|
-
|
557
|
+
|
558
|
+
def main(reloading=False):
|
467
559
|
global root, settings_window
|
468
560
|
logger.info("Script started.")
|
469
|
-
util.run_new_thread(check_for_stdin)
|
470
561
|
root = ttk.Window(themename='darkly')
|
471
562
|
settings_window = config_gui.ConfigApp(root)
|
472
563
|
init_utility_window(root)
|
473
564
|
initialize(reloading)
|
474
|
-
|
475
|
-
initial_checks()
|
476
|
-
event_handler = VideoToAudioHandler()
|
565
|
+
initialize_async()
|
477
566
|
observer = Observer()
|
478
|
-
observer.schedule(
|
567
|
+
observer.schedule(VideoToAudioHandler(), get_config().paths.folder_to_watch, recursive=False)
|
479
568
|
observer.start()
|
480
|
-
|
481
|
-
logger.info("Script Initialized. Happy Mining!")
|
482
569
|
if not is_linux():
|
483
570
|
register_hotkeys()
|
484
571
|
|
@@ -40,7 +40,7 @@ def open_anki_card(note_id):
|
|
40
40
|
|
41
41
|
def send_notification(title, message, timeout):
|
42
42
|
if windows:
|
43
|
-
notifier.show_toast(title, message, duration=timeout)
|
43
|
+
notifier.show_toast(title, message, duration=timeout, threaded=True)
|
44
44
|
else:
|
45
45
|
notification.notify(
|
46
46
|
title=title,
|
GameSentenceMiner/obs.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
import subprocess
|
2
3
|
import time
|
3
4
|
|
@@ -9,6 +10,8 @@ from GameSentenceMiner.configuration import *
|
|
9
10
|
from GameSentenceMiner.model import *
|
10
11
|
|
11
12
|
client: obsws = None
|
13
|
+
obs_process = None
|
14
|
+
logging.getLogger('obswebsocket').setLevel(logging.CRITICAL)
|
12
15
|
|
13
16
|
# REFERENCE: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
|
14
17
|
|
@@ -17,6 +20,7 @@ def get_obs_path():
|
|
17
20
|
return os.path.join(configuration.get_app_directory(), 'obs-studio/bin/64bit/obs64.exe')
|
18
21
|
|
19
22
|
def start_obs():
|
23
|
+
global obs_process
|
20
24
|
obs_path = get_obs_path()
|
21
25
|
if not os.path.exists(obs_path):
|
22
26
|
logger.error(f"OBS not found at {obs_path}. Please install OBS.")
|
@@ -26,9 +30,9 @@ def start_obs():
|
|
26
30
|
obs_pid = is_obs_running(obs_path)
|
27
31
|
if obs_pid:
|
28
32
|
return obs_pid
|
29
|
-
|
33
|
+
obs_process = subprocess.Popen([obs_path, '--disable-shutdown-check', '--portable'], cwd=os.path.dirname(obs_path))
|
30
34
|
logger.info("OBS launched")
|
31
|
-
return
|
35
|
+
return obs_process.pid
|
32
36
|
except Exception as e:
|
33
37
|
logger.error(f"Error launching OBS: {e}")
|
34
38
|
return None
|
@@ -87,7 +91,7 @@ def on_disconnect(obs):
|
|
87
91
|
logger.error("OBS Connection Lost!")
|
88
92
|
|
89
93
|
|
90
|
-
def connect_to_obs(
|
94
|
+
def connect_to_obs():
|
91
95
|
global client
|
92
96
|
if get_config().obs.enabled:
|
93
97
|
if util.is_windows():
|
@@ -97,8 +101,7 @@ def connect_to_obs(start_replay=False):
|
|
97
101
|
on_disconnect=on_disconnect)
|
98
102
|
client.connect()
|
99
103
|
|
100
|
-
|
101
|
-
if start_replay and get_config().obs.start_buffer:
|
104
|
+
if get_config().obs.start_buffer:
|
102
105
|
start_replay_buffer()
|
103
106
|
update_current_game()
|
104
107
|
|
@@ -111,22 +114,45 @@ def disconnect_from_obs():
|
|
111
114
|
client = None
|
112
115
|
logger.info("Disconnected from OBS WebSocket.")
|
113
116
|
|
117
|
+
def do_obs_call(request, from_dict = None, retry=5):
|
118
|
+
try:
|
119
|
+
response = client.call(request)
|
120
|
+
if not response.status and retry > 0:
|
121
|
+
time.sleep(1)
|
122
|
+
return do_obs_call(request, from_dict, retry - 1)
|
123
|
+
if from_dict:
|
124
|
+
return from_dict(response.datain)
|
125
|
+
return None
|
126
|
+
except Exception as e:
|
127
|
+
if "socket is already closed" in str(e) or "object has no attribute" in str(e):
|
128
|
+
if retry > 0:
|
129
|
+
time.sleep(1)
|
130
|
+
return do_obs_call(request, from_dict, retry - 1)
|
131
|
+
else:
|
132
|
+
logger.error(f"Error doing obs call: {e}")
|
133
|
+
raise e
|
134
|
+
return None
|
114
135
|
|
115
136
|
def toggle_replay_buffer():
|
116
137
|
try:
|
117
|
-
|
138
|
+
do_obs_call(requests.ToggleReplayBuffer())
|
118
139
|
logger.info("Replay buffer Toggled.")
|
119
140
|
except Exception as e:
|
120
141
|
logger.error(f"Error toggling buffer: {e}")
|
121
142
|
|
122
143
|
|
123
144
|
# Start replay buffer
|
124
|
-
def start_replay_buffer():
|
145
|
+
def start_replay_buffer(retry=5):
|
125
146
|
try:
|
126
147
|
client.call(requests.GetVersion())
|
127
148
|
client.call(requests.StartReplayBuffer())
|
128
149
|
except Exception as e:
|
129
|
-
|
150
|
+
if "socket is already closed" in str(e):
|
151
|
+
if retry > 0:
|
152
|
+
time.sleep(1)
|
153
|
+
start_replay_buffer(retry - 1)
|
154
|
+
else:
|
155
|
+
logger.error(f"Error starting replay buffer: {e}")
|
130
156
|
|
131
157
|
|
132
158
|
# Stop replay buffer
|
@@ -153,9 +179,7 @@ def save_replay_buffer():
|
|
153
179
|
|
154
180
|
def get_current_scene():
|
155
181
|
try:
|
156
|
-
|
157
|
-
scene_info = SceneInfo.from_dict(response.datain)
|
158
|
-
return scene_info.sceneName
|
182
|
+
return do_obs_call(requests.GetCurrentProgramScene(), SceneInfo.from_dict).sceneName
|
159
183
|
except Exception as e:
|
160
184
|
logger.error(f"Couldn't get scene: {e}")
|
161
185
|
return ''
|
@@ -163,9 +187,7 @@ def get_current_scene():
|
|
163
187
|
|
164
188
|
def get_source_from_scene(scene_name):
|
165
189
|
try:
|
166
|
-
|
167
|
-
scene_list = SceneItemsResponse.from_dict(response.datain)
|
168
|
-
return scene_list.sceneItems[0]
|
190
|
+
return do_obs_call(requests.GetSceneItemList(sceneName=scene_name), SceneItemsResponse.from_dict).sceneItems[0]
|
169
191
|
except Exception as e:
|
170
192
|
logger.error(f"Error getting source from scene: {e}")
|
171
193
|
return ''
|
GameSentenceMiner/util.py
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
import importlib
|
1
2
|
import os
|
2
3
|
import random
|
3
4
|
import re
|
4
5
|
import string
|
5
6
|
import subprocess
|
7
|
+
import sys
|
6
8
|
import threading
|
7
9
|
from datetime import datetime
|
8
10
|
from sys import platform
|
9
11
|
|
10
12
|
from rapidfuzz import process
|
11
13
|
|
12
|
-
from GameSentenceMiner.configuration import logger
|
14
|
+
from GameSentenceMiner.configuration import logger, get_config
|
13
15
|
|
14
16
|
SCRIPTS_DIR = r"E:\Japanese Stuff\agent-v0.1.4-win32-x64\data\scripts"
|
15
17
|
|
@@ -156,25 +158,58 @@ def combine_dialogue(dialogue_lines, new_lines=None):
|
|
156
158
|
new_lines = []
|
157
159
|
|
158
160
|
if len(dialogue_lines) == 1 and '「' not in dialogue_lines[0]:
|
159
|
-
new_lines.append(dialogue_lines[0]
|
161
|
+
new_lines.append(dialogue_lines[0])
|
160
162
|
return new_lines
|
161
163
|
|
162
164
|
character_name = dialogue_lines[0].split("「")[0]
|
163
165
|
text = character_name + "「"
|
164
|
-
next_character = ''
|
165
166
|
|
166
167
|
for i, line in enumerate(dialogue_lines):
|
167
168
|
if not line.startswith(character_name + "「"):
|
168
|
-
text = text + "
|
169
|
-
next_character = line.split("「")[0]
|
169
|
+
text = text + "」" + get_config().advanced.multi_line_line_break
|
170
170
|
new_lines.append(text)
|
171
171
|
new_lines.extend(combine_dialogue(dialogue_lines[i:]))
|
172
172
|
break
|
173
173
|
else:
|
174
|
-
text += (
|
174
|
+
text += (get_config().advanced.multi_line_line_break if i > 0 else "") + line.split("「")[1].rstrip("」") + ""
|
175
175
|
else:
|
176
176
|
text = text + "」"
|
177
177
|
new_lines.append(text)
|
178
178
|
|
179
179
|
return new_lines
|
180
180
|
|
181
|
+
# def import_vad_models():
|
182
|
+
# silero_trim, whisper_helper, vosk_helper = None, None, None
|
183
|
+
#
|
184
|
+
# def check_and_install(package_name):
|
185
|
+
# try:
|
186
|
+
# importlib.import_module(package_name)
|
187
|
+
# return True
|
188
|
+
# except ImportError:
|
189
|
+
# logger.warning(f"{package_name} is not installed. Attempting to install...")
|
190
|
+
# try:
|
191
|
+
# python_executable = sys.executable
|
192
|
+
# subprocess.check_call([python_executable, "-m", "pip", "install", package_name])
|
193
|
+
# logger.info(f"{package_name} installed successfully.")
|
194
|
+
# return True
|
195
|
+
# except subprocess.CalledProcessError as e:
|
196
|
+
# logger.error(f"Failed to install {package_name}: {e}")
|
197
|
+
# return False
|
198
|
+
#
|
199
|
+
# if get_config().vad.is_silero():
|
200
|
+
# if check_and_install("silero_vad"):
|
201
|
+
# from GameSentenceMiner.vad import silero_trim
|
202
|
+
# else:
|
203
|
+
# logger.error("Silero VAD is enabled and silero_vad package could not be installed.")
|
204
|
+
# if get_config().vad.is_whisper():
|
205
|
+
# if check_and_install("stable-ts"):
|
206
|
+
# from GameSentenceMiner.vad import whisper_helper
|
207
|
+
# else:
|
208
|
+
# logger.error("Whisper is enabled and whisper package could not be installed.")
|
209
|
+
# if get_config().vad.is_vosk():
|
210
|
+
# if check_and_install("vosk"):
|
211
|
+
# from GameSentenceMiner.vad import vosk_helper
|
212
|
+
# else:
|
213
|
+
# logger.error("Vosk is enabled and vosk package could not be installed.")
|
214
|
+
#
|
215
|
+
# return silero_trim, whisper_helper, vosk_helper
|
GameSentenceMiner/utility_gui.py
CHANGED
@@ -3,16 +3,22 @@ import os
|
|
3
3
|
import tkinter as tk
|
4
4
|
from tkinter import ttk
|
5
5
|
|
6
|
-
from GameSentenceMiner
|
6
|
+
from GameSentenceMiner import obs
|
7
|
+
from GameSentenceMiner.configuration import logger, get_app_directory, get_config
|
7
8
|
|
8
9
|
|
9
10
|
class UtilityApp:
|
10
11
|
def __init__(self, root):
|
11
12
|
self.root = root
|
12
13
|
self.items = []
|
14
|
+
self.play_audio_buttons = []
|
15
|
+
self.get_screenshot_buttons = []
|
13
16
|
self.checkboxes = []
|
14
17
|
self.multi_mine_window = None # Store the multi-mine window reference
|
15
18
|
self.checkbox_frame = None
|
19
|
+
self.line_for_audio = None
|
20
|
+
self.line_for_screenshot = None
|
21
|
+
self.line_counter = 0
|
16
22
|
|
17
23
|
style = ttk.Style()
|
18
24
|
style.configure("TCheckbutton", font=("Arial", 20)) # Change the font and size
|
@@ -80,17 +86,49 @@ class UtilityApp:
|
|
80
86
|
self.checkboxes[0].destroy()
|
81
87
|
self.checkboxes.pop(0)
|
82
88
|
self.items.pop(0)
|
89
|
+
if self.play_audio_buttons:
|
90
|
+
self.play_audio_buttons[0].destroy()
|
91
|
+
self.play_audio_buttons.pop(0)
|
92
|
+
if self.get_screenshot_buttons:
|
93
|
+
self.get_screenshot_buttons[0].destroy()
|
94
|
+
self.get_screenshot_buttons.pop(0)
|
83
95
|
|
84
96
|
if self.multi_mine_window and tk.Toplevel.winfo_exists(self.multi_mine_window):
|
85
97
|
self.add_checkbox_to_gui(line, var)
|
98
|
+
self.line_for_audio = None
|
99
|
+
self.line_for_screenshot = None
|
86
100
|
|
87
101
|
def add_checkbox_to_gui(self, line, var):
|
88
102
|
""" Add a single checkbox without repainting everything. """
|
89
103
|
if self.checkbox_frame:
|
104
|
+
column = 0
|
105
|
+
if get_config().advanced.show_screenshot_buttons:
|
106
|
+
get_screenshot_button = ttk.Button(self.checkbox_frame, text="📸", command=lambda: self.take_screenshot(line))
|
107
|
+
get_screenshot_button.grid(row=self.line_counter, column=column, sticky='w', padx=5)
|
108
|
+
self.get_screenshot_buttons.append(get_screenshot_button)
|
109
|
+
column += 1
|
110
|
+
|
111
|
+
if get_config().advanced.video_player_path or get_config().advanced.audio_player_path:
|
112
|
+
play_audio_button = ttk.Button(self.checkbox_frame, text="🔊", command=lambda: self.play_audio(line))
|
113
|
+
play_audio_button.grid(row=self.line_counter, column=column, sticky='w', padx=5)
|
114
|
+
self.play_audio_buttons.append(play_audio_button)
|
115
|
+
column += 1
|
116
|
+
|
90
117
|
chk = ttk.Checkbutton(self.checkbox_frame, text=f"{line.time.strftime('%H:%M:%S')} - {line.text}", variable=var)
|
91
|
-
chk.
|
118
|
+
chk.grid(row=self.line_counter, column=column, sticky='w', padx=5)
|
92
119
|
self.checkboxes.append(chk)
|
93
120
|
|
121
|
+
self.line_counter += 1
|
122
|
+
|
123
|
+
|
124
|
+
def play_audio(self, line):
|
125
|
+
self.line_for_audio = line
|
126
|
+
obs.save_replay_buffer()
|
127
|
+
|
128
|
+
def take_screenshot(self, line):
|
129
|
+
self.line_for_screenshot = line
|
130
|
+
obs.save_replay_buffer()
|
131
|
+
|
94
132
|
|
95
133
|
# def update_multi_mine_window(self):
|
96
134
|
# for widget in self.multi_mine_window.winfo_children():
|
@@ -0,0 +1,29 @@
|
|
1
|
+
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
GameSentenceMiner/anki.py,sha256=YcdNDpUskjdxi8upZ5SLGTyRbO8NlOPcqsV_8akTwuM,12877
|
3
|
+
GameSentenceMiner/config_gui.py,sha256=cLKVliB0X61WNduNisOmaEtqSr1mvTO6ZAiv-t2jW-8,61194
|
4
|
+
GameSentenceMiner/configuration.py,sha256=-b1wW6EkkOFwF1No1uZLD2nPUV5gfBIzG5a8_ykqyUI,16691
|
5
|
+
GameSentenceMiner/ffmpeg.py,sha256=Bvkk0TMHtoQkpEYQls48CbC4TB0-FzrnqQhRNg36hVk,11831
|
6
|
+
GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
|
7
|
+
GameSentenceMiner/gsm.py,sha256=UeGSoFWV1lq3YS0lcDL4zXmvOEEMvSjeS1KKQpnG3co,24169
|
8
|
+
GameSentenceMiner/model.py,sha256=bZm-2vkIw4gQCGLB02eDoTtO1Ymb_dnHk0VDJDFO3y8,5228
|
9
|
+
GameSentenceMiner/notification.py,sha256=2d8_8DUxImeC1zY-V4c_PZw1zbvvGZ6KiAnPaSmH9G0,2592
|
10
|
+
GameSentenceMiner/obs.py,sha256=N7XoPSzLk9rHi4sgsG_LS2dN06MrqwN2mpSHfjONTjE,7368
|
11
|
+
GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
|
12
|
+
GameSentenceMiner/util.py,sha256=tkaoU1bj8iPMTNwUCWUzFLAnT44Ot92D1tYwQMEnARw,7336
|
13
|
+
GameSentenceMiner/utility_gui.py,sha256=aVdI9zVXADS53g7QtTgmkVK1LumBsXF4Lou3qJzgHN8,7487
|
14
|
+
GameSentenceMiner/communication/__init__.py,sha256=_jGn9PJxtOAOPtJ2rI-Qu9hEHVZVpIvWlxKvqk91_zI,638
|
15
|
+
GameSentenceMiner/communication/send.py,sha256=oOJdCS6-LNX90amkRn5FL2xqx6THGm56zHR2ntVIFTE,229
|
16
|
+
GameSentenceMiner/communication/websocket.py,sha256=vrZ9KwRUyZOepjayJkxZZTsNIbHGcDLgDRO9dNDwizM,2914
|
17
|
+
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
18
|
+
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
|
20
|
+
GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
+
GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
|
22
|
+
GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
|
23
|
+
GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
|
24
|
+
gamesentenceminer-2.5.7.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
25
|
+
gamesentenceminer-2.5.7.dist-info/METADATA,sha256=TVU0qDawh6UwnB9CD2y_zKATbvuVSVhTDKGjgPCRBLI,5435
|
26
|
+
gamesentenceminer-2.5.7.dist-info/WHEEL,sha256=DK49LOLCYiurdXXOXwGJm6U4DkHkg4lcxjhqwRa0CP4,91
|
27
|
+
gamesentenceminer-2.5.7.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
28
|
+
gamesentenceminer-2.5.7.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
29
|
+
gamesentenceminer-2.5.7.dist-info/RECORD,,
|
@@ -1,27 +0,0 @@
|
|
1
|
-
GameSentenceMiner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
GameSentenceMiner/anki.py,sha256=zjZ3oE0iTMk1BQFuEhLWk8qgqJMYihO6-jTkKaC3eUk,12229
|
3
|
-
GameSentenceMiner/config_gui.py,sha256=PzzLX-OwK71U5JSFaxup0ec0oWBWaF6AeCXs0m13HK0,56762
|
4
|
-
GameSentenceMiner/configuration.py,sha256=kyvNCkZSZcqXbGjak8lb_GyhhjN8tIfb7eEfusz_M8A,16038
|
5
|
-
GameSentenceMiner/electron_messaging.py,sha256=fBk9Ipo0jg2OZwYaKe1Qsm05P2ftrdTRGgFYob7ZA-k,139
|
6
|
-
GameSentenceMiner/ffmpeg.py,sha256=2dwKbKxw_9sUXud67pPAx_6dGd1j-D99CdqLFVtbhSk,11479
|
7
|
-
GameSentenceMiner/gametext.py,sha256=LORVdE2WEo1CDI8gonc7qxrhbS4KFKXFQVKjhlkpLbc,7368
|
8
|
-
GameSentenceMiner/gsm.py,sha256=MkRaty1ATLLJKZs9rv9Rdlitx3C9fwz66iD2AJS5LQQ,20343
|
9
|
-
GameSentenceMiner/model.py,sha256=bZm-2vkIw4gQCGLB02eDoTtO1Ymb_dnHk0VDJDFO3y8,5228
|
10
|
-
GameSentenceMiner/notification.py,sha256=p24TPw1iF8vDVL56NRgRi3oZ-5oQa9DkqqVjr1gZRJk,2577
|
11
|
-
GameSentenceMiner/obs.py,sha256=8ImXAVUWa4JdzwcBOEFShlZRZzh1dCvdpD1aEGhQfbU,6566
|
12
|
-
GameSentenceMiner/package.py,sha256=YlS6QRMuVlm6mdXx0rlXv9_3erTGS21jaP3PNNWfAH0,1250
|
13
|
-
GameSentenceMiner/util.py,sha256=lAFwAQNeHpVZs_Aeb2K2ShVdcmfrQzEwxUnbf7e6fOc,5699
|
14
|
-
GameSentenceMiner/utility_gui.py,sha256=Ox63XZnj08MezcEG8X5EYJlbHmw-bZitN3CsWgP8lLo,5743
|
15
|
-
GameSentenceMiner/downloader/Untitled_json.py,sha256=RUUl2bbbCpUDUUS0fP0tdvf5FngZ7ILdA_J5TFYAXUQ,15272
|
16
|
-
GameSentenceMiner/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
GameSentenceMiner/downloader/download_tools.py,sha256=mI1u_FGBmBqDIpCH3jOv8DOoZ3obgP5pIf9o9SVfX2Q,8131
|
18
|
-
GameSentenceMiner/vad/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
GameSentenceMiner/vad/silero_trim.py,sha256=-thDIZLuTLra3YBj7WR16Z6JeDgSpge2YuahprBvD8I,1585
|
20
|
-
GameSentenceMiner/vad/vosk_helper.py,sha256=BI_mg_qyrjNbuEJjXSUDoV0FWEtQtEOAPmrrNixnZ_8,5974
|
21
|
-
GameSentenceMiner/vad/whisper_helper.py,sha256=OF4J8TPPoKPJR1uFwrWAZ2Q7v0HJkVvNGmF8l1tACX0,3447
|
22
|
-
gamesentenceminer-2.5.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
23
|
-
gamesentenceminer-2.5.5.dist-info/METADATA,sha256=sHUHiKel1xt8b3oAys3w0In5FNvH0KGxlxsyxEMJWgs,5435
|
24
|
-
gamesentenceminer-2.5.5.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
25
|
-
gamesentenceminer-2.5.5.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
|
26
|
-
gamesentenceminer-2.5.5.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
|
27
|
-
gamesentenceminer-2.5.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|