describealign 0.1.8__py3-none-any.whl → 1.0.1__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.
- {describealign-0.1.8.dist-info → describealign-1.0.1.dist-info}/METADATA +4 -2
- describealign-1.0.1.dist-info/RECORD +7 -0
- describealign.py +305 -48
- describealign-0.1.8.dist-info/RECORD +0 -7
- {describealign-0.1.8.dist-info → describealign-1.0.1.dist-info}/LICENSE +0 -0
- {describealign-0.1.8.dist-info → describealign-1.0.1.dist-info}/WHEEL +0 -0
- {describealign-0.1.8.dist-info → describealign-1.0.1.dist-info}/entry_points.txt +0 -0
- {describealign-0.1.8.dist-info → describealign-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: describealign
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Combines videos with matching audio files (e.g. audio descriptions)
|
|
5
5
|
Author-email: Julian Brown <julbean@proton.me>
|
|
6
6
|
Project-URL: Homepage, https://github.com/julbean/describealign
|
|
@@ -17,7 +17,9 @@ Requires-Dist: matplotlib >=3.5.0
|
|
|
17
17
|
Requires-Dist: numpy >=1.21.4
|
|
18
18
|
Requires-Dist: python-speech-features >=0.6
|
|
19
19
|
Requires-Dist: scipy >=1.10.1
|
|
20
|
-
Requires-Dist: pytsmod >=0.3.
|
|
20
|
+
Requires-Dist: pytsmod >=0.3.7
|
|
21
|
+
Requires-Dist: PySimpleGUIQt >=0.35.0 ; platform_system != "Windows"
|
|
22
|
+
Requires-Dist: PySimpleGUIWx ==0.17.2 ; platform_system == "Windows"
|
|
21
23
|
|
|
22
24
|
For usage help, simply run the script directly.
|
|
23
25
|
If the Scripts folder has been added to PATH, can be run
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
describealign.py,sha256=GLDxXbWfET4yn4aa32KUCGvHtIBlbltZm8RUSuajUiQ,64661
|
|
2
|
+
describealign-1.0.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
+
describealign-1.0.1.dist-info/METADATA,sha256=fxI3IaRakgXwFnkPR6GcMhHeIZ4F81pcOUnpTyD6puc,1195
|
|
4
|
+
describealign-1.0.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
5
|
+
describealign-1.0.1.dist-info/entry_points.txt,sha256=7o7N6v3r4vFIH_XBdgk7WWhr-vZ_YitY8JWMdzN5xU0,71
|
|
6
|
+
describealign-1.0.1.dist-info/top_level.txt,sha256=VYHWy4TeimBAF5BQAuDj4adGdLaWs2AoYx6qQjGPJ4M,14
|
|
7
|
+
describealign-1.0.1.dist-info/RECORD,,
|
describealign.py
CHANGED
|
@@ -24,11 +24,8 @@ You should have received a copy of the GNU General Public License
|
|
|
24
24
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
25
25
|
'''
|
|
26
26
|
|
|
27
|
-
VIDEO_EXTENSIONS = set(['mp4', 'mkv', 'avi', 'mov', 'webm', '
|
|
27
|
+
VIDEO_EXTENSIONS = set(['mp4', 'mkv', 'avi', 'mov', 'webm', 'm4v', 'flv', 'vob'])
|
|
28
28
|
AUDIO_EXTENSIONS = set(['mp3', 'm4a', 'opus', 'wav', 'aac', 'flac', 'ac3', 'mka'])
|
|
29
|
-
OUTPUT_DIR = "videos_with_ad"
|
|
30
|
-
PLOT_DIR = "alignment_plots"
|
|
31
|
-
EXTERNAL_FILES_FOLDER = "resources"
|
|
32
29
|
PLOT_ALIGNMENT_TO_FILE = True
|
|
33
30
|
|
|
34
31
|
TIMESTEP_SIZE_SECONDS = .16
|
|
@@ -69,23 +66,50 @@ import scipy.interpolate
|
|
|
69
66
|
import scipy.ndimage as nd
|
|
70
67
|
import scipy.sparse
|
|
71
68
|
import pytsmod
|
|
69
|
+
import configparser
|
|
70
|
+
import traceback
|
|
71
|
+
import multiprocessing
|
|
72
|
+
import platform
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
IS_RUNNING_WINDOWS = platform.system() == 'Windows'
|
|
75
|
+
if IS_RUNNING_WINDOWS:
|
|
76
|
+
import PySimpleGUIWx as sg
|
|
77
|
+
else:
|
|
78
|
+
import PySimpleGUIQt as sg
|
|
79
|
+
|
|
80
|
+
def display(text, func=None):
|
|
81
|
+
if func:
|
|
82
|
+
func(text)
|
|
83
|
+
print(text)
|
|
84
|
+
|
|
85
|
+
def throw_runtime_error(text, func=None):
|
|
86
|
+
if func:
|
|
87
|
+
func(text)
|
|
88
|
+
raise RuntimeError(text)
|
|
89
|
+
|
|
90
|
+
def ensure_folders_exist(dirs, display_func=None):
|
|
74
91
|
for dir in dirs:
|
|
75
92
|
if not os.path.isdir(dir):
|
|
76
|
-
|
|
93
|
+
display("Directory not found, creating it: " + dir, display_func)
|
|
77
94
|
os.makedirs(dir)
|
|
78
95
|
|
|
79
96
|
def get_sorted_filenames(path, extensions, alt_extensions=set([])):
|
|
80
|
-
path
|
|
81
|
-
if
|
|
82
|
-
files =
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
# path could be three different things: a file, a directory, a list of files
|
|
98
|
+
if type(path) is list:
|
|
99
|
+
files = [os.path.abspath(file) for file in path]
|
|
100
|
+
for file in files:
|
|
101
|
+
if not os.path.isfile(file):
|
|
102
|
+
raise RuntimeError(f"No file found at input path:\n {file}")
|
|
85
103
|
else:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
path = os.path.abspath(path)
|
|
105
|
+
if os.path.isdir(path):
|
|
106
|
+
files = glob.glob(glob.escape(path) + "/*")
|
|
107
|
+
if len(files) == 0:
|
|
108
|
+
raise RuntimeError(f"Empty input directory:\n {path}")
|
|
109
|
+
else:
|
|
110
|
+
if not os.path.isfile(path):
|
|
111
|
+
raise RuntimeError(f"No file or directory found at input path:\n {path}")
|
|
112
|
+
files = [path]
|
|
89
113
|
files = [file for file in files if os.path.splitext(file)[1][1:] in extensions | alt_extensions]
|
|
90
114
|
if len(files) == 0:
|
|
91
115
|
error_msg = [f"No files with valid extensions found at input path:\n {path}",
|
|
@@ -754,12 +778,20 @@ def write_replaced_media_to_disk(output_filename, media_arr, video_file=None, au
|
|
|
754
778
|
'bsf:s': 'setts=ts=\'' + setts_cmd + '\''})
|
|
755
779
|
write_command.run(cmd=get_ffmpeg())
|
|
756
780
|
|
|
781
|
+
# check whether static_ffmpeg has already installed ffmpeg and ffprobe
|
|
782
|
+
def is_ffmpeg_installed():
|
|
783
|
+
ffmpeg_dir = static_ffmpeg.run.get_platform_dir()
|
|
784
|
+
indicator_file = os.path.join(ffmpeg_dir, "installed.crumb")
|
|
785
|
+
return os.path.exists(indicator_file)
|
|
786
|
+
|
|
757
787
|
# combines videos with matching audio files (e.g. audio descriptions)
|
|
758
788
|
# this is the main function of this script, it calls the other functions in order
|
|
759
|
-
def combine(video, audio, smoothness=50,
|
|
789
|
+
def combine(video, audio, smoothness=50, stretch_audio=False, keep_non_ad=False,
|
|
760
790
|
boost=0, ad_detect_sensitivity=.6, boost_sensitivity=.4, yes=False,
|
|
761
|
-
prepend="ad_", no_pitch_correction=False
|
|
791
|
+
prepend="ad_", no_pitch_correction=False, output_dir="videos_with_ad",
|
|
792
|
+
alignment_dir="alignment_plots", display_func=None):
|
|
762
793
|
video_files, video_file_types = get_sorted_filenames(video, VIDEO_EXTENSIONS, AUDIO_EXTENSIONS)
|
|
794
|
+
|
|
763
795
|
if yes == False and sum(video_file_types) > 0:
|
|
764
796
|
print("")
|
|
765
797
|
print("One or more audio files found in video input. Was this intentional?")
|
|
@@ -773,29 +805,38 @@ def combine(video, audio, smoothness=50, stretch_video=False, keep_non_ad=False,
|
|
|
773
805
|
f"The audio path has {len(audio_desc_files)} files"]
|
|
774
806
|
raise RuntimeError("\n".join(error_msg))
|
|
775
807
|
|
|
776
|
-
ensure_folders_exist([
|
|
808
|
+
ensure_folders_exist([output_dir], display_func)
|
|
777
809
|
if PLOT_ALIGNMENT_TO_FILE:
|
|
778
|
-
ensure_folders_exist([
|
|
810
|
+
ensure_folders_exist([alignment_dir], display_func)
|
|
779
811
|
|
|
780
|
-
|
|
812
|
+
display("", display_func)
|
|
781
813
|
for (video_file, audio_desc_file) in zip(video_files, audio_desc_files):
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
814
|
+
display(os.path.split(video_file)[1], display_func)
|
|
815
|
+
display(os.path.split(audio_desc_file)[1], display_func)
|
|
816
|
+
display("", display_func)
|
|
785
817
|
if yes == False:
|
|
786
818
|
print("Are the above input file pairings correct?")
|
|
787
819
|
print("If not, press ctrl+c to kill this script.")
|
|
788
820
|
input("If they are correct, press Enter to continue...")
|
|
789
821
|
print("")
|
|
790
|
-
|
|
822
|
+
|
|
823
|
+
# if ffmpeg isn't installed, install it
|
|
824
|
+
if not is_ffmpeg_installed():
|
|
825
|
+
display("Downloading and installing ffmpeg (media editor, 50 MB download)...", display_func)
|
|
826
|
+
get_ffmpeg()
|
|
827
|
+
if not is_ffmpeg_installed():
|
|
828
|
+
RuntimeError("Failed to install ffmpeg.")
|
|
829
|
+
display("Successfully installed ffmpeg.", display_func)
|
|
830
|
+
|
|
831
|
+
display("Processing files:", display_func)
|
|
791
832
|
|
|
792
833
|
for (video_file, audio_desc_file, video_filetype) in zip(video_files, audio_desc_files,
|
|
793
834
|
video_file_types):
|
|
794
|
-
output_filename = os.path.join(
|
|
795
|
-
|
|
835
|
+
output_filename = os.path.join(output_dir, prepend + os.path.split(video_file)[1])
|
|
836
|
+
display(" " + output_filename, display_func)
|
|
796
837
|
|
|
797
838
|
if os.path.exists(output_filename):
|
|
798
|
-
|
|
839
|
+
display(" output file already exists, skipping...", display_func)
|
|
799
840
|
continue
|
|
800
841
|
|
|
801
842
|
video_arr = parse_audio_from_file(video_file)
|
|
@@ -815,15 +856,7 @@ def combine(video, audio, smoothness=50, stretch_video=False, keep_non_ad=False,
|
|
|
815
856
|
cap_synced_end_points(smooth_path, video_arr, audio_desc_arr)
|
|
816
857
|
|
|
817
858
|
ad_timings = None
|
|
818
|
-
if
|
|
819
|
-
if video_filetype == 1:
|
|
820
|
-
raise RuntimeError("Argument --stretch_video cannot be used when both inputs are audio files.")
|
|
821
|
-
video_offset = np.diff(smooth_path[clips[0][0]])[0]
|
|
822
|
-
start_key_frame = get_closest_key_frame_time(video_file, video_offset)
|
|
823
|
-
setts_cmd = encode_fit_as_ffmpeg_expr(smooth_path, clips, video_offset, start_key_frame)
|
|
824
|
-
write_replaced_media_to_disk(output_filename, None, video_file, audio_desc_file,
|
|
825
|
-
setts_cmd, start_key_frame)
|
|
826
|
-
else:
|
|
859
|
+
if stretch_audio:
|
|
827
860
|
if keep_non_ad:
|
|
828
861
|
video_arr_original = video_arr.copy()
|
|
829
862
|
|
|
@@ -852,11 +885,227 @@ def combine(video, audio, smoothness=50, stretch_video=False, keep_non_ad=False,
|
|
|
852
885
|
write_replaced_media_to_disk(output_filename, video_arr, video_file)
|
|
853
886
|
else:
|
|
854
887
|
write_replaced_media_to_disk(output_filename, video_arr)
|
|
888
|
+
else:
|
|
889
|
+
if video_filetype == 1:
|
|
890
|
+
raise RuntimeError("Argument --stretch_audio is required when both inputs are audio files.")
|
|
891
|
+
video_offset = np.diff(smooth_path[clips[0][0]])[0]
|
|
892
|
+
start_key_frame = get_closest_key_frame_time(video_file, video_offset)
|
|
893
|
+
setts_cmd = encode_fit_as_ffmpeg_expr(smooth_path, clips, video_offset, start_key_frame)
|
|
894
|
+
write_replaced_media_to_disk(output_filename, None, video_file, audio_desc_file,
|
|
895
|
+
setts_cmd, start_key_frame)
|
|
855
896
|
|
|
856
897
|
del video_arr
|
|
857
898
|
if PLOT_ALIGNMENT_TO_FILE:
|
|
858
|
-
plot_filename = os.path.join(
|
|
899
|
+
plot_filename = os.path.join(alignment_dir, os.path.splitext(os.path.split(video_file)[1])[0])
|
|
859
900
|
plot_alignment(plot_filename, path, smooth_path, quals, runs, bad_clips, ad_timings)
|
|
901
|
+
display("All files processed.", display_func)
|
|
902
|
+
|
|
903
|
+
def write_config_file(config_path, settings):
|
|
904
|
+
config = configparser.ConfigParser()
|
|
905
|
+
config.add_section('alignment')
|
|
906
|
+
config['alignment'] = {}
|
|
907
|
+
for key, value in settings.items():
|
|
908
|
+
config['alignment'][key] = str(value)
|
|
909
|
+
with open(config_path, 'w') as f:
|
|
910
|
+
config.write(f)
|
|
911
|
+
|
|
912
|
+
def read_config_file(config_path):
|
|
913
|
+
config = configparser.ConfigParser()
|
|
914
|
+
config.read(config_path)
|
|
915
|
+
settings = {'smoothness': config.getfloat('alignment', 'smoothness', fallback=50),
|
|
916
|
+
'stretch_audio': config.getboolean('alignment', 'stretch_audio', fallback=False),
|
|
917
|
+
'keep_non_ad': config.getboolean('alignment', 'keep_non_ad', fallback=False),
|
|
918
|
+
'boost': config.getfloat('alignment', 'boost', fallback=0),
|
|
919
|
+
'ad_detect_sensitivity':config.getfloat('alignment', 'ad_detect_sensitivity', fallback=.6),
|
|
920
|
+
'boost_sensitivity': config.getfloat('alignment', 'boost_sensitivity', fallback=.4),
|
|
921
|
+
'prepend': config.get('alignment', 'prepend', fallback='ad_'),
|
|
922
|
+
'no_pitch_correction': config.getboolean('alignment', 'no_pitch_correction', fallback=False),
|
|
923
|
+
'output_dir': config.get('alignment', 'output_dir', fallback='videos_with_ad'),
|
|
924
|
+
'alignment_dir': config.get('alignment', 'alignment_dir', fallback='alignment_plots')}
|
|
925
|
+
if not config.has_section('alignment'):
|
|
926
|
+
write_config_file(config_path, settings)
|
|
927
|
+
return settings
|
|
928
|
+
|
|
929
|
+
def settings_gui(config_path):
|
|
930
|
+
settings = read_config_file(config_path)
|
|
931
|
+
layout = [[sg.Text('Check tooltips (i.e. mouse-over text) for descriptions:')],
|
|
932
|
+
[sg.Column([[sg.Text('prepend:', size=(8, 1.2), pad=(1,5)),
|
|
933
|
+
sg.Input(default_text=str(settings['prepend']), size=(8, 1.2), pad=(10,5), key='prepend',
|
|
934
|
+
tooltip='Output file name prepend text. Default is "ad_"')]])],
|
|
935
|
+
[sg.Column([[sg.Text('output_dir:', size=(10, 1.2), pad=(1,5)),
|
|
936
|
+
sg.Input(default_text=str(settings['output_dir']), size=(22, 1.2), pad=(10,5), key='output_dir',
|
|
937
|
+
tooltip='Directory combined output media is saved to. Default is "videos_with_ad"'),
|
|
938
|
+
sg.FolderBrowse(button_text="Browse Folder", key='output_browse')]])],
|
|
939
|
+
[sg.Column([[sg.Text('alignment_dir:', size=(13, 1.2), pad=(1,5)),
|
|
940
|
+
sg.Input(default_text=str(settings['alignment_dir']), size=(22, 1.2), pad=(10,5), key='alignment_dir',
|
|
941
|
+
tooltip='Directory alignment data and plots are saved to. Default is "alignment_plots"'),
|
|
942
|
+
sg.FolderBrowse(button_text="Browse Folder", key='alignment_browse')]], pad=(2,7))],
|
|
943
|
+
[sg.Column([[sg.Text('smoothness:', size=(12, 1), pad=(1,5)),
|
|
944
|
+
sg.Input(default_text=str(settings['smoothness']), size=(8, 1.2), pad=(10,5), key='smoothness',
|
|
945
|
+
tooltip='Lower values make the alignment more accurate when there are skips ' + \
|
|
946
|
+
'(e.g. describer pauses), but also make it more likely to misalign. ' + \
|
|
947
|
+
'Default is 50.')]])],
|
|
948
|
+
[sg.Checkbox('stretch_audio', default=settings['stretch_audio'], key='stretch_audio', change_submits=True,
|
|
949
|
+
tooltip='Stretches the input audio to fit the input video. ' + \
|
|
950
|
+
'Default is to stretch the video to fit the audio.')],
|
|
951
|
+
[sg.Checkbox('keep_non_ad', default=settings['keep_non_ad'], key='keep_non_ad',
|
|
952
|
+
disabled=not settings['stretch_audio'],
|
|
953
|
+
tooltip='Tries to only replace segments with audio description. Useful if ' + \
|
|
954
|
+
'video\'s audio quality is better. Default is to replace all aligned audio. ' + \
|
|
955
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')],
|
|
956
|
+
[sg.Column([[sg.Text('boost:', size=(6, 1), pad=(1,5)),
|
|
957
|
+
sg.Input(default_text=str(settings['boost']), size=(8, 1.2), pad=(10,5),
|
|
958
|
+
key='boost', disabled=not settings['stretch_audio'],
|
|
959
|
+
tooltip='Boost (or quieten) description volume. Units are decibels (dB), so ' + \
|
|
960
|
+
'-3 makes the describer about 2x quieter, while 3 makes them 2x louder. ' + \
|
|
961
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')]])],
|
|
962
|
+
[sg.Column([[sg.Text('ad_detect_sensitivity:', size=(21, 1.2), pad=(1.8,5)),
|
|
963
|
+
sg.Input(default_text=str(settings['ad_detect_sensitivity']), size=(8, 1.2), pad=(10,5),
|
|
964
|
+
key='ad_detect_sensitivity', disabled=not settings['stretch_audio'],
|
|
965
|
+
tooltip='Audio description detection sensitivity ratio. Higher values make ' + \
|
|
966
|
+
'--keep_non_ad more likely to replace aligned audio. Default is 0.6')]])],
|
|
967
|
+
[sg.Column([[sg.Text('boost_sensitivity:', size=(17, 1.2), pad=(1,5)),
|
|
968
|
+
sg.Input(default_text=str(settings['boost_sensitivity']), size=(8, 1.2), pad=(10,5),
|
|
969
|
+
key='boost_sensitivity', disabled=not settings['stretch_audio'],
|
|
970
|
+
tooltip='Higher values make --boost less likely to miss a description, but ' + \
|
|
971
|
+
'also make it more likely to boost non-description audio. Default is 0.4')]])],
|
|
972
|
+
[sg.Checkbox('no_pitch_correction', default=settings['no_pitch_correction'], key='no_pitch_correction',
|
|
973
|
+
disabled=not settings['stretch_audio'],
|
|
974
|
+
tooltip='Skips pitch correction step when stretching audio. ' + \
|
|
975
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')],
|
|
976
|
+
[sg.Column([[sg.Submit('Save', pad=(40,3)),
|
|
977
|
+
sg.Button('Cancel')]], pad=((135,3),10))]]
|
|
978
|
+
settings_window = sg.Window('Settings - describealign', layout, font=('Arial', 16), finalize=True)
|
|
979
|
+
settings_window['prepend'].set_focus()
|
|
980
|
+
while True:
|
|
981
|
+
event, values = settings_window.read()
|
|
982
|
+
if event in (sg.WIN_CLOSED, 'Cancel') or settings_window.TKrootDestroyed:
|
|
983
|
+
break
|
|
984
|
+
if event == 'stretch_audio':
|
|
985
|
+
# work around bug in PySimpleGUIWx's InputText Update function where enabling/disabling are flipped
|
|
986
|
+
if IS_RUNNING_WINDOWS:
|
|
987
|
+
settings_window['boost'].Update(disabled = values['stretch_audio'])
|
|
988
|
+
settings_window['ad_detect_sensitivity'].Update(disabled = values['stretch_audio'])
|
|
989
|
+
settings_window['boost_sensitivity'].Update(disabled = values['stretch_audio'])
|
|
990
|
+
else:
|
|
991
|
+
settings_window['boost'].Update(disabled = not values['stretch_audio'])
|
|
992
|
+
settings_window['ad_detect_sensitivity'].Update(disabled = not values['stretch_audio'])
|
|
993
|
+
settings_window['boost_sensitivity'].Update(disabled = not values['stretch_audio'])
|
|
994
|
+
settings_window['keep_non_ad'].Update(disabled = not values['stretch_audio'])
|
|
995
|
+
settings_window['no_pitch_correction'].Update(disabled = not values['stretch_audio'])
|
|
996
|
+
if event == 'Save':
|
|
997
|
+
settings = values.copy()
|
|
998
|
+
del settings['output_browse']
|
|
999
|
+
del settings['alignment_browse']
|
|
1000
|
+
write_config_file(config_path, settings)
|
|
1001
|
+
break
|
|
1002
|
+
settings_window.close()
|
|
1003
|
+
|
|
1004
|
+
def combine_print_exceptions(print_queue, *args, **kwargs):
|
|
1005
|
+
try:
|
|
1006
|
+
combine(*args, **kwargs)
|
|
1007
|
+
except:
|
|
1008
|
+
print_queue.put(traceback.format_exc())
|
|
1009
|
+
# raise
|
|
1010
|
+
|
|
1011
|
+
def combine_gui(video_files, audio_files, config_path):
|
|
1012
|
+
output_textbox = sg.Multiline(size=(80,30), key='-OUTPUT-')
|
|
1013
|
+
layout = [[output_textbox],
|
|
1014
|
+
[sg.Button('Close', pad=(360,5))]]
|
|
1015
|
+
combine_window = sg.Window('Combining - describealign', layout, font=('Arial', 16),
|
|
1016
|
+
disable_close=True, finalize=True)
|
|
1017
|
+
output_textbox.update('Combining media files:', append=True)
|
|
1018
|
+
print_queue = multiprocessing.Queue()
|
|
1019
|
+
|
|
1020
|
+
settings = read_config_file(config_path)
|
|
1021
|
+
settings.update({'display_func':print_queue.put, 'yes':True})
|
|
1022
|
+
proc = multiprocessing.Process(target=combine_print_exceptions,
|
|
1023
|
+
args=(print_queue, video_files, audio_files),
|
|
1024
|
+
kwargs=settings, daemon=True)
|
|
1025
|
+
proc.start()
|
|
1026
|
+
while True:
|
|
1027
|
+
# if the script isn't running anymore, re-enable the default close window button
|
|
1028
|
+
if not proc.is_alive():
|
|
1029
|
+
combine_window.DisableClose = False
|
|
1030
|
+
if not print_queue.empty():
|
|
1031
|
+
if IS_RUNNING_WINDOWS:
|
|
1032
|
+
cursor_position = output_textbox.WxTextCtrl.GetInsertionPoint()
|
|
1033
|
+
output_textbox.update('\n' + print_queue.get(), append=True)
|
|
1034
|
+
if IS_RUNNING_WINDOWS:
|
|
1035
|
+
output_textbox.WxTextCtrl.SetInsertionPoint(cursor_position)
|
|
1036
|
+
event, values = combine_window.read(timeout=100)
|
|
1037
|
+
# window closed event isn't always emitted, so also manually check window status
|
|
1038
|
+
if event == sg.WIN_CLOSED or combine_window.TKrootDestroyed:
|
|
1039
|
+
if proc.is_alive():
|
|
1040
|
+
proc.terminate()
|
|
1041
|
+
break
|
|
1042
|
+
if event == 'Close':
|
|
1043
|
+
if not proc.is_alive():
|
|
1044
|
+
combine_window.DisableClose = False
|
|
1045
|
+
break
|
|
1046
|
+
selection = sg.PopupYesNo('Combiner is still running, stop it and close anyway?')
|
|
1047
|
+
if selection != 'Yes':
|
|
1048
|
+
continue
|
|
1049
|
+
proc.terminate()
|
|
1050
|
+
combine_window.DisableClose = False
|
|
1051
|
+
break
|
|
1052
|
+
combine_window.close()
|
|
1053
|
+
|
|
1054
|
+
def main_gui():
|
|
1055
|
+
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini')
|
|
1056
|
+
sg.theme('Light Blue 2')
|
|
1057
|
+
|
|
1058
|
+
all_audio_file_types = [('All Audio File Types', '*.' + ';*.'.join(AUDIO_EXTENSIONS)),]
|
|
1059
|
+
all_video_file_types = [('All Video File Types', '*.' + ';*.'.join(VIDEO_EXTENSIONS)),]
|
|
1060
|
+
all_video_and_audio_file_types = [('All Video and Audio File Types',
|
|
1061
|
+
'*.' + ';*.'.join(VIDEO_EXTENSIONS | AUDIO_EXTENSIONS)),]
|
|
1062
|
+
audio_file_types = [(ext, "*." + ext) for ext in AUDIO_EXTENSIONS]
|
|
1063
|
+
video_and_audio_file_types = [(ext, "*." + ext) for ext in VIDEO_EXTENSIONS] + audio_file_types
|
|
1064
|
+
audio_file_types = all_audio_file_types + audio_file_types
|
|
1065
|
+
video_and_audio_file_types = all_video_file_types + all_video_and_audio_file_types + video_and_audio_file_types
|
|
1066
|
+
# work around bug in PySimpleGUIWx's convert_tkinter_filetypes_to_wx function
|
|
1067
|
+
if IS_RUNNING_WINDOWS:
|
|
1068
|
+
file_fix = lambda file_types: file_types[:1] + [('|' + type[0], type[1]) for type in file_types[1:]]
|
|
1069
|
+
audio_file_types = file_fix(audio_file_types)
|
|
1070
|
+
video_and_audio_file_types = file_fix(video_and_audio_file_types)
|
|
1071
|
+
|
|
1072
|
+
layout = [[sg.Text('Select media files to combine:', size=(40, 2), font=('Arial', 20), pad=(3,15))],
|
|
1073
|
+
[sg.Column([[sg.Text('Video Input:', size=(11, 2), pad=(1,5)),
|
|
1074
|
+
sg.Input(size=(35, 1.2), pad=(10,5), key='-VIDEO_FILES-',
|
|
1075
|
+
tooltip='List video filenames here, in order, separated by semicolons'),
|
|
1076
|
+
sg.FilesBrowse(button_text="Browse Video",
|
|
1077
|
+
file_types=video_and_audio_file_types,
|
|
1078
|
+
tooltip='Select one or more video files')]], pad=(2,7))],
|
|
1079
|
+
[sg.Column([[sg.Text('Audio Input:', size=(11, 2), pad=(1,5)),
|
|
1080
|
+
sg.Input(size=(35, 1.2), pad=(10,5), key='-AUDIO_FILES-',
|
|
1081
|
+
tooltip='List audio filenames here, in order, separated by semicolons'),
|
|
1082
|
+
sg.FilesBrowse(button_text="Browse Audio",
|
|
1083
|
+
file_types=audio_file_types,
|
|
1084
|
+
tooltip='Select one or more audio files')]], pad=(2,7))],
|
|
1085
|
+
[sg.Column([[sg.Submit('Combine', pad=(40,3), tooltip='Combine selected video and audio files'),
|
|
1086
|
+
sg.Button('Settings', tooltip='Edit settings for the GUI and algorithm.')]],
|
|
1087
|
+
pad=((135,3),10))]]
|
|
1088
|
+
window = sg.Window('describealign', layout, font=('Arial', 16), resizable=False, finalize=True)
|
|
1089
|
+
window['-VIDEO_FILES-'].set_focus()
|
|
1090
|
+
while True:
|
|
1091
|
+
event, values = window.read()
|
|
1092
|
+
if event == 'Combine':
|
|
1093
|
+
if len(values['-VIDEO_FILES-']) == 0 or \
|
|
1094
|
+
len(values['-AUDIO_FILES-']) == 0:
|
|
1095
|
+
window.disable()
|
|
1096
|
+
sg.Popup('Error: empty input field.', font=('Arial', 20))
|
|
1097
|
+
window.enable()
|
|
1098
|
+
continue
|
|
1099
|
+
video_files = values['-VIDEO_FILES-'].split(';')
|
|
1100
|
+
audio_files = values['-AUDIO_FILES-'].split(';')
|
|
1101
|
+
combine_gui(video_files, audio_files, config_path)
|
|
1102
|
+
if event == 'Settings':
|
|
1103
|
+
window.disable()
|
|
1104
|
+
settings_gui(config_path)
|
|
1105
|
+
window.enable()
|
|
1106
|
+
if event == sg.WIN_CLOSED:
|
|
1107
|
+
break
|
|
1108
|
+
window.close()
|
|
860
1109
|
|
|
861
1110
|
# Entry point for command line interaction, for example:
|
|
862
1111
|
# > describealign video.mp4 audio_desc.mp3
|
|
@@ -866,10 +1115,11 @@ def command_line_interface():
|
|
|
866
1115
|
class ArgumentParser(argparse.ArgumentParser):
|
|
867
1116
|
def error(self, message):
|
|
868
1117
|
if 'required: video, audio' in message:
|
|
869
|
-
print('No input arguments detected,
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1118
|
+
print('No input arguments detected, starting GUI...')
|
|
1119
|
+
main_gui()
|
|
1120
|
+
self.exit()
|
|
1121
|
+
else:
|
|
1122
|
+
self.exit(2, f'{self.prog}: error: {message}\n')
|
|
873
1123
|
parser = ArgumentParser(description="Replaces a video's sound with an audio description.",
|
|
874
1124
|
usage="describealign video_file.mp4 audio_file.mp3")
|
|
875
1125
|
parser.add_argument("video", help='A video file or directory containing video files.')
|
|
@@ -878,15 +1128,17 @@ def command_line_interface():
|
|
|
878
1128
|
help='Lower values make the alignment more accurate when there are skips ' + \
|
|
879
1129
|
'(e.g. describer pauses), but also make it more likely to misalign. ' + \
|
|
880
1130
|
'Default is 50.')
|
|
881
|
-
parser.add_argument('--
|
|
882
|
-
help='Stretches the input
|
|
883
|
-
'Default is to stretch the
|
|
1131
|
+
parser.add_argument('--stretch_audio', action='store_true',
|
|
1132
|
+
help='Stretches the input audio to fit the input video. ' + \
|
|
1133
|
+
'Default is to stretch the video to fit the audio.')
|
|
884
1134
|
parser.add_argument('--keep_non_ad', action='store_true',
|
|
885
1135
|
help='Tries to only replace segments with audio description. Useful if ' + \
|
|
886
|
-
'video\'s audio quality is better. Default is to replace all aligned audio.'
|
|
1136
|
+
'video\'s audio quality is better. Default is to replace all aligned audio. ' + \
|
|
1137
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')
|
|
887
1138
|
parser.add_argument('--boost', type=float, default=0,
|
|
888
1139
|
help='Boost (or quieten) description volume. Units are decibels (dB), so ' + \
|
|
889
|
-
'-3 makes the describer about 2x quieter, while 3 makes them 2x louder.'
|
|
1140
|
+
'-3 makes the describer about 2x quieter, while 3 makes them 2x louder. ' + \
|
|
1141
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')
|
|
890
1142
|
parser.add_argument('--ad_detect_sensitivity', type=float, default=.6,
|
|
891
1143
|
help='Audio description detection sensitivity ratio. Higher values make ' + \
|
|
892
1144
|
'--keep_non_ad more likely to replace aligned audio. Default is 0.6')
|
|
@@ -897,12 +1149,17 @@ def command_line_interface():
|
|
|
897
1149
|
help='Auto-skips user prompts asking to verify information.')
|
|
898
1150
|
parser.add_argument("--prepend", default="ad_", help='Output file name prepend text. Default is "ad_"')
|
|
899
1151
|
parser.add_argument('--no_pitch_correction', action='store_true',
|
|
900
|
-
help='Skips pitch correction step when stretching audio.'
|
|
1152
|
+
help='Skips pitch correction step when stretching audio. ' + \
|
|
1153
|
+
'Requires --stretch_audio to be set, otherwise does nothing.')
|
|
1154
|
+
parser.add_argument("--output_dir", default="videos_with_ad",
|
|
1155
|
+
help='Directory combined output media is saved to. Default is "videos_with_ad"')
|
|
1156
|
+
parser.add_argument("--alignment_dir", default="alignment_plots",
|
|
1157
|
+
help='Directory alignment data and plots are saved to. Default is "alignment_plots"')
|
|
901
1158
|
args = parser.parse_args()
|
|
902
1159
|
|
|
903
|
-
combine(args.video, args.audio, args.smoothness, args.
|
|
1160
|
+
combine(args.video, args.audio, args.smoothness, args.stretch_audio, args.keep_non_ad,
|
|
904
1161
|
args.boost, args.ad_detect_sensitivity, args.boost_sensitivity, args.yes,
|
|
905
|
-
args.prepend, args.no_pitch_correction)
|
|
1162
|
+
args.prepend, args.no_pitch_correction, args.output_dir, args.alignment_dir)
|
|
906
1163
|
|
|
907
1164
|
# allows the script to be run on its own, rather than through the package, for example:
|
|
908
1165
|
# python3 describealign.py video.mp4 audio_desc.mp3
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
describealign.py,sha256=RzIjyzZG8VBGdLHU00byGwJREq2dRSzcPbtRquzV43g,49422
|
|
2
|
-
describealign-0.1.8.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
3
|
-
describealign-0.1.8.dist-info/METADATA,sha256=SqkE8NrnfZMnVRHVaJq8gJmKX-69zg8wHeumku-mf90,1055
|
|
4
|
-
describealign-0.1.8.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
5
|
-
describealign-0.1.8.dist-info/entry_points.txt,sha256=7o7N6v3r4vFIH_XBdgk7WWhr-vZ_YitY8JWMdzN5xU0,71
|
|
6
|
-
describealign-0.1.8.dist-info/top_level.txt,sha256=VYHWy4TeimBAF5BQAuDj4adGdLaWs2AoYx6qQjGPJ4M,14
|
|
7
|
-
describealign-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|