2026-02-16
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
name: Full Build (Espeak + Blender Addon)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Espeak NG for Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Clone Espeak NG repo
|
||||
- name: Clone forked espeak NG
|
||||
# Clone fork containing a fix to filter out mbrola voices on Windows
|
||||
# Without it, phonemizer will try to use unexisting voices
|
||||
run: git clone --depth 1 -b fix-windows-list-voices https://github.com/Charley3d/espeak-ng.git
|
||||
|
||||
# Clone pcaudiolib repo (required for Windows build)
|
||||
- name: Clone pcaudiolib
|
||||
run: git clone --depth 1 https://github.com/espeak-ng/pcaudiolib.git espeak-ng/src/pcaudiolib
|
||||
|
||||
- name: Modify config.h
|
||||
working-directory: espeak-ng
|
||||
shell: bash
|
||||
run: |
|
||||
CONFIG_FILE=src/windows/config.h
|
||||
echo "Updating $CONFIG_FILE..."
|
||||
|
||||
# Add or update the definitions
|
||||
sed -i "s/^#define USE_KLATT.*/#define USE_KLATT 0/" $CONFIG_FILE || echo "#define USE_KLATT 0" >> $CONFIG_FILE
|
||||
sed -i "s/^#define USE_SPEECHPLAYER.*/#define USE_SPEECHPLAYER 0/" $CONFIG_FILE || echo "#define USE_SPEECHPLAYER 0" >> $CONFIG_FILE
|
||||
sed -i "s/^#define USE_MBROLA.*/#define USE_MBROLA 0/" $CONFIG_FILE || echo "#define USE_MBROLA 0" >> $CONFIG_FILE
|
||||
sed -i "s/^#define USE_SONIC.*/#define USE_SONIC 0/" $CONFIG_FILE || echo "#define USE_SONIC 0" >> $CONFIG_FILE
|
||||
sed -i "s/^#define USE_ASYNC.*/#define USE_ASYNC 0/" $CONFIG_FILE || echo "#define USE_ASYNC 0" >> $CONFIG_FILE
|
||||
|
||||
# Build Espeak NG for Windows
|
||||
- name: Build with MSBuild
|
||||
working-directory: espeak-ng
|
||||
shell: cmd
|
||||
run: |
|
||||
cd src/windows
|
||||
"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe > msbuild_path.txt
|
||||
set /p MSBUILD_PATH=<msbuild_path.txt
|
||||
call "%MSBUILD_PATH%" espeak-ng.sln /p:Configuration=Release
|
||||
|
||||
# Copy .dll to temporary dist dir
|
||||
- name: Copy Windows DLL + Data
|
||||
run: |
|
||||
mkdir dist
|
||||
copy espeak-ng\src\windows\x64\Release\libespeak-ng.dll dist\
|
||||
|
||||
# Zip artifacts
|
||||
- name: Zip Windows build
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path dist\* -DestinationPath espeak-ng-windows.zip
|
||||
|
||||
# Upload ZIP artifacts
|
||||
- name: Upload Windows Build of Espeak NG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-windows
|
||||
path: espeak-ng-windows.zip
|
||||
|
||||
build-linux:
|
||||
name: Build Espeak NG for linux
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clone forked eSpeak NG
|
||||
run: git clone --depth 1 -b fix-windows-list-voices https://github.com/Charley3d/espeak-ng.git
|
||||
|
||||
# Step 2: Install Build Dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y make autoconf automake libtool pkg-config
|
||||
sudo apt-get install -y gcc g++
|
||||
sudo apt-get install -y libpcaudio-dev
|
||||
|
||||
# Build & Copy lib and data folder to temporary dist dir
|
||||
- name: Building
|
||||
working-directory: espeak-ng
|
||||
run: |
|
||||
./autogen.sh
|
||||
./configure --with-klatt=no --with-speechplayer=no --with-mbrola=no --with-sonic=no --with-async=no
|
||||
make
|
||||
mkdir -p ../dist
|
||||
cp src/.libs/libespeak-ng.so* ../dist/
|
||||
zip -r ../espeak-ng-data.zip espeak-ng-data
|
||||
cd ../dist
|
||||
zip -r ../espeak-ng-linux.zip .
|
||||
|
||||
|
||||
# Upload ZIP artifacts
|
||||
- name: Upload Linux lib
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-linux
|
||||
path: espeak-ng-linux.zip
|
||||
|
||||
# Upload ZIP artifacts
|
||||
- name: Upload espeak data folder
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-data
|
||||
path: espeak-ng-data.zip
|
||||
|
||||
build-arm64:
|
||||
name: Build Espeak NG for macOS (arm64)
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clone forked eSpeak NG
|
||||
run: git clone --depth 1 -b fix-windows-list-voices https://github.com/Charley3d/espeak-ng.git
|
||||
|
||||
# Step 2: Install Build Dependencies
|
||||
- name: Install dependencies on macOS
|
||||
run: |
|
||||
brew update
|
||||
brew install make gcc autoconf automake libtool pkg-config portaudio
|
||||
|
||||
- name: Building arm64
|
||||
working-directory: espeak-ng
|
||||
run: |
|
||||
./autogen.sh
|
||||
./configure CFLAGS="-arch arm64" LDFLAGS="-arch arm64" --with-klatt=no --with-speechplayer=no --with-mbrola=no --with-sonic=no --with-async=no
|
||||
make
|
||||
cp src/.libs/libespeak-ng.dylib ../libespeak-ng-arm64.dylib
|
||||
|
||||
- name: Upload arm64 lib + data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-arm64
|
||||
path: libespeak-ng-arm64.dylib
|
||||
|
||||
build-x86_64:
|
||||
name: Build Espeak NG for macOS (x86_64)
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clone forked eSpeak NG
|
||||
run: git clone --depth 1 -b fix-windows-list-voices https://github.com/Charley3d/espeak-ng.git
|
||||
|
||||
# Step 2: Install Build Dependencies
|
||||
- name: Install dependencies on macOS
|
||||
run: |
|
||||
brew update
|
||||
brew install make gcc autoconf automake libtool pkg-config portaudio
|
||||
|
||||
- name: Building x86_64
|
||||
working-directory: espeak-ng
|
||||
run: |
|
||||
./autogen.sh
|
||||
./configure CFLAGS="-arch x86_64" LDFLAGS="-arch x86_64" --with-klatt=no --with-speechplayer=no --with-mbrola=no --with-sonic=no --with-async=no
|
||||
make
|
||||
cp src/.libs/libespeak-ng.dylib ../libespeak-ng-x86_64.dylib
|
||||
|
||||
- name: Upload x86_64 lib
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-x86_64
|
||||
path: libespeak-ng-x86_64.dylib
|
||||
|
||||
merge:
|
||||
name: Create Universal macOS Build
|
||||
runs-on: macos-latest
|
||||
needs: [build-arm64, build-x86_64]
|
||||
|
||||
steps:
|
||||
- name: Download arm64 lib + data
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-arm64
|
||||
|
||||
- name: Download x86_64 lib
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-x86_64
|
||||
|
||||
- name: Merge with lipo and prepare bundle
|
||||
run: |
|
||||
mkdir -p dist
|
||||
lipo -create -output dist/libespeak-ng.dylib \
|
||||
libespeak-ng-arm64.dylib \
|
||||
libespeak-ng-x86_64.dylib
|
||||
file dist/libespeak-ng.dylib
|
||||
cd dist
|
||||
zip -r ../espeak-ng-darwin.zip .
|
||||
|
||||
- name: Upload Universal lib
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-darwin
|
||||
path: espeak-ng-darwin.zip
|
||||
|
||||
collect-artifacts:
|
||||
name: Make Blender Addon
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-windows, merge, build-linux ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Blender via Snap
|
||||
run: sudo snap install blender --classic
|
||||
|
||||
- name: Download eSpeak Data Folder
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-data
|
||||
path: temp/espeak-ng-data
|
||||
|
||||
# Download each platform’s build artifact
|
||||
- name: Download Windows build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-windows
|
||||
path: temp/windows
|
||||
|
||||
- name: Download macOS build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-darwin
|
||||
path: temp/macos
|
||||
|
||||
- name: Download Linux build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: espeak-ng-linux
|
||||
path: temp/linux
|
||||
|
||||
# Move them to Assets/{platform}
|
||||
- name: Move artifacts to Assets
|
||||
run: |
|
||||
mkdir -p Assets/Archives/windows Assets/Archives/darwin Assets/Archives/linux
|
||||
cp -r temp/windows/* Assets/Archives/windows/
|
||||
cp -r temp/macos/* Assets/Archives/darwin/
|
||||
cp -r temp/linux/* Assets/Archives/linux/
|
||||
mkdir -p Assets/Archives/common && cp -r temp/espeak-ng-data/* Assets/Archives/common/
|
||||
|
||||
# Remove temp files and folders + unwanted files for release
|
||||
- name: Clean up folder to zip Blender Addon
|
||||
run: |
|
||||
rm -rf temp || true
|
||||
rm -rf dist || true
|
||||
rm -f .gitignore || true
|
||||
rm -f dev_tools.py || true
|
||||
rm -rf .github || true
|
||||
rm -rf .idea || true
|
||||
|
||||
- name: Build Extension with blender
|
||||
run: blender --command extension build
|
||||
|
||||
- name: Get generated zip filename
|
||||
id: get-zip
|
||||
run: echo "ZIP_NAME=$(basename $(ls ./*.zip))" >> $GITHUB_OUTPUT
|
||||
|
||||
# Upload the full release of Blender Add-on
|
||||
- name: Upload Blender Addon
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lipsync-addon
|
||||
path: ${{ steps.get-zip.outputs.ZIP_NAME }}
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-extension:
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
|
||||
name: Blender Extension Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get next version from semantic-release
|
||||
id: next_version
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
OUTPUT=$(npx semantic-release --dry-run 2>&1 || true)
|
||||
echo "$OUTPUT"
|
||||
|
||||
VERSION=$(echo "$OUTPUT" | grep -oP 'The next release version is \K[0-9]+\.[0-9]+\.[0-9]+' || true)
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "⚠️ No new version detected (probably no semantic commits)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "✅ Found next version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.next_version.outputs.version != ''
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install tomlkit
|
||||
if: steps.next_version.outputs.version != ''
|
||||
run: pip install tomlkit
|
||||
|
||||
- name: Update version in Blender Manifest
|
||||
if: steps.next_version.outputs.version != ''
|
||||
run: |
|
||||
python scripts/update_version.py ${{ steps.next_version.outputs.version }}
|
||||
|
||||
- name: Install Blender via Snap
|
||||
run: sudo snap install blender --classic
|
||||
|
||||
- name: Build Extension with blender
|
||||
run: blender --command extension build --split-platforms
|
||||
|
||||
# Upload the full release of Blender Add-on
|
||||
- name: Clean platform-specific archives
|
||||
run: |
|
||||
for zipfile in iocgpoly_lip_sync-*-*.zip; do
|
||||
echo "Examining $zipfile"
|
||||
unzip -l "$zipfile" | grep -i "archives"
|
||||
|
||||
# Extract platform from filename (between last - and _)
|
||||
platform=$(echo $zipfile | sed 's/.*-\(.*\)_.*/\1/')
|
||||
|
||||
echo "Processing $zipfile (platform: $platform)"
|
||||
|
||||
# Create temp dir
|
||||
temp_dir="${platform}_temp"
|
||||
unzip -q "$zipfile" -d "$temp_dir"
|
||||
|
||||
# List the actual directory structure
|
||||
echo "Directory structure in temp_dir:"
|
||||
ls -la "$temp_dir/Assets/Archives/"
|
||||
|
||||
rm "$zipfile"
|
||||
cd "$temp_dir"
|
||||
|
||||
# Remove irrelevant platform folders based on the platform
|
||||
case $platform in
|
||||
linux)
|
||||
rm -rf Assets/Archives/darwin Assets/Archives/windows
|
||||
;;
|
||||
macos)
|
||||
rm -rf Assets/Archives/linux Assets/Archives/windows
|
||||
;;
|
||||
windows)
|
||||
rm -rf Assets/Archives/darwin Assets/Archives/linux
|
||||
;;
|
||||
esac
|
||||
|
||||
# List what remains
|
||||
echo "Remaining directories:"
|
||||
ls -la Assets/Archives/
|
||||
|
||||
# Rezip with same name
|
||||
zip -r "../$zipfile" .
|
||||
cd ..
|
||||
rm -rf "$temp_dir"
|
||||
done
|
||||
|
||||
- name: Semantic release
|
||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release --debug true
|
||||
|
||||
- name: Upload Blender Addon
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || steps.next_version.outputs.version == '' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: extension-builds
|
||||
path: ./*.zip
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
name: Upload to Extensions Platform
|
||||
|
||||
on:
|
||||
# release:
|
||||
# types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upload-to-extensions:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Release Assets and Notes
|
||||
run: |
|
||||
mkdir assets
|
||||
gh release download -p "*.zip" -D assets/
|
||||
RELEASE_NOTES=$(gh release view --json body | jq -r .body)
|
||||
echo "$RELEASE_NOTES" > release_notes.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload to Extensions Platform
|
||||
env:
|
||||
EXTENSION: iocgpoly_lip_sync
|
||||
run: |
|
||||
for zipfile in assets/*.zip; do
|
||||
echo "Uploading $zipfile to Blender Extensions Platform"
|
||||
curl -X POST https://extensions.blender.org/api/v1/extensions/${EXTENSION}/versions/upload/ \
|
||||
-H "Authorization:bearer ${{ secrets.BLENDER_EXTENSIONS_TOKEN }}" \
|
||||
-F "version_file=@${zipfile}" \
|
||||
-F "release_notes=<release_notes.txt"
|
||||
done
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"repositoryUrl": "git@github.com:Charley3d/lip-sync.git",
|
||||
"debug": true,
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{ "path": "iocgpoly_lip_sync-*-linux_x64.zip", "label": "LipSync for Linux ${nextRelease.gitTag}" },
|
||||
{ "path": "iocgpoly_lip_sync-*-macos_arm64.zip", "label": "LipSync for MacOS arm64 ${nextRelease.gitTag}" },
|
||||
{ "path": "iocgpoly_lip_sync-*-macos_x64.zip", "label": "LipSync for MacOS x64 ${nextRelease.gitTag}" },
|
||||
{ "path": "iocgpoly_lip_sync-*-windows_x64.zip", "label": "LipSync for Windows x64 ${nextRelease.gitTag}" }
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+30
-1
@@ -177,14 +177,43 @@ class LIPSYNC2D_PoseAssetsAnimator:
|
||||
:param interpolation: The interpolation type for the keyframes.
|
||||
:type interpolation: Literal["LINEAR"]
|
||||
"""
|
||||
# Determine where to read the pose asset fcurves from. In newer Blender
|
||||
# APIs pose assets may not expose `.fcurves` directly; the pose data is
|
||||
# stored in the action's first layer/strip channelbag. Fall back to
|
||||
# `pose_action.fcurves` when available.
|
||||
pose_fcurves = None
|
||||
|
||||
# Try to get fcurves from the pose asset's strip channelbag first
|
||||
try:
|
||||
if hasattr(pose_action, "layers") and len(pose_action.layers) > 0:
|
||||
pose_strip = pose_action.layers[0].strips[0]
|
||||
# Use first slot if present
|
||||
pose_slot = pose_action.slots[0] if getattr(pose_action, "slots", None) else None
|
||||
if pose_strip is not None and pose_slot is not None:
|
||||
pose_channelbag = pose_strip.channelbag(pose_slot)
|
||||
pose_fcurves = getattr(pose_channelbag, "fcurves", None)
|
||||
except Exception:
|
||||
pose_fcurves = None
|
||||
|
||||
# Fallback to action.fcurves when present
|
||||
if pose_fcurves is None:
|
||||
pose_fcurves = getattr(pose_action, "fcurves", None)
|
||||
|
||||
if pose_fcurves is None:
|
||||
# Nothing we can copy from
|
||||
return
|
||||
|
||||
for fcurve in self.channelbag.fcurves:
|
||||
pose_asset_fcurve = pose_action.fcurves.find(
|
||||
pose_asset_fcurve = pose_fcurves.find(
|
||||
fcurve.data_path, index=fcurve.array_index
|
||||
)
|
||||
if pose_asset_fcurve is None:
|
||||
continue
|
||||
|
||||
# Since Action is from a Pose Asset, we can safely assume that first keyframe point holds the Pose
|
||||
if len(pose_asset_fcurve.keyframe_points) == 0:
|
||||
continue
|
||||
|
||||
fcurve_value = pose_asset_fcurve.keyframe_points[0].co.y
|
||||
kframe = fcurve.keyframe_points.insert(
|
||||
frame,
|
||||
|
||||
+41
-6
@@ -57,11 +57,29 @@ class LIPSYNC_SpriteSheetAnimator:
|
||||
:type obj: BpyObject
|
||||
:return: None
|
||||
"""
|
||||
action = obj.animation_data.action if obj.animation_data else None
|
||||
if action:
|
||||
for fcurve in action.fcurves:
|
||||
if (action := obj.animation_data.action) is None:
|
||||
return
|
||||
|
||||
# Retrieve the strip and channelbag correctly for Layered Actions
|
||||
# Assuming single layer/strip structure as per setup()
|
||||
if not action.layers or not action.layers[0].strips:
|
||||
return
|
||||
|
||||
strip = cast(BpyActionKeyframeStrip, action.layers[0].strips[0])
|
||||
|
||||
# Ensure we get the correct slot for sprite sheet
|
||||
slot = action.slots.get(f"OB{SLOT_SPRITE_SHEET_NAME}")
|
||||
if not slot:
|
||||
return
|
||||
|
||||
try:
|
||||
channelbag = strip.channelbag(slot)
|
||||
for fcurve in channelbag.fcurves:
|
||||
if fcurve.data_path == "lipsync2d_props.lip_sync_2d_sprite_sheet_index":
|
||||
fcurve.keyframe_points.clear()
|
||||
except Exception:
|
||||
# Fallback or silence if channelbag creation fails (though it should exist if we are clearing)
|
||||
pass
|
||||
|
||||
def insert_keyframes(self, obj: BpyObject, props: BpyPropertyGroup, visemes_data: VisemeData,
|
||||
word_timing: WordTiming,
|
||||
@@ -203,12 +221,29 @@ class LIPSYNC_SpriteSheetAnimator:
|
||||
:type obj: BpyObject
|
||||
:return: None
|
||||
"""
|
||||
action = obj.animation_data.action if obj.animation_data else None
|
||||
if (action := obj.animation_data.action) is None:
|
||||
return
|
||||
|
||||
# We can try to use self.channelbag if it's already set up, but to be robust
|
||||
# we re-fetch it similar to clear_previous_keyframes to ensure we have the right one.
|
||||
|
||||
if not action.layers or not action.layers[0].strips:
|
||||
return
|
||||
|
||||
if action:
|
||||
for fcurve in action.fcurves:
|
||||
strip = cast(BpyActionKeyframeStrip, action.layers[0].strips[0])
|
||||
# Ensure we get the correct slot for sprite sheet
|
||||
slot = action.slots.get(f"OB{SLOT_SPRITE_SHEET_NAME}")
|
||||
|
||||
if not slot:
|
||||
return
|
||||
|
||||
try:
|
||||
channelbag = strip.channelbag(slot)
|
||||
for fcurve in channelbag.fcurves:
|
||||
for keyframe in fcurve.keyframe_points:
|
||||
keyframe.interpolation = 'CONSTANT'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def setup(self, obj: BpyObject):
|
||||
self.setup_animation_properties(obj)
|
||||
|
||||
@@ -53,8 +53,9 @@ phoneme_to_viseme_arkit_v2 = {
|
||||
"ŋ": "nn",
|
||||
"ɲ": "nn",
|
||||
"ɳ": "nn",
|
||||
"l": "nn",
|
||||
"ɫ": "nn",
|
||||
# LL – lateral group
|
||||
"l": "LL",
|
||||
"ɫ": "LL",
|
||||
# aa – open and low/mid front vowels
|
||||
"a": "aa",
|
||||
"aː": "aa",
|
||||
@@ -119,7 +120,8 @@ def viseme_items_mpeg4_v2(self, context):
|
||||
("kk", "kk", "K, G (velar stops)"),
|
||||
("CH", "CH", "CH, J (affricates)"),
|
||||
("SS", "SS", "S, Z, SH, ZH (narrow fricatives)"),
|
||||
("nn", "nn", "N, NG, L (nasals and laterals)"),
|
||||
("nn", "nn", "N, NG (nasals)"),
|
||||
("LL", "LL", "L (lateral)"),
|
||||
("RR", "RR", "R (r-like sounds)"),
|
||||
("aa", "aa", "A, Æ (open/low vowels)"),
|
||||
("E", "E", "E, Ø, Ə (mid front vowels)"),
|
||||
@@ -141,6 +143,7 @@ def phonemes_to_default_sprite_index():
|
||||
"CH": 10,
|
||||
"SS": 2,
|
||||
"nn": 10,
|
||||
"LL": 10,
|
||||
"RR": 3,
|
||||
"aa": 11,
|
||||
"E": 8,
|
||||
|
||||
@@ -62,7 +62,26 @@ class LIPSYNC2D_OT_AnalyzeAudio(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.set_bake_range()
|
||||
file_path = extract_audio()
|
||||
|
||||
props = context.active_object.lipsync2d_props # type: ignore
|
||||
target_channel = props.lip_sync_2d_bake_channel
|
||||
|
||||
muted_strips = []
|
||||
try:
|
||||
if target_channel != "ALL":
|
||||
target_ch_int = int(target_channel)
|
||||
for strip in context.scene.sequence_editor.strips_all:
|
||||
if strip.type == "SOUND" and not strip.mute:
|
||||
if strip.channel != target_ch_int:
|
||||
strip.mute = True
|
||||
muted_strips.append(strip)
|
||||
|
||||
file_path = extract_audio()
|
||||
|
||||
finally:
|
||||
# Restore mute state
|
||||
for strip in muted_strips:
|
||||
strip.mute = False
|
||||
|
||||
if not os.path.isfile(f"{file_path}"):
|
||||
self.report(
|
||||
@@ -90,15 +109,20 @@ class LIPSYNC2D_OT_AnalyzeAudio(bpy.types.Operator):
|
||||
phonemes = LIPSYNC2D_DialogInspector.extract_phonemes(words, context)
|
||||
|
||||
auto_obj = self.get_animator(obj)
|
||||
props = obj.lipsync2d_props # type: ignore
|
||||
debug_entries = [] if props.lip_sync_2d_debug_output else None
|
||||
|
||||
auto_obj.setup(obj)
|
||||
self.auto_insert_keyframes(
|
||||
auto_obj, obj, recognized_words, dialog_inspector, total_words, phonemes
|
||||
auto_obj, obj, recognized_words, dialog_inspector, total_words, phonemes, debug_entries
|
||||
)
|
||||
auto_obj.set_interpolation(obj)
|
||||
auto_obj.cleanup(obj)
|
||||
self.reset_bake_range()
|
||||
|
||||
if debug_entries is not None:
|
||||
self.write_debug_output(debug_entries)
|
||||
|
||||
if bpy.context.view_layer:
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
@@ -116,6 +140,7 @@ class LIPSYNC2D_OT_AnalyzeAudio(bpy.types.Operator):
|
||||
dialog_inspector: LIPSYNC2D_DialogInspector,
|
||||
total_words,
|
||||
phonemes,
|
||||
debug_entries: list | None = None,
|
||||
):
|
||||
props = obj.lipsync2d_props # type: ignore
|
||||
words = enumerate(recognized_words)
|
||||
@@ -123,9 +148,19 @@ class LIPSYNC2D_OT_AnalyzeAudio(bpy.types.Operator):
|
||||
for index, recognized_word in words:
|
||||
is_last_word = index == total_words - 1
|
||||
word_timing = dialog_inspector.get_word_timing(recognized_word)
|
||||
current_phonemes = phonemes[index]
|
||||
visemes_data = dialog_inspector.get_visemes(
|
||||
phonemes[index], word_timing["duration"]
|
||||
current_phonemes, word_timing["duration"]
|
||||
)
|
||||
|
||||
if debug_entries is not None:
|
||||
debug_entries.append({
|
||||
"word": recognized_word["word"],
|
||||
"phonemes": current_phonemes,
|
||||
"visemes": visemes_data,
|
||||
"start": word_timing["word_frame_start"],
|
||||
})
|
||||
|
||||
next_word_timing = dialog_inspector.get_next_word_timing(
|
||||
recognized_words, index
|
||||
)
|
||||
@@ -151,6 +186,54 @@ class LIPSYNC2D_OT_AnalyzeAudio(bpy.types.Operator):
|
||||
index,
|
||||
)
|
||||
|
||||
def write_debug_output(self, entries):
|
||||
text_name = "LipSync Debug"
|
||||
text = bpy.data.texts.get(text_name)
|
||||
if text is None:
|
||||
text = bpy.data.texts.new(text_name)
|
||||
else:
|
||||
text.clear()
|
||||
|
||||
# Header
|
||||
output = [
|
||||
f"{'Word':<15} {'Start':<10} {'Phonemes':<15} {'Viseme':<10} {'Frame':<10}",
|
||||
"-" * 60
|
||||
]
|
||||
|
||||
for entry in entries:
|
||||
word = entry['word']
|
||||
start_frame = entry['start']
|
||||
phonemes = entry['phonemes'] # list of phonemes strings
|
||||
phonemes_str = " ".join(phonemes)
|
||||
|
||||
viseme_data = entry['visemes']
|
||||
visemes_list = viseme_data['visemes']
|
||||
part_duration = viseme_data['visemes_parts']
|
||||
|
||||
# First line with word info
|
||||
first_viseme = visemes_list[0] if visemes_list else ""
|
||||
first_viseme_frame = f"{start_frame:.2f}"
|
||||
|
||||
# If no visemes, just print word info
|
||||
if not visemes_list:
|
||||
output.append(f"{word:<15} {start_frame:<10} {phonemes_str:<15}")
|
||||
continue
|
||||
|
||||
# Print first viseme with word info
|
||||
output.append(f"{word:<15} {start_frame:<10} {phonemes_str:<15} {visemes_list[0]:<10} {first_viseme_frame:<10}")
|
||||
|
||||
# Print remaining visemes
|
||||
current_frame = start_frame
|
||||
for i in range(1, len(visemes_list)):
|
||||
current_frame += part_duration
|
||||
viseme = visemes_list[i]
|
||||
output.append(f"{'':<15} {'':<10} {'':<15} {viseme:<10} {current_frame:.2f}")
|
||||
|
||||
# Add a separator blank line or just spacing
|
||||
# output.append("")
|
||||
|
||||
text.write("\n".join(output))
|
||||
|
||||
@staticmethod
|
||||
def get_animator(obj: BpyObject) -> LIPSYNC2D_LipSyncAnimator:
|
||||
props = obj.lipsync2d_props # type: ignore
|
||||
|
||||
@@ -95,6 +95,8 @@ class AnimatorPanelMixin:
|
||||
row = panel_body.row()
|
||||
row.prop(self.props, "lip_sync_2d_use_clear_keyframes")
|
||||
row = panel_body.row()
|
||||
row.prop(self.props, "lip_sync_2d_debug_output")
|
||||
row = panel_body.row()
|
||||
row.label(text="Range:")
|
||||
row = panel_body.row()
|
||||
row.prop(self.props, "lip_sync_2d_use_bake_range")
|
||||
@@ -102,3 +104,6 @@ class AnimatorPanelMixin:
|
||||
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
|
||||
|
||||
row = panel_body.row()
|
||||
row.prop(self.props, "lip_sync_2d_bake_channel")
|
||||
|
||||
+73
-4
@@ -154,47 +154,93 @@ def get_lip_sync_type_items(self, context: BpyContext | None):
|
||||
|
||||
|
||||
def poll_pose_assets(self, obj: bpy.types.ID):
|
||||
return bool(obj.asset_data)
|
||||
# Ensure it's an Action
|
||||
if not isinstance(obj, bpy.types.Action):
|
||||
return False
|
||||
|
||||
# Must have asset data (be marked as a pose asset)
|
||||
if not obj.asset_data:
|
||||
return False
|
||||
|
||||
# Allow local, linked, and library override actions
|
||||
# This is necessary because linked library pose assets should still be usable
|
||||
return True
|
||||
|
||||
|
||||
def get_channel_items(self, context: BpyContext | None):
|
||||
items = [("ALL", "All Channels", "Bake audio from all channels")]
|
||||
|
||||
if context is None or context.scene is None or context.scene.sequence_editor is None:
|
||||
return intern_enum_items(items)
|
||||
|
||||
channels = set()
|
||||
for strip in context.scene.sequence_editor.strips_all:
|
||||
if strip.type == "SOUND" and not strip.mute:
|
||||
channels.add(strip.channel)
|
||||
|
||||
sorted_channels = sorted(list(channels))
|
||||
|
||||
for channel in sorted_channels:
|
||||
c_data = context.scene.sequence_editor.channels[channel]
|
||||
channel_name = str(channel)+" - "+c_data.name
|
||||
if c_data.mute:
|
||||
channel_name += " (Muted)"
|
||||
items.append((str(channel), channel_name, f"Bake audio from Channel {channel}"))
|
||||
return intern_enum_items(items)
|
||||
|
||||
|
||||
class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
lip_sync_2d_bake_channel: bpy.props.EnumProperty(
|
||||
name="Channel",
|
||||
description="Select specific channel to bake audio from",
|
||||
items=get_channel_items
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_initialized: bpy.props.BoolProperty(
|
||||
name="Initilize Lip Sync",
|
||||
description="Initilize Lip Sync on selection",
|
||||
default=False,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet: bpy.props.PointerProperty(
|
||||
name="Sprite Sheet",
|
||||
description="The name of the addon to reload",
|
||||
type=bpy.types.Image,
|
||||
update=update_sprite_sheet,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_main_material: bpy.props.PointerProperty(
|
||||
name="Main Material",
|
||||
description="Material containing Sprite sheet",
|
||||
type=bpy.types.Material,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_columns: bpy.props.IntProperty(
|
||||
name="Columns", description="Total of columns in sprite sheet", default=1
|
||||
name="Columns", description="Total of columns in sprite sheet", default=1,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_rows: bpy.props.IntProperty(
|
||||
name="Rows",
|
||||
description="Total of rows in sprite sheet",
|
||||
update=update_sprite_sheet_rows,
|
||||
default=1,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_sprite_scale: bpy.props.FloatProperty(
|
||||
name="Sprite",
|
||||
description="Adjust sprite scale so it fits in mouth area",
|
||||
default=1,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_main_scale: bpy.props.FloatProperty(
|
||||
name="Lips", description="Adjust Lips scale", default=1
|
||||
name="Lips", description="Adjust Lips scale", default=1,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_index: bpy.props.IntProperty(
|
||||
name="Sprite Index",
|
||||
description="Sprite Index. Start at 0, from Bottom Left to Top Right",
|
||||
default=1,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
lip_sync_2d_sprite_sheet_format: bpy.props.EnumProperty(
|
||||
name="Sprite sheet format",
|
||||
@@ -215,6 +261,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
],
|
||||
update=update_sprite_sheet_format,
|
||||
default=3,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_lips_type: bpy.props.EnumProperty(
|
||||
@@ -223,6 +270,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
items=get_lip_sync_type_items,
|
||||
update=update_sprite_sheet_format,
|
||||
default=0,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_in_between_threshold: bpy.props.FloatProperty(
|
||||
@@ -231,6 +279,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
default=0.0417,
|
||||
subtype="TIME",
|
||||
unit="TIME_ABSOLUTE",
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_sil_threshold: bpy.props.FloatProperty(
|
||||
@@ -239,6 +288,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
default=0.22,
|
||||
subtype="TIME",
|
||||
unit="TIME_ABSOLUTE",
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_sps_in_between_threshold: bpy.props.FloatProperty(
|
||||
@@ -247,6 +297,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
default=0.0417,
|
||||
subtype="TIME",
|
||||
unit="TIME_ABSOLUTE",
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_sps_sil_threshold: bpy.props.FloatProperty(
|
||||
@@ -255,6 +306,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
default=0.22,
|
||||
subtype="TIME",
|
||||
unit="TIME_ABSOLUTE",
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_close_motion_duration: bpy.props.FloatProperty(
|
||||
@@ -263,30 +315,41 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
default=0.2,
|
||||
subtype="TIME",
|
||||
unit="TIME_ABSOLUTE",
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_remove_animation_data: bpy.props.BoolProperty(
|
||||
name="Remove Animation",
|
||||
description="Also remove action, action slot and keyframes",
|
||||
default=True,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_remove_cgp_node_group: bpy.props.BoolProperty(
|
||||
name="Remove Nodes",
|
||||
description="Also remove node groups from Object's Materials",
|
||||
default=True,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_use_clear_keyframes: bpy.props.BoolProperty(
|
||||
name="Clear Keyframes",
|
||||
description="Clear Keyframes before Bake",
|
||||
default=True,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_use_bake_range: bpy.props.BoolProperty(
|
||||
name="Use Range",
|
||||
description="Only bake between specified range",
|
||||
default=False,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_debug_output: bpy.props.BoolProperty(
|
||||
name="Debug Output",
|
||||
description="Output phoneme and viseme data to a text block",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_bake_start: bpy.props.IntProperty(
|
||||
@@ -296,6 +359,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
min=0,
|
||||
set=set_bake_start,
|
||||
get=get_bake_start,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_bake_end: bpy.props.IntProperty(
|
||||
@@ -305,6 +369,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
min=0,
|
||||
set=set_bake_end,
|
||||
get=get_bake_end,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_rig_type_basic: bpy.props.BoolProperty(
|
||||
@@ -315,6 +380,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
),
|
||||
default=True,
|
||||
update=update_rig_type_basic,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_rig_type_advanced: bpy.props.BoolProperty(
|
||||
@@ -325,6 +391,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
"Only use this if Basic Rig is not working."
|
||||
),
|
||||
update=update_rig_type_advanced,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
lip_sync_2d_prioritize_accuracy: bpy.props.BoolProperty(
|
||||
@@ -335,6 +402,7 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
"from being skipped when they occur in rapid succession."
|
||||
),
|
||||
default=False,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
@classmethod
|
||||
@@ -373,5 +441,6 @@ class LIPSYNC2D_PG_CustomProperties(bpy.types.PropertyGroup):
|
||||
name=f"Viseme {name}",
|
||||
description=desc,
|
||||
poll=poll_pose_assets,
|
||||
),
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
), # type: ignore
|
||||
)
|
||||
|
||||
@@ -38,7 +38,10 @@ def register():
|
||||
bpy.utils.register_class(LIPSYNC2D_PT_Edit)
|
||||
bpy.utils.register_class(LIPSYNC2D_OT_RemoveAnimations)
|
||||
bpy.utils.register_class(LIPSYNC2D_OT_refresh_pose_assets)
|
||||
bpy.types.Object.lipsync2d_props = bpy.props.PointerProperty(type=LIPSYNC2D_PG_CustomProperties) # type: ignore
|
||||
bpy.types.Object.lipsync2d_props = bpy.props.PointerProperty(
|
||||
type=LIPSYNC2D_PG_CustomProperties,
|
||||
override={'LIBRARY_OVERRIDABLE'}
|
||||
) # type: ignore
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
schema_version = "1.0.0"
|
||||
id = "iocgpoly_lip_sync"
|
||||
version = "2.3.2"
|
||||
version = "1.0.6"
|
||||
name = "Lip Sync"
|
||||
tagline = "Automatic lip sync for your Blender models"
|
||||
maintainer = "Charley 3D <charley@cgpoly.fr>"
|
||||
@@ -30,11 +30,3 @@ paths_exclude_pattern = [
|
||||
"/scripts/",
|
||||
".releaserc"
|
||||
]
|
||||
|
||||
|
||||
# BEGIN GENERATED CONTENT.
|
||||
# This must not be included in source manifests.
|
||||
[build.generated]
|
||||
platforms = ["windows-x64"]
|
||||
wheels = ["./wheels/attrs-25.3.0-py3-none-any.whl", "./wheels/babel-2.17.0-py3-none-any.whl", "./wheels/cffi-1.17.1-cp311-cp311-win_amd64.whl", "./wheels/csvw-3.5.1-py2.py3-none-any.whl", "./wheels/dlinfo-2.0.0-py3-none-any.whl", "./wheels/isodate-0.7.2-py3-none-any.whl", "./wheels/joblib-1.4.2-py3-none-any.whl", "./wheels/jsonschema-4.23.0-py3-none-any.whl", "./wheels/jsonschema_specifications-2024.10.1-py3-none-any.whl", "./wheels/language_tags-1.2.0-py3-none-any.whl", "./wheels/phonemizer-3.3.0-py3-none-any.whl", "./wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", "./wheels/rdflib-7.1.4-py3-none-any.whl", "./wheels/referencing-0.36.2-py3-none-any.whl", "./wheels/regex-2024.11.6-cp311-cp311-win_amd64.whl", "./wheels/rfc3986-1.5.0-py2.py3-none-any.whl", "./wheels/rpds_py-0.24.0-cp311-cp311-win_amd64.whl", "./wheels/segments-2.3.0-py2.py3-none-any.whl", "./wheels/six-1.17.0-py2.py3-none-any.whl", "./wheels/tqdm-4.67.1-py3-none-any.whl", "./wheels/typing_extensions-4.13.2-py3-none-any.whl", "./wheels/uritemplate-4.1.1-py2.py3-none-any.whl", "./wheels/vosk-0.3.41-py3-none-win_amd64.whl"]
|
||||
# END GENERATED CONTENT.
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from collections import defaultdict
|
||||
from glob import glob
|
||||
|
||||
import tomlkit
|
||||
|
||||
|
||||
def update_wheels():
|
||||
folder = "./wheels"
|
||||
files = os.listdir(folder)
|
||||
toml_path = "blender_manifest.toml"
|
||||
|
||||
wheel_paths = glob("./wheels/**/*.whl", recursive=True)
|
||||
|
||||
|
||||
print(wheel_paths)
|
||||
clean = [path.replace("\\", "/") for path in wheel_paths]
|
||||
|
||||
# Load the TOML file
|
||||
with open(toml_path, "r", encoding="utf-8") as f:
|
||||
toml_data = tomlkit.load(f)
|
||||
# Update the "wheels" entry
|
||||
toml_data["wheels"] = clean
|
||||
|
||||
|
||||
# Write back to the TOML file, preserving formatting and comments
|
||||
with open(toml_path, "w", encoding="utf-8") as f:
|
||||
f.write(tomlkit.dumps(toml_data))
|
||||
|
||||
def build_addon():
|
||||
current_dir = os.getcwd()
|
||||
exclude_patterns = [
|
||||
'.venv',
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'.idea',
|
||||
'__pycache__',
|
||||
'dist',
|
||||
'dev_tools.py'
|
||||
]
|
||||
|
||||
dist_dir = os.path.join(current_dir, "dist")
|
||||
|
||||
if not os.path.exists(dist_dir):
|
||||
os.makedirs(dist_dir)
|
||||
|
||||
with open("blender_manifest.toml", "r") as f:
|
||||
parsed = tomlkit.parse(f.read())
|
||||
version = parsed["version"]
|
||||
addon_id = parsed["id"]
|
||||
|
||||
with zipfile.ZipFile(f"{current_dir}/dist/{addon_id}_{version}.zip", "w") as zip_file:
|
||||
for root, dirs, files in os.walk(f"{current_dir}"):
|
||||
dirs[:] = [d for d in dirs if os.path.relpath(os.path.join(root,d), current_dir) not in exclude_patterns]
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.relpath(os.path.join(root, file), current_dir)
|
||||
# Skip files based on exclude patterns
|
||||
if any(file_path.startswith(pattern) or file_path in exclude_patterns for pattern in exclude_patterns):
|
||||
continue
|
||||
|
||||
# Add the file to the zip archive
|
||||
zip_file.write(os.path.join(root, file), file_path)
|
||||
|
||||
print(f"Build complete. Addon file saved to {dist_dir}")
|
||||
|
||||
|
||||
def handle_duplicate_wheels(directory: str):
|
||||
"""
|
||||
Find and handle duplicate .whl files in the given directory and its subdirectories.
|
||||
Keeps files in ./wheels/common and removes duplicates. If a file is not in ./wheels/common,
|
||||
moves one to ./wheels/common and deletes the others.
|
||||
|
||||
:param directory: str: The path to the directory to search for .whl files.
|
||||
:return: None
|
||||
"""
|
||||
# Path to the common directory
|
||||
common_path = os.path.join(directory, "common")
|
||||
# Create the common directory if it doesn't exist
|
||||
os.makedirs(common_path, exist_ok=True)
|
||||
|
||||
# Dictionary to store .whl filenames and their paths
|
||||
files_dict = defaultdict(list)
|
||||
|
||||
# Traverse the directory and its subdirectories
|
||||
for root, _, files in os.walk(directory):
|
||||
for file in files:
|
||||
print(f"Processing file: {file}")
|
||||
|
||||
# Check if the file is a .whl file
|
||||
if file.endswith(".whl"):
|
||||
# Store the file and its full path in the dictionary
|
||||
full_path = os.path.join(root, file)
|
||||
files_dict[file].append(full_path)
|
||||
|
||||
# Handle duplicates (files with more than one associated path)
|
||||
for file, paths in files_dict.items():
|
||||
if len(paths) > 1: # Check if there are duplicates
|
||||
print(f"\nDuplicate found for: {file}")
|
||||
print("Locations:")
|
||||
for path in paths:
|
||||
print(f" - {path}")
|
||||
|
||||
# Check if the file already exists in the common directory
|
||||
common_file_path = os.path.join(common_path, file)
|
||||
if os.path.exists(common_file_path):
|
||||
# Remove all duplicates, as the file already exists in common
|
||||
print(f"File already in common directory: {common_file_path}")
|
||||
for path in paths:
|
||||
if path != common_file_path:
|
||||
print(f"Removing duplicate: {path}")
|
||||
os.remove(path)
|
||||
else:
|
||||
# Move one file to common and remove the others
|
||||
print(f"Moving one copy to common directory: {common_file_path}")
|
||||
shutil.move(paths[0], common_file_path) # Move the first file to common
|
||||
for path in paths[1:]:
|
||||
print(f"Removing duplicate: {path}")
|
||||
os.remove(path)
|
||||
else:
|
||||
print(f"No duplicates for: {file}")
|
||||
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
|
||||
def md5_for_folder(folder_path: str):
|
||||
"""
|
||||
Compute the MD5 hash of the contents of a folder including file names and contents.
|
||||
|
||||
:param folder_path: str: Path to the folder.
|
||||
:return: str: The MD5 hash of the folder.
|
||||
"""
|
||||
md5_hash = hashlib.md5()
|
||||
|
||||
# Walk through the directory
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
# Sort directories and files to ensure consistent order (important for consistent hash values)
|
||||
for names in sorted(dirs + files):
|
||||
# Update hash with file/folder name
|
||||
path = os.path.join(root, names)
|
||||
md5_hash.update(names.encode('utf-8'))
|
||||
|
||||
# If it's a file, include its content in the hash
|
||||
if os.path.isfile(path):
|
||||
with open(path, 'rb') as f:
|
||||
while chunk := f.read(8192): # Read file in chunks
|
||||
md5_hash.update(chunk)
|
||||
|
||||
return md5_hash.hexdigest()
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_wheels()
|
||||
# Example usage
|
||||
# folder = "./Assets/Archives/darwin/espeak-ng-darwin/espeak-ng-data"
|
||||
# print(f"MD5 Hash for the folder '{folder}': {md5_for_folder(folder)}")
|
||||
|
||||
# folder = "./Assets/Archives/linux/espeak-ng-data/lang"
|
||||
# print(f"MD5 Hash for the folder '{folder}': {md5_for_folder(folder)}")
|
||||
|
||||
# folder = "./Assets/Archives/windows/espeak-ng-data/lang"
|
||||
# print(f"MD5 Hash for the folder '{folder}': {md5_for_folder(folder)}")
|
||||
# update_wheels()
|
||||
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from tomlkit import parse, dumps # type: ignore
|
||||
|
||||
version = sys.argv[1]
|
||||
toml_path = "blender_manifest.toml"
|
||||
|
||||
with open(toml_path, "r", encoding="utf-8") as f:
|
||||
doc = parse(f.read())
|
||||
|
||||
doc["version"] = version
|
||||
|
||||
with open(toml_path, "w", encoding="utf-8") as f:
|
||||
f.write(dumps(doc))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user