├── requirements.txt ├── Dockerfile ├── LICENSE ├── jumpcutter.sh ├── .gitignore ├── default.nix ├── README.md ├── test.py └── jumpcutter.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | audiotsm 3 | scipy 4 | numpy 5 | pytube3 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update 4 | 5 | RUN apt-get install -y \ 6 | python3-minimal \ 7 | python3-pip \ 8 | ffmpeg \ 9 | && ln -s /usr/bin/python3 /usr/bin/python \ 10 | && ln -s /usr/bin/pip3 /usr/bin/pip 11 | 12 | EXPOSE 5000 13 | 14 | WORKDIR /jumpcutter/ 15 | COPY . ./ 16 | 17 | RUN pip3 install -r requirements.txt 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 carykh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jumpcutter.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | IN=$(echo $@ | sed 's/--input_file /\n--input_file /g' | grep "\-\-input_file" | awk '{print $1,$2}' | sed 's/--input_file //g') 3 | OUT=$(echo $@ | sed 's/--output_file /\n--output_file /g' | grep "\-\-output_file" | awk '{print $1,$2}' | sed 's/--output_file //g') 4 | TEMPFOLDER="TEMP$(date +%s)" 5 | CURDIR=$(pwd) 6 | BACKUPFILE="BACKUP$(date +%s)" 7 | 8 | echo "This script can automatically split your videos every x minutes, feed the splited videos into jumpcutter and merge the resulting videos." 9 | echo "Just append all argument you want to be passed to the jumpcutter process" 10 | echo "For example \"echo 00:20:00 | ./jumpcutter.sh --input_file ...\" will set x to 20" 11 | echo "This limits how much memory the jumpcutter process needs" 12 | echo "The script now listens to input:" 13 | read SPLIT 14 | 15 | mkdir $TEMPFOLDER 16 | 17 | cp $IN $BACKUPFILE 18 | 19 | ffmpeg -i $IN -c copy -map 0 -segment_time $SPLIT -f segment -reset_timestamps 1 $TEMPFOLDER/output%03d.mp4 20 | 21 | cd $TEMPFOLDER 22 | for FILE in $(ls .) 23 | do 24 | mv $FILE $IN 25 | python3 ../jumpcutter.py $@ 26 | mv $OUT $FILE 27 | echo "file $FILE" >> mylist.txt 28 | done 29 | ffmpeg -f concat -safe 0 -i mylist.txt -c copy $OUT 30 | cd $CURDIR 31 | mv $BACKUPFILE $IN 32 | rm -rf $TEMPFOLDER 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # IntelliJ project files 92 | .idea 93 | *.iml 94 | out 95 | gen -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | let 4 | python = python2; 5 | audiotsm = python.pkgs.buildPythonPackage { 6 | name = "audiotsm-0.1.2"; 7 | src = pkgs.fetchurl { url = "https://files.pythonhosted.org/packages/f8/b8/721a9c613641c938a6fb9c7c3efb173b7f77b519de066e9cd2eeb27c3289/audiotsm-0.1.2.tar.gz"; sha256 = "8870af28fad0a76cac1d2bb2b55e7eac6ad5d1ad5416293eb16120dece6c0281"; }; 8 | doCheck = false; 9 | buildInputs = []; 10 | propagatedBuildInputs = [ 11 | python.pkgs.numpy 12 | ]; 13 | meta = with pkgs.stdenv.lib; { 14 | homepage = "https://github.com/Muges/audiotsm"; 15 | license = licenses.mit; 16 | description = "A real-time audio time-scale modification library"; 17 | }; 18 | }; 19 | 20 | pythonForThis = python.withPackages (ps: with ps;[ 21 | scipy 22 | numpy 23 | pillow 24 | audiotsm 25 | ]); 26 | jumpcutter = stdenv.mkDerivation { 27 | pname = "jumpcutter"; 28 | version = "0.0.1"; 29 | src = ./.; 30 | buildInputs = [ 31 | pythonForThis 32 | ffmpeg 33 | ]; 34 | installPhase = '' 35 | mkdir -p $out/bin 36 | echo "#!${pythonForThis}/bin/python" > $out/bin/jumpcutter 37 | cat $src/jumpcutter.py >> $out/bin/jumpcutter 38 | substituteInPlace $out/bin/jumpcutter --replace ffmpeg ${ffmpeg} 39 | chmod +x $out/bin/jumpcutter 40 | ''; 41 | }; 42 | 43 | nix-bundle-src = builtins.fetchGit { 44 | url = "https://github.com/matthewbauer/nix-bundle"; 45 | rev = "113d8c6b426b0932a64c58c21cd065baad4c2314"; 46 | }; 47 | nix-bundle = (import ("${nix-bundle-src}/appimage-top.nix") {}) // (import "${nix-bundle-src}/default.nix" {}); 48 | in 49 | jumpcutter // { 50 | bundle = nix-bundle.nix-bootstrap { 51 | extraTargets = []; 52 | target = jumpcutter; 53 | run = "/bin/jumpcutter"; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jumpcutter 2 | Automatically edits videos. Explanation here: https://www.youtube.com/watch?v=DQ8orIurGxw 3 | 4 | ## Some heads-up: 5 | 6 | It uses Python 3. 7 | 8 | It works on Ubuntu 16.04 and Windows 10. (It might work on other OSs too, we just haven't tested it yet.) 9 | 10 | This program relies heavily on ffmpeg. It will start subprocesses that call ffmpeg, so be aware of that! 11 | 12 | As the program runs, it saves every frame of the video as an image file in a 13 | temporary folder. If your video is long, this could take a LOT of space. 14 | I have processed 17-minute videos completely fine, but be wary if you're gonna go longer. 15 | 16 | I want to use pyinstaller to turn this into an executable, so non-techy people 17 | can use it EVEN IF they don't have Python and all those libraries. Jabrils 18 | recommended this to me. However, my pyinstaller build did not work. :( HELP 19 | 20 | ## Building with nix 21 | `nix-build` to get a script with all the libraries and ffmpeg, `nix-build -A bundle` to get a single binary. 22 | 23 | ## Building without nix 24 | ### Windows 25 | Something along the lines of 26 | 1. Download https://www.python.org/ftp/python/3.7.3/python-3.7.3-amd64.exe 27 | 2. Execute downloaded File -> Click Install Python 28 | 3. Download https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20190612-caabe1b-win64-static.zip 29 | 4. Extract downloaded File 30 | 5. Download https://github.com/lamaun/jumpcutter/archive/master.zip 31 | 6. Extract downloaded File 32 | 7. Move ffmpeg/bin/ffmpeg.exe to jumpcutter-master/jumpcutter-master folder 33 | 8. Open cmd 34 | 9. cd C:\Users\YOUR_USERNAME_HERE\Downloads\jumpcutter-master\jumpcutter-master 35 | 10. C:\Users\YOUR_USERNAME_HERE\AppData\Local\Programs\Python\Python37\python.exe -m pip install -r requirements.txt 36 | 11. C:\Users\YOUR_USERNAME_HERE\AppData\Local\Programs\Python\Python37\python.exe jumpcutter.py --input_file input.mp4 37 | 38 | ### Linux 39 | ```BASH 40 | sudo apt-get install python3-minimal python3-pip ffmpeg 41 | cd /some/folder/you/like/ 42 | # For https: 43 | git clone https://github.com/Lamaun/jumpcutter.git 44 | # For ssh: 45 | git clone git@github.com:Lamaun/jumpcutter.git 46 | cd jumpcutter 47 | pip3 install -r requirements.txt # you might want --user 48 | ``` 49 | 50 | ### Docker 51 | ``` 52 | docker build -t jumpcutter . 53 | docker exec -it jumpcutter 54 | ``` 55 | 56 | ## Usage 57 | ```BASH 58 | python3 jumpcutter.py --help # get an overview of the available commands 59 | python3 jumpcutter.py --input_file some_input_video.mp4 60 | ``` 61 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from pytube import YouTube 2 | import os 3 | import subprocess 4 | 5 | testfiles = ["30fps.mp4", "60 fps.mp4", 6 | "15fps.mp4", "soundless.mp4", "music.mp4"] 7 | 8 | 9 | def downloadFile(url): 10 | sep = os.path.sep 11 | originalPath = YouTube(url).streams.first().download() 12 | filepath = originalPath.split(sep) 13 | filepath[-1] = filepath[-1].replace(' ', '_') 14 | filepath = sep.join(filepath) 15 | os.rename(originalPath, filepath) 16 | return filepath 17 | 18 | 19 | def downloadTestdata(): 20 | p = downloadFile("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 21 | command = ["ffmpeg", "-i", p, "-r", "15", "-t", "00:01:00", testfiles[2]] 22 | subprocess.run(command) 23 | command = ["ffmpeg", "-i", p, "-r", "60", "-t", "00:01:00", testfiles[1]] 24 | subprocess.run(command) 25 | command = ["ffmpeg", "-i", p, "-t", "00:01:00", testfiles[0]] 26 | subprocess.run(command) 27 | command = ["ffmpeg", "-i", testfiles[0], "-an", testfiles[3]] 28 | subprocess.run(command) 29 | command = ["ffmpeg", "-i", testfiles[0], "-vn", testfiles[4]] 30 | subprocess.run(command) 31 | os.remove(p) 32 | 33 | 34 | # prepare testdata if missing 35 | for src in testfiles: 36 | if(not os.path.isfile(src)): 37 | print("missing "+src) 38 | downloadTestdata() 39 | 40 | print("15fps autodetection test") 41 | command = ["python3", "jumpcutter.py", "--input_file", 42 | testfiles[2], "--output_file", "t.mp4"] 43 | subprocess.run(command) 44 | assert(os.path.getsize("t.mp4") == 8443196) 45 | os.remove("t.mp4") 46 | 47 | print("30fps autodetection test") 48 | command = ["python3", "jumpcutter.py", "--input_file", 49 | testfiles[0], "--output_file", "t.mp4"] 50 | subprocess.run(command) 51 | assert(os.path.getsize("t.mp4") == 8571040) 52 | os.remove("t.mp4") 53 | 54 | print("60fps autodetection test + space test") 55 | command = ["python3", "jumpcutter.py", "--input_file", 56 | testfiles[1], "--output_file", "t t.mp4"] 57 | subprocess.run(command) 58 | assert(os.path.getsize("t t.mp4") == 8113359) 59 | os.remove("t t.mp4") 60 | 61 | print("soundless test") 62 | command = ["python3", "jumpcutter.py", "--input_file", 63 | testfiles[3], "--output_file", "t.mp4"] 64 | subprocess.run(command) 65 | 66 | print("music test") 67 | command = ["python3", "jumpcutter.py", "--input_file", 68 | testfiles[4], "--output_file", "t.mp4"] 69 | subprocess.run(command) 70 | 71 | print("audio_only music test") 72 | command = ["python3", "jumpcutter.py", "--input_file", 73 | testfiles[4], "--output_file", "t.mp4", "--audio_only"] 74 | subprocess.run(command) 75 | assert(os.path.getsize("t.mp4") == 565547) 76 | os.remove("t.mp4") 77 | 78 | print("audio_only video test") 79 | command = ["python3", "jumpcutter.py", "--input_file", 80 | testfiles[2], "--output_file", "t.mp4", "--audio_only"] 81 | subprocess.run(command) 82 | assert(os.path.getsize("t.mp4") == 408510) 83 | 84 | print("slowdown test + force test") 85 | command = ["python3", "jumpcutter.py", "--input_file", 86 | testfiles[2], "--output_file", "t.mp4", "--force", "--sounded_speed", "0.5", "--silent_speed", "0.9"] 87 | subprocess.run(command) 88 | assert(os.path.getsize("t.mp4") == 22962113) 89 | os.remove("t.mp4") 90 | 91 | print("low quality test") 92 | command = ["python3", "jumpcutter.py", "--input_file", 93 | testfiles[2], "--output_file", "t.mp4", "--frame_quality", "31", "--crf", "50", "--preset", "ultrafast"] 94 | subprocess.run(command) 95 | assert(os.path.getsize("t.mp4") == 796732) 96 | os.remove("t.mp4") 97 | 98 | print("phasevocoder test") 99 | command = ["python3", "jumpcutter.py", "--input_file", 100 | testfiles[2], "--output_file", "t.mp4", "--stretch_algorithm", "phasevocoder", "--sounded_speed", "0.5"] 101 | subprocess.run(command) 102 | assert(os.path.getsize("t.mp4") == 19991295) 103 | os.remove("t.mp4") 104 | 105 | print("edl test") 106 | command = ["python3", "jumpcutter.py", "--input_file", 107 | testfiles[2], "--output_file", "t.edl", "--edl"] 108 | subprocess.run(command) 109 | assert(os.path.getsize("t.edl") == 1464) 110 | os.remove("t.edl") -------------------------------------------------------------------------------- /jumpcutter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # PYTHON_ARGCOMPLETE_OK 3 | import subprocess 4 | from audiotsm.io.wav import WavReader, WavWriter 5 | from scipy.io import wavfile 6 | import numpy as np 7 | import re 8 | import math 9 | from shutil import rmtree, move, copyfile 10 | import os 11 | import argparse 12 | from pytube import YouTube 13 | from time import time 14 | import distutils.util 15 | import tempfile 16 | 17 | def safe_remove(path): 18 | try: 19 | os.remove(path) 20 | return True 21 | except OSError: 22 | return False 23 | 24 | 25 | def downloadFile(url): 26 | sep = os.path.sep 27 | originalPath = YouTube(url).streams.first().download() 28 | filepath = originalPath.split(sep) 29 | filepath[-1] = filepath[-1].replace(' ','_') 30 | filepath = sep.join(filepath) 31 | os.rename(originalPath, filepath) 32 | return filepath 33 | 34 | 35 | def getFrameRate(path): 36 | process = subprocess.Popen(["ffmpeg", "-i", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 37 | stdout, _ = process.communicate() 38 | output = stdout.decode() 39 | match_dict = re.search(r"\s(?P[\d\.]+?)\stbr", output).groupdict() 40 | return float(match_dict["fps"]) 41 | 42 | def getMaxVolume(s): 43 | maxv = float(np.max(s)) 44 | minv = float(np.min(s)) 45 | return max(maxv,-minv) 46 | 47 | def copyFrame(inputFrame,outputFrame): 48 | src = TEMP_FOLDER.name+"/frame{:06d}".format(inputFrame+1)+".jpg" 49 | dst = TEMP_FOLDER.name+"/newFrame{:06d}".format(outputFrame+1)+".jpg" 50 | if not os.path.isfile(src): 51 | return False 52 | copyfile(src, dst) 53 | # Remove unneeded frames 54 | inputFrame-=1 55 | src = TEMP_FOLDER.name+"/frame{:06d}".format(inputFrame+1)+".jpg" 56 | while safe_remove(src): 57 | inputFrame-=1 58 | src = TEMP_FOLDER.name+"/frame{:06d}".format(inputFrame+1)+".jpg" 59 | return True 60 | 61 | def inputToOutputFilename(filename): 62 | dotIndex = filename.rfind(".") 63 | return filename[:dotIndex]+"_ALTERED"+filename[dotIndex:] 64 | 65 | def deletePathAndExit(s, msg="", rc=0): # Dangerous! Watch out! 66 | s.cleanup() 67 | print(msg) 68 | exit(rc) 69 | 70 | def writeELD(start, end, number): 71 | startFrame = int(start % frameRate) 72 | startSecond = int((start / frameRate) % 60) 73 | startMinute = int((start / frameRate / 60) % 60) 74 | startHour = int((start / frameRate / 60 / 60)) 75 | endFrame = int(end % frameRate) 76 | endSecond = int((end / frameRate) % 60) 77 | endMinute = int((end / frameRate / 60) % 60) 78 | endHour = int((end / frameRate / 60 / 60)) 79 | eld_file = open(OUTPUT_FILE, "a") 80 | eld_file.write("{0} 001 V C {4}:{3}:{2}:{1} {8}:{7}:{6}:{5} {4}:{3}:{2}:{1} {8}:{7}:{6}:{5}\r\n".format( 81 | str(number).zfill(3), 82 | str(startFrame).zfill(2), 83 | str(startSecond).zfill(2), 84 | str(startMinute).zfill(2), 85 | str(startHour).zfill(2), 86 | str(endFrame).zfill(2), 87 | str(endSecond).zfill(2), 88 | str(endMinute).zfill(2), 89 | str(endHour).zfill(2) 90 | )) 91 | eld_file.close() 92 | 93 | parser = argparse.ArgumentParser(description='Modifies a video file to play at different speeds when there is sound vs. silence.') 94 | parser.add_argument('-i', '--input_file', type=str, help='the video file you want modified') 95 | parser.add_argument('-u', '--url', type=str, help='A youtube url to download and process') 96 | parser.add_argument('-o', '--output_file', type=str, default="", help="the output file. (optional. if not included, it'll just modify the input file name)") 97 | parser.add_argument('-f', '--force', default=False, action='store_true', help='Overwrite output_file without asking') 98 | parser.add_argument('-t', '--silent_threshold', type=float, default=0.03, help="the volume amount that frames' audio needs to surpass to be consider \"sounded\". It ranges from 0 (silence) to 1 (max volume)") 99 | parser.add_argument('-snd', '--sounded_speed', type=float, default=1.70, help="the speed that sounded (spoken) frames should be played at. Typically 1.") 100 | parser.add_argument('-sil', '--silent_speed', type=float, default=8.00, help="the speed that silent frames should be played at. 999999 for jumpcutting.") 101 | parser.add_argument('-fm', '--frame_margin', type=float, default=1, help="some silent frames adjacent to sounded frames are included to provide context. How many frames on either the side of speech should be included? That's this variable.") 102 | parser.add_argument('-sr', '--sample_rate', type=float, default=44100, help="sample rate of the input and output videos") 103 | parser.add_argument('-fr', '--frame_rate', type=float, help="frame rate of the input and output videos. optional... I try to find it out myself, but it doesn't always work.") 104 | parser.add_argument('-fq', '--frame_quality', type=int, default=3, help="quality of frames to be extracted from input video. 1 is highest, 31 is lowest, 3 is the default.") 105 | parser.add_argument('-p', '--preset', type=str, default="medium", help="A preset is a collection of options that will provide a certain encoding speed to compression ratio. See https://trac.ffmpeg.org/wiki/Encode/H.264") 106 | parser.add_argument('-crf', '--crf', type=int, default=23, help="Constant Rate Factor (CRF). Lower value - better quality but large filesize. See https://trac.ffmpeg.org/wiki/Encode/H.264") 107 | parser.add_argument('-alg', '--stretch_algorithm', type=str, default="wsola", help="Sound stretching algorithm. 'phasevocoder' is best in general, but sounds phasy. 'wsola' may have a bit of wobble, but sounds better in many cases.") 108 | parser.add_argument('-a', '--audio_only', default=False, action='store_true', help="outputs an audio file") 109 | parser.add_argument('-edl', '--edl', default=False, action='store_true', help='EDL export option. (Supports only cuts off)') 110 | 111 | try: # If you want bash completion take a look at https://pypi.org/project/argcomplete/ 112 | import argcomplete 113 | argcomplete.autocomplete(parser) 114 | except ImportError: 115 | pass 116 | args = parser.parse_args() 117 | 118 | 119 | 120 | frameRate = args.frame_rate 121 | SAMPLE_RATE = args.sample_rate 122 | SILENT_THRESHOLD = args.silent_threshold 123 | FRAME_SPREADAGE = args.frame_margin 124 | AUDIO_ONLY = args.audio_only 125 | NEW_SPEED = [args.silent_speed, args.sounded_speed] 126 | if args.url != None: 127 | INPUT_FILE = downloadFile(args.url) 128 | else: 129 | INPUT_FILE = args.input_file 130 | URL = args.url 131 | FRAME_QUALITY = args.frame_quality 132 | EDL = args.edl 133 | FORCE = args.force 134 | H264_PRESET = args.preset 135 | H264_CRF = args.crf 136 | 137 | STRETCH_ALGORITHM = args.stretch_algorithm 138 | if(STRETCH_ALGORITHM == "phasevocoder"): 139 | from audiotsm import phasevocoder as audio_stretch_algorithm 140 | elif (STRETCH_ALGORITHM == "wsola"): 141 | from audiotsm import wsola as audio_stretch_algorithm 142 | else: 143 | raise Exception("Unknown audio stretching algorithm.") 144 | 145 | assert INPUT_FILE != None , "why u put no input file, that dum" 146 | assert os.path.isfile(INPUT_FILE), "I can't read/find your input file" 147 | assert FRAME_QUALITY < 32 , "The max value for frame quality is 31." 148 | assert FRAME_QUALITY > 0 , "The min value for frame quality is 1." 149 | 150 | if len(args.output_file) >= 1: 151 | OUTPUT_FILE = args.output_file 152 | else: 153 | OUTPUT_FILE = inputToOutputFilename(INPUT_FILE) 154 | 155 | if FORCE: 156 | safe_remove(OUTPUT_FILE) 157 | else: 158 | if os.path.isfile(OUTPUT_FILE): 159 | if distutils.util.strtobool(input(f"Do you want to overwrite {OUTPUT_FILE}? (y/n)")): 160 | safe_remove(OUTPUT_FILE) 161 | else: 162 | exit(0) 163 | 164 | TEMP_FOLDER = tempfile.TemporaryDirectory() 165 | AUDIO_FADE_ENVELOPE_SIZE = 400 # smooth out transitiion's audio by quickly fading in/out (arbitrary magic number whatever) 166 | 167 | if not (AUDIO_ONLY or EDL): 168 | command = ["ffmpeg", "-i", INPUT_FILE, "-qscale:v", str(FRAME_QUALITY), TEMP_FOLDER.name+"/frame%06d.jpg", "-hide_banner"] 169 | rc = subprocess.run(command) 170 | if rc.returncode != 0: 171 | deletePathAndExit(TEMP_FOLDER,"The input file doesn't have any video. Try --audio_only",rc.returncode) 172 | 173 | command = ["ffmpeg", "-i", INPUT_FILE, "-ab", "160k", "-ac", "2", "-ar", str(SAMPLE_RATE), "-vn" ,TEMP_FOLDER.name+"/audio.wav"] 174 | rc = subprocess.run(command) 175 | if rc.returncode != 0: 176 | deletePathAndExit(TEMP_FOLDER,"The input file doesn't have any sound.",rc.returncode) 177 | 178 | sampleRate, audioData = wavfile.read(TEMP_FOLDER.name+"/audio.wav") 179 | audioSampleCount = audioData.shape[0] 180 | maxAudioVolume = getMaxVolume(audioData) 181 | 182 | if frameRate is None: 183 | try: 184 | frameRate = getFrameRate(INPUT_FILE) 185 | except AttributeError: 186 | if AUDIO_ONLY: 187 | frameRate = 1 188 | else: 189 | deletePathAndExit(TEMP_FOLDER,"Couldn't detect a framerate.",rc.returncode) 190 | 191 | samplesPerFrame = sampleRate/frameRate 192 | 193 | audioFrameCount = int(math.ceil(audioSampleCount/samplesPerFrame)) 194 | 195 | hasLoudAudio = np.zeros((audioFrameCount)) 196 | 197 | 198 | 199 | for i in range(audioFrameCount): 200 | start = int(i*samplesPerFrame) 201 | end = min(int((i+1)*samplesPerFrame),audioSampleCount) 202 | audiochunks = audioData[start:end] 203 | maxchunksVolume = float(getMaxVolume(audiochunks))/maxAudioVolume 204 | if maxchunksVolume >= SILENT_THRESHOLD: 205 | hasLoudAudio[i] = 1 206 | 207 | chunks = [[0,0,0]] 208 | shouldIncludeFrame = np.zeros((audioFrameCount)) 209 | for i in range(audioFrameCount): 210 | start = int(min(max(0,i-FRAME_SPREADAGE),audioFrameCount)) 211 | end = int(max(0,min(audioFrameCount,i+1+FRAME_SPREADAGE))) 212 | if(start>end): 213 | end=start+1 214 | if(end>audioFrameCount): 215 | continue 216 | shouldIncludeFrame[i] = np.max(hasLoudAudio[start:end]) 217 | if (i >= 1 and shouldIncludeFrame[i] != shouldIncludeFrame[i-1]): # Did we flip? 218 | chunks.append([chunks[-1][1],i,shouldIncludeFrame[i-1]]) 219 | 220 | chunks.append([chunks[-1][1],audioFrameCount,shouldIncludeFrame[i-1]]) 221 | chunks = chunks[1:] 222 | outputAudioData = [] 223 | outputPointer = 0 224 | 225 | mask = [x/AUDIO_FADE_ENVELOPE_SIZE for x in range(AUDIO_FADE_ENVELOPE_SIZE)] # Create audio envelope mask 226 | 227 | lastExistingFrame = None 228 | if EDL: 229 | edlFrameNumber = 0 230 | 231 | for chunk in chunks: 232 | if EDL: 233 | if (chunk[2] == True): 234 | edlFrameNumber += 1 235 | writeELD(chunk[0], chunk[1], edlFrameNumber) 236 | continue 237 | audioChunk = audioData[int(chunk[0]*samplesPerFrame):int(chunk[1]*samplesPerFrame)] 238 | 239 | sFile = TEMP_FOLDER.name+"/tempStart.wav" 240 | eFile = TEMP_FOLDER.name+"/tempEnd.wav" 241 | wavfile.write(sFile,SAMPLE_RATE,audioChunk) 242 | with WavReader(sFile) as reader: 243 | with WavWriter(eFile, reader.channels, reader.samplerate) as writer: 244 | tsm = audio_stretch_algorithm(reader.channels, speed=NEW_SPEED[int(chunk[2])]) 245 | tsm.run(reader, writer) 246 | _, alteredAudioData = wavfile.read(eFile) 247 | leng = alteredAudioData.shape[0] 248 | endPointer = outputPointer+leng 249 | outputAudioData.extend((alteredAudioData/maxAudioVolume).tolist()) 250 | 251 | # Smoothing the audio 252 | if leng < AUDIO_FADE_ENVELOPE_SIZE: 253 | for i in range(outputPointer,endPointer): 254 | outputAudioData[i] = 0 255 | else: 256 | for i in range(outputPointer,outputPointer+AUDIO_FADE_ENVELOPE_SIZE): 257 | outputAudioData[i][0]*=mask[i-outputPointer] 258 | outputAudioData[i][1]*=mask[i-outputPointer] 259 | for i in range(endPointer-AUDIO_FADE_ENVELOPE_SIZE, endPointer): 260 | outputAudioData[i][0]*=(1-mask[i-endPointer+AUDIO_FADE_ENVELOPE_SIZE]) 261 | outputAudioData[i][1]*=(1-mask[i-endPointer+AUDIO_FADE_ENVELOPE_SIZE]) 262 | if not AUDIO_ONLY: 263 | startOutputFrame = int(math.ceil(outputPointer/samplesPerFrame)) 264 | endOutputFrame = int(math.ceil(endPointer/samplesPerFrame)) 265 | for outputFrame in range(startOutputFrame, endOutputFrame): 266 | inputFrame = int(chunk[0]+NEW_SPEED[int(chunk[2])]*(outputFrame-startOutputFrame)) 267 | didItWork = copyFrame(inputFrame,outputFrame) 268 | if outputFrame % 1000 == 999: 269 | print(str(inputFrame + 1) + "/" + str(audioFrameCount) + " frames processed.", end="\r", flush=True) 270 | if didItWork: 271 | lastExistingFrame = inputFrame 272 | else: 273 | copyFrame(lastExistingFrame,outputFrame) 274 | outputPointer = endPointer 275 | 276 | outputAudioData = np.asarray(outputAudioData) 277 | if not EDL: 278 | wavfile.write(TEMP_FOLDER.name+"/audioNew.wav",SAMPLE_RATE,outputAudioData) 279 | 280 | ''' 281 | outputFrame = math.ceil(outputPointer/samplesPerFrame) 282 | for endGap in range(outputFrame,audioFrameCount): 283 | copyFrame(int(audioSampleCount/samplesPerFrame)-1,endGap) 284 | ''' 285 | if not EDL: 286 | if AUDIO_ONLY: 287 | command = ["ffmpeg", "-i", TEMP_FOLDER.name+"/audioNew.wav", OUTPUT_FILE] 288 | else: 289 | command = ["ffmpeg", "-framerate", str(frameRate), "-i", TEMP_FOLDER.name+"/newFrame%06d.jpg", "-i", TEMP_FOLDER.name + 290 | "/audioNew.wav", "-strict", "-2", "-c:v", "libx264", "-preset", str(H264_PRESET), "-crf", str(H264_CRF), "-pix_fmt", "yuvj420p", OUTPUT_FILE] 291 | rc = subprocess.run(command) 292 | if rc.returncode != 0: 293 | deletePathAndExit(TEMP_FOLDER,rc,rc.returncode) 294 | 295 | deletePathAndExit(TEMP_FOLDER) 296 | --------------------------------------------------------------------------------