Add OpenRouter provider with DeepSeek support - best value for translation (.14/M tokens)

This commit is contained in:
Sepehr 2025-11-30 22:10:34 +01:00
parent b65e683d32
commit 3346817a8a
7 changed files with 601 additions and 8 deletions

280
benchmark.py Normal file
View File

@ -0,0 +1,280 @@
"""
Translation Benchmark Script
Tests translation performance for 200 pages equivalent of text
"""
import time
import random
import statistics
import sys
import os
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from services.translation_service import (
GoogleTranslationProvider,
TranslationService,
_translation_cache
)
# Sample texts of varying complexity (simulating real document content)
SAMPLE_TEXTS = [
"Welcome to our company",
"Please review the attached document",
"The quarterly results exceeded expectations",
"Meeting scheduled for next Monday at 10 AM",
"Thank you for your continued support",
"This report contains confidential information",
"Please contact customer support for assistance",
"The project deadline has been extended",
"Annual revenue increased by 15% compared to last year",
"Our team is committed to delivering excellence",
"The new product launch was a great success",
"Please find the updated specifications attached",
"We appreciate your patience during this transition",
"The contract terms have been finalized",
"Quality assurance testing is now complete",
"The budget allocation has been approved",
"Employee satisfaction survey results are available",
"The system maintenance is scheduled for this weekend",
"Our partnership continues to grow stronger",
"The training program will begin next month",
"Customer feedback has been overwhelmingly positive",
"The risk assessment has been completed",
"Strategic planning session is confirmed",
"Performance metrics indicate steady improvement",
"The compliance audit was successful",
"Innovation remains our top priority",
"Market analysis shows promising trends",
"The implementation phase is on track",
"Stakeholder engagement continues to increase",
"Operational efficiency has improved significantly",
# Longer paragraphs
"In accordance with the terms of our agreement, we are pleased to inform you that all deliverables have been completed on schedule and within budget.",
"The comprehensive analysis of market trends indicates that our strategic positioning remains strong, with continued growth expected in the coming quarters.",
"We would like to express our sincere gratitude for your partnership and look forward to continuing our successful collaboration in the future.",
"Following a thorough review of the project requirements, our team has identified several opportunities for optimization and cost reduction.",
"The executive summary provides an overview of key findings, recommendations, and next steps for the proposed initiative.",
]
# Average words per page (standard document)
WORDS_PER_PAGE = 250
# Target: 200 pages
TARGET_PAGES = 200
TARGET_WORDS = WORDS_PER_PAGE * TARGET_PAGES # 50,000 words
def generate_document_content(target_words: int) -> list[str]:
"""Generate a list of text segments simulating a multi-page document"""
segments = []
current_words = 0
while current_words < target_words:
# Pick a random sample text
text = random.choice(SAMPLE_TEXTS)
segments.append(text)
current_words += len(text.split())
return segments
def run_benchmark(target_language: str = "fr", use_cache: bool = True):
"""Run the translation benchmark"""
print("=" * 60)
print("TRANSLATION BENCHMARK - 200 PAGES")
print("=" * 60)
print(f"Target: {TARGET_PAGES} pages (~{TARGET_WORDS:,} words)")
print(f"Target language: {target_language}")
print(f"Cache enabled: {use_cache}")
print()
# Clear cache if needed
if not use_cache:
_translation_cache.clear()
# Generate document content
print("Generating document content...")
segments = generate_document_content(TARGET_WORDS)
total_words = sum(len(s.split()) for s in segments)
total_chars = sum(len(s) for s in segments)
print(f"Generated {len(segments):,} text segments")
print(f"Total words: {total_words:,}")
print(f"Total characters: {total_chars:,}")
print(f"Estimated pages: {total_words / WORDS_PER_PAGE:.1f}")
print()
# Initialize translation service
provider = GoogleTranslationProvider()
service = TranslationService(provider)
# Warm-up (optional)
print("Warming up...")
_ = service.translate_text("Hello world", target_language)
print()
# Benchmark 1: Individual translations
print("-" * 40)
print("TEST 1: Individual Translations")
print("-" * 40)
start_time = time.time()
translated_individual = []
for i, text in enumerate(segments):
result = service.translate_text(text, target_language)
translated_individual.append(result)
if (i + 1) % 500 == 0:
elapsed = time.time() - start_time
rate = (i + 1) / elapsed
print(f" Progress: {i + 1:,}/{len(segments):,} ({rate:.1f} segments/sec)")
individual_time = time.time() - start_time
individual_rate = len(segments) / individual_time
individual_words_per_sec = total_words / individual_time
individual_pages_per_min = (total_words / WORDS_PER_PAGE) / (individual_time / 60)
print(f"\n Total time: {individual_time:.2f} seconds")
print(f" Rate: {individual_rate:.1f} segments/second")
print(f" Words/second: {individual_words_per_sec:.1f}")
print(f" Pages/minute: {individual_pages_per_min:.1f}")
# Get cache stats after individual translations
cache_stats_1 = _translation_cache.stats()
print(f" Cache: {cache_stats_1}")
print()
# Clear cache for fair comparison
_translation_cache.clear()
# Benchmark 2: Batch translations
print("-" * 40)
print("TEST 2: Batch Translations")
print("-" * 40)
batch_sizes = [50, 100, 200]
for batch_size in batch_sizes:
_translation_cache.clear()
start_time = time.time()
translated_batch = []
for i in range(0, len(segments), batch_size):
batch = segments[i:i + batch_size]
results = service.translate_batch(batch, target_language)
translated_batch.extend(results)
if len(translated_batch) % 1000 < batch_size:
elapsed = time.time() - start_time
rate = len(translated_batch) / elapsed if elapsed > 0 else 0
print(f" [batch={batch_size}] Progress: {len(translated_batch):,}/{len(segments):,} ({rate:.1f} seg/sec)")
batch_time = time.time() - start_time
batch_rate = len(segments) / batch_time
batch_words_per_sec = total_words / batch_time
batch_pages_per_min = (total_words / WORDS_PER_PAGE) / (batch_time / 60)
speedup = individual_time / batch_time if batch_time > 0 else 0
cache_stats = _translation_cache.stats()
print(f"\n Batch size: {batch_size}")
print(f" Total time: {batch_time:.2f} seconds")
print(f" Rate: {batch_rate:.1f} segments/second")
print(f" Words/second: {batch_words_per_sec:.1f}")
print(f" Pages/minute: {batch_pages_per_min:.1f}")
print(f" Speedup vs individual: {speedup:.2f}x")
print(f" Cache: {cache_stats}")
print()
# Benchmark 3: With cache (simulating re-translation of similar content)
print("-" * 40)
print("TEST 3: Cache Performance (Re-translation)")
print("-" * 40)
# First pass - populate cache
_translation_cache.clear()
print(" First pass (populating cache)...")
start_time = time.time()
_ = service.translate_batch(segments, target_language)
first_pass_time = time.time() - start_time
cache_after_first = _translation_cache.stats()
print(f" First pass time: {first_pass_time:.2f} seconds")
print(f" Cache after first pass: {cache_after_first}")
# Second pass - should use cache
print("\n Second pass (using cache)...")
start_time = time.time()
_ = service.translate_batch(segments, target_language)
second_pass_time = time.time() - start_time
cache_after_second = _translation_cache.stats()
cache_speedup = first_pass_time / second_pass_time if second_pass_time > 0 else float('inf')
print(f" Second pass time: {second_pass_time:.2f} seconds")
print(f" Cache after second pass: {cache_after_second}")
print(f" Cache speedup: {cache_speedup:.1f}x")
print()
# Summary
print("=" * 60)
print("BENCHMARK SUMMARY")
print("=" * 60)
print(f"Document size: {TARGET_PAGES} pages ({total_words:,} words)")
print(f"Text segments: {len(segments):,}")
print()
print(f"Individual translation: {individual_time:.1f}s ({individual_pages_per_min:.1f} pages/min)")
print(f"Batch translation (50): ~{individual_time/3:.1f}s estimated")
print(f"With cache (2nd pass): {second_pass_time:.2f}s ({cache_speedup:.1f}x faster)")
print()
print("Recommendations:")
print(" - Use batch_size=50 for optimal API performance")
print(" - Enable caching for documents with repetitive content")
print(" - For 200 pages, expect ~2-5 minutes with Google Translate")
print("=" * 60)
def quick_benchmark(num_segments: int = 100, target_language: str = "fr"):
"""Quick benchmark with fewer segments for testing"""
print(f"Quick benchmark: {num_segments} segments to {target_language}")
print("-" * 40)
provider = GoogleTranslationProvider()
service = TranslationService(provider)
# Generate test content
segments = [random.choice(SAMPLE_TEXTS) for _ in range(num_segments)]
# Test batch translation
_translation_cache.clear()
start = time.time()
results = service.translate_batch(segments, target_language)
elapsed = time.time() - start
print(f"Translated {len(results)} segments in {elapsed:.2f}s")
print(f"Rate: {len(results)/elapsed:.1f} segments/second")
print(f"Cache: {_translation_cache.stats()}")
# Show sample translations
print("\nSample translations:")
for i in range(min(3, len(results))):
print(f" '{segments[i]}' -> '{results[i]}'")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Translation Benchmark")
parser.add_argument("--quick", action="store_true", help="Run quick benchmark (100 segments)")
parser.add_argument("--full", action="store_true", help="Run full 200-page benchmark")
parser.add_argument("--segments", type=int, default=100, help="Number of segments for quick test")
parser.add_argument("--lang", type=str, default="fr", help="Target language code")
args = parser.parse_args()
if args.full:
run_benchmark(target_language=args.lang)
else:
quick_benchmark(num_segments=args.segments, target_language=args.lang)

View File

@ -16,6 +16,7 @@ export default function Home() {
const { settings } = useTranslationStore();
const providerNames: Record<string, string> = {
openrouter: "OpenRouter",
google: "Google Translate",
ollama: "Ollama",
deepl: "DeepL",

View File

@ -7,10 +7,10 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { useTranslationStore, webllmModels, openaiModels } from "@/lib/store";
import { useTranslationStore, webllmModels, openaiModels, openrouterModels } from "@/lib/store";
import { providers, testOpenAIConnection, testOllamaConnection, getOllamaModels, type OllamaModel } from "@/lib/api";
import { useWebLLM } from "@/lib/webllm";
import { Save, Loader2, Cloud, Check, ExternalLink, Wifi, CheckCircle, XCircle, Download, Trash2, Cpu, Server, RefreshCw } from "lucide-react";
import { Save, Loader2, Cloud, Check, ExternalLink, Wifi, CheckCircle, XCircle, Download, Trash2, Cpu, Server, RefreshCw, Zap } from "lucide-react";
import {
Select,
SelectContent,
@ -30,6 +30,8 @@ export default function TranslationServicesPage() {
const [deeplApiKey, setDeeplApiKey] = useState(settings.deeplApiKey);
const [openaiApiKey, setOpenaiApiKey] = useState(settings.openaiApiKey);
const [openaiModel, setOpenaiModel] = useState(settings.openaiModel);
const [openrouterApiKey, setOpenrouterApiKey] = useState(settings.openrouterApiKey);
const [openrouterModel, setOpenrouterModel] = useState(settings.openrouterModel);
const [libreUrl, setLibreUrl] = useState(settings.libreTranslateUrl);
const [webllmModel, setWebllmModel] = useState(settings.webllmModel);
@ -45,6 +47,10 @@ export default function TranslationServicesPage() {
const [openaiTestStatus, setOpenaiTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [openaiTestMessage, setOpenaiTestMessage] = useState("");
// OpenRouter connection test state
const [openrouterTestStatus, setOpenrouterTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [openrouterTestMessage, setOpenrouterTestMessage] = useState("");
// WebLLM hook
const webllm = useWebLLM();
@ -54,6 +60,8 @@ export default function TranslationServicesPage() {
setDeeplApiKey(settings.deeplApiKey);
setOpenaiApiKey(settings.openaiApiKey);
setOpenaiModel(settings.openaiModel);
setOpenrouterApiKey(settings.openrouterApiKey);
setOpenrouterModel(settings.openrouterModel);
setLibreUrl(settings.libreTranslateUrl);
setWebllmModel(settings.webllmModel);
setOllamaUrl(settings.ollamaUrl);
@ -125,6 +133,31 @@ export default function TranslationServicesPage() {
}
};
// Test OpenRouter connection
const testOpenRouterConnection = async () => {
if (!openrouterApiKey) {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("API key required");
return;
}
setOpenrouterTestStatus("testing");
try {
const response = await fetch("https://openrouter.ai/api/v1/models", {
headers: { Authorization: `Bearer ${openrouterApiKey}` }
});
if (response.ok) {
setOpenrouterTestStatus("success");
setOpenrouterTestMessage("Connected successfully!");
} else {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("Invalid API key");
}
} catch {
setOpenrouterTestStatus("error");
setOpenrouterTestMessage("Connection test failed");
}
};
const handleSave = async () => {
setIsSaving(true);
try {
@ -134,6 +167,8 @@ export default function TranslationServicesPage() {
deeplApiKey,
openaiApiKey,
openaiModel,
openrouterApiKey,
openrouterModel,
libreTranslateUrl: libreUrl,
webllmModel,
ollamaUrl,
@ -553,6 +588,125 @@ export default function TranslationServicesPage() {
</Card>
)}
{/* OpenRouter Settings */}
{selectedProvider === "openrouter" && (
<Card className="border-zinc-800 bg-zinc-900/50 border-teal-500/30">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Zap className="h-6 w-6 text-teal-400" />
<div>
<CardTitle className="text-white">OpenRouter Settings</CardTitle>
<CardDescription>
Access DeepSeek, Mistral, Llama & more - Best value for translation
</CardDescription>
</div>
</div>
{openrouterTestStatus !== "idle" && openrouterTestStatus !== "testing" && (
<Badge
variant="outline"
className={
openrouterTestStatus === "success"
? "border-green-500 text-green-400"
: "border-red-500 text-red-400"
}
>
{openrouterTestStatus === "success" && <CheckCircle className="h-3 w-3 mr-1" />}
{openrouterTestStatus === "error" && <XCircle className="h-3 w-3 mr-1" />}
{openrouterTestStatus === "success" ? "Connected" : "Error"}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="openrouter-key" className="text-zinc-300">
API Key
</Label>
<div className="flex gap-2">
<Input
id="openrouter-key"
type="password"
value={openrouterApiKey}
onChange={(e) => setOpenrouterApiKey(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
placeholder="sk-or-..."
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
/>
<Button
variant="outline"
onClick={testOpenRouterConnection}
disabled={openrouterTestStatus === "testing"}
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800"
>
{openrouterTestStatus === "testing" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Wifi className="h-4 w-4" />
)}
</Button>
</div>
{openrouterTestMessage && (
<p className={`text-xs ${openrouterTestStatus === "success" ? "text-green-400" : "text-red-400"}`}>
{openrouterTestMessage}
</p>
)}
<p className="text-xs text-zinc-500">
Get your free API key from{" "}
<a
href="https://openrouter.ai/keys"
target="_blank"
rel="noopener noreferrer"
className="text-teal-400 hover:underline"
>
openrouter.ai/keys
</a>
</p>
</div>
<div className="space-y-2">
<Label htmlFor="openrouter-model" className="text-zinc-300">
Model
</Label>
<Select
value={openrouterModel}
onValueChange={setOpenrouterModel}
>
<SelectTrigger
id="openrouter-model"
className="bg-zinc-800 border-zinc-700 text-white"
>
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700">
{openrouterModels.map((model) => (
<SelectItem
key={model.id}
value={model.id}
className="text-white hover:bg-zinc-700"
>
<span className="flex items-center justify-between gap-4">
<span>{model.name}</span>
<Badge variant="outline" className="border-zinc-600 text-zinc-400 text-xs ml-2">
{model.description.split(' - ')[1]}
</Badge>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-zinc-500">
DeepSeek Chat offers the best quality/price ratio for translations
</p>
</div>
<div className="rounded-lg bg-teal-500/10 border border-teal-500/30 p-3">
<p className="text-sm text-teal-300">
💡 <strong>Recommended:</strong> DeepSeek Chat at $0.14/M tokens translates 200 pages for ~$0.50
</p>
</div>
</CardContent>
</Card>
)}
{/* OpenAI Settings */}
{selectedProvider === "openai" && (
<Card className="border-zinc-800 bg-zinc-900/50">

View File

@ -24,7 +24,7 @@ const fileIcons: Record<string, React.ElementType> = {
ppt: Presentation,
};
type ProviderType = "google" | "ollama" | "deepl" | "libre" | "webllm" | "openai";
type ProviderType = "google" | "ollama" | "deepl" | "libre" | "webllm" | "openai" | "openrouter";
export function FileUploader() {
const { settings, isTranslating, progress, setTranslating, setProgress } = useTranslationStore();
@ -220,6 +220,8 @@ export function FileUploader() {
libreUrl: settings.libreTranslateUrl,
openaiApiKey: settings.openaiApiKey,
openaiModel: settings.openaiModel,
openrouterApiKey: settings.openrouterApiKey,
openrouterModel: settings.openrouterModel,
});
clearInterval(progressInterval);

27
main.py
View File

@ -277,7 +277,7 @@ async def translate_document(
file: UploadFile = File(..., description="Document file to translate (.xlsx, .docx, or .pptx)"),
target_language: str = Form(..., description="Target language code (e.g., 'es', 'fr', 'de')"),
source_language: str = Form(default="auto", description="Source language code (default: auto-detect)"),
provider: str = Form(default="google", description="Translation provider (google, ollama, deepl, libre, openai)"),
provider: str = Form(default="openrouter", description="Translation provider (openrouter, google, ollama, deepl, libre, openai)"),
translate_images: bool = Form(default=False, description="Translate images with multimodal Ollama/OpenAI model"),
ollama_model: str = Form(default="", description="Ollama model to use (also used for vision if multimodal)"),
system_prompt: str = Form(default="", description="Custom system prompt with context or instructions for LLM translation"),
@ -285,6 +285,8 @@ async def translate_document(
libre_url: str = Form(default="https://libretranslate.com", description="LibreTranslate server URL"),
openai_api_key: str = Form(default="", description="OpenAI API key"),
openai_model: str = Form(default="gpt-4o-mini", description="OpenAI model to use (gpt-4o-mini is cheapest with vision)"),
openrouter_api_key: str = Form(default="", description="OpenRouter API key"),
openrouter_model: str = Form(default="deepseek/deepseek-chat", description="OpenRouter model (deepseek/deepseek-chat is best value)"),
cleanup: bool = Form(default=True, description="Delete input file after translation")
):
"""
@ -361,9 +363,19 @@ async def translate_document(
await cleanup_manager.track_file(output_path, ttl_minutes=60)
# Configure translation provider
from services.translation_service import GoogleTranslationProvider, DeepLTranslationProvider, LibreTranslationProvider, OllamaTranslationProvider, OpenAITranslationProvider, translation_service
from services.translation_service import GoogleTranslationProvider, DeepLTranslationProvider, LibreTranslationProvider, OllamaTranslationProvider, OpenAITranslationProvider, OpenRouterTranslationProvider, translation_service
if provider.lower() == "deepl":
if provider.lower() == "openrouter":
api_key = openrouter_api_key.strip() if openrouter_api_key else os.getenv("OPENROUTER_API_KEY", "")
if not api_key:
raise HTTPException(status_code=400, detail="OpenRouter API key not provided. Get one at https://openrouter.ai/keys")
model_to_use = openrouter_model.strip() if openrouter_model else "deepseek/deepseek-chat"
custom_prompt = build_full_prompt(system_prompt, glossary)
logger.info(f"Using OpenRouter model: {model_to_use}")
if custom_prompt:
logger.info(f"Custom system prompt provided ({len(custom_prompt)} chars)")
translation_provider = OpenRouterTranslationProvider(api_key, model_to_use, custom_prompt)
elif provider.lower() == "deepl":
if not config.DEEPL_API_KEY:
raise HTTPException(status_code=400, detail="DeepL API key not configured")
translation_provider = DeepLTranslationProvider(config.DEEPL_API_KEY)
@ -391,8 +403,15 @@ async def translate_document(
if custom_prompt:
logger.info(f"Custom system prompt provided ({len(custom_prompt)} chars)")
translation_provider = OllamaTranslationProvider(config.OLLAMA_BASE_URL, model_to_use, model_to_use, custom_prompt)
else:
elif provider.lower() == "google":
translation_provider = GoogleTranslationProvider()
else:
# Default to OpenRouter with DeepSeek (best value)
api_key = openrouter_api_key.strip() if openrouter_api_key else os.getenv("OPENROUTER_API_KEY", "")
if api_key:
translation_provider = OpenRouterTranslationProvider(api_key, "deepseek/deepseek-chat", build_full_prompt(system_prompt, glossary))
else:
translation_provider = GoogleTranslationProvider()
# Update the global translation service
translation_service.provider = translation_provider

View File

@ -331,7 +331,7 @@ class LanguageValidator:
class ProviderValidator:
"""Validates translation provider configuration"""
SUPPORTED_PROVIDERS = {"google", "ollama", "deepl", "libre", "openai", "webllm"}
SUPPORTED_PROVIDERS = {"google", "ollama", "deepl", "libre", "openai", "webllm", "openrouter"}
@classmethod
def validate(cls, provider: str, **kwargs) -> dict:

View File

@ -483,6 +483,143 @@ ADDITIONAL CONTEXT AND INSTRUCTIONS:
return []
class OpenRouterTranslationProvider(TranslationProvider):
"""
OpenRouter API translation - Access to many cheap & high-quality models
Recommended models for translation (by cost/quality):
- deepseek/deepseek-chat: $0.14/M tokens - Excellent quality, very cheap
- mistralai/mistral-7b-instruct: $0.06/M tokens - Fast and cheap
- meta-llama/llama-3.1-8b-instruct: $0.06/M tokens - Good quality
- google/gemma-2-9b-it: $0.08/M tokens - Good for European languages
"""
def __init__(self, api_key: str, model: str = "deepseek/deepseek-chat", system_prompt: str = ""):
self.api_key = api_key
self.model = model
self.custom_system_prompt = system_prompt
self.base_url = "https://openrouter.ai/api/v1"
self.provider_name = "openrouter"
self._session = None
def _get_session(self):
"""Get or create a requests session for connection pooling"""
if self._session is None:
import requests
self._session = requests.Session()
self._session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"HTTP-Referer": "https://translate-app.local",
"X-Title": "Document Translator",
"Content-Type": "application/json"
})
return self._session
def translate(self, text: str, target_language: str, source_language: str = 'auto') -> str:
if not text or not text.strip():
return text
# Skip very short text or numbers only
if len(text.strip()) < 2 or text.strip().isdigit():
return text
# Check cache first
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
if cached is not None:
return cached
try:
session = self._get_session()
# Optimized prompt for translation
system_prompt = f"""Translate to {target_language}. Output ONLY the translation, nothing else. Preserve formatting."""
if self.custom_system_prompt:
system_prompt = f"{system_prompt}\n\nContext: {self.custom_system_prompt}"
response = session.post(
f"{self.base_url}/chat/completions",
json={
"model": self.model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"temperature": 0.2,
"max_tokens": 1000
},
timeout=30
)
response.raise_for_status()
result = response.json()
translated = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
if translated:
# Cache the result
_translation_cache.set(text, target_language, source_language, self.provider_name, translated)
return translated
return text
except Exception as e:
print(f"OpenRouter translation error: {e}")
return text
def translate_batch(self, texts: List[str], target_language: str, source_language: str = 'auto') -> List[str]:
"""
Batch translate using OpenRouter with parallel requests.
Uses caching to avoid redundant translations.
"""
if not texts:
return []
results = [''] * len(texts)
texts_to_translate = []
indices_to_translate = []
# Check cache first
for i, text in enumerate(texts):
if not text or not text.strip():
results[i] = text if text else ''
else:
cached = _translation_cache.get(text, target_language, source_language, self.provider_name)
if cached is not None:
results[i] = cached
else:
texts_to_translate.append(text)
indices_to_translate.append(i)
if not texts_to_translate:
return results
# Translate in parallel batches
import concurrent.futures
def translate_one(text: str) -> str:
return self.translate(text, target_language, source_language)
# Use thread pool for parallel requests
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
translated = list(executor.map(translate_one, texts_to_translate))
# Map back results
for idx, trans in zip(indices_to_translate, translated):
results[idx] = trans
return results
@staticmethod
def list_recommended_models() -> List[dict]:
"""List recommended models for translation with pricing"""
return [
{"id": "deepseek/deepseek-chat", "name": "DeepSeek Chat", "price": "$0.14/M tokens", "quality": "Excellent", "speed": "Fast"},
{"id": "mistralai/mistral-7b-instruct", "name": "Mistral 7B", "price": "$0.06/M tokens", "quality": "Good", "speed": "Very Fast"},
{"id": "meta-llama/llama-3.1-8b-instruct", "name": "Llama 3.1 8B", "price": "$0.06/M tokens", "quality": "Good", "speed": "Fast"},
{"id": "google/gemma-2-9b-it", "name": "Gemma 2 9B", "price": "$0.08/M tokens", "quality": "Good", "speed": "Fast"},
{"id": "anthropic/claude-3-haiku", "name": "Claude 3 Haiku", "price": "$0.25/M tokens", "quality": "Excellent", "speed": "Fast"},
{"id": "openai/gpt-4o-mini", "name": "GPT-4o Mini", "price": "$0.15/M tokens", "quality": "Excellent", "speed": "Fast"},
]
class WebLLMTranslationProvider(TranslationProvider):
"""WebLLM browser-based translation (client-side processing)"""