Compare commits

..

26 Commits

Author SHA1 Message Date
snipeslow 1f6b33c2a1 Update blender_manifest.toml 2026-05-20 23:31:24 -05:00
snipeslow f4593f6846 Windows and Linux only Wheels. 2026-05-20 23:28:03 -05:00
snipeslow 4106ff2c94 Update README.md 2026-05-20 18:20:02 -05:00
snipeslow 2ffb8fb1f1 Update blender_manifest.toml 2026-05-20 18:13:36 -05:00
snipeslow 78200ab935 Update blender_manifest.toml 2026-05-20 18:13:13 -05:00
snipeslow 0881ed3831 Upload files to "wheels" 2026-05-20 18:12:48 -05:00
snipeslow 3ce1bc2ab3 Update core/updater.py 2026-05-20 17:51:34 -05:00
RinaDev 63460cf5a2 Update README.md 2026-02-07 23:03:58 +01:00
Yusarina cde0457ee1 Bump version to 0.5.3 in blender_manifest.toml 2025-12-09 02:52:17 +00:00
Yusarina 4ff17a66fe Update Blender version requirements to 5.0 2025-12-09 02:33:04 +00:00
Yusarina 8222f8b24c Delete CHANGELOG_NON_ASCII_FIX.md 2025-12-09 02:31:02 +00:00
Yusarina 37b92ded6d Change allowed version series to 0.5 2025-11-29 22:52:13 +00:00
Yusarina fb470f19da Merge pull request #217 from Yusarina/Current
Fix garbled Japanese/Unicode text in armature and mesh dropdowns
2025-11-29 22:45:11 +00:00
Yusarina 843147db69 Fix garbled Japanese/Unicode text in armature and mesh dropdowns
- Add proper caching to EnumProperty callbacks to prevent encoding corruption
- Use ASCII-safe identifiers (ARM_/MESH_ + pointer) with Unicode display names
- Add get_mesh_from_identifier() helper for safe mesh retrieval
- Update visemes panel to use new mesh identifier system
- Ensure stable string objects prevent Blender RNA encoding issues
2025-11-29 22:44:26 +00:00
Yusarina fe2b0d50cb Merge pull request #216 from Yusarina/Current
Fix swapped operator IDs for Apply Pose as Rest/Shapekey buttons #211
2025-11-29 22:26:40 +00:00
Yusarina c4dca2455d Fix swapped operator IDs for Apply Pose as Rest/Shapekey buttons #211 2025-11-29 22:22:58 +00:00
Yusarina 659f3eb91e Update version label in Korean translation 2025-11-22 15:26:19 +00:00
Yusarina ff19a895dc Update AvatarToolkit label version to 0.5.2 2025-11-22 15:26:07 +00:00
Yusarina e6e5a98e58 Update Avatar Toolkit version to Alpha 0.5.2 2025-11-22 15:25:55 +00:00
Yusarina 3fe00da569 Bump version from 0.5.1 to 0.5.2 2025-11-22 15:25:17 +00:00
Yusarina 108f9d3bc8 Merge pull request #214 from Yusarina/Current
Fix to translation service
2025-11-22 14:11:16 +00:00
Yusarina 1847628dc8 Fix to translation service 2025-11-22 13:12:48 +00:00
Yusarina 25a43afdbc Merge pull request #213 from Yusarina/atk-next
Logging Fix
2025-11-20 03:22:12 +00:00
Yusarina baaf4049f6 Logging Fix 2025-11-20 03:21:31 +00:00
Yusarina 299800e5c2 Update allowed version series to 0.6 2025-11-19 06:41:43 +00:00
Yusarina f6197ccbbf Merge pull request #210 from teamneoneko/Current
Bring Next Up To Speed
2025-11-19 06:41:05 +00:00
20 changed files with 317 additions and 55 deletions
+6 -3
View File
@@ -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 -5
View File
@@ -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
View File
@@ -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"""
+3 -2
View File
@@ -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)
+46 -8
View File
@@ -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,14 +844,18 @@ 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
armature = get_active_armature(context)
if armature:
logger.info(f"Active armature set to: {armature.name}")
# Deselect all objects first # Deselect all objects first
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
# Select and make active the chosen armature # Select and make active the chosen armature
self.active_armature.select_set(True) armature.select_set(True)
context.view_layer.objects.active = self.active_armature context.view_layer.objects.active = armature
logger.info(f"Selected and activated armature: {self.active_armature.name}") logger.info(f"Selected and activated armature: {armature.name}")
# Clear armature caches when armature changes to ensure fresh validation # Clear armature caches when armature changes to ensure fresh validation
try: try:
@@ -827,6 +863,8 @@ def update_active_armature(self: PropertyGroup, context: Context) -> None:
clear_armature_caches() clear_armature_caches()
except ImportError: except ImportError:
pass # UI module might not be loaded yet 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")
+61 -4
View File
@@ -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):
# Try UTF-8 first, fallback to other encodings
try:
with open(self._cache_file, 'r', encoding='utf-8') as f: with open(self._cache_file, 'r', encoding='utf-8') as f:
self._cache = json.load(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"
+51
View File
@@ -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
View File
@@ -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')
+2 -2
View File
@@ -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")
+8 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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.
Binary file not shown.
Binary file not shown.