GameSentenceMiner 2.9.3__py3-none-any.whl → 2.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- GameSentenceMiner/ai/ai_prompting.py +3 -3
- GameSentenceMiner/anki.py +17 -11
- GameSentenceMiner/assets/icon.png +0 -0
- GameSentenceMiner/assets/icon128.png +0 -0
- GameSentenceMiner/assets/icon256.png +0 -0
- GameSentenceMiner/assets/icon32.png +0 -0
- GameSentenceMiner/assets/icon512.png +0 -0
- GameSentenceMiner/assets/icon64.png +0 -0
- GameSentenceMiner/assets/pickaxe.png +0 -0
- GameSentenceMiner/config_gui.py +22 -7
- GameSentenceMiner/gametext.py +5 -5
- GameSentenceMiner/gsm.py +26 -67
- GameSentenceMiner/obs.py +7 -9
- GameSentenceMiner/ocr/owocr_area_selector.py +1 -1
- GameSentenceMiner/ocr/owocr_helper.py +30 -13
- GameSentenceMiner/owocr/owocr/ocr.py +0 -2
- GameSentenceMiner/owocr/owocr/run.py +1 -1
- GameSentenceMiner/{communication → util/communication}/__init__.py +1 -1
- GameSentenceMiner/{communication → util/communication}/send.py +1 -1
- GameSentenceMiner/{communication → util/communication}/websocket.py +2 -2
- GameSentenceMiner/{downloader → util/downloader}/download_tools.py +3 -3
- GameSentenceMiner/vad.py +344 -0
- GameSentenceMiner/web/texthooking_page.py +78 -55
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/METADATA +2 -3
- gamesentenceminer-2.9.5.dist-info/RECORD +57 -0
- GameSentenceMiner/configuration.py +0 -647
- GameSentenceMiner/electron_config.py +0 -315
- GameSentenceMiner/ffmpeg.py +0 -441
- GameSentenceMiner/model.py +0 -177
- GameSentenceMiner/notification.py +0 -105
- GameSentenceMiner/package.py +0 -39
- GameSentenceMiner/ss_selector.py +0 -121
- GameSentenceMiner/text_log.py +0 -186
- GameSentenceMiner/util.py +0 -262
- GameSentenceMiner/vad/groq_trim.py +0 -82
- GameSentenceMiner/vad/result.py +0 -21
- GameSentenceMiner/vad/silero_trim.py +0 -52
- GameSentenceMiner/vad/vad_utils.py +0 -13
- GameSentenceMiner/vad/vosk_helper.py +0 -158
- GameSentenceMiner/vad/whisper_helper.py +0 -105
- gamesentenceminer-2.9.3.dist-info/RECORD +0 -64
- /GameSentenceMiner/{downloader → assets}/__init__.py +0 -0
- /GameSentenceMiner/{downloader → util/downloader}/Untitled_json.py +0 -0
- /GameSentenceMiner/{vad → util/downloader}/__init__.py +0 -0
- /GameSentenceMiner/{downloader → util/downloader}/oneocr_dl.py +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/WHEEL +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/entry_points.txt +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/licenses/LICENSE +0 -0
- {gamesentenceminer-2.9.3.dist-info → gamesentenceminer-2.9.5.dist-info}/top_level.txt +0 -0
GameSentenceMiner/ffmpeg.py
DELETED
@@ -1,441 +0,0 @@
|
|
1
|
-
import shutil
|
2
|
-
import tempfile
|
3
|
-
|
4
|
-
import GameSentenceMiner.configuration
|
5
|
-
from GameSentenceMiner import obs, util, configuration
|
6
|
-
from GameSentenceMiner.configuration import *
|
7
|
-
from GameSentenceMiner.text_log import initial_time
|
8
|
-
from GameSentenceMiner.util import *
|
9
|
-
|
10
|
-
|
11
|
-
def get_ffmpeg_path():
|
12
|
-
return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
|
13
|
-
|
14
|
-
def get_ffprobe_path():
|
15
|
-
return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
|
16
|
-
|
17
|
-
ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
|
18
|
-
|
19
|
-
|
20
|
-
def call_frame_extractor(video_path, timestamp):
|
21
|
-
"""
|
22
|
-
Calls the video frame extractor script and captures the output.
|
23
|
-
|
24
|
-
Args:
|
25
|
-
video_path (str): Path to the video file.
|
26
|
-
timestamp (str): Timestamp string (HH:MM:SS).
|
27
|
-
|
28
|
-
Returns:
|
29
|
-
str: The path of the selected image, or None on error.
|
30
|
-
"""
|
31
|
-
try:
|
32
|
-
# Get the directory of the current script
|
33
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
34
|
-
# Construct the path to the frame extractor script
|
35
|
-
script_path = os.path.join(current_dir, "ss_selector.py") # Replace with the actual script name if different
|
36
|
-
|
37
|
-
logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp)]))
|
38
|
-
|
39
|
-
# Run the script using subprocess.run()
|
40
|
-
result = subprocess.run(
|
41
|
-
[sys.executable, "-m", "GameSentenceMiner.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
|
42
|
-
capture_output=True,
|
43
|
-
text=True, # Get output as text
|
44
|
-
check=False # Raise an exception for non-zero exit codes
|
45
|
-
)
|
46
|
-
if result.returncode != 0:
|
47
|
-
logger.error(f"Script failed with return code: {result.returncode}")
|
48
|
-
return None
|
49
|
-
logger.info(result)
|
50
|
-
# Print the standard output
|
51
|
-
logger.info(f"Frame extractor script output: {result.stdout.strip()}")
|
52
|
-
return result.stdout.strip() # Return the output
|
53
|
-
|
54
|
-
except subprocess.CalledProcessError as e:
|
55
|
-
logger.error(f"Error calling script: {e}")
|
56
|
-
logger.error(f"Script output (stderr): {e.stderr.strip()}")
|
57
|
-
return None
|
58
|
-
except FileNotFoundError:
|
59
|
-
logger.error(f"Error: Script not found at {script_path}. Make sure the script name is correct.")
|
60
|
-
return None
|
61
|
-
except Exception as e:
|
62
|
-
logger.error(f"An unexpected error occurred: {e}")
|
63
|
-
return None
|
64
|
-
|
65
|
-
def get_screenshot(video_file, screenshot_timing, try_selector=False):
|
66
|
-
screenshot_timing = screenshot_timing if screenshot_timing else 1
|
67
|
-
if try_selector:
|
68
|
-
filepath = call_frame_extractor(video_path=video_file, timestamp=screenshot_timing)
|
69
|
-
output = process_image(filepath)
|
70
|
-
if output:
|
71
|
-
return output
|
72
|
-
else:
|
73
|
-
logger.error("Frame extractor script failed to run or returned no output, defaulting")
|
74
|
-
output_image = make_unique_file_name(os.path.join(
|
75
|
-
get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
|
76
|
-
# FFmpeg command to extract the last frame of the video
|
77
|
-
ffmpeg_command = ffmpeg_base_command_list + [
|
78
|
-
"-ss", f"{screenshot_timing}",
|
79
|
-
"-i", f"{video_file}",
|
80
|
-
"-vframes", "1" # Extract only one frame
|
81
|
-
]
|
82
|
-
|
83
|
-
if get_config().screenshot.custom_ffmpeg_settings:
|
84
|
-
ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.replace("\"", "").split(" "))
|
85
|
-
else:
|
86
|
-
ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
|
87
|
-
|
88
|
-
if get_config().screenshot.width or get_config().screenshot.height:
|
89
|
-
ffmpeg_command.extend(
|
90
|
-
["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
|
91
|
-
|
92
|
-
ffmpeg_command.append(f"{output_image}")
|
93
|
-
|
94
|
-
logger.debug(f"FFMPEG SS Command: {ffmpeg_command}")
|
95
|
-
|
96
|
-
try:
|
97
|
-
for i in range(3):
|
98
|
-
logger.debug(" ".join(ffmpeg_command))
|
99
|
-
result = subprocess.run(ffmpeg_command)
|
100
|
-
if result.returncode != 0 and i < 2:
|
101
|
-
raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
|
102
|
-
else:
|
103
|
-
break
|
104
|
-
except Exception as e:
|
105
|
-
logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
|
106
|
-
output_image = make_unique_file_name(os.path.join(
|
107
|
-
get_config().paths.screenshot_destination,
|
108
|
-
f"{obs.get_current_game(sanitize=True)}.png"))
|
109
|
-
ffmpeg_command = ffmpeg_base_command_list + [
|
110
|
-
"-ss", f"{screenshot_timing}", # Default to 1 second
|
111
|
-
"-i", video_file,
|
112
|
-
"-vframes", "1",
|
113
|
-
output_image
|
114
|
-
]
|
115
|
-
subprocess.run(ffmpeg_command)
|
116
|
-
|
117
|
-
logger.debug(f"Screenshot saved to: {output_image}")
|
118
|
-
|
119
|
-
return output_image
|
120
|
-
|
121
|
-
def get_screenshot_for_line(video_file, game_line, try_selector=False):
|
122
|
-
return get_screenshot(video_file, get_screenshot_time(video_file, game_line), try_selector)
|
123
|
-
|
124
|
-
|
125
|
-
def get_screenshot_time(video_path, game_line, default_beginning=False, vad_result=None, doing_multi_line=False, previous_line=False):
|
126
|
-
if game_line:
|
127
|
-
line_time = game_line.time
|
128
|
-
else:
|
129
|
-
# Assuming initial_time is defined elsewhere if game_line is None
|
130
|
-
line_time = initial_time
|
131
|
-
if previous_line:
|
132
|
-
logger.debug(f"Calculating screenshot time for previous line: {str(game_line.text)}")
|
133
|
-
else:
|
134
|
-
logger.debug("Calculating screenshot time for line: " + str(game_line.text))
|
135
|
-
|
136
|
-
file_length = get_video_duration(video_path)
|
137
|
-
file_mod_time = get_file_modification_time(video_path)
|
138
|
-
|
139
|
-
# Calculate when the line occurred within the video file (seconds from start)
|
140
|
-
time_delta = file_mod_time - line_time
|
141
|
-
line_timestamp_in_video = file_length - time_delta.total_seconds()
|
142
|
-
screenshot_offset = get_config().screenshot.seconds_after_line
|
143
|
-
|
144
|
-
# Calculate screenshot time from the beginning by adding the offset
|
145
|
-
# if vad_result and vad_result.success and not doing_multi_line:
|
146
|
-
# screenshot_time_from_beginning = line_timestamp_in_video + vad_result.end - 1
|
147
|
-
# logger.info(f"Using VAD result {vad_result} for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
|
148
|
-
if get_config().screenshot.screenshot_timing_setting == "beginning":
|
149
|
-
screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
|
150
|
-
logger.debug(f"Using 'beginning' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
|
151
|
-
elif get_config().screenshot.screenshot_timing_setting == "middle":
|
152
|
-
if game_line.next:
|
153
|
-
screenshot_time_from_beginning = line_timestamp_in_video + ((game_line.next.time - game_line.time).total_seconds() / 2) + screenshot_offset
|
154
|
-
else:
|
155
|
-
screenshot_time_from_beginning = (file_length - ((file_length - line_timestamp_in_video) / 2)) + screenshot_offset
|
156
|
-
logger.debug(f"Using 'middle' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
|
157
|
-
elif get_config().screenshot.screenshot_timing_setting == "end":
|
158
|
-
if game_line.next:
|
159
|
-
screenshot_time_from_beginning = line_timestamp_in_video + (game_line.next.time - game_line.time).total_seconds() - screenshot_offset
|
160
|
-
else:
|
161
|
-
screenshot_time_from_beginning = file_length - screenshot_offset
|
162
|
-
logger.debug(f"Using 'end' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
|
163
|
-
else:
|
164
|
-
logger.error(f"Invalid screenshot timing setting: {get_config().screenshot.screenshot_timing_setting}")
|
165
|
-
screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
|
166
|
-
|
167
|
-
# Check if the calculated time is out of bounds
|
168
|
-
if screenshot_time_from_beginning < 0 or screenshot_time_from_beginning > file_length:
|
169
|
-
logger.error(
|
170
|
-
f"Calculated screenshot time ({screenshot_time_from_beginning:.2f}s) is out of bounds for video (length {file_length:.2f}s)."
|
171
|
-
)
|
172
|
-
if default_beginning:
|
173
|
-
return 1.0
|
174
|
-
return file_length - screenshot_offset
|
175
|
-
|
176
|
-
# Return the calculated time from the beginning
|
177
|
-
return screenshot_time_from_beginning
|
178
|
-
|
179
|
-
|
180
|
-
def process_image(image_file):
|
181
|
-
output_image = make_unique_file_name(
|
182
|
-
os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
|
183
|
-
|
184
|
-
# FFmpeg command to process the input image
|
185
|
-
ffmpeg_command = ffmpeg_base_command_list + [
|
186
|
-
"-i", image_file
|
187
|
-
]
|
188
|
-
|
189
|
-
if get_config().screenshot.custom_ffmpeg_settings:
|
190
|
-
ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.split(" "))
|
191
|
-
else:
|
192
|
-
ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
|
193
|
-
|
194
|
-
if get_config().screenshot.width or get_config().screenshot.height:
|
195
|
-
ffmpeg_command.extend(
|
196
|
-
["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
|
197
|
-
|
198
|
-
ffmpeg_command.append(output_image)
|
199
|
-
logger.debug(ffmpeg_command)
|
200
|
-
logger.debug(" ".join(ffmpeg_command))
|
201
|
-
# Run the command
|
202
|
-
try:
|
203
|
-
for i in range(3):
|
204
|
-
logger.debug(" ".join(ffmpeg_command))
|
205
|
-
result = subprocess.run(ffmpeg_command)
|
206
|
-
if result.returncode != 0 and i < 2:
|
207
|
-
raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
|
208
|
-
else:
|
209
|
-
break
|
210
|
-
except Exception as e:
|
211
|
-
logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
|
212
|
-
output_image = make_unique_file_name(os.path.join(get_config().paths.screenshot_destination, f"{obs.get_current_game(sanitize=True)}.png"))
|
213
|
-
shutil.move(image_file, output_image)
|
214
|
-
|
215
|
-
logger.info(f"Processed image saved to: {output_image}")
|
216
|
-
|
217
|
-
return output_image
|
218
|
-
|
219
|
-
|
220
|
-
def get_audio_codec(video_path):
|
221
|
-
command = [
|
222
|
-
f"{get_ffprobe_path()}",
|
223
|
-
"-v", "error",
|
224
|
-
"-select_streams", "a:0",
|
225
|
-
"-show_entries", "stream=codec_name",
|
226
|
-
"-of", "json",
|
227
|
-
video_path
|
228
|
-
]
|
229
|
-
|
230
|
-
logger.debug(" ".join(command))
|
231
|
-
# Run the command and capture the output
|
232
|
-
result = subprocess.run(command, capture_output=True, text=True)
|
233
|
-
|
234
|
-
# Parse the JSON output
|
235
|
-
try:
|
236
|
-
output = json.loads(result.stdout)
|
237
|
-
codec_name = output['streams'][0]['codec_name']
|
238
|
-
return codec_name
|
239
|
-
except (json.JSONDecodeError, KeyError, IndexError):
|
240
|
-
logger.error("Failed to get codec information. Re-encoding Anyways")
|
241
|
-
return None
|
242
|
-
|
243
|
-
|
244
|
-
def get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time):
|
245
|
-
supported_formats = {
|
246
|
-
'opus': 'libopus',
|
247
|
-
'mp3': 'libmp3lame',
|
248
|
-
'ogg': 'libvorbis',
|
249
|
-
'aac': 'aac',
|
250
|
-
'm4a': 'aac',
|
251
|
-
}
|
252
|
-
|
253
|
-
codec = get_audio_codec(video_path)
|
254
|
-
|
255
|
-
if codec == get_config().audio.extension:
|
256
|
-
codec_command = ['-c:a', 'copy']
|
257
|
-
logger.debug(f"Extracting {get_config().audio.extension} from video")
|
258
|
-
else:
|
259
|
-
codec_command = ["-c:a", f"{supported_formats[get_config().audio.extension]}"]
|
260
|
-
logger.debug(f"Re-encoding {codec} to {get_config().audio.extension}")
|
261
|
-
|
262
|
-
untrimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
|
263
|
-
suffix=f"_untrimmed.{get_config().audio.extension}").name
|
264
|
-
|
265
|
-
command = ffmpeg_base_command_list + [
|
266
|
-
"-i", video_path,
|
267
|
-
"-map", "0:a"] + codec_command + [
|
268
|
-
untrimmed_audio
|
269
|
-
]
|
270
|
-
|
271
|
-
# FFmpeg command to extract OR re-encode the audio
|
272
|
-
# command = f"{ffmpeg_base_command} -i \"{video_path}\" -map 0:a {codec_command} \"{untrimmed_audio}\""
|
273
|
-
logger.debug("Doing initial audio extraction")
|
274
|
-
logger.debug(" ".join(command))
|
275
|
-
|
276
|
-
subprocess.run(command)
|
277
|
-
|
278
|
-
return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time, anki_card_creation_time)
|
279
|
-
|
280
|
-
|
281
|
-
def get_video_duration(file_path):
|
282
|
-
ffprobe_command = [
|
283
|
-
f"{get_ffprobe_path()}",
|
284
|
-
"-v", "error",
|
285
|
-
"-show_entries", "format=duration",
|
286
|
-
"-of", "json",
|
287
|
-
file_path
|
288
|
-
]
|
289
|
-
logger.debug(" ".join(ffprobe_command))
|
290
|
-
result = subprocess.run(ffprobe_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
291
|
-
duration_info = json.loads(result.stdout)
|
292
|
-
logger.debug(f"Video duration: {duration_info}")
|
293
|
-
return float(duration_info["format"]["duration"]) # Return the duration in seconds
|
294
|
-
|
295
|
-
|
296
|
-
def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line, anki_card_creation_time):
|
297
|
-
trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
|
298
|
-
suffix=f".{get_config().audio.extension}").name
|
299
|
-
start_trim_time, total_seconds, total_seconds_after_offset = get_video_timings(video_path, game_line, anki_card_creation_time)
|
300
|
-
|
301
|
-
ffmpeg_command = ffmpeg_base_command_list + [
|
302
|
-
"-i", untrimmed_audio,
|
303
|
-
"-ss", str(start_trim_time)]
|
304
|
-
if next_line and next_line > game_line.time:
|
305
|
-
end_total_seconds = total_seconds + (next_line - game_line.time).total_seconds() + 1
|
306
|
-
hours, remainder = divmod(end_total_seconds, 3600)
|
307
|
-
minutes, seconds = divmod(remainder, 60)
|
308
|
-
end_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
309
|
-
ffmpeg_command.extend(['-to', end_trim_time])
|
310
|
-
logger.debug(
|
311
|
-
f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time}")
|
312
|
-
|
313
|
-
ffmpeg_command.extend([
|
314
|
-
"-c", "copy", # Using copy to avoid re-encoding, adjust if needed
|
315
|
-
trimmed_audio
|
316
|
-
])
|
317
|
-
|
318
|
-
logger.debug(" ".join(ffmpeg_command))
|
319
|
-
subprocess.run(ffmpeg_command)
|
320
|
-
|
321
|
-
logger.debug(f"{total_seconds_after_offset} trimmed off of beginning")
|
322
|
-
|
323
|
-
logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
|
324
|
-
return trimmed_audio
|
325
|
-
|
326
|
-
def get_video_timings(video_path, game_line, anki_card_creation_time=None):
|
327
|
-
if anki_card_creation_time and get_config().advanced.use_anki_note_creation_time:
|
328
|
-
file_mod_time = anki_card_creation_time
|
329
|
-
else:
|
330
|
-
file_mod_time = get_file_modification_time(video_path)
|
331
|
-
file_length = get_video_duration(video_path)
|
332
|
-
time_delta = file_mod_time - game_line.time
|
333
|
-
# Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
|
334
|
-
total_seconds = file_length - time_delta.total_seconds()
|
335
|
-
total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
|
336
|
-
if total_seconds < 0 or total_seconds >= file_length:
|
337
|
-
logger.error("Line mined is outside of the replay buffer! Defaulting to the beginning of the replay buffer. ")
|
338
|
-
logger.info("Recommend either increasing replay buffer length in OBS Settings or mining faster.")
|
339
|
-
return 0, 0, 0
|
340
|
-
|
341
|
-
hours, remainder = divmod(total_seconds_after_offset, 3600)
|
342
|
-
minutes, seconds = divmod(remainder, 60)
|
343
|
-
start_trim_time = "{:02}:{:02}:{:06.3f}".format(int(hours), int(minutes), seconds)
|
344
|
-
return start_trim_time, total_seconds, total_seconds_after_offset
|
345
|
-
|
346
|
-
|
347
|
-
def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
|
348
|
-
logger.debug(f"Re-encode running with settings: {user_ffmpeg_options}")
|
349
|
-
temp_file = create_temp_file_with_same_name(input_file)
|
350
|
-
command = ffmpeg_base_command_list + [
|
351
|
-
"-i", input_file,
|
352
|
-
"-map", "0:a"
|
353
|
-
] + user_ffmpeg_options.replace("\"", "").split(" ") + [
|
354
|
-
temp_file
|
355
|
-
]
|
356
|
-
|
357
|
-
logger.debug(" ".join(command))
|
358
|
-
process = subprocess.run(command)
|
359
|
-
|
360
|
-
if process.returncode != 0:
|
361
|
-
logger.error("Re-encode failed, using original audio")
|
362
|
-
return
|
363
|
-
|
364
|
-
replace_file_with_retry(temp_file, final_output_audio)
|
365
|
-
|
366
|
-
|
367
|
-
def create_temp_file_with_same_name(input_file: str):
|
368
|
-
split = input_file.split(".")
|
369
|
-
return f"{split[0]}_temp.{split[1]}"
|
370
|
-
|
371
|
-
|
372
|
-
def replace_file_with_retry(temp_file, input_file, retries=5, delay=1):
|
373
|
-
for attempt in range(retries):
|
374
|
-
try:
|
375
|
-
shutil.move(temp_file, input_file)
|
376
|
-
return
|
377
|
-
except OSError as e:
|
378
|
-
if attempt < retries - 1:
|
379
|
-
logger.warning(f"Attempt {attempt + 1}: File still in use. Retrying in {delay} seconds...")
|
380
|
-
time.sleep(delay)
|
381
|
-
else:
|
382
|
-
logger.error(f"Failed to replace the file after {retries} attempts. Error: {e}")
|
383
|
-
raise
|
384
|
-
|
385
|
-
|
386
|
-
def trim_audio_by_end_time(input_audio, end_time, output_audio):
|
387
|
-
command = ffmpeg_base_command_list + [
|
388
|
-
"-i", input_audio,
|
389
|
-
"-to", str(end_time),
|
390
|
-
"-c", "copy",
|
391
|
-
output_audio
|
392
|
-
]
|
393
|
-
logger.debug(" ".join(command))
|
394
|
-
subprocess.run(command)
|
395
|
-
|
396
|
-
|
397
|
-
def convert_audio_to_wav(input_audio, output_wav):
|
398
|
-
command = ffmpeg_base_command_list + [
|
399
|
-
"-i", input_audio,
|
400
|
-
"-ar", "16000", # Resample to 16kHz
|
401
|
-
"-ac", "1", # Convert to mono
|
402
|
-
"-af", "afftdn,dialoguenhance" if not is_linux() else "afftdn",
|
403
|
-
output_wav
|
404
|
-
]
|
405
|
-
logger.debug(" ".join(command))
|
406
|
-
subprocess.run(command)
|
407
|
-
|
408
|
-
|
409
|
-
# Trim the audio using FFmpeg based on detected speech timestamps
|
410
|
-
def trim_audio(input_audio, start_time, end_time, output_audio):
|
411
|
-
command = ffmpeg_base_command_list.copy()
|
412
|
-
|
413
|
-
command.extend(['-i', input_audio])
|
414
|
-
|
415
|
-
if get_config().vad.trim_beginning and start_time > 0:
|
416
|
-
logger.debug(f"trimming beginning to {start_time}")
|
417
|
-
command.extend(['-ss', f"{start_time:.2f}"])
|
418
|
-
|
419
|
-
command.extend([
|
420
|
-
'-to', f"{end_time:.2f}",
|
421
|
-
'-c', 'copy',
|
422
|
-
output_audio
|
423
|
-
])
|
424
|
-
|
425
|
-
logger.debug(" ".join(command))
|
426
|
-
|
427
|
-
subprocess.run(command)
|
428
|
-
|
429
|
-
|
430
|
-
def is_video_big_enough(file_path, min_size_kb=250):
|
431
|
-
try:
|
432
|
-
file_size = os.path.getsize(file_path) # Size in bytes
|
433
|
-
file_size_kb = file_size / 1024 # Convert to KB
|
434
|
-
return file_size_kb >= min_size_kb
|
435
|
-
except FileNotFoundError:
|
436
|
-
logger.error("File not found!")
|
437
|
-
return False
|
438
|
-
except Exception as e:
|
439
|
-
logger.error(f"Error: {e}")
|
440
|
-
return False
|
441
|
-
|
GameSentenceMiner/model.py
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from typing import Optional, List
|
3
|
-
|
4
|
-
from dataclasses_json import dataclass_json
|
5
|
-
|
6
|
-
from GameSentenceMiner.configuration import get_config, logger, save_current_config
|
7
|
-
|
8
|
-
|
9
|
-
# OBS
|
10
|
-
@dataclass_json
|
11
|
-
@dataclass
|
12
|
-
class SceneInfo:
|
13
|
-
currentProgramSceneName: str
|
14
|
-
currentProgramSceneUuid: str
|
15
|
-
sceneName: str
|
16
|
-
sceneUuid: str
|
17
|
-
|
18
|
-
|
19
|
-
@dataclass_json
|
20
|
-
@dataclass
|
21
|
-
class SceneItemTransform:
|
22
|
-
alignment: int
|
23
|
-
boundsAlignment: int
|
24
|
-
boundsHeight: float
|
25
|
-
boundsType: str
|
26
|
-
boundsWidth: float
|
27
|
-
cropBottom: int
|
28
|
-
cropLeft: int
|
29
|
-
cropRight: int
|
30
|
-
cropToBounds: bool
|
31
|
-
cropTop: int
|
32
|
-
height: float
|
33
|
-
positionX: float
|
34
|
-
positionY: float
|
35
|
-
rotation: float
|
36
|
-
scaleX: float
|
37
|
-
scaleY: float
|
38
|
-
sourceHeight: float
|
39
|
-
sourceWidth: float
|
40
|
-
width: float
|
41
|
-
|
42
|
-
|
43
|
-
@dataclass_json
|
44
|
-
@dataclass
|
45
|
-
class SceneItem:
|
46
|
-
inputKind: str
|
47
|
-
isGroup: Optional[bool]
|
48
|
-
sceneItemBlendMode: str
|
49
|
-
sceneItemEnabled: bool
|
50
|
-
sceneItemId: int
|
51
|
-
sceneItemIndex: int
|
52
|
-
sceneItemLocked: bool
|
53
|
-
sceneItemTransform: SceneItemTransform
|
54
|
-
sourceName: str
|
55
|
-
sourceType: str
|
56
|
-
sourceUuid: str
|
57
|
-
|
58
|
-
# def __init__(self, **kwargs):
|
59
|
-
# self.inputKind = kwargs['inputKind']
|
60
|
-
# self.isGroup = kwargs['isGroup']
|
61
|
-
# self.sceneItemBlendMode = kwargs['sceneItemBlendMode']
|
62
|
-
# self.sceneItemEnabled = kwargs['sceneItemEnabled']
|
63
|
-
# self.sceneItemId = kwargs['sceneItemId']
|
64
|
-
# self.sceneItemIndex = kwargs['sceneItemIndex']
|
65
|
-
# self.sceneItemLocked = kwargs['sceneItemLocked']
|
66
|
-
# self.sceneItemTransform = SceneItemTransform(**kwargs['sceneItemTransform'])
|
67
|
-
# self.sourceName = kwargs['sourceName']
|
68
|
-
# self.sourceType = kwargs['sourceType']
|
69
|
-
# self.sourceUuid = kwargs['sourceUuid']
|
70
|
-
|
71
|
-
|
72
|
-
@dataclass_json
|
73
|
-
@dataclass
|
74
|
-
class SceneItemsResponse:
|
75
|
-
sceneItems: List[SceneItem]
|
76
|
-
|
77
|
-
# def __init__(self, **kwargs):
|
78
|
-
# self.sceneItems = [SceneItem(**item) for item in kwargs['sceneItems']]
|
79
|
-
|
80
|
-
|
81
|
-
@dataclass_json
|
82
|
-
@dataclass
|
83
|
-
class RecordDirectory:
|
84
|
-
recordDirectory: str
|
85
|
-
|
86
|
-
|
87
|
-
@dataclass_json
|
88
|
-
@dataclass
|
89
|
-
class SceneItemInfo:
|
90
|
-
sceneIndex: int
|
91
|
-
sceneName: str
|
92
|
-
sceneUuid: str
|
93
|
-
|
94
|
-
|
95
|
-
@dataclass_json
|
96
|
-
@dataclass
|
97
|
-
class SceneListResponse:
|
98
|
-
scenes: List[SceneItemInfo]
|
99
|
-
currentProgramSceneName: Optional[str] = None
|
100
|
-
currentProgramSceneUuid: Optional[str] = None
|
101
|
-
currentPreviewSceneName: Optional[str] = None
|
102
|
-
currentPreviewSceneUuid: Optional[str] = None
|
103
|
-
|
104
|
-
#
|
105
|
-
# @dataclass_json
|
106
|
-
# @dataclass
|
107
|
-
# class SourceActive:
|
108
|
-
# videoActive: bool
|
109
|
-
# videoShowing: bool
|
110
|
-
|
111
|
-
@dataclass_json
|
112
|
-
@dataclass
|
113
|
-
class AnkiCard:
|
114
|
-
noteId: int
|
115
|
-
tags: list[str]
|
116
|
-
fields: dict[str, dict[str, str]]
|
117
|
-
cards: list[int]
|
118
|
-
alternatives = {
|
119
|
-
"word_field": ["Front", "Word", "TargetWord", "Expression"],
|
120
|
-
"sentence_field": ["Example", "Context", "Back", "Sentence"],
|
121
|
-
"picture_field": ["Image", "Visual", "Media", "Picture", "Screenshot", 'AnswerImage'],
|
122
|
-
"sentence_audio_field": ["SentenceAudio"]
|
123
|
-
}
|
124
|
-
|
125
|
-
def get_field(self, field_name: str) -> str:
|
126
|
-
if self.has_field(field_name):
|
127
|
-
return self.fields[field_name]['value']
|
128
|
-
else:
|
129
|
-
raise ValueError(f"Field '{field_name}' not found in AnkiCard. Please make sure your Anki Field Settings in GSM Match your fields in your Anki Note!")
|
130
|
-
|
131
|
-
def has_field (self, field_name: str) -> bool:
|
132
|
-
return field_name in self.fields
|
133
|
-
|
134
|
-
def __post_init__(self):
|
135
|
-
config = get_config()
|
136
|
-
changes_found = False
|
137
|
-
if not self.has_field(config.anki.word_field):
|
138
|
-
found_alternative_field, field = self.find_field(config.anki.word_field, "word_field")
|
139
|
-
if found_alternative_field:
|
140
|
-
logger.warning(f"{config.anki.word_field} Not found in Anki Card! Saving alternative field '{field}' for word_field to settings.")
|
141
|
-
config.anki.word_field = field
|
142
|
-
changes_found = True
|
143
|
-
|
144
|
-
if not self.has_field(config.anki.sentence_field):
|
145
|
-
found_alternative_field, field = self.find_field(config.anki.sentence_field, "sentence_field")
|
146
|
-
if found_alternative_field:
|
147
|
-
logger.warning(f"{config.anki.sentence_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_field to settings.")
|
148
|
-
config.anki.sentence_field = field
|
149
|
-
changes_found = True
|
150
|
-
|
151
|
-
if not self.has_field(config.anki.picture_field):
|
152
|
-
found_alternative_field, field = self.find_field(config.anki.picture_field, "picture_field")
|
153
|
-
if found_alternative_field:
|
154
|
-
logger.warning(f"{config.anki.picture_field} Not found in Anki Card! Saving alternative field '{field}' for picture_field to settings.")
|
155
|
-
config.anki.picture_field = field
|
156
|
-
changes_found = True
|
157
|
-
|
158
|
-
if not self.has_field(config.anki.sentence_audio_field):
|
159
|
-
found_alternative_field, field = self.find_field(config.anki.sentence_audio_field, "sentence_audio_field")
|
160
|
-
if found_alternative_field:
|
161
|
-
logger.warning(f"{config.anki.sentence_audio_field} Not found in Anki Card! Saving alternative field '{field}' for sentence_audio_field to settings.")
|
162
|
-
config.anki.sentence_audio_field = field
|
163
|
-
changes_found = True
|
164
|
-
|
165
|
-
if changes_found:
|
166
|
-
save_current_config(config)
|
167
|
-
|
168
|
-
def find_field(self, field, field_type):
|
169
|
-
if field in self.fields:
|
170
|
-
return False, field
|
171
|
-
|
172
|
-
for alt_field in self.alternatives[field_type]:
|
173
|
-
for key in self.fields:
|
174
|
-
if alt_field.lower() == key.lower():
|
175
|
-
return True, key
|
176
|
-
|
177
|
-
return False, None
|