GameSentenceMiner 2.14.7__py3-none-any.whl → 2.14.9__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.
Files changed (66) hide show
  1. GameSentenceMiner/config_gui.py +19 -10
  2. GameSentenceMiner/gsm.py +68 -8
  3. GameSentenceMiner/locales/en_us.json +4 -0
  4. GameSentenceMiner/locales/ja_jp.json +4 -0
  5. GameSentenceMiner/locales/zh_cn.json +4 -0
  6. GameSentenceMiner/obs.py +12 -8
  7. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/METADATA +1 -2
  8. gamesentenceminer-2.14.9.dist-info/RECORD +24 -0
  9. GameSentenceMiner/ai/__init__.py +0 -0
  10. GameSentenceMiner/ai/ai_prompting.py +0 -473
  11. GameSentenceMiner/ocr/__init__.py +0 -0
  12. GameSentenceMiner/ocr/gsm_ocr_config.py +0 -174
  13. GameSentenceMiner/ocr/ocrconfig.py +0 -129
  14. GameSentenceMiner/ocr/owocr_area_selector.py +0 -629
  15. GameSentenceMiner/ocr/owocr_helper.py +0 -638
  16. GameSentenceMiner/ocr/ss_picker.py +0 -140
  17. GameSentenceMiner/owocr/owocr/__init__.py +0 -1
  18. GameSentenceMiner/owocr/owocr/__main__.py +0 -9
  19. GameSentenceMiner/owocr/owocr/config.py +0 -148
  20. GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -1238
  21. GameSentenceMiner/owocr/owocr/ocr.py +0 -1691
  22. GameSentenceMiner/owocr/owocr/run.py +0 -1817
  23. GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -109
  24. GameSentenceMiner/tools/__init__.py +0 -0
  25. GameSentenceMiner/tools/audio_offset_selector.py +0 -215
  26. GameSentenceMiner/tools/ss_selector.py +0 -135
  27. GameSentenceMiner/tools/window_transparency.py +0 -214
  28. GameSentenceMiner/util/__init__.py +0 -0
  29. GameSentenceMiner/util/communication/__init__.py +0 -22
  30. GameSentenceMiner/util/communication/send.py +0 -7
  31. GameSentenceMiner/util/communication/websocket.py +0 -94
  32. GameSentenceMiner/util/configuration.py +0 -1198
  33. GameSentenceMiner/util/db.py +0 -408
  34. GameSentenceMiner/util/downloader/Untitled_json.py +0 -472
  35. GameSentenceMiner/util/downloader/__init__.py +0 -0
  36. GameSentenceMiner/util/downloader/download_tools.py +0 -194
  37. GameSentenceMiner/util/downloader/oneocr_dl.py +0 -250
  38. GameSentenceMiner/util/electron_config.py +0 -259
  39. GameSentenceMiner/util/ffmpeg.py +0 -571
  40. GameSentenceMiner/util/get_overlay_coords.py +0 -366
  41. GameSentenceMiner/util/gsm_utils.py +0 -323
  42. GameSentenceMiner/util/model.py +0 -206
  43. GameSentenceMiner/util/notification.py +0 -147
  44. GameSentenceMiner/util/text_log.py +0 -214
  45. GameSentenceMiner/web/__init__.py +0 -0
  46. GameSentenceMiner/web/service.py +0 -132
  47. GameSentenceMiner/web/static/__init__.py +0 -0
  48. GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  49. GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  50. GameSentenceMiner/web/static/favicon.ico +0 -0
  51. GameSentenceMiner/web/static/favicon.svg +0 -3
  52. GameSentenceMiner/web/static/site.webmanifest +0 -21
  53. GameSentenceMiner/web/static/style.css +0 -292
  54. GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  55. GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  56. GameSentenceMiner/web/templates/__init__.py +0 -0
  57. GameSentenceMiner/web/templates/index.html +0 -50
  58. GameSentenceMiner/web/templates/text_replacements.html +0 -238
  59. GameSentenceMiner/web/templates/utility.html +0 -483
  60. GameSentenceMiner/web/texthooking_page.py +0 -584
  61. GameSentenceMiner/wip/__init___.py +0 -0
  62. gamesentenceminer-2.14.7.dist-info/RECORD +0 -77
  63. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/WHEEL +0 -0
  64. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/entry_points.txt +0 -0
  65. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/licenses/LICENSE +0 -0
  66. {gamesentenceminer-2.14.7.dist-info → gamesentenceminer-2.14.9.dist-info}/top_level.txt +0 -0
@@ -1,571 +0,0 @@
1
- import subprocess
2
- import tempfile
3
- import time
4
- from pathlib import Path
5
-
6
- from GameSentenceMiner import obs
7
- from GameSentenceMiner.util.gsm_utils import make_unique_file_name, get_file_modification_time
8
- from GameSentenceMiner.util import configuration
9
- from GameSentenceMiner.util.configuration import *
10
- from GameSentenceMiner.util.model import VADResult
11
- from GameSentenceMiner.util.text_log import initial_time
12
-
13
-
14
- def get_ffmpeg_path():
15
- return os.path.join(get_app_directory(), "ffmpeg", "ffmpeg.exe") if is_windows() else "ffmpeg"
16
-
17
- def get_ffprobe_path():
18
- return os.path.join(get_app_directory(), "ffmpeg", "ffprobe.exe") if is_windows() else "ffprobe"
19
-
20
- ffmpeg_base_command_list = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", '-nostdin']
21
-
22
- supported_formats = {
23
- 'opus': 'libopus',
24
- 'mp3': 'libmp3lame',
25
- 'ogg': 'libvorbis',
26
- 'aac': 'aac',
27
- 'm4a': 'aac',
28
- }
29
-
30
- def call_frame_extractor(video_path, timestamp):
31
- """
32
- Calls the video frame extractor script and captures the output.
33
-
34
- Args:
35
- video_path (str): Path to the video file.
36
- timestamp (str): Timestamp string (HH:MM:SS).
37
-
38
- Returns:
39
- str: The path of the selected image, or None on error.
40
- """
41
- try:
42
- logger.info(' '.join([sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp)]))
43
-
44
- # Run the script using subprocess.run()
45
- result = subprocess.run(
46
- [sys.executable, "-m", "GameSentenceMiner.tools.ss_selector", video_path, str(timestamp), get_config().screenshot.screenshot_timing_setting], # Use sys.executable
47
- capture_output=True,
48
- text=True, # Get output as text
49
- check=False # Raise an exception for non-zero exit codes
50
- )
51
- if result.returncode != 0:
52
- logger.error(f"Script failed with return code: {result.returncode}")
53
- return None
54
- logger.info(result)
55
- # Print the standard output
56
- logger.info(f"Frame extractor script output: {result.stdout.strip()}")
57
- return result.stdout.strip() # Return the output
58
-
59
- except subprocess.CalledProcessError as e:
60
- logger.error(f"Error calling script: {e}")
61
- logger.error(f"Script output (stderr): {e.stderr.strip()}")
62
- return None
63
- except Exception as e:
64
- logger.error(f"An unexpected error occurred: {e}")
65
- return None
66
-
67
- def get_screenshot(video_file, screenshot_timing, try_selector=False):
68
- screenshot_timing = screenshot_timing if screenshot_timing else 1
69
- if try_selector:
70
- filepath = call_frame_extractor(video_path=video_file, timestamp=screenshot_timing)
71
- output = process_image(filepath)
72
- if output:
73
- return output
74
- else:
75
- logger.error("Frame extractor script failed to run or returned no output, defaulting")
76
- output_image = make_unique_file_name(os.path.join(
77
- get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
78
- # FFmpeg command to extract the last frame of the video
79
- ffmpeg_command = ffmpeg_base_command_list + [
80
- "-ss", f"{screenshot_timing}",
81
- "-i", f"{video_file}",
82
- "-vframes", "1" # Extract only one frame
83
- ]
84
-
85
- if get_config().screenshot.custom_ffmpeg_settings:
86
- ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.replace("\"", "").split(" "))
87
- else:
88
- ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
89
-
90
- if get_config().screenshot.width or get_config().screenshot.height:
91
- ffmpeg_command.extend(
92
- ["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
93
-
94
- ffmpeg_command.append(f"{output_image}")
95
-
96
- logger.debug(f"FFMPEG SS Command: {ffmpeg_command}")
97
-
98
- try:
99
- for i in range(3):
100
- logger.debug(" ".join(ffmpeg_command))
101
- result = subprocess.run(ffmpeg_command)
102
- if result.returncode != 0 and i < 2:
103
- raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
104
- else:
105
- break
106
- except Exception as e:
107
- logger.error(f"Error running FFmpeg command: {e}. Defaulting to standard PNG.")
108
- output_image = make_unique_file_name(os.path.join(
109
- get_temporary_directory(),
110
- f"{obs.get_current_game(sanitize=True)}.png"))
111
- ffmpeg_command = ffmpeg_base_command_list + [
112
- "-ss", f"{screenshot_timing}",
113
- "-i", video_file,
114
- "-vframes", "1",
115
- output_image
116
- ]
117
- subprocess.run(ffmpeg_command)
118
-
119
- logger.debug(f"Screenshot saved to: {output_image}")
120
-
121
- return output_image
122
-
123
- def get_screenshot_for_line(video_file, game_line, try_selector=False):
124
- return get_screenshot(video_file, get_screenshot_time(video_file, game_line), try_selector)
125
-
126
-
127
- def get_screenshot_time(video_path, game_line, default_beginning=False, vad_result=None, doing_multi_line=False, previous_line=False, anki_card_creation_time=0):
128
- if game_line:
129
- line_time = game_line.time
130
- else:
131
- # Assuming initial_time is defined elsewhere if game_line is None
132
- line_time = initial_time
133
- if previous_line:
134
- logger.debug(f"Calculating screenshot time for previous line: {str(game_line.text)}")
135
- else:
136
- logger.debug("Calculating screenshot time for line: " + str(game_line.text))
137
-
138
- file_length = get_video_duration(video_path)
139
- if anki_card_creation_time:
140
- file_mod_time = anki_card_creation_time
141
- else:
142
- file_mod_time = get_file_modification_time(video_path)
143
-
144
- # Calculate when the line occurred within the video file (seconds from start)
145
- time_delta = file_mod_time - line_time
146
- line_timestamp_in_video = file_length - time_delta.total_seconds()
147
- screenshot_offset = get_config().screenshot.seconds_after_line
148
-
149
- # Calculate screenshot time from the beginning by adding the offset
150
- # if vad_result and vad_result.success and not doing_multi_line:
151
- # screenshot_time_from_beginning = line_timestamp_in_video + vad_result.end - 1
152
- # logger.info(f"Using VAD result {vad_result} for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
153
- if get_config().screenshot.screenshot_timing_setting == "beginning":
154
- screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
155
- logger.debug(f"Using 'beginning' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
156
- elif get_config().screenshot.screenshot_timing_setting == "middle":
157
- if game_line.next:
158
- screenshot_time_from_beginning = line_timestamp_in_video + ((game_line.next.time - game_line.time).total_seconds() / 2) + screenshot_offset
159
- else:
160
- screenshot_time_from_beginning = (file_length - ((file_length - line_timestamp_in_video) / 2)) + screenshot_offset
161
- logger.debug(f"Using 'middle' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
162
- elif get_config().screenshot.screenshot_timing_setting == "end":
163
- if game_line.next:
164
- screenshot_time_from_beginning = line_timestamp_in_video + (game_line.next.time - game_line.time).total_seconds() - screenshot_offset
165
- else:
166
- screenshot_time_from_beginning = file_length - screenshot_offset
167
- logger.debug(f"Using 'end' setting for screenshot time: {screenshot_time_from_beginning} seconds from beginning of replay")
168
- else:
169
- logger.error(f"Invalid screenshot timing setting: {get_config().screenshot.screenshot_timing_setting}")
170
- screenshot_time_from_beginning = line_timestamp_in_video + screenshot_offset
171
-
172
- # Check if the calculated time is out of bounds
173
- if screenshot_time_from_beginning < 0 or screenshot_time_from_beginning > file_length:
174
- logger.error(
175
- f"Calculated screenshot time ({screenshot_time_from_beginning:.2f}s) is out of bounds for video (length {file_length:.2f}s)."
176
- )
177
- if default_beginning:
178
- return 1.0
179
- return file_length - screenshot_offset
180
-
181
- # Return the calculated time from the beginning
182
- return screenshot_time_from_beginning
183
-
184
-
185
- def process_image(image_file):
186
- output_image = make_unique_file_name(
187
- os.path.join(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.{get_config().screenshot.extension}"))
188
-
189
- # FFmpeg command to process the input image
190
- ffmpeg_command = ffmpeg_base_command_list + [
191
- "-i", image_file
192
- ]
193
-
194
- if get_config().screenshot.custom_ffmpeg_settings:
195
- ffmpeg_command.extend(get_config().screenshot.custom_ffmpeg_settings.split(" "))
196
- else:
197
- ffmpeg_command.extend(["-compression_level", "6", "-q:v", get_config().screenshot.quality])
198
-
199
- if get_config().screenshot.width or get_config().screenshot.height:
200
- ffmpeg_command.extend(
201
- ["-vf", f"scale={get_config().screenshot.width or -1}:{get_config().screenshot.height or -1}"])
202
-
203
- ffmpeg_command.append(output_image)
204
- logger.debug(ffmpeg_command)
205
- logger.debug(" ".join(ffmpeg_command))
206
- # Run the command
207
- try:
208
- for i in range(3):
209
- logger.debug(" ".join(ffmpeg_command))
210
- result = subprocess.run(ffmpeg_command)
211
- if result.returncode != 0 and i < 2:
212
- raise RuntimeError(f"FFmpeg command failed with return code {result.returncode}")
213
- else:
214
- break
215
- except Exception as e:
216
- logger.error(f"Error re-encoding screenshot: {e}. Defaulting to standard PNG.")
217
- output_image = make_unique_file_name(os.path.join(get_temporary_directory(), f"{obs.get_current_game(sanitize=True)}.png"))
218
- shutil.move(image_file, output_image)
219
-
220
- logger.info(f"Processed image saved to: {output_image}")
221
-
222
- return output_image
223
-
224
-
225
- def get_audio_codec(video_path):
226
- command = [
227
- f"{get_ffprobe_path()}",
228
- "-v", "error",
229
- "-select_streams", "a:0",
230
- "-show_entries", "stream=codec_name",
231
- "-of", "json",
232
- video_path
233
- ]
234
-
235
- logger.debug(" ".join(command))
236
- # Run the command and capture the output
237
- result = subprocess.run(command, capture_output=True, text=True)
238
-
239
- # Parse the JSON output
240
- try:
241
- output = json.loads(result.stdout)
242
- codec_name = output['streams'][0]['codec_name']
243
- return codec_name
244
- except (json.JSONDecodeError, KeyError, IndexError):
245
- logger.error("Failed to get codec information. Re-encoding Anyways")
246
- return None
247
-
248
-
249
- def get_audio_and_trim(video_path, game_line, next_line_time, anki_card_creation_time):
250
- codec = get_audio_codec(video_path)
251
-
252
- if codec == get_config().audio.extension:
253
- codec_command = ['-c:a', 'copy']
254
- logger.debug(f"Extracting {get_config().audio.extension} from video")
255
- else:
256
- codec_command = ["-c:a", f"{supported_formats[get_config().audio.extension]}"]
257
- logger.debug(f"Re-encoding {codec} to {get_config().audio.extension}")
258
-
259
- untrimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
260
- suffix=f"_untrimmed.{get_config().audio.extension}").name
261
-
262
- command = ffmpeg_base_command_list + [
263
- "-i", video_path,
264
- "-map", "0:a"] + codec_command + [
265
- untrimmed_audio
266
- ]
267
-
268
- # FFmpeg command to extract OR re-encode the audio
269
- # command = f"{ffmpeg_base_command} -i \"{video_path}\" -map 0:a {codec_command} \"{untrimmed_audio}\""
270
- logger.debug("Doing initial audio extraction")
271
- logger.debug(" ".join(command))
272
-
273
- subprocess.run(command)
274
-
275
- return trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line_time, anki_card_creation_time)
276
-
277
-
278
- def get_video_duration(file_path):
279
- ffprobe_command = [
280
- f"{get_ffprobe_path()}",
281
- "-v", "error",
282
- "-show_entries", "format=duration",
283
- "-of", "json",
284
- file_path
285
- ]
286
- logger.debug(" ".join(ffprobe_command))
287
- result = subprocess.run(ffprobe_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
288
- duration_info = json.loads(result.stdout)
289
- logger.debug(f"Video duration: {duration_info}")
290
- return float(duration_info["format"]["duration"]) # Return the duration in seconds
291
-
292
-
293
- def trim_audio_based_on_last_line(untrimmed_audio, video_path, game_line, next_line, anki_card_creation_time):
294
- trimmed_audio = tempfile.NamedTemporaryFile(dir=configuration.get_temporary_directory(),
295
- suffix=f".{get_config().audio.extension}").name
296
- start_trim_time, total_seconds, total_seconds_after_offset, file_length = get_video_timings(video_path, game_line, anki_card_creation_time)
297
- end_trim_time = 0
298
- end_trim_seconds = 0
299
-
300
- ffmpeg_command = ffmpeg_base_command_list + [
301
- "-i", untrimmed_audio,
302
- "-ss", str(start_trim_time)]
303
- if next_line and next_line > game_line.time:
304
- end_trim_seconds = total_seconds + (next_line - game_line.time).total_seconds() + get_config().audio.pre_vad_end_offset
305
- end_trim_time = f"{end_trim_seconds:.3f}"
306
- ffmpeg_command.extend(['-to', end_trim_time])
307
- logger.debug(
308
- f"Looks Like this is mining from History, or Multiple Lines were selected Trimming end of audio to {end_trim_time} seconds")
309
- elif get_config().audio.pre_vad_end_offset and get_config().audio.pre_vad_end_offset < 0:
310
- end_trim_seconds = file_length + get_config().audio.pre_vad_end_offset
311
- ffmpeg_command.extend(['-to', str(end_trim_seconds)])
312
- logger.debug(f"Trimming end of audio to {end_trim_seconds} seconds due to pre-vad end offset")
313
-
314
- ffmpeg_command.extend([
315
- "-c", "copy", # Using copy to avoid re-encoding, adjust if needed
316
- trimmed_audio
317
- ])
318
-
319
- logger.debug(" ".join(ffmpeg_command))
320
- subprocess.run(ffmpeg_command)
321
- gsm_state.previous_trim_args = (untrimmed_audio, start_trim_time, end_trim_seconds)
322
-
323
- logger.debug(f"{total_seconds_after_offset} trimmed off of beginning")
324
-
325
- if end_trim_seconds:
326
- logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds with end time {end_trim_seconds} seconds")
327
- else:
328
- logger.info(f"Audio Extracted and trimmed to {start_trim_time} seconds")
329
-
330
-
331
- logger.debug(f"Audio trimmed and saved to {trimmed_audio}")
332
- return trimmed_audio, start_trim_time, end_trim_seconds
333
-
334
- def get_video_timings(video_path, game_line, anki_card_creation_time=None):
335
- if anki_card_creation_time:
336
- file_mod_time = anki_card_creation_time
337
- else:
338
- file_mod_time = get_file_modification_time(video_path)
339
- file_length = get_video_duration(video_path)
340
- time_delta = file_mod_time - game_line.time
341
- # Convert time_delta to FFmpeg-friendly format (HH:MM:SS.milliseconds)
342
- total_seconds = file_length - time_delta.total_seconds()
343
- total_seconds_after_offset = total_seconds + get_config().audio.beginning_offset
344
- if total_seconds < 0 or total_seconds >= file_length:
345
- logger.error("Line mined is outside of the replay buffer! Defaulting to the beginning of the replay buffer. ")
346
- logger.info("Recommend either increasing replay buffer length in OBS Settings or mining faster.")
347
- return 0, 0, 0, file_length
348
-
349
- return total_seconds_after_offset, total_seconds, total_seconds_after_offset, file_length
350
-
351
-
352
- def reencode_file_with_user_config(input_file, final_output_audio, user_ffmpeg_options):
353
- logger.debug(f"Re-encode running with settings: {user_ffmpeg_options}")
354
- temp_file = create_temp_file_with_same_name(input_file)
355
- command = ffmpeg_base_command_list + [
356
- "-i", input_file,
357
- "-map", "0:a"
358
- ] + user_ffmpeg_options.replace("\"", "").split(" ") + [
359
- temp_file
360
- ]
361
-
362
- logger.debug(" ".join(command))
363
- process = subprocess.run(command)
364
-
365
- if process.returncode != 0:
366
- logger.error("Re-encode failed, using original audio")
367
- return
368
-
369
- replace_file_with_retry(temp_file, final_output_audio)
370
-
371
-
372
- def create_temp_file_with_same_name(input_file: str):
373
- path = Path(input_file)
374
- return str(path.with_name(f"{path.stem}_temp{path.suffix}"))
375
-
376
-
377
- def replace_file_with_retry(temp_file, input_file, retries=5, delay=1):
378
- for attempt in range(retries):
379
- try:
380
- shutil.move(temp_file, input_file)
381
- return
382
- except OSError as e:
383
- if attempt < retries - 1:
384
- logger.warning(f"Attempt {attempt + 1}: File still in use. Retrying in {delay} seconds...")
385
- time.sleep(delay)
386
- else:
387
- logger.error(f"Failed to replace the file after {retries} attempts. Error: {e}")
388
- raise
389
-
390
-
391
- def trim_audio_by_end_time(input_audio, end_time, output_audio):
392
- command = ffmpeg_base_command_list + [
393
- "-i", input_audio,
394
- "-to", str(end_time),
395
- "-c", "copy",
396
- output_audio
397
- ]
398
- logger.debug(" ".join(command))
399
- subprocess.run(command)
400
-
401
-
402
- def convert_audio_to_wav(input_audio, output_wav):
403
- command = ffmpeg_base_command_list + [
404
- "-i", input_audio,
405
- "-ar", "16000", # Resample to 16kHz
406
- "-ac", "1", # Convert to mono
407
- "-af", "afftdn,dialoguenhance" if not is_linux() else "afftdn",
408
- output_wav
409
- ]
410
- logger.debug(" ".join(command))
411
- subprocess.run(command)
412
-
413
- def convert_audio_to_wav_lossless(input_audio):
414
- output_wav = make_unique_file_name(
415
- os.path.join(configuration.get_temporary_directory(), "output.wav")
416
- )
417
- command = ffmpeg_base_command_list + [
418
- "-i", input_audio,
419
- output_wav
420
- ]
421
- logger.debug(" ".join(command))
422
- subprocess.run(command)
423
- return output_wav
424
-
425
- def convert_audio_to_mp3(input_audio):
426
- output_mp3 = make_unique_file_name(
427
- os.path.join(configuration.get_temporary_directory(), "output.mp3")
428
- )
429
- command = ffmpeg_base_command_list + [
430
- "-i", input_audio,
431
- "-codec:a", "libmp3lame",
432
- "-qscale:a", "2", # Quality scale for MP3
433
- output_mp3
434
- ]
435
- logger.debug(" ".join(command))
436
- subprocess.run(command)
437
- return output_mp3
438
-
439
-
440
- # Trim the audio using FFmpeg based on detected speech timestamps
441
- def trim_audio(input_audio, start_time, end_time=0, output_audio=None, trim_beginning=False, fade_in_duration=0.05,
442
- fade_out_duration=0.05):
443
- command = ffmpeg_base_command_list.copy()
444
-
445
- command.extend(['-i', input_audio])
446
-
447
- if trim_beginning and start_time > 0:
448
- logger.debug(f"trimming beginning to {start_time}")
449
- command.extend(['-ss', f"{start_time:.2f}"])
450
-
451
- fade_filter = []
452
- if fade_in_duration > 0:
453
- fade_filter.append(f'afade=t=in:d={fade_in_duration}')
454
- if fade_out_duration > 0:
455
- fade_filter.append(f'afade=t=out:st={end_time - fade_out_duration:.2f}:d={fade_out_duration}')
456
- # fade_filter.append(f'afade=t=out:d={fade_out_duration}')
457
-
458
- if end_time > 0:
459
- command.extend([
460
- '-to', f"{end_time:.2f}",
461
- ])
462
-
463
- if fade_filter:
464
- command.extend(['-af', f'afade=t=in:d={fade_in_duration},afade=t=out:st={end_time - fade_out_duration:.2f}:d={fade_out_duration}'])
465
- command.extend(['-c:a', supported_formats[get_config().audio.extension]])
466
- else:
467
- command.extend(['-c', 'copy'])
468
-
469
- command.append(output_audio)
470
-
471
- logger.debug(" ".join(command))
472
-
473
- try:
474
- subprocess.run(command, check=True)
475
- except subprocess.CalledProcessError as e:
476
- logger.error(f"FFmpeg command failed with error: {e}")
477
- logger.error(f"Command: {' '.join(command)}")
478
- except FileNotFoundError:
479
- logger.error("FFmpeg not found. Please ensure FFmpeg is installed and in your PATH.")
480
-
481
-
482
- def combine_audio_files(audio_files, output_file):
483
- if not audio_files:
484
- logger.error("No audio files provided for combination.")
485
- return
486
-
487
- command = ffmpeg_base_command_list + [
488
- "-i", "concat:" + "|".join(audio_files),
489
- "-c", "copy",
490
- output_file
491
- ]
492
-
493
- logger.debug("Combining audio files with command: " + " ".join(command))
494
-
495
- subprocess.run(command)
496
-
497
-
498
- def trim_replay_for_gameline(video_path, start_time, end_time, accurate=False):
499
- """
500
- Trims the video replay based on the start and end times.
501
-
502
- Offers two modes:
503
- 1. Fast (default): Uses stream copy. Very fast, no quality loss, but may not be
504
- frame-accurate (cut starts at the keyframe before start_time).
505
- 2. Accurate: Re-encodes the video. Slower, but provides frame-perfect cuts.
506
-
507
- :param video_path: Path to the video file.
508
- :param start_time: Start time in seconds.
509
- :param end_time: End time in seconds.
510
- :param accurate: If True, re-encodes for frame-perfect trimming. Defaults to False.
511
- :return: Path to the trimmed video file.
512
- """
513
- output_name = f"trimmed_{Path(video_path).stem}.mp4"
514
- trimmed_video = make_unique_file_name(
515
- os.path.join(configuration.get_temporary_directory(), output_name))
516
-
517
- # We use input seeking for accuracy, as it's faster when re-encoding.
518
- # We place -ss before -i for fast seeking.
519
- command = ffmpeg_base_command_list + [
520
- "-ss", str(start_time),
521
- "-i", video_path,
522
- ]
523
-
524
- # The duration is now more reliable to calculate
525
- duration = end_time - start_time
526
- if duration > 0:
527
- command.extend(["-t", str(duration)])
528
-
529
- if accurate:
530
- # Re-encode. Slower but frame-accurate.
531
- # You can specify encoding parameters here if needed, e.g., -crf 23
532
- command.extend(["-c:v", "libx264", "-preset", "veryfast", "-c:a", "aac"])
533
- log_msg = f"Accurately trimming video (re-encoding) {video_path}"
534
- else:
535
- # Stream copy. Fast but not frame-accurate.
536
- command.extend(["-c:v", "copy", "-c:a", "copy"])
537
- log_msg = f"Fast trimming video (stream copy) {video_path}"
538
-
539
- command.append(trimmed_video)
540
-
541
- video_length = get_video_duration(video_path)
542
- logger.info(f"{log_msg} of length {video_length} from {start_time} to {end_time} seconds.")
543
- logger.debug(" ".join(command))
544
-
545
- subprocess.run(command)
546
-
547
- return trimmed_video
548
-
549
-
550
- def is_video_big_enough(file_path, min_size_kb=250):
551
- try:
552
- file_size = os.path.getsize(file_path) # Size in bytes
553
- file_size_kb = file_size / 1024 # Convert to KB
554
- return file_size_kb >= min_size_kb
555
- except FileNotFoundError:
556
- logger.error("File not found!")
557
- return False
558
- except Exception as e:
559
- logger.error(f"Error: {e}")
560
- return False
561
-
562
-
563
- def get_audio_length(path):
564
- result = subprocess.run(
565
- [get_ffprobe_path(), "-v", "error", "-show_entries", "format=duration", "-of",
566
- "default=noprint_wrappers=1:nokey=1", path],
567
- stdout=subprocess.PIPE,
568
- stderr=subprocess.PIPE,
569
- text=True
570
- )
571
- return float(result.stdout.strip())