Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea
.vscode
venv
.venv
__pycache__
/release/
copyfiles.bat
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

### Get the correct zip file

Download the zip file specifically for your platform. The zip file format is `rhubarb_lipsync_ng-<your_system>-<version>.zip`. For example: `rhubarb_lipsync_ng-Windows-1.6.1.zip` for Windows.
Download the zip file specifically for your platform. The zip file format is `rhubarb_lipsync_ng-<your_system>-<version>.zip`. For example: `rhubarb_lipsync_ng-Windows-1.7.0.zip` for Windows.

| [🪟 Windows](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.6.1/rhubarb_lipsync_ng-Windows-1.6.1.zip) | [🍏 macOS](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.6.1/rhubarb_lipsync_ng-macOS-1.6.1.zip) | [🐧 Linux](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.6.1/rhubarb_lipsync_ng-Linux-1.6.1.zip) |
| [🪟 Windows](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.7.0/rhubarb_lipsync_ng-Windows-1.7.0.zip) | [🍏 macOS](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.7.0/rhubarb_lipsync_ng-macOS-1.7.0.zip) | [🐧 Linux](https://github.com/Premik/blender_rhubarb_lipsync_ng/releases/download/v1.7.0/rhubarb_lipsync_ng-Linux-1.7.0.zip) |
|----------|--------------|------|

### Blender 4.2+
Expand Down Expand Up @@ -141,7 +141,7 @@ Note: Blender doesn't support baking shape-keys NLA tracks out-of-the-box. So if
1. Select the Armature and go to `Pose mode` (for normal-action tracks).
1. Select the Bones you want to bake. For example, press `a` to select all.
1. Select the strips in the NLA track you want to bake. Use the `b` key and box-select strips if you don't want to include all tracks.
1. Then go to `NLA Editor/main menu/Edit/Bake Action`.
1. Then go to `NLA Editor/main menu/Strip/Bake Action` (or `NLA Editor/main menu/Edit/Bake Action` in older Blender versions).
1. Consider checking the `Visual Keying` and `Clean Curves` options:

![Capture](doc/img/BakeNLATracks.png)
Expand All @@ -158,21 +158,21 @@ A new `Action` will be created and selected in the `Action Editor`. The two RLPS

Any Blender version newer than **v3.2**. Test results:

| Version | System | Total | Passed | Failed | Errors | Skipped | Status |
|--------------|---------|-------|--------|--------|--------|---------|--------|
| **4.5**.3 LTS| Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.4**.0 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.3**.2 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.2**.1 LTS| Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.1**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.0**.2 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.6**.13 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.5**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.4**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.3**.20 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.2**.2 | Windows | 70 | 51 | 7 | 8 | 4 | |
| **3.1**.2 | Windows | 70 | 51 | 7 | 8 | 4 | ❌ |
| **3.0**.1 | Windows | 70 | 42 | 0 | 24 | 4 | ❌ |
| Version | System | Total | Passed | Failed | Errors | Skipped | Status |
|---------------|---------|-------|--------|--------|--------|---------|--------|
| **5.0**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.5**.5 LTS | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.4**.3 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.3**.2 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.2**.16 LTS| Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.1**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **4.0**.2 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.6**.23 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.5**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.4**.1 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.3**.21 | Windows | 70 | 66 | 0 | 0 | 4 | ✔️ |
| **3.2**.2 | Windows | 70 | 51 | 7 | 8 | 4 | ❌ |
| **3.1**.2 | Windows | 70 | 51 | 7 | 8 | 4 | ❌ |



Expand Down
76 changes: 74 additions & 2 deletions dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,78 @@ https://docs.blender.org/api/blender_python_api_master/bpy.props.html?highlight=

### High

### v5.0

Legacy now: https://projects.blender.org/blender/blender/issues/146586
https://code.blender.org/2023/07/animation-workshop-june-2023/
https://blender.stackexchange.com/questions/339852/how-exactly-do-actionslots-work
https://developer.blender.org/docs/release_notes/4.4/python_api/#slotted-actions

- `action.fcurves` Update the use of Action.fcurves to use channelbag = anim_utils.action_get_channelbag_for_slot(action, action_slot)
```
action_ensure_channelbag_for_slot( # Blender 5.0+
action: bpy.types.Action,
slot: bpy.types.ActionSlot
) -> bpy.types.ActionChannelbag
Ensure a layer and a keyframe strip exists, then ensure that strip has a channelbag for the slot.
```

```
action_get_channelbag_for_slot(
action: bpy.types.Action | None,
slot: bpy.types.ActionSlot | None
) -> bpy.types.ActionChannelbag | None
Returns the first channelbag found for the slot.
In case there are multiple layers or strips they are iterated until a
channelbag for that slot is found. In case no matching channelbag is found, returns None.
```

- `action.groups`e
- `action.id_root`

`import bpy_extras.anim_utils`
```
['Action', 'ActionChannelbag',
'ActionSlot', 'AutoKeying', 'BakeOptions', 'Context',
'FCurveKey', 'Iterable', 'Iterator', 'KeyframesCo',
'KeyingSet', 'ListKeyframes', 'Mapping', 'Object', 'Optional', 'PoseBone', 'Sequence', 'Union',
'action_ensure_channelbag_for_slot', 'action_get_channelbag_for_slot', 'action_get_first_suitable_slot', 'animdata_get_channelbag_for_assigned_slot',
'bake_action', 'bake_action_iter', 'bake_action_objects', 'bake_action_objects_iter', 'bpy', 'contextlib', 'dataclass', 'rna_idprop_value_to_python']
```

Action
---layers->*ActionLayers (only one layer, and one strip?)
----slots->*ActionSlots

ActionLayer
----strips->*ActionStrips-->

ActionStrip=>ActionKeyframeStrip
channelbags--->ActionChannelbags

ActionChannelbag ----fscurves->*ActionChannelbagFCurves
---slot->ActionSlot

ActionSlot
target_id_type (https://upbge.org/docs/latest/api/bpy.types.ActionSlot.html#bpy.types.ActionSlot.target_id_type)

The Channelbag is what contains a set of F-Curves, and so what was the legacy Action data model now is represented by a Channelbag.


```
Ensuring FCurves Exist:
obj = bpy.context.object
action = obj.animation_data.action
fcurve = action.fcurve_ensure_for_datablock(obj, "location", index=0)

action = D.actions[0]
for layer in action.layers:
for strip in layer.strips:
if hasattr(strip, "channelbag"):
for fcurve in strip.channelbag.fcurves:
print(fcurve)
```

* Mapping wizards
* Clear - will remove the mapping (delete from the object completly?)
* Face-it - From Test rig - Normal actions+shape-key corrections? 2) From shape-key test action 3) The final control rig (doesn't need a wizard)
Expand Down Expand Up @@ -191,14 +263,14 @@ https://docs.blender.org/api/blender_python_api_master/bpy.props.html?highlight=
- [ ] Paste sound_strip first, create captures of them and render to NLA
* Bake to NLA - batchmode?
- There should be an option to bake multiple captures of the (selected/all objects). Based on the channel or othe crit. So bake can be done 1x for each single character.



### Normal
* `is_fcurve_for_shapekey`
- there is actually better way, the Action has targe_type/id or similar
- there is actually better way, the Action has targe_type/id or similar: target_id_types # OBJECT, KEY, NODETREE - replace the only_shapekeys bool in the MappingProperties with generic action type and add support from NODETREE type (animating material properties, like frame offset)
- Add support for other action types , like material/shader graph
- Refacotr the action filtering toolbar, have the action-types like a dropdown with checkable action types,
* The rslp tab name in the preferences has separate entries for mapping/capturing, but renaming them won't make two tabs as expected.
* In prefs, when executable doesn't exist make the control red
* Integrate with: https://mecabricks.com/en/shop/product/6
* Bring some of the old baking method back with fixed lengths
Expand Down
Binary file added doc/img/release/action_slots.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/img/release/aud_disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rhubarb_lipsync"
version = "1.6.1"
version = "1.7.0"
license = { file = "LISENSE" }


Expand Down
23 changes: 22 additions & 1 deletion release_notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Release Notes

## v1.7.0
**Date:** 2025-12-29

- Action **Slots** are now supported, while maintaining compatibility with older Blender versions. Slots were added in Blender `v4.4`.

![AudioStripSync](doc/img/release/action_slots.png)

- Blender version `v5+` is supported.
- Fixed the minimal Blender version supported (`v3.3`) in the metadata to avoid warning at the Blender start.


## v1.6.1
**Date:** 2025-09-15

Made the `aud` module optional. Sound conversion is disabled when the module is broken or missing.
This is a workaround for the #27, #19 and #24

![AudioStripSync](doc/img/release/aud_disabled.png)

---

## v1.6.0

**Date:** 2025-06-12
Expand Down Expand Up @@ -225,4 +246,4 @@ Bugfix release, removed stalled imports
- Project renamed to `rhubarb_lipsync_ng` as there was nothing left from the original code-base. The versioning was reset as well.
- The Action baking somehow works now. But the Strip Placement needs a rework. Especially the strip-ends placing. Baking of the shape keys actions is not implemented yet.
- Captures are now bound to `Scene` and only Mapping-settings are bound to individual `Object(s)` (typically armature). So one capture can be used for multiple objects and baked at once.
- Setting the start frame works as expected.
- Setting the start frame works as expected.
4 changes: 2 additions & 2 deletions rhubarb_lipsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
bl_info = {
'name': 'Rhubarb Lipsync NG',
'author': 'Premysl Srubar. Inspired by the original version by Andrew Charlton. Includes Rhubarb Lip Sync by Daniel S. Wolf',
'version': (1, 6, 1),
'blender': (4, 0, 2),
'version': (1, 7, 0),
'blender': (3, 3, 0),
'location': '3d View > Sidebar',
'description': 'Integrate Rhubarb Lipsync into Blender',
'wiki_url': 'https://github.com/Premik/blender_rhubarb_lipsync_ng',
Expand Down
95 changes: 95 additions & 0 deletions rhubarb_lipsync/blender/action_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import logging

import bpy

log = logging.getLogger(__name__)


def is_fcurve_for_shapekey(fcurve: bpy.types.FCurve) -> bool:
"""Determine if an fcurve is for a shape-key action."""
return fcurve.data_path.startswith("key_blocks[") # There doesn't seems to be a better way that check the data path


def is_action_shape_key_action(action: bpy.types.Action) -> bool:
"""Determine whether an action is a shape-key action or a regular one."""
if not action:
return False
types = get_target_id_types_for_action(action)
return "KEY" in types


def slots_supported_for_action(action: bpy.types.Action) -> bool:
"""Check if the provided action supports slots. Since Blender v4.4. Mandatory in v5+"""
return hasattr(action, "slots")


def get_target_id_types_for_action(action: bpy.types.Action) -> list[str]:
if not slots_supported_for_action(action):
return [action.id_root]
return [slot.target_id_type for slot in action.slots]


def is_action_blank(action: bpy.types.Action) -> bool:
if not slots_supported_for_action(action):
return not bool(action.fcurves)
if not bool(action.slots) or not bool(action.layers) or not bool(action.layers[0].strips):
return True
return False


def get_action_slot_keys(action: bpy.types.Action, target_id_type: str = None) -> list[str]:
if is_action_blank(action):
return []
if not slots_supported_for_action(action):
return [""]
if not target_id_type:
return [slot.identifier for slot in action.slots]
return [slot.identifier for slot in action.slots if slot.target_id_type == target_id_type]


def get_animdata_slot_key(ad: bpy.types.AnimData) -> str:
if not ad:
return ""
if not hasattr(ad, "action_slot"):
return "" # Old Blender without slots
if not ad.action_slot:
return ""
return ad.action_slot.identifier


def set_animdata_slot_key(ad: bpy.types.AnimData, slot_key: str) -> None:
if not ad:
return
if not hasattr(ad, "action_slot"):
return
if not ad.action:
return
if not slot_key:
slot = ad.action.slots[0] # The first legacy slot
else:
slot = ad.action.slots[slot_key]
ad.action_slot = slot


def get_action_fcurves(action: bpy.types.Action, slot_key: str | int = 0) -> bpy.types.bpy_prop_collection:
if is_action_blank(action):
return [] # type: ignore
if not slots_supported_for_action(action):
return action.fcurves # bpy.types.ActionFCurves
# https://developer.blender.org/docs/release_notes/4.4/python_api/#deprecated

first_strip: bpy.types.ActionKeyframeStrip = action.layers[0].strips[0] # type: ignore
bag = first_strip.channelbag(action.slots[slot_key])
if not bag:
return []
return bag.fcurves # bpy.types.ActionChannelbagFCurves


def ensure_action_fcurves(action: bpy.types.Action, slot_name: str, slot_type='OBJECT') -> bpy.types.bpy_prop_collection:
if not slots_supported_for_action(action):
action.id_root = slot_type
return action.fcurves
layer = action.layers[0] if action.layers else action.layers.new("Layer1")
strip = layer.strips[0] if layer.strips else layer.strips.new(type='KEYFRAME')
slot = action.slots.new(id_type=slot_type, name=slot_name)
return strip.channelbag(slot, ensure=True).fcurves
18 changes: 11 additions & 7 deletions rhubarb_lipsync/blender/baking_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def on_object(self, bctx: baking_utils.BakingContext) -> None:
for t in bctx.unique_tracks:
self.on_track(bctx, t)

def execute(self, ctx: Context) -> set[str]:
def execute(self, ctx: Context) -> ui_utils.OperatorReturnSet:
b = baking_utils.BakingContext(ctx)
self.strips_removed = 0
self.tracks_cleaned = 0
Expand Down Expand Up @@ -102,7 +102,7 @@ class PlacementBlendInOutRatioPreset(bpy.types.Operator):
default='0.5',
)

def execute(self, ctx: Context) -> set[str]:
def execute(self, ctx: Context) -> ui_utils.OperatorReturnSet:
prefs = RhubarbAddonPreferences.from_context(ctx)
sprops: StripPlacementPreferences = prefs.strip_placement
rate = float(self.ratio_type)
Expand Down Expand Up @@ -130,7 +130,7 @@ class PlacementScaleFromPreset(bpy.types.Operator):
default='1.25',
)

def execute(self, ctx: Context) -> set[str]:
def execute(self, ctx: Context) -> ui_utils.OperatorReturnSet:
prefs = RhubarbAddonPreferences.from_context(ctx)
sprops: StripPlacementPreferences = prefs.strip_placement
rate = float(self.scale_type)
Expand Down Expand Up @@ -159,7 +159,7 @@ class PlacementCueTrimFromPreset(bpy.types.Operator):
),
)

def execute(self, ctx: Context) -> set[str]:
def execute(self, ctx: Context) -> ui_utils.OperatorReturnSet:
prefs = RhubarbAddonPreferences.from_context(ctx)
clp: CueListPreferences = prefs.cue_list_prefs
fps = ctx.scene.render.fps
Expand Down Expand Up @@ -190,7 +190,7 @@ def draw_popup(this: bpy.types.UIPopupMenu, context: Context) -> None:
row.template_image(tex, "image", tex.image_user)
# row.label(text=f"{tex.image}")

def execute(self, context: Context) -> set[str]:
def execute(self, context: Context) -> ui_utils.OperatorReturnSet:
# self.tex=tex
img, tex = IconsManager.placement_help_image()
img.preview_ensure()
Expand Down Expand Up @@ -246,6 +246,8 @@ def invoke(self, context: Context, event: bpy.types.Event) -> set:

rll: ResultLogListProperties = CaptureListProperties.from_context(context).last_resut_log
rll.clear() # Clear log entries from last bake
bctx = baking_utils.BakingContext(context)
bctx.migrate_obj_mapping_to_slots()
wm = context.window_manager
return wm.invoke_props_dialog(self, width=480)

Expand Down Expand Up @@ -290,7 +292,7 @@ def to_strip(self) -> None:

self.place_strip(start, end, blend_in, blend_out, scale)

def place_strip(self, start: float, end: float, blend_in: float, blend_out: float, scale: float):
def place_strip(self, start: float, end: float, blend_in: float, blend_out: float, scale: float) -> None:
b = self.bctx
cf = b.current_cue
cue: MouthCue = cf and cf.cue or None
Expand All @@ -305,6 +307,8 @@ def place_strip(self, start: float, end: float, blend_in: float, blend_out: floa
strip.action_frame_end = b.current_mapping_item.frame_end
strip.frame_start = start # Set start frame again as float (ctor takes only int)
strip.scale = scale
if b.current_mapping_item.slot:
strip.action_slot = b.current_mapping_item.slot
# if b.ctx.scene.show_subframe:
strip.frame_end = end
self.strips_added += 1
Expand Down Expand Up @@ -346,7 +350,7 @@ def bake_cue(self) -> None:
log.trace(f"Baking on object {obj} ") # type: ignore
self.bake_cue_on_object(obj)

def execute(self, ctx: Context) -> set[str]:
def execute(self, ctx: Context) -> ui_utils.OperatorReturnSet:
self.bctx = baking_utils.BakingContext(ctx)
self.strips_added = 0
b = self.bctx
Expand Down
Loading
Loading