diff --git a/op_blender_rhubarb.py b/op_blender_rhubarb.py index 224eb0e..9918c1a 100644 --- a/op_blender_rhubarb.py +++ b/op_blender_rhubarb.py @@ -6,10 +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 +from bpy.types import Context, Action +import traceback class RhubarbLipsyncOperator(bpy.types.Operator): """Run Rhubarb lipsync""" @@ -20,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 @@ -31,7 +34,7 @@ def modal(self, context, event): try: (stdout, stderr) = self.rhubarb.communicate(timeout=1) - + try: result = json.loads(stderr) if result['type'] == 'progress': @@ -48,37 +51,35 @@ 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 + lib = context.object last_frame = 0 - prev_pose = 0 + prev_pose = lib.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) - - print("start: {0} frame: {1} value: {2}".format(cue['start'], frame_num , cue['value'])) + self.apply_pose(context, frame_num - self.hold_frame_threshold, bpy.data.actions[prev_pose]) + 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 = 0 - - bpy.ops.poselib.apply_pose(pose_index=pose_index) - self.set_keyframes(context, frame_num) + 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 @@ -95,38 +96,45 @@ 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 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:Context, frame:int, pose:Action): + bpy.context.scene.frame_set(frame) + + + + context.object.pose.apply_pose_from_action(action=pose,evaluation_time=frame) + + for i in pose.fcurves: + i.evaluate(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) - + # 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) @@ -151,7 +159,6 @@ def cancel(self, context): wm.event_timer_remove(self._timer) - def register(): bpy.utils.register_class(RhubarbLipsyncOperator) @@ -159,6 +166,6 @@ def register(): def unregister(): bpy.utils.unregister_class(RhubarbLipsyncOperator) + if __name__ == "__main__": register() - 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():