office_translator/services/translation_service.py

202 lines
7.4 KiB
Python

"""
Translation Service Abstraction
Provides a unified interface for different translation providers
"""
from abc import ABC, abstractmethod
from typing import Optional, List
import requests
from deep_translator import GoogleTranslator, DeeplTranslator, LibreTranslator
from config import config
class TranslationProvider(ABC):
"""Abstract base class for translation providers"""
@abstractmethod
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
"""Translate text from source to target language"""
pass
class GoogleTranslationProvider(TranslationProvider):
"""Google Translate implementation"""
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
try:
translator = GoogleTranslator(source=source_language, target=target_language)
return translator.translate(text)
except Exception as e:
print(f"Translation error: {e}")
return text
class DeepLTranslationProvider(TranslationProvider):
"""DeepL Translate implementation"""
def __init__(self, api_key: str):
self.api_key = api_key
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
try:
translator = DeeplTranslator(api_key=self.api_key, source=source_language, target=target_language)
return translator.translate(text)
except Exception as e:
print(f"Translation error: {e}")
return text
class LibreTranslationProvider(TranslationProvider):
"""LibreTranslate implementation"""
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
try:
# LibreTranslator doesn't need API key for self-hosted instances
translator = LibreTranslator(source=source_language, target=target_language, custom_url="http://localhost:5000")
return translator.translate(text)
except Exception as e:
# Fail silently and return original text
return text
class OllamaTranslationProvider(TranslationProvider):
"""Ollama LLM translation implementation"""
def __init__(self, base_url: str = "http://localhost:11434", model: str = "llama3", vision_model: str = "llava"):
self.base_url = base_url.rstrip('/')
self.model = model
self.vision_model = vision_model
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
try:
prompt = f"Translate the following text to {target_language}. Return ONLY the translation, nothing else:\n\n{text}"
response = requests.post(
f"{self.base_url}/api/generate",
json={
"model": self.model,
"prompt": prompt,
"stream": False
},
timeout=30
)
response.raise_for_status()
result = response.json()
return result.get("response", text).strip()
except Exception as e:
print(f"Ollama translation error: {e}")
return text
def translate_image(self, image_path: str, target_language: str) -> str:
"""Translate text within an image using Ollama vision model"""
import base64
try:
# Read and encode image
with open(image_path, 'rb') as img_file:
image_data = base64.b64encode(img_file.read()).decode('utf-8')
prompt = f"Extract all text from this image and translate it to {target_language}. Return ONLY the translated text, preserving the structure and formatting."
response = requests.post(
f"{self.base_url}/api/generate",
json={
"model": self.vision_model,
"prompt": prompt,
"images": [image_data],
"stream": False
},
timeout=60
)
response.raise_for_status()
result = response.json()
return result.get("response", "").strip()
except Exception as e:
print(f"Ollama vision translation error: {e}")
return ""
@staticmethod
def list_models(base_url: str = "http://localhost:11434") -> List[str]:
"""List available Ollama models"""
try:
response = requests.get(f"{base_url.rstrip('/')}/api/tags", timeout=5)
response.raise_for_status()
models = response.json().get("models", [])
return [model["name"] for model in models]
except Exception as e:
print(f"Error listing Ollama models: {e}")
return []
class WebLLMTranslationProvider(TranslationProvider):
"""WebLLM browser-based translation (client-side processing)"""
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
# WebLLM translation happens client-side in the browser
# This is just a placeholder - actual translation is done by JavaScript
# For server-side, we'll just pass through for now
return text
class TranslationService:
"""Main translation service that delegates to the configured provider"""
def __init__(self, provider: Optional[TranslationProvider] = None):
if provider:
self.provider = provider
else:
# Auto-select provider based on configuration
self.provider = self._get_default_provider()
def _get_default_provider(self) -> TranslationProvider:
"""Get the default translation provider from configuration"""
# Always use Google Translate by default to avoid API key issues
# Provider will be overridden per request in the API endpoint
return GoogleTranslationProvider()
def translate_text(self, text: str, target_language: str, source_language: str = 'auto') -> str:
"""
Translate a single text string
Args:
text: Text to translate
target_language: Target language code (e.g., 'es', 'fr', 'de')
source_language: Source language code (default: 'auto' for auto-detection)
Returns:
Translated text
"""
if not text or not text.strip():
return text
return self.provider.translate(text, target_language, source_language)
def translate_batch(self, texts: list[str], target_language: str, source_language: str = 'auto') -> list[str]:
"""
Translate multiple text strings
Args:
texts: List of texts to translate
target_language: Target language code
source_language: Source language code (default: 'auto')
Returns:
List of translated texts
"""
return [self.translate_text(text, target_language, source_language) for text in texts]
# Global translation service instance
translation_service = TranslationService()