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(): 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(): parser = argparse.ArgumentParser(prog='Auto-Editor', usage='Auto-Editor: [options]') basic = parser.add_argument_group('Basic Options') basic.add_argument( 'input', nargs='*', help='the path to the file, folder, or url you want edited.') basic.add_argument( '--frame_margin', '-m', type=int, default=4, metavar='', 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='', help= 'set the volume that frames audio needs to surpass to be sounded. (0-1)' ) basic.add_argument( '--video_speed', '--sounded_speed', '-v', type=float_type, default=1.00, metavar='', help='set the speed that "loud" sections should be played at.') basic.add_argument( '--silent_speed', '-s', type=float_type, default=99999, metavar='', help='set the speed that "silent" sections should be played at.') basic.add_argument('--output_file', '-o', type=str, default='', metavar='', help='set the name 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( '--zoom_threshold', type=float_type, default=1.01, metavar='', help= 'set the volume that needs to be surpassed to zoom in the video. (0-1)' ) advance.add_argument( '--combine_files', action='store_true', help= 'when using a folder as the input, combine all files in a folder before editing.' ) advance.add_argument('--hardware_accel', type=str, metavar='', help='set the hardware used for gpu acceleration.') audio = parser.add_argument_group('Audio Options') audio.add_argument( '--sample_rate', '-r', type=sample_rate_type, default=48000, metavar='', help='set the sample rate of the input and output videos.') audio.add_argument('--audio_bitrate', type=str, default='160k', metavar='', help='set the number of bits per second for audio.') audio.add_argument( '--background_music', type=file_type, metavar='', help='set an audio file to be added as background music to your output.' ) audio.add_argument( '--background_volume', type=float, default=-8, metavar='', help= "set the dBs louder or softer compared to the audio track that bases the cuts." ) cutting = parser.add_argument_group('Cutting Options') cutting.add_argument( '--cut_by_this_audio', 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='', help='base cuts by a different audio track in the video.') cutting.add_argument( '--cut_by_all_tracks', 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. mutually exclusive with cut_by_all_tracks." ) debug = parser.add_argument_group('Developer/Debugging Options') debug.add_argument('--clear_cache', action='store_true', help='delete the cache folder and all its contents.') 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('--preview', action='store_true', help='show stats on how the input will be cut.') misc.add_argument( '--export_to_premiere', action='store_true', help= 'export as an XML file for Adobe Premiere Pro instead of outputting a media file.' ) #dep = parser.add_argument_group('Deprecated Options') 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)) CACHE = os.path.join(dirPath, 'cache') if (args.version): print('Auto-Editor version:', version) sys.exit() if (args.clear_cache): print('Removing cache') removeDir(CACHE) if (args.input == []): sys.exit() # Set the file path to the ffmpeg installation. ffmpeg = 'ffmpeg' if (platform.system() == 'Windows' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'win-ffmpeg/bin/ffmpeg.exe') if (os.path.isfile(newF)): ffmpeg = newF if (platform.system() == 'Darwin' and not args.my_ffmpeg): newF = os.path.join(dirPath, 'mac-ffmpeg/bin/ffmpeg') if (os.path.isfile(newF)): ffmpeg = newF if (args.debug): is64bit = '64-bit' if sys.maxsize > 2**32 else '32-bit' print('Python Version:', platform.python_version(), is64bit) # platform can be 'Linux', 'Darwin' (macOS), 'Java', 'Windows' # more here: https://docs.python.org/3/library/platform.html#platform.system print('Platform:', platform.system()) print('FFmpeg:', ffmpeg) print('Auto-Editor Version:', version) if (args.input == []): sys.exit() if (args.input == []): print('Error! The following arguments are required: input') print( '\nIn other words, you need the path to a video or an audio file so that auto-editor can do the work for you.' ) sys.exit(1) INPUT_FILE = args.input[0] OUTPUT_FILE = args.output_file 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 if (os.path.isdir(INPUT_FILE)): # Get the file path and date modified so that it can be sorted later. INPUTS = [] for filename in os.listdir(INPUT_FILE): if (not filename.startswith('.')): dic = {} dic['file'] = os.path.join(INPUT_FILE, filename) dic['time'] = os.path.getmtime(dic['file']) INPUTS.append(dic) # Sort the list by the key 'time'. newlist = sorted(INPUTS, key=itemgetter('time'), reverse=False) # Then reduce to a list that only has strings. INPUTS = [] for item in newlist: INPUTS.append(item['file']) if (args.combine_files): outputDir = '' with open('combine_files.txt', 'w') as outfile: for fileref in INPUTS: outfile.write(f"file '{fileref}'\n") cmd = [ ffmpeg, '-f', 'concat', '-safe', '0', '-i', 'combine_files.txt', '-c', 'copy', 'combined.mp4' ] subprocess.call(cmd) INPUTS = ['combined.mp4'] os.remove('combine_files.txt') else: outputDir = INPUT_FILE + '_ALTERED' # Create the new folder for all the outputs. try: os.mkdir(outputDir) except OSError: rmtree(outputDir) os.mkdir(outputDir) else: if (args.combine_files): print( 'Warning! --combine_files does nothing since input is not a folder.' ) outputDir = '' if (os.path.isfile(INPUT_FILE)): INPUTS = [INPUT_FILE] elif (INPUT_FILE.startswith('http://') or INPUT_FILE.startswith('https://')): # If input is a URL, download as a mp4 with youtube-dl. print('URL detected, using youtube-dl to download from webpage.') basename = re.sub(r'\W+', '-', INPUT_FILE) cmd = [ "youtube-dl", "-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4", INPUT_FILE, "--output", basename ] subprocess.call(cmd) INPUT_FILE = basename + '.mp4' INPUTS = [INPUT_FILE] if (OUTPUT_FILE == ''): OUTPUT_FILE = basename + '_ALTERED.mp4' else: print('Could not find file:', INPUT_FILE) sys.exit(1) if (args.preview): from preview import preview preview(ffmpeg, INPUT_FILE, args.silent_threshold, args.zoom_threshold, args.frame_margin, args.sample_rate, args.video_speed, args.silent_speed, args.cut_by_this_track, args.audio_bitrate) sys.exit() startTime = time.time() for INPUT_FILE in INPUTS: dotIndex = INPUT_FILE.rfind('.') extension = INPUT_FILE[dotIndex:] if (outputDir != ''): newOutput = os.path.join(outputDir, os.path.basename(INPUT_FILE)) print(newOutput) else: newOutput = OUTPUT_FILE if (args.export_to_premiere): from premiere import exportToPremiere outFile = exportToPremiere(ffmpeg, INPUT_FILE, newOutput, args.silent_threshold, args.zoom_threshold, args.frame_margin, args.sample_rate, args.video_speed, args.silent_speed) continue isAudio = extension in ['.wav', '.mp3', '.m4a'] if (isAudio): from fastAudio import fastAudio outFile = fastAudio(ffmpeg, INPUT_FILE, newOutput, args.silent_threshold, args.frame_margin, args.sample_rate, args.audio_bitrate, args.debug, args.silent_speed, args.video_speed, True) continue else: try: path = INPUT_FILE process = subprocess.Popen([ffmpeg, '-i', path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, __ = process.communicate() output = stdout.decode() if (args.debug): print('FFmpeg test:') print(output) print('\n') matchDict = re.search(r'\s(?P<fps>[\d\.]+?)\stbr', output).groupdict() __ = float(matchDict['fps']) except AttributeError: print('Warning! frame rate detection failed.') print( 'If your video has a variable frame rate, ignore this message.' ) # Auto-Editor wouldn't work if the video has a variable framerate, so # it needs to make a video with a constant framerate and use that for # it's input instead. TEMP = tempfile.mkdtemp() cmd = [ ffmpeg, '-i', INPUT_FILE, '-filter:v', f'fps=fps=30', f'{TEMP}/constantVid{extension}', '-hide_banner' ] if (not args.debug): cmd.extend(['-nostats', '-loglevel', '0']) subprocess.call(cmd) INPUT_FILE = f'{TEMP}/constantVid{extension}' if (args.background_music is None and args.background_volume != -8): print( 'Warning! Background volume specified even though no music was provided.' ) if (args.background_music is None and args.zoom_threshold > 1 and args.cut_by_this_audio is None): from fastVideo import fastVideo outFile = fastVideo(ffmpeg, INPUT_FILE, newOutput, args.silent_threshold, args.frame_margin, args.sample_rate, args.audio_bitrate, args.debug, args.video_speed, args.silent_speed, args.cut_by_this_track, args.keep_tracks_seperate) else: from originalMethod import originalMethod outFile = originalMethod( ffmpeg, INPUT_FILE, newOutput, args.frame_margin, args.silent_threshold, args.zoom_threshold, args.sample_rate, args.audio_bitrate, args.silent_speed, args.video_speed, args.keep_tracks_seperate, args.background_music, args.background_volume, args.cut_by_this_audio, args.cut_by_this_track, args.cut_by_all_tracks, args.debug, args.hardware_accel, CACHE) print('Finished.') timeLength = round(time.time() - startTime, 2) minutes = timedelta(seconds=round(timeLength)) print(f'took {timeLength} seconds ({minutes})') if (not os.path.isfile(outFile)): print(f'Error! The file {outFile} was not created.') sys.exit(1) if (not args.no_open and not args.export_to_premiere): try: # should work on Windows os.startfile(outFile) except AttributeError: try: # should work on MacOS and most Linux versions subprocess.call(['open', outFile]) except: try: # should work on WSL2 subprocess.call(['cmd.exe', '/C', 'start', outFile]) except: print('Warning! Could not open output file.') if ('TEMP' in locals()): 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)) 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)