Collection of scripts to make automatic video highlights of varying quality
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

280 строки
10 KiB

  1. #!/bin/python
  2. """jsonclips.py - generate highlights based on JSON
  3. usage: jsonclips.py clips.json
  4. JSON file can specify:
  5. - output filename
  6. - source filepath
  7. - start time
  8. - duration
  9. - transition type [default: luma]
  10. - transition duration [default: 30]
  11. """
  12. TRANSITION_DURATION = 2 # seconds
  13. FFMPEG_COMMON_ARGS = ["ffmpeg", "-loglevel", "quiet", "-hide_banner"]
  14. def process_highlights(jsonfile):
  15. """Go through highlights, make clips, join to a video"""
  16. import tempfile
  17. def parse_time(timestring):
  18. from datetime import datetime
  19. """Custom time parsing function"""
  20. if timestring.count(":") == 0:
  21. # probably seconds
  22. formatstring = "%S"
  23. elif timestring.count(":") == 1:
  24. formatstring = "%M:%S"
  25. elif timestring.count(":") == 2:
  26. formatstring = "%H:%M:%S"
  27. if "." in timestring:
  28. formatstring += ".%f"
  29. return datetime.strptime(timestring, formatstring)
  30. def datetime_to_seconds(dt):
  31. """Take a datetime object and convert to seconds"""
  32. from datetime import datetime
  33. zero = datetime.strptime("0", "%S")
  34. return (dt - zero).seconds
  35. def make_clips(sources=None, outputdir=None):
  36. """Use ffmpeg to create output from processed JSON"""
  37. import subprocess
  38. import os
  39. clipnum = 0
  40. for source in sources:
  41. # decide on duration vs start+end
  42. if "duration" not in source:
  43. try:
  44. start = parse_time(source["start"])
  45. end = parse_time(source["end"])
  46. duration = (end - start).seconds
  47. start = datetime_to_seconds(start)
  48. except ValueError as e:
  49. print("Error: {}".format(e))
  50. else:
  51. start = source["start"]
  52. duration = source["duration"]
  53. print("Making {}s clip from {} at {}".format(
  54. duration, source["filepath"], start))
  55. ffmpeg_file_args = ["-ss", str(start),
  56. "-i", source["filepath"],
  57. "-t", str(duration),
  58. "-c", "copy",
  59. os.path.join(outputdir,
  60. "clip{}.mkv".format(clipnum))]
  61. ffmpeg_args = FFMPEG_COMMON_ARGS + ffmpeg_file_args
  62. print("Running ffmpeg with args:\n\n{}".
  63. format(ffmpeg_args))
  64. subprocess.run(ffmpeg_args)
  65. clipnum += 1
  66. def concat(workingdir=None, outputfile=None):
  67. """Use ffmpeg concat to join clips"""
  68. import subprocess
  69. import os
  70. joinfile = os.path.join(workingdir, "join.txt")
  71. ffmpeg_concat_args = ["-y", "-safe", "0",
  72. "-f", "concat",
  73. "-i", str(joinfile),
  74. "-c", "copy",
  75. str(outputfile)]
  76. print(FFMPEG_COMMON_ARGS + ffmpeg_concat_args)
  77. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_concat_args)
  78. def parse_json(jsonfile=None):
  79. """Parse global / per-clip options from jsonfile
  80. Requires:
  81. - outputfile
  82. - list of sources
  83. """
  84. import json
  85. import sys
  86. if jsonfile:
  87. with open(jsonfile, "r") as fh:
  88. json_in = json.load(fh)
  89. if "outputfile" not in json_in:
  90. print("Please specify an outputfile to write to")
  91. sys.exit(1)
  92. if "sources" not in json_in:
  93. print("Please specify some sources to process")
  94. sys.exit(1)
  95. return json_in
  96. def split_first(videofile=None):
  97. """Split first videofile into videofileM + videofileO"""
  98. from basicclips import get_video_duration
  99. import subprocess
  100. video_length = get_video_duration(videofile)
  101. # main clip: full length minus half transition duration
  102. ffmpeg_video_args = ["-i", videofile,
  103. "-t", str(video_length - 1),
  104. "-c", "copy",
  105. "{}".format(videofile.replace(".mkv", "M.mkv"))]
  106. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  107. # omega clip: half transition duration
  108. ffmpeg_video_args = ["-i", videofile,
  109. "-ss", str(video_length - 1),
  110. "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
  111. "{}".format(videofile.replace(".mkv", "O.mkv"))]
  112. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  113. def split_last(videofile=None):
  114. """Split last videofile into videofileA + videofileM"""
  115. import subprocess
  116. # alpha clip: half transition duration
  117. ffmpeg_video_args = ["-i", videofile,
  118. "-t", str(TRANSITION_DURATION/2),
  119. "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
  120. "{}".format(videofile.replace(".mkv", "A.mkv"))]
  121. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  122. # main clip: start at half transition duration, full length
  123. ffmpeg_video_args = ["-i", videofile,
  124. "-ss", str(TRANSITION_DURATION/2),
  125. "-c", "copy",
  126. "{}".format(videofile.replace(".mkv", "M.mkv"))]
  127. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  128. def split_middle(videofile=None):
  129. """Split middle videofile into videofileA + videofileM + videofileO"""
  130. from basicclips import get_video_duration
  131. import subprocess
  132. video_length = get_video_duration(videofile)
  133. # alpha clip: half transition duration
  134. ffmpeg_video_args = ["-i", videofile,
  135. "-t", str(TRANSITION_DURATION/2),
  136. "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
  137. "{}".format(videofile.replace(".mkv", "A.mkv"))]
  138. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  139. # main clip: full length minus half transition duration
  140. ffmpeg_video_args = ["-i", videofile,
  141. "-ss", str(TRANSITION_DURATION/2),
  142. "-t", str(video_length - (TRANSITION_DURATION/2)),
  143. "-c", "copy",
  144. "{}".format(videofile.replace(".mkv", "M.mkv"))]
  145. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  146. # omega clip: half transition duration
  147. ffmpeg_video_args = ["-i", videofile,
  148. "-ss",
  149. str(video_length - (TRANSITION_DURATION/2)),
  150. "-c:v", "libx264", "-crf", "20", "-c:a", "copy",
  151. "{}".format(videofile.replace(".mkv", "O.mkv"))]
  152. subprocess.run(FFMPEG_COMMON_ARGS + ffmpeg_video_args)
  153. def transition_join(video1=None, video2=None, workingdir=None, i=None):
  154. """Join videos 1 and 2 with a transition"""
  155. from moviepy.editor import VideoFileClip
  156. from moviepy.editor import CompositeVideoClip
  157. tr = TRANSITION_DURATION/2
  158. try:
  159. clip1 = VideoFileClip(os.path.join(workingdir, video1))
  160. except IOError as e:
  161. print("Error making joined clip: {}".format(e))
  162. import pdb
  163. pdb.set_trace()
  164. clip2 = VideoFileClip(os.path.join(workingdir, video2))
  165. final = CompositeVideoClip([clip1,
  166. clip2.set_start(clip1.end-tr)
  167. .crossfadein(tr)])
  168. outputfile = os.path.join(workingdir, "clip{}J.mkv".format(i))
  169. final.write_videofile(outputfile, codec="libx264", audio_codec="aac")
  170. return outputfile
  171. # Start
  172. import os
  173. if jsonfile:
  174. json_in = parse_json(jsonfile)
  175. # in temporary dir
  176. with tempfile.TemporaryDirectory() as tmpdir:
  177. # make main clips using codec copy
  178. # from clip0.mkv to clip[len(sources)-1]
  179. make_clips(sources=json_in["sources"], outputdir=tmpdir)
  180. # make subclips (main + transition subclips)
  181. # add to concat 'join.txt' as we go
  182. joinfile = os.path.join(tmpdir, "join.txt")
  183. with open(joinfile, "w") as fh:
  184. for i in range(len(json_in["sources"])):
  185. # First clip needs only main + end
  186. if i == 0:
  187. split_first(os.path.join(tmpdir, "clip0.mkv"))
  188. fh.write("file '{}'\n".format(
  189. os.path.join(tmpdir, "clip0M.mkv")))
  190. # End clip needs only start + main
  191. elif i == len(json_in["sources"]):
  192. split_last(os.path.join(tmpdir, "clip{}.mkv".format(i)))
  193. # Join previous clip omega + this clip alpha
  194. joined = transition_join("clip{}O.mkv".format(i-1),
  195. "clip{}A.mkv".format(i),
  196. tmpdir,
  197. i)
  198. fh.write("file '{}'\n".format(joined))
  199. fh.write("file '{}'\n".format(
  200. os.path.join(tmpdir, "clip{}M.mkv".format(i))))
  201. # Others need start + main + end
  202. else:
  203. split_middle(os.path.join(tmpdir, "clip{}.mkv".format(i)))
  204. # Join previous clip omega + this clip alpha
  205. joined = transition_join("clip{}O.mkv".format(i-1),
  206. "clip{}A.mkv".format(i),
  207. tmpdir,
  208. i)
  209. fh.write("file '{}'\n".format(joined))
  210. fh.write("file '{}'\n".format(
  211. os.path.join(tmpdir, "clip{}M.mkv".format(i))))
  212. # Done, join all of the above
  213. print("Making concatenated video: {}".format(json_in["outputfile"]))
  214. with open(os.path.join(tmpdir, "join.txt"), "r") as fh:
  215. for line in fh:
  216. print(line)
  217. concat(workingdir=tmpdir, outputfile=json_in["outputfile"])
  218. if __name__ == "__main__":
  219. import sys
  220. process_highlights(sys.argv[1])