diff --git a/core/common.py b/core/common.py index 4b2e39f..8299b30 100644 --- a/core/common.py +++ b/core/common.py @@ -92,20 +92,75 @@ class ProgressTracker: def get_active_armature(context: Context) -> Optional[Object]: """Get the currently selected armature from Avatar Toolkit properties""" - armature_name = str(context.scene.avatar_toolkit.active_armature) - if armature_name and armature_name != 'NONE': - return bpy.data.objects.get(armature_name) + try: + # Get the safe identifier from the enum property + 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 def set_active_armature(context: Context, armature: Object) -> None: - """Set the active armature for Avatar Toolkit operations""" - context.scene.avatar_toolkit.active_armature = armature + """Set the active armature for Avatar Toolkit operations using safe identifier""" + 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_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 + """ if context is None: context = bpy.context - armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE'] + + # Use object's as_pointer() value as a safe ASCII identifier + armatures = [] + for obj in context.scene.objects: + if obj.type == 'ARMATURE': + # Create a safe ASCII identifier using the object pointer + safe_id = f"ARM_{obj.as_pointer()}" + armatures.append((safe_id, obj.name, "")) + if not armatures: return [('NONE', t("Armature.validation.no_armature"), '')] return armatures diff --git a/core/logging_setup.py b/core/logging_setup.py index dbfe003..b6b51d8 100644 --- a/core/logging_setup.py +++ b/core/logging_setup.py @@ -33,9 +33,10 @@ def configure_logging(enabled: bool = False, level: str = "WARNING") -> None: logger.addHandler(handler) 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()}" - _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: _original_error(msg, *args, **kwargs) diff --git a/core/properties.py b/core/properties.py index 4db9744..7c14f56 100644 --- a/core/properties.py +++ b/core/properties.py @@ -812,21 +812,27 @@ def update_translation_mode(self: PropertyGroup, context: Context) -> None: def update_active_armature(self: PropertyGroup, context: Context) -> None: """Update the active armature when selection changes""" - if self.active_armature: - logger.info(f"Active armature set to: {self.active_armature}") - # Deselect all objects first - 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}") + if self.active_armature and self.active_armature != 'NONE': + # Get the actual armature object from the identifier + armature = get_active_armature(context) - # 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 + if armature: + logger.info(f"Active armature set to: {armature.name}") + # Deselect all objects first + bpy.ops.object.select_all(action='DESELECT') + # Select and make active the chosen armature + 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: logger.info("No armature selected") diff --git a/core/translation_manager.py b/core/translation_manager.py index ef18c96..5a571a9 100644 --- a/core/translation_manager.py +++ b/core/translation_manager.py @@ -67,8 +67,14 @@ class TranslationCache: """Load cache from file""" try: if os.path.exists(self._cache_file): - with open(self._cache_file, 'r', encoding='utf-8') as f: - self._cache = json.load(f) + # Try UTF-8 first, fallback to other encodings + 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") else: self._cache = {} @@ -147,6 +153,15 @@ class AvatarToolkitTranslationManager: def translate_single(self, name: str, category: str = "auto", source_lang: str = "ja", target_lang: str = "en") -> TranslationResult: """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(): return TranslationResult(name, name, "skipped") @@ -300,6 +315,8 @@ class AvatarToolkitTranslationManager: def _process_category_batch_optimized(self, category_jobs: List[TranslationJob], 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""" + from .translation_service import safe_decode_text + if not category_jobs: return [] @@ -315,6 +332,14 @@ class AvatarToolkitTranslationManager: results[i] = TranslationResult(job.name, job.name, "skipped", category=job.category) 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() # Check cache first @@ -426,13 +451,21 @@ class AvatarToolkitTranslationManager: def translate_armature_bones(self, armature: Object, apply_results: bool = True) -> List[TranslationResult]: """Translate all bone names in an armature""" + from .translation_service import safe_decode_text + if not armature or armature.type != 'ARMATURE': return [] jobs = [] 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( - name=bone.name, + name=bone_name, category="bones", object_ref=bone, property_name="name" @@ -442,13 +475,21 @@ class AvatarToolkitTranslationManager: def translate_object_shapekeys(self, mesh_obj: Object, apply_results: bool = True) -> List[TranslationResult]: """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: return [] jobs = [] 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( - name=shape_key.name, + name=sk_name, category="shapekeys", object_ref=shape_key, property_name="name" @@ -458,6 +499,8 @@ class AvatarToolkitTranslationManager: def translate_scene_materials(self, apply_results: bool = True) -> List[TranslationResult]: """Translate all material names in the scene""" + from .translation_service import safe_decode_text + jobs = [] processed_materials: Set[str] = set() @@ -465,8 +508,14 @@ class AvatarToolkitTranslationManager: if obj.type == 'MESH' and obj.data.materials: for material in obj.data.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( - name=material.name, + name=mat_name, category="materials", object_ref=material, property_name="name" @@ -478,14 +527,22 @@ class AvatarToolkitTranslationManager: def translate_scene_objects(self, object_types: Optional[Set[str]] = None, apply_results: bool = True) -> List[TranslationResult]: """Translate all object names in the scene""" + from .translation_service import safe_decode_text + if object_types is None: object_types = {'MESH', 'ARMATURE', 'EMPTY'} jobs = [] for obj in bpy.data.objects: 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( - name=obj.name, + name=obj_name, category="objects", object_ref=obj, property_name="name" diff --git a/core/translation_service.py b/core/translation_service.py index d7edb19..de9a242 100644 --- a/core/translation_service.py +++ b/core/translation_service.py @@ -14,6 +14,43 @@ from .logging_setup import logger 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 class TranslationRequest: """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: """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}") if not text or not text.strip(): @@ -220,6 +259,8 @@ class DeepLService(TranslationService): if not texts: 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}") 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: """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}") if not text or not text.strip(): @@ -430,6 +473,8 @@ class MyMemoryService(TranslationService): if not texts: 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}") 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: """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}") if not text or not text.strip(): @@ -658,6 +705,8 @@ class LibreTranslateService(TranslationService): if not texts: 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}") # 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]: """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(): return text, "none"