Translation Service
- Added translation service with 3 services. - MyMemory (Free no api key needed but 1000 words a day and Skow) - Deepl (Free with API key, 500000 words a month and fast) - Libre Translate (Paid unless you host your own server, open source) - Added caching for Quick Access and the translate service to speed up the UI. Can be fast depending on the service you use/ PC specs and etc).
This commit is contained in:
@@ -17,6 +17,17 @@ from ..core.common import (
|
||||
get_armature_list,
|
||||
get_armature_stats
|
||||
)
|
||||
|
||||
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
||||
_validation_cache = {}
|
||||
_stats_cache = {}
|
||||
|
||||
def clear_armature_caches():
|
||||
"""Clear all armature-related caches - called when armature changes"""
|
||||
global _validation_cache, _stats_cache
|
||||
_validation_cache.clear()
|
||||
_stats_cache.clear()
|
||||
|
||||
from ..functions.pose_mode import (
|
||||
AvatarToolkit_OT_StartPoseMode,
|
||||
AvatarToolkit_OT_StopPoseMode,
|
||||
@@ -84,10 +95,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
# Armature Selection
|
||||
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
|
||||
|
||||
# Armature Validation
|
||||
# Armature Validation (cached to improve performance)
|
||||
active_armature: Optional[Object] = get_active_armature(context)
|
||||
if active_armature:
|
||||
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
|
||||
# Cache validation results to avoid expensive recalculations on every draw
|
||||
cache_key = f"validation_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
||||
|
||||
if cache_key not in _validation_cache:
|
||||
_validation_cache[cache_key] = validate_armature(active_armature, detailed_messages=True)
|
||||
|
||||
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = _validation_cache[cache_key]
|
||||
|
||||
# Check if this is a PMX model
|
||||
is_pmx_model = False
|
||||
@@ -235,7 +252,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
row = info_box.row()
|
||||
split = row.split(factor=0.6)
|
||||
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
|
||||
stats = get_armature_stats(active_armature)
|
||||
|
||||
# Cache armature stats to avoid expensive recalculations
|
||||
stats_cache_key = f"stats_{active_armature.name}_{active_armature.data.name}_{len(active_armature.data.bones)}"
|
||||
|
||||
if stats_cache_key not in _stats_cache:
|
||||
_stats_cache[stats_cache_key] = get_armature_stats(active_armature)
|
||||
|
||||
stats = _stats_cache[stats_cache_key]
|
||||
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
|
||||
|
||||
if stats['has_pose']:
|
||||
|
||||
@@ -0,0 +1,731 @@
|
||||
# GPL License
|
||||
|
||||
import bpy
|
||||
from typing import Set, Dict, List, Optional, Any
|
||||
from bpy.types import (
|
||||
Operator,
|
||||
Panel,
|
||||
Context,
|
||||
UILayout,
|
||||
WindowManager,
|
||||
Event,
|
||||
Object
|
||||
)
|
||||
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
|
||||
from ..core.translations import t
|
||||
from ..core.logging_setup import logger
|
||||
from ..core.common import get_active_armature, ProgressTracker
|
||||
|
||||
# Module-level cache for UI performance (avoids Blender scene property write restrictions)
|
||||
_ui_cache = {
|
||||
'translation_status': {},
|
||||
'deepl_config': {},
|
||||
'libretranslate_config': {},
|
||||
'last_refresh_frame': 0,
|
||||
'cache_refresh_interval': 30
|
||||
}
|
||||
|
||||
|
||||
class AvatarToolkit_OT_TranslateNames(Operator):
|
||||
"""Translate names using the translation system"""
|
||||
bl_idname: str = "avatar_toolkit.translate_names"
|
||||
bl_label: str = t("Translation.translate_names")
|
||||
bl_description: str = t("Translation.translate_names_desc")
|
||||
|
||||
translation_type: bpy.props.EnumProperty(
|
||||
items=[
|
||||
('bones', t("Translation.type.bones"), t("Translation.type.bones_desc")),
|
||||
('shapekeys', t("Translation.type.shapekeys"), t("Translation.type.shapekeys_desc")),
|
||||
('materials', t("Translation.type.materials"), t("Translation.type.materials_desc")),
|
||||
('objects', t("Translation.type.objects"), t("Translation.type.objects_desc")),
|
||||
('all', t("Translation.type.all"), t("Translation.type.all_desc"))
|
||||
],
|
||||
default='bones'
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info(f"Starting translation operation: {self.translation_type}")
|
||||
|
||||
try:
|
||||
from ..core.translation_manager import get_avatar_translation_manager
|
||||
manager = get_avatar_translation_manager()
|
||||
|
||||
# Set up progress callback for detailed feedback
|
||||
def progress_callback(current: int, total: int, message: str):
|
||||
progress_percent = (current / max(total, 1)) * 100
|
||||
logger.info(f"Translation progress: {current}/{total} ({progress_percent:.1f}%) - {message}")
|
||||
context.area.header_text_set(f"Translating: {current}/{total} - {message}")
|
||||
|
||||
manager.set_progress_callback(progress_callback)
|
||||
|
||||
results = []
|
||||
armature = get_active_armature(context)
|
||||
|
||||
total_steps = 0
|
||||
if self.translation_type == 'bones' or self.translation_type == 'all':
|
||||
if armature:
|
||||
total_steps += len(armature.data.bones)
|
||||
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
||||
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
||||
for mesh in meshes:
|
||||
if mesh.data.shape_keys:
|
||||
total_steps += len(mesh.data.shape_keys.key_blocks)
|
||||
if self.translation_type == 'materials' or self.translation_type == 'all':
|
||||
materials = set()
|
||||
for obj in context.scene.objects:
|
||||
if obj.type == 'MESH' and obj.data.materials:
|
||||
for mat in obj.data.materials:
|
||||
if mat:
|
||||
materials.add(mat)
|
||||
total_steps += len(materials)
|
||||
if self.translation_type == 'objects' or self.translation_type == 'all':
|
||||
objects = [obj for obj in context.scene.objects if obj.type in {'MESH', 'ARMATURE', 'EMPTY'}]
|
||||
total_steps += len(objects)
|
||||
|
||||
logger.info(f"Translation operation will process approximately {total_steps} items")
|
||||
|
||||
with ProgressTracker(context, total_steps, "Translation") as progress:
|
||||
if self.translation_type == 'bones' or self.translation_type == 'all':
|
||||
if armature:
|
||||
logger.info(f"Starting bone translation for armature: {armature.name}")
|
||||
self.report({'INFO'}, f"Translating {len(armature.data.bones)} bones...")
|
||||
|
||||
bone_results = manager.translate_armature_bones(armature, apply_results=True)
|
||||
results.extend(bone_results)
|
||||
|
||||
successful_bones = sum(1 for r in bone_results if r.method not in ['failed', 'skipped'])
|
||||
progress.step(f"Bones: {successful_bones}/{len(bone_results)} translated")
|
||||
logger.info(f"Bone translation complete: {successful_bones}/{len(bone_results)} successful")
|
||||
else:
|
||||
self.report({'WARNING'}, t("Translation.no_armature"))
|
||||
logger.warning("No armature selected for bone translation")
|
||||
|
||||
if self.translation_type == 'shapekeys' or self.translation_type == 'all':
|
||||
meshes = [obj for obj in context.scene.objects if obj.type == 'MESH']
|
||||
logger.info(f"Starting shape key translation for {len(meshes)} mesh objects")
|
||||
|
||||
total_shapekeys = 0
|
||||
for mesh in meshes:
|
||||
if mesh.data.shape_keys:
|
||||
shapekey_count = len(mesh.data.shape_keys.key_blocks)
|
||||
self.report({'INFO'}, f"Translating {shapekey_count} shape keys in {mesh.name}...")
|
||||
|
||||
shapekey_results = manager.translate_object_shapekeys(mesh, apply_results=True)
|
||||
results.extend(shapekey_results)
|
||||
total_shapekeys += len(shapekey_results)
|
||||
|
||||
successful_shapekeys = sum(1 for r in results[-total_shapekeys:] if r.method not in ['failed', 'skipped'])
|
||||
progress.step(f"Shape keys: {successful_shapekeys}/{total_shapekeys} translated")
|
||||
logger.info(f"Shape key translation complete: {successful_shapekeys}/{total_shapekeys} successful")
|
||||
|
||||
if self.translation_type == 'materials' or self.translation_type == 'all':
|
||||
logger.info("Starting material translation")
|
||||
self.report({'INFO'}, "Translating materials...")
|
||||
|
||||
material_results = manager.translate_scene_materials(apply_results=True)
|
||||
results.extend(material_results)
|
||||
|
||||
successful_materials = sum(1 for r in material_results if r.method not in ['failed', 'skipped'])
|
||||
progress.step(f"Materials: {successful_materials}/{len(material_results)} translated")
|
||||
logger.info(f"Material translation complete: {successful_materials}/{len(material_results)} successful")
|
||||
|
||||
if self.translation_type == 'objects' or self.translation_type == 'all':
|
||||
logger.info("Starting object translation")
|
||||
self.report({'INFO'}, "Translating objects...")
|
||||
|
||||
object_results = manager.translate_scene_objects(apply_results=True)
|
||||
results.extend(object_results)
|
||||
|
||||
successful_objects = sum(1 for r in object_results if r.method not in ['failed', 'skipped'])
|
||||
progress.step(f"Objects: {successful_objects}/{len(object_results)} translated")
|
||||
logger.info(f"Object translation complete: {successful_objects}/{len(object_results)} successful")
|
||||
|
||||
manager.set_progress_callback(None)
|
||||
context.area.header_text_set(None)
|
||||
|
||||
# Final results summary
|
||||
successful = sum(1 for r in results if r.method not in ['failed', 'skipped'])
|
||||
total = len(results)
|
||||
|
||||
dictionary_count = sum(1 for r in results if r.method == 'dictionary')
|
||||
api_count = sum(1 for r in results if r.method == 'api')
|
||||
cache_count = sum(1 for r in results if r.method == 'cache')
|
||||
failed_count = sum(1 for r in results if r.method == 'failed')
|
||||
|
||||
logger.info(f"Translation summary: {successful}/{total} successful (Dictionary: {dictionary_count}, API: {api_count}, Cache: {cache_count}, Failed: {failed_count})")
|
||||
|
||||
if successful > 0:
|
||||
success_msg = f"Successfully translated {successful}/{total} items"
|
||||
if dictionary_count > 0:
|
||||
success_msg += f" (Dictionary: {dictionary_count}"
|
||||
if api_count > 0:
|
||||
success_msg += f", API: {api_count}"
|
||||
if cache_count > 0:
|
||||
success_msg += f", Cache: {cache_count}"
|
||||
if dictionary_count > 0 or api_count > 0 or cache_count > 0:
|
||||
success_msg += ")"
|
||||
|
||||
self.report({'INFO'}, success_msg)
|
||||
else:
|
||||
if total > 0:
|
||||
self.report({'WARNING'}, f"No translations were applied ({total} items checked)")
|
||||
else:
|
||||
self.report({'WARNING'}, "No items found to translate")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
manager.set_progress_callback(None)
|
||||
context.area.header_text_set(None)
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.error(f"Translation operation failed: {e}", exc_info=True)
|
||||
self.report({'ERROR'}, f"Translation failed: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class AvatarToolkit_OT_TestTranslationService(Operator):
|
||||
"""Test the currently selected translation service"""
|
||||
bl_idname: str = "avatar_toolkit.test_translation_service"
|
||||
bl_label: str = t("Translation.test_service")
|
||||
bl_description: str = t("Translation.test_service_desc")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Starting translation service test")
|
||||
|
||||
try:
|
||||
from ..core.translation_manager import get_avatar_translation_manager
|
||||
manager = get_avatar_translation_manager()
|
||||
|
||||
self.report({'INFO'}, "Testing translation service...")
|
||||
context.area.header_text_set("Testing translation service...")
|
||||
|
||||
# Test translation with a simple word
|
||||
test_word = "テスト" # "Test" in Japanese
|
||||
logger.info(f"Testing translation of '{test_word}'")
|
||||
|
||||
result = manager.translate_single(test_word, "auto")
|
||||
|
||||
# Clear status
|
||||
context.area.header_text_set(None)
|
||||
|
||||
if result.method == "failed":
|
||||
logger.error(f"Translation test failed: {result}")
|
||||
self.report({'ERROR'}, t("Translation.test_failed"))
|
||||
else:
|
||||
service_info = f" ({result.service})" if result.service else ""
|
||||
success_msg = f"Translation test successful: '{test_word}' → '{result.translated}' via {result.method}{service_info}"
|
||||
logger.info(f"Translation test successful: {result}")
|
||||
self.report({'INFO'}, success_msg)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
context.area.header_text_set(None)
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.error(f"Translation service test failed: {e}", exc_info=True)
|
||||
self.report({'ERROR'}, f"Service test failed: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class AvatarToolkit_OT_ClearTranslationCache(Operator):
|
||||
"""Clear all translation caches"""
|
||||
bl_idname: str = "avatar_toolkit.clear_translation_cache"
|
||||
bl_label: str = t("Translation.clear_cache")
|
||||
bl_description: str = t("Translation.clear_cache_desc")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
from ..core.translation_manager import get_avatar_translation_manager
|
||||
manager = get_avatar_translation_manager()
|
||||
manager.clear_all_caches()
|
||||
|
||||
self.report({'INFO'}, t("Translation.cache_cleared"))
|
||||
return {'FINISHED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear translation cache: {e}")
|
||||
self.report({'ERROR'}, f"Failed to clear cache: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
class AvatarToolkit_OT_ConfigureDeepL(Operator):
|
||||
"""Configure DeepL API settings"""
|
||||
bl_idname: str = "avatar_toolkit.configure_deepl"
|
||||
bl_label: str = t("Translation.configure_deepl")
|
||||
bl_description: str = t("Translation.configure_deepl_desc")
|
||||
|
||||
api_key: bpy.props.StringProperty(
|
||||
name=t("Translation.deepl_api_key"),
|
||||
description=t("Translation.deepl_api_key_desc"),
|
||||
default="",
|
||||
subtype='PASSWORD'
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
if not self.api_key.strip():
|
||||
self.report({'ERROR'}, "API key cannot be empty")
|
||||
return {'CANCELLED'}
|
||||
|
||||
from ..core.translation_manager import configure_translation_service
|
||||
success = configure_translation_service("deepl", api_key=self.api_key.strip())
|
||||
|
||||
if success:
|
||||
_ui_cache['deepl_config'].clear()
|
||||
_ui_cache['translation_status'].clear()
|
||||
if 'batch_info' in _ui_cache:
|
||||
del _ui_cache['batch_info']
|
||||
self.report({'INFO'}, "DeepL API configured successfully")
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'ERROR'}, "Failed to configure DeepL API - check your API key")
|
||||
return {'CANCELLED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"DeepL configuration failed: {e}")
|
||||
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
# Load existing API key if available
|
||||
try:
|
||||
from ..core.addon_preferences import get_preference
|
||||
existing_key = get_preference("deepl_api_key", "")
|
||||
if existing_key:
|
||||
# Show only first/last few characters for security
|
||||
if len(existing_key) > 8:
|
||||
display_key = existing_key[:4] + "..." + existing_key[-4:]
|
||||
self.api_key = existing_key # Keep full key for editing
|
||||
else:
|
||||
self.api_key = existing_key
|
||||
except:
|
||||
pass
|
||||
|
||||
wm: WindowManager = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
|
||||
info_box = layout.box()
|
||||
info_col = info_box.column()
|
||||
info_col.label(text="DeepL API Configuration", icon='SETTINGS')
|
||||
info_col.separator()
|
||||
info_col.label(text="1. Visit deepl.com/pro to get your free API key")
|
||||
info_col.label(text="2. Free tier: 500,000 characters/month")
|
||||
info_col.label(text="3. Higher quality than other services")
|
||||
info_col.label(text="4. The Fastest Option due to native batching support")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, "api_key")
|
||||
|
||||
|
||||
class AvatarToolkit_OT_ConfigureLibreTranslate(Operator):
|
||||
"""Configure LibreTranslate server settings"""
|
||||
bl_idname: str = "avatar_toolkit.configure_libretranslate"
|
||||
bl_label: str = t("Translation.configure_libretranslate")
|
||||
bl_description: str = t("Translation.configure_libretranslate_desc")
|
||||
|
||||
server_url: bpy.props.StringProperty(
|
||||
name=t("Translation.server_url"),
|
||||
description=t("Translation.server_url_desc"),
|
||||
default="https://libretranslate.com"
|
||||
)
|
||||
|
||||
api_key: bpy.props.StringProperty(
|
||||
name=t("Translation.api_key"),
|
||||
description=t("Translation.api_key_desc"),
|
||||
default="",
|
||||
subtype='PASSWORD'
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
if not self.server_url.strip():
|
||||
self.report({'ERROR'}, "Server URL cannot be empty")
|
||||
return {'CANCELLED'}
|
||||
|
||||
from ..core.translation_manager import configure_translation_service
|
||||
success = configure_translation_service("libretranslate",
|
||||
server_url=self.server_url.strip(),
|
||||
api_key=self.api_key.strip() if self.api_key.strip() else None)
|
||||
|
||||
if success:
|
||||
_ui_cache['libretranslate_config'].clear()
|
||||
_ui_cache['translation_status'].clear()
|
||||
if 'batch_info' in _ui_cache:
|
||||
del _ui_cache['batch_info']
|
||||
self.report({'INFO'}, f"LibreTranslate server configured: {self.server_url}")
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'ERROR'}, "Failed to connect to LibreTranslate server")
|
||||
return {'CANCELLED'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LibreTranslate configuration failed: {e}")
|
||||
self.report({'ERROR'}, f"Configuration failed: {str(e)}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
# Load existing server URL and API key if available
|
||||
try:
|
||||
from ..core.addon_preferences import get_preference
|
||||
existing_url = get_preference("libretranslate_url", "https://libretranslate.com")
|
||||
existing_api_key = get_preference("libretranslate_api_key", "")
|
||||
self.server_url = existing_url
|
||||
self.api_key = existing_api_key
|
||||
except:
|
||||
pass
|
||||
|
||||
wm: WindowManager = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=500)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
|
||||
info_box = layout.box()
|
||||
info_col = info_box.column()
|
||||
info_col.label(text="LibreTranslate Server Configuration", icon='SETTINGS')
|
||||
info_col.separator()
|
||||
info_col.label(text="⚠ libretranslate.com requires payment for API access")
|
||||
info_col.label(text="✓ You can run your own LibreTranslate server")
|
||||
info_col.label(text="✓ Or find community-hosted instances")
|
||||
info_col.separator()
|
||||
info_col.label(text="Examples:")
|
||||
info_col.label(text=" • Your server: https://translate.yoursite.com")
|
||||
info_col.label(text=" • Docker local: http://localhost:5000")
|
||||
|
||||
layout.separator()
|
||||
layout.prop(self, "server_url")
|
||||
layout.prop(self, "api_key")
|
||||
|
||||
|
||||
class AvatarToolkit_OT_TranslationStats(Operator):
|
||||
"""Show translation statistics"""
|
||||
bl_idname: str = "avatar_toolkit.translation_stats"
|
||||
bl_label: str = t("Translation.show_stats")
|
||||
bl_description: str = t("Translation.show_stats_desc")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
wm: WindowManager = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout: UILayout = self.layout
|
||||
|
||||
try:
|
||||
from ..core.translation_manager import get_avatar_translation_manager
|
||||
manager = get_avatar_translation_manager()
|
||||
stats = manager.get_translation_stats()
|
||||
|
||||
dict_box = layout.box()
|
||||
dict_box.label(text="Dictionary Translations", icon='BOOKMARKS')
|
||||
dict_stats = stats['dictionary_translations']
|
||||
for category, count in dict_stats.items():
|
||||
if count > 0:
|
||||
dict_box.label(text=f"{category.title()}: {count}")
|
||||
|
||||
cache_box = layout.box()
|
||||
cache_box.label(text="Translation Cache", icon='FILE_CACHE')
|
||||
cache_stats = stats['cache_stats']
|
||||
cache_box.label(text=f"Language pairs: {cache_stats['language_pairs']}")
|
||||
cache_box.label(text=f"Total cached: {cache_stats['total_entries']}")
|
||||
|
||||
service_box = layout.box()
|
||||
service_box.label(text="Translation Services", icon='WORLD')
|
||||
service_box.label(text=f"Current mode: {stats['current_mode']}")
|
||||
service_box.label(text=f"Primary service: {stats['primary_service']}")
|
||||
|
||||
available_services = stats['available_services']
|
||||
if available_services:
|
||||
service_box.label(text="Available services:")
|
||||
for service_id, service_name in available_services:
|
||||
service_box.label(text=f" • {service_name}")
|
||||
else:
|
||||
service_box.label(text="No services available", icon='ERROR')
|
||||
|
||||
except Exception as e:
|
||||
layout.label(text=f"Error loading stats: {str(e)}", icon='ERROR')
|
||||
|
||||
|
||||
class AvatarToolKit_PT_TranslationPanel(Panel):
|
||||
"""Translation panel for Avatar Toolkit"""
|
||||
bl_label: str = t("Translation.label")
|
||||
bl_idname: str = "OBJECT_PT_avatar_toolkit_translation"
|
||||
bl_space_type: str = 'VIEW_3D'
|
||||
bl_region_type: str = 'UI'
|
||||
bl_category: str = CATEGORY_NAME
|
||||
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
|
||||
bl_order: int = 9
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
"""Draw the translation panel layout"""
|
||||
layout: UILayout = self.layout
|
||||
props = context.scene.avatar_toolkit
|
||||
|
||||
# Translation Service Settings
|
||||
service_box: UILayout = layout.box()
|
||||
col: UILayout = service_box.column(align=True)
|
||||
row: UILayout = col.row()
|
||||
row.scale_y = 1.2
|
||||
row.label(text=t("Translation.service_settings"), icon='WORLD')
|
||||
col.separator()
|
||||
|
||||
col.prop(props, "translation_service", text="")
|
||||
|
||||
col.prop(props, "translation_mode", text="")
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(props, "translation_expand",
|
||||
icon="TRIA_DOWN" if props.translation_expand else "TRIA_RIGHT",
|
||||
icon_only=True, emboss=False)
|
||||
row.label(text=t("Translation.advanced_settings"))
|
||||
|
||||
if props.translation_expand:
|
||||
config_col = service_box.column(align=True)
|
||||
|
||||
# MyMemory settings (no configuration needed)
|
||||
if props.translation_service == 'mymemory':
|
||||
config_col.separator()
|
||||
config_col.label(text="MyMemory Configuration:", icon='CHECKMARK')
|
||||
success_col = config_col.column()
|
||||
success_col.alert = False
|
||||
success_col.label(text="✓ No API key required!", icon='CHECKMARK')
|
||||
success_col.label(text="✓ Completely free service")
|
||||
success_col.label(text="✓ 1000 translations per day")
|
||||
success_col.label(text="✓ Slowest Option due to no native batching")
|
||||
success_col.label(text="✓ Ready to use!")
|
||||
|
||||
elif props.translation_service == 'libretranslate':
|
||||
config_col.separator()
|
||||
config_col.label(text="LibreTranslate Configuration:", icon='SETTINGS')
|
||||
|
||||
# Check current server configuration (cached to avoid performance issues)
|
||||
try:
|
||||
if 'libretranslate_url' not in _ui_cache['libretranslate_config']:
|
||||
from ..core.addon_preferences import get_preference
|
||||
_ui_cache['libretranslate_config']['libretranslate_url'] = get_preference("libretranslate_url", "https://libretranslate.com")
|
||||
|
||||
server_url = _ui_cache['libretranslate_config']['libretranslate_url']
|
||||
|
||||
info_col = config_col.column()
|
||||
info_col.alert = False
|
||||
info_col.label(text=f"Server: {server_url}", icon='URL')
|
||||
|
||||
if "libretranslate.com" in server_url.lower():
|
||||
warning_col = config_col.column()
|
||||
warning_col.alert = True
|
||||
warning_col.label(text="⚠ Default server requires payment", icon='ERROR')
|
||||
warning_col.label(text="Configure your own LibreTranslate server")
|
||||
else:
|
||||
success_col = config_col.column()
|
||||
success_col.alert = False
|
||||
success_col.label(text="✓ Custom server configured", icon='CHECKMARK')
|
||||
|
||||
config_row = config_col.row()
|
||||
config_row.operator("avatar_toolkit.configure_libretranslate", text="Configure Server", icon='SETTINGS')
|
||||
except Exception as e:
|
||||
config_col.label(text="LibreTranslate configuration error", icon='ERROR')
|
||||
|
||||
elif props.translation_service == 'deepl':
|
||||
config_col.separator()
|
||||
config_col.label(text="DeepL Configuration:", icon='SETTINGS')
|
||||
|
||||
# Check if API key is configured (cached to avoid performance issues)
|
||||
try:
|
||||
if 'deepl_api_key' not in _ui_cache['deepl_config']:
|
||||
from ..core.addon_preferences import get_preference
|
||||
_ui_cache['deepl_config']['deepl_api_key'] = get_preference("deepl_api_key", "")
|
||||
|
||||
deepl_api_key = _ui_cache['deepl_config']['deepl_api_key']
|
||||
|
||||
if deepl_api_key and deepl_api_key.strip():
|
||||
success_col = config_col.column()
|
||||
success_col.alert = False
|
||||
success_col.label(text="✓ API key configured", icon='CHECKMARK')
|
||||
success_col.label(text="✓ High quality translations")
|
||||
success_col.label(text="✓ 500,000 chars/month free")
|
||||
success_col.label(text="✓ Ready to use!")
|
||||
|
||||
reconfig_row = config_col.row()
|
||||
reconfig_row.operator("avatar_toolkit.configure_deepl", text="Reconfigure API Key", icon='SETTINGS')
|
||||
else:
|
||||
warning_col = config_col.column()
|
||||
warning_col.alert = True
|
||||
warning_col.label(text="⚠ API key required!", icon='ERROR')
|
||||
warning_col.label(text="Get free key at deepl.com/pro")
|
||||
warning_col.label(text="500,000 characters/month free")
|
||||
|
||||
config_row = config_col.row()
|
||||
config_row.operator("avatar_toolkit.configure_deepl", text="Configure API Key", icon='PLUS')
|
||||
except Exception as e:
|
||||
config_col.label(text="DeepL configuration error", icon='ERROR')
|
||||
|
||||
|
||||
|
||||
# Language Settings
|
||||
lang_box: UILayout = layout.box()
|
||||
col = lang_box.column(align=True)
|
||||
row = col.row()
|
||||
row.scale_y = 1.2
|
||||
row.label(text=t("Translation.language_settings"), icon='SYNTAX_ON')
|
||||
col.separator()
|
||||
col.prop(props, "translation_source_language", text="From")
|
||||
col.prop(props, "translation_target_language", text="To")
|
||||
|
||||
# Quick Actions
|
||||
action_box: UILayout = layout.box()
|
||||
col = action_box.column(align=True)
|
||||
row = col.row()
|
||||
row.scale_y = 1.2
|
||||
row.label(text=t("Translation.quick_actions"), icon='PLAY')
|
||||
col.separator()
|
||||
|
||||
# Translate buttons
|
||||
row = col.row(align=True)
|
||||
op_bones = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Bones", icon='BONE_DATA')
|
||||
op_bones.translation_type = 'bones'
|
||||
|
||||
op_shapes = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Shape Keys", icon='SHAPEKEY_DATA')
|
||||
op_shapes.translation_type = 'shapekeys'
|
||||
|
||||
row = col.row(align=True)
|
||||
op_mats = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Materials", icon='MATERIAL_DATA')
|
||||
op_mats.translation_type = 'materials'
|
||||
|
||||
op_objs = row.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Objects", icon='OBJECT_DATA')
|
||||
op_objs.translation_type = 'objects'
|
||||
|
||||
col.separator()
|
||||
op_all = col.operator(AvatarToolkit_OT_TranslateNames.bl_idname, text="Translate All", icon='WORLD')
|
||||
op_all.translation_type = 'all'
|
||||
|
||||
# Utility buttons
|
||||
util_box: UILayout = layout.box()
|
||||
col = util_box.column(align=True)
|
||||
row = col.row()
|
||||
row.scale_y = 1.2
|
||||
row.label(text=t("Translation.utilities"), icon='TOOL_SETTINGS')
|
||||
col.separator()
|
||||
|
||||
row = col.row(align=True)
|
||||
row.operator(AvatarToolkit_OT_TestTranslationService.bl_idname, icon='PLAY')
|
||||
row.operator(AvatarToolkit_OT_TranslationStats.bl_idname, icon='INFO')
|
||||
|
||||
col.operator(AvatarToolkit_OT_ClearTranslationCache.bl_idname, icon='TRASH')
|
||||
|
||||
status_box = layout.box()
|
||||
status_col = status_box.column()
|
||||
|
||||
try:
|
||||
status_cache_key = f"translation_status_{props.translation_service}_{props.translation_mode}"
|
||||
|
||||
# Refresh cache periodically
|
||||
frame = context.scene.frame_current
|
||||
cache_expired = (frame - _ui_cache['last_refresh_frame'] >= _ui_cache['cache_refresh_interval']) or status_cache_key not in _ui_cache['translation_status']
|
||||
|
||||
if cache_expired:
|
||||
from ..core.translation_manager import get_available_translation_services, get_avatar_translation_manager
|
||||
|
||||
manager = get_avatar_translation_manager()
|
||||
available_services = get_available_translation_services()
|
||||
|
||||
_ui_cache['translation_status'][status_cache_key] = {
|
||||
'available_services': available_services,
|
||||
'manager': manager,
|
||||
'cache_stats': None
|
||||
}
|
||||
_ui_cache['last_refresh_frame'] = frame
|
||||
|
||||
try:
|
||||
stats = manager.get_translation_stats()
|
||||
_ui_cache['translation_status'][status_cache_key]['cache_stats'] = stats['cache_stats']
|
||||
except:
|
||||
pass
|
||||
|
||||
# Use cached data
|
||||
cached_data = _ui_cache['translation_status'].get(status_cache_key, {})
|
||||
available_services = cached_data.get('available_services', [])
|
||||
cache_stats = cached_data.get('cache_stats')
|
||||
|
||||
if available_services:
|
||||
status_col.label(text="Translation services ready", icon='CHECKMARK')
|
||||
|
||||
# Show current service status
|
||||
current_service = props.translation_service
|
||||
service_available = any(service_id == current_service for service_id, _ in available_services)
|
||||
|
||||
if service_available:
|
||||
service_name = next((name for sid, name in available_services if sid == current_service), current_service)
|
||||
status_col.label(text=f"Active: {service_name}", icon='WORLD')
|
||||
|
||||
# Show translation mode
|
||||
mode_display = {
|
||||
'hybrid': 'Dictionary + API',
|
||||
'dictionary_only': 'Dictionary Only',
|
||||
'api_only': 'API Only'
|
||||
}.get(props.translation_mode, props.translation_mode)
|
||||
status_col.label(text=f"Mode: {mode_display}", icon='SETTINGS')
|
||||
|
||||
# Show cache status
|
||||
if cache_stats and cache_stats['total_entries'] > 0:
|
||||
status_col.label(text=f"Cache: {cache_stats['total_entries']} translations", icon='FILE_CACHE')
|
||||
|
||||
# Show batch translation capability
|
||||
try:
|
||||
if 'batch_info' not in _ui_cache:
|
||||
from ..core.translation_manager import get_batch_translation_info
|
||||
_ui_cache['batch_info'] = get_batch_translation_info()
|
||||
|
||||
batch_info = _ui_cache['batch_info'].get(current_service, {})
|
||||
if batch_info.get('supports_batch', False):
|
||||
batch_type = batch_info.get('batch_type', 'individual')
|
||||
if batch_type == 'native':
|
||||
status_col.label(text="⚡ DeepL Native batch translation (up to 50x faster)", icon='LIGHT')
|
||||
elif batch_type == 'concurrent':
|
||||
if current_service == 'mymemory':
|
||||
status_col.label(text="⚡ Slowest Option, no native Batching", icon='LIGHT')
|
||||
else:
|
||||
status_col.label(text="⚡ Slightly Faster then MyMemory processing (3x faster)", icon='LIGHT')
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
warning_col = status_col.column()
|
||||
warning_col.alert = True
|
||||
warning_col.label(text=f"Service unavailable: {props.translation_service}", icon='ERROR')
|
||||
|
||||
|
||||
else:
|
||||
warning_col = status_col.column()
|
||||
warning_col.alert = True
|
||||
warning_col.label(text="No translation services available", icon='ERROR')
|
||||
|
||||
if props.translation_service == 'mymemory':
|
||||
warning_col.label(text="Internet connection required")
|
||||
|
||||
except Exception as e:
|
||||
error_col = status_col.column()
|
||||
error_col.alert = True
|
||||
error_col.label(text="Translation system error", icon='ERROR')
|
||||
logger.error(f"Status display error: {e}")
|
||||
|
||||
try:
|
||||
if hasattr(context.area, 'header_text') and context.area.header_text:
|
||||
progress_col = status_col.column()
|
||||
progress_col.alert = False
|
||||
progress_col.label(text=context.area.header_text, icon='TIME')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user