用 Rust 实现 基于机器学习的漫画翻译工具:Koharu 源码深度解析
1. 为什么需要它?
在同人社团或轻小说工作室里,一部 50 页的漫画往往蕴含 上千个气泡文字。手工抠图、OCR、再人工翻译的流水线,常常导致:
- 人力成本:每位译者需要 30–40 分钟才能完成一页的文字提取与排版。
- 一致性风险:不同译者使用的字体、行距、颜色不统一,成稿质量参差。
- 交付周期:当并发翻译需求冲到 10 万页时,传统的本地 Photoshop + 手工脚本方案会因为磁盘 I/O 与 CPU 计算瓶颈而崩溃。
Koharu 把这条“翻译生产线”搬进本地机器,把目标检测 → OCR → Inpainting → 大语言模型(LLM)翻译 → 渲染 串成一条自动化流水线。对比手工方式,Koharu 能在 GPU 加速下把单页处理时间从 30 分钟压到 8 秒,并且所有模型都可以离线运行,彻底摆脱网络带宽限制。
核心技术栈概览:
| 层级 | 技术 |
|---|---|
| 前端 | React + TypeScript + Tauri(跨平台桌面) |
| 核心 | Rust(workspace)+ axum + rmcp(MCP 双协议) |
| 机器学习 | candle(自研推理框架)+ CUDA / Metal 动态加载 |
| 模型 | PP‑DocLayoutV3、comic‑text‑detector、PaddleOCR‑VL、LLaMA GGUF 等 |
| 渲染 | tiny‑skia + fontdb + skrifa(高质量文字排版、PSD 导出) |
2. 核心架构解析
类比:把 Koharu 想象成一家自动化印刷厂。
- 原材料(原始漫画图片)被送到 检测机(目标检测),把文字框位置挑出来;
- 剪切机(OCR)把文字内容读出来;
- 清洗机(Inpainting)把原始文字抹掉,留下干净画布;
- 翻译机(本地 LLM)把原文转成目标语言;
- 装帧机(Renderer)把译文按原来的排版重新写回,最后包装成 PSD 交付。
整个流程在代码层面由 koharu-pipeline 调度器 orchestrate,MCP 服务器(koharu-rpc)充当工厂的调度台,接受外部 AI Agent 或 CLI 的“开工指令”,并把进度、错误通过 WebSocket 实时推送回 UI。
请求流转(举例一次翻译任务):
- UI(或外部 Agent)通过 HTTP/WebSocket 向 MCP 发送
process请求,携带图片路径与目标语言。 - MCP 把请求包装成
PipelineTask,放入pipeline::run_task的 async 队列。 pipeline按Detect → Ocr → Inpaint → LlmGenerate → Render的顺序调用对应 ops,每一步完成后立即广播进度。- 如果用户在任意阶段点击 “取消”,
pipeline收到取消信号,使用RwLock中的共享标记安全终止后续 ops。 - 最终渲染好的图层通过
koharu-psd写入磁盘,MCP 返回文件路径,UI 渲染预览。
3. 源码级复盘
下面挑选 入口初始化、工作流调度器、LLM 抽象层 三个关键模块,逐行拆解其设计取舍。
3.1 koharu/src/app.rs:启动入口 & 资源统一初始化
// koharu/src/app.rs(省略 import 部分)
static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
// 采用 dirs crate 自动定位 OS 本地数据目录
dirs::data_local_dir()
.map(|path| path.join("Koharu"))
.unwrap_or_default()
});
static MODEL_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("models"));
- 设计哲学:把所有运行时状态(日志、模型缓存)集中在
APP_ROOT,避免硬编码路径导致跨平台兼容性问题。once_cell::Lazy确保在首次访问时才触发磁盘查询,省去启动时不必要的 I/O。 - 决策分析:相比
std::sync::Once手写单例,Lazy直接提供Sync+Send,代码量更少且对 panic 更安全;如果改用Mutex<PathBuf>,每次读取都要上锁,影响启动并发度。 - 潜在风险:在极端的文件系统只读环境下,
MODEL_ROOT创建会失败;启动阶段应捕获Result并提示用户手动指定模型目录。
3.2 koharu-pipeline/src/pipeline.rs:核心调度循环
// koharu-pipeline/src/pipeline.rs
pub async fn run_task(task: PipelineTask, ctx: Arc<AppContext>) -> Result<()> {
// 步骤 1:检测文字块
let boxes = ops::detect::run(&ctx.ml, &task.image).await?;
ctx.progress.broadcast(Step::Detect, boxes.len()).await;
// 步骤 2:OCR 提取文字
let ocr_res = ops::ocr::run(&ctx.ml, &boxes, &task.image).await?;
ctx.progress.broadcast(Step::Ocr, ocr_res.len()).await;
// 步骤 3:原文抹除(Inpainting)
let clean_img = ops::inpaint::run(&ctx.ml, &ocr_res, &task.image).await?;
ctx.progress.broadcast(Step::Inpaint, 1).await;
// 步骤 4:LLM 翻译
let translated = ops::llm::generate(&ctx.llm, &ocr_res.texts, &task.lang).await?;
ctx.progress.broadcast(Step::LlmGenerate, translated.len()).await;
// 步骤 5:渲染回图像
let final_img = ops::render::run(&ctx.renderer, &clean_img, &translated).await?;
ctx.progress.broadcast(Step::Render, 1).await;
// 写盘 & 返回
koharu_psd::export(&final_img, &task.output).await?;
Ok(())
}
- 设计哲学:每一步都是 纯函数(只依赖
ctx与输入),返回值立即用于下一步。这样可以在 单元测试 中对任意 step 进行 mock,极大提升可维护性。 - 关键点:
ctx.progress.broadcast通过axum+rmcp的 WebSocket 连接把实时进度推给 UI;实现上使用tokio::sync::broadcast,无阻塞的多订阅者模型。 - 决策分析:若改用
Mutex<Vec<Step>>累积进度,需要每次锁定整个集合,吞吐量在高并发(批量翻译)时会成为瓶颈。广播式设计让 UI 与日志系统各自独立消费,同步成本几乎为零。 - 潜在风险:在
ops::llm::generate调用远程模型时,如果网络抖动导致超时,整个run_task会被卡住。生产环境应在此层包装 超时重试(tokio::time::timeout)并在失败时回滚已完成的中间产物。
3.3 koharu-llm/src/providers/gguf.rs:本地 GGUF LLM 加载
// koharu-llm/src/providers/gguf.rs
pub async fn new(cpu: bool) -> Result<Self> {
// 步骤 1:选择后端(CUDA / Metal / CPU)
let device = if !cpu && koharu_runtime::gpu_available() {
Device::Cuda
} else {
Device::Cpu
};
tracing::info!("LLM will run on {device:?}");
// 步骤 2:懒加载模型文件(避免一次性读入全部权重)
let model_path = ModelPath::from_env()?;
let model = candle_transformers::gguf::load(&model_path, device).await?;
// 步骤 3:构造推理上下文,开启 KV 缓存以提升多轮对话吞吐
let ctx = InferenceContext::new(model, /*max_batch=*/ 4);
Ok(Self { ctx })
}
- 设计哲学:动态后端选择 让同一二进制在 CPU 机器与配备 RTX / M1 GPU 的工作站上都能跑;
Device::Cuda与Device::Metal的抽象统一在koharu-runtime,避免在每个 provider 中硬编码平台细节。 - 决策分析:不使用
candle::Device::new()的原因是它在每次调用时会重新检查 GPU 驱动,带来数毫秒的启动开销;这里把检查提升到 一次(在new时),后续推理只做算子调度。 - 潜在风险:在 CUDA 13.1 以下的驱动上,
candle_transformers仍会尝试加载不兼容的 PTX,导致运行时 panic。代码中已经通过koharu_runtime::cuda_driver_version在app.rs做了兼容性过滤,但在升级显卡驱动后仍需回归测试。
4. 生产环境避坑指南
- 别在低配 CPU 机器上开启全部模型:
candle对显存的需求与模型大小成正比,PP‑DocLayoutV3 在 4 GB 显存下会 OOM。- 推荐在 8 GB 以上显存机器上启用 GPU 加速;CPU‑only模式仅在开发调试时使用。
- 模型缓存路径必须可写:
APP_ROOT/models默认位于系统的本地数据目录,如果容器化部署请通过-v /data/koharu/models:/root/.local/share/Koharu/models挂载。- 若磁盘空间不足(模型总计约 12 GB),可以在启动参数
--model-dir /custom/path指定外部 SSD。
- 并发任务数:
pipeline本身支持多任务调度,但 GPU 同时只能执行 1 个推理作业,否则会出现显存竞争导致 OOM。- 生产配置中将
MAX_CONCURRENT_TASKS=2(一个 GPU 推理 + 一个 CPU OCR)是经验上最稳的组合。
- 网络 LLM 调用(OpenAI / Claude):
- 超时阈值建议设为 30 s,并开启指数退避重试(
retry::ExponentialBackoff),避免因瞬时网络抖动导致整批翻译失败。 - 对于机密漫画内容,务必使用本地 GGUF 方案,避免泄漏至第三方 API。
- 超时阈值建议设为 30 s,并开启指数退避重试(
- 日志与监控:
tracing_subscriber已内置 JSON 输出,配合 Loki / ELK 可实时观察pipeline::run_task的每一步耗时。- 关键指标:
detect_ms,ocr_ms,inpaint_ms,llm_ms,render_ms。若llm_ms超过 2 s,检查显存碎片或模型量化等级。
5. 总结
Koharu 本质上就是 一条把漫画页面装配成成品的全自动流水线,从目标检测的“分拣机”到 LLM 的“翻译机”,全部用 Rust+GPU 打通,既保留了原生性能,又提供了跨平台的 UI 与可扩展的 RPC 接口。
(仓库地址已在官方 README 中提供)
项目地址: https://github.com/mayocream/koharu
夜雨聆风