Skip to content

Instantly share code, notes, and snippets.

@hrsmwithoutspook
Created October 1, 2023 16:42
Show Gist options
  • Save hrsmwithoutspook/a17ff0e8c95be7eb1bc13c1ef9301609 to your computer and use it in GitHub Desktop.
Save hrsmwithoutspook/a17ff0e8c95be7eb1bc13c1ef9301609 to your computer and use it in GitHub Desktop.
New keyframe generator script (using vpy)
import argparse
import pathlib
import os
import os.path
from collections import deque
import vapoursynth as vs
from vapoursynth import core
from vstools import get_w
parser = argparse.ArgumentParser()
parser.add_argument(
'--use-scxvid', '-sc', action='store_true',
help="Use Scxvid instead of WWXD to detect scene changes."
)
parser.add_argument(
'--use-slices', '-sl', action='store_true',
help="When using Scxvid, speeds things up at the cost of differences in scene detection."
)
parser.add_argument(
'--xvid', '-x', action='store_true',
help='Use Sushi compatible (pseudo-XviD 2pass stat file) format.'
)
parser.add_argument(
'--height', '-dh', default=0, type=int,
help='Height the clip is scaled to. 0 to process at native res.'
)
parser.add_argument(
'--out-file', '-o', default='null', type=str,
help="The file to write scene changes to (Aegisub format); defaults to the same directory as the input video file."
)
parser.add_argument(
'clip',
help="The input video file."
)
args = parser.parse_args()
def resize_clip(clip):
if args.height != 0:
width = get_w(args.height, clip.width / clip.height)
clip = core.resize.Bilinear(clip, width, args.height, format=vs.YUV420P8) # speed up the analysis by resizing first
return clip
def make_keyframes_filename(filename, output_file):
if output_file == "null":
extlen = filename[::-1].find(".") + 1
return filename[:len(filename) - extlen] + "_keyframes.txt"
else:
return output_file
def make_keyframes(clip, use_scxvid):
if use_scxvid:
clip = core.scxvid.Scxvid(clip, use_slices=args.use_slices)
else:
clip = core.wwxd.WWXD(clip)
keyframes = {}
done = 0
def _cb(n: int, f: vs.VideoFrame) -> vs.VideoFrame:
nonlocal done
keyframes[n] = f.props._SceneChangePrev if use_scxvid else f.props.Scenechange # type: ignore
done += 1
percent = float(int(10000 * done // clip.num_frames) / 100)
if done % (clip.num_frames // 27) == 0:
print("Detecting keyframes... {}/{} {:.2f}%".format(done, clip.num_frames, percent))
return f
deque(clip.std.ModifyFrame(clip, _cb).frames(close=True), 0)
print("Done detecting keyframes.\n")
return [n for n in range(clip.num_frames) if keyframes[n]]
def save_keyframes(filename, keyframes, sushi):
with open(filename, "w") as f:
if sushi:
f.write("# XviD 2pass stat file\n")
f.write("\n\n")
else:
f.write("# keyframe format v1\n")
f.write("fps 0\n")
f.write("".join(f"{n}\n" for n in keyframes))
kffilename = make_keyframes_filename(args.clip, args.out_file)
if hasattr(core, 'lsmas'):
clip = core.lsmas.LWLibavSource(args.clip)
elif hasattr(core, 'bs'):
clip = core.bs.VideoSource(args.clip)
elif hasattr(core, 'ffms2'):
clip = core.ffms2.Source(args.clip)
else:
raise CustomRuntimeError(
'You need a source filter! Install one of:\n'
'\tlsmash - https://github.com/AkarinVS/L-SMASH-Works\n'
'\tbs - https://github.com/vapoursynth/bestsource\n'
'\tffms2 - https://github.com/FFMS/ffms2'
)
print("Generating keyframes for {}".format(os.path.basename(args.clip)))
clip = resize_clip(clip)
keyframes = make_keyframes(clip, args.use_scxvid)
save_keyframes(kffilename, keyframes, args.xvid)
print("Written keyframes file to {}".format(os.path.basename(kffilename)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment