import os import io import uuid import asyncio import aiohttp import uvicorn from typing import List, Dict, Any, Optional, Generator from fastapi import FastAPI, UploadFile, File, Form, Query, BackgroundTasks, Request from fastapi.responses import StreamingResponse, JSONResponse, FileResponse, HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field, HttpUrl import time from PIL import Image # Import utility functions from utils.ocr import detect_text, group_text_regions from utils.web import scrape_comic_images, download_image from utils.pdf import pdf_to_images, pdf_stream_to_images from utils.image import overlay_grouped_text, save_image from utils.translation import translate_grouped_regions # Configuration STATIC_DIR = "static" TRANSLATED_IMAGE_DIR = os.path.join(STATIC_DIR, "translated") FONT_PATH = "font/Movistar Text Regular.ttf" # Ensure directories exist os.makedirs(TRANSLATED_IMAGE_DIR, exist_ok=True) # Initialize FastAPI app app = FastAPI( title="Manga OCR Translator API", description="API for translating manga images using OCR and machine translation", version="1.0.0", ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # Allow all origins allow_credentials=True, allow_methods=["*"], # Allow all methods allow_headers=["*"], # Allow all headers ) # Mount static files directory app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") # Define request and response models class TranslationRequest(BaseModel): url: HttpUrl = Field(..., description="URL of the manga chapter to translate") src_lang: str = Field(default="auto", description="Source language (auto, ja, ko, zh)") tgt_lang: str = Field(default="en", description="Target language (en, es, fr, de, it, pt, ru)") translator: str = Field(default="google", description="Translation engine (google, mymemory, linguee, pollinations)") class TranslationResponse(BaseModel): status: str message: str images: List[str] = [] # Basic homepage route - now serves our UI @app.get("/", response_class=HTMLResponse) async def root(): # Serve the UI instead of the API response return FileResponse("static/ui/index.html") # API info endpoint @app.get("/api/info") async def api_info(): return { "message": "Manga OCR Translator API", "endpoints": { "/translate/url": "Translate manga from a URL", "/translate/pdf": "Translate manga from a PDF file", "/docs": "API documentation" } } # Route for processing manga URL with streaming response @app.post("/translate/url") async def translate_manga_url(request: TranslationRequest): """ Process a manga URL and return translated images with streaming response. Each image is processed and returned as soon as it's ready. """ print(f"Received request to translate URL: {request.url}") # Create a generator function that yields translated images async def process_images(): try: # Scrape image URLs from the manga page image_urls = scrape_comic_images(str(request.url)) if not image_urls: yield f"data: {{'status': 'error', 'message': 'No images found at the URL', 'images': []}}\n\n" return print(f"Found {len(image_urls)} images to process") # Limit to first 5 images if too many if len(image_urls) > 5: print("Limiting to first 5 images") image_urls = image_urls[:5] # Process each image for i, image_url in enumerate(image_urls): try: # Download image print(f"Processing image {i+1}/{len(image_urls)}: {image_url}") # Update client with status yield f"data: {{'status': 'processing', 'message': 'Processing image {i+1}/{len(image_urls)}', 'image_url': '{image_url}'}}\n\n" # Download the image image_content = await download_image(image_url) if not image_content: print(f"Failed to download image {i+1}") continue # Detect text regions text_regions = detect_text(image_content, request.src_lang) if not text_regions: print(f"No text detected in image {i+1}") continue # Group text regions grouped_regions = group_text_regions(text_regions) if not grouped_regions: print(f"No text groups formed in image {i+1}") continue # Translate grouped regions use_pollinations = request.translator == "pollinations" free_translator = request.translator if not use_pollinations else "google" translated_regions = translate_grouped_regions( grouped_regions, request.src_lang, request.tgt_lang, use_pollinations, free_translator ) # Overlay translated text on image translated_image = overlay_grouped_text(image_content, translated_regions) # Save image and get path image_path = save_image(translated_image, TRANSLATED_IMAGE_DIR) # Create a URL to the saved image image_url = f"/static/translated/{os.path.basename(image_path)}" # Stream the result back to the client json_response = { "status": "success", "message": f"Processed image {i+1}/{len(image_urls)}", "image_url": image_url } # Send this single image result yield f"data: {json_response}\n\n" except Exception as e: print(f"Error processing image {i+1}: {e}") yield f"data: {{'status': 'error', 'message': 'Error processing image {i+1}: {str(e)}'}}\n\n" # Final message yield f"data: {{'status': 'complete', 'message': 'All images processed'}}\n\n" except Exception as e: print(f"Error in process_images: {e}") yield f"data: {{'status': 'error', 'message': 'Error: {str(e)}'}}\n\n" # Return a streaming response return StreamingResponse( process_images(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" # Disable buffering for Nginx } ) # Route for processing PDF file with streaming response @app.post("/translate/pdf") async def translate_manga_pdf( file: UploadFile = File(...), src_lang: str = Form("auto"), tgt_lang: str = Form("en"), translator: str = Form("google") ): """ Process a manga PDF file and return translated images with streaming response. Each image is processed and returned as soon as it's ready. """ print(f"Received PDF file: {file.filename}, size: {file.size} bytes") # Create a generator function that yields translated images async def process_pdf(): try: # Read the PDF file pdf_content = await file.read() # Convert PDF to images yield f"data: {{'status': 'processing', 'message': 'Converting PDF to images...'}}\n\n" # Convert PDF to images in memory pdf_images = await pdf_stream_to_images(pdf_content) if not pdf_images: yield f"data: {{'status': 'error', 'message': 'Failed to extract images from PDF', 'images': []}}\n\n" return print(f"Extracted {len(pdf_images)} pages from PDF") # Limit to first 5 images if too many if len(pdf_images) > 5: print("Limiting to first 5 pages") pdf_images = pdf_images[:5] # Process each image for i, image_content in enumerate(pdf_images): try: # Update client with status print(f"Processing PDF page {i+1}/{len(pdf_images)}") yield f"data: {{'status': 'processing', 'message': 'Processing PDF page {i+1}/{len(pdf_images)}'}}\n\n" # Detect text regions text_regions = detect_text(image_content, src_lang) if not text_regions: print(f"No text detected in PDF page {i+1}") continue # Group text regions grouped_regions = group_text_regions(text_regions) if not grouped_regions: print(f"No text groups formed in PDF page {i+1}") continue # Translate grouped regions use_pollinations = translator == "pollinations" free_translator = translator if not use_pollinations else "google" translated_regions = translate_grouped_regions( grouped_regions, src_lang, tgt_lang, use_pollinations, free_translator ) # Overlay translated text on image pil_image = Image.open(io.BytesIO(image_content)) translated_image = overlay_grouped_text(image_content, translated_regions) # Save image and get path image_path = save_image(translated_image, TRANSLATED_IMAGE_DIR) # Create a URL to the saved image image_url = f"/static/translated/{os.path.basename(image_path)}" # Stream the result back to the client json_response = { "status": "success", "message": f"Processed PDF page {i+1}/{len(pdf_images)}", "image_url": image_url } # Send this single image result yield f"data: {json_response}\n\n" except Exception as e: print(f"Error processing PDF page {i+1}: {e}") yield f"data: {{'status': 'error', 'message': 'Error processing PDF page {i+1}: {str(e)}'}}\n\n" # Final message yield f"data: {{'status': 'complete', 'message': 'All PDF pages processed'}}\n\n" except Exception as e: print(f"Error in process_pdf: {e}") yield f"data: {{'status': 'error', 'message': 'Error: {str(e)}'}}\n\n" # Return a streaming response return StreamingResponse( process_pdf(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" # Disable buffering for Nginx } ) # Main entry point if __name__ == "__main__": print("Starting Manga OCR Translator API server...") uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)