From abe77e3b29ab62b4d9e57ca2af589f2a3bf967cb Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sun, 30 Nov 2025 11:27:13 +0100 Subject: [PATCH] Add Ollama support, progress bar, and professional UI redesign --- .env.example | 6 +- LICENSE | 21 ++ config.py | 4 + main.py | 66 ++++ sample_files/webllm.html | 119 +++++++ services/translation_service.py | 50 ++- static/index.html | 576 ++++++++++++++++++++++++++++++++ 7 files changed, 840 insertions(+), 2 deletions(-) create mode 100644 LICENSE create mode 100644 sample_files/webllm.html create mode 100644 static/index.html diff --git a/.env.example b/.env.example index 2975d2f..4144f29 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,11 @@ # Translation Service Configuration -TRANSLATION_SERVICE=google # Options: google, deepl, libre +TRANSLATION_SERVICE=google # Options: google, deepl, libre, ollama DEEPL_API_KEY=your_deepl_api_key_here +# Ollama Configuration (for LLM-based translation) +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_MODEL=llama3 + # API Configuration MAX_FILE_SIZE_MB=50 UPLOAD_DIR=./uploads diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..891bfff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sepehr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config.py b/config.py index 5aa0ac5..799c597 100644 --- a/config.py +++ b/config.py @@ -12,6 +12,10 @@ class Config: TRANSLATION_SERVICE = os.getenv("TRANSLATION_SERVICE", "google") DEEPL_API_KEY = os.getenv("DEEPL_API_KEY", "") + # Ollama Configuration + OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") + # File Upload Configuration MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", "50")) MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024 diff --git a/main.py b/main.py index 72de639..b1fa303 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ FastAPI application for translating complex documents while preserving formattin from fastapi import FastAPI, UploadFile, File, Form, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles from pathlib import Path from typing import Optional import asyncio @@ -37,6 +38,11 @@ app.add_middleware( allow_headers=["*"], ) +# Mount static files +static_dir = Path(__file__).parent / "static" +if static_dir.exists(): + app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") + @app.get("/") async def root(): @@ -104,6 +110,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)"), cleanup: bool = Form(default=True, description="Delete input file after translation") ): """ @@ -145,6 +152,24 @@ async def translate_document( await file_handler.save_upload_file(file, input_path) logger.info(f"Saved input file to: {input_path}") + # Configure translation provider + from services.translation_service import TranslationService, GoogleTranslationProvider, DeepLTranslationProvider, LibreTranslationProvider, OllamaTranslationProvider + + if 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) + elif provider.lower() == "libre": + translation_provider = LibreTranslationProvider() + elif provider.lower() == "ollama": + translation_provider = OllamaTranslationProvider(config.OLLAMA_BASE_URL, config.OLLAMA_MODEL) + else: + translation_provider = GoogleTranslationProvider() + + # Update the global translation service + from services import translation_service as ts_module + ts_module.translation_service.provider = translation_provider + # Translate based on file type if file_extension == ".xlsx": logger.info("Translating Excel file...") @@ -302,6 +327,47 @@ async def download_file(filename: str): ) +@app.get("/ollama/models") +async def list_ollama_models(base_url: Optional[str] = None): + """ + List available Ollama models + + **Parameters:** + - **base_url**: Ollama server URL (default: from config) + """ + from services.translation_service import OllamaTranslationProvider + + url = base_url or config.OLLAMA_BASE_URL + models = OllamaTranslationProvider.list_models(url) + + return { + "ollama_url": url, + "models": models, + "count": len(models) + } + + +@app.post("/ollama/configure") +async def configure_ollama(base_url: str = Form(...), model: str = Form(...)): + """ + Configure Ollama settings + + **Parameters:** + - **base_url**: Ollama server URL (e.g., http://localhost:11434) + - **model**: Model name to use for translation (e.g., llama3, mistral) + """ + config.OLLAMA_BASE_URL = base_url + config.OLLAMA_MODEL = model + + return { + "status": "success", + "message": "Ollama configuration updated", + "ollama_url": base_url, + "model": model + } + + if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) + diff --git a/sample_files/webllm.html b/sample_files/webllm.html new file mode 100644 index 0000000..5c969e0 --- /dev/null +++ b/sample_files/webllm.html @@ -0,0 +1,119 @@ + + + + + + Test LLM Local - WebGPU + + + + +

🤖 Mon LLM Local (via Chrome WebGPU)

+ +
Initialisation... cliquez sur "Charger le modèle" pour commencer.
+ +
+ +
+ + +
+ + + + + + \ No newline at end of file diff --git a/services/translation_service.py b/services/translation_service.py index d775bb5..e46f379 100644 --- a/services/translation_service.py +++ b/services/translation_service.py @@ -3,7 +3,8 @@ Translation Service Abstraction Provides a unified interface for different translation providers """ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, List +import requests from deep_translator import GoogleTranslator, DeeplTranslator, LibreTranslator from config import config @@ -65,6 +66,49 @@ class LibreTranslationProvider(TranslationProvider): return text +class OllamaTranslationProvider(TranslationProvider): + """Ollama LLM translation implementation""" + + def __init__(self, base_url: str = "http://localhost:11434", model: str = "llama3"): + self.base_url = base_url.rstrip('/') + self.model = 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 + + @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 TranslationService: """Main translation service that delegates to the configured provider""" @@ -85,6 +129,10 @@ class TranslationService: return DeepLTranslationProvider(config.DEEPL_API_KEY) elif service_type == "libre": return LibreTranslationProvider() + elif service_type == "ollama": + ollama_url = getattr(config, 'OLLAMA_BASE_URL', 'http://localhost:11434') + ollama_model = getattr(config, 'OLLAMA_MODEL', 'llama3') + return OllamaTranslationProvider(base_url=ollama_url, model=ollama_model) else: # Default to Google return GoogleTranslationProvider() diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a5b67fb --- /dev/null +++ b/static/index.html @@ -0,0 +1,576 @@ + + + + + + Document Translation API - Interface de Test + + + +
+
+

Document Translation API

+

Professional document translation service with format preservation

+
+ + +
+

Ollama Configuration

+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+

Document Translation

+
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ + + +
+
+

Translation in progress, please wait...

+
+ +
+
+
+
+ +
+
+ + +
+

API Health Check

+ +
+
+
+ + + +