乐于分享
好东西不私藏

用 Rust 实现 基于机器学习的漫画翻译工具:Koharu 源码深度解析

用 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。

请求流转(举例一次翻译任务):

  1. UI(或外部 Agent)通过 HTTP/WebSocket 向 MCP 发送 process 请求,携带图片路径与目标语言。  
  2. MCP 把请求包装成 PipelineTask,放入 pipeline::run_task 的 async 队列。  
  3. pipelineDetect → Ocr → Inpaint → LlmGenerate → Render 的顺序调用对应 ops,每一步完成后立即广播进度。  
  4. 如果用户在任意阶段点击 “取消”,pipeline 收到取消信号,使用 RwLock 中的共享标记安全终止后续 ops。  
  5. 最终渲染好的图层通过 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::CudaDevice::Metal 的抽象统一在 koharu-runtime,避免在每个 provider 中硬编码平台细节。  
  • 决策分析:不使用 candle::Device::new() 的原因是它在每次调用时会重新检查 GPU 驱动,带来数毫秒的启动开销;这里把检查提升到 一次(在 new 时),后续推理只做算子调度。  
  • 潜在风险:在 CUDA 13.1 以下的驱动上,candle_transformers 仍会尝试加载不兼容的 PTX,导致运行时 panic。代码中已经通过 koharu_runtime::cuda_driver_versionapp.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。
  • 日志与监控:  
    • 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