漫画翻译并不是把对白逐句改掉那么简单。真正影响观感的是,原图经过检测、识别、翻译、抹字、嵌字这些步骤后,是否还能保持原始画作的线条、网点和整体氛围。Mee Manga Translator 这类 AI 漫画翻译工具的核心价值,就是在完成语言转换的同时保留原始 artwork 的完整度。这篇文章从工程实现角度拆解一套可运行的漫画翻译管线:怎么定位文字区域、怎么识别原文、怎么调用翻译模型、怎么清除文字并修复背景、怎么把译文字体重排进原框。适合已经能写 Python 脚本、想从零搭建“图片翻译”小项目的开发者,也适合正在做漫画本地化、批量图片翻译工具的产品和技术人员。最终你会得到一条可以输入一张漫画页、输出翻译后预览图的最小管线,并知道每个环节的性能瓶颈和排查方向。
1. 为什么“保留原始画作”是漫画翻译的核心指标
1.1 简单覆盖文字会破坏哪些东西
如果只是把 OCR 识别出来的文字区域用矩形圈住,再用翻译 API 返回译文,最后用draw.text直接把译文盖在原图上,结果通常惨不忍睹:原文字边缘残留、气泡外背景被盖住、翻译文字溢出对话框、网点线条断裂。
原因在于,漫画原图中的文字并不是独立图层。文字、气泡、背景网点、人物线条在同一张 PNG 或 JPG 中已经是合成状态。你没有办法像操作 PSD 一样只替换文字层,只能通过算法把“文字”和“画”分离开。任何覆盖、擦除、修复操作都可能伤害到底层画作。所谓 “preserves original artwork”,在技术上的含义就是让这种伤害尽可能不可见,而不是简单地在原图上面覆盖一段字符串。
所以,漫画翻译工具不能做成“文本框定位 + 翻译 + 贴字”三条命令就完成。真正影响成品质量的,是文本区域是否被精准扣掉、修复后的背景是否连续、译文是否按照原框宽度重排,以及字体风格是否与画面氛围匹配。这些细节共同决定了读者看到翻译页面时,是像在看一本原版漫画,还是像在看一张被 PS 过的扫描图。
1.2 “保留原始画作”在工程上指什么
从工程视角看,“保留原始画作”至少包含四个层面:
- 边缘不丢失:文本框检测若比实际文字大,会连同周围的线条、网点一起抹掉;若比实际文字小,又会留下文字残影。
- 纹理可连续:修复算法需要参考文字周围的颜色、图案、渐变和网点密度,让修复区域与周围画面连续。
- 颜色不崩溃:简单填充白色只适合白色气泡内文字,如果原文字压在背景上,直接填充白色会非常突兀。
- 排版不遮挡画作:渲染译文时,译文不能超出原气泡边界,不能用整块白底盖住精心绘制的背景。
因此,工程上更推荐把漫画翻译处理成一条“图层分离”式管线:先用检测框确定文字位置,再用修复算法生成“没有文字的画层”,最后在画层上方重新排版译文。画层和文字层分开维护,才能随时调整字体、颜色和翻译内容,而不需要重新修复背景。
1.3 评价翻译页时应该看哪些维度
评价一张漫画翻译页面是否合格,不能只看译文准确率。实际验收时,建议按这几个维度逐项打分:
| 维度 | 检查方式 | 通过标准 |
|---|---|---|
| 文字清除干净度 | 将修复区域放大 200% 对比 | 无明显原文字残影、断笔画 |
| 背景连续性 | 观察修复区域与周围网点/渐变 | 纹理、颗粒、颜色过渡自然 |
| 译文可读性 | 阅读气泡内中文 | 无错别字、无溢出、断行合理 |
| 位置对齐 | 查看译文中心与气泡中心 | 留白均匀,不压到气泡线 |
| 阅读顺序 | 按原版从右到左或从左到右浏览 | 对白顺序与人称一致 |
| 单页耗时 | 记录从输入到输出耗时 | 根据业务需求确定容忍上限 |
这些维度是后面每步代码设计的目标。检测模块要尽量给出紧贴文本的四边形,修复模块要根据文字所在区域选择填充策略,渲染模块要自动缩放和换行,而不是把译文硬塞进一个固定文本框。
2. 整体架构:把翻译任务拆成五个子任务
2.1 管线式处理比“一步到位”更适合漫画翻译
漫画翻译可以拆成五个子任务:文本检测、文本识别 OCR、机器翻译、文字区域抹除与背景修复、译文渲染与重排版。
为什么不用一个端到端模型直接输入原图输出翻译图?虽然端到端思路看起来很简洁,但漫画语言种类多、翻译目标语言多、文字风格差异大,端到端模型很难兼顾。一旦翻译不准或嵌字难看,用户无法单独替换某个模块。而管线式处理的好处是:
- 检测不准就调整检测模型;
- 翻译质量差就换翻译模型或改写 prompt;
- 修复有伪影就换修复引擎;
- 字体溢出就改渲染逻辑。
各模块相互独立,便于测试、缓存和扩展。这也是当前很多漫画翻译工具采用的实际架构。
2.2 五个子任务的输入输出定义
| 阶段 | 输入 | 输出 | 典型技术 |
|---|---|---|---|
| 文本检测 | 整张漫画页 | 文本框坐标、多边形坐标 | DB、CRAFT、YOLO |
| 文字识别 OCR | 文本区域截图或整图 | 原文内容、置信度 | PaddleOCR、Tesseract、trOCR |
| 机器翻译 | 原文和上下文 | 目标语言译文 | 通用机器翻译 API、LLM、本地模型 |
| 抹字与背景修复 | 原图 + 文本 mask | 无文字背景图 | OpenCV inpaint、LaMa、AOT-GAN |
| 渲染嵌字 | 修复图 + 译文 + 文本框 | 翻译后漫画页 | Pillow、OpenCV、Skia |
这里要特别说明,PaddleOCR 的ocr方法默认会同时做检测和识别,所以“文本检测”和“OCR”在实际代码里可能是一次调用。但架构上仍要把它们看作两个阶段,因为你会需要检测框坐标去生成 mask,也需要识别文本去调用翻译接口。
2.3 中间产物的设计决定了排错效率
强烈建议在管线中保存中间产物。第一次实现时,至少保存四类文件:
- 带文本框叠加的预览图,用来检查检测是否框歪、框大;
mask.png,用来检查文字区域是否被完整覆盖;inpainted.png,用来检查背景修复是否自然;detections.json,记录每段文本的坐标、原文、置信度、译文,方便后续重跑翻译和渲染。
中间产物可以放在data/intermediate/下。这样后续调试时,不需要每次都从第一段重跑,也可以直接根据 JSON 单独调整渲染参数。
3. 环境准备与依赖选型
3.1 Python 环境与基础依赖
这里的示例使用 Python 3.10。先创建一个独立环境,避免污染其他项目:
conda create -n manga-translator python=3.10 -y conda activate manga-translator pip install --upgrade pip基础依赖可以用一个requirements.txt管理,下面内容用于复现思路,实际安装时以你当前环境可用的版本为准:
paddlepaddle>=2.5 paddleocr>=2.7 opencv-python>=4.8 Pillow>=10.0 requests>=2.31 pyyaml>=6.0如果你的机器有 NVIDIA GPU,并且已经装好 CUDA,可以安装 GPU 版 PaddlePaddle。CPU 环境也能跑通整条链路,但单张高分辨率漫画页的检测和修复会比较慢,适合学习验证。
注意:PaddleOCR 的接口在不同版本之间有差异。本文代码以
paddleocr2.7 常见的ocr.ocr调用方式为例,落地前先执行pip show paddleocr确认版本,再根据实际返回结构调整解析代码。
3.2 各模块选型参考
| 任务 | 可选方案 | 适用场景 |
|---|---|---|
| 文本检测 | PaddleOCR det、CRAFT、YOLO | 日常扫描图可用 PaddleOCR;复杂分镜和竖排文本需要单独评估 |
| 文字识别 OCR | PaddleOCR、Tesseract、trOCR | 日文/中文优先 PaddleOCR;Tesseract 适合小规模快速验证 |
| 翻译 | 通用翻译 API、OpenAI 兼容接口、本地 LLM | 需要批量译名/术语一致时用 LLM + 术语表;预算敏感用云翻译 |
| 背景修复 | OpenCV inpaint、LaMa | 小气泡文字用 OpenCV;大区域或复杂背景建议 LaMa |
| 渲染嵌字 | Pillow、OpenCV | 快速验证用 Pillow;出版级排版需要定制字体和换行逻辑 |
选择原则是:每一层都要有“可替换接口”。不要在代码里把翻译逻辑写成某个厂商专用 SDK,也不要把修复逻辑写成只适合某一个模型。这样后续换模型时,只需要替换一个模块。
3.3 项目目录结构与配置文件
一个适合后续扩展的目录结构如下:
manga_translator/ ├── config.yaml ├── requirements.txt ├── pipeline.py ├── lib/ │ ├── __init__.py │ ├── translator.py │ └── renderer.py ├── data/ │ ├── input/ │ ├── intermediate/ │ └── output/ └── tests/ └── sample/配置项统一放在config.yaml。翻译接口的 key 不要写死在文件里,使用环境变量占位,加载配置时再展开。
detector: model: "paddleocr" lang: "japan" use_angle_cls: true det_limit_side_len: 960 translator: provider: "openai_compatible" url: "${TRANSLATOR_URL}" api_key: "${TRANSLATOR_API_KEY}" model: "gpt-4o-mini" temperature: 0.3 inpaint: engine: "opencv" radius: 5 render: font_path: "fonts/NotoSansSC-Medium.ttf" font_color: "#000000" max_font_size: 32加载配置的代码可以这样写:
import os import yaml def load_config(path): with open(path, "r", encoding="utf-8") as f: # 先展开 ${TRANSLATOR_URL} 这类环境变量 content = os.path.expandvars(f.read()) return yaml.safe_load(content) if __name__ == "__main__": cfg = load_config("config.yaml") print(cfg["translator"]["url"])这样既能避免密钥入库,也方便在 CI、测试环境里通过不同的环境变量指向不同翻译后端。
4. 用代码实现一个最小可运行的漫画翻译管线
4.1 第一步:文本检测,获取文本框位置
文本检测的目标是拿到每个文字区域的坐标。下面代码使用 PaddleOCR 同时做检测和识别,但先把结果转换成统一结构,这样后续可以替换成其它检测模型。
import cv2 import numpy as np from paddleocr import PaddleOCR class TextDetector: def __init__(self, cfg): self.ocr = PaddleOCR( lang=cfg.get("lang", "japan"), use_angle_cls=cfg.get("use_angle_cls", True), det_limit_side_len=cfg.get("det_limit_side_len", 960), ) def detect(self, image): # PaddleOCR 返回结构: # [ # [ # [多边形坐标], # ["识别出的文字", 置信度] # ] # ] result = self.ocr.ocr(image, cls=True) regions = [] if not result or not result[0]: return regions for line in result[0]: box = line[0] text, confidence = line[1][0], line[1][1] regions.append({ "polygon": [tuple(map(int, p)) for p in box], "text": text, "confidence": float(confidence), }) return regions这里的detector.detect会返回一串regions。每个region包含多边形顶点、识别文本和置信度。后面在做修复 mask 时,用的就是这个polygon。
4.2 第二步:OCR 与原文清洗
如果检测模块是独立的,你还需要把检测框内区域裁出来,再送给 OCR 模型。但在 PaddleOCR 的常见接口里,识别文本已经和检测结果一起返回。下面的代码演示如何用一个裁剪函数,把检测框对应区域保存为中间产物,方便检查 OCR 到底读到了什么。
from PIL import Image def crop_polygon(image, polygon, expand_ratio=0.05): xs = [p[0] for p in polygon] ys = [p[1] for p in polygon] x1, y1, x2, y2 = min(xs), min(ys), max(xs), max(ys) # 向四周稍微扩展一点,避免文字边缘被切掉 w = x2 - x1 h = y2 - y1 x1 = max(0, int(x1 - expand_ratio * w)) y1 = max(0, int(y1 - expand_ratio * h)) x2 = min(image.width, int(x2 + expand_ratio * w)) y2 = min(image.height, int(y2 + expand_ratio * h)) return image.crop((x1, y1, x2, y2))在漫画场景中,OCR 结果经常带有空格、换行或日文助词噪声。翻译前可以做一个轻量清洗:去掉首尾空白,把全角空格统一为半角,合并连续换行。
def clean_ocr_text(raw_text): if not raw_text: return "" text = raw_text.strip() text = text.replace("\u3000", " ") text = " ".join(text.split()) return text清洗前后对比可以作为调试信息打印出来,避免把明显的 OCR 噪声直接传给翻译模型。
4.3 第三步:调用翻译接口,加入漫画上下文
翻译是决定“读起来是否自然”的关键环节。这里实现一个 Open AI 兼容接口的翻译器。实际项目可以换成任何支持 HTTP 的翻译服务,也可以换成本地 Ollama 模型。
import requests class Translator: def __init__(self, cfg): self.url = cfg["url"] self.api_key = cfg.get("api_key", "") self.model = cfg.get("model", "gpt-4o-mini") self.temperature = cfg.get("temperature", 0.3) def translate_batch(self, texts): payload = { "model": self.model, "messages": [ { "role": "system", "content": ( "你是漫画译者。请把用户提供的漫画文本翻译成简体中文。" "要求:保留语气、人称和简短风格;不要过度书面化;" "如果原文是拟声词,可以保留原文或给出接近音效的中文表达。" ), }, { "role": "user", "content": "\n---\n".join(texts), }, ], "temperature": self.temperature, } headers = {} if self.api_key: headers["Authorization"] = f"Bearer {self.api_key}" resp = requests.post(self.url, json=payload, headers=headers, timeout=30) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"].strip() # 简单按分隔符切回多条译文 parts = content.split("---") return [p.strip() for p in parts if p.strip()]批量翻译比一条一条调用更省时间,但要注意接口的上下文长度限制。如果文本条数太多,可以分成每 20 条一批。译文结果需要和原文 region 一一对应。如果返回的条数与输入不一致,最好根据顺序做安全校验,而不是直接下标读取。
4.4 第四步:生成文字 mask 并修复背景
“保留原始画作”的关键集中在修复阶段。最小实现可以用 OpenCV 的inpaint。它根据 mask 区域周围的像素把内容 “补” 进来。
def build_text_mask(image_size, regions): # image_size 是 (width, height) mask = np.zeros((image_size[1], image_size[0]), dtype=np.uint8) for region in regions: pts = np.array(region["polygon"], dtype=np.int32) cv2.fillPoly(mask, [pts], 255) return mask def inpaint_image(image_bgr, mask, radius=5, method=cv2.INPAINT_NS): # 输入必须是 BGR 的 numpy 数组 result = cv2.inpaint( src=image_bgr, inpaintMask=mask, inpaintRadius=radius, flags=method, ) return resultOpenCV 的inpaint对小面积、颜色单一的气泡文字效果不错,但对大面积网点或复杂背景会产生模糊。示例中的radius=5只是一个起点。太小会残留文字,太大会让边缘线条糊掉。
提示:如果你的漫画页普遍是“白底气泡 + 黑色文字”,可以在
inpaint之前先对 mask 区域填充白色。这样做简单、干净、速度快。只有当文字压在背景图案上时,才使用真正的图像修复模型。
可以按区域的上下文决定处理方式:
def remove_text_with_strategy(image_bgr, regions, cfg): mask = build_text_mask((image_bgr.shape[1], image_bgr.shape[0]), regions) # 如果配置允许,并且 mask 区域的平均颜色接近白色,则直接填充白色 if cfg.get("inpaint", {}).get("fill_white_if_low_confidence", False): result = image_bgr.copy() for region in regions: pts = np.array(region["polygon"], dtype=np.int32) cv2.fillPoly(result, [pts], (255, 255, 255)) return result return inpaint_image(image_bgr, mask, cfg.get("inpaint", {}).get("radius", 5))生产环境更推荐使用 LaMa 这类专门训练的图像修复模型。它们对大面积、复杂纹理的修复效果明显好于 OpenCV,但模型权重和推理资源要求也更高。
4.5 第五步:把译文渲染回原框
渲染并不是把文字随便画上去。漫画译文需要居中、适配气泡宽度、自动缩小字号、避免溢出。
from PIL import ImageDraw, ImageFont def fit_font(draw, text, box_width, box_height, font_path, max_font_size): size = max_font_size while size > 6: font = ImageFont.truetype(font_path, size=size) left, top, right, bottom = draw.textbbox((0, 0), text, font=font) w = right - left h = bottom - top if w <= box_width and h <= box_height: return font size -= 2 return ImageFont.truetype(font_path, size=6) def draw_text_in_polygon(image_pil, polygon, text, font_path, max_font_size=32): draw = ImageDraw.Draw(image_pil) xs = [p[0] for p in polygon] ys = [p[1] for p in polygon] box_width = max(xs) - min(xs) box_height = max(ys) - min(ys) if not text.strip(): return font = fit_font(draw, text, box_width, box_height, font_path, max_font_size) left, top, right, bottom = draw.textbbox((0, 0), text, font=font) text_w = right - left text_h = bottom - top # 居中坐标 x = min(xs) + (box_width - text_w) / 2 - left y = min(ys) + (box_height - text_h) / 2 - top draw.text((x, y), text, font=font, fill="black")这里使用textbbox计算文字实际像素宽高,避免字体左右留白导致视觉偏移。对于日文转中文,译文往往比原文短或长,自动缩放能处理大部分情况。但遇到一句话特别长、需要换行时,上面的代码还不够。生产环境要补充“按字宽换行”或“按标点换行”函数。
4.6 组装整条管线
有了检测、翻译、修复、渲染这些函数后,可以把它们串成一条最小的pipeline:
import cv2 from PIL import Image def run_pipeline(image_path, detector, translator, render_cfg): # 1. 检测并识别文字 image_bgr = cv2.imread(image_path) image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) image_pil = Image.fromarray(image_rgb) regions = detector.detect(image_bgr) texts = [clean_ocr_text(r["text"]) for r in regions] translations = translator.translate_batch(texts) # 给每个 region 补充译文 for i, region in enumerate(regions): region["translation"] = translations[i] if i < len(translations) else "" # 2. 修复背景 without_text = remove_text_with_strategy(image_bgr, regions, cfg) # 3. 渲染译文 result_pil = Image.fromarray(cv2.cvtColor(without_text, cv2.COLOR_BGR2RGB)) for region in regions: draw_text_in_polygon( result_pil, region["polygon"], region.get("translation", ""), render_cfg["font_path"], render_cfg.get("max_font_size", 32), ) return result_pil, regions代码还需要补充:翻译条数不一致时如何告警、低置信度文本如何处理、是否需要跳过空文本。这些看起来不复杂,但正是实际项目中决定稳定性的地方。
5. 关键参数与常见“脏图”原因
5.1 参数速查表
不同模块的参数并不需要全部调整。先记住几个对结果影响最大的参数:
| 参数 | 所属模块 | 作用 | 调大影响 | 调小影响 |
|---|---|---|---|---|
det_limit_side_len | 检测 | 控制输入检测图像的短边/长边缩放 | 小字更容易检测,但更慢、更占内存 | 速度快,但容易漏检 |
use_angle_cls | OCR | 是否启用文本方向分类 | 对横排/竖排混排更友好 | 无法纠正 180 度倒置文本 |
temperature | 翻译 | 控制译文随机性 | 译法更灵活,但可能不稳定 | 更稳定,接近直译 |
inpaintRadius | 修复 | 控制插值参考范围 | 更容易填满大块区域,但会模糊 | 保留细节,但容易残留文字 |
max_font_size | 渲染 | 控制译文最大字号 | 大字易读,但容易溢出 | 小字安全,但可读性差 |
5.2 为什么修复后画面“发虚”或“有残影”
最容易让画面显脏的原因有三个:
- mask 比文字区域小,文字边缘没有被完整覆盖,修复后出现原字残影。检查方式是保存 mask 和原图叠加预览,确认文字是否都被白色覆盖。
- mask 比文字区域大很多,附近线条、网点被一起抹掉,修复算法用周围颜色填充,于是出现色块。
- OpenCV
inpaint对大面积区域能力不足,即使半径调大,修复结果仍然是“糊”的。遇到这种问题不要继续调半径,而是直接换 LaMa 或 AOT-GAN 这类修复模型。
5.3 竖排日文和拟声词要单独处理
日文漫画最常见的是竖排文本。PaddleOCR 在竖排文本上的效果比横排差,常见处理方式是把检测框裁剪出来顺时针旋转 90 度后再识别,识别完再转回来。这个步骤会增加不少代码,但对日文漫画效果提升明显。
拟声词是另一个容易翻车的地方。像“ドドン”“グワッ”这类效果字,如果翻译成普通中文并贴回原框,反而破坏画面。比较稳妥的做法是:常规对白走翻译管线,效果字尽量保留原文;如果确实需要翻译,可以考虑做成手写风格或特殊字体渲染。
6. 运行验证与效果评估
6.1 跑一张最小测试页
准备一张漫画页保存为data/input/page_01.png,建议包含 4 到 10 个对话气泡、至少一个旁白框、最好有一个拟声词。然后执行:
python pipeline.py \ --config config.yaml \ --input data/input/page_01.png \ --output data/output/page_01_zh.png为了更好地定位问题,可以在run_pipeline里加入分阶段保存逻辑:
[stage:detect] found 8 regions [stage:ocr] conf=0.97 text="やめてよ" [stage:translate] "やめてよ" -> "别这样啦" [stage:inpaint] remove region count=8, estimated_area=12800px [stage:render] rendered 8 regions, output saved to data/output/page_01_zh.png这样的日志看起来简单,但在多页批量处理时非常有用。哪一页失败、哪一页质量低,可以通过日志快速定位。
6.2 建立人工验收样本集
自动评估很难完全替代人眼。建议建立一个 20 到 50 张的测试样本集,覆盖:
- 白底气泡文字;
- 背景复杂区域文字;
- 竖排文本;
- 长句、短句;
- 拟声词;
- 倒置文本或倾斜文本;
- 低分辨率扫描页。
每次修改检测或修复策略后,用同一批样本重新跑一遍,并记录每个样本的失败原因。这样你才能知道某次改动到底提升的是整体质量,还是只修复了单个样本。
6.3 实用评估指标
| 指标 | 计算方式 | 使用场景 |
|---|---|---|
| 检测框数量差异 | 机器检测数 / 人工标注数 | 判断漏检和误检 |
| OCR 置信度均值 | 所有 region 的 confidence 平均 | 低均值说明图像质量差或文本方向问题 |
| 翻译接口失败率 | 失败次数 / 总调用次数 | 判断接口超时、限流和参数错误 |
| 单页耗时 | 总耗时 / 页数 | 评估是否满足生产吞吐 |
| 人工返修率 | 需要人工重修的页数 / 总页数 | 最终质量指标,最贴近业务 |
BLEU 等传统机器翻译指标在漫画短文本场景参考价值有限。漫画语言极度口语化,且依赖上下文,自动翻译指标很难反映“是否自然”。
7. 常见问题排查
7.1 问题现象、原因和处理方式
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| OCR 完全没识别出日文 | 图片过大被压缩、分辨率过低 | 放大图片后单测 | 调整det_limit_side_len,或先用预处理提高对比度 |
| 竖排文本识别乱码 | 竖排文字被当成横排 | 查看 OCR 原始裁剪图 | 对竖排区域旋转 90 度后重新识别 |
| 修复区域出现明显色块 | mask 覆盖到了非文字区域 | 保存 mask 叠加图 | 收紧检测框,或改用修复模型 |
| 修复后原文字边还在 | mask 太小,未覆盖文字边缘 | 放大 mask 观察 | 对多边形做 2 到 4 像素膨胀 |
| 译文溢出气泡 | 自动缩放逻辑缺失 | 查看渲染中间层 | 增加fit_font或换行函数 |
| 翻译接口偶发失败 | 网络超时、限流 | 查看日志和响应码 | 增加重试、退避、熔断 |
| 整页处理很慢 | CPU 推理、检测图像过大 | 看各阶段耗时日志 | 压缩长边到 960,或换 GPU |
7.2 先按日志定位,不要凭肉眼猜
遇到问题,第一件事是确认卡在哪个模块。建议在每个阶段写入耗时日志:
import logging import time logging.basicConfig(level=logging.INFO) def run_stage(stage_name, func): start = time.time() result = func() elapsed = time.time() - start logging.info("stage %s done, elapsed=%.2fs", stage_name, elapsed) return result如果单页总耗时突然从 3 秒变成 30 秒,日志会告诉你卡在检测、翻译还是修复。
7.3 把中间产物当成第一排查工具
很多网友调试漫画翻译时,最大的误区是只盯着最终输出图看,却不看 mask。直接看最终图只能看到“脏”,但看不到是哪一步造成的。正确流程是:
- 看
overlay_boxes.jpg,确认检测框位置; - 看
mask.png,确认文字覆盖完整; - 看
inpainted.png,确认背景修复效果; - 看
rendered.png,确认译文排版。
哪个阶段不对,就改哪个阶段,不要在一次修改里同时改检测阈值、翻译 prompt、修复半径和字体大小。否则出了问题很难回滚原因。
8. 最佳实践与扩展方向
8.1 学习环境与生产环境的差别
在学习阶段,用 CPU、小图、OpenCV inpaint 跑通单页即可。但生产环境还需要额外考虑:
- 使用 GPU 推理,并为检测、OCR、修复模型分别设置 batch;
- 依赖版本锁定,用 requirements freeze 或镜像固定;
- 翻译接口加入超时、重试、限流和失败重试队列;
- 低置信度 OCR 文本进入人工审核,不要直接渲染;
- 保存原图和中间产物,方便回滚和对比;
- 增加监控,记录每页耗时、失败率、人工返修率。
8.2 发布前检查清单
每次发布新模型或调整参数前,按下面清单过一遍:
- [ ] 是否用统一测试样本集跑完并人工验收?
- [ ] 是否保存了中间产物,方便回滚?
- [ ] 翻译密钥是否只通过环境变量注入?
- [ ] 是否处理了空文本和 OCR 低置信度文本?
- [ ] 拟声词是否单独处理,而不是直接走普通对白翻译?
- [ ] 单页渲染是否会出现文字溢出?
- [ ] 是否对翻译接口失败做了降级或重试?
8.3 从单页翻译扩展到整本漫画
单页管线跑通后,下一步通常是批量处理整本漫画。这时会新增三个问题:
- 页面切分与阅读顺序:要识别从右到左、从上到下的阅读顺序,决定文本排序。
- 人物名和专有名词一致性:翻译整本时,角色名需要统一,不能每页翻译结果都不一样。可以通过术语表或让 LLM 接收“前文已译人名表”来保持一致性。
- 并行调度:检测、OCR、翻译、修复可以分队列处理,让 GPU 密集任务和网络请求任务解耦,避免一个超时拖垮全部流程。
8.4 给新手的练习建议
先不要追求“完全无人值守”。把单页识别链路跑通,然后把样本集中错误率最高的页面挑出来,逐张分析是检测、识别、翻译还是修复的问题。每解决一类问题,就把它沉淀成一条测试用例或一个配置项。这样你积累的不仅是代码,而是一套可以复现和验证的漫画翻译工程流程。
当单页效果稳定后,再开始替换更大的修复模型、加入竖排文本旋转识别、引入术语表,最后再考虑批量和并发。漫画翻译看起来只是“翻译 + P图”,真正做起来,难点全在细节和质量稳定性上。保留原始画作不是一句口号,而是每一步代码决策都要围绕的目标。