2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,104 @@
from typing import cast
import bpy
from ..LIPSYNC2D_Utils import get_package_name
from ..lipsync_types import BpyContext, BpyObject, BpyPropertyGroup, BpyUILayout
class AnimatorPanelMixin:
def __init__(self, obj: BpyObject):
self.package_name = get_package_name()
self.prefs: bpy.types.AddonPreferences | None = None
self.obj: BpyObject = obj
lipsync_props = self.obj.lipsync2d_props # type: ignore
self.props: bpy.types.PropertyGroup = cast(BpyPropertyGroup, lipsync_props)
if (
self.package_name
and bpy.context.preferences is not None
and self.package_name in bpy.context.preferences.addons
):
self.prefs = bpy.context.preferences.addons[self.package_name].preferences
self.current_lang = self.prefs.current_lang if self.prefs is not None else None # type: ignore
self.is_model_installed = (
True if self.current_lang not in ["", "none"] else False
)
def draw_animation_section(self, context: BpyContext, layout: BpyUILayout):
raise NotImplementedError()
def draw_visemes_section(self, context: BpyContext, layout: BpyUILayout):
raise NotImplementedError()
def draw_animator_section(self, context: BpyContext, layout: BpyUILayout):
raise NotImplementedError()
def draw_edit_section(self, context: BpyContext, layout: BpyUILayout):
row = layout.row()
row.label(text="Clean Up:")
box = layout.box()
row = box.row()
row.label(text="Shader Editor:")
row = box.row()
row.operator("object.remove_lip_sync_node_groups")
box = layout.box()
row = box.row()
row.label(text="Animation:")
row = box.row()
operator = row.operator("object.remove_lip_sync_animations", text="Remove SK")
operator.animation_type = "SHAPEKEYS" # type: ignore
operator = row.operator("object.remove_lip_sync_animations", text="Remove SPT")
operator.animation_type = "SPRITESHEET" # type: ignore
row = box.row()
operator = row.operator(
"object.remove_lip_sync_animations", text="Remove all Animations"
)
operator.animation_type = "ALL" # type: ignore
box = layout.box()
row = box.row()
row.prop(self.props, "lip_sync_2d_remove_animation_data")
row.prop(self.props, "lip_sync_2d_remove_cgp_node_group")
row = box.row()
row.alert = True
row.operator("object.remove_lip_sync_from_selection")
def draw_baking_section(self, context: BpyContext, layout: BpyUILayout):
if not self.is_model_installed:
new_row = layout.row()
new_row.label(
text="Select a Language Model before Analyzing audio",
icon="WARNING_LARGE",
)
new_row = layout.row()
new_row.operator(
"sound.cgp_analyze_audio", text="Bake audio", icon="SCRIPTPLUGINS"
)
new_row.enabled = self.is_model_installed
def draw_bake_section(self, context: BpyContext, layout: BpyUILayout):
panel_head, panel_body = layout.panel(
"cgp_lipsync_bake_settings_dropdown", default_closed=True
)
panel_head.label(text="Bake Settings")
if panel_body is not None:
row = panel_body.row()
row.label(text="Misc:")
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_use_clear_keyframes")
row = panel_body.row()
row.label(text="Range:")
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_use_bake_range")
row = panel_body.row(align=True)
row.prop(self.props, "lip_sync_2d_bake_start", text="Start")
row.prop(self.props, "lip_sync_2d_bake_end", text="End")
row.enabled = self.props.lip_sync_2d_use_bake_range # type: ignore
@@ -0,0 +1,117 @@
import bpy
from ..lipsync_types import BpyObject
from .AnimatorPanelMixin import AnimatorPanelMixin
from ..Core.phoneme_to_viseme import viseme_items_mpeg4_v2 as viseme_items
from ..lipsync_types import BpyContext, BpyUILayout
class AnimatorPanelPoseAssetsStrategy(AnimatorPanelMixin):
def __init__(self, obj: BpyObject):
super().__init__(obj)
if not isinstance(obj.data, bpy.types.Mesh):
return
def draw_animator_section(self, context: BpyContext, layout: BpyUILayout):
panel_header, panel_body = layout.panel(
"vpg_lipsync_animator_dropdown", default_closed=True
)
panel_header.label(text="Rig Settings")
if panel_body is not None:
row = panel_body.row()
row.label(text="Rig Type")
row = panel_body.row(align=True)
row.prop(self.props, "lip_sync_2d_rig_type_basic", toggle=True)
row.prop(self.props, "lip_sync_2d_rig_type_advanced", toggle=True)
panel_body.separator(factor=1)
def draw_animation_section(self, context: BpyContext, layout: BpyUILayout):
panel_header, panel_body = layout.panel(
"cgp_lipsync_animation_dropdown", default_closed=False
)
panel_header.label(text="Animation Settings")
if panel_body is not None:
row = panel_body.row()
row.label(text="Motion:")
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_close_motion_duration")
self.draw_thresholds(self.props, panel_body)
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_prioritize_accuracy", toggle=True)
def draw_visemes_section(self, context: BpyContext, layout: BpyUILayout):
panel_head, panel_body = layout.panel(
"cgp_lipsync_viseme_dropdown", default_closed=False
)
panel_head.label(text="Viseme Settings")
if panel_body is not None:
row = panel_body.row(align=True)
row.label(text="Viseme")
row.label(text="Pose")
visemes = viseme_items(None, None)
for i, viseme in enumerate(visemes):
lang_code = list(viseme)[0]
row = panel_body.row(align=True)
row.label(text=f"{lang_code}")
row.prop(self.props, f"lip_sync_2d_viseme_pose_{lang_code}", text="")
def draw_thresholds(self, props, layout):
row = layout.row()
row.label(text="Thresholds:")
data_list = ["lip_sync_2d_in_between_threshold", "lip_sync_2d_sil_threshold"]
row = layout.row()
for data in data_list:
row.prop(props, data)
def draw_baking_section(self, context: BpyContext, layout: BpyUILayout):
if not self.is_model_installed:
new_row = layout.row()
new_row.label(
text="Select a Language Model before Analyzing audio",
icon="WARNING_LARGE",
)
new_row = layout.row()
new_row.operator(
"sound.cgp_analyze_audio", text="Bake audio", icon="SCRIPTPLUGINS"
)
new_row.enabled = self.is_model_installed
def draw_edit_section(self, context: BpyContext, layout: BpyUILayout):
row = layout.row()
row.label(text="Pose Assets:")
box = layout.box()
row = box.row(align=True)
row.operator("lipsync2d.refresh_pose_assets")
row = layout.row()
row.label(text="Clean Up:")
box = layout.box()
row = box.row()
row.label(text="Animation:")
row = box.row()
operator = row.operator(
"object.remove_lip_sync_animations", text="Remove Pose Animation"
)
operator.animation_type = "POSEASSETS" # type: ignore
row = box.row()
operator = row.operator(
"object.remove_lip_sync_animations", text="Remove all Animations"
)
operator.animation_type = "ALL" # type: ignore
box = layout.box()
row = box.row()
row.prop(self.props, "lip_sync_2d_remove_animation_data")
row = box.row()
row.alert = True
row.operator("object.remove_lip_sync_from_selection")
@@ -0,0 +1,100 @@
import bpy
from ..lipsync_types import BpyObject
from .AnimatorPanelMixin import AnimatorPanelMixin
from ..Core.phoneme_to_viseme import viseme_items_mpeg4_v2 as viseme_items
from ..lipsync_types import BpyContext, BpyUILayout
class AnimatorPanelShapeKeysStrategy(AnimatorPanelMixin):
def __init__(self, obj: BpyObject):
super().__init__(obj)
if not isinstance(obj.data, bpy.types.Mesh):
return
self.at_least_two_shape_keys = bool(
(
obj.data
and obj.data.shape_keys
and obj.data.shape_keys.key_blocks.__len__() > 1
)
)
self.is_relative = bool(
(obj.data and obj.data.shape_keys and obj.data.shape_keys.use_relative)
)
def draw_animator_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_animation_section(self, context: BpyContext, layout: BpyUILayout):
panel_header, panel_body = layout.panel(
"cgp_lipsync_animation_dropdown", default_closed=False
)
panel_header.label(text="Animation Settings")
if panel_body is not None:
row = panel_body.row()
row.label(text="Motion:")
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_close_motion_duration")
self.draw_thresholds(self.props, panel_body)
def draw_visemes_section(self, context: BpyContext, layout: BpyUILayout):
panel_head, panel_body = layout.panel(
"cgp_lipsync_viseme_dropdown", default_closed=True
)
panel_head.label(text="Viseme Settings")
if panel_body is not None:
row = panel_body.row(align=True)
row.label(text="Viseme")
row.label(text="Shape Key")
visemes = viseme_items(None, None)
for i, viseme in enumerate(visemes):
lang_code = list(viseme)[0]
row = panel_body.row(align=True)
row.label(text=f"{lang_code}")
row.prop(
self.props, f"lip_sync_2d_viseme_shape_keys_{lang_code}", text=""
)
def draw_thresholds(self, props, layout):
row = layout.row()
row.label(text="Thresholds:")
data_list = ["lip_sync_2d_in_between_threshold", "lip_sync_2d_sil_threshold"]
row = layout.row()
for data in data_list:
row.prop(props, data)
def draw_baking_section(self, context: BpyContext, layout: BpyUILayout):
if not self.is_model_installed:
new_row = layout.row()
new_row.label(
text="Select a Language Model before Analyzing audio",
icon="WARNING_LARGE",
)
if not self.at_least_two_shape_keys:
new_row = layout.row()
new_row.label(
text="Missing Shape Keys",
icon="WARNING_LARGE",
)
if not self.is_relative:
new_row = layout.row()
new_row.label(
text="Shape Keys have to be set to 'Relative'",
icon="WARNING_LARGE",
)
new_row = layout.row()
new_row.operator(
"sound.cgp_analyze_audio", text="Bake audio", icon="SCRIPTPLUGINS"
)
new_row.enabled = (
self.is_model_installed
and self.at_least_two_shape_keys
and self.is_relative
)
@@ -0,0 +1,101 @@
from .AnimatorPanelMixin import AnimatorPanelMixin
from ..Core.phoneme_to_viseme import viseme_items_mpeg4_v2 as viseme_items
from ..lipsync_types import BpyContext, BpyUILayout
class AnimatorPanelSpriteSheetStrategy(AnimatorPanelMixin):
def draw_animator_section(self, context: BpyContext, layout: BpyUILayout):
panel_header, panel_body = layout.panel(
"cgp_lipsync_animator_dropdown", default_closed=False
)
panel_header.label(text="Sprite Sheet Settings")
if panel_body is not None:
row = panel_body.row()
row.label(text="Area")
row = panel_body.row()
row.label(
text="Go in Edit Mode to define Mouth Area",
icon="INFO_LARGE",
)
row = panel_body.row()
row.operator("mesh.set_lips_area", text="Set Mouth Area")
panel_body.separator(factor=1)
row = panel_body.row()
row.label(text="Select your Sprite sheet")
panel_body.template_ID_preview(
self.props,
"lip_sync_2d_sprite_sheet",
rows=2,
cols=6,
open="image.open",
)
panel_body.separator(factor=1)
row = panel_body.row(align=True)
row.label(text="Spritesheet Format")
row.prop(self.props, "lip_sync_2d_sprite_sheet_format", text="")
panel_body.separator(factor=1)
row = panel_body.row(align=True)
if self.props["lip_sync_2d_sprite_sheet_format"] == 3:
row.prop(self.props, "lip_sync_2d_sprite_sheet_rows")
elif self.props["lip_sync_2d_sprite_sheet_format"] == 2:
row.prop(self.props, "lip_sync_2d_sprite_sheet_columns")
elif self.props["lip_sync_2d_sprite_sheet_format"] == 0:
row.prop(self.props, "lip_sync_2d_sprite_sheet_rows")
elif self.props["lip_sync_2d_sprite_sheet_format"] == 1:
row.prop(self.props, "lip_sync_2d_sprite_sheet_columns")
row.prop(self.props, "lip_sync_2d_sprite_sheet_rows")
row = panel_body.row()
row.prop(self.props, "lip_sync_2d_sprite_sheet_index")
panel_body.separator(factor=1)
row = panel_body.row()
row.label(text="Scale")
row = panel_body.row(align=True)
row.prop(self.props, "lip_sync_2d_sprite_sheet_sprite_scale")
row.prop(self.props, "lip_sync_2d_sprite_sheet_main_scale", text="Main")
def draw_animation_section(self, context: BpyContext, layout: BpyUILayout):
panel_header, panel_body = layout.panel(
"cgp_lipsync_animation_dropdown", default_closed=True
)
panel_header.label(text="Animation Settings")
if panel_body is not None:
self.draw_thresholds(self.props, panel_body)
def draw_visemes_section(self, context: BpyContext, layout: BpyUILayout):
panel_head, panel_body = layout.panel(
"cgp_lipsync_viseme_dropdown", default_closed=True
)
panel_head.label(text="Viseme Settings")
if panel_body is not None:
row = panel_body.row(align=True)
row.label(text="Viseme")
row.label(text="Image index")
visemes = viseme_items(None, None)
for i, viseme in enumerate(visemes):
lang_code = list(viseme)[0]
row = panel_body.row(align=True)
row.label(text=f"{lang_code}")
row.prop(self.props, f"lip_sync_2d_viseme_{lang_code}", text="")
def draw_thresholds(self, props, layout):
row = layout.row()
row.label(text="Thresholds:")
data_list = [
"lip_sync_2d_sps_in_between_threshold",
"lip_sync_2d_sps_sil_threshold",
]
row = layout.row()
for data in data_list:
row.prop(props, data)
@@ -0,0 +1,68 @@
import bpy
from .AnimatorPanelPoseAssetsStrategy import AnimatorPanelPoseAssetsStrategy
from .protocols import LIPSYNC2D_AnimatorPanel
from ..Panels.AnimatorPanelShapeKeysStrategy import AnimatorPanelShapeKeysStrategy
from ..Panels.AnimatorPanelSpriteSheetStrategy import AnimatorPanelSpriteSheetStrategy
class LIPSYNC2D_PT_Edit(bpy.types.Panel):
bl_idname = "LIPSYNC2D_PT_Edit"
bl_label = "Quick Edit"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Lip Sync"
bl_options = {"DEFAULT_CLOSED"}
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
self.animator_panel: LIPSYNC2D_AnimatorPanel | None = None
def draw(self, context: bpy.types.Context):
if self.layout is None:
return
if context.scene is None:
return
if context.preferences is None:
return
active_obj = context.active_object
if active_obj is None or (
active_obj.type != "MESH" and active_obj.type != "ARMATURE"
):
self.layout.label(
text="Please, select an Object with Mesh Data or an Armature",
icon="INFO_LARGE",
)
return
if not hasattr(active_obj, "lipsync2d_props"):
self.layout.label(
text="Something wrong happened. Try uninstall / reinstall Lip Sync Addon",
icon="WARNING_LARGE",
)
return
if "lip_sync_2d_initialized" not in context.active_object.lipsync2d_props: # type: ignore
self.layout.label(text="Press on Add Lip Sync to start", icon="INFO_LARGE")
return
props = active_obj.lipsync2d_props # type: ignore
if props is None:
return
if props.lip_sync_2d_lips_type == "SHAPEKEYS":
self.animator_panel = AnimatorPanelShapeKeysStrategy(active_obj)
elif props.lip_sync_2d_lips_type == "SPRITESHEET":
self.animator_panel = AnimatorPanelSpriteSheetStrategy(active_obj)
elif props.lip_sync_2d_lips_type == "POSEASSETS":
self.animator_panel = AnimatorPanelPoseAssetsStrategy(active_obj)
if self.animator_panel is None:
return
self.animator_panel.draw_edit_section(context, self.layout)
@@ -0,0 +1,90 @@
import bpy
from .AnimatorPanelPoseAssetsStrategy import AnimatorPanelPoseAssetsStrategy
from .protocols import LIPSYNC2D_AnimatorPanel
from .AnimatorPanelSpriteSheetStrategy import AnimatorPanelSpriteSheetStrategy
from .AnimatorPanelShapeKeysStrategy import AnimatorPanelShapeKeysStrategy
class LIPSYNC2D_PT_Panel(bpy.types.Panel):
"""Creates a Panel in the scene context of the property editor"""
bl_label = "Lip Sync"
bl_idname = "LIPSYNC2D_PT_Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Lip Sync"
def __init__(self, *args, **kargs) -> None:
super().__init__(*args, **kargs)
self.animator_panel: LIPSYNC2D_AnimatorPanel | None = None
def draw(self, context: bpy.types.Context):
if self.layout is None:
return
if context.scene is None:
return
if context.preferences is None:
return
active_obj = context.active_object
if active_obj is None or (
active_obj.type != "MESH" and active_obj.type != "ARMATURE"
):
self.layout.label(
text="Please, select an Object with Mesh Data or an Armature",
icon="INFO_LARGE",
)
return
if not hasattr(active_obj, "lipsync2d_props"):
self.layout.label(
text="Something wrong happened. Try uninstall / reinstall Lip Sync Addon",
icon="WARNING_LARGE",
)
return
props = active_obj.lipsync2d_props # type: ignore
if props is None:
return
if props.lip_sync_2d_lips_type == "SHAPEKEYS":
self.animator_panel = AnimatorPanelShapeKeysStrategy(active_obj)
elif props.lip_sync_2d_lips_type == "SPRITESHEET":
self.animator_panel = AnimatorPanelSpriteSheetStrategy(active_obj)
elif props.lip_sync_2d_lips_type == "POSEASSETS":
self.animator_panel = AnimatorPanelPoseAssetsStrategy(active_obj)
layout = self.layout
if (
context.active_object is None
or not hasattr(context.active_object, "lipsync2d_props")
or context.active_object.lipsync2d_props.lip_sync_2d_initialized == False # type: ignore
):
row = layout.row(align=True)
row.operator(
"object.set_lipsync_custom_properties", text="Add Lip Sync on Selection"
)
return
row = layout.row(align=True)
row.label(text="Animation type")
row.prop(props, "lip_sync_2d_lips_type", text="")
if self.animator_panel is None:
return
layout.separator()
self.animator_panel.draw_animator_section(context, layout)
self.animator_panel.draw_visemes_section(context, layout)
self.animator_panel.draw_animation_section(context, layout)
self.animator_panel.draw_bake_section(context, layout)
layout.separator()
self.animator_panel.draw_baking_section(context, layout)
@@ -0,0 +1,32 @@
import platform
import bpy
from ..LIPSYNC2D_Utils import get_package_name
from ..Preferences.LIPSYNC2D_AP_Preferences import LIPSYNC2D_AP_Preferences
class LIPSYNC2D_PT_Settings(bpy.types.Panel):
bl_idname="LIPSYNC2D_PT_Settings"
bl_label="Quick Setup"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lip Sync'
platform = platform.system()
def draw(self, context: bpy.types.Context):
layout = self.layout
prefs = context.preferences.addons[get_package_name()].preferences # type: ignore
if not layout or prefs is None:
return
self.draw_espeak_model_settings(layout, prefs)
def draw_espeak_model_settings(self, layout: bpy.types.UILayout, prefs: bpy.types.AddonPreferences):
LIPSYNC2D_AP_Preferences.draw_online_access_warning(layout)
row = layout.row()
row.label(text="Language Model")
row.prop(prefs, "current_lang", text="")
LIPSYNC2D_AP_Preferences.draw_model_state(row) #type: ignore
@@ -0,0 +1,23 @@
from typing import Protocol
from ..lipsync_types import BpyContext, BpyUILayout
class LIPSYNC2D_AnimatorPanel(Protocol):
def draw_animation_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_visemes_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_edit_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_baking_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_animator_section(self, context: BpyContext, layout: BpyUILayout):
pass
def draw_bake_section(self, context: BpyContext, layout: BpyUILayout):
pass