├── .gitignore ├── dlr_turns_30.mp4 ├── manifest.csv ├── LICENSE ├── README.md └── clipatron.py /.gitignore: -------------------------------------------------------------------------------- 1 | dlr_turns_30 2 | -------------------------------------------------------------------------------- /dlr_turns_30.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/clipatron/master/dlr_turns_30.mp4 -------------------------------------------------------------------------------- /manifest.csv: -------------------------------------------------------------------------------- 1 | filename,start_time,duration,notes 2 | intro.mp4,00:00,00:03,animated "DLR 30" 3 | social_media.mp4,00:35.5,00:11,social media information 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clipatron 2 | 3 | **This is a script for creating clips from a video file.** 4 | You create a spreadsheet that describes the clips you want to create, and then the script calls [`ffmpeg`](https://ffmpeg.org/) to create the clips. 5 | 6 | This is an example spreadsheet: 7 | 8 | 9 | 10 | 11 | 12 |
filenamestart_timedurationnotes
intro.mp400:0000:03animated "DLR 30"
social_media.mp400:35.500:11social media information
13 | 14 | Combined with a video file, this would create two clips (`intro.mp4` and `social_media.mp4`). 15 | 16 | This is deliberately a simple script. 17 | If you need more complex options, you might be better off invoking ffmpeg directly, or looking at script like 18 | 19 | Original design and idea are from [@bessyboo](https://twitter.com/bessyboo) 20 | 21 | 22 | 23 | ## Installation 24 | 25 | You need two programs installed and in your PATH: 26 | 27 | - ffmpeg; see the [ffmpeg installation guide](https://www.ffmpeg.org/download.html). 28 | - Python; which you can download [from python.org](https://www.python.org/downloads/). 29 | Any recent version of Python should be fine (2.7 or 3.x). 30 | 31 | Then download this repository (click *"Clone or download"* > *"Download ZIP"*). 32 | Unzip the repository. 33 | 34 | 35 | 36 | ## Usage 37 | 38 | 1. Create a spreadsheet that describes the clips you want to create; one row per clip. 39 | This needs to have at least three named columns: 40 | 41 | * `filename` -- the name of the file you want to create for this clip 42 | * `start_time` -- where in the file the clip starts 43 | * `duration` -- how long the clip lasts 44 | 45 | You can add other columns if it's helpful (e.g. a `notes` column describing what's in the clip), but the script will ignore them. 46 | 47 | Save this spreadsheet as a CSV. 48 | 49 | 2. Open a terminal window, and navigate to the repository. 50 | 51 | ```console 52 | $ cd /path/to/repository 53 | ``` 54 | 55 | 3. Run the script, passing (1) the path to the video file you want to clip and (2) the path to the CSV you created in step 1. 56 | 57 | The repository includes an example: 58 | 59 | ```console 60 | $ python clipatron.py --manifest manifest.csv --input dlr_turns_30.mp4 61 | ✨ Clipping done! ✨ 62 | ``` 63 | 64 | 65 | 66 | ## Example 67 | 68 | So you can see what the script expects, this repository includes an example [`manifest.csv`](manifest.csv) and video file. 69 | 70 | (This is a video from the [Transport for London YouTube channel](https://www.youtube.com/watch?v=6BoDePDGBHs).) 71 | 72 | 73 | 74 | ## Getting help 75 | 76 | If you get stuck, you can ask me for help [on Twitter](https://twitter.com/alexwlchan). 77 | 78 | 79 | 80 | 81 | ## License 82 | 83 | MIT. 84 | -------------------------------------------------------------------------------- /clipatron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a script for splitting video clips using ffmpeg. 5 | 6 | It takes a path to a video and a CSV file as input. The CSV must have 7 | at least three (named) columns: 8 | 9 | filename,start_time,duration 10 | scene_1.mp4,01:00,00:30 # creates a clip starting at 01:00, 30 seconds long 11 | scene_2.mp4,02:31,00:25 # creates a clip starting at 02:31, 25 seconds long 12 | 13 | Any extra columns in the CSV will be ignored. 14 | 15 | """ 16 | 17 | from __future__ import print_function 18 | 19 | import argparse 20 | import subprocess 21 | import csv 22 | import errno 23 | import os 24 | import sys 25 | 26 | 27 | class ClipatronException(Exception): 28 | pass 29 | 30 | 31 | def parse_args(argv): 32 | """ 33 | Read the command-line arguments. 34 | """ 35 | parser = argparse.ArgumentParser( 36 | description="A script for clipping videos using ffmpeg." 37 | ) 38 | parser.add_argument( 39 | "--input", dest="VIDEO", help="Path to the video file to clip", required=True 40 | ) 41 | parser.add_argument( 42 | "--manifest", 43 | dest="CSV_MANIFEST", 44 | help="Path to the CSV file describing the clips to create", 45 | required=True, 46 | ) 47 | return parser.parse_args(argv) 48 | 49 | 50 | def ffmpeg(*args): 51 | try: 52 | subprocess.check_call(["ffmpeg"] + list(args)) 53 | except subprocess.CalledProcessError as err: 54 | print("Something went wrong: %r" % err, file=sys.stderr) 55 | print("Do you have ffmpeg installed?", file=sys.stderr) 56 | sys.exit(1) 57 | 58 | 59 | def get_rows(csv_manifest_path): 60 | """ 61 | Read the rows from the CSV file 62 | """ 63 | with open(csv_manifest_path) as infile: 64 | reader = csv.DictReader(infile) 65 | 66 | # Start at 2 because spreadsheet programs number their rows starting at 1, 67 | # and then the header is row 1 68 | for row_number, row in enumerate(reader, start=2): 69 | try: 70 | start_time = row["start_time"] 71 | duration = row["duration"] 72 | filename = row["filename"] 73 | except KeyError as err: 74 | print( 75 | "Row %d in your CSV is missing a required column: %s" % 76 | (row_number, err), file=sys.stderr 77 | ) 78 | sys.exit(1) 79 | 80 | for (value, column_name) in [ 81 | (start_time, "start_time"), 82 | (duration, "duration"), 83 | (filename, "filename") 84 | ]: 85 | if not value: 86 | print( 87 | "Row %d in your CSV has an empty value for %s!" % 88 | (row_number, column_name), file=sys.stderr 89 | ) 90 | sys.exit(1) 91 | 92 | yield (start_time, duration, filename) 93 | 94 | 95 | def mkdir_p(path): 96 | """ 97 | Create a directory if it doesn't already exist. 98 | 99 | From https://stackoverflow.com/a/600612/1558022 100 | """ 101 | try: 102 | os.makedirs(path) 103 | except OSError as exc: 104 | if exc.errno == errno.EEXIST and os.path.isdir(path): 105 | pass 106 | else: 107 | raise 108 | 109 | 110 | if __name__ == "__main__": 111 | args = parse_args(sys.argv[1:]) 112 | 113 | for (start_time, duration, filename) in get_rows(args.CSV_MANIFEST): 114 | 115 | # Should this video go in a standalone directory? 116 | dirname = os.path.dirname(filename) 117 | if not dirname: 118 | dirname = os.path.basename(os.path.splitext(args.VIDEO)[0]) 119 | 120 | mkdir_p(dirname) 121 | out_path = os.path.join(dirname, os.path.basename(filename)) 122 | 123 | ffmpeg( 124 | # See in the input file to `start_time` 125 | "-ss", start_time, 126 | 127 | # Read from `args.VIDEO` 128 | "-i", args.VIDEO, 129 | 130 | # Read at most `duration` from the video file 131 | "-t", duration, 132 | 133 | # Use the same codec as the original file 134 | "-vcodec", "copy", 135 | 136 | # Don't write any audio to the new file 137 | "-an", 138 | 139 | # Save the clip to `out_path` 140 | out_path 141 | ) 142 | 143 | print("✨ Clipping done! ✨") 144 | --------------------------------------------------------------------------------