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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: describealign
3
- Version: 0.1.8
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.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', 'mkv', 'm4v', 'flv', 'vob'])
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
- def ensure_folders_exist(dirs):
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
- print("Directory not found, creating it:", dir)
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 = os.path.abspath(path)
81
- if os.path.isdir(path):
82
- files = glob.glob(glob.escape(path) + "/*")
83
- if len(files) == 0:
84
- raise RuntimeError(f"Empty input directory:\n {path}")
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
- if not os.path.isfile(path):
87
- raise RuntimeError(f"No file or directory found at input path:\n {path}")
88
- files = [path]
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, stretch_video=False, keep_non_ad=False,
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([OUTPUT_DIR])
808
+ ensure_folders_exist([output_dir], display_func)
777
809
  if PLOT_ALIGNMENT_TO_FILE:
778
- ensure_folders_exist([PLOT_DIR])
810
+ ensure_folders_exist([alignment_dir], display_func)
779
811
 
780
- print("")
812
+ display("", display_func)
781
813
  for (video_file, audio_desc_file) in zip(video_files, audio_desc_files):
782
- print(os.path.split(video_file)[1])
783
- print(os.path.split(audio_desc_file)[1])
784
- print("")
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
- print("Processing files:")
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(OUTPUT_DIR, prepend + os.path.split(video_file)[1])
795
- print(" ", output_filename)
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
- print(" ", "output file already exists, skipping...")
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 stretch_video:
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(PLOT_DIR, os.path.splitext(os.path.split(video_file)[1])[0])
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, did you accidentally run the binary directly?')
870
- print('describealign must be run from command line (e.g. command prompt, terminal)')
871
- input("Press Enter to exit...")
872
- self.exit(2, f'{self.prog}: error: {message}\n')
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('--stretch_video', action='store_true',
882
- help='Stretches the input video to fit the audio description. ' + \
883
- 'Default is to stretch the audio to fit the video.')
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.stretch_video, args.keep_non_ad,
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,,