Collection of scripts to make automatic video highlights of varying quality
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

efficientjson.py 10 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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])