コード例 #1
0
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)
コード例 #2
0
ファイル: __main__.py プロジェクト: syntacops/auto-editor
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)
コード例 #3
0
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)
コード例 #4
0
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)
コード例 #5
0
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)