Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f6b33c2a1 | |||
| f4593f6846 | |||
| 4106ff2c94 | |||
| 2ffb8fb1f1 | |||
| 78200ab935 | |||
| 0881ed3831 | |||
| 3ce1bc2ab3 | |||
| 63460cf5a2 | |||
| cde0457ee1 | |||
| 4ff17a66fe | |||
| 8222f8b24c | |||
| 37b92ded6d | |||
| fb470f19da | |||
| 843147db69 | |||
| fe2b0d50cb | |||
| c4dca2455d | |||
| 659f3eb91e | |||
| ff19a895dc | |||
| e6e5a98e58 | |||
| 3fe00da569 | |||
| 108f9d3bc8 | |||
| 1847628dc8 | |||
| 25a43afdbc | |||
| baaf4049f6 | |||
| 299800e5c2 | |||
| f6197ccbbf |
@@ -6,6 +6,9 @@ We are aware the wiki is down and are working on a new one, please don't report
|
|||||||
|
|
||||||
Avatar Toolkit is a modern, Blender addon designed to streamline the process of preparing 3D avatars for virtual platforms including VRChat, ChilloutVR, Resonite, and other similar applications.
|
Avatar Toolkit is a modern, Blender addon designed to streamline the process of preparing 3D avatars for virtual platforms including VRChat, ChilloutVR, Resonite, and other similar applications.
|
||||||
|
|
||||||
|
# No longer maintained by neoneko, neoneko has ceased all operations.
|
||||||
|
# This fork is maintained by snipeslow to barely function as is. Good luck if you find this.
|
||||||
|
|
||||||
## What is Avatar Toolkit?
|
## What is Avatar Toolkit?
|
||||||
Avatar Toolkit simplifies the workflow for avatar creation and optimization by providing an all-in-one solution that:
|
Avatar Toolkit simplifies the workflow for avatar creation and optimization by providing an all-in-one solution that:
|
||||||
- Automates complex optimization processes like mesh joining and vertex merging.
|
- Automates complex optimization processes like mesh joining and vertex merging.
|
||||||
@@ -33,8 +36,8 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
1) Blender Version
|
1) Blender Version
|
||||||
- Blender 4.5 or newer is required
|
- Blender 5.0 or newer is required
|
||||||
- Blender 4.5 is the current recommended version
|
- Blender 5.0 is the current recommended version
|
||||||
|
|
||||||
2) Python Requirements
|
2) Python Requirements
|
||||||
- If using a custom Python installation with Blender, ensure NumPy is installed
|
- If using a custom Python installation with Blender, ensure NumPy is installed
|
||||||
@@ -42,7 +45,7 @@ See everything Avatar Toolkit has ot offer [here](https://avatartoolkit.xyz/lega
|
|||||||
|
|
||||||
3) Recommended Setup
|
3) Recommended Setup
|
||||||
- Download Blender directly from https://blender.org
|
- Download Blender directly from https://blender.org
|
||||||
- Use Blender 4.5 for the best experience
|
- Use Blender 5.0 for the best experience
|
||||||
|
|
||||||
#### Unfortunately, due to the increased number of people complaining to me (yes, we get DMs about this) that AT or CATS is broken when it's not, we are going to have to be a bit more strict about which Blender releases we will provide support for.
|
#### Unfortunately, due to the increased number of people complaining to me (yes, we get DMs about this) that AT or CATS is broken when it's not, we are going to have to be a bit more strict about which Blender releases we will provide support for.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
schema_version = "1.0.0"
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
id = "avatar_toolkit"
|
id = "avatar_toolkit"
|
||||||
version = "0.5.1"
|
version = "0.5.5"
|
||||||
name = "Avatar Toolkit"
|
name = "Avatar Toolkit"
|
||||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||||
maintainer = "Team NekoNeo"
|
maintainer = "Team NekoNeo"
|
||||||
@@ -16,10 +16,8 @@ license = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
wheels = [
|
wheels = [
|
||||||
"./wheels/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl",
|
"./wheels/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
||||||
"./wheels/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl",
|
"./wheels/lz4-4.4.5-cp313-cp313-win_amd64.whl"
|
||||||
"./wheels/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",
|
|
||||||
"./wheels/lz4-4.4.5-cp311-cp311-win_amd64.whl"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[permissions]
|
[permissions]
|
||||||
|
|||||||
+118
-9
@@ -92,23 +92,132 @@ class ProgressTracker:
|
|||||||
|
|
||||||
def get_active_armature(context: Context) -> Optional[Object]:
|
def get_active_armature(context: Context) -> Optional[Object]:
|
||||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||||
armature_name = str(context.scene.avatar_toolkit.active_armature)
|
try:
|
||||||
if armature_name and armature_name != 'NONE':
|
# Get the safe identifier from the enum property
|
||||||
return bpy.data.objects.get(armature_name)
|
armature_id = context.scene.avatar_toolkit.active_armature
|
||||||
|
|
||||||
|
if not armature_id or armature_id == 'NONE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The identifier format is "ARM_{pointer_value}"
|
||||||
|
if armature_id.startswith('ARM_'):
|
||||||
|
try:
|
||||||
|
pointer_str = armature_id[4:]
|
||||||
|
pointer_value = int(pointer_str)
|
||||||
|
|
||||||
|
# Find the armature with this pointer value
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE' and obj.as_pointer() == pointer_value:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
logger.warning(f"Armature with pointer {pointer_value} not found")
|
||||||
|
except (ValueError, AttributeError) as e:
|
||||||
|
logger.error(f"Failed to parse armature identifier: {e}")
|
||||||
|
|
||||||
|
# Fallback for old-style identifiers (direct name)
|
||||||
|
# This handles backward compatibility
|
||||||
|
return bpy.data.objects.get(armature_id)
|
||||||
|
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError) as e:
|
||||||
|
# Handle encoding issues as a last resort
|
||||||
|
logger.warning(f"Encoding issue with active_armature property: {e}")
|
||||||
|
|
||||||
|
# Final fallback: return active object if it's an armature, or first armature found
|
||||||
|
if context.view_layer.objects.active and context.view_layer.objects.active.type == 'ARMATURE':
|
||||||
|
return context.view_layer.objects.active
|
||||||
|
|
||||||
|
for obj in context.scene.objects:
|
||||||
|
if obj.type == 'ARMATURE':
|
||||||
|
logger.info(f"Falling back to first armature found: {obj.name}")
|
||||||
|
return obj
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def set_active_armature(context: Context, armature: Object) -> None:
|
def set_active_armature(context: Context, armature: Object) -> None:
|
||||||
"""Set the active armature for Avatar Toolkit operations"""
|
"""Set the active armature for Avatar Toolkit operations using safe identifier"""
|
||||||
context.scene.avatar_toolkit.active_armature = armature
|
if armature and armature.type == 'ARMATURE':
|
||||||
|
# Use the same safe identifier format as get_armature_list
|
||||||
|
safe_id = f"ARM_{armature.as_pointer()}"
|
||||||
|
context.scene.avatar_toolkit.active_armature = safe_id
|
||||||
|
else:
|
||||||
|
context.scene.avatar_toolkit.active_armature = 'NONE'
|
||||||
|
|
||||||
|
def get_mesh_from_identifier(mesh_id: str) -> Optional[Object]:
|
||||||
|
"""Get mesh object from safe identifier
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mesh_id: Safe identifier in format "MESH_{pointer}" or direct object name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mesh object or None if not found
|
||||||
|
"""
|
||||||
|
if not mesh_id or mesh_id == 'NONE':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle new-style identifiers (MESH_{pointer})
|
||||||
|
if mesh_id.startswith('MESH_'):
|
||||||
|
try:
|
||||||
|
pointer_str = mesh_id[5:] # Remove "MESH_" prefix
|
||||||
|
target_pointer = int(pointer_str)
|
||||||
|
|
||||||
|
# Search for object with matching pointer
|
||||||
|
for obj in bpy.data.objects:
|
||||||
|
if obj.type == 'MESH' and obj.as_pointer() == target_pointer:
|
||||||
|
return obj
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback for old-style identifiers (direct name)
|
||||||
|
return bpy.data.objects.get(mesh_id)
|
||||||
|
|
||||||
|
def clear_enum_caches() -> None:
|
||||||
|
"""Clear all enum property caches to force refresh of dropdown lists"""
|
||||||
|
if hasattr(get_armature_list, '_cache_key'):
|
||||||
|
delattr(get_armature_list, '_cache_key')
|
||||||
|
if hasattr(get_armature_list, '_cached_items'):
|
||||||
|
delattr(get_armature_list, '_cached_items')
|
||||||
|
|
||||||
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
|
def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = None) -> List[Tuple[str, str, str]]:
|
||||||
"""Get list of all armature objects in the scene"""
|
"""Get list of all armature objects in the scene
|
||||||
|
|
||||||
|
Returns tuples of (identifier, display_name, description) where:
|
||||||
|
- identifier: ASCII-safe unique ID (uses object's memory address)
|
||||||
|
- display_name: The actual object name (can contain Japanese characters)
|
||||||
|
- description: Empty string
|
||||||
|
|
||||||
|
Uses caching to prevent encoding issues with Blender's EnumProperty system
|
||||||
|
"""
|
||||||
if context is None:
|
if context is None:
|
||||||
context = bpy.context
|
context = bpy.context
|
||||||
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
|
||||||
|
# Create a cache key based on armature objects in scene
|
||||||
|
armature_objects = [obj for obj in context.scene.objects if obj.type == 'ARMATURE']
|
||||||
|
cache_key = tuple((obj.name, obj.as_pointer()) for obj in armature_objects)
|
||||||
|
|
||||||
|
# Check if we have a cached result
|
||||||
|
if hasattr(get_armature_list, '_cache_key') and get_armature_list._cache_key == cache_key:
|
||||||
|
if hasattr(get_armature_list, '_cached_items'):
|
||||||
|
return get_armature_list._cached_items
|
||||||
|
|
||||||
|
# Build the list
|
||||||
|
armatures = []
|
||||||
|
for obj in armature_objects:
|
||||||
|
# Create a safe ASCII identifier using the object pointer
|
||||||
|
safe_id = f"ARM_{obj.as_pointer()}"
|
||||||
|
# Use the name directly - Blender should handle Unicode in display names
|
||||||
|
display_name = obj.name
|
||||||
|
armatures.append((safe_id, display_name, ""))
|
||||||
|
|
||||||
if not armatures:
|
if not armatures:
|
||||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
result = [('NONE', t("Armature.validation.no_armature"), '')]
|
||||||
return armatures
|
else:
|
||||||
|
result = armatures
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
get_armature_list._cache_key = cache_key
|
||||||
|
get_armature_list._cached_items = result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def auto_select_single_armature(context: Context) -> None:
|
def auto_select_single_armature(context: Context) -> None:
|
||||||
"""Automatically select armature if only one exists in scene"""
|
"""Automatically select armature if only one exists in scene"""
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ def configure_logging(enabled: bool = False, level: str = "WARNING") -> None:
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
def error_with_traceback(msg, *args, **kwargs):
|
def error_with_traceback(msg, *args, **kwargs):
|
||||||
if isinstance(kwargs.get('exception', None), Exception):
|
# If exc_info is True, include traceback in the message
|
||||||
|
if kwargs.get('exc_info', False):
|
||||||
full_msg = f"{msg}\n{traceback.format_exc()}"
|
full_msg = f"{msg}\n{traceback.format_exc()}"
|
||||||
_original_error(full_msg, *args, **{**kwargs, 'exc_info': False})
|
_original_error(full_msg, *args, **{k: v for k, v in kwargs.items() if k != 'exc_info'})
|
||||||
else:
|
else:
|
||||||
_original_error(msg, *args, **kwargs)
|
_original_error(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
+55
-17
@@ -67,10 +67,42 @@ def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
|||||||
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
||||||
|
|
||||||
def get_mesh_objects(self, context):
|
def get_mesh_objects(self, context):
|
||||||
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
"""Get list of all mesh objects with ASCII-safe identifiers
|
||||||
|
|
||||||
|
Returns tuples of (identifier, display_name, description) where:
|
||||||
|
- identifier: ASCII-safe unique ID (uses object's memory address)
|
||||||
|
- display_name: The actual object name (can contain Japanese/non-ASCII characters)
|
||||||
|
- description: Empty string
|
||||||
|
|
||||||
|
Uses caching to prevent encoding issues with Blender's EnumProperty system
|
||||||
|
"""
|
||||||
|
# Create a cache key based on mesh objects
|
||||||
|
mesh_objects = [obj for obj in bpy.data.objects if obj.type == 'MESH']
|
||||||
|
cache_key = tuple((obj.name, obj.as_pointer()) for obj in mesh_objects)
|
||||||
|
|
||||||
|
# Check if we have a cached result
|
||||||
|
if hasattr(get_mesh_objects, '_cache_key') and get_mesh_objects._cache_key == cache_key:
|
||||||
|
if hasattr(get_mesh_objects, '_cached_items'):
|
||||||
|
return get_mesh_objects._cached_items
|
||||||
|
|
||||||
|
# Build the list
|
||||||
|
meshes = []
|
||||||
|
for obj in mesh_objects:
|
||||||
|
safe_id = f"MESH_{obj.as_pointer()}"
|
||||||
|
# Use the name directly - Blender should handle Unicode in display names
|
||||||
|
display_name = obj.name
|
||||||
|
meshes.append((safe_id, display_name, ""))
|
||||||
|
|
||||||
if not meshes:
|
if not meshes:
|
||||||
return [('NONE', t("Visemes.no_meshes"), '')]
|
result = [('NONE', t("Visemes.no_meshes"), '')]
|
||||||
return meshes
|
else:
|
||||||
|
result = meshes
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
get_mesh_objects._cache_key = cache_key
|
||||||
|
get_mesh_objects._cached_items = result
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def auto_populate_merge_armatures(context: Context) -> None:
|
def auto_populate_merge_armatures(context: Context) -> None:
|
||||||
"""Auto-populate merge armature fields when there are 2+ armatures"""
|
"""Auto-populate merge armature fields when there are 2+ armatures"""
|
||||||
@@ -812,21 +844,27 @@ def update_translation_mode(self: PropertyGroup, context: Context) -> None:
|
|||||||
|
|
||||||
def update_active_armature(self: PropertyGroup, context: Context) -> None:
|
def update_active_armature(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Update the active armature when selection changes"""
|
"""Update the active armature when selection changes"""
|
||||||
if self.active_armature:
|
if self.active_armature and self.active_armature != 'NONE':
|
||||||
logger.info(f"Active armature set to: {self.active_armature}")
|
# Get the actual armature object from the identifier
|
||||||
# Deselect all objects first
|
armature = get_active_armature(context)
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
# Select and make active the chosen armature
|
|
||||||
self.active_armature.select_set(True)
|
|
||||||
context.view_layer.objects.active = self.active_armature
|
|
||||||
logger.info(f"Selected and activated armature: {self.active_armature.name}")
|
|
||||||
|
|
||||||
# Clear armature caches when armature changes to ensure fresh validation
|
if armature:
|
||||||
try:
|
logger.info(f"Active armature set to: {armature.name}")
|
||||||
from ..ui.quick_access_panel import clear_armature_caches
|
# Deselect all objects first
|
||||||
clear_armature_caches()
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
except ImportError:
|
# Select and make active the chosen armature
|
||||||
pass # UI module might not be loaded yet
|
armature.select_set(True)
|
||||||
|
context.view_layer.objects.active = armature
|
||||||
|
logger.info(f"Selected and activated armature: {armature.name}")
|
||||||
|
|
||||||
|
# Clear armature caches when armature changes to ensure fresh validation
|
||||||
|
try:
|
||||||
|
from ..ui.quick_access_panel import clear_armature_caches
|
||||||
|
clear_armature_caches()
|
||||||
|
except ImportError:
|
||||||
|
pass # UI module might not be loaded yet
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to get armature object from identifier")
|
||||||
else:
|
else:
|
||||||
logger.info("No armature selected")
|
logger.info("No armature selected")
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,14 @@ class TranslationCache:
|
|||||||
"""Load cache from file"""
|
"""Load cache from file"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self._cache_file):
|
if os.path.exists(self._cache_file):
|
||||||
with open(self._cache_file, 'r', encoding='utf-8') as f:
|
# Try UTF-8 first, fallback to other encodings
|
||||||
self._cache = json.load(f)
|
try:
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# Try with UTF-8 error handling
|
||||||
|
with open(self._cache_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
self._cache = json.load(f)
|
||||||
logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
|
logger.debug(f"Loaded translation cache with {len(self._cache)} entries")
|
||||||
else:
|
else:
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
@@ -147,6 +153,15 @@ class AvatarToolkitTranslationManager:
|
|||||||
def translate_single(self, name: str, category: str = "auto",
|
def translate_single(self, name: str, category: str = "auto",
|
||||||
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
|
source_lang: str = "ja", target_lang: str = "en") -> TranslationResult:
|
||||||
"""Translate a single name with comprehensive fallback logic"""
|
"""Translate a single name with comprehensive fallback logic"""
|
||||||
|
# Import safe_decode_text from translation_service
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
name = safe_decode_text(name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode name: {e}")
|
||||||
|
|
||||||
if not name or not name.strip():
|
if not name or not name.strip():
|
||||||
return TranslationResult(name, name, "skipped")
|
return TranslationResult(name, name, "skipped")
|
||||||
|
|
||||||
@@ -300,6 +315,8 @@ class AvatarToolkitTranslationManager:
|
|||||||
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
|
def _process_category_batch_optimized(self, category_jobs: List[TranslationJob],
|
||||||
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
|
completed: int, total_jobs: int, start_time: float) -> Optional[List[TranslationResult]]:
|
||||||
"""Process a batch of jobs from the same category using optimized API batch translation"""
|
"""Process a batch of jobs from the same category using optimized API batch translation"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not category_jobs:
|
if not category_jobs:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -315,6 +332,14 @@ class AvatarToolkitTranslationManager:
|
|||||||
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
|
results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Ensure name is properly encoded
|
||||||
|
try:
|
||||||
|
original_name = safe_decode_text(job.name.strip())
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode job name: {e}")
|
||||||
|
original_name = job.name.strip()
|
||||||
|
continue
|
||||||
|
|
||||||
original_name = job.name.strip()
|
original_name = job.name.strip()
|
||||||
|
|
||||||
# Check cache first
|
# Check cache first
|
||||||
@@ -426,13 +451,21 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all bone names in an armature"""
|
"""Translate all bone names in an armature"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not armature or armature.type != 'ARMATURE':
|
if not armature or armature.type != 'ARMATURE':
|
||||||
return []
|
return []
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for bone in armature.data.bones:
|
for bone in armature.data.bones:
|
||||||
|
try:
|
||||||
|
bone_name = safe_decode_text(bone.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode bone name, using as-is: {e}")
|
||||||
|
bone_name = bone.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=bone.name,
|
name=bone_name,
|
||||||
category="bones",
|
category="bones",
|
||||||
object_ref=bone,
|
object_ref=bone,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -442,13 +475,21 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all shape key names in a mesh object"""
|
"""Translate all shape key names in a mesh object"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
|
if not mesh_obj or mesh_obj.type != 'MESH' or not mesh_obj.data.shape_keys:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for shape_key in mesh_obj.data.shape_keys.key_blocks:
|
for shape_key in mesh_obj.data.shape_keys.key_blocks:
|
||||||
|
try:
|
||||||
|
sk_name = safe_decode_text(shape_key.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode shape key name, using as-is: {e}")
|
||||||
|
sk_name = shape_key.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=shape_key.name,
|
name=sk_name,
|
||||||
category="shapekeys",
|
category="shapekeys",
|
||||||
object_ref=shape_key,
|
object_ref=shape_key,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -458,6 +499,8 @@ class AvatarToolkitTranslationManager:
|
|||||||
|
|
||||||
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
|
def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all material names in the scene"""
|
"""Translate all material names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
processed_materials: Set[str] = set()
|
processed_materials: Set[str] = set()
|
||||||
|
|
||||||
@@ -465,8 +508,14 @@ class AvatarToolkitTranslationManager:
|
|||||||
if obj.type == 'MESH' and obj.data.materials:
|
if obj.type == 'MESH' and obj.data.materials:
|
||||||
for material in obj.data.materials:
|
for material in obj.data.materials:
|
||||||
if material and material.name not in processed_materials:
|
if material and material.name not in processed_materials:
|
||||||
|
try:
|
||||||
|
mat_name = safe_decode_text(material.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode material name, using as-is: {e}")
|
||||||
|
mat_name = material.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=material.name,
|
name=mat_name,
|
||||||
category="materials",
|
category="materials",
|
||||||
object_ref=material,
|
object_ref=material,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
@@ -478,14 +527,22 @@ class AvatarToolkitTranslationManager:
|
|||||||
def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
|
def translate_scene_objects(self, object_types: Optional[Set[str]] = None,
|
||||||
apply_results: bool = True) -> List[TranslationResult]:
|
apply_results: bool = True) -> List[TranslationResult]:
|
||||||
"""Translate all object names in the scene"""
|
"""Translate all object names in the scene"""
|
||||||
|
from .translation_service import safe_decode_text
|
||||||
|
|
||||||
if object_types is None:
|
if object_types is None:
|
||||||
object_types = {'MESH', 'ARMATURE', 'EMPTY'}
|
object_types = {'MESH', 'ARMATURE', 'EMPTY'}
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
for obj in bpy.data.objects:
|
for obj in bpy.data.objects:
|
||||||
if obj.type in object_types:
|
if obj.type in object_types:
|
||||||
|
try:
|
||||||
|
obj_name = safe_decode_text(obj.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to decode object name, using as-is: {e}")
|
||||||
|
obj_name = obj.name
|
||||||
|
|
||||||
jobs.append(TranslationJob(
|
jobs.append(TranslationJob(
|
||||||
name=obj.name,
|
name=obj_name,
|
||||||
category="objects",
|
category="objects",
|
||||||
object_ref=obj,
|
object_ref=obj,
|
||||||
property_name="name"
|
property_name="name"
|
||||||
|
|||||||
@@ -14,6 +14,43 @@ from .logging_setup import logger
|
|||||||
from .addon_preferences import save_preference, get_preference
|
from .addon_preferences import save_preference, get_preference
|
||||||
|
|
||||||
|
|
||||||
|
def safe_decode_text(text: str) -> str:
|
||||||
|
"""Safely decode text that might be in various encodings (UTF-8, Shift-JIS, etc.)"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# If it's already a proper string, return it
|
||||||
|
if isinstance(text, str):
|
||||||
|
try:
|
||||||
|
# Test if it's valid UTF-8
|
||||||
|
text.encode('utf-8')
|
||||||
|
return text
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try common encodings for Japanese text
|
||||||
|
encodings = ['utf-8', 'shift-jis', 'cp932', 'euc-jp', 'iso-2022-jp']
|
||||||
|
|
||||||
|
for encoding in encodings:
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode(encoding)
|
||||||
|
else:
|
||||||
|
# Try to re-encode and decode
|
||||||
|
return text.encode('latin-1', errors='ignore').decode(encoding, errors='ignore')
|
||||||
|
except (UnicodeDecodeError, UnicodeEncodeError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback: replace problematic characters
|
||||||
|
try:
|
||||||
|
if isinstance(text, bytes):
|
||||||
|
return text.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
return str(text).encode('utf-8', errors='replace').decode('utf-8')
|
||||||
|
except:
|
||||||
|
return str(text)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TranslationRequest:
|
class TranslationRequest:
|
||||||
"""Represents a translation request"""
|
"""Represents a translation request"""
|
||||||
@@ -116,6 +153,8 @@ class DeepLService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using DeepL API"""
|
"""Translate text using DeepL API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"DeepL: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -220,6 +259,8 @@ class DeepLService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"DeepL: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
results = [None] * len(texts)
|
results = [None] * len(texts)
|
||||||
@@ -341,6 +382,8 @@ class MyMemoryService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using MyMemory free API"""
|
"""Translate text using MyMemory free API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"MyMemory: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -430,6 +473,8 @@ class MyMemoryService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"MyMemory: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
results = [None] * len(texts)
|
results = [None] * len(texts)
|
||||||
@@ -545,6 +590,8 @@ class LibreTranslateService(TranslationService):
|
|||||||
|
|
||||||
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
def translate_text(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> str:
|
||||||
"""Translate text using LibreTranslate API"""
|
"""Translate text using LibreTranslate API"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
logger.info(f"LibreTranslate: Starting translation of '{text}' from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
@@ -658,6 +705,8 @@ class LibreTranslateService(TranslationService):
|
|||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Ensure all texts are properly encoded
|
||||||
|
texts = [safe_decode_text(text) for text in texts]
|
||||||
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
logger.info(f"LibreTranslate: Starting batch translation of {len(texts)} texts from {source_lang} to {target_lang}")
|
||||||
|
|
||||||
# Check cache and separate cached vs uncached texts
|
# Check cache and separate cached vs uncached texts
|
||||||
@@ -814,6 +863,8 @@ class TranslationServiceManager:
|
|||||||
|
|
||||||
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
|
def translate_with_fallback(self, text: str, source_lang: str = "ja", target_lang: str = "en") -> Tuple[str, str]:
|
||||||
"""Translate text with automatic fallback to other services"""
|
"""Translate text with automatic fallback to other services"""
|
||||||
|
# Ensure text is properly encoded
|
||||||
|
text = safe_decode_text(text)
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return text, "none"
|
return text, "none"
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ from .addon_preferences import get_preference, get_current_version, save_prefere
|
|||||||
from ..ui.main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
from ..ui.main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||||
from typing import Dict, List, Tuple, Optional, Set, Any
|
from typing import Dict, List, Tuple, Optional, Set, Any
|
||||||
|
|
||||||
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
GITHUB_REPO = "snipeslow/Avatar-Toolkit"
|
||||||
|
|
||||||
# Define which version series this installation can update to
|
# Define which version series this installation can update to
|
||||||
# For example: ["0.1"] means only look for 0.1.x updates
|
# For example: ["0.1"] means only look for 0.1.x updates
|
||||||
@@ -158,7 +158,7 @@ def get_github_releases() -> bool:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ssl._create_default_https_context = ssl._create_unverified_context
|
ssl._create_default_https_context = ssl._create_unverified_context
|
||||||
with request.urlopen(f'https://api.github.com/repos/{GITHUB_REPO}/releases') as url:
|
with request.urlopen(f'https://git.snipeslow.dev/api/v1/repos/{GITHUB_REPO}/releases') as url:
|
||||||
data = json.loads(url.read().decode())
|
data = json.loads(url.read().decode())
|
||||||
except error.URLError:
|
except error.URLError:
|
||||||
print('URL ERROR')
|
print('URL ERROR')
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_StopPoseMode(Operator):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.stop", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
||||||
@@ -136,7 +136,7 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
|||||||
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=traceback.format_exc()))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
||||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||||
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
||||||
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
|
||||||
|
|||||||
@@ -137,15 +137,17 @@ class AvatarToolkit_OT_PreviewVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
# Validate mesh
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh = bpy.data.objects.get(props.viseme_mesh)
|
mesh = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
if props.viseme_preview_mode:
|
if props.viseme_preview_mode:
|
||||||
VisemePreview.end_preview(mesh)
|
VisemePreview.end_preview(mesh)
|
||||||
@@ -191,15 +193,17 @@ class AvatarToolkit_OT_CreateVisemes(Operator):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get mesh from UI selection
|
# Get mesh from UI selection
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
# Validate mesh
|
# Validate mesh
|
||||||
return mesh_obj and mesh_obj.type == 'MESH'
|
return mesh_obj and mesh_obj.type == 'MESH'
|
||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
|
from ..core.common import get_mesh_from_identifier
|
||||||
props = context.scene.avatar_toolkit
|
props = context.scene.avatar_toolkit
|
||||||
mesh = bpy.data.objects.get(props.viseme_mesh) # Changed from context.active_object
|
mesh = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
|
|
||||||
if not mesh or not mesh.data.shape_keys:
|
if not mesh or not mesh.data.shape_keys:
|
||||||
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.1)",
|
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.5.2)",
|
||||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||||
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.5.1)",
|
"AvatarToolkit.label": "アバターツールキット (アルファ 0.5.2)",
|
||||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"authors": ["Avatar Toolkit Team"],
|
"authors": ["Avatar Toolkit Team"],
|
||||||
"messages": {
|
"messages": {
|
||||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.5.1)",
|
"AvatarToolkit.label": "아바타 툴킷 (알파 0.5.2)",
|
||||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||||
|
|||||||
+3
-2
@@ -34,8 +34,9 @@ class AvatarToolKit_PT_VisemesPanel(Panel):
|
|||||||
else:
|
else:
|
||||||
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
col.label(text=t("Visemes.no_armature"), icon='ERROR')
|
||||||
|
|
||||||
# Get selected mesh
|
# Get selected mesh using safe identifier
|
||||||
mesh_obj = bpy.data.objects.get(props.viseme_mesh)
|
from ..core.common import get_mesh_from_identifier
|
||||||
|
mesh_obj = get_mesh_from_identifier(props.viseme_mesh)
|
||||||
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
if not mesh_obj or not mesh_obj.data or not mesh_obj.data.shape_keys:
|
||||||
layout.label(text=t("Visemes.no_shapekeys"))
|
layout.label(text=t("Visemes.no_shapekeys"))
|
||||||
return
|
return
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user