use modal and display shots created one after other
parent
b1bde01b57
commit
940cd1100f
106
auto_splitter.py
106
auto_splitter.py
|
@ -1,36 +1,102 @@
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from vse_toolbox.constants import AUTO_SPLITTER_LOG
|
import bpy
|
||||||
from vse_toolbox import file_utils
|
|
||||||
|
from vse_toolbox import bl_utils
|
||||||
|
|
||||||
|
|
||||||
class AutoSplitter(object):
|
def launch_split(movie_strip, threshold, frame_start=None, frame_end=None):
|
||||||
|
"""Launch ffmpeg command to detect changing frames from a movie strip.
|
||||||
|
|
||||||
def __init__(self, path, fps=None):
|
Args:
|
||||||
|
movie_strip (bpy.types.Sequence): blender sequence strip to detect changes.
|
||||||
|
threshold (float): value of the detection factor (from 0 to 1).
|
||||||
|
frame_start (int, optional): first frame to detect.
|
||||||
|
Defaults to None.
|
||||||
|
frame_end (int, optional): last frame to detect.
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
self.path = path
|
Returns:
|
||||||
self.fps = fps
|
str: ffmpeg command log.
|
||||||
|
"""
|
||||||
|
|
||||||
self.log_path = AUTO_SPLITTER_LOG
|
path = bl_utils.abspath(movie_strip.filepath)
|
||||||
|
fps = bpy.context.scene.render.fps
|
||||||
|
|
||||||
def launch_analysis(self, threshold=0.6):
|
if frame_start is None:
|
||||||
|
frame_start = movie_strip.frame_final_start
|
||||||
|
if frame_end is None:
|
||||||
|
frame_end = movie_strip.frame_final_end
|
||||||
|
|
||||||
ffmpeg_cmd = f"ffmpeg -i {str(self.path)} -filter:v \"select='gt(scene,{threshold})',showinfo\" -f null - 2> {str(self.log_path)}"
|
frame_start -= movie_strip.frame_final_start
|
||||||
print(ffmpeg_cmd)
|
frame_end -= movie_strip.frame_final_start
|
||||||
|
|
||||||
subprocess.call(ffmpeg_cmd, shell=True)
|
# Launch ffmpeg command to split
|
||||||
|
ffmpeg_cmd = get_command(str(path), threshold, frame_start, frame_end, fps)
|
||||||
|
|
||||||
def get_split_times(self, as_frame=True):
|
print(ffmpeg_cmd)
|
||||||
log = file_utils.read_file(self.log_path)
|
process = subprocess.Popen(
|
||||||
|
ffmpeg_cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
timecodes = re.findall(r'pts_time:([\d.]+)', log)
|
return process
|
||||||
|
|
||||||
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]
|
def get_command(path, threshold, frame_start, frame_end, fps):
|
||||||
|
"""Generate the ffmpeg command which detect change from a movie.
|
||||||
|
|
||||||
return timecodes
|
Args:
|
||||||
|
path (_type_): path to detect changes.
|
||||||
|
threshold (_type_): value of the detection factor (from 0 to 1).
|
||||||
|
frame_start (_type_): first frame to detect.
|
||||||
|
frame_end (_type_): last frame to detect.
|
||||||
|
fps (_type_): framerate of the movie.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: ffmpeg command as list for subprocess module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_time = frame_start/fps
|
||||||
|
end_time = frame_end/fps
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ffmpeg',
|
||||||
|
'-i',
|
||||||
|
str(path),
|
||||||
|
'-vf',
|
||||||
|
f"trim=start={start_time}:end={end_time}, select='gt(scene, {threshold})',showinfo",
|
||||||
|
'-f',
|
||||||
|
'null',
|
||||||
|
'-'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_split_time(log, as_frame=True, fps=None):
|
||||||
|
"""Parse ffmpeg command lines to detect the timecode
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log (str): log to parse.
|
||||||
|
as_frame (bool, optional): if wanted the timecode as frame number.
|
||||||
|
Defaults to True.
|
||||||
|
fps (_type_, optional): framerate of the movie (mandatory if as_frame used).
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: _description_
|
||||||
|
"""
|
||||||
|
timecodes = re.findall(r'pts_time:([\d.]+)', log)
|
||||||
|
|
||||||
|
if not timecodes:
|
||||||
|
return
|
||||||
|
|
||||||
|
timecode = timecodes[0]
|
||||||
|
|
||||||
|
if as_frame:
|
||||||
|
# convert timecode to frame number
|
||||||
|
return round(float(timecode) * fps)
|
||||||
|
|
||||||
|
return timecode
|
||||||
|
|
|
@ -37,5 +37,3 @@ ASSET_PREVIEWS = bpy.utils.previews.new()
|
||||||
CASTING_BUFFER = CONFIG_DIR / 'casting.json'
|
CASTING_BUFFER = CONFIG_DIR / 'casting.json'
|
||||||
|
|
||||||
SPREADSHEET = []
|
SPREADSHEET = []
|
||||||
|
|
||||||
AUTO_SPLITTER_LOG = CONFIG_DIR / 'auto_splitter.log'
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ from os.path import expandvars, abspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator
|
from bpy.types import Operator
|
||||||
from bpy.props import (BoolProperty, StringProperty, EnumProperty, FloatProperty)
|
from bpy.props import (BoolProperty, StringProperty, FloatProperty)
|
||||||
|
|
||||||
from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels,
|
from vse_toolbox.sequencer_utils import (get_strips, rename_strips, set_channels,
|
||||||
get_channel_index, new_text_strip, get_strip_at, get_channel_name)
|
get_channel_index, new_text_strip, get_strip_at, get_channel_name)
|
||||||
|
|
||||||
|
from vse_toolbox import auto_splitter
|
||||||
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings
|
from vse_toolbox.bl_utils import get_scene_settings, get_strip_settings
|
||||||
from vse_toolbox.auto_splitter import AutoSplitter
|
from vse_toolbox.sequencer_utils import create_shot_strip
|
||||||
from vse_toolbox.constants import REVIEW_TEMPLATE_BLEND
|
|
||||||
from shutil import copy2
|
from shutil import copy2
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,8 +199,10 @@ class VSETB_OT_auto_split(Operator):
|
||||||
col.prop(self, 'selected_only')
|
col.prop(self, 'selected_only')
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
scn = context.scene
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
strips = get_strips('Movie')
|
strips = get_strips('Movie')
|
||||||
if self.selected_only:
|
if self.selected_only:
|
||||||
strips = context.selected_sequences
|
strips = context.selected_sequences
|
||||||
|
@ -210,23 +212,35 @@ class VSETB_OT_auto_split(Operator):
|
||||||
if strip.type != 'MOVIE':
|
if strip.type != 'MOVIE':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
splitter = AutoSplitter(bpy.path.abspath(strip.filepath))
|
process = auto_splitter.launch_split(strip, self.threshold)
|
||||||
splitter.launch_analysis(self.threshold)
|
|
||||||
|
|
||||||
split_frames = [0]
|
i = 1
|
||||||
split_frames += splitter.get_split_times(as_frame=True)
|
frame_start = 0
|
||||||
|
for line in process.stdout:
|
||||||
|
frame_end = auto_splitter.get_split_time(line, fps=24)
|
||||||
|
|
||||||
for i, frame in enumerate(split_frames):
|
if not frame_end:
|
||||||
|
continue
|
||||||
|
|
||||||
shot_strip = scn.sequence_editor.sequences.new_effect(
|
create_shot_strip(
|
||||||
f'tmp_shot_{str(i).zfill(3)}',
|
f'tmp_shot_{str(i).zfill(3)}',
|
||||||
'COLOR',
|
start=frame_start+strip.frame_final_start,
|
||||||
get_channel_index('Shots'),
|
end=frame_end+strip.frame_final_start
|
||||||
frame_start=frame + strip.frame_final_start,
|
)
|
||||||
frame_end=split_frames[i+1] + strip.frame_final_start if i+1 < len(split_frames) else strip.frame_final_end
|
|
||||||
|
i += 1
|
||||||
|
frame_start = frame_end
|
||||||
|
|
||||||
|
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
|
||||||
|
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
# last strip:
|
||||||
|
create_shot_strip(
|
||||||
|
f'tmp_shot_{str(i).zfill(3)}',
|
||||||
|
start=frame_start+strip.frame_final_start,
|
||||||
|
end=strip.frame_final_end
|
||||||
)
|
)
|
||||||
shot_strip.blend_alpha = 0
|
|
||||||
shot_strip.color = (0.5, 0.5, 0.5)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
|
@ -582,4 +582,19 @@ def update_text_strips(scene):
|
||||||
strip['text_pattern'] = strip.text
|
strip['text_pattern'] = strip.text
|
||||||
|
|
||||||
if 'text_pattern' in strip.keys():
|
if 'text_pattern' in strip.keys():
|
||||||
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
|
strip.text = strip['text_pattern'].format_map(MissingKey(**format_data))
|
||||||
|
|
||||||
|
|
||||||
|
def create_shot_strip(name, start, end):
|
||||||
|
|
||||||
|
shot_strip = bpy.context.scene.sequence_editor.sequences.new_effect(
|
||||||
|
name,
|
||||||
|
'COLOR',
|
||||||
|
get_channel_index('Shots'),
|
||||||
|
frame_start=start,
|
||||||
|
frame_end=end
|
||||||
|
)
|
||||||
|
shot_strip.blend_alpha = 0
|
||||||
|
shot_strip.color = (0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
return shot_strip
|
||||||
|
|
Loading…
Reference in New Issue