Update power sequencer to v2.0.1
Changelog: https://github.com/GDQuest/blender-power-sequencer/blob/master/CHANGELOG.md#power-sequencer-201 Commit range: https://github.com/GDQuest/blender-power-sequencer/compare/1.5.0...2.0.1
This commit is contained in:
parent
b482ca0078
commit
cd176b2617
|
@ -38,11 +38,11 @@ bl_info = {
|
|||
"name": "Power Sequencer",
|
||||
"description": "Video editing tools for content creators",
|
||||
"author": "Nathan Lovato",
|
||||
"version": (1, 5, 1),
|
||||
"blender": (2, 90, 1),
|
||||
"version": (1, 5, 0),
|
||||
"blender": (2, 81, 0),
|
||||
"location": "Sequencer",
|
||||
"tracker_url": "https://github.com/GDquest/Blender-power-sequencer/issues",
|
||||
"doc_url": "https://www.gdquest.com/docs/documentation/power-sequencer/",
|
||||
"wiki_url": "https://www.gdquest.com/docs/documentation/power-sequencer/",
|
||||
"support": "COMMUNITY",
|
||||
"category": "Sequencer",
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ def register():
|
|||
keymaps = register_shortcuts(classes_operator)
|
||||
addon_keymaps += keymaps
|
||||
|
||||
# print("Registered {} with {} modules".format(bl_info["name"], len(modules)))
|
||||
print("Registered {} with {} modules".format(bl_info["name"], len(modules)))
|
||||
|
||||
|
||||
def unregister():
|
||||
|
@ -104,4 +104,4 @@ def unregister():
|
|||
unregister_properties()
|
||||
unregister_handlers()
|
||||
|
||||
# print("Unregistered {}".format(bl_info["name"]))
|
||||
print("Unregistered {}".format(bl_info["name"]))
|
||||
|
|
|
@ -55,7 +55,7 @@ class PowerSequencerPreferences(bpy.types.AddonPreferences):
|
|||
error_message, info = "", ""
|
||||
try:
|
||||
info: str = subprocess.check_output([path, "-version"]).decode("utf-8")
|
||||
info = info[:info.find("Copyright")]
|
||||
info = info[: info.find("Copyright")]
|
||||
print(info)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
error_message = "Path `{}` is not a valid ffmpeg executable".format(path)
|
||||
|
|
|
@ -22,17 +22,13 @@ def get_operator_classes():
|
|||
"""Returns the list of operators in the add-on"""
|
||||
this_file = os.path.dirname(__file__)
|
||||
module_files = [
|
||||
f
|
||||
for f in os.listdir(this_file)
|
||||
if f.endswith(".py") and not f.startswith("__init__")
|
||||
f for f in os.listdir(this_file) if f.endswith(".py") and not f.startswith("__init__")
|
||||
]
|
||||
module_paths = ["." + os.path.splitext(f)[0] for f in module_files]
|
||||
classes = []
|
||||
for path in module_paths:
|
||||
module = importlib.import_module(path, package="power_sequencer.operators")
|
||||
operator_names = [
|
||||
entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT")
|
||||
]
|
||||
operator_names = [entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT")]
|
||||
classes.extend([getattr(module, name) for name in operator_names])
|
||||
return classes
|
||||
|
||||
|
@ -41,9 +37,7 @@ doc = {
|
|||
"sequencer.refresh_all": {
|
||||
"name": "Refresh All",
|
||||
"description": "",
|
||||
"shortcuts": [
|
||||
({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")
|
||||
],
|
||||
"shortcuts": [({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")],
|
||||
"demo": "",
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
|
|
|
@ -14,17 +14,12 @@
|
|||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
from operator import attrgetter
|
||||
|
||||
from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
|
||||
from .utils.functions import (
|
||||
slice_selection,
|
||||
get_frame_range,
|
||||
get_channel_range,
|
||||
trim_strips,
|
||||
find_strips_in_range,
|
||||
)
|
||||
import bpy
|
||||
|
||||
from .utils.doc import doc_brief, doc_description, doc_idname, doc_name
|
||||
from .utils.functions import find_strips_in_range, move_selection, trim_strips
|
||||
|
||||
|
||||
class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator):
|
||||
|
@ -39,7 +34,7 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator):
|
|||
"shortcuts": [
|
||||
(
|
||||
{"type": "UP_ARROW", "value": "PRESS", "alt": True},
|
||||
{"direction": "up"},
|
||||
{"direction": "up", "trim_target_channel": False},
|
||||
"Move to Open Channel Above",
|
||||
),
|
||||
(
|
||||
|
@ -49,7 +44,7 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator):
|
|||
),
|
||||
(
|
||||
{"type": "DOWN_ARROW", "value": "PRESS", "alt": True},
|
||||
{"direction": "down"},
|
||||
{"direction": "down", "trim_target_channel": False},
|
||||
"Move to Open Channel Below",
|
||||
),
|
||||
(
|
||||
|
@ -79,35 +74,71 @@ class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator):
|
|||
description="Trim strips to make space in the target channel",
|
||||
default=False,
|
||||
)
|
||||
keep_selection_offset: bpy.props.BoolProperty(
|
||||
name="Keep selection offset",
|
||||
description="The selected strips preserve their relative positions",
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_sequences
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
max_channel = 32
|
||||
min_channel = 1
|
||||
|
||||
if self.direction == "up":
|
||||
channel_offset = +1
|
||||
limit_channel = max_channel
|
||||
comparison_function = min
|
||||
|
||||
if self.direction == "down":
|
||||
channel_offset = -1
|
||||
limit_channel = min_channel
|
||||
comparison_function = max
|
||||
|
||||
selection = [s for s in context.selected_sequences if not s.lock]
|
||||
|
||||
if not selection:
|
||||
return {"FINISHED"}
|
||||
|
||||
selection_blocks = slice_selection(context, selection)
|
||||
for block in selection_blocks:
|
||||
sequences = sorted(block, key=attrgetter("channel", "frame_final_start"))
|
||||
frame_start, frame_end = get_frame_range(sequences)
|
||||
channel_start, channel_end = get_channel_range(sequences)
|
||||
sequences = sorted(selection, key=attrgetter("channel", "frame_final_start"))
|
||||
if self.direction == "up":
|
||||
sequences = [s for s in reversed(sequences)]
|
||||
|
||||
if self.trim_target_channel:
|
||||
to_delete, to_trim = find_strips_in_range(frame_start, frame_end, context.sequences)
|
||||
channel_trim = (
|
||||
channel_end + 1 if self.direction == "up" else max(1, channel_start - 1)
|
||||
)
|
||||
to_trim = [s for s in to_trim if s.channel == channel_trim]
|
||||
to_delete = [s for s in to_delete if s.channel == channel_trim]
|
||||
trim_strips(context, frame_start, frame_end, to_trim, to_delete)
|
||||
head = sequences[0]
|
||||
if not self.keep_selection_offset or (
|
||||
head.channel != limit_channel and self.keep_selection_offset
|
||||
):
|
||||
for s in sequences:
|
||||
if self.trim_target_channel:
|
||||
channel_trim = s.channel + channel_offset
|
||||
strips_in_trim_channel = [
|
||||
sequence
|
||||
for sequence in context.sequences
|
||||
if (sequence.channel == channel_trim)
|
||||
]
|
||||
if strips_in_trim_channel:
|
||||
to_delete, to_trim = find_strips_in_range(
|
||||
s.frame_final_start, s.frame_final_end, strips_in_trim_channel
|
||||
)
|
||||
trim_strips(
|
||||
context, s.frame_final_start, s.frame_final_end, to_trim, to_delete
|
||||
)
|
||||
|
||||
if self.direction == "up":
|
||||
for s in reversed(sequences):
|
||||
s.channel += 1
|
||||
elif self.direction == "down":
|
||||
for s in sequences:
|
||||
s.channel = max(1, s.channel - 1)
|
||||
if not self.keep_selection_offset:
|
||||
s.channel = comparison_function(limit_channel, s.channel + channel_offset)
|
||||
if s.channel == limit_channel:
|
||||
move_selection(context, [s], 0, 0)
|
||||
|
||||
if self.keep_selection_offset:
|
||||
start_frame = head.frame_final_start
|
||||
x_difference = 0
|
||||
while not head.channel == limit_channel:
|
||||
move_selection(context, sequences, -x_difference, channel_offset)
|
||||
x_difference = head.frame_final_start - start_frame
|
||||
if x_difference == 0:
|
||||
break
|
||||
return {"FINISHED"}
|
||||
|
|
|
@ -30,9 +30,7 @@ class POWER_SEQUENCER_OT_split_strips_under_cursor(bpy.types.Operator):
|
|||
"name": doc_name(__qualname__),
|
||||
"demo": "https://i.imgur.com/ZyEd0jD.gif",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
({"type": "K", "value": "PRESS"}, {}, "Cut All Strips Under Cursor")
|
||||
],
|
||||
"shortcuts": [({"type": "K", "value": "PRESS"}, {}, "Cut All Strips Under Cursor")],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
bl_idname = doc_idname(__qualname__)
|
||||
|
@ -66,10 +64,5 @@ class POWER_SEQUENCER_OT_split_strips_under_cursor(bpy.types.Operator):
|
|||
deselect = False
|
||||
if deselect:
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
(
|
||||
context.selected_sequences
|
||||
or bpy.ops.power_sequencer.select_strips_under_cursor()
|
||||
)
|
||||
return bpy.ops.sequencer.split(
|
||||
frame=context.scene.frame_current, side=self.side
|
||||
)
|
||||
(context.selected_sequences or bpy.ops.power_sequencer.select_strips_under_cursor())
|
||||
return bpy.ops.sequencer.split(frame=context.scene.frame_current, side=self.side)
|
||||
|
|
|
@ -50,9 +50,7 @@ class POWER_SEQUENCER_OT_delete_direct(bpy.types.Operator):
|
|||
bl_description = doc_brief(doc["description"])
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
is_removing_transitions: bpy.props.BoolProperty(
|
||||
name="Remove Transitions", default=False
|
||||
)
|
||||
is_removing_transitions: bpy.props.BoolProperty(name="Remove Transitions", default=False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
@ -61,17 +59,12 @@ class POWER_SEQUENCER_OT_delete_direct(bpy.types.Operator):
|
|||
def invoke(self, context, event):
|
||||
frame, channel = get_mouse_frame_and_channel(context, event)
|
||||
if not context.selected_sequences:
|
||||
bpy.ops.power_sequencer.select_closest_to_mouse(
|
||||
frame=frame, channel=channel
|
||||
)
|
||||
bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
selection = context.selected_sequences
|
||||
if (
|
||||
self.is_removing_transitions
|
||||
and bpy.ops.power_sequencer.transitions_remove.poll()
|
||||
):
|
||||
if self.is_removing_transitions and bpy.ops.power_sequencer.transitions_remove.poll():
|
||||
bpy.ops.power_sequencer.transitions_remove()
|
||||
bpy.ops.sequencer.delete()
|
||||
|
||||
|
|
|
@ -33,11 +33,7 @@ class POWER_SEQUENCER_OT_expand_to_surrounding_cuts(bpy.types.Operator):
|
|||
"demo": "",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
(
|
||||
{"type": "E", "value": "PRESS", "ctrl": True},
|
||||
{},
|
||||
"Expand to Surrounding Cuts",
|
||||
)
|
||||
({"type": "E", "value": "PRESS", "ctrl": True}, {}, "Expand to Surrounding Cuts",)
|
||||
],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
|
@ -68,37 +64,24 @@ class POWER_SEQUENCER_OT_expand_to_surrounding_cuts(bpy.types.Operator):
|
|||
sequences_frame_start = min(
|
||||
sequences, key=lambda s: s.frame_final_start
|
||||
).frame_final_start
|
||||
sequences_frame_end = max(
|
||||
sequences, key=lambda s: s.frame_final_end
|
||||
).frame_final_end
|
||||
sequences_frame_end = max(sequences, key=lambda s: s.frame_final_end).frame_final_end
|
||||
|
||||
frame_left, frame_right = find_closest_cuts(
|
||||
context, sequences_frame_start, sequences_frame_end
|
||||
)
|
||||
if (
|
||||
sequences_frame_start == frame_left
|
||||
and sequences_frame_end == frame_right
|
||||
):
|
||||
if sequences_frame_start == frame_left and sequences_frame_end == frame_right:
|
||||
continue
|
||||
|
||||
to_extend_left = [
|
||||
s for s in sequences if s.frame_final_start == sequences_frame_start
|
||||
]
|
||||
to_extend_right = [
|
||||
s for s in sequences if s.frame_final_end == sequences_frame_end
|
||||
]
|
||||
to_extend_left = [s for s in sequences if s.frame_final_start == sequences_frame_start]
|
||||
to_extend_right = [s for s in sequences if s.frame_final_end == sequences_frame_end]
|
||||
|
||||
for s in to_extend_left:
|
||||
s.frame_final_start = (
|
||||
frame_left
|
||||
if frame_left < sequences_frame_start
|
||||
else sequences_frame_start
|
||||
frame_left if frame_left < sequences_frame_start else sequences_frame_start
|
||||
)
|
||||
for s in to_extend_right:
|
||||
s.frame_final_end = (
|
||||
frame_right
|
||||
if frame_right > sequences_frame_end
|
||||
else sequences_frame_end
|
||||
frame_right if frame_right > sequences_frame_end else sequences_frame_end
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
@ -110,8 +93,6 @@ def find_closest_cuts(context, frame_min, frame_max):
|
|||
).frame_final_end
|
||||
frame_right = min(
|
||||
context.sequences,
|
||||
key=lambda s: s.frame_final_start
|
||||
if s.frame_final_start >= frame_max
|
||||
else 1000000,
|
||||
key=lambda s: s.frame_final_start if s.frame_final_start >= frame_max else 1000000,
|
||||
).frame_final_start
|
||||
return frame_left, frame_right
|
||||
|
|
|
@ -51,10 +51,7 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator):
|
|||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
duration_seconds: bpy.props.FloatProperty(
|
||||
name="Fade Duration",
|
||||
description="Duration of the fade in seconds",
|
||||
default=1.0,
|
||||
min=0.01,
|
||||
name="Fade Duration", description="Duration of the fade in seconds", default=1.0, min=0.01,
|
||||
)
|
||||
type: bpy.props.EnumProperty(
|
||||
items=[
|
||||
|
@ -98,12 +95,8 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator):
|
|||
if s.frame_final_start < context.scene.frame_current < s.frame_final_end
|
||||
]
|
||||
|
||||
max_duration = min(
|
||||
sequences, key=lambda s: s.frame_final_duration
|
||||
).frame_final_duration
|
||||
max_duration = (
|
||||
floor(max_duration / 2.0) if self.type == "IN_OUT" else max_duration
|
||||
)
|
||||
max_duration = min(sequences, key=lambda s: s.frame_final_duration).frame_final_duration
|
||||
max_duration = floor(max_duration / 2.0) if self.type == "IN_OUT" else max_duration
|
||||
|
||||
faded_sequences = []
|
||||
for sequence in sequences:
|
||||
|
@ -113,15 +106,9 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator):
|
|||
if not self.is_long_enough(sequence, duration):
|
||||
continue
|
||||
|
||||
animated_property = (
|
||||
"volume" if hasattr(sequence, "volume") else "blend_alpha"
|
||||
)
|
||||
fade_fcurve = fade_find_or_create_fcurve(
|
||||
context, sequence, animated_property
|
||||
)
|
||||
fades = self.calculate_fades(
|
||||
sequence, fade_fcurve, animated_property, duration
|
||||
)
|
||||
animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha"
|
||||
fade_fcurve = fade_find_or_create_fcurve(context, sequence, animated_property)
|
||||
fades = self.calculate_fades(sequence, fade_fcurve, animated_property, duration)
|
||||
fade_animation_clear(context, fade_fcurve, fades)
|
||||
fade_animation_create(fade_fcurve, fades)
|
||||
faded_sequences.append(sequence)
|
||||
|
@ -129,9 +116,7 @@ class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator):
|
|||
sequence_string = "sequence" if len(faded_sequences) == 1 else "sequences"
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"Added fade animation to {} {}.".format(
|
||||
len(faded_sequences), sequence_string
|
||||
),
|
||||
"Added fade animation to {} {}.".format(len(faded_sequences), sequence_string),
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
@ -232,13 +217,9 @@ class Fade:
|
|||
|
||||
if type == "IN":
|
||||
self.start = Vector((sequence.frame_final_start, 0.0))
|
||||
self.end = Vector(
|
||||
(sequence.frame_final_start + self.duration, self.max_value)
|
||||
)
|
||||
self.end = Vector((sequence.frame_final_start + self.duration, self.max_value))
|
||||
elif type == "OUT":
|
||||
self.start = Vector(
|
||||
(sequence.frame_final_end - self.duration, self.max_value)
|
||||
)
|
||||
self.start = Vector((sequence.frame_final_end - self.duration, self.max_value))
|
||||
self.end = Vector((sequence.frame_final_end, 0.0))
|
||||
|
||||
def calculate_max_value(self, sequence, fade_fcurve):
|
||||
|
@ -253,15 +234,11 @@ class Fade:
|
|||
else:
|
||||
if self.type == "IN":
|
||||
fade_end = sequence.frame_final_start + self.duration
|
||||
keyframes = (
|
||||
k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end
|
||||
)
|
||||
keyframes = (k for k in fade_fcurve.keyframe_points if k.co[0] >= fade_end)
|
||||
if self.type == "OUT":
|
||||
fade_start = sequence.frame_final_end - self.duration
|
||||
keyframes = (
|
||||
k
|
||||
for k in reversed(fade_fcurve.keyframe_points)
|
||||
if k.co[0] <= fade_start
|
||||
k for k in reversed(fade_fcurve.keyframe_points) if k.co[0] <= fade_start
|
||||
)
|
||||
try:
|
||||
max_value = next(keyframes).co[1]
|
||||
|
@ -275,6 +252,4 @@ class Fade:
|
|||
|
||||
|
||||
def calculate_duration_frames(context, duration_seconds):
|
||||
return round(
|
||||
duration_seconds * context.scene.render.fps / context.scene.render.fps_base
|
||||
)
|
||||
return round(duration_seconds * context.scene.render.fps / context.scene.render.fps_base)
|
||||
|
|
|
@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_fade_clear(bpy.types.Operator):
|
|||
"demo": "",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
(
|
||||
{"type": "F", "value": "PRESS", "alt": True, "ctrl": True},
|
||||
{},
|
||||
"Clear Fades",
|
||||
)
|
||||
({"type": "F", "value": "PRESS", "alt": True, "ctrl": True}, {}, "Clear Fades",)
|
||||
],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
|
@ -53,9 +49,7 @@ class POWER_SEQUENCER_OT_fade_clear(bpy.types.Operator):
|
|||
fcurves = context.scene.animation_data.action.fcurves
|
||||
|
||||
for sequence in context.selected_sequences:
|
||||
animated_property = (
|
||||
"volume" if hasattr(sequence, "volume") else "blend_alpha"
|
||||
)
|
||||
animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha"
|
||||
data_path = sequence.path_from_id() + "." + animated_property
|
||||
fcurve_map = {
|
||||
curve.data_path: curve
|
||||
|
|
|
@ -71,9 +71,7 @@ class POWER_SEQUENCER_OT_gap_remove(bpy.types.Operator):
|
|||
else context.sequences
|
||||
)
|
||||
sequences = [
|
||||
s
|
||||
for s in sequences
|
||||
if s.frame_final_start >= frame or s.frame_final_end > frame
|
||||
s for s in sequences if s.frame_final_start >= frame or s.frame_final_end > frame
|
||||
]
|
||||
sequence_blocks = slice_selection(context, sequences)
|
||||
if not sequence_blocks:
|
||||
|
@ -100,18 +98,12 @@ class POWER_SEQUENCER_OT_gap_remove(bpy.types.Operator):
|
|||
Finds and returns the frame at which the gap starts.
|
||||
Takes a list sequences sorted by frame_final_start.
|
||||
"""
|
||||
strips_start = min(
|
||||
sorted_sequences, key=attrgetter("frame_final_start")
|
||||
).frame_final_start
|
||||
strips_end = max(
|
||||
sorted_sequences, key=attrgetter("frame_final_end")
|
||||
).frame_final_end
|
||||
strips_start = min(sorted_sequences, key=attrgetter("frame_final_start")).frame_final_start
|
||||
strips_end = max(sorted_sequences, key=attrgetter("frame_final_end")).frame_final_end
|
||||
|
||||
gap_frame = -1
|
||||
if strips_start > frame:
|
||||
strips_before_frame_start = [
|
||||
s for s in context.sequences if s.frame_final_end <= frame
|
||||
]
|
||||
strips_before_frame_start = [s for s in context.sequences if s.frame_final_end <= frame]
|
||||
frame_target = 0
|
||||
if strips_before_frame_start:
|
||||
frame_target = max(
|
||||
|
|
|
@ -77,9 +77,7 @@ class POWER_SEQUENCER_OT_make_hold_frame(bpy.types.Operator):
|
|||
try:
|
||||
next_strip_start = next(
|
||||
s
|
||||
for s in sorted(
|
||||
context.sequences, key=operator.attrgetter("frame_final_start")
|
||||
)
|
||||
for s in sorted(context.sequences, key=operator.attrgetter("frame_final_start"))
|
||||
if s.frame_final_start > active.frame_final_end
|
||||
).frame_final_start
|
||||
offset = next_strip_start - active.frame_final_end
|
||||
|
@ -90,9 +88,7 @@ class POWER_SEQUENCER_OT_make_hold_frame(bpy.types.Operator):
|
|||
source_blend_type = active.blend_type
|
||||
sequencer.split(frame=scene.frame_current, type="SOFT", side="RIGHT")
|
||||
transform.seq_slide(value=(offset, 0))
|
||||
sequencer.split(
|
||||
frame=scene.frame_current + offset + 1, type="SOFT", side="LEFT"
|
||||
)
|
||||
sequencer.split(frame=scene.frame_current + offset + 1, type="SOFT", side="LEFT")
|
||||
transform.seq_slide(value=(-offset, 0))
|
||||
|
||||
sequencer.meta_make()
|
||||
|
|
|
@ -67,7 +67,6 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator):
|
|||
context.window.scene.name = context.selected_sequences[0].name
|
||||
new_scene_name = context.window.scene.name
|
||||
|
||||
|
||||
###after full copy also unselected strips are in the sequencer... Delete those strips
|
||||
bpy.ops.sequencer.select_all(action="INVERT")
|
||||
bpy.ops.power_sequencer.delete_direct()
|
||||
|
@ -85,8 +84,10 @@ class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator):
|
|||
|
||||
bpy.ops.power_sequencer.delete_direct()
|
||||
bpy.ops.sequencer.scene_strip_add(
|
||||
frame_start=selection_start_frame, channel=selection_start_channel, scene=new_scene_name
|
||||
frame_start=selection_start_frame,
|
||||
channel=selection_start_channel,
|
||||
scene=new_scene_name,
|
||||
)
|
||||
scene_strip = context.selected_sequences[0]
|
||||
# scene_strip.use_sequence = True
|
||||
# scene_strip.use_sequence = True
|
||||
return {"FINISHED"}
|
||||
|
|
|
@ -73,9 +73,7 @@ class POWER_SEQUENCER_OT_merge_from_scene_strip(bpy.types.Operator):
|
|||
context.window.scene = strip_scene
|
||||
bpy.ops.scene.delete()
|
||||
context.window.scene = start_scene
|
||||
self.report(
|
||||
type={"WARNING"}, message="Merged scenes lose all their animation data."
|
||||
)
|
||||
self.report(type={"WARNING"}, message="Merged scenes lose all their animation data.")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
|
|
@ -62,4 +62,4 @@ class POWER_SEQUENCER_OT_select_all_left_or_right(bpy.types.Operator):
|
|||
return context.sequences
|
||||
|
||||
def execute(self, context):
|
||||
return bpy.ops.sequencer.select_side_of_frame("INVOKE_DEFAULT", side=self.side)
|
||||
return bpy.ops.sequencer.select("INVOKE_DEFAULT", left_right=self.side)
|
||||
|
|
|
@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_snap(bpy.types.Operator):
|
|||
"demo": "",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
(
|
||||
{"type": "S", "value": "PRESS", "shift": True},
|
||||
{},
|
||||
"Snap sequences to cursor",
|
||||
)
|
||||
({"type": "S", "value": "PRESS", "shift": True}, {}, "Snap sequences to cursor",)
|
||||
],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
from .utils.functions import get_sequences_under_cursor, apply_time_offset
|
||||
from .utils.functions import get_sequences_under_cursor, move_selection
|
||||
from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
|
||||
|
||||
|
||||
|
@ -32,11 +32,7 @@ class POWER_SEQUENCER_OT_snap_selection(bpy.types.Operator):
|
|||
"demo": "",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
(
|
||||
{"type": "S", "value": "PRESS", "alt": True},
|
||||
{},
|
||||
"Snap selection to cursor",
|
||||
)
|
||||
({"type": "S", "value": "PRESS", "alt": True}, {}, "Snap selection to cursor",)
|
||||
],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
|
@ -55,9 +51,7 @@ class POWER_SEQUENCER_OT_snap_selection(bpy.types.Operator):
|
|||
if context.selected_sequences
|
||||
else get_sequences_under_cursor(context)
|
||||
)
|
||||
frame_first = min(
|
||||
sequences, key=lambda s: s.frame_final_start
|
||||
).frame_final_start
|
||||
frame_first = min(sequences, key=lambda s: s.frame_final_start).frame_final_start
|
||||
time_offset = context.scene.frame_current - frame_first
|
||||
apply_time_offset(context, sequences, time_offset)
|
||||
move_selection(context, sequences, time_offset)
|
||||
return {"FINISHED"}
|
||||
|
|
|
@ -78,16 +78,9 @@ class POWER_SEQUENCER_OT_trim_left_or_right_handles(bpy.types.Operator):
|
|||
frame_current = context.scene.frame_current
|
||||
|
||||
# Only select sequences under the time cursor
|
||||
sequences = (
|
||||
context.selected_sequences
|
||||
if context.selected_sequences
|
||||
else context.sequences
|
||||
)
|
||||
sequences = context.selected_sequences if context.selected_sequences else context.sequences
|
||||
for s in sequences:
|
||||
s.select = (
|
||||
s.frame_final_start <= frame_current
|
||||
and s.frame_final_end >= frame_current
|
||||
)
|
||||
s.select = s.frame_final_start <= frame_current and s.frame_final_end >= frame_current
|
||||
sequences = [s for s in sequences if s.select]
|
||||
if not sequences:
|
||||
return {"FINISHED"}
|
||||
|
|
|
@ -21,6 +21,9 @@ import bpy
|
|||
|
||||
from .global_settings import SequenceTypes
|
||||
|
||||
max_channel = 32
|
||||
min_channel = 1
|
||||
|
||||
|
||||
def calculate_distance(x1, y1, x2, y2):
|
||||
return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
|
@ -34,10 +37,8 @@ def find_linked(context, sequences, selected_sequences):
|
|||
"""
|
||||
Takes a list of sequences and returns a list of all the sequences
|
||||
and effects that are linked in time
|
||||
|
||||
Args:
|
||||
- sequences: a list of sequences
|
||||
|
||||
- sequences: a list of sequences
|
||||
Returns a list of all the linked sequences, but not the sequences passed to the function
|
||||
"""
|
||||
start, end = get_frame_range(sequences, selected_sequences)
|
||||
|
@ -99,7 +100,7 @@ def find_sequences_after(context, sequence):
|
|||
"""
|
||||
Finds the strips following the sequences passed to the function
|
||||
Args:
|
||||
- Sequences, the sequences to check
|
||||
- Sequences, the sequences to check
|
||||
Returns all the strips after the sequence in the current context
|
||||
"""
|
||||
return [s for s in context.sequences if s.frame_final_start > sequence.frame_final_start]
|
||||
|
@ -133,12 +134,10 @@ def find_snap_candidate(context, frame=0):
|
|||
def find_strips_mouse(context, frame, channel, select_linked=False):
|
||||
"""
|
||||
Finds a list of sequences to select based on the frame and channel the mouse cursor is at
|
||||
|
||||
Args:
|
||||
- frame: the frame the mouse or cursor is on
|
||||
- channel: the channel the mouse is hovering
|
||||
- select_linked: find and append the sequences linked in time if True
|
||||
|
||||
- frame: the frame the mouse or cursor is on
|
||||
- channel: the channel the mouse is hovering
|
||||
- select_linked: find and append the sequences linked in time if True
|
||||
Returns the sequence(s) under the mouse cursor as a list
|
||||
Returns an empty list if nothing found
|
||||
"""
|
||||
|
@ -201,10 +200,9 @@ def get_mouse_frame_and_channel(context, event):
|
|||
def is_in_range(context, sequence, start, end):
|
||||
"""
|
||||
Checks if a single sequence's start or end is in the range
|
||||
|
||||
Args:
|
||||
- sequence: the sequence to check for
|
||||
- start, end: the start and end frames
|
||||
- sequence: the sequence to check for
|
||||
- start, end: the start and end frames
|
||||
Returns True if the sequence is within the range, False otherwise
|
||||
"""
|
||||
s_start = sequence.frame_final_start
|
||||
|
@ -225,47 +223,32 @@ def set_preview_range(context, start, end):
|
|||
scene.frame_preview_end = end
|
||||
|
||||
|
||||
def slice_selection(context, sequences):
|
||||
def slice_selection(context, sequences, range_block=0):
|
||||
"""
|
||||
Takes a list of sequences and breaks it down
|
||||
into multiple lists of connected sequences
|
||||
|
||||
Returns a list of lists of sequences,
|
||||
each list corresponding to a block of sequences
|
||||
that are connected in time and sorted by frame_final_start
|
||||
"""
|
||||
# Find when 2 sequences are not connected in time
|
||||
if not sequences:
|
||||
return []
|
||||
|
||||
break_ids = [0]
|
||||
sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start"))
|
||||
last_sequence = sorted_sequences[0]
|
||||
last_biggest_frame_end = last_sequence.frame_final_end
|
||||
index = 0
|
||||
for s in sorted_sequences:
|
||||
if s.frame_final_start > last_biggest_frame_end + 1:
|
||||
break_ids.append(index)
|
||||
last_biggest_frame_end = max(last_biggest_frame_end, s.frame_final_end)
|
||||
last_sequence = s
|
||||
index += 1
|
||||
|
||||
# Create lists
|
||||
break_ids.append(len(sorted_sequences))
|
||||
cuts_count = len(break_ids) - 1
|
||||
# Indicates the index number of the lists from the "broken_selection" list
|
||||
index = -1
|
||||
block_end = 0
|
||||
broken_selection = []
|
||||
index = 0
|
||||
while index < cuts_count:
|
||||
temp_list = []
|
||||
index_range = range(break_ids[index], break_ids[index + 1] - 1)
|
||||
if len(index_range) == 0:
|
||||
temp_list.append(sorted_sequences[break_ids[index]])
|
||||
else:
|
||||
for counter in range(break_ids[index], break_ids[index + 1]):
|
||||
temp_list.append(sorted_sequences[counter])
|
||||
if temp_list:
|
||||
broken_selection.append(temp_list)
|
||||
index += 1
|
||||
sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start"))
|
||||
|
||||
for s in sorted_sequences:
|
||||
if not broken_selection or (block_end + 1 + range_block < s.frame_final_start):
|
||||
broken_selection.append([s])
|
||||
block_end = s.frame_final_end
|
||||
index += 1
|
||||
continue
|
||||
block_end = max(block_end, s.frame_final_end)
|
||||
broken_selection[index].append(s)
|
||||
|
||||
return broken_selection
|
||||
|
||||
|
||||
|
@ -278,8 +261,10 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]):
|
|||
trim_end = max(frame_start, frame_end)
|
||||
|
||||
to_trim = [s for s in to_trim if s.type in SequenceTypes.CUTABLE]
|
||||
initial_selection = context.selected_sequences
|
||||
|
||||
for s in to_trim:
|
||||
strips_in_target_channel = []
|
||||
# Cut strip longer than the trim range in three
|
||||
is_strip_longer_than_trim_range = (
|
||||
s.frame_final_start < trim_start and s.frame_final_end > trim_end
|
||||
|
@ -290,6 +275,13 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]):
|
|||
bpy.ops.sequencer.split(frame=trim_start, type="SOFT", side="RIGHT")
|
||||
bpy.ops.sequencer.split(frame=trim_end, type="SOFT", side="LEFT")
|
||||
to_delete.append(context.selected_sequences[0])
|
||||
|
||||
for c in context.sequences:
|
||||
if c.channel == s.channel:
|
||||
strips_in_target_channel.append(c)
|
||||
|
||||
if s in initial_selection:
|
||||
initial_selection.append(strips_in_target_channel[0])
|
||||
continue
|
||||
|
||||
# Resize strips that overlap the trim range
|
||||
|
@ -298,20 +290,12 @@ def trim_strips(context, frame_start, frame_end, to_trim, to_delete=[]):
|
|||
elif s.frame_final_end > trim_start and s.frame_final_start < trim_start:
|
||||
s.frame_final_end = trim_start
|
||||
|
||||
delete_strips(to_delete)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def delete_strips(to_delete):
|
||||
"""
|
||||
Deletes the list of sequences `to_delete`
|
||||
"""
|
||||
if not to_delete:
|
||||
return
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in to_delete:
|
||||
bpy.context.sequences.remove(s)
|
||||
|
||||
for s in initial_selection:
|
||||
s.select = True
|
||||
bpy.ops.sequencer.delete()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def find_closest_surrounding_cuts(context, frame):
|
||||
|
@ -378,30 +362,7 @@ def ripple_move(context, sequences, duration_frames, delete=False):
|
|||
else:
|
||||
to_ripple = set(to_ripple + sequences)
|
||||
|
||||
# Use the built-in seq_slide operator to move strips, for best performances
|
||||
initial_selection = context.selected_sequences
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in to_ripple:
|
||||
s.select = True
|
||||
bpy.ops.transform.seq_slide(value=(duration_frames, 0))
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in initial_selection:
|
||||
s.select = True
|
||||
|
||||
|
||||
def apply_time_offset(context, sequences=[], offset=0):
|
||||
"""Offsets a list of sequences in time using bpy.ops.transform.seq_slide. Mutates and restores the
|
||||
user's selection. Use this function to ensure maximum performances and avoid having to figure
|
||||
out the logic to move strips in the right order.
|
||||
"""
|
||||
selection = context.selected_sequences
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in sequences:
|
||||
s.select = True
|
||||
bpy.ops.transform.seq_slide(value=(offset, 0))
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in selection:
|
||||
s.select = True
|
||||
move_selection(context, to_ripple, duration_frames, 0)
|
||||
|
||||
|
||||
def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=True):
|
||||
|
@ -409,18 +370,18 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru
|
|||
Returns a tuple of two lists: (strips_inside_range, strips_overlapping_range)
|
||||
strips_inside_range are strips entirely contained in the frame range.
|
||||
strips_overlapping_range are strips that only overlap the frame range.
|
||||
|
||||
Args:
|
||||
- frame_start, the start of the frame range
|
||||
- frame_end, the end of the frame range
|
||||
- sequences (optional): only work with these sequences.
|
||||
If it doesn't receive any, the function works with all the sequences in the current context
|
||||
- find_overlapping (optional): find and return a list of strips that overlap the
|
||||
- frame_start, the start of the frame range
|
||||
- frame_end, the end of the frame range
|
||||
- sequences (optional): only work with these sequences.
|
||||
If it doesn't receive any, the function works with all the sequences in the current context
|
||||
- find_overlapping (optional): find and return a list of strips that overlap the
|
||||
frame range
|
||||
|
||||
"""
|
||||
strips_inside_range = []
|
||||
strips_overlapping_range = []
|
||||
if not sequences:
|
||||
sequences = bpy.context.sequences
|
||||
for s in sequences:
|
||||
if (
|
||||
frame_start <= s.frame_final_start <= frame_end
|
||||
|
@ -429,8 +390,8 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru
|
|||
strips_inside_range.append(s)
|
||||
elif find_overlapping:
|
||||
if (
|
||||
frame_start <= s.frame_final_end <= frame_end
|
||||
or frame_start <= s.frame_final_start <= frame_end
|
||||
frame_start < s.frame_final_end <= frame_end
|
||||
or frame_start <= s.frame_final_start < frame_end
|
||||
):
|
||||
strips_overlapping_range.append(s)
|
||||
|
||||
|
@ -438,3 +399,19 @@ def find_strips_in_range(frame_start, frame_end, sequences, find_overlapping=Tru
|
|||
if s.frame_final_start < frame_start and s.frame_final_end > frame_end:
|
||||
strips_overlapping_range.append(s)
|
||||
return strips_inside_range, strips_overlapping_range
|
||||
|
||||
|
||||
def move_selection(context, sequences, frame_offset, channel_offset=0):
|
||||
"""Offsets the selected `sequences` horizontally and vertically and preserves
|
||||
the current selected sequences.
|
||||
"""
|
||||
if not sequences:
|
||||
return
|
||||
initial_selection = context.selected_sequences
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in sequences:
|
||||
s.select = True
|
||||
bpy.ops.transform.seq_slide(value=(frame_offset, channel_offset))
|
||||
bpy.ops.sequencer.select_all(action="DESELECT")
|
||||
for s in initial_selection:
|
||||
s.select = True
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
#
|
||||
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
|
||||
from .utils.doc import doc_brief, doc_description, doc_idname, doc_name
|
||||
from .utils.functions import convert_duration_to_frames
|
||||
|
||||
|
||||
class POWER_SEQUENCER_OT_value_offset(bpy.types.Operator):
|
||||
"""Instantly offset selected strips, either using frames or seconds. Allows to
|
||||
nudge the selection quickly, using keyboard shortcuts.
|
||||
"""
|
||||
|
||||
doc = {
|
||||
"name": doc_name(__qualname__),
|
||||
"demo": "",
|
||||
"description": doc_description(__doc__),
|
||||
"shortcuts": [
|
||||
(
|
||||
{"type": "LEFT_ARROW", "value": "PRESS", "shift": True, "alt": True},
|
||||
{"direction": "left"},
|
||||
"Offset the selection to the left.",
|
||||
),
|
||||
(
|
||||
{"type": "RIGHT_ARROW", "value": "PRESS", "shift": True, "alt": True},
|
||||
{"direction": "right"},
|
||||
"Offset the selection to the right.",
|
||||
),
|
||||
],
|
||||
"keymap": "Sequencer",
|
||||
}
|
||||
bl_idname = doc_idname(__qualname__)
|
||||
bl_label = doc["name"]
|
||||
bl_description = doc_brief(doc["description"])
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
direction: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("left", "left", "Move the selection to the left"),
|
||||
("right", "right", "Move the selection to the right"),
|
||||
],
|
||||
name="Direction",
|
||||
description="Move the selection given frames or seconds",
|
||||
default="right",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
value_type: bpy.props.EnumProperty(
|
||||
items=[
|
||||
("seconds", "Seconds", "Move with the value as seconds"),
|
||||
("frames", "Frames", "Move with the value as frames"),
|
||||
],
|
||||
name="Value Type",
|
||||
description="Toggle between offset in frames or seconds",
|
||||
default="seconds",
|
||||
)
|
||||
offset: bpy.props.FloatProperty(
|
||||
name="Offset",
|
||||
description="Offset amount to apply",
|
||||
default=1.0,
|
||||
step=5,
|
||||
precision=3,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.selected_sequences
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.offset = abs(self.offset)
|
||||
if self.direction == "left":
|
||||
self.offset *= -1.0
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
offset_frames = (
|
||||
convert_duration_to_frames(context, self.offset)
|
||||
if self.value_type == "seconds"
|
||||
else self.offset
|
||||
)
|
||||
return bpy.ops.transform.seq_slide(value=(offset_frames, 0))
|
|
@ -1,16 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
|
@ -1,171 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Tool to render video proxies using FFMPEG
|
||||
Offers mp4 and webm options
|
||||
"""
|
||||
import argparse as ap
|
||||
import glob as g
|
||||
import logging as lg
|
||||
import os.path as osp
|
||||
import sys
|
||||
from itertools import compress, starmap, tee
|
||||
|
||||
from .call import call, call_makedirs
|
||||
from .commands import get_commands, get_commands_vi
|
||||
from .config import CONFIG as C
|
||||
from .config import LOGGER, LOGLEV
|
||||
from .utils import checktools, printw, printd, prints, ToolError
|
||||
|
||||
|
||||
def find_files(
|
||||
directory=".", ignored_directory=C["proxy_directory"], extensions=C["extensions"]["all"]
|
||||
):
|
||||
"""
|
||||
Find files to process.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
directory: str
|
||||
Working directory.
|
||||
ignored_directory: str
|
||||
Don't check for files in this directory. By default `BL_proxy`.
|
||||
extensions: set(str)
|
||||
Set of file extensions for filtering the directory tree.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: list(str)
|
||||
List of file paths to be processed.
|
||||
"""
|
||||
if not osp.isdir(directory):
|
||||
raise ValueError(("The given path '{}' is not a valid directory.".format(directory)))
|
||||
xs = g.iglob("{}/**".format(osp.abspath(directory)), recursive=True)
|
||||
xs = filter(lambda x: osp.isfile(x), xs)
|
||||
xs = filter(lambda x: ignored_directory not in osp.dirname(x), xs)
|
||||
xs = [x for x in xs if osp.splitext(x)[1].lower() in extensions]
|
||||
return xs
|
||||
|
||||
|
||||
def parse_arguments(cfg):
|
||||
"""
|
||||
Uses `argparse` to parse the command line arguments.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: Namespace
|
||||
Command line arguments.
|
||||
"""
|
||||
p = ap.ArgumentParser(description="Create proxies for Blender VSE using FFMPEG.")
|
||||
p.add_argument(
|
||||
"working_directory",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="The directory containing media to create proxies for",
|
||||
)
|
||||
p.add_argument(
|
||||
"-p",
|
||||
"--preset",
|
||||
default="mp4",
|
||||
choices=cfg["presets"],
|
||||
help="a preset name for proxy encoding",
|
||||
)
|
||||
p.add_argument(
|
||||
"-s",
|
||||
"--sizes",
|
||||
nargs="+",
|
||||
type=int,
|
||||
default=[25],
|
||||
choices=cfg["proxy_sizes"],
|
||||
help="A list of sizes of the proxies to render, either 25, 50, or 100",
|
||||
)
|
||||
p.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)."
|
||||
)
|
||||
p.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Run the script without actual rendering or creating files and"
|
||||
" folders. For DEBUGGING purposes"
|
||||
),
|
||||
)
|
||||
|
||||
clargs = p.parse_args()
|
||||
# normalize directory
|
||||
clargs.working_directory = osp.abspath(clargs.working_directory)
|
||||
# --dry-run implies maximum verbosity level
|
||||
clargs.verbose = 99999 if clargs.dry_run else clargs.verbose
|
||||
return clargs
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Script entry point.
|
||||
"""
|
||||
tools = ["ffmpeg", "ffprobe"]
|
||||
try:
|
||||
# get command line arguments and set log level
|
||||
clargs = parse_arguments(C)
|
||||
lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)])
|
||||
|
||||
# log basic command line arguments
|
||||
clargs.dry_run and LOGGER.info("DRY-RUN")
|
||||
LOGGER.info("WORKING-DIRECTORY :: {}".format(clargs.working_directory))
|
||||
LOGGER.info("PRESET :: {}".format(clargs.preset))
|
||||
LOGGER.info("SIZES :: {}".format(clargs.sizes))
|
||||
|
||||
# check for external dependencies
|
||||
checktools(tools)
|
||||
|
||||
# find files to process
|
||||
path_i = find_files(clargs.working_directory)
|
||||
kwargs = {"path_i": path_i}
|
||||
|
||||
printw(C, "Creating directories if necessary")
|
||||
call_makedirs(C, clargs, **kwargs)
|
||||
|
||||
printw(C, "Checking for existing proxies")
|
||||
cmds = tee(get_commands(C, clargs, what="check", **kwargs))
|
||||
stdouts = call(C, clargs, cmds=cmds[0], check=False, shell=True, **kwargs)
|
||||
checks = map(lambda s: s.strip().split(), stdouts)
|
||||
checks = starmap(lambda fst, *tail: not all(fst == t for t in tail), checks)
|
||||
kwargs["path_i"] = list(compress(kwargs["path_i"], checks))
|
||||
|
||||
if len(kwargs["path_i"]) != 0:
|
||||
printw(C, "Processing", s="\n")
|
||||
cmds = get_commands_vi(C, clargs, **kwargs)
|
||||
call(C, clargs, cmds=cmds, **kwargs)
|
||||
else:
|
||||
printd(C, "All proxies exist or no files found, nothing to process", s="\n")
|
||||
printd(C, "Done")
|
||||
except (ToolError, ValueError) as e:
|
||||
LOGGER.error(e)
|
||||
prints(C, "Exiting")
|
||||
except KeyboardInterrupt:
|
||||
prints(C, "DirtyInterrupt. Exiting", s="\n\n")
|
||||
sys.exit()
|
||||
|
||||
|
||||
# this is so it can be ran as a module: `python3 -m bpsrender` (for testing)
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,95 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# import multiprocessing as mp
|
||||
import os
|
||||
import subprocess as sp
|
||||
import sys
|
||||
|
||||
from functools import partial
|
||||
from itertools import chain, tee
|
||||
from tqdm import tqdm
|
||||
from .config import LOGGER
|
||||
from .utils import get_dir, kickstart
|
||||
|
||||
WINDOWS = ("win32", "cygwin")
|
||||
|
||||
|
||||
def call_makedirs(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Make BL_proxy directories if necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
kwargs: dict
|
||||
MANDATORY: path_i
|
||||
Dictionary with additional information from previous step.
|
||||
"""
|
||||
path_i = kwargs["path_i"]
|
||||
path_d = map(partial(get_dir, cfg, clargs, **kwargs), path_i)
|
||||
path_d = tee(chain(*path_d))
|
||||
kickstart(map(lambda p: LOGGER.info("Directory @ {}".format(p)), path_d[0]))
|
||||
if clargs.dry_run:
|
||||
return
|
||||
path_d = (os.makedirs(p, exist_ok=True) for p in path_d[1])
|
||||
kickstart(path_d)
|
||||
|
||||
|
||||
def call(cfg, clargs, *, cmds, **kwargs):
|
||||
"""
|
||||
Generic subprocess calls.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
kwargs: dict
|
||||
MANDATORY: path_i
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: str
|
||||
Stdout & Stderr gathered from subprocess call.
|
||||
"""
|
||||
kwargs_s = {
|
||||
"stdout": sp.PIPE,
|
||||
"stderr": sp.STDOUT,
|
||||
"universal_newlines": True,
|
||||
"check": kwargs.get("check", True),
|
||||
"shell": kwargs.get("shell", False),
|
||||
"creationflags": sp.CREATE_NEW_PROCESS_GROUP if sys.platform in WINDOWS else 0,
|
||||
}
|
||||
if kwargs_s["shell"]:
|
||||
cmds = map(lambda cmd: (cmd[0], " ".join(cmd[1])), cmds)
|
||||
cmds = tee(cmds)
|
||||
kickstart(map(lambda cmd: LOGGER.debug("CALL :: {}".format(cmd[1])), cmds[0]))
|
||||
if clargs.dry_run:
|
||||
return []
|
||||
n = len(kwargs["path_i"])
|
||||
ps = tqdm(
|
||||
map(lambda cmd: sp.run(cmd[1], **kwargs_s), cmds[1]),
|
||||
total=n,
|
||||
unit="file" if n == 1 else "files",
|
||||
)
|
||||
return [p.stdout for p in ps]
|
|
@ -1,190 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os.path as osp
|
||||
import shlex as sl
|
||||
from itertools import chain
|
||||
from .utils import get_path
|
||||
|
||||
|
||||
def get_commands_check(cfg, clargs, **kwargs):
|
||||
"""
|
||||
ffprobe subprocess command generation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
kwargs: dict
|
||||
MANDATORY: path_i_1, path_o_1
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple(str))
|
||||
Iterator containing commands.
|
||||
"""
|
||||
cmd = (
|
||||
"ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of"
|
||||
" default=noprint_wrappers=1:nokey=1 '{file}'"
|
||||
)
|
||||
out = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
|
||||
out = map(lambda f: cmd.format(file=f), out)
|
||||
out = sl.split(cmd.format(file=kwargs["path_i_1"]) + " && " + " && ".join(out))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_image_1(cfg, clargs, **kwargs):
|
||||
"""
|
||||
ffmpeg subprocess command generation for processing an image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
kwargs: dict
|
||||
MANDATORY: path_i_1, path_o_1
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple(str))
|
||||
Iterator containing commands.
|
||||
"""
|
||||
cmd = "ffmpeg -hwaccel auto -y -v quiet -stats -i '{path_i_1}' {common_all}"
|
||||
common = "-f apng -filter:v scale=iw*{size}:ih*{size} '{path_o_1}'"
|
||||
common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
|
||||
common_all = map(
|
||||
lambda s: common.format(size=s[0] / 100.0, path_o_1=s[1]), zip(clargs.sizes, common_all)
|
||||
)
|
||||
common_all = " ".join(common_all)
|
||||
out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_video_1(cfg, clargs, **kwargs):
|
||||
"""
|
||||
ffmpeg subprocess command generation for processing a video.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
kwargs: dict
|
||||
MANDATORY: path_i_1, path_o_1
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple(str))
|
||||
Iterator containing commands.
|
||||
"""
|
||||
cmd = "ffmpeg -hwaccel auto -y -v quiet -stats -noautorotate -i '{path_i_1}' {common_all}"
|
||||
common = (
|
||||
"-pix_fmt yuv420p"
|
||||
" -g 1"
|
||||
" -sn -an"
|
||||
" -vf colormatrix=bt601:bt709"
|
||||
" -vf scale=ceil(iw*{size}/2)*2:ceil(ih*{size}/2)*2"
|
||||
" {preset}"
|
||||
" '{path_o_1}'"
|
||||
)
|
||||
common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
|
||||
common_all = map(
|
||||
lambda s: common.format(
|
||||
preset=cfg["presets"][clargs.preset], size=s[0] / 100.0, path_o_1=s[1]
|
||||
),
|
||||
zip(clargs.sizes, common_all),
|
||||
)
|
||||
common_all = " ".join(common_all)
|
||||
out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands(cfg, clargs, *, what, **kwargs):
|
||||
"""
|
||||
Delegates the creation of commands lists to appropriate functions based on `what` parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
what: str
|
||||
Determines the returned value (see: Returns[out]).
|
||||
kwargs: dict
|
||||
MANDATORY: path_i
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple(str, tuple(str)))
|
||||
An iterator with the 1st element as a tag (the `what` parameter) and the 2nd
|
||||
element as the iterator of the actual commands.
|
||||
"""
|
||||
get_commands_f = {
|
||||
"video": get_commands_video_1,
|
||||
"image": get_commands_image_1,
|
||||
"check": get_commands_check,
|
||||
}
|
||||
ps = (
|
||||
kwargs["path_i"]
|
||||
if what not in cfg["extensions"]
|
||||
else filter(
|
||||
lambda p: osp.splitext(p)[1].lower() in cfg["extensions"][what], kwargs["path_i"]
|
||||
)
|
||||
)
|
||||
ps = map(lambda p: (p, get_path(cfg, clargs, p, **kwargs)), ps)
|
||||
out = chain.from_iterable(
|
||||
map(lambda p: get_commands_f[what](cfg, clargs, path_i_1=p[0], path_o_1=p[1], **kwargs), ps)
|
||||
)
|
||||
return map(lambda c: (what, c), out)
|
||||
|
||||
|
||||
def get_commands_vi(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Delegates the creation of commands lists to appropriate functions for video/image processing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments.
|
||||
cmds: iter(tuple(str))
|
||||
kwargs: dict
|
||||
MANDATORY: path_i_1, path_o_1
|
||||
Dictionary with additional information from previous step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple(str, tuple(str)))
|
||||
An iterator with the 1st element as a tag (the `what` parameter) and the 2nd
|
||||
element as the iterator of the actual commands.
|
||||
"""
|
||||
ws = filter(lambda x: x is not "all", cfg["extensions"])
|
||||
return chain.from_iterable(map(lambda w: get_commands(cfg, clargs, what=w, **kwargs), ws))
|
|
@ -1,41 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import multiprocessing as mp
|
||||
from itertools import chain
|
||||
import logging as lg
|
||||
|
||||
|
||||
CONFIG = {
|
||||
"logger": "BPS",
|
||||
"proxy_directory": "BL_proxy",
|
||||
"proxy_sizes": (25, 50, 100),
|
||||
"extensions": {
|
||||
"video": {".mp4", ".mkv", ".mov", ".flv", ".mts"},
|
||||
"image": {".png", ".jpg", ".jpeg"},
|
||||
},
|
||||
"presets": {
|
||||
"webm": "-c:v libvpx -crf 25 -speed 16 -threads {}".format(str(mp.cpu_count())),
|
||||
"mp4": "-c:v libx264 -crf 25 -preset faster -tune fastdecode",
|
||||
"nvenc": "-c:v h264_nvenc -qp 25 -preset fast",
|
||||
},
|
||||
"pre": {"work": "»", "done": "•", "skip": "~"},
|
||||
}
|
||||
CONFIG["extensions"]["all"] = set(chain(*CONFIG["extensions"].values()))
|
||||
|
||||
LOGGER = lg.getLogger(CONFIG["logger"])
|
||||
LOGLEV = [lg.INFO, lg.DEBUG]
|
||||
LOGLEV = [None] + sorted(LOGLEV, reverse=True)
|
|
@ -1,109 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Collection of utility functions, class-independent
|
||||
"""
|
||||
import os.path as osp
|
||||
from collections import deque
|
||||
from shutil import which
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Raised if external dependencies aren't found on system.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def checktools(tools):
|
||||
tools = [(t, which(t) or "") for t in tools]
|
||||
check = {"tools": tools, "test": all(map(lambda x: x[1], tools))}
|
||||
if not check["test"]:
|
||||
msg = ["BPSProxy couldn't find external dependencies:"]
|
||||
msg += [
|
||||
"[{check}] {tool}: {path}".format(
|
||||
check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND"
|
||||
)
|
||||
for tool, path in check["tools"]
|
||||
]
|
||||
msg += [
|
||||
(
|
||||
"Check if you have them properly installed and available in the PATH"
|
||||
" environemnt variable."
|
||||
)
|
||||
]
|
||||
raise ToolError("\n".join(msg))
|
||||
|
||||
|
||||
def get_path_video(cfg, clargs, path, **kwargs):
|
||||
return osp.join(
|
||||
osp.dirname(path), cfg["proxy_directory"], osp.basename(path), "proxy_{size}.avi"
|
||||
)
|
||||
|
||||
|
||||
def get_path_image(cfg, clargs, path, **kwargs):
|
||||
return osp.join(
|
||||
osp.dirname(path),
|
||||
cfg["proxy_directory"],
|
||||
"images",
|
||||
"{size}",
|
||||
"{file}_proxy.jpg".format(file=osp.basename(path)),
|
||||
)
|
||||
|
||||
|
||||
def get_path(cfg, clargs, path, **kwargs):
|
||||
get_path_f = {"video": get_path_video, "image": get_path_image}
|
||||
what = what_vi(cfg, clargs, path, **kwargs)
|
||||
return get_path_f[what](cfg, clargs, path, **kwargs)
|
||||
|
||||
|
||||
def get_dir_video(cfg, clargs, path, **kwargs):
|
||||
return iter((osp.join(osp.dirname(path), cfg["proxy_directory"], osp.basename(path)),))
|
||||
|
||||
|
||||
def get_dir_image(cfg, clargs, path, **kwargs):
|
||||
ps = osp.join(osp.dirname(path), cfg["proxy_directory"], "images", "{size}")
|
||||
return map(lambda s: ps.format(size=s), clargs.sizes)
|
||||
|
||||
|
||||
def get_dir(cfg, clargs, path, **kwargs):
|
||||
get_dir_f = {"video": get_dir_video, "image": get_dir_image}
|
||||
what = what_vi(cfg, clargs, path, **kwargs)
|
||||
return get_dir_f[what](cfg, clargs, path, **kwargs)
|
||||
|
||||
|
||||
def what_vi(cfg, clargs, p, **kwargs):
|
||||
return "video" if osp.splitext(p)[1].lower() in cfg["extensions"]["video"] else "image"
|
||||
|
||||
|
||||
def kickstart(it):
|
||||
deque(it, maxlen=0)
|
||||
|
||||
|
||||
def printw(cfg, text, s="\n", e="...", p="", **kwargs):
|
||||
p = p or cfg["pre"]["work"]
|
||||
print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs)
|
||||
|
||||
|
||||
def printd(cfg, text, s="", e=".", p="", **kwargs):
|
||||
p = p or cfg["pre"]["done"]
|
||||
printw(cfg, text, s=s, e=e, p=p, **kwargs)
|
||||
|
||||
|
||||
def prints(cfg, text, s="", e=".", p="", **kwargs):
|
||||
p = p or cfg["pre"]["skip"]
|
||||
printw(cfg, text, s=s, e=e, p=p, **kwargs)
|
|
@ -1,57 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
def readme():
|
||||
with open("README.md") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="bpsproxy",
|
||||
version="0.2.1",
|
||||
description="Blender Power Sequencer proxy generator tool",
|
||||
long_description=readme(),
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Utilities",
|
||||
],
|
||||
url="https://github.com/GDquest/BPSProxy",
|
||||
keywords="blender proxy vse sequence editor productivity",
|
||||
author="Răzvan C. Rădulescu",
|
||||
author_email="razcore.art@gmail.com",
|
||||
license="GPLv3",
|
||||
packages=["bpsproxy"],
|
||||
install_requires=["tqdm"],
|
||||
zip_safe=False,
|
||||
entry_points={"console_scripts": ["bpsproxy=bpsproxy.__main__:main"]},
|
||||
include_package_data=True,
|
||||
)
|
|
@ -1,16 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
|
@ -1,147 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Renders videos edited in Blender 3D's Video Sequence Editor using multiple CPU
|
||||
cores. Original script by Justin Warren:
|
||||
https://github.com/sciactive/pulverize/blob/master/pulverize.py
|
||||
Modified by sudopluto (Pranav Sharma), gdquest (Nathan Lovato) and
|
||||
razcore (Razvan Radulescu)
|
||||
|
||||
Under GPLv3 license
|
||||
"""
|
||||
import argparse as ap
|
||||
import os.path as osp
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
from .calls import call
|
||||
from .config import CONFIG as C
|
||||
from .config import LOGGER
|
||||
from .helpers import BSError, ToolError, checktools, kickstart, prints
|
||||
from .setup import setup
|
||||
|
||||
# https://github.com/mikeycal/the-video-editors-render-script-for-blender#configuring-the-script
|
||||
# there seems no easy way to grab the ram usage in a mulitplatform way
|
||||
# without writing platform dependent code, or by using a python module
|
||||
|
||||
# Most popluar config is 4 cores, 8 GB ram, this is the default for the script
|
||||
# https://store.steampowered.com/hwsurvey/
|
||||
|
||||
|
||||
def parse_arguments(cfg):
|
||||
"""
|
||||
Uses `argparse` to parse the command line arguments.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: Namespace
|
||||
Command line arguments (normalized).
|
||||
"""
|
||||
p = ap.ArgumentParser(
|
||||
description="Multi-process Blender VSE rendering - will attempt to"
|
||||
" create a folder called `render` inside of the folder"
|
||||
" containing `blendfile`. Insider `render` another folder called"
|
||||
" `parts` will be created for storing temporary files. These files"
|
||||
" will be joined together as the last step to produce the final"
|
||||
" render which will be stored inside `render` and it will have the"
|
||||
" same name as `blendfile`"
|
||||
)
|
||||
p.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
default=".",
|
||||
help="Output folder (will contain a `bpsrender` temp folder for" "rendering parts).",
|
||||
)
|
||||
p.add_argument(
|
||||
"-w",
|
||||
"--workers",
|
||||
type=int,
|
||||
default=cfg["cpu_count"],
|
||||
help="Number of workers in the pool (for video rendering).",
|
||||
)
|
||||
p.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)."
|
||||
)
|
||||
p.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Run the script without actual rendering or creating files and"
|
||||
" folders. For DEBUGGING purposes"
|
||||
),
|
||||
)
|
||||
p.add_argument("-s", "--start", type=int, default=None, help="Start frame")
|
||||
p.add_argument("-e", "--end", type=int, default=None, help="End frame")
|
||||
p.add_argument(
|
||||
"-m", "--mixdown-only", action="store_true", help="ONLY render the audio MIXDOWN"
|
||||
)
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--concatenate-only",
|
||||
action="store_true",
|
||||
help="ONLY CONCATENATE the (already) available video chunks",
|
||||
)
|
||||
p.add_argument(
|
||||
"-d",
|
||||
"--video-only",
|
||||
action="store_true",
|
||||
help="ONLY render the VIDEO (implies --concatenate-only).",
|
||||
)
|
||||
p.add_argument(
|
||||
"-j",
|
||||
"--join-only",
|
||||
action="store_true",
|
||||
help="ONLY JOIN the mixdown with the video. This will produce the" " final render",
|
||||
)
|
||||
p.add_argument("blendfile", help="Blender project file to render.")
|
||||
|
||||
clargs = p.parse_args()
|
||||
clargs.blendfile = osp.abspath(clargs.blendfile)
|
||||
clargs.output = osp.abspath(clargs.output)
|
||||
# --video-only implies --concatenate-only
|
||||
clargs.concatenate_only = clargs.concatenate_only or clargs.video_only
|
||||
# --dry-run implies maximum verbosity level
|
||||
clargs.verbose = 99999 if clargs.dry_run else clargs.verbose
|
||||
return clargs
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Script entry point.
|
||||
"""
|
||||
tools = ["blender", "ffmpeg"]
|
||||
try:
|
||||
clargs = parse_arguments(C)
|
||||
checktools(tools)
|
||||
cmds, kwargs = setup(C, clargs)
|
||||
kickstart(map(partial(call, C, clargs, **kwargs), cmds))
|
||||
except (BSError, ToolError) as e:
|
||||
LOGGER.error(e)
|
||||
except KeyboardInterrupt:
|
||||
# TODO: add actual clean up code
|
||||
prints(C, "DirtyInterrupt. Exiting", s="\n\n", e="...")
|
||||
sys.exit()
|
||||
|
||||
|
||||
# this is so it can be ran as a module: `python3 -m bpsrender` (for testing)
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,30 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
import os.path as osp
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
for strip in bpy.context.scene.sequence_editor.sequences_all:
|
||||
if strip.type == "META":
|
||||
continue
|
||||
if strip.type != "SOUND":
|
||||
strip.mute = True
|
||||
|
||||
path = sys.argv[-1]
|
||||
ext = osp.splitext(path)[1][1:].upper()
|
||||
bpy.ops.sound.mixdown(filepath=path, check_existing=False, container=ext, codec=ext)
|
|
@ -1,30 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
|
||||
EXT = {
|
||||
"AVI_JPEG": ".avi",
|
||||
"AVI_RAW": ".avi",
|
||||
"FFMPEG": {"MKV": ".mkv", "OGG": ".ogv", "QUICKTIME": ".mov", "AVI": ".avi", "MPEG4": ".mp4"},
|
||||
}
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
ext = EXT.get(scene.render.image_settings.file_format, "UNDEFINED")
|
||||
if scene.render.image_settings.file_format == "FFMPEG":
|
||||
ext = ext[scene.render.ffmpeg.format]
|
||||
print("\nBPS:{} {} {}\n".format(scene.frame_start, scene.frame_end, ext))
|
|
@ -1,19 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import bpy
|
||||
|
||||
bpy.context.scene.render.ffmpeg.audio_codec = "NONE"
|
|
@ -1,410 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# IMPURE
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import signal as sig
|
||||
import subprocess as sp
|
||||
from functools import partial, reduce
|
||||
from itertools import chain, islice, starmap, tee
|
||||
from multiprocessing import Queue
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from .config import LOGGER
|
||||
from .helpers import BSError, checkblender, kickstart, printd, prints, printw
|
||||
|
||||
|
||||
def chunk_frames(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
Recover the chunk start/end frames from the constructed commands for the
|
||||
video step. This is necessary to preserve purity until later steps.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple)
|
||||
Start/end pairs of frames corresponding to the chunk commands created at
|
||||
the video step.
|
||||
"""
|
||||
out = map(lambda x: (x, islice(x, 1, None)), cmds)
|
||||
out = map(lambda x: zip(*x), out)
|
||||
out = map(lambda x: filter(lambda y: y[0] in ("-s", "-e"), x), out)
|
||||
out = map(lambda x: map(lambda y: int(y[1]), x), out)
|
||||
out = map(lambda x: reduce(lambda acc, y: acc + (y,), x, ()), out)
|
||||
return out
|
||||
|
||||
|
||||
def append_chunks_file(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Helper function for creating the chunks file that will be used by `ffmpeg`
|
||||
to concatenate the chunks into one video file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
MANDATORY w_frame_start, w_frame_end, ext
|
||||
Dictionary with additional information from the setup step.
|
||||
"""
|
||||
with open(kwargs["chunks_file_path"], "a") as f:
|
||||
for fs, fe in chunk_frames(cfg, clargs, cmds, **kwargs):
|
||||
f.write(
|
||||
"file '{rcp}{fs}-{fe}{ext}'\n".format(
|
||||
rcp=kwargs["render_chunk_path"].rstrip("#"),
|
||||
fs="{fs:0{frame_pad}d}".format(fs=fs, **cfg),
|
||||
fe="{fe:0{frame_pad}d}".format(fe=fe, **cfg),
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def call_probe(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Probe `clargs.blendfile` for frame start, frame end and extension (for
|
||||
video only).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: dict
|
||||
Dictionary with info extracted from `clargs.blendfile`, namely: start
|
||||
frame, end frame and extension (only useful for video step).
|
||||
"""
|
||||
kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
|
||||
|
||||
printw(cfg, "Probing")
|
||||
printw(cfg, "Input(blend) @ {}".format(clargs.blendfile), s="")
|
||||
frame_start, frame_end, ext = (0, 0, "")
|
||||
if not clargs.dry_run:
|
||||
with sp.Popen(next(cmds), **kwargs_p) as cp:
|
||||
try:
|
||||
tmp = map(partial(checkblender, "PROBE", [cfg["probe_py"]], cp), cp.stdout)
|
||||
tmp = filter(lambda x: x.startswith("BPS"), tmp)
|
||||
tmp = map(lambda x: x[4:].strip().split(), tmp)
|
||||
frame_start, frame_end, ext = chain(*tmp)
|
||||
except BSError as e:
|
||||
LOGGER.error(e)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
finally:
|
||||
cp.terminate()
|
||||
returncode = cp.poll()
|
||||
if returncode != 0:
|
||||
raise sp.CalledProcessError(returncode, cp.args)
|
||||
frame_start = frame_start if clargs.start is None else clargs.start
|
||||
frame_end = frame_end if clargs.end is None else clargs.end
|
||||
out = {
|
||||
"frame_start": int(frame_start),
|
||||
"frame_end": int(frame_end),
|
||||
"frames_total": int(frame_end) - int(frame_start) + 1,
|
||||
"ext": ext,
|
||||
}
|
||||
if out["ext"] == "UNDEFINED":
|
||||
raise BSError("Video extension is {ext}. Stopping!".format(ext=ext))
|
||||
printd(cfg, "Probing done")
|
||||
return out
|
||||
|
||||
|
||||
def call_mixdown(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Calls blender to render the audio mixdown.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
MANDATORY render_mixdown_path
|
||||
Dictionary with additional information from the setup step.
|
||||
"""
|
||||
kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
|
||||
|
||||
printw(cfg, "Rendering mixdown")
|
||||
printw(cfg, "Output @ {}".format(kwargs["render_mixdown_path"]), s="")
|
||||
if not clargs.dry_run:
|
||||
with sp.Popen(next(cmds), **kwargs_p) as cp:
|
||||
try:
|
||||
tmp = map(partial(checkblender, "MIXDOWN", [cfg["mixdown_py"]], cp), cp.stdout)
|
||||
tmp = filter(lambda x: x.startswith("BPS"), tmp)
|
||||
tmp = map(lambda x: x[4:].strip().split(), tmp)
|
||||
kickstart(tmp)
|
||||
except BSError as e:
|
||||
LOGGER.error(e)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
finally:
|
||||
cp.terminate()
|
||||
returncode = cp.poll()
|
||||
if returncode != 0:
|
||||
raise sp.CalledProcessError(returncode, cp.args)
|
||||
printd(cfg, "Mixdown done")
|
||||
|
||||
|
||||
def call_chunk(cfg, clargs, queue, cmd, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Calls blender to render one chunk (which part is determined by `cmd`).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmd: tuple
|
||||
Tuple to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the setup step.
|
||||
"""
|
||||
sig.signal(sig.SIGINT, sig.SIG_IGN)
|
||||
kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
|
||||
|
||||
if not clargs.dry_run:
|
||||
# can't use nice functional syntax if we want to simplify with `with`
|
||||
with sp.Popen(cmd, **kwargs_p) as cp:
|
||||
try:
|
||||
tmp = map(
|
||||
partial(
|
||||
checkblender,
|
||||
"VIDEO",
|
||||
[cfg["video_py"], "The encoder timebase is not set"],
|
||||
cp,
|
||||
),
|
||||
cp.stdout,
|
||||
)
|
||||
tmp = filter(lambda x: x.startswith("Append frame"), tmp)
|
||||
tmp = map(lambda x: x.split()[-1], tmp)
|
||||
tmp = map(int, tmp)
|
||||
tmp = map(lambda x: True, tmp)
|
||||
kickstart(map(queue.put, tmp))
|
||||
queue.put(False)
|
||||
except BSError as e:
|
||||
LOGGER.error(e)
|
||||
|
||||
|
||||
def call_video(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Multi-process call to blender for rendering the (video) chunks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the setup step.
|
||||
"""
|
||||
printw(cfg, "Rendering video (w/o audio)")
|
||||
printw(cfg, "Output @ {}".format(kwargs["render_chunk_path"]), s="")
|
||||
try:
|
||||
not clargs.dry_run and os.remove(kwargs["chunks_file_path"])
|
||||
LOGGER.info("CALL-VIDEO: generating {}".format(kwargs["chunks_file_path"]))
|
||||
except OSError as e:
|
||||
LOGGER.info("CALL-VIDEO: skipping {}: {}".format(e.filename, e.strerror))
|
||||
|
||||
cmds, cmds_cf = tee(cmds)
|
||||
(not clargs.dry_run and append_chunks_file(cfg, clargs, cmds_cf, **kwargs))
|
||||
# prepare queue/worker
|
||||
queues = queues_close = (Queue(),) * clargs.workers
|
||||
# prpare processes
|
||||
proc = starmap(
|
||||
lambda q, cmd: mp.Process(target=partial(call_chunk, cfg, clargs, **kwargs), args=(q, cmd)),
|
||||
zip(queues, cmds),
|
||||
)
|
||||
# split iterator in 2 for later joining the processes and sum
|
||||
# one of them
|
||||
proc, proc_close = tee(proc)
|
||||
proc = map(lambda p: p.start(), proc)
|
||||
try:
|
||||
not clargs.dry_run and kickstart(proc)
|
||||
|
||||
# communicate with processes through the queues and use tqdm to show a
|
||||
# simple terminal progress bar baesd on video total frames
|
||||
queues = map(lambda q: iter(q.get, False), queues)
|
||||
queues = chain(*queues)
|
||||
queues = tqdm(queues, total=kwargs["frame_end"] - kwargs["frame_start"] + 1, unit="frames")
|
||||
not clargs.dry_run and kickstart(queues)
|
||||
except KeyboardInterrupt:
|
||||
proc_close = map(lambda x: x.terminate(), proc_close)
|
||||
not clargs.dry_run and kickstart(proc_close)
|
||||
raise
|
||||
finally:
|
||||
# close and join processes and queues
|
||||
proc_close = map(lambda x: x.join(), proc_close)
|
||||
not clargs.dry_run and kickstart(proc_close)
|
||||
|
||||
queues_close = map(lambda q: (q, q.close()), queues_close)
|
||||
queues_close = starmap(lambda q, _: q.join_thread(), queues_close)
|
||||
not clargs.dry_run and kickstart(queues_close)
|
||||
printd(cfg, "Video chunks rendering done")
|
||||
|
||||
|
||||
def call_concatenate(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Calls ffmpeg in order to concatenate the video chunks together.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
MANDATORY: render_video_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Note
|
||||
----
|
||||
It expects the video chunk files to already be available.
|
||||
"""
|
||||
kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
|
||||
printw(cfg, "Concatenating (video) chunks")
|
||||
printw(cfg, "Output @ {}".format(kwargs["render_video_path"]), s="")
|
||||
if not clargs.dry_run:
|
||||
with sp.Popen(next(cmds), **kwargs_p) as cp:
|
||||
try:
|
||||
returncode = cp.wait()
|
||||
if returncode != 0:
|
||||
raise sp.CalledProcessError(returncode, cp.args)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
finally:
|
||||
cp.terminate()
|
||||
printd(cfg, "Concatenating done")
|
||||
|
||||
|
||||
def call_join(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Calls ffmpeg for joining the audio mixdown and the video.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`.
|
||||
kwargs: dict
|
||||
MANDATORY: render_audiovideo_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Note
|
||||
----
|
||||
It expects the audio mixdown and video files to already be available.
|
||||
"""
|
||||
kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
|
||||
printw(cfg, "Joining audio/video")
|
||||
printw(cfg, "Output @ {}".format(kwargs["render_audiovideo_path"]), s="")
|
||||
if not clargs.dry_run:
|
||||
with sp.Popen(next(cmds), **kwargs_p) as cp:
|
||||
try:
|
||||
returncode = cp.wait()
|
||||
if returncode != 0:
|
||||
raise sp.CalledProcessError(returncode, cp.args)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
finally:
|
||||
cp.terminate()
|
||||
printd(cfg, "Joining done")
|
||||
|
||||
|
||||
def call(cfg, clargs, cmds, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Delegates work to appropriate `call_*` functions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
cmds: iter(tuple)
|
||||
Iterator of commands to be passed to `subprocess`
|
||||
kwargs: dict
|
||||
MANDATORY: render_audiovideo_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: dict or None
|
||||
It passes on the output from the `call_*` functions. See `call_*` for
|
||||
specific details.
|
||||
|
||||
Note
|
||||
----
|
||||
It tries to be smart and skip steps if child subprocesses give errors.
|
||||
Example if `--join-only` is passed, but the audio mixdown or video file
|
||||
aren't available on hard drive.
|
||||
"""
|
||||
calls = {
|
||||
"probe": call_probe,
|
||||
"mixdown": call_mixdown,
|
||||
"video": call_video,
|
||||
"concatenate": call_concatenate,
|
||||
"join": call_join,
|
||||
}
|
||||
try:
|
||||
out = calls[cmds[0]](cfg, clargs, cmds[1], **kwargs)
|
||||
return out
|
||||
except sp.CalledProcessError:
|
||||
prints(
|
||||
cfg,
|
||||
("WARNING:{}: Something went wrong when calling" " command - SKIPPING").format(cmds[0]),
|
||||
)
|
|
@ -1,341 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import math as m
|
||||
from collections import OrderedDict
|
||||
from itertools import chain, islice
|
||||
|
||||
from .config import LOGGER
|
||||
|
||||
|
||||
def get_commands_probe(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the command for probing the `clargs.blendfile`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(list)
|
||||
An iterator for which each element is a list to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
out = (
|
||||
"blender",
|
||||
"--background",
|
||||
clargs.blendfile,
|
||||
"--python",
|
||||
kwargs["probe_py_normalized"],
|
||||
"--disable-autoexec",
|
||||
)
|
||||
LOGGER.debug("CMD-PROBE: {cmd}".format(cmd=" ".join(out)))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_chunk(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the command for rendering a (video) chunk from `clargs.blendfile`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY render_chunk_path, w_frame_start, w_frame_end
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(list)
|
||||
An iterator for which each element is a list to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
out = (
|
||||
"blender",
|
||||
"--background",
|
||||
clargs.blendfile,
|
||||
"--python",
|
||||
kwargs["video_py_normalized"],
|
||||
"--disable-autoexec",
|
||||
"--render-output",
|
||||
kwargs["render_chunk_path"],
|
||||
"-s",
|
||||
str(kwargs["w_frame_start"]),
|
||||
"-e",
|
||||
str(kwargs["w_frame_end"]),
|
||||
"--render-anim",
|
||||
)
|
||||
LOGGER.debug(
|
||||
"CMD-CHUNK({w_frame_start}-{w_frame_end}): {cmd}".format(cmd=" ".join(out), **kwargs)
|
||||
)
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_video(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the list of commands (one command per chunk) for rendering a video
|
||||
from `clargs.blendfile`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY chunk_file_path, frame_start, frame_end, frames_total
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple)
|
||||
An iterator for which each element is a tuple to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
LOGGER.debug("CMD-VIDEO:")
|
||||
chunk_length = int(m.floor(kwargs["frames_total"] / clargs.workers))
|
||||
out = map(lambda w: (w, kwargs["frame_start"] + w * chunk_length), range(clargs.workers))
|
||||
out = map(
|
||||
lambda x: (
|
||||
x[1],
|
||||
x[1] + chunk_length - 1 if x[0] != clargs.workers - 1 else kwargs["frame_end"],
|
||||
),
|
||||
out,
|
||||
)
|
||||
out = map(
|
||||
lambda x: get_commands(
|
||||
cfg, clargs, "chunk", w_frame_start=x[0], w_frame_end=x[1], **kwargs
|
||||
),
|
||||
out,
|
||||
)
|
||||
out = map(lambda x: x[1], out)
|
||||
out = chain(*out)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def get_commands_mixdown(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the command to render the mixdown from `clargs.blendfile`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY render_mixdown_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple)
|
||||
An iterator for which each element is a tuple to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
out = (
|
||||
"blender --background {blendfile} --python {mixdown_py_normalized}"
|
||||
" --disable-autoexec -- {render_mixdown_path}".format(**cfg, **vars(clargs), **kwargs)
|
||||
)
|
||||
out = (
|
||||
"blender",
|
||||
"--background",
|
||||
clargs.blendfile,
|
||||
"--python",
|
||||
kwargs["mixdown_py_normalized"],
|
||||
"--disable-autoexec",
|
||||
"--",
|
||||
kwargs["render_mixdown_path"],
|
||||
)
|
||||
LOGGER.debug("CMD-MIXDOWN: {cmd}".format(cmd=" ".join(out)))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_concatenate(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the command to concatenate the available video chunks generated
|
||||
beforehand.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY chunks_file_path, render_video_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple)
|
||||
An iterator for which each element is a tuple to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
out = (
|
||||
"ffmpeg",
|
||||
"-stats",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"-0",
|
||||
"-i",
|
||||
kwargs["chunks_file_path"],
|
||||
"-c",
|
||||
"copy",
|
||||
"-y",
|
||||
kwargs["render_video_path"],
|
||||
)
|
||||
LOGGER.debug("CMD-CONCATENATE: {cmd}".format(cmd=" ".join(out)))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands_join(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Create the command to join the available audio mixdown and video generated
|
||||
beforehand.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY chunks_file_path, render_video_path
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter(tuple)
|
||||
An iterator for which each element is a tuple to be sent to functions like
|
||||
`subprocess.run`.
|
||||
"""
|
||||
out = (
|
||||
"ffmpeg",
|
||||
"-stats",
|
||||
"-i",
|
||||
kwargs["render_video_path"],
|
||||
"-i",
|
||||
kwargs["render_mixdown_path"],
|
||||
"-map",
|
||||
"0:v:0",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-map",
|
||||
"1:a:0",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"320k",
|
||||
"-y",
|
||||
kwargs["render_audiovideo_path"],
|
||||
)
|
||||
LOGGER.debug("CMD-JOIN: {cmd}".format(cmd=" ".join(out)))
|
||||
return iter((out,))
|
||||
|
||||
|
||||
def get_commands(cfg, clargs, what="", **kwargs):
|
||||
"""
|
||||
Delegates the creation of commands lists to appropriate functions based on
|
||||
`what` parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
what: str (default = '')
|
||||
Determines the returned value (see: Returns[out]).
|
||||
kwargs: dict
|
||||
MANDATORY -- see individual functions for the list of mandatory keys
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter or (str, iter)
|
||||
|- what == '' is True
|
||||
An iterator with elements of the type (str) for determining the order in
|
||||
which to call the functions in the setup step.
|
||||
NOTE: it skipps the "internal use only" functions.
|
||||
|- else
|
||||
A tuple with the 1st element as a tag (the `what` parameter) and the 2nd
|
||||
element as the iterator of the actual commands.
|
||||
"""
|
||||
get_commands_f = OrderedDict(
|
||||
(
|
||||
# internal use only
|
||||
("probe", get_commands_probe),
|
||||
("chunk", get_commands_chunk),
|
||||
# direct connection to command line arguments - in order of execution
|
||||
("mixdown", get_commands_mixdown),
|
||||
("video", get_commands_video),
|
||||
("concatenate", get_commands_concatenate),
|
||||
("join", get_commands_join),
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
islice(get_commands_f, 2, None)
|
||||
if what == ""
|
||||
else (what, get_commands_f[what](cfg, clargs, **kwargs))
|
||||
)
|
||||
|
||||
|
||||
def get_commands_all(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Prepare the list of commands to be executed depending on the command line
|
||||
arguments.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY -- see individual functions for the list of mandatory keys
|
||||
Dictionary with additional information from the setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: iter((str, tuple))
|
||||
An iterator for which each element is a (str, iter(tuple)). The string
|
||||
value is for tagging the iterator command list (2nd element) for filtering
|
||||
later based on the given command line arguments.
|
||||
"""
|
||||
end = "_only"
|
||||
out = filter(lambda x: x[0].endswith(end), vars(clargs).items())
|
||||
out = map(lambda x: (x[0][: -len(end)], x[1]), out)
|
||||
order = list(get_commands(cfg, clargs))
|
||||
out = sorted(out, key=lambda x: order.index(x[0]))
|
||||
out = (
|
||||
map(lambda k: k[0], out)
|
||||
if all(map(lambda k: not k[1], out))
|
||||
else map(lambda k: k[0], filter(lambda k: k[1], out))
|
||||
)
|
||||
out = map(lambda k: get_commands(cfg, clargs, k, **kwargs), out)
|
||||
return out
|
|
@ -1,37 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging as lg
|
||||
import multiprocessing as mp
|
||||
import os.path as osp
|
||||
|
||||
CONFIG = {
|
||||
"logger": "BPS",
|
||||
"cpu_count": min(int(mp.cpu_count() / 2), 6),
|
||||
"bs_path": osp.join(osp.dirname(osp.abspath(__file__)), "bscripts"),
|
||||
"frame_pad": 7,
|
||||
"parts_folder": "bpsrender",
|
||||
"chunks_file": "chunks.txt",
|
||||
"video_file": "video{}",
|
||||
"pre": {"work": "»", "done": "•", "skip": "~"},
|
||||
"probe_py": "probe.py",
|
||||
"mixdown_py": "mixdown.py",
|
||||
"video_py": "video.py",
|
||||
}
|
||||
|
||||
LOGGER = lg.getLogger(CONFIG["logger"])
|
||||
LOGLEV = [lg.INFO, lg.DEBUG]
|
||||
LOGLEV = [None] + sorted(LOGLEV, reverse=True)
|
|
@ -1,110 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections import deque
|
||||
from shutil import which
|
||||
|
||||
|
||||
class BSError(Exception):
|
||||
"""
|
||||
Custom Exception raised if Blender is called with a python script argument
|
||||
and gives error while trying to execute the script.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Raised if external dependencies aren't found on system.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def checktools(tools):
|
||||
tools = [(t, which(t) or "") for t in tools]
|
||||
check = {"tools": tools, "test": all(map(lambda x: x[1], tools))}
|
||||
if not check["test"]:
|
||||
msg = ["BPSRender couldn't find external dependencies:"]
|
||||
msg += [
|
||||
"[{check}] {tool}: {path}".format(
|
||||
check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND"
|
||||
)
|
||||
for tool, path in check["tools"]
|
||||
]
|
||||
msg += [
|
||||
(
|
||||
"Check if you have them properly installed and available in the PATH"
|
||||
" environemnt variable."
|
||||
),
|
||||
"Exiting...",
|
||||
]
|
||||
raise ToolError("\n".join(msg))
|
||||
|
||||
|
||||
def checkblender(what, search, cp, s):
|
||||
"""
|
||||
IMPURE
|
||||
Check Blender output for python script execution error.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
what: str
|
||||
A tag used in the exception message.
|
||||
search: iter(str)
|
||||
One or more string(s) to search for in Blender's output.
|
||||
cp: Popen
|
||||
Blender subprocess.
|
||||
s: PIPE
|
||||
Blender's output.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: PIPE
|
||||
The same pipe `s` is returned so that it can be iterated over on later
|
||||
steps.
|
||||
"""
|
||||
if not isinstance(search, list):
|
||||
search = [search]
|
||||
for search_item in search:
|
||||
if search_item in s:
|
||||
message = (
|
||||
"Script {what} was not properly executed in" " Blender".format(what=what),
|
||||
"CMD: {cmd}".format(what=what, cmd=" ".join(cp.args)),
|
||||
"DUMP:".format(what=what),
|
||||
s,
|
||||
)
|
||||
raise BSError("\n".join(message))
|
||||
return s
|
||||
|
||||
|
||||
def printw(cfg, text, s="\n", e="...", p="", **kwargs):
|
||||
p = p or cfg["pre"]["work"]
|
||||
print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs)
|
||||
|
||||
|
||||
def printd(cfg, text, s="", e=".", p="", **kwargs):
|
||||
p = p or cfg["pre"]["done"]
|
||||
printw(cfg, text, s=s, e=e, p=p, **kwargs)
|
||||
|
||||
|
||||
def prints(cfg, text, s="", e=".", p="", **kwargs):
|
||||
p = p or cfg["pre"]["skip"]
|
||||
printw(cfg, text, s=s, e=e, p=p, **kwargs)
|
||||
|
||||
|
||||
def kickstart(it):
|
||||
deque(it, maxlen=0)
|
|
@ -1,182 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
# IMPURE
|
||||
import logging as lg
|
||||
import os
|
||||
import os.path as osp
|
||||
from functools import reduce
|
||||
from itertools import starmap
|
||||
|
||||
from .calls import call
|
||||
from .commands import get_commands, get_commands_all
|
||||
from .config import LOGGER, LOGLEV
|
||||
from .helpers import kickstart
|
||||
|
||||
|
||||
def setup_bspy(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Normalize the names of the script to be ran in Blender for certain steps.
|
||||
Eg. the probe step depends on the script located in
|
||||
`bpsrender/cfg['probe_py']`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: dict
|
||||
Dictoinary to be used in call steps.
|
||||
"""
|
||||
out = filter(lambda x: x[0].endswith("_py"), cfg.items())
|
||||
out = starmap(lambda k, v: ("{}_normalized".format(k), osp.join(cfg["bs_path"], v)), out)
|
||||
return dict(out)
|
||||
|
||||
|
||||
def setup_probe(cfg, clargs, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Call Blender and extract information that will be necessary for later
|
||||
steps.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY -- see individual functions for the list of mandatory keys
|
||||
Dictionary with additional information from the previous setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: dict
|
||||
Dictoinary to be used in call steps.
|
||||
"""
|
||||
return call(cfg, clargs, get_commands(cfg, clargs, "probe", **kwargs), **kwargs)
|
||||
|
||||
|
||||
def setup_paths(cfg, clargs, **kwargs):
|
||||
"""
|
||||
Figure out appropriate path locations to store output for parts and final
|
||||
render.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
MANDATORY -- see individual functions for the list of mandatory keys
|
||||
Dictionary with additional information from the previous setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: dict
|
||||
Dictionary storing all relevant information pertaining to folder and file
|
||||
paths.
|
||||
|
||||
Note
|
||||
----
|
||||
It also creates the folder structure 'render/parts' where
|
||||
`clargs.blendfile` is stored on disk.
|
||||
"""
|
||||
render_parts_path = osp.join(clargs.output, cfg["parts_folder"])
|
||||
name = osp.splitext(osp.basename(clargs.blendfile))[0]
|
||||
render_mixdown_path = osp.join(render_parts_path, "{}_m.flac".format(name))
|
||||
render_chunk_path = osp.join(render_parts_path, "{}_c_{}".format(name, "#" * cfg["frame_pad"]))
|
||||
render_video_path = osp.join(render_parts_path, "{}_v{}".format(name, kwargs["ext"]))
|
||||
render_audiovideo_path = osp.join(clargs.output, "{}{}".format(name, kwargs["ext"]))
|
||||
chunks_file_path = osp.join(render_parts_path, cfg["chunks_file"])
|
||||
|
||||
out = {
|
||||
"render_path": clargs.output,
|
||||
"render_parts_path": render_parts_path,
|
||||
"chunks_file_path": chunks_file_path,
|
||||
"render_chunk_path": render_chunk_path,
|
||||
"render_video_path": render_video_path,
|
||||
"render_mixdown_path": render_mixdown_path,
|
||||
"render_audiovideo_path": render_audiovideo_path,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def setup_folders_hdd(cfg, clargs, **kwargs):
|
||||
"""
|
||||
IMPURE
|
||||
Prepares the folder structure `cfg['render']/cfg['parts']'`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the previous setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: (iter((str, iter(tuple))), dict)
|
||||
1st element: see commands.py:get_commands_all
|
||||
2nd elment: the keyword arguments used by calls.py:call
|
||||
"""
|
||||
# create folder structure if it doesn't exist already only if
|
||||
# appropriate command line arguments are given
|
||||
do_it = filter(lambda x: x[0].endswith("_only"), vars(clargs).items())
|
||||
do_it = all(map(lambda x: not x[1], do_it))
|
||||
do_it = not clargs.dry_run and clargs.video_only or clargs.mixdown_only or do_it
|
||||
do_it and os.makedirs(kwargs["render_parts_path"], exist_ok=True)
|
||||
return {}
|
||||
|
||||
|
||||
def setup(cfg, clargs):
|
||||
"""
|
||||
IMPURE -- setup_paths
|
||||
Prepares the folder structure 'render/parts', the appropriate command lists
|
||||
to be called and the keyword arguments to be passed to call functions
|
||||
(calls.py).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg: dict
|
||||
Configuration dictionary.
|
||||
clargs: Namespace
|
||||
Command line arguments (normalized).
|
||||
kwargs: dict
|
||||
Dictionary with additional information from the previous setup step.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: (iter((str, iter(tuple))), dict)
|
||||
1st element: see commands.py:get_commands_all
|
||||
2nd elment: the keyword arguments used by calls.py:call
|
||||
"""
|
||||
setups_f = (setup_bspy, setup_probe, setup_paths, setup_folders_hdd)
|
||||
lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)])
|
||||
|
||||
kwargs = dict(reduce(lambda acc, sf: {**acc, **sf(cfg, clargs, **acc)}, setups_f, {}))
|
||||
|
||||
LOGGER.info("Setup:")
|
||||
kickstart(starmap(lambda k, v: LOGGER.info("{}: {}".format(k, v)), kwargs.items()))
|
||||
return get_commands_all(cfg, clargs, **kwargs), kwargs
|
|
@ -1,56 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
|
||||
#
|
||||
# This file is part of Power Sequencer.
|
||||
#
|
||||
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
|
||||
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
|
||||
# not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
def readme():
|
||||
with open("README.rst") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="bpsrender",
|
||||
version="0.1.40.post1",
|
||||
description="Blender Power Sequencer Renderer",
|
||||
long_description=readme(),
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Topic :: Text Processing :: Linguistic",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Utilities",
|
||||
],
|
||||
url="https://gitlab.com/razcore/BPSRender",
|
||||
keywords="blender render parallel multiprocess speedup utility" " productivty",
|
||||
author="Răzvan C. Rădulescu",
|
||||
author_email="razcore.art@gmail.com",
|
||||
license="GPLv3",
|
||||
packages=["bpsrender"],
|
||||
install_requires=["tqdm"],
|
||||
zip_safe=False,
|
||||
entry_points={"console_scripts": ["bpsrender=bpsrender.__main__:main"]},
|
||||
include_package_data=True,
|
||||
)
|
Loading…
Reference in New Issue