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 @@ + + +
+ + +Professional document translation service with format preservation
+Translation in progress, please wait...
+