Guest User

Server_Manga_Translator.py

a guest
Oct 13th, 2025
237
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.30 KB | Source Code | 0 0
  1. """
  2. Manga Translation Server v1.0: YOLO for bubble detection + MangaOCR + mBART for Translation + Colorization - Usage:
  3. pip install fastapi uvicorn ultralytics manga-ocr transformers torch torchvision networkx opencv-python
  4. python Server_Manga_Translator.py
  5. """
  6. import os, io, base64, time, requests, logging
  7. import numpy as np, cv2, torch, torch.nn as nn, torch.nn.functional as F
  8. from PIL import Image
  9. from fastapi import FastAPI, Body
  10. from fastapi.middleware.cors import CORSMiddleware
  11. from fastapi.responses import JSONResponse
  12. from ultralytics import YOLO
  13. from manga_ocr import MangaOcr
  14. from transformers import MBartForConditionalGeneration, MBart50TokenizerFast
  15. from torchvision.transforms import ToTensor
  16. from collections import OrderedDict
  17.  
  18. logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(message)s', datefmt='%H:%M:%S')
  19. log = logging.getLogger(__name__)
  20. DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  21. MODEL_DIR = "./models"; COLOR_DIR = os.path.join(MODEL_DIR, "colorization")
  22. os.makedirs(COLOR_DIR, exist_ok=True)
  23. log.info(f"🚀 {DEVICE}" + (f" ({torch.cuda.get_device_name(0)}, {torch.cuda.get_device_properties(0).total_memory/1e9:.1f}GB)" if torch.cuda.is_available() else ""))
  24.  
  25. YOLO_URL="https://huggingface.co/Kirogii/Yolo-Manga_Textbox-Region_Detect/resolve/main/model.pt"
  26. GEN_URL="https://github.com/zyddnys/manga-image-translator/releases/download/beta-0.3/manga-colorization-v2-generator.zip"
  27. DEN_URL="https://github.com/zyddnys/manga-image-translator/releases/download/beta-0.3/manga-colorization-v2-net_rgb.pth"
  28. YOLO_PATH=os.path.join(MODEL_DIR,"yolo.pt")
  29. GEN_PATH=os.path.join(COLOR_DIR,"generator.zip")
  30. DEN_PATH=os.path.join(COLOR_DIR,"net_rgb.pth")
  31.  
  32. def dl(url, path):
  33.     if os.path.exists(path): return
  34.     with requests.get(url, stream=True) as r:
  35.         r.raise_for_status()
  36.         with open(path, 'wb') as f:
  37.             for c in r.iter_content(8192):
  38.                 if c: f.write(c)
  39.  
  40. def ensure(url, path, name):
  41.     if os.path.exists(path): log.info(f"✓ {name} exists")
  42.     else: dl(url, path); log.info(f"↓ {name} downloaded")
  43.  
  44. ensure(YOLO_URL, YOLO_PATH, "YOLO")
  45. ensure(GEN_URL,  GEN_PATH,  "Generator")
  46. ensure(DEN_URL,  DEN_PATH,  "Denoiser")
  47.  
  48. yolo = YOLO(YOLO_PATH);
  49. if torch.cuda.is_available(): yolo.to(DEVICE)
  50. log.info("✓ YOLO loaded")
  51. ocr_model = MangaOcr(); log.info("✓ MangaOCR loaded")
  52. mbart_tok = MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")
  53. mbart_model = MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-many-mmt").to(DEVICE)
  54. mbart_model.eval(); mbart_tok.src_lang = "ja_XX"; log.info("✓ mBART loaded")
  55.  
  56. def detect_boxes(im, conf=0.25, iou=0.8, size=640):
  57.     t=time.time()
  58.     res=yolo.predict(im,verbose=False,conf=conf,iou=iou,imgsz=size,device=DEVICE,half=torch.cuda.is_available())[0]
  59.     boxes=[[int(x) for x in b.xyxy[0].cpu().tolist()] for b in res.boxes if (b.xyxy[0][2]-b.xyxy[0][0])>20 and (b.xyxy[0][3]-b.xyxy[0][1])>10]
  60.     dt=time.time()-t; log.info(f"🧭 detect boxes={len(boxes)} in {dt:.3f}s")
  61.     return boxes,dt
  62.  
  63. def ocr_text(im,box):
  64.     x1,y1,x2,y2=box
  65.     try: return ocr_model(Image.fromarray(im[y1:y2,x1:x2])).strip()
  66.     except: return ""
  67.  
  68. def translate_batch(texts):
  69.     if not texts: return []
  70.     inp=mbart_tok(texts,return_tensors="pt",padding=True,truncation=True,max_length=256).to(DEVICE)
  71.     with torch.no_grad():
  72.         gen=mbart_model.generate(**inp,forced_bos_token_id=mbart_tok.lang_code_to_id["en_XX"],max_length=128,num_beams=3,early_stopping=True)
  73.     return mbart_tok.batch_decode(gen,skip_special_tokens=True)
  74.  
  75. _COLOR={'gen':None,'den':None}
  76. def colorize_image(im, denoise=15):
  77.     dev='cuda' if torch.cuda.is_available() else 'cpu'
  78.     if _COLOR['gen'] is None:
  79.         class Selayer(nn.Module):
  80.             def __init__(self,c): super().__init__(); self.global_avgpool=nn.AdaptiveAvgPool2d(1); self.conv1=nn.Conv2d(c,c//16,1,1); self.conv2=nn.Conv2d(c//16,c,1,1); self.relu=nn.ReLU(True); self.sigmoid=nn.Sigmoid()
  81.             def forward(self,x): y=self.global_avgpool(x); y=self.conv1(y); y=self.relu(y); y=self.conv2(y); y=self.sigmoid(y); return x*y
  82.         class BottleneckX_Origin(nn.Module):
  83.             expansion=4
  84.             def __init__(self,inp,p,card,s=1,down=None):
  85.                 super().__init__(); self.conv1=nn.Conv2d(inp,p*2,1,bias=False); self.bn1=nn.BatchNorm2d(p*2); self.conv2=nn.Conv2d(p*2,p*2,3,s,1,groups=card,bias=False); self.bn2=nn.BatchNorm2d(p*2); self.conv3=nn.Conv2d(p*2,p*4,1,bias=False); self.bn3=nn.BatchNorm2d(p*4); self.selayer=Selayer(p*4); self.relu=nn.ReLU(True); self.downsample=down
  86.             def forward(self,x): r=x; y=self.conv1(x); y=self.bn1(y); y=self.relu(y); y=self.conv2(y); y=self.bn2(y); y=self.relu(y); y=self.conv3(y); y=self.bn3(y); y=self.selayer(y); r=self.downsample(x) if self.downsample else r; return self.relu(y+r)
  87.         class SEResNeXt_Origin(nn.Module):
  88.             def __init__(self,blk,layers,input_channels=3,cardinality=32):
  89.                 super().__init__(); self.cardinality=cardinality; self.inplanes=64
  90.                 self.conv1=nn.Conv2d(input_channels,64,7,2,3,bias=False); self.bn1=nn.BatchNorm2d(64); self.relu=nn.ReLU(True)
  91.                 self.layer1=self._mk(blk,64,layers[0]); self.layer2=self._mk(blk,128,layers[1],2); self.layer3=self._mk(blk,256,layers[2],2)
  92.             def _mk(self,blk,p,n,s=1):
  93.                 down=None
  94.                 if s!=1 or self.inplanes!=p*blk.expansion: down=nn.Sequential(nn.Conv2d(self.inplanes,p*blk.expansion,1,s,bias=False),nn.BatchNorm2d(p*blk.expansion))
  95.                 L=[blk(self.inplanes,p,self.cardinality,s,down)]; self.inplanes=p*blk.expansion
  96.                 for _ in range(1,n): L.append(blk(self.inplanes,p,self.cardinality))
  97.                 return nn.Sequential(*L)
  98.             def forward(self,x): x=self.conv1(x); x=self.bn1(x); x1=self.relu(x); x2=self.layer1(x1); x3=self.layer2(x2); x4=self.layer3(x3); return x1,x2,x3,x4
  99.         class ResNeXtBottleneck(nn.Module):
  100.             def __init__(self,c,s=1,card=32,d=1):
  101.                 super().__init__(); D=c//2
  102.                 self.conv_reduce=nn.Conv2d(c,D,1,bias=False); self.conv_conv=nn.Conv2d(D,D,2+s,s,d,dilation=d,groups=card,bias=False); self.conv_expand=nn.Conv2d(D,c,1,bias=False); self.shortcut=nn.AvgPool2d(2,2) if s!=1 else nn.Identity(); self.selayer=Selayer(c)
  103.             def forward(self,x): b=self.conv_reduce(x); b=F.leaky_relu(b,0.2,True); b=self.conv_conv(b); b=F.leaky_relu(b,0.2,True); b=self.conv_expand(b); b=self.selayer(b); return self.shortcut(x)+b
  104.         class Generator(nn.Module):
  105.             def __init__(self):
  106.                 super().__init__(); self.encoder=SEResNeXt_Origin(BottleneckX_Origin,[3,4,6,3],input_channels=1)
  107.                 C=lambda i,o,s:nn.Sequential(nn.Conv2d(i,o,3,s,1),nn.LeakyReLU(0.2),nn.Conv2d(o,o,3,1,1),nn.LeakyReLU(0.2))
  108.                 self.to0=C(5,32,1); self.to1=C(32,64,2); self.to2=C(64,92,2); self.to3=C(92,128,2); self.to4=C(128,256,2)
  109.                 self.deconv_for_decoder=nn.Sequential(nn.ConvTranspose2d(256,128,3,2,1,1),nn.LeakyReLU(0.2),nn.ConvTranspose2d(128,64,3,2,1,1),nn.LeakyReLU(0.2),nn.ConvTranspose2d(64,32,3,1,1,0),nn.LeakyReLU(0.2),nn.ConvTranspose2d(32,3,3,1,1,0),nn.Tanh())
  110.                 t4=nn.Sequential(*[ResNeXtBottleneck(512,card=32,d=1) for _ in range(20)])
  111.                 self.tunnel4=nn.Sequential(nn.Conv2d(1024+128,512,3,1,1),nn.LeakyReLU(0.2,True),t4,nn.Conv2d(512,1024,3,1,1),nn.PixelShuffle(2),nn.LeakyReLU(0.2,True))
  112.                 depth=2
  113.                 t3=nn.Sequential(*([ResNeXtBottleneck(256,card=32,d=1) for _ in range(depth)]+[ResNeXtBottleneck(256,card=32,d=2) for _ in range(depth)]+[ResNeXtBottleneck(256,card=32,d=4) for _ in range(depth)]+[ResNeXtBottleneck(256,card=32,d=2),ResNeXtBottleneck(256,card=32,d=1)]))
  114.                 self.tunnel3=nn.Sequential(nn.Conv2d(512+256,256,3,1,1),nn.LeakyReLU(0.2,True),t3,nn.Conv2d(256,512,3,1,1),nn.PixelShuffle(2),nn.LeakyReLU(0.2,True))
  115.                 t2=nn.Sequential(*([ResNeXtBottleneck(128,card=32,d=1) for _ in range(depth)]+[ResNeXtBottleneck(128,card=32,d=2) for _ in range(depth)]+[ResNeXtBottleneck(128,card=32,d=4) for _ in range(depth)]+[ResNeXtBottleneck(128,card=32,d=2),ResNeXtBottleneck(128,card=32,d=1)]))
  116.                 self.tunnel2=nn.Sequential(nn.Conv2d(128+256+64,128,3,1,1),nn.LeakyReLU(0.2,True),t2,nn.Conv2d(128,256,3,1,1),nn.PixelShuffle(2),nn.LeakyReLU(0.2,True))
  117.                 t1=nn.Sequential(*[ResNeXtBottleneck(64,card=16,d=1),ResNeXtBottleneck(64,card=16,d=2),ResNeXtBottleneck(64,card=16,d=4),ResNeXtBottleneck(64,card=16,d=2),ResNeXtBottleneck(64,card=16,d=1)])
  118.                 self.tunnel1=nn.Sequential(nn.Conv2d(64+32,64,3,1,1),nn.LeakyReLU(0.2,True),t1,nn.Conv2d(64,128,3,1,1),nn.PixelShuffle(2),nn.LeakyReLU(0.2,True))
  119.                 self.exit=nn.Sequential(nn.Conv2d(64+32,32,3,1,1),nn.LeakyReLU(0.2,True),nn.Conv2d(32,3,1,1,0))
  120.             def forward(self,sk):
  121.                 x0=self.to0(sk); aux=self.to1(x0); aux=self.to2(aux); aux=self.to3(aux)
  122.                 x1,x2,x3,x4=self.encoder(sk[:,0:1])
  123.                 out=self.tunnel4(torch.cat([x4,aux],1))
  124.                 x=self.tunnel3(torch.cat([out,x3],1)); x=self.tunnel2(torch.cat([x,x2,x1],1))
  125.                 return torch.tanh(self.exit(torch.cat([x,x0],1)))
  126.         def cat_nm(x, ns):
  127.             N,C,H,W=x.size(); ds=torch.zeros((N,4*C,H//2,W//2),device=x.device,dtype=x.dtype)
  128.             nm=ns.view(N,1,1,1).repeat(1,C,H//2,W//2); idx=((0,0),(0,1),(1,0),(1,1))
  129.             for i,(a,b) in enumerate(idx): ds[:,i:4*C:4]=x[:,:,a::2,b::2]
  130.             return torch.cat((nm,ds),1)
  131.         class IntermediateDnCNN(nn.Module):
  132.             def __init__(self,inp,mid,n):
  133.                 super().__init__(); out=4 if inp==5 else 12
  134.                 L=[nn.Conv2d(inp,mid,3,1,1,bias=False),nn.ReLU(True)]
  135.                 for _ in range(n-2): L+=[nn.Conv2d(mid,mid,3,1,1,bias=False),nn.BatchNorm2d(mid),nn.ReLU(True)]
  136.                 L.append(nn.Conv2d(mid,out,3,1,1,bias=False)); self.itermediate_dncnn=nn.Sequential(*L)
  137.             def forward(self,x): return self.itermediate_dncnn(x)
  138.         class FFDNet(nn.Module):
  139.             def __init__(self,ch):
  140.                 super().__init__();
  141.                 if ch==1: nf,nl,dc=64,15,5
  142.                 elif ch==3: nf,nl,dc=96,12,15
  143.                 else: raise Exception('bad ch')
  144.                 self.intermediate_dncnn=IntermediateDnCNN(dc,nf,nl)
  145.             def forward(self,x,ns):
  146.                 h=self.intermediate_dncnn(cat_nm(x.data,ns.data))
  147.                 N,Ci,Hi,Wi=h.size(); out=torch.zeros((N,Ci//4,Hi*2,Wi*2),device=h.device,dtype=h.dtype); idx=((0,0),(0,1),(1,0),(1,1))
  148.                 for i,(a,b) in enumerate(idx): out[:,:,a::2,b::2]=h[:,i:Ci:4]
  149.                 return out
  150.         def resize_pad(img,size=256):
  151.             if len(img.shape)==2: img=np.expand_dims(img,2)
  152.             if img.shape[2]==1: img=np.repeat(img,3,2)
  153.             if img.shape[2]==4: img=img[:,:,:3]
  154.             if img.shape[0]<img.shape[1]:
  155.                 r=img.shape[0]/(size*1.5); w=int(np.ceil(img.shape[1]/r)); img=cv2.resize(img,(w,int(size*1.5)),cv2.INTER_AREA); pad=(0, w+(32-w%32)-w); img=np.pad(img,((0,0),(0,pad[1]),(0,0)),'maximum')
  156.             else:
  157.                 r=img.shape[1]/size; h=int(np.ceil(img.shape[0]/r)); img=cv2.resize(img,(size,h),cv2.INTER_AREA); pad=(h+(32-h%32)-h, 0); img=np.pad(img,((0,pad[0]),(0,0),(0,0)),'maximum')
  158.             return img[:,:,:1], pad
  159.  
  160.         gen=Generator().to(dev); gsd=torch.load(GEN_PATH,map_location=dev)
  161.         if isinstance(gsd,dict) and 'state_dict' in gsd: gsd=gsd['state_dict']
  162.         if any(k.startswith('generator.') for k in gsd): gsd={k[len('generator.'):]:v for k,v in gsd.items() if k.startswith('generator.')}
  163.         gen.load_state_dict(gsd, strict=True); gen.eval()
  164.         den=FFDNet(3); dsd=torch.load(DEN_PATH,map_location=dev)
  165.         dsd=OrderedDict((k[7:] if k.startswith('module.') else k,v) for k,v in dsd.items()); den.load_state_dict(dsd); den.eval().to(dev)
  166.         _COLOR['gen'],_COLOR['den']=gen,den
  167.  
  168.     gen,den=_COLOR['gen'],_COLOR['den']
  169.     sz=min(min(im.shape[:2])//32*32,576)
  170.     if 0<=denoise<=255:
  171.         s=denoise/255; x=im
  172.         if x.ndim<3 or x.shape[2]==1: x=np.repeat(np.expand_dims(x,2),3,2)
  173.         x=x[...,:3]
  174.         if max(x.shape[:2])>1200: r=max(x.shape[:2])/1200; x=cv2.resize(x,(int(x.shape[1]/r),int(x.shape[0]/r)),cv2.INTER_AREA)
  175.         x=x.transpose(2,0,1); x=x/255.0 if x.max()>1.2 else x; x=np.expand_dims(x,0)
  176.         eh,ew=x.shape[2]%2==1,x.shape[3]%2==1
  177.         if eh: x=np.concatenate((x,x[:,:,-1:,:]),2)
  178.         if ew: x=np.concatenate((x,x[:,:,:,-1:]),3)
  179.         xt=torch.tensor(x,device=dev,dtype=torch.float32); ns=torch.tensor([s],device=dev,dtype=torch.float32)
  180.         with torch.no_grad(): out=torch.clamp(xt-den(xt,ns),0,1)
  181.         if eh: out=out[:,:,:-1,:]
  182.         if ew: out=out[:,:,:,:-1]
  183.         im=(out.permute(0,2,3,1).cpu().numpy()[0]*255).astype(np.uint8)
  184.     def resize_pad(img,size=256):
  185.         if len(img.shape)==2: img=np.expand_dims(img,2)
  186.         if img.shape[2]==1: img=np.repeat(img,3,2)
  187.         if img.shape[2]==4: img=img[:,:,:3]
  188.         if img.shape[0]<img.shape[1]:
  189.             r=img.shape[0]/(size*1.5); w=int(np.ceil(img.shape[1]/r)); img=cv2.resize(img,(w,int(size*1.5)),cv2.INTER_AREA); pad=(0, w+(32-w%32)-w); img=np.pad(img,((0,0),(0,pad[1]),(0,0)),'maximum')
  190.         else:
  191.             r=img.shape[1]/size; h=int(np.ceil(img.shape[0]/r)); img=cv2.resize(img,(size,h),cv2.INTER_AREA); pad=(h+(32-h%32)-h, 0); img=np.pad(img,((0,pad[0]),(0,0),(0,0)),'maximum')
  192.         return img[:,:,:1], pad
  193.     img,pad=resize_pad(im,sz)
  194.     cur=ToTensor()(img).unsqueeze(0).to(dev); hint=torch.zeros(1,4,cur.shape[2],cur.shape[3],device=dev)
  195.     with torch.no_grad(): fake=gen(torch.cat([cur,hint],1))
  196.     res=fake[0].cpu().permute(1,2,0)*0.5+0.5
  197.     if pad[0]: res=res[:-pad[0]]
  198.     if pad[1]: res=res[:,:-pad[1]]
  199.     return np.clip(res.numpy()*255,0,255).astype(np.uint8)
  200.  
  201. app = FastAPI()
  202. app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
  203.  
  204. @app.post("/detect")
  205. async def detect_api(data: dict = Body(...)):
  206.     try:
  207.         im=np.array(Image.open(io.BytesIO(base64.b64decode(data['image']))).convert("RGB"))
  208.         boxes,dt=detect_boxes(im)
  209.         return {'status':'ok','boxes':boxes,'width':im.shape[1],'height':im.shape[0],'time':dt}
  210.     except Exception as e:
  211.         log.error(f"500 detect: {e}"); return JSONResponse(status_code=500, content={'status':'error','message':str(e)})
  212.  
  213. @app.post("/ocr")
  214. async def ocr_api(data: dict = Body(...)):
  215.     try:
  216.         im=np.array(Image.open(io.BytesIO(base64.b64decode(data['image']))).convert("RGB"))
  217.         texts=[ocr_text(im,b) for b in data.get('boxes',[])]
  218.         return {'status':'ok','texts':texts}
  219.     except Exception as e:
  220.         log.error(f"500 ocr: {e}"); return JSONResponse(status_code=500, content={'status':'error','message':str(e)})
  221.  
  222. @app.post("/translate_mbart")
  223. async def translate_api(data: dict = Body(...)):
  224.     try:
  225.         t=time.time(); trans=translate_batch(data.get('texts',[])); log.info(f"🌐 translate n={len(trans)} in {time.time()-t:.2f}s")
  226.         return {'status':'ok','translations':trans}
  227.     except Exception as e:
  228.         log.error(f"500 translate: {e}"); return JSONResponse(status_code=500, content={'status':'error','message':str(e)})
  229.  
  230. @app.post("/colorize")
  231. async def colorize_api(data: dict = Body(...)):
  232.     t=time.time()
  233.     try:
  234.         im=np.array(Image.open(io.BytesIO(base64.b64decode(data['image']))).convert("RGB"))
  235.         d=int(data.get('denoise_sigma',30)); out=colorize_image(im,denoise=d)
  236.         if out.shape[:2]!=im.shape[:2]: out=cv2.resize(out,(im.shape[1],im.shape[0]),cv2.INTER_CUBIC)
  237.         buf=io.BytesIO(); Image.fromarray(out).save(buf,format="PNG")
  238.         dt=time.time()-t; log.info(f"🎨 colorize {im.shape[0]}x{im.shape[1]} σ={d} -> {dt:.2f}s")
  239.         return {'status':'ok','image':base64.b64encode(buf.getvalue()).decode(),'time':dt}
  240.     except Exception as e:
  241.         log.error(f"500 colorize: {e}"); return JSONResponse(status_code=500, content={'status':'error','message':str(e)})
  242.  
  243. @app.get("/health")
  244. async def health(): return {'status':'ok','device':str(DEVICE)}
  245.  
  246. if __name__=="__main__":
  247.     import uvicorn
  248.     log.info("🌐 Server: http://localhost:8000")
  249.     uvicorn.run(app,host="0.0.0.0",port=8000,log_level="warning")
Add Comment
Please, Sign In to add comment