Merge pull request #214 from Yusarina/Current
Fix to translation service
This commit is contained in:
+62
-7
@@ -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
|
||||
|
||||
+20
-14
@@ -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")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user