def main(): options = [] option_names = [] def add_argument(*names, nargs=1, type=str, default=None, action='default', range=None, choices=None, help='', extra=''): nonlocal options nonlocal option_names newDic = {} newDic['names'] = names newDic['nargs'] = nargs newDic['type'] = type newDic['default'] = default newDic['action'] = action newDic['help'] = help newDic['extra'] = extra newDic['range'] = range newDic['choices'] = choices options.append(newDic) option_names = option_names + list(names) add_argument('(input)', nargs='*', help='the path to a file, folder, or url you want edited.') add_argument('--help', '-h', action='store_true', help='print this message and exit.') add_argument( '--frame_margin', '-m', type=int, default=6, range='0 to Infinity', help= 'set how many "silent" frames of on either side of "loud" sections be included.' ) add_argument( '--silent_threshold', '-t', type=float_type, default=0.04, range='0 to 1', help='set the volume that frames audio needs to surpass to be "loud".') add_argument( '--video_speed', '--sounded_speed', '-v', type=float_type, default=1.00, range='0 to 999999', help='set the speed that "loud" sections should be played at.') add_argument( '--silent_speed', '-s', type=float_type, default=99999, range='0 to 99999', help='set the speed that "silent" sections should be played at.') add_argument('--output_file', '-o', nargs='*', help='set the name(s) of the new output.') add_argument('--no_open', action='store_true', help='do not open the file after editing is done.') add_argument( '--min_clip_length', '-mclip', type=int, default=3, range='0 to Infinity', help= 'set the minimum length a clip can be. If a clip is too short, cut it.' ) add_argument( '--min_cut_length', '-mcut', type=int, default=6, range='0 to Infinity', help= "set the minimum length a cut can be. If a cut is too short, don't cut" ) add_argument('--combine_files', action='store_true', help='combine all input files into one before editing.') add_argument('--preview', action='store_true', help='show stats on how the input will be cut.') add_argument( '--cut_by_this_audio', '-ca', type=file_type, help="base cuts by this audio file instead of the video's audio.") add_argument('--cut_by_this_track', '-ct', type=int, default=0, range='0 to the number of audio tracks minus one', help='base cuts by a different audio track in the video.') add_argument('--cut_by_all_tracks', '-cat', action='store_true', help='combine all audio tracks into one before basing cuts.') add_argument('--keep_tracks_seperate', action='store_true', help="don't combine audio tracks when exporting.") add_argument( '--my_ffmpeg', action='store_true', help='use your ffmpeg and other binaries instead of the ones packaged.' ) add_argument('--version', action='store_true', help='show which auto-editor you have.') add_argument('--debug', '--verbose', action='store_true', help='show debugging messages and values.') add_argument('--show_ffmpeg_debug', action='store_true', help='show ffmpeg progress and output.') # TODO: add export_as_video add_argument('--export_as_audio', '-exa', action='store_true', help='export as a WAV audio file.') add_argument( '--export_to_premiere', '-exp', action='store_true', help= 'export as an XML file for Adobe Premiere Pro instead of outputting a media file.' ) add_argument( '--export_to_resolve', '-exr', action='store_true', help= 'export as an XML file for DaVinci Resolve instead of outputting a media file.' ) add_argument('--video_bitrate', '-vb', help='set the number of bits per second for video.') add_argument('--audio_bitrate', '-ab', help='set the number of bits per second for audio.') add_argument('--sample_rate', '-r', type=sample_rate_type, help='set the sample rate of the input and output videos.') add_argument('--video_codec', '-vcodec', default='uncompressed', help='set the video codec for the output media file.') add_argument( '--preset', '-p', default='medium', choices=[ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow' ], help= 'set the preset for ffmpeg to help save file size or increase quality.' ) add_argument('--tune', default='none', choices=[ 'film', 'animation', 'grain', 'stillimage', 'fastdecode', 'zerolatency', 'none' ], help='set the tune for ffmpeg to compress video better.') add_argument('--ignore', nargs='*', help='the range that will be marked as "loud"') add_argument('--cut_out', nargs='*', help='the range that will be marked as "silent"') add_argument('--motion_threshold', type=float_type, default=0.02, range='0 to 1', help='how much motion is required to be considered "moving"') add_argument('--edit_based_on', default='audio', choices=[ 'audio', 'motion', 'not_audio', 'not_motion', 'audio_or_motion', 'audio_and_motion', 'audio_xor_motion', 'audio_and_not_motion' ], help='decide which method to use when making edits.') dirPath = os.path.dirname(os.path.realpath(__file__)) # Fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) from usefulFunctions import Log audioExtensions = [ '.wav', '.mp3', '.m4a', '.aiff', '.flac', '.ogg', '.oga', '.acc', '.nfa', '.mka' ] # videoExtensions = ['.mp4', '.mkv', '.mov', '.webm', '.ogv'] invalidExtensions = [ '.txt', '.md', '.rtf', '.csv', '.cvs', '.html', '.htm', '.xml', '.json', '.yaml', '.png', '.jpeg', '.jpg', '.gif', '.exe', '.doc', '.docx', '.odt', '.pptx', '.xlsx', '.xls', 'ods', '.pdf', '.bat', '.dll', '.prproj', '.psd', '.aep', '.zip', '.rar', '.7z', '.java', '.class', '.js', '.c', '.cpp', '.csharp', '.py', '.app', '.git', '.github', '.gitignore', '.db', '.ini', '.BIN' ] class parse_options(): def __init__(self, userArgs, log, *args): # Set the default options. for options in args: for option in options: key = option['names'][0].replace('-', '') if (option['action'] == 'store_true'): value = False elif (option['nargs'] != 1): value = [] else: value = option['default'] setattr(self, key, value) def get_option(item, the_args: list): for options in the_args: for option in options: if (item in option['names']): return option return None # Figure out attributes changed by user. myList = [] settingInputs = True optionList = 'input' i = 0 while i < len(userArgs): item = userArgs[i] if (i == len(userArgs) - 1): nextItem = None else: nextItem = userArgs[i + 1] option = get_option(item, args) if (option is not None): if (optionList is not None): setattr(self, optionList, myList) settingInputs = False optionList = None myList = [] key = option['names'][0].replace('-', '') # Show help for specific option. if (nextItem == '-h' or nextItem == '--help'): print(' ', ', '.join(option['names'])) print(' ', option['help']) print(' ', option['extra']) if (option['action'] == 'default'): print(' type:', option['type'].__name__) print(' default:', option['default']) if (option['range'] is not None): print(' range:', option['range']) if (option['choices'] is not None): print(' choices:', ', '.join(option['choices'])) else: print(' type: flag') sys.exit() if (option['nargs'] != 1): settingInputs = True optionList = key elif (option['action'] == 'store_true'): value = True else: try: # Convert to correct type. value = option['type'](nextItem) except: typeName = option['type'].__name__ log.error( f'Couldn\'t convert "{nextItem}" to {typeName}' ) if (option['choices'] is not None): if (value not in option['choices']): optionName = option['names'][0] myChoices = ', '.join(option['choices']) log.error(f'{value} is not a choice for {optionName}' \ f'\nchoices are:\n {myChoices}') i += 1 setattr(self, key, value) else: if (settingInputs and not item.startswith('-')): # Input file names myList.append(item) else: # Unknown Option! hmm = difflib.get_close_matches(item, option_names) potential_options = ', '.join(hmm) append = '' if (hmm != []): append = f'\n\n Did you mean:\n {potential_options}' log.error(f'Unknown option: {item}{append}') i += 1 if (settingInputs): setattr(self, optionList, myList) args = parse_options(sys.argv[1:], Log(3), options) # Print the help screen for the entire program. if (args.help): print('') for option in options: print(' ', ', '.join(option['names']) + ':', option['help']) print('\nThe help command can also be used on a specific option.') print('example:') print(' auto-editor --frame_margin --help') print('\nHave an issue? Make an issue. '\ 'Visit https://github.com/wyattblue/auto-editor/issues') sys.exit() if (args.version): print('Auto-Editor version', version) sys.exit() from usefulFunctions import vidTracks, conwrite, getBinaries from wavfile import read if (not args.preview): if (args.export_to_premiere): conwrite('Exporting to Adobe Premiere Pro XML file.') elif (args.export_to_resolve): conwrite('Exporting to DaVinci Resolve XML file.') elif (args.export_as_audio): conwrite('Exporting as audio.') else: conwrite('Starting.') ffmpeg, ffprobe = getBinaries(platform.system(), dirPath, args.my_ffmpeg) makingDataFile = args.export_to_premiere or args.export_to_resolve is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if (args.debug and args.input == []): print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system(), platform.release()) # Platform can be 'Linux', 'Darwin' (macOS), 'Java', 'Windows' ffmpegVersion = pipeToConsole([ffmpeg, '-version']).split('\n')[0] ffmpegVersion = ffmpegVersion.replace('ffmpeg version', '').strip() ffmpegVersion = ffmpegVersion.split(' ')[0] print('FFmpeg path:', ffmpeg) print('FFmpeg version:', ffmpegVersion) print('Auto-Editor version', version) sys.exit() log = Log(args.debug, args.show_ffmpeg_debug) log.debug('') if (is64bit == '32-bit'): log.warning( 'You have the 32-bit version of Python, which may lead to memory crashes.' ) if (args.frame_margin < 0): log.error('Frame margin cannot be negative.') if (args.input == []): log.error( 'You need the (input) argument so that auto-editor can do the work for you.' ) try: from requests import get latestVersion = get( 'https://raw.githubusercontent.com/wyattblue/auto-editor/master/resources/version.txt' ) if (latestVersion.text != version): print('\nAuto-Editor is out of date. Run:\n') print(' pip3 install -U auto-editor') print('\nto upgrade to the latest version.\n') del latestVersion except Exception as err: log.debug('Check for update Error: ' + str(err)) if (args.silent_speed <= 0 or args.silent_speed > 99999): args.silent_speed = 99999 if (args.video_speed <= 0 or args.video_speed > 99999): args.video_speed = 99999 inputList = [] for myInput in args.input: if (os.path.isdir(myInput)): def validFiles(path: str, badExts: list): for f in os.listdir(path): if (not f[f.rfind('.'):] in badExts): yield os.path.join(path, f) inputList += sorted(validFiles(myInput, invalidExtensions)) elif (os.path.isfile(myInput)): inputList.append(myInput) elif (myInput.startswith('http://') or myInput.startswith('https://')): basename = re.sub(r'\W+', '-', myInput) if (not os.path.isfile(basename + '.mp4')): print( 'URL detected, using youtube-dl to download from webpage.') cmd = [ 'youtube-dl', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', myInput, '--output', basename, '--no-check-certificate' ] if (ffmpeg != 'ffmpeg'): cmd.extend(['--ffmpeg-location', ffmpeg]) subprocess.call(cmd) inputList.append(basename + '.mp4') else: log.error('Could not find file: ' + myInput) startTime = time.time() if (args.output_file is None): args.output_file = [] # Figure out the output file names. if (len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): oldFile = inputList[i] dotIndex = oldFile.rfind('.') if (args.export_to_premiere or args.export_to_resolve): args.output_file.append(oldFile[:dotIndex] + '.xml') else: ext = oldFile[dotIndex:] if (args.export_as_audio): ext = '.wav' end = '_ALTERED' + ext args.output_file.append(oldFile[:dotIndex] + end) TEMP = tempfile.mkdtemp() log.debug(f'\n - Temp Directory: {TEMP}') if (args.combine_files): # Combine video files, then set input to 'combined.mp4'. cmd = [ffmpeg, '-y'] for fileref in inputList: cmd.extend(['-i', fileref]) cmd.extend([ '-filter_complex', f'[0:v]concat=n={len(inputList)}:v=1:a=1', '-codec:v', 'h264', '-pix_fmt', 'yuv420p', '-strict', '-2', f'{TEMP}/combined.mp4' ]) if (log.ffmpeg): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '8']) subprocess.call(cmd) inputList = [f'{TEMP}/combined.mp4'] speeds = [args.silent_speed, args.video_speed] numCuts = 0 for i, INPUT_FILE in enumerate(inputList): log.debug(f' - INPUT_FILE: {INPUT_FILE}') # Ignore folders if (os.path.isdir(INPUT_FILE)): continue # Throw error if file referenced doesn't exist. if (not os.path.isfile(INPUT_FILE)): log.error(f"{INPUT_FILE} doesn't exist!") # Check if the file format is valid. fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] if (fileFormat in invalidExtensions): log.error( f'Invalid file extension "{fileFormat}" for {INPUT_FILE}') audioFile = fileFormat in audioExtensions # Get output file name. newOutput = args.output_file[i] log.debug(f' - newOutput: {newOutput}') # Grab the sample rate from the input. sr = args.sample_rate if (sr is None): output = pipeToConsole([ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'\s(?P<grp>\w+?)\sHz', output).groupdict() sr = matchDict['grp'] except AttributeError: sr = 48000 args.sample_rate = sr # Grab the audio bitrate from the input. abit = args.audio_bitrate if (abit is None): if (not INPUT_FILE.endswith('.mkv')): output = pipeToConsole([ ffprobe, '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'stream=bit_rate', '-of', 'compact=p=0:nk=1', INPUT_FILE ]) try: abit = int(output) except: log.warning("Couldn't automatically detect audio bitrate.") abit = '500k' log.debug('Setting audio bitrate to ' + abit) else: abit = str(round(abit / 1000)) + 'k' else: abit = str(abit) args.audio_bitrate = abit if (audioFile): fps = 30 # Audio files don't have frames, so give fps a dummy value. tracks = 1 cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-b:a', args.audio_bitrate, '-ac', '2', '-ar', str(args.sample_rate), '-vn', f'{TEMP}/fastAud.wav' ] if (log.is_ffmpeg): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '8']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/fastAud.wav') else: if (args.export_to_premiere): # This is the default fps value for Premiere Pro Projects. fps = 29.97 else: # Grab fps to know what the output video's fps should be. # DaVinci Resolve doesn't need fps, but grab it away just in case. fps = ffmpegFPS(ffmpeg, INPUT_FILE, log) tracks = vidTracks(INPUT_FILE, ffprobe, log) if (args.cut_by_this_track >= tracks): log.error("You choose a track that doesn't exist.\n" \ f'There are only {tracks-1} tracks. (starting from 0)') vcodec = args.video_codec if (vcodec == 'copy'): output = pipeToConsole( [ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'Video:\s(?P<video>\w+?)\s', output).groupdict() vcodec = matchDict['video'] log.debug(vcodec) except AttributeError: vcodec = 'uncompressed' log.warning("Couldn't automatically detect video codec.") if (args.video_bitrate is not None and vcodec == 'uncompressed'): log.warning('Your bitrate will not be applied because' \ ' the video codec is "uncompressed".') if (vcodec == 'uncompressed'): # FFmpeg copies the uncompressed output that cv2 spits out. vcodec = 'copy' # Split audio tracks into: 0.wav, 1.wav, etc. for trackNum in range(tracks): cmd = [ffmpeg, '-y', '-i', INPUT_FILE] if (args.audio_bitrate is not None): cmd.extend(['-ab', args.audio_bitrate]) cmd.extend([ '-ac', '2', '-ar', str(args.sample_rate), '-map', f'0:a:{trackNum}', f'{TEMP}/{trackNum}.wav' ]) if (log.is_ffmpeg): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '8']) subprocess.call(cmd) # Check if the `--cut_by_all_tracks` flag has been set or not. if (args.cut_by_all_tracks): # Combine all audio tracks into one audio file, then read. cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter_complex', f'[0:a]amerge=inputs={tracks}', '-map', 'a', '-ar', str(args.sample_rate), '-ac', '2', '-f', 'wav', f'{TEMP}/combined.wav' ] if (log.is_ffmpeg): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '8']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/combined.wav') else: # Read only one audio file. if (os.path.isfile(f'{TEMP}/{args.cut_by_this_track}.wav')): sampleRate, audioData = read( f'{TEMP}/{args.cut_by_this_track}.wav') else: log.error('Audio track not found!') from cutting import audioToHasLoud, motionDetection, applySpacingRules import numpy as np audioList = None motionList = None if ('audio' in args.edit_based_on): log.debug('Analyzing audio volume.') audioList = audioToHasLoud(audioData, sampleRate, args.silent_threshold, fps, log) if ('motion' in args.edit_based_on): log.debug('Analyzing video motion.') motionList = motionDetection(INPUT_FILE, ffprobe, args.motion_threshold, log, width=400, dilates=2, blur=21) if (audioList is not None): if (len(audioList) > len(motionList)): log.debug( 'Reducing the size of audioList to match motionList') log.debug(f'audioList Length: {len(audioList)}') log.debug(f'motionList Length: {len(motionList)}') audioList = audioList[:len(motionList)] if (args.edit_based_on == 'audio' or args.edit_based_on == 'not_audio'): if (max(audioList) == 0): log.error( 'There was no place where audio exceeded the threshold.') if (args.edit_based_on == 'motion' or args.edit_based_on == 'not_motion'): if (max(motionList) == 0): log.error( 'There was no place where motion exceeded the threshold.') # Only raise a warning for other cases. if (audioList is not None and max(audioList) == 0): log.warning( 'There was no place where audio exceeded the threshold.') if (motionList is not None and max(motionList) == 0): log.warning( 'There was no place where motion exceeded the threshold.') if (args.edit_based_on == 'audio'): hasLoud = audioList if (args.edit_based_on == 'motion'): hasLoud = motionList if (args.edit_based_on == 'not_audio'): hasLoud = np.invert(audioList) if (args.edit_based_on == 'not_motion'): hasLoud = np.invert(motionList) if (args.edit_based_on == 'audio_and_motion'): log.debug('Applying "Python bitwise and" on arrays.') hasLoud = audioList & motionList if (args.edit_based_on == 'audio_or_motion'): log.debug('Applying "Python bitwise or" on arrays.') hasLoud = audioList | motionList if (args.edit_based_on == 'audio_xor_motion'): log.debug('Applying "numpy bitwise_xor" on arrays') hasLoud = np.bitwise_xor(audioList, motionList) if (args.edit_based_on == 'audio_and_not_motion'): log.debug( 'Applying "Python bitwise and" with "numpy bitwise not" on arrays.' ) hasLoud = audioList & np.invert(motionList) chunks, includeFrame = applySpacingRules( hasLoud, fps, args.frame_margin, args.min_clip_length, args.min_cut_length, args.ignore, args.cut_out, log) clips = [] for chunk in chunks: if (speeds[chunk[2]] == 99999): numCuts += 1 else: clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) if (fps is None and not audioFile): if (makingDataFile): dotIndex = INPUT_FILE.rfind('.') end = '_constantFPS' + oldFile[dotIndex:] constantLoc = oldFile[:dotIndex] + end else: constantLoc = f'{TEMP}/constantVid{fileFormat}' cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter:v', 'fps=fps=30', constantLoc ] if (log.is_ffmpeg): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '8']) subprocess.call(cmd) INPUT_FILE = constantLoc if (args.preview): args.no_open = True from preview import preview preview(INPUT_FILE, chunks, speeds, fps, audioFile, log) continue if (args.export_to_premiere): args.no_open = True from premiere import exportToPremiere exportToPremiere(INPUT_FILE, TEMP, newOutput, clips, tracks, sampleRate, audioFile, log) continue if (args.export_to_resolve): args.no_open = True duration = chunks[len(chunks) - 1][1] from resolve import exportToResolve exportToResolve(INPUT_FILE, newOutput, clips, duration, sampleRate, audioFile, log) continue if (audioFile and not makingDataFile): from fastAudio import fastAudio fastAudio(ffmpeg, INPUT_FILE, newOutput, chunks, speeds, args.audio_bitrate, sampleRate, True, TEMP, log, fps) continue from fastVideo import fastVideo fastVideo(ffmpeg, INPUT_FILE, newOutput, chunks, includeFrame, speeds, tracks, args.audio_bitrate, sampleRate, TEMP, args.keep_tracks_seperate, vcodec, fps, args.export_as_audio, args.video_bitrate, args.preset, args.tune, log) if (not os.path.isfile(newOutput)): log.error(f'The file {newOutput} was not created.') if (not args.preview and not makingDataFile): timeLength = round(time.time() - startTime, 2) minutes = timedelta(seconds=round(timeLength)) print(f'Finished. took {timeLength} seconds ({minutes})') if (not args.preview and makingDataFile): timeSave = numCuts * 2 # assuming making each cut takes about 2 seconds. units = 'seconds' if (timeSave >= 3600): timeSave = round(timeSave / 3600, 1) if (timeSave % 1 == 0): timeSave = round(timeSave) units = 'hours' if (timeSave >= 60): timeSave = round(timeSave / 60, 1) if (timeSave >= 10 or timeSave % 1 == 0): timeSave = round(timeSave) units = 'minutes' plural = 's' if numCuts != 1 else '' print(f'Auto-Editor made {numCuts} cut{plural}', end='') if (numCuts > 4): print(f', which would have taken about {timeSave} {units} if' \ ' edited manually.') else: print('.') if (not args.no_open): try: # should work on Windows os.startfile(newOutput) except AttributeError: try: # should work on MacOS and most Linux versions subprocess.call(['open', newOutput]) except: try: # should work on WSL2 subprocess.call(['cmd.exe', '/C', 'start', newOutput]) except: log.warning('Could not open output file.') rmtree(TEMP)
def main(): dirPath = os.path.dirname(os.path.realpath(__file__)) # Fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) # Print the version if only the -v option is added. if (sys.argv[1:] == ['-v'] or sys.argv[1:] == ['-V']): print(f'Auto-Editor version {version}\nPlease use --version instead.') sys.exit() # If the users just runs: $ auto-editor if (sys.argv[1:] == []): # Print print( '\nAuto-Editor is an automatic video/audio creator and editor.\n') print( 'By default, it will detect silence and create a new video with ') print( 'those sections cut out. By changing some of the options, you can') print( 'export to a traditional editor like Premiere Pro and adjust the') print( 'edits there, adjust the pacing of the cuts, and change the method' ) print('of editing like using audio loudness and video motion to judge') print('making cuts.') print( '\nRun:\n auto-editor --help\n\nTo get the list of options.\n') sys.exit() from vanparse import ParseOptions from usefulFunctions import Log, Timer if (len(sys.argv) > 1 and sys.argv[1] == 'generate_test'): option_data = generate_options() args = ParseOptions(sys.argv[2:], Log(), 'generate_test', option_data) if (args.help): genHelp(option_data) sys.exit() from generateTestMedia import generateTestMedia from usefulFunctions import FFmpeg ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, Log()) generateTestMedia(ffmpeg, args.output_file, args.fps, args.duration, args.width, args.height) sys.exit() elif (len(sys.argv) > 1 and sys.argv[1] == 'test'): from testAutoEditor import testAutoEditor testAutoEditor() sys.exit() elif (len(sys.argv) > 1 and sys.argv[1] == 'info'): option_data = info_options() args = ParseOptions(sys.argv[2:], Log(), 'info', option_data) if (args.help): genHelp(option_data) sys.exit() from info import getInfo from usefulFunctions import FFmpeg, FFprobe log = Log() ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, log) ffprobe = FFprobe(dirPath, args.my_ffmpeg, log) getInfo(args.input, ffmpeg, ffprobe, log) sys.exit() else: option_data = main_options() args = ParseOptions(sys.argv[1:], Log(True), 'auto-editor', option_data) timer = Timer(args.quiet) # Print the help screen for the entire program. if (args.help): print('\n Have an issue? Make an issue. '\ 'Visit https://github.com/wyattblue/auto-editor/issues\n') print(' The help option can also be used on a specific option:') print(' auto-editor --frame_margin --help\n') genHelp(option_data) sys.exit() del option_data from usefulFunctions import FFmpeg, FFprobe, sep ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, Log()) ffprobe = FFprobe(dirPath, args.my_ffmpeg, Log()) makingDataFile = (args.export_to_premiere or args.export_to_resolve or args.export_to_final_cut_pro or args.export_as_json) is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if (args.debug and args.input == []): import platform print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system(), platform.release()) print('Config File path:', dirPath + sep() + 'config.txt') print('FFmpeg path:', ffmpeg.getPath()) ffmpegVersion = ffmpeg.pipe(['-version']).split('\n')[0] ffmpegVersion = ffmpegVersion.replace('ffmpeg version', '').strip() ffmpegVersion = ffmpegVersion.split(' ')[0] print('FFmpeg version:', ffmpegVersion) print('Auto-Editor version', version) sys.exit() if (is64bit == '32-bit'): log.warning('You have the 32-bit version of Python, which may lead to' \ 'memory crashes.') if (args.version): print('Auto-Editor version', version) sys.exit() TEMP = tempfile.mkdtemp() log = Log(args.debug, args.show_ffmpeg_debug, args.quiet, temp=TEMP) log.debug(f'\n - Temp Directory: {TEMP}') ffmpeg.updateLog(log) ffprobe.updateLog(log) from wavfile import read from usefulFunctions import isLatestVersion if (not args.quiet and isLatestVersion(version, log)): log.print('\nAuto-Editor is out of date. Run:\n') log.print(' pip3 install -U auto-editor') log.print('\nto upgrade to the latest version.\n') from argsCheck import hardArgsCheck, softArgsCheck hardArgsCheck(args, log) args = softArgsCheck(args, log) from validateInput import validInput inputList = validInput(args.input, ffmpeg, args, log) # Figure out the output file names. def newOutputName(oldFile: str, exa=False, data=False, exc=False) -> str: dotIndex = oldFile.rfind('.') if (exc): return oldFile[:dotIndex] + '.json' elif (data): return oldFile[:dotIndex] + '.xml' ext = oldFile[dotIndex:] if (exa): ext = '.wav' return oldFile[:dotIndex] + '_ALTERED' + ext if (len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): args.output_file.append( newOutputName(inputList[i], args.export_as_audio, makingDataFile, args.export_as_json)) if (args.combine_files): # Combine video files, then set input to 'combined.mp4'. cmd = [] for fileref in inputList: cmd.extend(['-i', fileref]) cmd.extend([ '-filter_complex', f'[0:v]concat=n={len(inputList)}:v=1:a=1', '-codec:v', 'h264', '-pix_fmt', 'yuv420p', '-strict', '-2', f'{TEMP}{sep()}combined.mp4' ]) ffmpeg.run(cmd) del cmd inputList = [f'{TEMP}{sep()}combined.mp4'] speeds = [args.silent_speed, args.video_speed] log.debug(f' - Speeds: {speeds}') audioExtensions = [ '.wav', '.mp3', '.m4a', '.aiff', '.flac', '.ogg', '.oga', '.acc', '.nfa', '.mka' ] # videoExtensions = ['.mp4', '.mkv', '.mov', '.webm', '.ogv'] for i, INPUT_FILE in enumerate(inputList): fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] chunks = None if (fileFormat == '.json'): log.debug('Reading .json file') from makeCutList import readCutList INPUT_FILE, chunks, speeds = readCutList(INPUT_FILE, version, log) newOutput = newOutputName(INPUT_FILE, args.export_as_audio, makingDataFile, False) fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] else: newOutput = args.output_file[i] log.debug(f' - INPUT_FILE: {INPUT_FILE}') log.debug(f' - newOutput: {newOutput}') if (os.path.isfile(newOutput) and INPUT_FILE != newOutput): log.debug(f' Removing already existing file: {newOutput}') os.remove(newOutput) if (args.sample_rate is None): sampleRate = ffprobe.getSampleRate(INPUT_FILE) if (sampleRate == 'N/A'): sampleRate = '48000' log.warning( f"Samplerate couldn't be detected, using {sampleRate}.") else: sampleRate = str(args.sample_rate) log.debug(f' - sampleRate: {sampleRate}') if (args.audio_bitrate is None): if (INPUT_FILE.endswith('.mkv')): # audio bitrate not supported in the mkv container. audioBitrate = None else: audioBitrate = ffprobe.getPrettyABitrate(INPUT_FILE) if (audioBitrate == 'N/A'): log.warning("Couldn't automatically detect audio bitrate.") audioBitrate = None else: audioBitrate = args.audio_bitrate log.debug(f' - audioBitrate: {audioBitrate}') audioFile = fileFormat in audioExtensions if (audioFile): if (args.force_fps_to is None): fps = 30 # Audio files don't have frames, so give fps a dummy value. else: fps = args.force_fps_to if (args.force_tracks_to is None): tracks = 1 else: tracks = args.force_tracks_to cmd = ['-i', INPUT_FILE] if (audioBitrate is not None): cmd.extend(['-b:a', audioBitrate]) cmd.extend([ '-ac', '2', '-ar', sampleRate, '-vn', f'{TEMP}{sep()}fastAud.wav' ]) ffmpeg.run(cmd) del cmd sampleRate, audioData = read(f'{TEMP}{sep()}fastAud.wav') else: if (args.force_fps_to is not None): fps = args.force_fps_to elif (args.export_to_premiere or args.export_to_final_cut_pro or args.export_to_resolve): # Based on timebase. fps = int(ffprobe.getFrameRate(INPUT_FILE)) else: fps = ffprobe.getFrameRate(INPUT_FILE) log.debug(f'Frame rate: {fps}') tracks = args.force_tracks_to if (tracks is None): tracks = ffprobe.getAudioTracks(INPUT_FILE) if (args.cut_by_this_track >= tracks): allTracks = '' for trackNum in range(tracks): allTracks += f'Track {trackNum}\n' if (tracks == 1): message = f'is only {tracks} track' else: message = f'are only {tracks} tracks' log.error("You choose a track that doesn't exist.\n" \ f'There {message}.\n {allTracks}') # Split audio tracks into: 0.wav, 1.wav, etc. for trackNum in range(tracks): cmd = ['-i', INPUT_FILE] if (audioBitrate is not None): cmd.extend(['-ab', audioBitrate]) cmd.extend([ '-ac', '2', '-ar', sampleRate, '-map', f'0:a:{trackNum}', f'{TEMP}{sep()}{trackNum}.wav' ]) ffmpeg.run(cmd) del cmd # Check if the `--cut_by_all_tracks` flag has been set or not. if (args.cut_by_all_tracks): # Combine all audio tracks into one audio file, then read. cmd = [ '-i', INPUT_FILE, '-filter_complex', f'[0:a]amix=inputs={tracks}:duration=longest', '-ar', sampleRate, '-ac', '2', '-f', 'wav', f'{TEMP}{sep()}combined.wav' ] ffmpeg.run(cmd) sampleRate, audioData = read(f'{TEMP}{sep()}combined.wav') del cmd else: # Read only one audio file. if (os.path.isfile( f'{TEMP}{sep()}{args.cut_by_this_track}.wav')): sampleRate, audioData = read( f'{TEMP}{sep()}{args.cut_by_this_track}.wav') else: log.bug('Audio track not found!') log.debug(f' - Frame Rate: {fps}') if (chunks is None): from cutting import audioToHasLoud, motionDetection audioList = None motionList = None if ('audio' in args.edit_based_on): log.debug('Analyzing audio volume.') audioList = audioToHasLoud(audioData, sampleRate, args.silent_threshold, fps, log) if ('motion' in args.edit_based_on): log.debug('Analyzing video motion.') motionList = motionDetection(INPUT_FILE, ffprobe, args.motion_threshold, log, width=args.width, dilates=args.dilates, blur=args.blur) if (audioList is not None): if (len(audioList) != len(motionList)): log.debug(f'audioList Length: {len(audioList)}') log.debug(f'motionList Length: {len(motionList)}') if (len(audioList) > len(motionList)): log.debug( 'Reducing the size of audioList to match motionList.' ) audioList = audioList[:len(motionList)] elif (len(motionList) > len(audioList)): log.debug( 'Reducing the size of motionList to match audioList.' ) motionList = motionList[:len(audioList)] from cutting import combineArrs, applySpacingRules hasLoud = combineArrs(audioList, motionList, args.edit_based_on, log) del audioList, motionList chunks = applySpacingRules(hasLoud, fps, args.frame_margin, args.min_clip_length, args.min_cut_length, args.ignore, args.cut_out, log) del hasLoud clips = [] numCuts = len(chunks) for chunk in chunks: if (speeds[chunk[2]] != 99999): clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) if (fps is None and not audioFile): if (makingDataFile): dotIndex = INPUT_FILE.rfind('.') end = '_constantFPS' + INPUT_FILE[dotIndex:] constantLoc = INPUT_FILE[:dotIndex] + end else: constantLoc = f'{TEMP}{sep()}constantVid{fileFormat}' ffmpeg.run( ['-i', INPUT_FILE, '-filter:v', 'fps=fps=30', constantLoc]) INPUT_FILE = constantLoc if (args.export_as_json): from makeCutList import makeCutList makeCutList(INPUT_FILE, newOutput, version, chunks, speeds, log) continue if (args.preview): newOutput = None from preview import preview preview(INPUT_FILE, chunks, speeds, fps, audioFile, log) continue if (args.export_to_premiere or args.export_to_resolve): from editor import editorXML editorXML(INPUT_FILE, TEMP, newOutput, clips, chunks, tracks, sampleRate, audioFile, args.export_to_resolve, fps, log) continue if (audioFile): from fastAudio import fastAudio, handleAudio, convertAudio theFile = handleAudio(ffmpeg, INPUT_FILE, audioBitrate, str(sampleRate), TEMP, log) fastAudio(theFile, f'{TEMP}{sep()}convert.wav', chunks, speeds, log, fps, args.machine_readable_progress, args.no_progress) convertAudio(ffmpeg, ffprobe, f'{TEMP}{sep()}convert.wav', INPUT_FILE, newOutput, args, log) continue from videoUtils import handleAudioTracks, muxVideo continueVid = handleAudioTracks(ffmpeg, newOutput, args, tracks, chunks, speeds, fps, TEMP, log) if (continueVid): if (args.render == 'auto'): try: import av args.render = 'av' except ImportError: args.render = 'opencv' log.debug(f'Using {args.render} method') if (args.render == 'av'): from renderVideo import renderAv renderAv(ffmpeg, INPUT_FILE, args, chunks, speeds, TEMP, log) if (args.render == 'opencv'): from renderVideo import renderOpencv renderOpencv(ffmpeg, INPUT_FILE, args, chunks, speeds, fps, TEMP, log) # Now mix new audio(s) and the new video. muxVideo(ffmpeg, newOutput, args, tracks, TEMP, log) if (newOutput is not None and not os.path.isfile(newOutput)): log.bug(f'The file {newOutput} was not created.') if (not args.preview and not makingDataFile): timer.stop() if (not args.preview and makingDataFile): from usefulFunctions import humanReadableTime # Assume making each cut takes about 30 seconds. timeSave = humanReadableTime(numCuts * 30) s = 's' if numCuts != 1 else '' log.print(f'Auto-Editor made {numCuts} cut{s}', end='') log.print( f', which would have taken about {timeSave} if edited manually.') if (not args.no_open): from usefulFunctions import smartOpen smartOpen(newOutput, log) log.debug('Deleting temp dir') rmtree(TEMP)
def main(): parser = argparse.ArgumentParser(prog='Auto-Editor', usage='auto-editor [input] [options]') basic = parser.add_argument_group('Basic Options') basic.add_argument('input', nargs='*', help='the path to the file(s), folder, or url you want edited.') basic.add_argument('--frame_margin', '-m', type=int, default=6, metavar='6', help='set how many "silent" frames of on either side of "loud" sections be included.') basic.add_argument('--silent_threshold', '-t', type=float_type, default=0.04, metavar='0.04', help='set the volume that frames audio needs to surpass to be "loud". (0-1)') basic.add_argument('--video_speed', '--sounded_speed', '-v', type=float_type, default=1.00, metavar='1', help='set the speed that "loud" sections should be played at.') basic.add_argument('--silent_speed', '-s', type=float_type, default=99999, metavar='99999', help='set the speed that "silent" sections should be played at.') basic.add_argument('--output_file', '-o', nargs='*', metavar='', help='set the name(s) of the new output.') advance = parser.add_argument_group('Advanced Options') advance.add_argument('--no_open', action='store_true', help='do not open the file after editing is done.') advance.add_argument('--min_clip_length', '-mclip', type=int, default=3, metavar='3', help='set the minimum length a clip can be. If a clip is too short, cut it.') advance.add_argument('--min_cut_length', '-mcut', type=int, default=6, metavar='6', help="set the minimum length a cut can be. If a cut is too short, don't cut") advance.add_argument('--combine_files', action='store_true', help='combine all input files into one before editing.') advance.add_argument('--preview', action='store_true', help='show stats on how the input will be cut.') cutting = parser.add_argument_group('Cutting Options') cutting.add_argument('--cut_by_this_audio', '-ca', type=file_type, metavar='', help="base cuts by this audio file instead of the video's audio.") cutting.add_argument('--cut_by_this_track', '-ct', type=int, default=0, metavar='0', help='base cuts by a different audio track in the video.') cutting.add_argument('--cut_by_all_tracks', '-cat', action='store_true', help='combine all audio tracks into one before basing cuts.') cutting.add_argument('--keep_tracks_seperate', action='store_true', help="don't combine audio tracks when exporting.") debug = parser.add_argument_group('Developer/Debugging Options') debug.add_argument('--my_ffmpeg', action='store_true', help='use your ffmpeg and other binaries instead of the ones packaged.') debug.add_argument('--version', action='store_true', help='show which auto-editor you have.') debug.add_argument('--debug', '--verbose', action='store_true', help='show helpful debugging values.') misc = parser.add_argument_group('Export Options') misc.add_argument('--export_as_audio', '-exa', action='store_true', help='export as a WAV audio file.') misc.add_argument('--export_to_premiere', '-exp', action='store_true', help='export as an XML file for Adobe Premiere Pro instead of outputting a media file.') misc.add_argument('--export_to_resolve', '-exr', action='store_true', help='export as an XML file for DaVinci Resolve instead of outputting a media file.') size = parser.add_argument_group('Size Options') size.add_argument('--video_bitrate', '-vb', metavar='', help='set the number of bits per second for video.') size.add_argument('--audio_bitrate', '-ab', metavar='', help='set the number of bits per second for audio.') size.add_argument('--sample_rate', '-r', type=sample_rate_type, metavar='', help='set the sample rate of the input and output videos.') size.add_argument('--video_codec', '-vcodec', metavar='', help='set the video codec for the output file.') args = parser.parse_args() dirPath = os.path.dirname(os.path.realpath(__file__)) # fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) if(args.version): print('Auto-Editor version', version) sys.exit() if(args.export_to_premiere): print('Exporting to Adobe Premiere Pro XML file.') if(args.export_to_resolve): print('Exporting to DaVinci Resolve XML file.') if(args.export_as_audio): print('Exporting as audio.') newF = None newP = None if(platform.system() == 'Windows' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'win-ffmpeg/bin/ffmpeg.exe') newP = os.path.join(dirPath, 'win-ffmpeg/bin/ffprobe.exe') if(platform.system() == 'Darwin' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'mac-ffmpeg/bin/ffmpeg') newP = os.path.join(dirPath, 'mac-ffmpeg/bin/ffprobe') if(newF is not None and os.path.isfile(newF)): ffmpeg = newF ffprobe = newP else: ffmpeg = 'ffmpeg' ffprobe = 'ffprobe' makingDataFile = args.export_to_premiere or args.export_to_resolve is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if(args.debug): print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system()) # Platform can be 'Linux', 'Darwin' (macOS), 'Java', 'Windows' print('FFmpeg path:', ffmpeg) print('Auto-Editor version', version) if(args.input == []): sys.exit() from usefulFunctions import Log log = Log(3 if args.debug else 2) if(is64bit == '32-bit'): # I should have put this warning a long time ago. log.warning("You have the 32-bit version of Python, which means you won't be " \ 'able to handle long videos.') if(args.frame_margin < 0): log.error('Frame margin cannot be negative.') if(args.input == []): log.error('The following arguments are required: input\n' \ 'In other words, you need the path to a video or an audio file ' \ 'so that auto-editor can do the work for you.') if(args.silent_speed <= 0 or args.silent_speed > 99999): args.silent_speed = 99999 if(args.video_speed <= 0 or args.video_speed > 99999): args.video_speed = 99999 inputList = [] for myInput in args.input: if(os.path.isdir(myInput)): def validFiles(path): for f in os.listdir(path): if(not f.startswith('.') and not f.endswith('.xml') and not f.endswith('.png') and not f.endswith('.md') and not os.path.isdir(f)): yield os.path.join(path, f) inputList += sorted(validFiles(myInput)) elif(os.path.isfile(myInput)): inputList.append(myInput) elif(myInput.startswith('http://') or myInput.startswith('https://')): print('URL detected, using youtube-dl to download from webpage.') basename = re.sub(r'\W+', '-', myInput) cmd = ['youtube-dl', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', myInput, '--output', basename, '--no-check-certificate'] if(ffmpeg != 'ffmpeg'): cmd.extend(['--ffmpeg-location', ffmpeg]) subprocess.call(cmd) inputList.append(basename + '.mp4') else: log.error('Could not find file: ' + myInput) if(args.output_file is None): args.output_file = [] if(len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): oldFile = inputList[i] dotIndex = oldFile.rfind('.') if(args.export_to_premiere or args.export_to_resolve): args.output_file.append(oldFile[:dotIndex] + '.xml') else: ext = oldFile[dotIndex:] if(args.export_as_audio): ext = '.wav' end = '_ALTERED' + ext args.output_file.append(oldFile[:dotIndex] + end) TEMP = tempfile.mkdtemp() if(args.combine_files): with open(f'{TEMP}/combines.txt', 'w') as outfile: for fileref in inputList: outfile.write(f"file '{fileref}'\n") cmd = [ffmpeg, '-f', 'concat', '-safe', '0', '-i', f'{TEMP}/combines.txt', '-c', 'copy', 'combined.mp4'] subprocess.call(cmd) inputList = ['combined.mp4'] speeds = [args.silent_speed, args.video_speed] startTime = time.time() from usefulFunctions import isAudioFile, vidTracks, conwrite, getAudioChunks from wavfile import read, write numCuts = 0 for i, INPUT_FILE in enumerate(inputList): newOutput = args.output_file[i] fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] # Grab the sample rate from the input. sr = args.sample_rate if(sr is None): output = pipeToConsole([ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'\s(?P<grp>\w+?)\sHz', output).groupdict() sr = matchDict['grp'] except AttributeError: sr = 48000 args.sample_rate = sr # Grab the audio bitrate from the input. abit = args.audio_bitrate if(abit is None): output = pipeToConsole([ffprobe, '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'stream=bit_rate', '-of', 'compact=p=0:nk=1', INPUT_FILE]) try: abit = int(output) except: log.warning("Couldn't automatically detect audio bitrate.") abit = '500k' log.debug('Setting audio bitrate to ' + abit) else: abit = str(round(abit / 1000)) + 'k' else: abit = str(abit) args.audio_bitrate = abit if(isAudioFile(INPUT_FILE)): fps = 30 tracks = 1 cmd = [ffmpeg, '-y', '-i', INPUT_FILE, '-b:a', args.audio_bitrate, '-ac', '2', '-ar', str(args.sample_rate), '-vn', f'{TEMP}/fastAud.wav'] if(args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/fastAud.wav') else: if(args.export_to_premiere): fps = 29.97 else: fps = ffmpegFPS(ffmpeg, INPUT_FILE, log) tracks = vidTracks(INPUT_FILE, ffprobe, log) if(args.cut_by_this_track >= tracks): log.error("You choose a track that doesn't exist.\n" \ f'There are only {tracks-1} tracks. (starting from 0)') vcodec = args.video_codec if(vcodec is None): output = pipeToConsole([ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'Video:\s(?P<video>\w+?)\s', output).groupdict() vcodec = matchDict['video'] log.debug(vcodec) except AttributeError: vcodec = 'copy' log.warning("Couldn't automatically detect the video codec.") vbit = args.video_bitrate if(vbit is None): output = pipeToConsole([ffprobe, '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=bit_rate', '-of', 'compact=p=0:nk=1', INPUT_FILE]) try: vbit = int(output) except: log.warning("Couldn't automatically detect video bitrate.") vbit = '500k' log.debug('Setting vbit to ' + vbit) else: vbit += 300 * 1000 # Add more for better quality. vbit = str(round(vbit / 1000)) + 'k' else: vbit = str(vbit) if(vcodec == 'copy'): log.warning('Your bitrate will not be applied because' \ ' the video codec is "copy".') args.video_bitrate = vbit for trackNum in range(tracks): cmd = [ffmpeg, '-y', '-i', INPUT_FILE, '-ab', args.audio_bitrate, '-ac', '2', '-ar', str(args.sample_rate), '-map', f'0:a:{trackNum}', f'{TEMP}/{trackNum}.wav'] if(args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) if(args.cut_by_all_tracks): cmd = [ffmpeg, '-y', '-i', INPUT_FILE, '-filter_complex', f'[0:a]amerge=inputs={tracks}', '-map', 'a', '-ar', str(args.sample_rate), '-ac', '2', '-f', 'wav', f'{TEMP}/combined.wav'] if(args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/combined.wav') else: if(os.path.isfile(f'{TEMP}/{args.cut_by_this_track}.wav')): sampleRate, audioData = read(f'{TEMP}/{args.cut_by_this_track}.wav') else: log.error('Audio track not found!') chunks = getAudioChunks(audioData, sampleRate, fps, args.silent_threshold, args.frame_margin, args.min_clip_length, args.min_cut_length, log) clips = [] for chunk in chunks: if(speeds[chunk[2]] == 99999): numCuts += 1 else: clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) if(fps is None and not isAudioFile(INPUT_FILE)): if(makingDataFile): dotIndex = INPUT_FILE.rfind('.') end = '_constantFPS' + oldFile[dotIndex:] constantLoc = oldFile[:dotIndex] + end else: constantLoc = f'{TEMP}/constantVid{fileFormat}' cmd = [ffmpeg, '-y', '-i', INPUT_FILE, '-filter:v', f'fps=fps=30', constantLoc] if(args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) INPUT_FILE = constancLoc if(args.preview): args.no_open = True from preview import preview preview(INPUT_FILE, chunks, speeds, args.debug) continue if(args.export_to_premiere): args.no_open = True from premiere import exportToPremiere exportToPremiere(INPUT_FILE, TEMP, newOutput, clips, tracks, sampleRate, log) continue if(args.export_to_resolve): args.no_open = True duration = chunks[len(chunks) - 1][1] from resolve import exportToResolve exportToResolve(INPUT_FILE, newOutput, clips, duration, sampleRate, log) continue if(isAudioFile(INPUT_FILE) and not makingDataFile): from fastAudio import fastAudio fastAudio(ffmpeg, INPUT_FILE, newOutput, chunks, speeds, args.audio_bitrate, sampleRate, args.debug, True, log) continue from fastVideo import fastVideo fastVideo(ffmpeg, INPUT_FILE, newOutput, chunks, speeds, tracks, args.audio_bitrate, sampleRate, args.debug, TEMP, args.keep_tracks_seperate, vcodec, fps, args.export_as_audio, args.video_bitrate, log) if(not os.path.isfile(newOutput)): log.error(f'The file {newOutput} was not created.') if(not args.preview and not makingDataFile): timeLength = round(time.time() - startTime, 2) minutes = timedelta(seconds=round(timeLength)) print(f'Finished. took {timeLength} seconds ({minutes})') if(not args.preview and makingDataFile): timeSave = numCuts * 2 # assuming making each cut takes about 2 seconds. units = 'seconds' if(timeSave >= 3600): timeSave = round(timeSave / 3600, 1) if(timeSave % 1 == 0): timeSave = round(timeSave) units = 'hours' if(timeSave >= 60): timeSave = round(timeSave / 60, 1) if(timeSave >= 10 or timeSave % 1 == 0): timeSave = round(timeSave) units = 'minutes' print(f'Auto-Editor made {numCuts} cuts', end='') # Don't add a newline. if(numCuts > 4): print(f', which would have taken about {timeSave} {units} if edited manually.') else: print('.') if(not args.no_open): try: # should work on Windows os.startfile(newOutput) except AttributeError: try: # should work on MacOS and most Linux versions subprocess.call(['open', newOutput]) except: try: # should work on WSL2 subprocess.call(['cmd.exe', '/C', 'start', newOutput]) except: log.warning('Could not open output file.') rmtree(TEMP)
def main(): options = [] option_names = [] def add_argument(*names, nargs=1, type=str, default=None, action='default', range=None, choices=None, help='', extra=''): nonlocal options nonlocal option_names newDic = {} newDic['names'] = names newDic['nargs'] = nargs newDic['type'] = type newDic['default'] = default newDic['action'] = action newDic['help'] = help newDic['extra'] = extra newDic['range'] = range newDic['choices'] = choices options.append(newDic) option_names = option_names + list(names) add_argument('(input)', nargs='*', help='the path to a file, folder, or url you want edited.') add_argument('--help', '-h', action='store_true', help='print this message and exit.') add_argument( '--frame_margin', '-m', type=int, default=6, range='0 to Infinity', help= 'set how many "silent" frames of on either side of "loud" sections be included.' ) add_argument( '--silent_threshold', '-t', type=float_type, default=0.04, range='0 to 1', help='set the volume that frames audio needs to surpass to be "loud".') add_argument( '--video_speed', '--sounded_speed', '-v', type=float_type, default=1.00, range='0 to 999999', help='set the speed that "loud" sections should be played at.') add_argument( '--silent_speed', '-s', type=float_type, default=99999, range='0 to 99999', help='set the speed that "silent" sections should be played at.') add_argument('--output_file', '-o', nargs='*', help='set the name(s) of the new output.') add_argument('--no_open', action='store_true', help='do not open the file after editing is done.') add_argument( '--min_clip_length', '-mclip', type=int, default=3, range='0 to Infinity', help= 'set the minimum length a clip can be. If a clip is too short, cut it.' ) add_argument( '--min_cut_length', '-mcut', type=int, default=6, range='0 to Infinity', help= "set the minimum length a cut can be. If a cut is too short, don't cut" ) add_argument('--combine_files', action='store_true', help='combine all input files into one before editing.') add_argument('--preview', action='store_true', help='show stats on how the input will be cut.') add_argument( '--cut_by_this_audio', '-ca', type=file_type, help="base cuts by this audio file instead of the video's audio.") add_argument('--cut_by_this_track', '-ct', type=int, default=0, range='0 to the number of audio tracks', help='base cuts by a different audio track in the video.') add_argument('--cut_by_all_tracks', '-cat', action='store_true', help='combine all audio tracks into one before basing cuts.') add_argument('--keep_tracks_seperate', action='store_true', help="don't combine audio tracks when exporting.") add_argument( '--my_ffmpeg', action='store_true', help='use your ffmpeg and other binaries instead of the ones packaged.' ) add_argument('--version', action='store_true', help='show which auto-editor you have.') add_argument('--debug', '--verbose', action='store_true', help='show helpful debugging values.') # TODO: add export_as_video add_argument('--export_as_audio', '-exa', action='store_true', help='export as a WAV audio file.') add_argument( '--export_to_premiere', '-exp', action='store_true', help= 'export as an XML file for Adobe Premiere Pro instead of outputting a media file.' ) add_argument( '--export_to_resolve', '-exr', action='store_true', help= 'export as an XML file for DaVinci Resolve instead of outputting a media file.' ) add_argument('--video_bitrate', '-vb', help='set the number of bits per second for video.') add_argument('--audio_bitrate', '-ab', help='set the number of bits per second for audio.') add_argument('--sample_rate', '-r', type=sample_rate_type, help='set the sample rate of the input and output videos.') add_argument('--video_codec', '-vcodec', help='set the video codec for the output file.') add_argument( '--preset', '-p', default='medium', choices=[ 'ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow' ], help= 'set the preset for ffmpeg to help save file size or increase quality.' ) add_argument('--tune', default='none', choices=[ 'film', 'animation', 'grain', 'stillimage', 'fastdecode', 'zerolatency', 'none' ], help='set the tune for ffmpeg to help compress video better.') add_argument( '--ignore', nargs='*', help= "the range (in seconds) that shouldn't be edited at all. (uses range syntax)" ) add_argument('--cut_out', nargs='*', help='the range (in seconds) that should be cut out completely, '\ 'regardless of anything else. (uses range syntax)') dirPath = os.path.dirname(os.path.realpath(__file__)) # Fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) from usefulFunctions import Log class parse_options(): def __init__(self, userArgs, log, *args): # Set the default options. for options in args: for option in options: key = option['names'][0].replace('-', '') if (option['action'] == 'store_true'): value = False elif (option['nargs'] != 1): value = [] else: value = option['default'] setattr(self, key, value) def get_option(item, the_args): for options in the_args: for option in options: if (item in option['names']): return option return None # Figure out attributes changed by user. myList = [] settingInputs = True optionList = 'input' i = 0 while i < len(userArgs): item = userArgs[i] if (i == len(userArgs) - 1): nextItem = None else: nextItem = userArgs[i + 1] option = get_option(item, args) if (option is not None): if (optionList is not None): setattr(self, optionList, myList) settingInputs = False optionList = None myList = [] key = option['names'][0].replace('-', '') # show help for specific option. if (nextItem == '-h' or nextItem == '--help'): print(' ', ', '.join(option['names'])) print(' ', option['help']) print(' ', option['extra']) if (option['action'] == 'default'): print(' type:', option['type'].__name__) print(' default:', option['default']) if (option['range'] is not None): print(' range:', option['range']) if (option['choices'] is not None): print(' choices:', ', '.join(option['choices'])) else: print(f' type: flag') sys.exit() if (option['nargs'] != 1): settingInputs = True optionList = key elif (option['action'] == 'store_true'): value = True else: try: # Convert to correct type. value = option['type'](nextItem) except: typeName = option['type'].__name__ log.error( f'Couldn\'t convert "{nextItem}" to {typeName}' ) if (option['choices'] is not None): if (value not in option['choices']): log.error( f'{value} is not a choice for {option}') i += 1 setattr(self, key, value) else: if (settingInputs and not item.startswith('-')): # Input file names myList.append(item) else: # Unknown Option! hmm = difflib.get_close_matches(item, option_names) potential_options = ', '.join(hmm) append = '' if (hmm != []): append = f'\n\n Did you mean:\n {potential_options}' log.error(f'Unknown option: {item}{append}') i += 1 if (settingInputs): setattr(self, optionList, myList) args = parse_options(sys.argv[1:], Log(3), options) # Print help screen for entire program. if (args.help): for option in options: print(' ', ', '.join(option['names']) + ':', option['help']) print('\nHave an issue? Make an issue. '\ 'Visit https://github.com/wyattblue/auto-editor/issues') sys.exit() if (args.version): print('Auto-Editor version', version) sys.exit() from usefulFunctions import isAudioFile, vidTracks, conwrite, getAudioChunks from wavfile import read, write if (not args.preview): if (args.export_to_premiere): conwrite('Exporting to Adobe Premiere Pro XML file.') elif (args.export_to_resolve): conwrite('Exporting to DaVinci Resolve XML file.') elif (args.export_as_audio): conwrite('Exporting as audio.') else: conwrite('Starting.') newF = None newP = None if (platform.system() == 'Windows' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'win-ffmpeg/bin/ffmpeg.exe') newP = os.path.join(dirPath, 'win-ffmpeg/bin/ffprobe.exe') if (platform.system() == 'Darwin' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'mac-ffmpeg/bin/ffmpeg') newP = os.path.join(dirPath, 'mac-ffmpeg/bin/ffprobe') if (newF is not None and os.path.isfile(newF)): ffmpeg = newF ffprobe = newP else: ffmpeg = 'ffmpeg' ffprobe = 'ffprobe' makingDataFile = args.export_to_premiere or args.export_to_resolve is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if (args.debug): print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system()) # Platform can be 'Linux', 'Darwin' (macOS), 'Java', 'Windows' print('FFmpeg path:', ffmpeg) print('Auto-Editor version', version) if (args.input == []): sys.exit() log = Log(3 if args.debug else 2) if (is64bit == '32-bit'): # I should have put this warning a long time ago. log.warning("You have the 32-bit version of Python, which means you won't be " \ 'able to handle long videos.') if (args.frame_margin < 0): log.error('Frame margin cannot be negative.') if (args.input == []): log.error( 'You need the (input) argument so that auto-editor can do the work for you.' ) if (args.silent_speed <= 0 or args.silent_speed > 99999): args.silent_speed = 99999 if (args.video_speed <= 0 or args.video_speed > 99999): args.video_speed = 99999 inputList = [] for myInput in args.input: if (os.path.isdir(myInput)): def validFiles(path): for f in os.listdir(path): if (not f.startswith('.') and not f.endswith('.xml') and not f.endswith('.png') and not f.endswith('.md') and not os.path.isdir(f)): yield os.path.join(path, f) inputList += sorted(validFiles(myInput)) elif (os.path.isfile(myInput)): inputList.append(myInput) elif (myInput.startswith('http://') or myInput.startswith('https://')): print('URL detected, using youtube-dl to download from webpage.') basename = re.sub(r'\W+', '-', myInput) cmd = [ 'youtube-dl', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', myInput, '--output', basename, '--no-check-certificate' ] if (ffmpeg != 'ffmpeg'): cmd.extend(['--ffmpeg-location', ffmpeg]) subprocess.call(cmd) inputList.append(basename + '.mp4') else: log.error('Could not find file: ' + myInput) if (args.output_file is None): args.output_file = [] if (len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): oldFile = inputList[i] dotIndex = oldFile.rfind('.') if (args.export_to_premiere or args.export_to_resolve): args.output_file.append(oldFile[:dotIndex] + '.xml') else: ext = oldFile[dotIndex:] if (args.export_as_audio): ext = '.wav' end = '_ALTERED' + ext args.output_file.append(oldFile[:dotIndex] + end) TEMP = tempfile.mkdtemp() if (args.combine_files): with open(f'{TEMP}/combines.txt', 'w') as outfile: for fileref in inputList: outfile.write(f"file '{fileref}'\n") cmd = [ ffmpeg, '-f', 'concat', '-safe', '0', '-i', f'{TEMP}/combines.txt', '-c', 'copy', 'combined.mp4' ] subprocess.call(cmd) inputList = ['combined.mp4'] speeds = [args.silent_speed, args.video_speed] startTime = time.time() numCuts = 0 for i, INPUT_FILE in enumerate(inputList): newOutput = args.output_file[i] fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] # Grab the sample rate from the input. sr = args.sample_rate if (sr is None): output = pipeToConsole([ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'\s(?P<grp>\w+?)\sHz', output).groupdict() sr = matchDict['grp'] except AttributeError: sr = 48000 args.sample_rate = sr # Grab the audio bitrate from the input. abit = args.audio_bitrate if (abit is None): output = pipeToConsole([ ffprobe, '-v', 'error', '-select_streams', 'a:0', '-show_entries', 'stream=bit_rate', '-of', 'compact=p=0:nk=1', INPUT_FILE ]) try: abit = int(output) except: log.warning("Couldn't automatically detect audio bitrate.") abit = '500k' log.debug('Setting audio bitrate to ' + abit) else: abit = str(round(abit / 1000)) + 'k' else: abit = str(abit) args.audio_bitrate = abit if (isAudioFile(INPUT_FILE)): fps = 30 tracks = 1 cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-b:a', args.audio_bitrate, '-ac', '2', '-ar', str(args.sample_rate), '-vn', f'{TEMP}/fastAud.wav' ] if (args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/fastAud.wav') else: if (args.export_to_premiere): fps = 29.97 else: fps = ffmpegFPS(ffmpeg, INPUT_FILE, log) tracks = vidTracks(INPUT_FILE, ffprobe, log) if (args.cut_by_this_track >= tracks): log.error("You choose a track that doesn't exist.\n" \ f'There are only {tracks-1} tracks. (starting from 0)') vcodec = args.video_codec if (vcodec is None): output = pipeToConsole( [ffmpeg, '-i', INPUT_FILE, '-hide_banner']) try: matchDict = re.search(r'Video:\s(?P<video>\w+?)\s', output).groupdict() vcodec = matchDict['video'] log.debug(vcodec) except AttributeError: vcodec = 'copy' log.warning( "Couldn't automatically detect the video codec.") if (args.video_bitrate is not None and vcodec == 'copy'): log.warning('Your bitrate will not be applied because' \ ' the video codec is "copy".') for trackNum in range(tracks): cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-ab', args.audio_bitrate, '-ac', '2', '-ar', str(args.sample_rate), '-map', f'0:a:{trackNum}', f'{TEMP}/{trackNum}.wav' ] if (args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) if (args.cut_by_all_tracks): cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter_complex', f'[0:a]amerge=inputs={tracks}', '-map', 'a', '-ar', str(args.sample_rate), '-ac', '2', '-f', 'wav', f'{TEMP}/combined.wav' ] if (args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/combined.wav') else: if (os.path.isfile(f'{TEMP}/{args.cut_by_this_track}.wav')): sampleRate, audioData = read( f'{TEMP}/{args.cut_by_this_track}.wav') else: log.error('Audio track not found!') chunks = getAudioChunks(audioData, sampleRate, fps, args.silent_threshold, args.frame_margin, args.min_clip_length, args.min_cut_length, args.ignore, args.cut_out, log) clips = [] for chunk in chunks: if (speeds[chunk[2]] == 99999): numCuts += 1 else: clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) if (fps is None and not isAudioFile(INPUT_FILE)): if (makingDataFile): dotIndex = INPUT_FILE.rfind('.') end = '_constantFPS' + oldFile[dotIndex:] constantLoc = oldFile[:dotIndex] + end else: constantLoc = f'{TEMP}/constantVid{fileFormat}' cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter:v', f'fps=fps=30', constantLoc ] if (args.debug): cmd.extend(['-hide_banner']) else: cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) INPUT_FILE = constancLoc if (args.preview): args.no_open = True from preview import preview preview(INPUT_FILE, chunks, speeds, args.debug) continue if (args.export_to_premiere): args.no_open = True from premiere import exportToPremiere exportToPremiere(INPUT_FILE, TEMP, newOutput, clips, tracks, sampleRate, log) continue if (args.export_to_resolve): args.no_open = True duration = chunks[len(chunks) - 1][1] from resolve import exportToResolve exportToResolve(INPUT_FILE, newOutput, clips, duration, sampleRate, log) continue if (isAudioFile(INPUT_FILE) and not makingDataFile): from fastAudio import fastAudio fastAudio(ffmpeg, INPUT_FILE, newOutput, chunks, speeds, args.audio_bitrate, sampleRate, args.debug, True, log) continue from fastVideo import fastVideo fastVideo(ffmpeg, INPUT_FILE, newOutput, chunks, speeds, tracks, args.audio_bitrate, sampleRate, args.debug, TEMP, args.keep_tracks_seperate, vcodec, fps, args.export_as_audio, args.video_bitrate, args.preset, args.tune, log) if (not os.path.isfile(newOutput)): log.error(f'The file {newOutput} was not created.') if (not args.preview and not makingDataFile): timeLength = round(time.time() - startTime, 2) minutes = timedelta(seconds=round(timeLength)) print(f'Finished. took {timeLength} seconds ({minutes})') if (not args.preview and makingDataFile): timeSave = numCuts * 2 # assuming making each cut takes about 2 seconds. units = 'seconds' if (timeSave >= 3600): timeSave = round(timeSave / 3600, 1) if (timeSave % 1 == 0): timeSave = round(timeSave) units = 'hours' if (timeSave >= 60): timeSave = round(timeSave / 60, 1) if (timeSave >= 10 or timeSave % 1 == 0): timeSave = round(timeSave) units = 'minutes' print(f'Auto-Editor made {numCuts} cuts', end='') # Don't add a newline. if (numCuts > 4): print( f', which would have taken about {timeSave} {units} if edited manually.' ) else: print('.') if (not args.no_open): try: # should work on Windows os.startfile(newOutput) except AttributeError: try: # should work on MacOS and most Linux versions subprocess.call(['open', newOutput]) except: try: # should work on WSL2 subprocess.call(['cmd.exe', '/C', 'start', newOutput]) except: log.warning('Could not open output file.') rmtree(TEMP)
def main(): dirPath = os.path.dirname(os.path.realpath(__file__)) # Fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) # Print the version if only the -v option is added. if(sys.argv[1:] == ['-v'] or sys.argv[1:] == ['-V']): print(f'Auto-Editor version {version}\nPlease use --version instead.') sys.exit() if(sys.argv[1:] == []): print('\nAuto-Editor is an automatic video/audio creator and editor.\n') print('By default, it will detect silence and create a new video with ') print('those sections cut out. By changing some of the options, you can') print('export to a traditional editor like Premiere Pro and adjust the') print('edits there, adjust the pacing of the cuts, and change the method') print('of editing like using audio loudness and video motion to judge') print('making cuts.') print('\nRun:\n auto-editor --help\n\nTo get the list of options.\n') sys.exit() from vanparse import ParseOptions from usefulFunctions import Log, Timer subcommands = ['create', 'test', 'info', 'levels'] if(len(sys.argv) > 1 and sys.argv[1] in subcommands): if(sys.argv[1] == 'create'): from create import create, create_options from usefulFunctions import FFmpeg args = ParseOptions(sys.argv[2:], Log(), 'create', create_options()) ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, True, Log()) create(ffmpeg, args.input, args.output_file, args.frame_rate, args.duration, args.width, args.height, Log()) if(sys.argv[1] == 'test'): from testAutoEditor import testAutoEditor testAutoEditor() if(sys.argv[1] == 'info'): from info import getInfo, info_options from usefulFunctions import FFmpeg, FFprobe args = ParseOptions(sys.argv[2:], Log(), 'info', info_options()) log = Log() ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, False, log) ffprobe = FFprobe(dirPath, args.my_ffmpeg, False, log) getInfo(args.input, ffmpeg, ffprobe, args.fast, log) if(sys.argv[1] == 'levels'): from levels import levels, levels_options from usefulFunctions import FFmpeg, FFprobe args = ParseOptions(sys.argv[2:], Log(), 'levels', levels_options()) TEMP = tempfile.mkdtemp() log = Log(temp=TEMP) ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, False, log) ffprobe = FFprobe(dirPath, args.my_ffmpeg, False, log) levels(args.input, args.track, args.output_file, ffmpeg, ffprobe, TEMP, log) sys.exit() else: option_data = main_options() args = ParseOptions(sys.argv[1:], Log(True), 'auto-editor', option_data) timer = Timer(args.quiet) del option_data from usefulFunctions import FFmpeg, FFprobe, sep ffmpeg = FFmpeg(dirPath, args.my_ffmpeg, args.show_ffmpeg_debug, Log()) ffprobe = FFprobe(dirPath, args.my_ffmpeg, args.show_ffmpeg_debug, Log()) # Stops "The file {file} does not exist." from showing. if(args.export_as_clip_sequence): args.no_open = True makingDataFile = (args.export_to_premiere or args.export_to_resolve or args.export_to_final_cut_pro or args.export_as_json) is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if(args.debug and args.input == []): import platform print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system(), platform.release()) print('Config File path:', dirPath + sep() + 'config.txt') print('FFmpeg path:', ffmpeg.getPath()) print('FFmpeg version:', ffmpeg.getVersion()) print('Auto-Editor version', version) sys.exit() TEMP = tempfile.mkdtemp() log = Log(args.debug, args.quiet, temp=TEMP) log.debug(f'\n - Temp Directory: {TEMP}') if(is64bit == '32-bit'): log.warning('You have the 32-bit version of Python, which may lead to' \ 'memory crashes.') if(args.version): print('Auto-Editor version', version) sys.exit() ffmpeg.updateLog(log) ffprobe.updateLog(log) from usefulFunctions import isLatestVersion if(not args.quiet and not isLatestVersion(version, log)): log.print('\nAuto-Editor is out of date. Run:\n') log.print(' pip3 install -U auto-editor') log.print('\nto upgrade to the latest version.\n') from argsCheck import hardArgsCheck, softArgsCheck hardArgsCheck(args, log) args = softArgsCheck(args, log) from validateInput import validInput inputList = validInput(args.input, ffmpeg, args, log) # Figure out the output file names. def newOutputName(oldFile: str, audio, final_cut_pro, data, json) -> str: dotIndex = oldFile.rfind('.') print(oldFile) if(json): return oldFile[:dotIndex] + '.json' if(final_cut_pro): return oldFile[:dotIndex] + '.fcpxml' if(data): return oldFile[:dotIndex] + '.xml' if(audio): return oldFile[:dotIndex] + '_ALTERED.wav' return oldFile[:dotIndex] + '_ALTERED' + oldFile[dotIndex:] if(len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): args.output_file.append(newOutputName(inputList[i], args.export_as_audio, args.export_to_final_cut_pro, makingDataFile, args.export_as_json)) if(args.combine_files): # Combine video files, then set input to 'combined.mp4'. cmd = [] for fileref in inputList: cmd.extend(['-i', fileref]) cmd.extend(['-filter_complex', f'[0:v]concat=n={len(inputList)}:v=1:a=1', '-codec:v', 'h264', '-pix_fmt', 'yuv420p', '-strict', '-2', f'{TEMP}{sep()}combined.mp4']) ffmpeg.run(cmd) del cmd inputList = [f'{TEMP}{sep()}combined.mp4'] speeds = [args.silent_speed, args.video_speed] if(args.cut_out != [] and 99999 not in speeds): speeds.append(99999) for item in args.set_speed_for_range: if(item[0] not in speeds): speeds.append(float(item[0])) log.debug(f' - Speeds: {speeds}') from wavfile import read audioExtensions = ['.wav', '.mp3', '.m4a', '.aiff', '.flac', '.ogg', '.oga', '.acc', '.nfa', '.mka'] sampleRate = None for i, INPUT_FILE in enumerate(inputList): if(len(inputList) > 1): log.conwrite(f'Working on {INPUT_FILE}') fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] chunks = None if(fileFormat == '.json'): log.debug('Reading .json file') from makeCutList import readCutList INPUT_FILE, chunks, speeds = readCutList(INPUT_FILE, version, log) newOutput = newOutputName(INPUT_FILE, args.export_as_audio, args.export_to_final_cut_pro, makingDataFile, False) fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] else: newOutput = args.output_file[i] if(not os.path.isdir(INPUT_FILE) and '.' not in newOutput): newOutput += INPUT_FILE[INPUT_FILE.rfind('.'):] log.debug(f' - INPUT_FILE: {INPUT_FILE}') log.debug(f' - newOutput: {newOutput}') if(os.path.isfile(newOutput) and INPUT_FILE != newOutput): log.debug(f' Removing already existing file: {newOutput}') os.remove(newOutput) if(args.sample_rate is None): sampleRate = ffprobe.getSampleRate(INPUT_FILE) if(sampleRate == 'N/A'): sampleRate = '48000' log.warning(f"Samplerate wasn't detected, so it will be set to {sampleRate}.") else: sampleRate = str(args.sample_rate) log.debug(f' - sampleRate: {sampleRate}') if(args.audio_bitrate is None): if(INPUT_FILE.endswith('.mkv')): # audio bitrate not supported in the mkv container. audioBitrate = None else: audioBitrate = ffprobe.getPrettyBitrate(INPUT_FILE, 'a') if(audioBitrate == 'N/A'): log.warning("Couldn't automatically detect audio bitrate.") audioBitrate = None else: audioBitrate = args.audio_bitrate log.debug(f' - audioBitrate: {audioBitrate}') audioData = None audioFile = fileFormat in audioExtensions if(audioFile): if(args.force_fps_to is None): fps = 30 # Audio files don't have frames, so give fps a dummy value. else: fps = args.force_fps_to if(args.force_tracks_to is None): tracks = 1 else: tracks = args.force_tracks_to cmd = ['-i', INPUT_FILE] if(audioBitrate is not None): cmd.extend(['-b:a', audioBitrate]) cmd.extend(['-ac', '2', '-ar', sampleRate, '-vn', f'{TEMP}{sep()}fastAud.wav']) ffmpeg.run(cmd) del cmd sampleRate, audioData = read(f'{TEMP}{sep()}fastAud.wav') else: if(args.force_fps_to is not None): fps = args.force_fps_to elif(args.export_to_premiere or args.export_to_final_cut_pro or args.export_to_resolve): # Based on timebase. fps = int(ffprobe.getFrameRate(INPUT_FILE)) else: fps = ffprobe.getFrameRate(INPUT_FILE) if(fps < 1): log.error(f"{INPUT_FILE}: Frame rate cannot be below 1. fps: {fps}") tracks = args.force_tracks_to if(tracks is None): tracks = ffprobe.getAudioTracks(INPUT_FILE) if(args.cut_by_this_track >= tracks): allTracks = '' for trackNum in range(tracks): allTracks += f'Track {trackNum}\n' if(tracks == 1): message = f'is only {tracks} track' else: message = f'are only {tracks} tracks' log.error("You choose a track that doesn't exist.\n" \ f'There {message}.\n {allTracks}') # Split audio tracks into: 0.wav, 1.wav, etc. for trackNum in range(tracks): cmd = ['-i', INPUT_FILE] if(audioBitrate is not None): cmd.extend(['-ab', audioBitrate]) cmd.extend(['-ac', '2', '-ar', sampleRate, '-map', f'0:a:{trackNum}', f'{TEMP}{sep()}{trackNum}.wav']) ffmpeg.run(cmd) del cmd # Check if the `--cut_by_all_tracks` flag has been set or not. if(args.cut_by_all_tracks): # Combine all audio tracks into one audio file, then read. cmd = ['-i', INPUT_FILE, '-filter_complex', f'[0:a]amix=inputs={tracks}:duration=longest', '-ar', sampleRate, '-ac', '2', '-f', 'wav', f'{TEMP}{sep()}combined.wav'] ffmpeg.run(cmd) sampleRate, audioData = read(f'{TEMP}{sep()}combined.wav') del cmd else: # Read only one audio file. if(os.path.isfile(f'{TEMP}{sep()}{args.cut_by_this_track}.wav')): sampleRate, audioData = read(f'{TEMP}{sep()}{args.cut_by_this_track}.wav') else: log.bug('Audio track not found!') log.debug(f' - Frame Rate: {fps}') if(chunks is None): from cutting import audioToHasLoud, motionDetection audioList = None motionList = None if('audio' in args.edit_based_on): log.debug('Analyzing audio volume.') audioList = audioToHasLoud(audioData, sampleRate, args.silent_threshold, fps, log) if('motion' in args.edit_based_on): log.debug('Analyzing video motion.') motionList = motionDetection(INPUT_FILE, ffprobe, args.motion_threshold, log, width=args.width, dilates=args.dilates, blur=args.blur) if(audioList is not None): if(len(audioList) != len(motionList)): log.debug(f'audioList Length: {len(audioList)}') log.debug(f'motionList Length: {len(motionList)}') if(len(audioList) > len(motionList)): log.debug('Reducing the size of audioList to match motionList.') audioList = audioList[:len(motionList)] elif(len(motionList) > len(audioList)): log.debug('Reducing the size of motionList to match audioList.') motionList = motionList[:len(audioList)] from cutting import combineArrs, applySpacingRules hasLoud = combineArrs(audioList, motionList, args.edit_based_on, log) del audioList, motionList effects = [] if(args.zoom != []): from cutting import applyZooms effects += applyZooms(args.zoom, audioData, sampleRate, fps, log) if(args.rectangle != []): from cutting import applyRects effects += applyRects(args.rectangle, audioData, sampleRate, fps, log) chunks = applySpacingRules(hasLoud, speeds, fps, args, log) del hasLoud def isClip(chunk): nonlocal speeds return speeds[chunk[2]] != 99999 def getNumberOfCuts(chunks, speeds): return len(list(filter(isClip, chunks))) def getClips(chunks, speeds): clips = [] for chunk in chunks: if(isClip(chunk)): clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) return clips numCuts = getNumberOfCuts(chunks, speeds) clips = getClips(chunks, speeds) if(fps is None and not audioFile): if(makingDataFile): constantLoc = appendFileName(INPUT_FILE, '_constantFPS') else: constantLoc = f'{TEMP}{sep()}constantVid{fileFormat}' ffmpeg.run(['-i', INPUT_FILE, '-filter:v', 'fps=fps=30', constantLoc]) INPUT_FILE = constantLoc if(args.export_as_json): from makeCutList import makeCutList makeCutList(INPUT_FILE, newOutput, version, chunks, speeds, log) continue if(args.preview): newOutput = None from preview import preview preview(INPUT_FILE, chunks, speeds, fps, audioFile, log) continue if(args.export_to_premiere or args.export_to_resolve): from editor import editorXML editorXML(INPUT_FILE, TEMP, newOutput, ffprobe, clips, chunks, tracks, sampleRate, audioFile, args.export_to_resolve, fps, log) continue if(args.export_to_final_cut_pro): from editor import fcpXML fcpXML(INPUT_FILE, TEMP, newOutput, ffprobe, clips, chunks, tracks, sampleRate, audioFile, fps, log) continue def makeAudioFile(input_, chunks, output): from fastAudio import fastAudio, handleAudio, convertAudio theFile = handleAudio(ffmpeg, input_, audioBitrate, str(sampleRate), TEMP, log) TEMP_FILE = f'{TEMP}{sep()}convert.wav' fastAudio(theFile, TEMP_FILE, chunks, speeds, log, fps, args.machine_readable_progress, args.no_progress) convertAudio(ffmpeg, ffprobe, TEMP_FILE, input_, output, args, log) if(audioFile): if(args.export_as_clip_sequence): i = 1 for item in chunks: if(speeds[item[2]] == 99999): continue makeAudioFile(INPUT_FILE, [item], appendFileName(newOutput, f'-{i}')) i += 1 else: makeAudioFile(INPUT_FILE, chunks, newOutput) continue def makeVideoFile(input_, chunks, output): from videoUtils import handleAudioTracks, muxVideo continueVid = handleAudioTracks(ffmpeg, output, args, tracks, chunks, speeds, fps, TEMP, log) if(continueVid): if(args.render == 'auto'): if(args.zoom != [] or args.rectangle != []): args.render = 'opencv' else: try: import av args.render = 'av' except ImportError: args.render = 'opencv' log.debug(f'Using {args.render} method') if(args.render == 'av'): if(args.zoom != []): log.error('Zoom effect is not supported on the av render method.') if(args.rectangle != []): log.error('Rectangle effect is not supported on the av render method.') from renderVideo import renderAv renderAv(ffmpeg, ffprobe, input_, args, chunks, speeds, fps, TEMP, log) if(args.render == 'opencv'): from renderVideo import renderOpencv renderOpencv(ffmpeg, ffprobe, input_, args, chunks, speeds, fps, effects, TEMP, log) # Now mix new audio(s) and the new video. muxVideo(ffmpeg, output, args, tracks, TEMP, log) if(output is not None and not os.path.isfile(output)): log.bug(f'The file {output} was not created.') if(args.export_as_clip_sequence): i = 1 totalFrames = chunks[len(chunks) - 1][1] speeds.append(99999) # guarantee we have a cut speed to work with. for item in chunks: if(speeds[item[2]] == 99999): continue makeVideoFile(INPUT_FILE, padChunk(item, totalFrames), appendFileName(newOutput, f'-{i}')) i += 1 else: makeVideoFile(INPUT_FILE, chunks, newOutput) if(not args.preview and not makingDataFile): timer.stop() if(not args.preview and makingDataFile): from usefulFunctions import humanReadableTime # Assume making each cut takes about 30 seconds. timeSave = humanReadableTime(numCuts * 30) s = 's' if numCuts != 1 else '' log.print(f'Auto-Editor made {numCuts} cut{s}', end='') log.print(f', which would have taken about {timeSave} if edited manually.') if(not args.no_open): from usefulFunctions import openWithSystemDefault openWithSystemDefault(newOutput, log) log.debug('Deleting temp dir') try: rmtree(TEMP) except PermissionError: from time import sleep sleep(1) try: rmtree(TEMP) except PermissionError: log.debug('Failed to delete temp dir.')
def main(): dirPath = os.path.dirname(os.path.realpath(__file__)) # Fixes pip not able to find other included modules. sys.path.append(os.path.abspath(dirPath)) from usefulFunctions import Log, Timer option_data = options() # Print the version if only the -v option is added. if (sys.argv[1:] == ['-v'] or sys.argv[1:] == ['-V']): print(f'Auto-Editor version {version}\nPlease use --version instead.') sys.exit() # If the users just runs: $ auto-editor if (sys.argv[1:] == []): # Print print( '\nAuto-Editor is an automatic video/audio creator and editor.\n') print( 'By default, it will detect silence and create a new video with ') print( 'those sections cut out. By changing some of the options, you can') print( 'export to a traditional editor like Premiere Pro and adjust the') print( 'edits there, adjust the pacing of the cuts, and change the method' ) print('of editing like using audio loudness and video motion to judge') print('making cuts.') print( '\nRun:\n auto-editor --help\n\nTo get the list of options.\n') sys.exit() from vanparse import ParseOptions args = ParseOptions(sys.argv[1:], Log(), option_data) log = Log(args.debug, args.show_ffmpeg_debug, args.quiet) log.debug('') # Print the help screen for the entire program. if (args.help): print('\n Have an issue? Make an issue. '\ 'Visit https://github.com/wyattblue/auto-editor/issues\n') print(' The help option can also be used on a specific option:') print(' auto-editor --frame_margin --help\n') for option in option_data: if (option['grouping'] == 'auto-editor'): print(' ', ', '.join(option['names']) + ':', option['help']) if (option['action'] == 'grouping'): print(' ...') print('') sys.exit() del option_data if (args.version): print('Auto-Editor version', version) sys.exit() from usefulFunctions import getBinaries, pipeToConsole, ffAddDebug from mediaMetadata import vidTracks, getSampleRate, getAudioBitrate from mediaMetadata import getVideoCodec, ffmpegFPS from wavfile import read ffmpeg, ffprobe = getBinaries(platform.system(), dirPath, args.my_ffmpeg) makingDataFile = (args.export_to_premiere or args.export_to_resolve or args.export_as_json) is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' if (args.debug and args.input == []): print('Python Version:', platform.python_version(), is64bit) print('Platform:', platform.system(), platform.release()) # Platform can be 'Linux', 'Darwin' (macOS), 'Java', 'Windows' ffmpegVersion = pipeToConsole([ffmpeg, '-version']).split('\n')[0] ffmpegVersion = ffmpegVersion.replace('ffmpeg version', '').strip() ffmpegVersion = ffmpegVersion.split(' ')[0] print('FFmpeg path:', ffmpeg) print('FFmpeg version:', ffmpegVersion) print('Auto-Editor version', version) sys.exit() if (is64bit == '32-bit'): log.warning('You have the 32-bit version of Python, which may lead to' \ 'memory crashes.') from usefulFunctions import isLatestVersion if (not args.quiet and isLatestVersion(version, log)): log.print('\nAuto-Editor is out of date. Run:\n') log.print(' pip3 install -U auto-editor') log.print('\nto upgrade to the latest version.\n') from argsCheck import hardArgsCheck, softArgsCheck hardArgsCheck(args, log) args = softArgsCheck(args, log) from validateInput import validInput inputList = validInput(args.input, ffmpeg, log) timer = Timer(args.quiet) # Figure out the output file names. def newOutputName(oldFile: str, exa=False, data=False, exc=False) -> str: dotIndex = oldFile.rfind('.') if (exc): return oldFile[:dotIndex] + '.json' elif (data): return oldFile[:dotIndex] + '.xml' ext = oldFile[dotIndex:] if (exa): ext = '.wav' return oldFile[:dotIndex] + '_ALTERED' + ext if (len(args.output_file) < len(inputList)): for i in range(len(inputList) - len(args.output_file)): args.output_file.append( newOutputName(inputList[i], args.export_as_audio, makingDataFile, args.export_as_json)) TEMP = tempfile.mkdtemp() log.debug(f'\n - Temp Directory: {TEMP}') if (args.combine_files): # Combine video files, then set input to 'combined.mp4'. cmd = [ffmpeg, '-y'] for fileref in inputList: cmd.extend(['-i', fileref]) cmd.extend([ '-filter_complex', f'[0:v]concat=n={len(inputList)}:v=1:a=1', '-codec:v', 'h264', '-pix_fmt', 'yuv420p', '-strict', '-2', f'{TEMP}/combined.mp4' ]) cmd = ffAddDebug(cmd, log.is_ffmpeg) subprocess.call(cmd) inputList = [f'{TEMP}/combined.mp4'] speeds = [args.silent_speed, args.video_speed] log.debug(f' - Speeds: {speeds}') audioExtensions = [ '.wav', '.mp3', '.m4a', '.aiff', '.flac', '.ogg', '.oga', '.acc', '.nfa', '.mka' ] # videoExtensions = ['.mp4', '.mkv', '.mov', '.webm', '.ogv'] for i, INPUT_FILE in enumerate(inputList): fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] chunks = None if (fileFormat == '.json'): log.debug('Reading .json file') from makeCutList import readCutList INPUT_FILE, chunks, speeds = readCutList(INPUT_FILE, version, log) newOutput = newOutputName(INPUT_FILE, args.export_as_audio, makingDataFile, False) fileFormat = INPUT_FILE[INPUT_FILE.rfind('.'):] else: newOutput = args.output_file[i] log.debug(f' - INPUT_FILE: {INPUT_FILE}') log.debug(f' - newOutput: {newOutput}') if (os.path.isfile(newOutput) and INPUT_FILE != newOutput): log.debug(f' Removing already existing file: {newOutput}') os.remove(newOutput) sampleRate = getSampleRate(INPUT_FILE, ffmpeg, args.sample_rate) audioBitrate = getAudioBitrate(INPUT_FILE, ffprobe, log, args.audio_bitrate) log.debug(f' - sampleRate: {sampleRate}') log.debug(f' - audioBitrate: {audioBitrate}') audioFile = fileFormat in audioExtensions if (audioFile): if (args.force_fps_to is None): fps = 30 # Audio files don't have frames, so give fps a dummy value. else: fps = args.force_fps_to if (args.force_tracks_to is None): tracks = 1 else: tracks = args.force_tracks_to cmd = [ffmpeg, '-y', '-i', INPUT_FILE] if (audioBitrate is not None): cmd.extend(['-b:a', audioBitrate]) cmd.extend( ['-ac', '2', '-ar', sampleRate, '-vn', f'{TEMP}/fastAud.wav']) cmd = ffAddDebug(cmd, log.is_ffmpeg) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/fastAud.wav') else: if (args.force_fps_to is not None): fps = args.force_fps_to elif (args.export_to_premiere): # This is the default fps value for Premiere Pro Projects. fps = 29.97 else: # Grab fps to know what the output video's fps should be. # DaVinci Resolve doesn't need fps, but grab it away just in case. fps = ffmpegFPS(ffmpeg, INPUT_FILE, log) tracks = args.force_tracks_to if (tracks is None): tracks = vidTracks(INPUT_FILE, ffprobe, log) if (args.cut_by_this_track >= tracks): allTracks = '' for trackNum in range(tracks): allTracks += f'Track {trackNum}\n' if (tracks == 1): message = f'is only {tracks} track' else: message = f'are only {tracks} tracks' log.error("You choose a track that doesn't exist.\n" \ f'There {message}.\n {allTracks}') # Get video codec vcodec = getVideoCodec(INPUT_FILE, ffmpeg, log, args.video_codec) # Split audio tracks into: 0.wav, 1.wav, etc. for trackNum in range(tracks): cmd = [ffmpeg, '-y', '-i', INPUT_FILE] if (audioBitrate is not None): cmd.extend(['-ab', audioBitrate]) cmd.extend([ '-ac', '2', '-ar', sampleRate, '-map', f'0:a:{trackNum}', f'{TEMP}/{trackNum}.wav' ]) cmd = ffAddDebug(cmd, log.is_ffmpeg) subprocess.call(cmd) # Check if the `--cut_by_all_tracks` flag has been set or not. if (args.cut_by_all_tracks): # Combine all audio tracks into one audio file, then read. cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter_complex', f'[0:a]amerge=inputs={tracks}', '-map', 'a', '-ar', sampleRate, '-ac', '2', '-f', 'wav', f'{TEMP}/combined.wav' ] cmd = ffAddDebug(cmd, log.is_ffmpeg) subprocess.call(cmd) sampleRate, audioData = read(f'{TEMP}/combined.wav') else: # Read only one audio file. if (os.path.isfile(f'{TEMP}/{args.cut_by_this_track}.wav')): sampleRate, audioData = read( f'{TEMP}/{args.cut_by_this_track}.wav') else: log.bug('Audio track not found!') log.debug(f' - Frame Rate: {fps}') if (chunks is None): from cutting import audioToHasLoud, motionDetection audioList = None motionList = None if ('audio' in args.edit_based_on): log.debug('Analyzing audio volume.') audioList = audioToHasLoud(audioData, sampleRate, args.silent_threshold, fps, log) if ('motion' in args.edit_based_on): log.debug('Analyzing video motion.') motionList = motionDetection(INPUT_FILE, ffprobe, args.motion_threshold, log, width=args.width, dilates=args.dilates, blur=args.blur) if (audioList is not None): if (len(audioList) != len(motionList)): log.debug(f'audioList Length: {len(audioList)}') log.debug(f'motionList Length: {len(motionList)}') if (len(audioList) > len(motionList)): log.debug( 'Reducing the size of audioList to match motionList.' ) audioList = audioList[:len(motionList)] elif (len(motionList) > len(audioList)): log.debug( 'Reducing the size of motionList to match audioList.' ) motionList = motionList[:len(audioList)] from cutting import combineArrs, applySpacingRules hasLoud = combineArrs(audioList, motionList, args.edit_based_on, log) del audioList, motionList chunks, includeFrame = applySpacingRules( hasLoud, fps, args.frame_margin, args.min_clip_length, args.min_cut_length, args.ignore, args.cut_out, log) del hasLoud else: from cutting import generateIncludes includeFrame = generateIncludes(chunks, log) clips = [] numCuts = len(chunks) for chunk in chunks: if (speeds[chunk[2]] != 99999): clips.append([chunk[0], chunk[1], speeds[chunk[2]] * 100]) if (fps is None and not audioFile): if (makingDataFile): dotIndex = INPUT_FILE.rfind('.') end = '_constantFPS' + INPUT_FILE[dotIndex:] constantLoc = INPUT_FILE[:dotIndex] + end else: constantLoc = f'{TEMP}/constantVid{fileFormat}' cmd = [ ffmpeg, '-y', '-i', INPUT_FILE, '-filter:v', 'fps=fps=30', constantLoc ] cmd = ffAddDebug(cmd, log.is_ffmpeg) subprocess.call(cmd) INPUT_FILE = constantLoc if (args.export_as_json): from makeCutList import makeCutList makeCutList(INPUT_FILE, newOutput, version, chunks, speeds, log) continue if (args.preview): newOutput = None from preview import preview preview(INPUT_FILE, chunks, speeds, fps, audioFile, log) continue if (args.export_to_premiere): from premiere import exportToPremiere exportToPremiere(INPUT_FILE, TEMP, newOutput, clips, tracks, sampleRate, audioFile, log) continue if (args.export_to_resolve): duration = chunks[len(chunks) - 1][1] from resolve import exportToResolve exportToResolve(INPUT_FILE, newOutput, clips, duration, sampleRate, audioFile, log) continue if (audioFile): from fastAudio import fastAudio, handleAudio theFile = handleAudio(ffmpeg, INPUT_FILE, audioBitrate, str(sampleRate), TEMP, log) fastAudio(theFile, newOutput, chunks, speeds, log, fps) continue from fastVideo import handleAudioTracks, fastVideo, muxVideo continueVid = handleAudioTracks(ffmpeg, newOutput, args.export_as_audio, tracks, args.keep_tracks_seperate, chunks, speeds, fps, TEMP, log) if (continueVid): fastVideo(INPUT_FILE, chunks, includeFrame, speeds, fps, TEMP, log) muxVideo(ffmpeg, newOutput, args.keep_tracks_seperate, tracks, args.video_bitrate, args.tune, args.preset, vcodec, args.constant_rate_factor, TEMP, log) if (newOutput is not None and not os.path.isfile(newOutput)): log.bug(f'The file {newOutput} was not created.') if (not args.preview and not makingDataFile): timer.stop() if (not args.preview and makingDataFile): from usefulFunctions import humanReadableTime # Assume making each cut takes about 30 seconds. timeSave = humanReadableTime(numCuts * 30) s = 's' if numCuts != 1 else '' log.print(f'Auto-Editor made {numCuts} cut{s}', end='') log.print( f', which would have taken about {timeSave} if edited manually.') if (not args.no_open): from usefulFunctions import smartOpen smartOpen(newOutput, log) rmtree(TEMP)