From a30e16606b78c2bf25d36d4014259b681ae9b0d6 Mon Sep 17 00:00:00 2001 From: "florentin.luce" Date: Fri, 24 May 2024 09:44:19 +0200 Subject: [PATCH] Split auto (UI + Core) - 1st version --- auto_splitter.py | 36 ++++++++++++++++++++++++++ constants.py | 4 ++- file_utils.py | 4 +-- operators/sequencer.py | 59 +++++++++++++++++++++++++++++++++++++++++- ui/panels.py | 1 + 5 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 auto_splitter.py diff --git a/auto_splitter.py b/auto_splitter.py new file mode 100644 index 0000000..84da28b --- /dev/null +++ b/auto_splitter.py @@ -0,0 +1,36 @@ +import re +import subprocess + +from vse_toolbox.constants import AUTO_SPLITTER_LOG +from vse_toolbox import file_utils + + +class AutoSplitter(object): + + def __init__(self, paths, fps=None): + + self.paths = paths + self.fps = fps + + self.log_path = AUTO_SPLITTER_LOG + + def launch_analysis(self, threshold=0.6): + + ffmpeg_cmd = f"ffmpeg -i {str(self.paths[0])} -filter:v \"select='gt(scene,{threshold})',showinfo\" -f null - 2> {str(self.log_path)}" + print(ffmpeg_cmd) + + subprocess.call(ffmpeg_cmd, shell=True) + + def get_split_times(self, as_frame=True): + log = file_utils.read_file(self.log_path) + + timecodes = re.findall(r'pts_time:([\d.]+)', log) + + if as_frame: + # convert timecode to frame number + if not self.fps: + self.fps = float(re.findall(r'([\d]+) fps', log)[0]) + + return [round(float(time) * self.fps) for time in timecodes] + + return timecodes diff --git a/constants.py b/constants.py index fb7d94e..7180420 100644 --- a/constants.py +++ b/constants.py @@ -36,4 +36,6 @@ ASSET_PREVIEWS = bpy.utils.previews.new() CASTING_BUFFER = CONFIG_DIR / 'casting.json' -SPREADSHEET = [] \ No newline at end of file +SPREADSHEET = [] + +AUTO_SPLITTER_LOG = CONFIG_DIR / 'auto_splitter.log' diff --git a/file_utils.py b/file_utils.py index 2e70c5a..b59f18e 100644 --- a/file_utils.py +++ b/file_utils.py @@ -89,9 +89,9 @@ def norm_name(string, separator='_', format=str.lower, padding=0): return string def read_file(path): - '''Read a file with an extension in (json, yaml, yml, txt)''' + '''Read a file with an extension in (json, yaml, yml, txt, log)''' - exts = ('.json', '.yaml', '.yml', '.txt') + exts = ('.json', '.yaml', '.yml', '.txt', '.log') if not path: print('Try to read empty file') diff --git a/operators/sequencer.py b/operators/sequencer.py index 7052703..b49f568 100644 --- a/operators/sequencer.py +++ b/operators/sequencer.py @@ -2,12 +2,13 @@ from os.path import expandvars, abspath from pathlib import Path import bpy from bpy.types import Operator -from bpy.props import (BoolProperty, StringProperty, EnumProperty) +from bpy.props import (BoolProperty, StringProperty, EnumProperty, FloatProperty) from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels, get_channel_index, new_text_strip, get_strip_at, get_channel_name) from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings +from vse_toolbox.auto_splitter import AutoSplitter from vse_toolbox.constants import REVIEW_TEMPLATE_BLEND from shutil import copy2 @@ -172,6 +173,61 @@ class VSETB_OT_set_sequencer(Operator): return {"FINISHED"} +class VSETB_OT_auto_split(Operator): + """Launch subprocess with ffmpeg and python to find and create each + shots strips from video source""" + + bl_idname = "vse_toolbox.auto_split" + bl_label = "Auto Split" + bl_description = "Generate shots strips" + bl_options = {"REGISTER", "UNDO"} + + threshold: FloatProperty(name="Threshold", default=0.6, min=0, max=1) + selected_only: BoolProperty(name="Selected Only", default=True) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + + col.prop(self, 'threshold') + col.prop(self, 'selected_only') + + def execute(self, context): + scn = context.scene + first_frame = int(scn.frame_start) + + strips = get_strips('Movie') + if self.selected_only: + strips = context.selected_sequences + + sources = list(set([bpy.path.abspath(strip.filepath) for strip in strips if strip.type == 'MOVIE'])) + + splitter = AutoSplitter(sources) + splitter.launch_analysis(self.threshold) + + split_frames = [0] + split_frames += splitter.get_split_times(as_frame=True) + + for i, frame in enumerate(split_frames): + strip = scn.sequence_editor.sequences.new_effect( + f'tmp_shot_{str(i).zfill(3)}', + 'COLOR', + get_channel_index('Shots'), + frame_start=frame + first_frame, + frame_end=split_frames[i+1] + first_frame if i < len(split_frames) else strips[0].frame_final_end + ) + strip.blend_alpha = 0 + strip.color = (0.5, 0.5, 0.5) + + return {'FINISHED'} + + class VSETB_OT_set_stamps(Operator): bl_idname = "vse_toolbox.set_stamps" bl_label = "Set Stamps" @@ -625,6 +681,7 @@ def unregister_keymaps(): classes = ( VSETB_OT_rename, VSETB_OT_set_sequencer, + VSETB_OT_auto_split, VSETB_OT_set_stamps, VSETB_OT_show_waveform, VSETB_OT_previous_shot, diff --git a/ui/panels.py b/ui/panels.py index cf3c13d..c4e3172 100644 --- a/ui/panels.py +++ b/ui/panels.py @@ -179,6 +179,7 @@ class VSETB_PT_sequencer(VSETB_main, Panel): col = layout.column() col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER') + col.operator('vse_toolbox.auto_split', text='Auto Split Shots') col.operator('vse_toolbox.strips_rename', text=f'Rename {channel}', icon='SORTALPHA') col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR') col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE')