Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 44 additions & 37 deletions op_blender_rhubarb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -20,18 +23,18 @@ 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
wm.progress_update(50)

try:
(stdout, stderr) = self.rhubarb.communicate(timeout=1)

try:
result = json.loads(stderr)
if result['type'] == 'progress':
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -151,14 +159,13 @@ def cancel(self, context):
wm.event_timer_remove(self._timer)



def register():
bpy.utils.register_class(RhubarbLipsyncOperator)


def unregister():
bpy.utils.unregister_class(RhubarbLipsyncOperator)


if __name__ == "__main__":
register()

100 changes: 61 additions & 39 deletions pnl_blender_rhubarb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
Expand All @@ -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():
Expand Down