Compare commits
No commits in common. "8e21dcc6f584e2a53974341c431fd00e6ae74574" and "b68b43e3e1c0c289ea2de3e2991cc99b438ae1dc" have entirely different histories.
8e21dcc6f5
...
b68b43e3e1
104
auto_splitter.py
104
auto_splitter.py
|
@ -1,104 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
from vse_toolbox import bl_utils
|
|
||||||
|
|
||||||
|
|
||||||
def launch_split(movie_strip, threshold, frame_start=None, frame_end=None):
|
|
||||||
"""Launch ffmpeg command to detect changing frames from a movie strip.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: ffmpeg command log.
|
|
||||||
"""
|
|
||||||
|
|
||||||
path = bl_utils.abspath(movie_strip.filepath)
|
|
||||||
fps = bpy.context.scene.render.fps
|
|
||||||
|
|
||||||
if frame_start is None:
|
|
||||||
frame_start = 0
|
|
||||||
if frame_end is None:
|
|
||||||
frame_end = movie_strip.frame_duration
|
|
||||||
|
|
||||||
frame_start = frame_start - movie_strip.frame_start
|
|
||||||
|
|
||||||
#frame_start += movie_strip.frame_offset_start
|
|
||||||
#frame_end -= movie_strip.frame_offset_end
|
|
||||||
|
|
||||||
# Launch ffmpeg command to split
|
|
||||||
ffmpeg_cmd = get_command(str(path), threshold, frame_start, frame_end, fps)
|
|
||||||
|
|
||||||
print(ffmpeg_cmd)
|
|
||||||
process = subprocess.Popen(
|
|
||||||
ffmpeg_cmd,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
universal_newlines=True)
|
|
||||||
|
|
||||||
return process
|
|
||||||
|
|
||||||
|
|
||||||
def get_command(path, threshold, frame_start, frame_end, fps):
|
|
||||||
"""Generate the ffmpeg command which detect change from a movie.
|
|
||||||
|
|
||||||
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
|
|
|
@ -36,4 +36,4 @@ ASSET_PREVIEWS = bpy.utils.previews.new()
|
||||||
|
|
||||||
CASTING_BUFFER = CONFIG_DIR / 'casting.json'
|
CASTING_BUFFER = CONFIG_DIR / 'casting.json'
|
||||||
|
|
||||||
SPREADSHEET = []
|
SPREADSHEET = []
|
|
@ -89,9 +89,9 @@ def norm_name(string, separator='_', format=str.lower, padding=0):
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def read_file(path):
|
def read_file(path):
|
||||||
'''Read a file with an extension in (json, yaml, yml, txt, log)'''
|
'''Read a file with an extension in (json, yaml, yml, txt)'''
|
||||||
|
|
||||||
exts = ('.json', '.yaml', '.yml', '.txt', '.log')
|
exts = ('.json', '.yaml', '.yml', '.txt')
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
print('Try to read empty file')
|
print('Try to read empty file')
|
||||||
|
|
|
@ -2,15 +2,13 @@ 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, FloatProperty,
|
from bpy.props import (BoolProperty, StringProperty, EnumProperty)
|
||||||
IntProperty, EnumProperty)
|
|
||||||
|
|
||||||
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)
|
||||||
create_shot_strip)
|
|
||||||
|
|
||||||
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.constants import REVIEW_TEMPLATE_BLEND
|
||||||
from shutil import copy2
|
from shutil import copy2
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,108 +172,6 @@ class VSETB_OT_set_sequencer(Operator):
|
||||||
return {"FINISHED"}
|
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)
|
|
||||||
frame_first: IntProperty(name='Start Split')
|
|
||||||
frame_last: IntProperty(name='End Split')
|
|
||||||
movie_channel_name: EnumProperty(
|
|
||||||
items=lambda self, ctx: ((c.name, c.name, '') for c in ctx.scene.sequence_editor.channels),
|
|
||||||
name='Movie Channel')
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
|
|
||||||
self.frame_first = context.scene.frame_start
|
|
||||||
self.frame_last = context.scene.frame_end
|
|
||||||
|
|
||||||
if context.selected_sequences:
|
|
||||||
self.frame_first = min([s.frame_final_start for s in context.selected_sequences])
|
|
||||||
self.frame_last = max([s.frame_final_end for s in context.selected_sequences])
|
|
||||||
|
|
||||||
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, 'movie_channel_name')
|
|
||||||
|
|
||||||
split_col = col.column(align=True)
|
|
||||||
split_col.prop(self, 'frame_first', text='Frame Split First')
|
|
||||||
split_col.prop(self, 'frame_last', text='Last')
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
def modal(self, context, event):
|
|
||||||
|
|
||||||
strips = get_strips(channel=self.movie_channel_name)
|
|
||||||
|
|
||||||
i = 1
|
|
||||||
frame_start = self.frame_first
|
|
||||||
for strip in strips:
|
|
||||||
|
|
||||||
if strip.type != 'MOVIE':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip strip outside the frame range to create shot from.
|
|
||||||
if strip.frame_final_start >= self.frame_last or strip.frame_final_end <= self.frame_first:
|
|
||||||
continue
|
|
||||||
|
|
||||||
process = auto_splitter.launch_split(strip, self.threshold, frame_start=self.frame_first, frame_end=self.frame_last)
|
|
||||||
|
|
||||||
for line in process.stdout:
|
|
||||||
|
|
||||||
# Get frame split from the movie timeline (not from blender strips timeline)
|
|
||||||
frame_end = auto_splitter.get_split_time(line, fps=24)
|
|
||||||
|
|
||||||
if not frame_end:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Convert movie frame to strips frame
|
|
||||||
if frame_start+int(strip.frame_start) < self.frame_first:
|
|
||||||
frame_start = self.frame_first
|
|
||||||
|
|
||||||
frame_end += int(strip.frame_final_start)
|
|
||||||
if frame_end > self.frame_last:
|
|
||||||
frame_end = self.frame_last
|
|
||||||
|
|
||||||
create_shot_strip(
|
|
||||||
f'tmp_shot_{str(i).zfill(3)}',
|
|
||||||
start=frame_start,
|
|
||||||
end=frame_end
|
|
||||||
)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
frame_start = frame_end
|
|
||||||
|
|
||||||
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
|
|
||||||
|
|
||||||
process.wait()
|
|
||||||
|
|
||||||
# Last strip:
|
|
||||||
if frame_start < self.frame_last:
|
|
||||||
create_shot_strip(
|
|
||||||
f'tmp_shot_{str(i).zfill(3)}',
|
|
||||||
start=frame_start,
|
|
||||||
end=self.frame_last
|
|
||||||
)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class VSETB_OT_set_stamps(Operator):
|
class VSETB_OT_set_stamps(Operator):
|
||||||
bl_idname = "vse_toolbox.set_stamps"
|
bl_idname = "vse_toolbox.set_stamps"
|
||||||
bl_label = "Set Stamps"
|
bl_label = "Set Stamps"
|
||||||
|
@ -729,7 +625,6 @@ def unregister_keymaps():
|
||||||
classes = (
|
classes = (
|
||||||
VSETB_OT_rename,
|
VSETB_OT_rename,
|
||||||
VSETB_OT_set_sequencer,
|
VSETB_OT_set_sequencer,
|
||||||
VSETB_OT_auto_split,
|
|
||||||
VSETB_OT_set_stamps,
|
VSETB_OT_set_stamps,
|
||||||
VSETB_OT_show_waveform,
|
VSETB_OT_show_waveform,
|
||||||
VSETB_OT_previous_shot,
|
VSETB_OT_previous_shot,
|
||||||
|
|
|
@ -582,19 +582,4 @@ 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
|
|
|
@ -179,7 +179,6 @@ class VSETB_PT_sequencer(VSETB_main, Panel):
|
||||||
|
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
col.operator('vse_toolbox.set_sequencer', text='Set-Up Sequencer', icon='SEQ_SEQUENCER')
|
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.strips_rename', text=f'Rename {channel}', icon='SORTALPHA')
|
||||||
col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR')
|
col.operator('vse_toolbox.set_stamps', text='Set Stamps', icon='COLOR')
|
||||||
col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE')
|
col.operator("vse_toolbox.collect_files", text='Collect Files', icon='PACKAGE')
|
||||||
|
|
Loading…
Reference in New Issue