bl_info = { "name": "Lip Sync", "description": "A tool for generating lip-sync animations using poses from the Asset Browser.", "author": "Blackout Creatively", "version": (1, 0, 3), "blender": (4, 3, 0), "location": "View3D > Tool > My Lipsync", "category": "Animation", } import bpy class LipSyncProperties(bpy.types.PropertyGroup): """Properties for Lip Sync Tool""" prefix: bpy.props.StringProperty(name="Pose Prefix", default="") # Prefix for pose names pose_0: bpy.props.StringProperty(name="Rest", default="Rest") pose_1: bpy.props.StringProperty(name="Aa", default="Aa") pose_2: bpy.props.StringProperty(name="D", default="D") pose_3: bpy.props.StringProperty(name="Ee", default="Ee") pose_4: bpy.props.StringProperty(name="F", default="F") pose_5: bpy.props.StringProperty(name="L", default="L") pose_6: bpy.props.StringProperty(name="M", default="M") pose_7: bpy.props.StringProperty(name="Oh", default="Oh") pose_8: bpy.props.StringProperty(name="R", default="R") pose_9: bpy.props.StringProperty(name="S", default="S") pose_10: bpy.props.StringProperty(name="Uh", default="Uh") pose_11: bpy.props.StringProperty(name="W-Oo", default="W-Oo") holdPoseOffset: bpy.props.IntProperty(name="Hold Pose Frame Offset", default=2) class LipSyncPluginOperator(bpy.types.Operator): """Lip Sync Animation Plugin""" bl_idname = "object.lip_sync_plugin" bl_label = "Lip Sync Animation" filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = context.scene.lip_sync_properties prefix = props.prefix.strip() # Get prefix, if any value_to_string = { 0: f"{prefix}{props.pose_0}", 1: f"{prefix}{props.pose_1}", 2: f"{prefix}{props.pose_2}", 3: f"{prefix}{props.pose_3}", 4: f"{prefix}{props.pose_4}", 5: f"{prefix}{props.pose_5}", 6: f"{prefix}{props.pose_6}", 7: f"{prefix}{props.pose_7}", 8: f"{prefix}{props.pose_8}", 9: f"{prefix}{props.pose_9}", 10: f"{prefix}{props.pose_10}", 11: f"{prefix}{props.pose_11}", } hold_offset = props.holdPoseOffset try: with open(self.filepath, 'r') as file: lines = file.readlines() except Exception as e: self.report({'ERROR'}, f"Failed to read file: {e}") return {'CANCELLED'} action_name = "Lip Sync" action = bpy.data.actions.get(action_name) if not action: action = bpy.data.actions.new(name=action_name) obj = context.object if not obj or obj.type != 'ARMATURE': self.report({'ERROR'}, "No armature selected.") return {'CANCELLED'} obj.animation_data_create() obj.animation_data.action = action bpy.ops.object.mode_set(mode='POSE') keyframe_data = [] for line in lines: try: parts = line.strip().split('\t') if len(parts) != 2: self.report({'WARNING'}, f"Skipping malformed line: '{line.strip()}'") continue time = round(float(parts[0])) int_value = int(parts[1]) pose_name = value_to_string.get(int_value, None) if pose_name is None: self.report({'WARNING'}, f"Integer value {int_value} is not mapped. Skipping.") continue keyframe_data.append((time, pose_name)) except Exception as e: self.report({'WARNING'}, f"Error processing line '{line.strip()}': {e}") continue keyframe_data.sort() selected_bones = [bone.name for bone in obj.pose.bones if bone.bone.select] for i, (time, pose_name) in enumerate(keyframe_data): # Apply the pose context.scene.frame_set(time) pose_asset = next( (action for action in bpy.data.actions if action.asset_data and action.name == pose_name), None ) if not pose_asset: self.report({'WARNING'}, f"Pose '{pose_name}' not found in the asset library. Skipping.") continue for fcurve in pose_asset.fcurves: data_path = fcurve.data_path bone_name = data_path.split('"')[1] if '"' in data_path else None if not bone_name or bone_name not in selected_bones: continue pose_bone = obj.pose.bones.get(bone_name) if not pose_bone: continue for keyframe in fcurve.keyframe_points: frame, value = keyframe.co if "location" in data_path: pose_bone.location[fcurve.array_index] = value elif "rotation_quaternion" in data_path: pose_bone.rotation_quaternion[fcurve.array_index] = value elif "scale" in data_path: pose_bone.scale[fcurve.array_index] = value for bone_name in selected_bones: bone = obj.pose.bones.get(bone_name) if bone: bone.keyframe_insert(data_path="location", frame=time) bone.keyframe_insert(data_path="rotation_quaternion", frame=time) bone.keyframe_insert(data_path="scale", frame=time) # Duplicate the pose if the gap between keyframes is too large and holdPoseOffset > 0 if hold_offset > 0 and i < len(keyframe_data) - 1: next_time, _ = keyframe_data[i + 1] if next_time - time > hold_offset + 1: hold_time = next_time - hold_offset context.scene.frame_set(hold_time) for bone_name in selected_bones: bone = obj.pose.bones.get(bone_name) if bone: bone.keyframe_insert(data_path="location", frame=hold_time) bone.keyframe_insert(data_path="rotation_quaternion", frame=hold_time) bone.keyframe_insert(data_path="scale", frame=hold_time) bpy.ops.object.mode_set(mode='OBJECT') self.report({'INFO'}, "Lip Sync animation created successfully.") return {'FINISHED'} def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} class LipSyncPluginPanel(bpy.types.Panel): """Creates a Panel in the Tool shelf""" bl_label = "Lip Sync Tool" bl_idname = "OBJECT_PT_lip_sync_tool" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Lipsync" def draw(self, context): layout = self.layout props = context.scene.lip_sync_properties layout.label(text="Pose Prefix:") layout.prop(props, "prefix") layout.label(text="Pose Mappings:") for i in range(12): layout.prop(props, f"pose_{i}") layout.prop(props, "holdPoseOffset") layout.operator(LipSyncPluginOperator.bl_idname, text="Run Lip Sync Tool") def register(): bpy.utils.register_class(LipSyncProperties) bpy.types.Scene.lip_sync_properties = bpy.props.PointerProperty(type=LipSyncProperties) bpy.utils.register_class(LipSyncPluginOperator) bpy.utils.register_class(LipSyncPluginPanel) def unregister(): bpy.utils.unregister_class(LipSyncPluginPanel) bpy.utils.unregister_class(LipSyncPluginOperator) del bpy.types.Scene.lip_sync_properties bpy.utils.unregister_class(LipSyncProperties) if __name__ == "__main__": register()