|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
STATIC_DIR = "static" |
|
TRANSLATED_IMAGE_DIR = os.path.join(STATIC_DIR, "translated") |
|
FONT_PATH = "font/Movistar Text Regular.ttf" |
|
|
|
|
|
os.makedirs(TRANSLATED_IMAGE_DIR, exist_ok=True) |
|
|
|
|
|
app = FastAPI( |
|
title="Manga OCR Translator API", |
|
description="API for translating manga images using OCR and machine translation", |
|
version="1.0.0", |
|
) |
|
|
|
|
|
app.add_middleware( |
|
CORSMiddleware, |
|
allow_origins=["*"], |
|
allow_credentials=True, |
|
allow_methods=["*"], |
|
allow_headers=["*"], |
|
) |
|
|
|
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") |
|
|
|
|
|
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] = [] |
|
|
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
async def root(): |
|
|
|
return FileResponse("static/ui/index.html") |
|
|
|
|
|
@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" |
|
} |
|
} |
|
|
|
|
|
@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}") |
|
|
|
|
|
async def process_images(): |
|
try: |
|
|
|
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") |
|
|
|
|
|
if len(image_urls) > 5: |
|
print("Limiting to first 5 images") |
|
image_urls = image_urls[:5] |
|
|
|
|
|
for i, image_url in enumerate(image_urls): |
|
try: |
|
|
|
print(f"Processing image {i+1}/{len(image_urls)}: {image_url}") |
|
|
|
|
|
yield f"data: {{'status': 'processing', 'message': 'Processing image {i+1}/{len(image_urls)}', 'image_url': '{image_url}'}}\n\n" |
|
|
|
|
|
image_content = await download_image(image_url) |
|
if not image_content: |
|
print(f"Failed to download image {i+1}") |
|
continue |
|
|
|
|
|
text_regions = detect_text(image_content, request.src_lang) |
|
if not text_regions: |
|
print(f"No text detected in image {i+1}") |
|
continue |
|
|
|
|
|
grouped_regions = group_text_regions(text_regions) |
|
if not grouped_regions: |
|
print(f"No text groups formed in image {i+1}") |
|
continue |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
translated_image = overlay_grouped_text(image_content, translated_regions) |
|
|
|
|
|
image_path = save_image(translated_image, TRANSLATED_IMAGE_DIR) |
|
|
|
|
|
image_url = f"/static/translated/{os.path.basename(image_path)}" |
|
|
|
|
|
json_response = { |
|
"status": "success", |
|
"message": f"Processed image {i+1}/{len(image_urls)}", |
|
"image_url": image_url |
|
} |
|
|
|
|
|
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" |
|
|
|
|
|
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 StreamingResponse( |
|
process_images(), |
|
media_type="text/event-stream", |
|
headers={ |
|
"Cache-Control": "no-cache", |
|
"Connection": "keep-alive", |
|
"X-Accel-Buffering": "no" |
|
} |
|
) |
|
|
|
|
|
@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") |
|
|
|
|
|
async def process_pdf(): |
|
try: |
|
|
|
pdf_content = await file.read() |
|
|
|
|
|
yield f"data: {{'status': 'processing', 'message': 'Converting PDF to images...'}}\n\n" |
|
|
|
|
|
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") |
|
|
|
|
|
if len(pdf_images) > 5: |
|
print("Limiting to first 5 pages") |
|
pdf_images = pdf_images[:5] |
|
|
|
|
|
for i, image_content in enumerate(pdf_images): |
|
try: |
|
|
|
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" |
|
|
|
|
|
text_regions = detect_text(image_content, src_lang) |
|
if not text_regions: |
|
print(f"No text detected in PDF page {i+1}") |
|
continue |
|
|
|
|
|
grouped_regions = group_text_regions(text_regions) |
|
if not grouped_regions: |
|
print(f"No text groups formed in PDF page {i+1}") |
|
continue |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
pil_image = Image.open(io.BytesIO(image_content)) |
|
translated_image = overlay_grouped_text(image_content, translated_regions) |
|
|
|
|
|
image_path = save_image(translated_image, TRANSLATED_IMAGE_DIR) |
|
|
|
|
|
image_url = f"/static/translated/{os.path.basename(image_path)}" |
|
|
|
|
|
json_response = { |
|
"status": "success", |
|
"message": f"Processed PDF page {i+1}/{len(pdf_images)}", |
|
"image_url": image_url |
|
} |
|
|
|
|
|
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" |
|
|
|
|
|
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 StreamingResponse( |
|
process_pdf(), |
|
media_type="text/event-stream", |
|
headers={ |
|
"Cache-Control": "no-cache", |
|
"Connection": "keep-alive", |
|
"X-Accel-Buffering": "no" |
|
} |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
print("Starting Manga OCR Translator API server...") |
|
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) |