From daa3ee8a400c27319ca85667a10befb51bc7cb8e Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 8 Nov 2022 16:31:19 -0500 Subject: [PATCH 1/3] Fix Addon Using Legacy Pose Library There is also a filter to prevent the addon from choosing poses that have bones that aren't in the current model. Also, I told it to use the pose assets to key frame the model and it stores pose assets in the mouth options. The addon also will only key the bones keyed in the pose assets for the model that were chosen. --- op_blender_rhubarb.py | 37 ++++++++------- pnl_blender_rhubarb.py | 100 +++++++++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/op_blender_rhubarb.py b/op_blender_rhubarb.py index 224eb0e..97f07eb 100644 --- a/op_blender_rhubarb.py +++ b/op_blender_rhubarb.py @@ -10,6 +10,9 @@ from queue import Queue, Empty import json import os +from mathutils import Matrix + + class RhubarbLipsyncOperator(bpy.types.Operator): """Run Rhubarb lipsync""" @@ -58,16 +61,16 @@ def modal(self, context, event): fps = context.scene.render.fps lib = context.object.pose_library last_frame = 0 - prev_pose = 0 + prev_pose = context.object.pose_library.mouth_shapes["mouth_x"] for cue in results['mouthCues']: frame_num = round(cue['start'] * fps) + lib.mouth_shapes.start_frame + # add hold key if time since last key is large if frame_num - last_frame > self.hold_frame_threshold: print("hold frame: {0}".format(frame_num- self.hold_frame_threshold)) - bpy.ops.poselib.apply_pose(pose_index=prev_pose) - self.set_keyframes(context, frame_num - self.hold_frame_threshold) + self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[prev_pose]) print("start: {0} frame: {1} value: {2}".format(cue['start'], frame_num , cue['value'])) @@ -75,10 +78,9 @@ def modal(self, context, event): if mouth_shape in context.object.pose_library.mouth_shapes: pose_index = context.object.pose_library.mouth_shapes[mouth_shape] else: - pose_index = 0 - - bpy.ops.poselib.apply_pose(pose_index=pose_index) - self.set_keyframes(context, frame_num) + pose_index = context.object.pose_library.mouth_shapes["mouth_x"] + + self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[pose_index]) prev_pose = pose_index @@ -99,15 +101,17 @@ def modal(self, context, event): print(template.format(type(ex).__name__, ex.args)) wm.progress_end() return {'CANCELLED'} - - def set_keyframes(self, context, frame): - for bone in context.selected_pose_bones: - bone.keyframe_insert(data_path='location', frame=frame) - if bone.rotation_mode == 'QUATERNION': - bone.keyframe_insert(data_path='rotation_quaternion', frame=frame) - else: - bone.keyframe_insert(data_path='rotation_euler', frame=frame) - bone.keyframe_insert(data_path='scale', frame=frame) + + def apply_pose(self,context, frame, pose): + bpy.context.scene.frame_set(frame) + + print(pose) + + context.object.pose.apply_pose_from_action(action=pose,evaluation_time=frame) + + for i in pose.fcurves: + i.evaluate(frame) + context.object.pose.bones[i.data_path.split("\"")[1]].keyframe_insert(data_path=i.data_path.split("]")[1].replace(".",""), frame=frame) def invoke(self, context, event): preferences = context.preferences @@ -149,6 +153,7 @@ def finished(self, context): def cancel(self, context): wm = context.window_manager wm.event_timer_remove(self._timer) + diff --git a/pnl_blender_rhubarb.py b/pnl_blender_rhubarb.py index a2e66b0..34c75b9 100644 --- a/pnl_blender_rhubarb.py +++ b/pnl_blender_rhubarb.py @@ -18,57 +18,79 @@ def poll(cls, context): def draw(self, context): layout = self.layout - if context.object.pose_library: - prop = context.object.pose_library.mouth_shapes - - col = layout.column() - col.prop(prop, 'mouth_a', text="Mouth A (MBP)") - col.prop(prop, 'mouth_b', text="Mouth B (EE/etc)") - col.prop(prop, 'mouth_c', text="Mouth C (E)") - col.prop(prop, 'mouth_d', text="Mouth D (AI)") - col.prop(prop, 'mouth_e', text="Mouth E (O)") - col.prop(prop, 'mouth_f', text="Mouth F (WQ)") - col.prop(prop, 'mouth_g', text="Mouth G (FV)") - col.prop(prop, 'mouth_h', text="Mouth H (L)") - col.prop(prop, 'mouth_x', text="Mouth X (rest)") - - row = layout.row(align=True) - row.prop(prop, 'sound_file', text='Sound file') - - row = layout.row(align=True) - row.prop(prop, 'dialog_file', text='Dialog file') + prop = context.object.mouth_shapes - row = layout.row() - row.prop(prop, 'start_frame', text='Start frame') + col = layout.column() + col.prop(prop, 'mouth_a', text="Mouth A (MBP)") + col.prop(prop, 'mouth_b', text="Mouth B (EE/etc)") + col.prop(prop, 'mouth_c', text="Mouth C (E)") + col.prop(prop, 'mouth_d', text="Mouth D (AI)") + col.prop(prop, 'mouth_e', text="Mouth E (O)") + col.prop(prop, 'mouth_f', text="Mouth F (WQ)") + col.prop(prop, 'mouth_g', text="Mouth G (FV)") + col.prop(prop, 'mouth_h', text="Mouth H (L)") + col.prop(prop, 'mouth_x', text="Mouth X (rest)") - row = layout.row() + row = layout.row(align=True) + row.prop(prop, 'sound_file', text='Sound file') - if not (context.preferences.addons[__package__].preferences.executable_path): - row.label(text="Please set rhubarb executable location in addon preferences") - row = layout.row() + row = layout.row(align=True) + row.prop(prop, 'dialog_file', text='Dialog file') - row.operator(operator = "object.rhubarb_lipsync") + row = layout.row() + row.prop(prop, 'start_frame', text='Start frame') - else: - row = layout.row(align=True) - row.label(text="Rhubarb Lipsync requires a pose library") + row = layout.row() + if not (context.preferences.addons[__package__].preferences.executable_path): + row.label(text="Please set rhubarb executable location in addon preferences") + row = layout.row() -pose_markers = [] + row.operator(operator = "object.rhubarb_lipsync") -def pose_markers_items(self, context): - """Dynamic list of items for Object.pose_libs_for_char.""" - lib = bpy.context.object.pose_library +#https://blender.stackexchange.com/a/78592 +enum_items_store = [] - if not context or not context.object: - return [] +def enum_items(self, context): - pose_markers = [(marker, marker, 'Poses', '', idx) for idx, marker in enumerate(lib.pose_markers.keys())] - return pose_markers + items = [] + for action in bpy.data.actions: + not_an_action = False + if (not(action.asset_data is None)): + for i in action.fcurves: + if (not (i.data_path.split("\"")[1] in context.object.pose.bones)): + not_an_action = True + print("hello!") + break + + else: + continue + if not_an_action: + continue + # NEW CODE + # Scan the list of IDs to see if we already have one for this mesh + maxid = -1 + id = -1 + found = False + for idrec in enum_items_store: + id = idrec[0] + if id > maxid: + maxid = id + if idrec[1] == action.name: + found = True + break + + if not found: + enum_items_store.append((maxid+1, action.name)) + + # AMENDED CODE - include the ID + items.append( (action.name, action.name, "", id) ) + + return items poses = bpy.props.EnumProperty( - items=pose_markers_items, + items=enum_items, name='Poses', description='Poses', ) @@ -93,7 +115,7 @@ def register(): bpy.utils.register_class(MouthShapesProperty) bpy.utils.register_class(RhubarbLipsyncPanel) - bpy.types.Action.mouth_shapes = bpy.props.PointerProperty(type=MouthShapesProperty) + bpy.types.Object.mouth_shapes = bpy.props.PointerProperty(type=MouthShapesProperty) def unregister(): From 1a4e78912995c13dee3c99051aa5227aa6cac61e Mon Sep 17 00:00:00 2001 From: Premik Date: Wed, 7 Dec 2022 12:13:28 +0100 Subject: [PATCH 2/3] Formated --- op_blender_rhubarb.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/op_blender_rhubarb.py b/op_blender_rhubarb.py index 97f07eb..1cae30f 100644 --- a/op_blender_rhubarb.py +++ b/op_blender_rhubarb.py @@ -6,14 +6,13 @@ import sys import select import subprocess -from threading import Thread +from threading import Thread from queue import Queue, Empty import json import os from mathutils import Matrix - class RhubarbLipsyncOperator(bpy.types.Operator): """Run Rhubarb lipsync""" bl_idname = "object.rhubarb_lipsync" @@ -34,7 +33,7 @@ def modal(self, context, event): try: (stdout, stderr) = self.rhubarb.communicate(timeout=1) - + try: result = json.loads(stderr) if result['type'] == 'progress': @@ -51,12 +50,12 @@ def modal(self, context, event): pass except json.decoder.JSONDecodeError: pass - + self.rhubarb.poll() if self.rhubarb.returncode is not None: wm.event_timer_remove(self._timer) - + results = json.loads(stdout) fps = context.scene.render.fps lib = context.object.pose_library @@ -66,7 +65,7 @@ def modal(self, context, event): for cue in results['mouthCues']: frame_num = round(cue['start'] * fps) + lib.mouth_shapes.start_frame - + # add hold key if time since last key is large if frame_num - last_frame > self.hold_frame_threshold: print("hold frame: {0}".format(frame_num- self.hold_frame_threshold)) @@ -79,9 +78,9 @@ def modal(self, context, event): pose_index = context.object.pose_library.mouth_shapes[mouth_shape] else: pose_index = context.object.pose_library.mouth_shapes["mouth_x"] + self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[pose_index]) - prev_pose = pose_index last_frame = frame_num @@ -101,14 +100,14 @@ def modal(self, context, event): print(template.format(type(ex).__name__, ex.args)) wm.progress_end() return {'CANCELLED'} - - def apply_pose(self,context, frame, pose): + + def apply_pose(self, context, frame, pose): bpy.context.scene.frame_set(frame) - + print(pose) - + context.object.pose.apply_pose_from_action(action=pose,evaluation_time=frame) - + for i in pose.fcurves: i.evaluate(frame) context.object.pose.bones[i.data_path.split("\"")[1]].keyframe_insert(data_path=i.data_path.split("]")[1].replace(".",""), frame=frame) @@ -121,16 +120,16 @@ def invoke(self, context, event): dialogfile = bpy.path.abspath(context.object.pose_library.mouth_shapes.dialog_file) recognizer = bpy.path.abspath(addon_prefs.recognizer) executable = bpy.path.abspath(addon_prefs.executable_path) - + # This is ugly, but Blender unpacks the zip without execute permission os.chmod(executable, 0o744) command = [executable, "-f", "json", "--machineReadable", "--extendedShapes", "GHX", "-r", recognizer, inputfile] - + if dialogfile: command.append("--dialogFile") - command.append(dialogfile ) - + command.append(dialogfile) + self.rhubarb = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True) @@ -153,8 +152,6 @@ def finished(self, context): def cancel(self, context): wm = context.window_manager wm.event_timer_remove(self._timer) - - def register(): @@ -164,6 +161,6 @@ def register(): def unregister(): bpy.utils.unregister_class(RhubarbLipsyncOperator) + if __name__ == "__main__": register() - From eb5360c8c1ebf6fb92b7590743cfd344f9ea50de Mon Sep 17 00:00:00 2001 From: Premik Date: Wed, 7 Dec 2022 11:18:27 +0100 Subject: [PATCH 3/3] Removed pose_library dependencies. Avoid crash on keyed custom properteis --- op_blender_rhubarb.py | 51 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/op_blender_rhubarb.py b/op_blender_rhubarb.py index 1cae30f..9918c1a 100644 --- a/op_blender_rhubarb.py +++ b/op_blender_rhubarb.py @@ -11,7 +11,8 @@ import json import os from mathutils import Matrix - +from bpy.types import Context, Action +import traceback class RhubarbLipsyncOperator(bpy.types.Operator): """Run Rhubarb lipsync""" @@ -22,10 +23,10 @@ class RhubarbLipsyncOperator(bpy.types.Operator): hold_frame_threshold = 4 @classmethod - def poll(cls, context): - return context.preferences.addons[__package__].preferences.executable_path and \ - context.selected_pose_bones and \ - context.object.pose_library.mouth_shapes.sound_file + def poll(cls, context:Context): + return (context.preferences.addons[__package__].preferences.executable_path and + context.selected_pose_bones and + context.object.mouth_shapes.sound_file) def modal(self, context, event): wm = context.window_manager @@ -58,9 +59,9 @@ def modal(self, context, event): results = json.loads(stdout) fps = context.scene.render.fps - lib = context.object.pose_library + lib = context.object last_frame = 0 - prev_pose = context.object.pose_library.mouth_shapes["mouth_x"] + prev_pose = lib.mouth_shapes["mouth_x"] for cue in results['mouthCues']: frame_num = round(cue['start'] * fps) + lib.mouth_shapes.start_frame @@ -71,16 +72,15 @@ def modal(self, context, event): print("hold frame: {0}".format(frame_num- self.hold_frame_threshold)) self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[prev_pose]) - print("start: {0} frame: {1} value: {2}".format(cue['start'], frame_num , cue['value'])) - + mouth_shape = 'mouth_' + cue['value'].lower() - if mouth_shape in context.object.pose_library.mouth_shapes: - pose_index = context.object.pose_library.mouth_shapes[mouth_shape] + if mouth_shape in lib.mouth_shapes: + pose_index = lib.mouth_shapes[mouth_shape] else: - pose_index = context.object.pose_library.mouth_shapes["mouth_x"] - - + pose_index = lib.mouth_shapes["mouth_x"] + print(f"start:{cue['start']} frame:{frame_num} value:{cue['value']} shape:{mouth_shape} poseIndex:{pose_index}") self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[pose_index]) + prev_pose = pose_index last_frame = frame_num @@ -96,28 +96,33 @@ def modal(self, context, event): wm.progress_end() return {'CANCELLED'} except Exception as ex: + traceback.print_exc() template = "An exception of type {0} occurred. Arguments:\n{1!r}" print(template.format(type(ex).__name__, ex.args)) wm.progress_end() return {'CANCELLED'} - - def apply_pose(self, context, frame, pose): + + def apply_pose(self,context:Context, frame:int, pose:Action): bpy.context.scene.frame_set(frame) - - print(pose) - + + + context.object.pose.apply_pose_from_action(action=pose,evaluation_time=frame) - + for i in pose.fcurves: i.evaluate(frame) - context.object.pose.bones[i.data_path.split("\"")[1]].keyframe_insert(data_path=i.data_path.split("]")[1].replace(".",""), frame=frame) + bone_name=i.data_path.split('"')[1] + bone=context.object.pose.bones[bone_name] + prop_name=i.data_path.partition('"]')[2].replace(".", "") + #print(f"{i.data_path}: bone:{bone_name} prom:{prop_name} {pose}") + bone.keyframe_insert(data_path=prop_name, frame=frame) def invoke(self, context, event): preferences = context.preferences addon_prefs = preferences.addons[__package__].preferences - inputfile = bpy.path.abspath(context.object.pose_library.mouth_shapes.sound_file) - dialogfile = bpy.path.abspath(context.object.pose_library.mouth_shapes.dialog_file) + inputfile = bpy.path.abspath(context.object.mouth_shapes.sound_file) + dialogfile = bpy.path.abspath(context.object.mouth_shapes.dialog_file) recognizer = bpy.path.abspath(addon_prefs.recognizer) executable = bpy.path.abspath(addon_prefs.executable_path)