【免费下载链接】koharu
ML-powered manga translator, written in Rust.
koharu-app 是 Koharu(Rust 编写的 ML 漫画翻译工具)桌面端的核心应用层 crate:它全权持有 Tauri 管理的应用状态、命令 API、项目生命周期、处理作业、类型化事件通道与 Agent host,并借助 koharu-desktop 为浏览器画布准备与发布页面帧。读完本文,你将掌握 koharu-app 的模块边界、49 条 Tauri 命令的组成与用途、cargo run -p koharu-app --bin generate 契约生成链路,以及类型化通道、项目库存储、pipeline 作业等关键机制在源码中的具体实现。
koharu-app 的定位:把「应用」从「引擎」中分离
在 Koharu 的工作区中,koharu-pipeline、koharu-scene、koharu-renderer、koharu-rasterizer、koharu-translator、koharu-ml 等 crate 构成了无界面的能力引擎;而 crates/koharu-app/README.md 明确指出,koharu-app 是这些引擎之上的「Tauri 应用壳」:
- 拥有 Tauri-managed application state(通过
application.manage(...)注册的所有状态对象); - 暴露 command API(全部 Tauri IPC 命令);
- 驱动 project lifecycle(创建、打开、关闭、删除、导入、选页);
- 调度 processing jobs(pipeline 运行、停止、进度广播);
- 维护 typed channels(画布、作业、下载、资源、项目五种实时事件通道);
- 托管 agent host(登录、运行、取消 agent 运行);
- 使用
koharu-desktop把页面帧准备并发布给浏览器画布。
koharu-app 只向外暴露两个符号(见 crates/koharu-app/src/lib.rs):
pub use app::run;
pub use commands::bindings;
run(context) 启动整个桌面运行时,bindings() 返回 tauri_specta::Builder,它同时是 IPC invoke handler 与 TypeScript 契约的唯一来源——这正是 README 强调的 "Rust command signatures are the authoritative frontend contract"(Rust 命令签名是权威前端契约)的工程落地。
依赖层面(见 crates/koharu-app/Cargo.toml),koharu-app 几乎聚合了全部业务 crate:koharu-agent、koharu-desktop、koharu-ml、koharu-pipeline、koharu-psd、koharu-renderer、koharu-rasterizer(启用 native feature)、koharu-runtime、koharu-scene、koharu-translator,并直接使用 tauri、tauri-runtime-cef、tauri-specta、specta、specta-typescript 构建前端契约。
命令 API 与契约生成链路
生成命令:cargo run -p koharu-app --bin generate
README 给出了唯一的构建期命令:
cargo run -p koharu-app --bin generate
该二进制位于 crates/koharu-app/src/bin/generate.rs,逻辑非常精简:取 koharu_app::bindings(),开启 semantic_types(Configuration::empty().enable_lossless_floats())(保证 f32/f64 浮点字段在 TypeScript 侧按无损语义映射),然后通过 specta-typescript 把全部命令签名导出到工作区目录下的 packages/bridge/src/protocol.ts,文件头写入 // generated by koharu; do not edit:
let output =
std::path::Path::new(env!("CARGO_WORKSPACE_DIR")).join("packages/bridge/src/protocol.ts");
koharu_app::bindings()
.semantic_types(Configuration::empty().enable_lossless_floats())
.export(
Typescript::default().header("// generated by koharu; do not edit\n"),
&output,
)
生成产物 packages/bridge/src/protocol.ts 以 import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core" 开头,为每条命令生成带完整参数类型与返回类型的封装函数(如 getAgentStatus、loginAgent、createProject、process 等),前端直接 import { commands } from "@bridge/protocol" 即可类型安全地调用。改 Rust 命令签名后必须重新运行 generate,前端契约才会同步,这是本 crate 最重要的开发约定。
命令注册与错误模型
bindings() 的实现见 crates/koharu-app/src/commands/mod.rs:
Builder::new()
.commands(collect_commands![
agent::get_agent_status,
agent::login_agent,
// ... 共 49 条
])
.disable_serde_phases()
.error_handling(ErrorHandlingMode::Throw)
ErrorHandlingMode::Throw:命令返回的错误直接以异常形式抛给前端调用方;- 自定义错误类型
Error用#[specta(transparent)]注解为String,并实现Serialize把anyhow::Error序列化为format!("{:#}")的多行可读文本——前端拿到的永远是带上下文的错误字符串,而不是晦涩的内部结构; - 任一
anyhow::Error可通过From自动转换为Error,因此命令内部可以自由使用?传播错误。
全部 49 条命令按模块划分如下(与 collect_commands! 列表一一对应):
| 模块 | 命令 | 职责 |
|---|---|---|
| agent | get_agent_status / login_agent / logout_agent / save_agent_config / run_agent / cancel_agent | Agent host:登录态、配置、运行与取消 |
| lifecycle | subscribe / get_project / get_pages / get_page / list_projects / create_project / open_project / delete_project / close_project / import / select_page | 启动订阅与项目生命周期 |
| editing | rename_page / delete_pages / move_page / set_source_text / set_translation / set_typography / set_geometry / set_visibility / delete_layers / move_layer / undo / redo | 文档编辑与撤销重做 |
| processing | process / stop_job | 启动与停止 pipeline 作业 |
| output | export / get_thumbnail | PNG/PSD/CBZ 导出与缩略图 |
| fonts | get_fonts / get_font_preview | 字体枚举与预览 |
| preferences | save_preferences / get_preferences / get_translation_models | 偏好设置与翻译模型列表 |
| canvas | get_canvas_manifest / get_canvas_resource / prepare_canvas_page / get_canvas_page_manifest / get_canvas_page_resource / add_point_text / add_text_box / commit_paint / commit_erase / commit_transform / commit_inpaint | 画布帧、资源与编辑提交 |
应用启动与状态管理(app.rs)
app::run(见 crates/koharu-app/src/app.rs)是桌面端唯一的入口函数,它完成三件事:配置 CEF runtime、注册插件、在 setup 中初始化状态与后台任务。
CEF runtime 与平台差异配置
let cef = Cef::default();
#[cfg(debug_assertions)]
let cef = cef.remote_debugging(tauri_runtime_cef::RemoteDebugging::Port {
port: 4000,
allowed_origins: Vec::new(),
});
#[cfg(target_os = "linux")]
let cef = cef
.enable_features(["Vulkan", "VulkanFromANGLE"])
.command_line_args([
("--enable-unsafe-webgpu", None),
("use-angle", Some("vulkan")),
("--ozone-platform", Some("x11")),
]);
- 调试构建(
debug_assertions)会开启 CEF 远程调试端口4000,便于前端调试; - Linux 下显式启用 Vulkan / WebGPU 特性(
--enable-unsafe-webgpu、use-angle=vulkan、--ozone-platform=x11),这是因为 koharu-canvas 在浏览器侧依赖 WebGPU 做高性能合成; - 平台专属配置还散落在
tauri.linux.conf.json、tauri.macos.conf.json、tauri.windows.conf.json与 Entitlements.plist(macOS 签名权限)中,由 tauri.conf.json 按平台合并。
插件栈
Builder::<CefRuntime>::new() 注册了六个官方插件:
tauri-plugin-log:日志写入LogDir,单文件上限 1,000,000 字节,等级Info;tauri-plugin-single-instance:单实例约束,二次启动时聚焦已存在的main窗口(unminimize+show+set_focus);tauri-plugin-window-state:持久化窗口的SIZE、POSITION、MAXIMIZED、FULLSCREEN状态;tauri-plugin-opener/tauri-plugin-process:外部打开与进程管理;tauri-plugin-updater:应用自更新。
invoke_handler(crate::commands::bindings().invoke_handler()) 把前面生成的 tauri_specta::Builder 直接挂载为 IPC 分发器——命令注册与契约生成共享同一份命令清单,从机制上杜绝了「契约与实现不同步」。
setup:托管状态一览
setup 闭包中通过 application.manage(...) 注册了全部共享状态:
application.manage(CurrentProject { project: Mutex::new(None) });
application.manage(ProjectLibrary::new()?);
application.manage(Processing::default());
application.manage(CanvasChannel::default());
application.manage(JobChannel::default());
application.manage(DownloadChannel::default());
application.manage(ResourceChannel::default());
application.manage(ProjectChannel::default());
application.manage(Initialization::default());
application.manage(koharu_desktop::Desktop::new()?);
application.manage(AgentState::new(handle.clone())?);
随后从 tauri.conf.json 的 app.windows 配置中查找 label == "main" 的窗口定义,用 WebviewWindowBuilder::from_config 构建并显示主窗口。
Windows 发布构建(cfg(all(target_os = "windows", not(debug_assertions))))还会调用 koharu_runtime::Store::configure(...),把模型运行时仓库定位到安装目录下的 store 子目录。
异步初始化:initialize
窗口创建后立即 spawn 一个后台任务执行 initialize(crates/koharu-app/src/app.rs 第 23–66 行),其顺序是:
koharu_ml::init().await初始化 ML 运行时;koharu_ml::device(false)探测计算设备,并把compute_backend、device_type、gpu_model、vram_bytes写入koharu_metrics::context(该字段注入app_started事件,形成首次启动的硬件画像);koharu_pipeline::Pipeline::load(device)?加载处理管线并handle.manage;- 订阅
pipeline.subscribe_resources(),循环把ResourceSnapshot转成ModelResources推送到ResourceChannel; - 若已有打开的项目,调用
desktop.show_page(&snapshot, page)恢复当前页,否则desktop.clear(); - 初始化完成后
Initialization::ready()置位(tokio::sync::watch通道),唤醒所有等待subscribe的调用方。
下载事件桥接
koharu_runtime::download::subscribe() 的广播事件在 setup 的另一个后台任务中被转为 Download 结构体(含 id、state、name、completed、total、error)推入 DownloadChannel;Started/Progress/Finished/Failed 四类事件同时以 download_start、download_progress、download_result 指标写入 koharu_metrics。
窗口关闭的清理协议
on_window_event 中,窗口 CloseRequested 或 Destroyed 时:
- 遍历
Processing.stops调用stop.stop()停止所有 pipeline 作业并清空; - 调用
AgentState::cancel_all()取消 agent 运行; Destroyed时上报app_closed指标并koharu_metrics::shutdown()收尾。
类型化通道与实时事件推送
通道抽象
commands/mod.rs 定义了 ChannelExt trait,为 Mutex<Option<Channel<T>>> 提供统一的 publish 语义:
impl<T: IpcResponse> ChannelExt<T> for Mutex<Option<Channel<T>>> {
fn publish(&self, value: T) {
let mut channel = self.lock();
if channel
.as_ref()
.is_some_and(|channel| channel.send(value).is_err())
{
channel.take();
}
}
}
即:有订阅者就推,推送失败(前端已释放)则丢弃通道。五个通道 CanvasChannel、JobChannel、DownloadChannel、ResourceChannel、ProjectChannel 全部复用此模式,共享状态中只保存「最新一个」前端通道。
subscribe:前端的第一条命令
lifecycle::subscribe(crates/koharu-app/src/commands/lifecycle.rs)是前端的启动握手:它接收五个 Channel 参数(on_canvas、on_job、on_download、on_resources、on_project),先 Initialization.wait() 等待后台初始化完成,再依次注册五个通道,最后返回 StartupState { preferences, jobs, canvas } 快照——偏好、进行中的作业、当前画布状态一次到位,避免首屏闪烁。
事件负载结构
CanvasState:page、revision、generation、size、element_frames(可选中元素的变换帧列表),由koharu-desktop生成;Job:id(JobId,Uuid v4 的透明包装)、state(Running/Finished/Failed/Stopped)、completed/total、page、stage、model、error;Download/ModelResources:见上文下载桥接与资源快照;ModelResources包含process_memory、system_memory、process_cpu及每个设备的DeviceResources(name、selected、memory_budget、memory_used、utilization)。
大整数字段(u64/usize)普遍使用 #[specta(type = f64)] 映射,规避 JS 端 Number 精度上限的边界问题。
项目生命周期与文档存储
项目库:Documents/Koharu
ProjectLibrary(crates/koharu-app/src/commands/project.rs)把项目根目录固定在系统「文档」目录下的 Koharu 文件夹:
let root = dirs::document_dir()
.context("the Documents directory is unavailable")?
.join("Koharu");
list() 扫描其中满足以下条件的子目录作为项目:目录名以 .khrproj 结尾,且内部存在 state-a.khr 或 state-b.khr 文件(koharu-storage 的双槽快照文件);按 state-a/b.khr 最近修改时间降序、名称升序排序,作为「最近使用」列表返回 ProjectSummary。
项目会话与撤销/重做
Project 持有 koharu_scene::Session、name、active_page 与两套 revision 栈(undo: Vec<Vec<Revision>>、redo):
create/open通过Session::create/Session::open建立会话,新项目自动激活第一页;- 所有编辑最终都收敛到
commit(patch)→session.commit(patch),成功后record(revisions)压入 undo 栈并清空 redo 栈; undo/redo通过session.undo_many(revisions)批量回滚多个 revision,失败时回滚自身栈以保持一致性;ProjectInfo暴露revision、active_page、can_undo、can_redo,前端据此渲染撤销/重做按钮状态。
生命周期命令的公共流程
create_project、open_project 都走 replace_project:
AgentState::reset()重置 agent,停止并清空所有 pipeline 作业;- 原子替换
CurrentProject; desktop.show_page(&snapshot, page)重建画布帧;- 通过
CanvasChannel与ProjectChannel广播新的画布状态与ProjectInfo。
close_project / delete_project 则走 close_current_project(停止任务 → desktop.clear() → 广播空状态)。delete_project 若删除的是当前项目会先关闭,再在 spawn_blocking 中执行 remove_dir_all,避免阻塞主线程。
导入:Files / Folder
import 命令(crates/koharu-app/src/commands/lifecycle.rs)先用 rfd::AsyncFileDialog 弹出系统文件选择器:
PageImportSource::Files:多选文件;PageImportSource::Folder:选择目录后用walkdir递归收集,并按import::Format解析扩展名过滤。
随后 tokio_rayon::spawn 调用 import::import(files)(crates/koharu-app/src/commands/import/mod.rs)在 Rayon 线程池中并行解码:Raster(png/jpg/jpeg/webp)直接读文件,Zip/Rar 解压,Pdf 渲染成页;路径按 alphanumeric_sort 自然排序保证页序正确。导入期间若 processing.stops 非空则直接拒绝("pages cannot be imported while processing is running")。
导入成功后以 AssetRole::new("source") 把每页原始位图作为资产写入场景快照,提交 revision 并 desktop.synchronize 同步画布。
处理作业与 pipeline 集成
process 命令(crates/koharu-app/src/commands/processing.rs)是引擎与 UI 之间的桥梁,流程如下:
- 取当前项目
snapshot,生成JobId(Uuid v4); - 单任务互斥:
Processing.stops非空时返回another process is already running; - 登记
StopToken与初始Job { state: Running, ... },立即通过JobChannel广播; - 克隆
Pipeline,spawn 异步任务执行pipeline.execute(snapshot, request, &mut committer); request.progress回调把Progress事件折算为completed/total(pages.len() * stages.len()为总量),逐条更新Processing.jobs并推送Job,同时以pipeline_start、stage_loading、model_run、stage_skip等指标上报;- 运行结束按
RunStatus::Stopped/ 错误 / 成功三种结局置JobState::Stopped/Failed/Finished,移除 stop 登记并广播终态。
PipelineCommitter 实现 koharu_pipeline::Committer,把每个阶段的 StageOutput.patch 交给 project.commit_rebased(patch)——先用当前快照做 rebase(koharu_scene::Patch::rebase_on),若 pipeline 输出已被用户编辑覆盖(PatchConflict/EntityNotFound/RelationNotFound)则安全放弃该阶段输出,避免文档被陈旧结果污染;提交成功后 desktop.synchronize 并广播新画布。
stop_job 通过 JobId 查找 StopToken 并调用 stop(),协作式取消会沿 pipeline 逐级传播。
画布:koharu-desktop 的帧发布
koharu-app 不直接渲染画布,而是把这项工作委托给 crates/koharu-desktop:它保留当前 koharu-renderer 帧、向前端发布帧 generation、按精确 generation 提供 manifest 与独立寻址的资源、校验可持久化的变换提交、并惰性持有导出与预览共用的原生 koharu-rasterizer。
帧与资源的分发协议
CanvasState 里的 generation 是单调递增的帧代次(wrapping_add(1).max(1),首个有效代次从 1 开始)。前端拿到 CanvasState 后:
get_canvas_manifest(generation)→desktop.frame_manifest_bytes(generation),返回prepared().manifest().encode()的字节;get_canvas_resource(generation, resource)→ 按ResourceId返回resource_packet(resource).encode();- 若 generation 已过期,
frame_at_generation直接报错 "canvas generation X is stale",前端据此重新订阅。
字节响应通过自定义 CanvasBytes 类型实现 tauri::ipc::IpcResponse(body() 返回 InvokeResponseBody),在 #[specta(transparent)] 下映射为 Vec<u8>——大体积二进制不经 JSON 序列化,直接走 IPC 原始字节通道。页面级(prepare_canvas_page / get_canvas_page_manifest / get_canvas_page_resource)则按 (page, revision) 寻址,允许前端为后续页面预取。
页面帧缓存与 latest-wins 语义
Desktop 内部维护 PageFrameCache(LRU,上限 MAX_CACHED_PAGE_FRAMES = 8 页,见 crates/koharu-desktop/src/lib.rs),prepare_page 命中缓存直接返回。所有渲染入口都遵循 latest-wins 协议:request_preparation() 使序号自增并通知等待者,过期的准备请求(is_current_preparation == false)在渲染完成后既不落盘也不替换当前帧——配套的两个单元测试(newer_request_preempts_current_preparation_and_acquires_ownership、obsolete_request_cannot_replace_the_presentation)直接验证了这一语义。
transform_geometries 是变换提交的守门员:校验 revision 一致、元素不重复、帧有限且非空、目标层可见且为文本层,然后把控制帧的位移/缩放/旋转反算成场景几何点(仿射系数见 frame_transform),返回给 commit_transform 持久化。
画笔与局部重绘
commit_paint/commit_erase:前端把笔画点列与画笔(直径 + RGBA 颜色)提交给apply_raster_stroke,在 Rust 侧光栅化为 PNG 资产写回场景(RasterStrokeMode::Paint/Erase,橡皮擦强制要求已存在的栅格层);commit_inpaint:前端笔画被encode_mask光栅化成单通道灰度遮罩 PNG,连同Bounds存入Processing.inpainting_mask,然后以Scope::Region { page, bounds }+Operation::Only { stage: Inpainting }启动一次仅含修复阶段的 pipeline 作业——局部涂鸦 → 遮罩编码 → 定向推理整条链路都在命令层完成;- 所有编辑命令都带
expected_revision,ensure_revision不匹配即拒绝("canvas edit expected revision X, but the project is at Y"),从 IPC 层面防止并发编辑下的乐观锁失效。
编辑、偏好与导出命令
编辑命令族(editing.rs)
rename_page / delete_pages / move_page / set_source_text / set_translation / set_typography / set_geometry / set_visibility / delete_layers / move_layer / undo / redo 覆盖了文档树的全部可变操作,统一模式为:snapshot.patch(...) 构造补丁 → project.commit(patch) → record_commit → desktop.synchronize → 广播 CanvasState。
值得注意的语义细节:
set_source_text/set_translation会先promote_entity_to_user把引擎自动生成的层提升为用户编辑层(Origin::User),保证后续用户修改不会被 pipeline 输出覆盖;set_translation(None)表示移除译文组件;set_geometry只允许文本层修改几何,且points == None时仅当该层是自动放置文本(automatic_target().is_some())才允许重置;delete_layers遇到文本组会先展开子层,删除后把失去唯一Presents引用的孤儿文本内容一并清理,且拒绝删除页面("delete pages with delete_pages")。
偏好设置(preferences.rs)
get_preferences / save_preferences / get_translation_models 管理全局偏好,StartupState 首帧即携带完整 Preferences(生成参数 temperature、top_p、min_p、repeat_penalty、frequency_penalty、presence_penalty,处理器阈值如 koharu-layout-rfdetr-seg-2xl 的 text_threshold、bubble_threshold、panel_threshold 等均可从 packages/bridge/src/protocol.ts 的生成类型中看到)。偏好写入使用 koharu-secrets 加密存储敏感项。
导出(output.rs)
export 命令支持三种格式(crates/koharu-app/src/commands/output.rs):
Png/Psd:弹出目录选择器,Psd走koharu_psd::export_page保留图层结构;Png用Rasterizer::rasterize把每个渲染帧合成为位图;Cbz:保存文件对话框(默认文件名<project>.cbz),逐页栅格化后打包 zip。
所有页面帧用 try_join_all 并行渲染,PNG 编码在 Rayon 中并行执行(PngEncoder::new_with_quality(CompressionType::Best, FilterType::Adaptive))。get_thumbnail 以 THUMBNAIL_EDGE = 128 为边长输出缩略图字节。
前端契约的工程实践总结
koharu-app 的架构可以用一句话概括:Rust 是唯一的真相源(single source of truth)。所有命令签名、类型、错误与事件负载都定义在 Rust 侧,经 tauri-specta 自动推导出 TypeScript 契约;cargo run -p koharu-app --bin generate 一次性完成「注册 invoke handler」与「生成 protocol.ts」两份产物,前端 packages/bridge 只依赖这份生成文件,不存在手写接口漂移的空间。
如果你要在这个仓库上做二次开发或调试,以下路径值得优先阅读:
- 命令全量与错误模型:crates/koharu-app/src/commands/mod.rs
- 启动、插件与状态注册:crates/koharu-app/src/app.rs
- 契约生成器:crates/koharu-app/src/bin/generate.rs
- 生成产物:packages/bridge/src/protocol.ts
- 画布帧发布与 latest-wins 协议:crates/koharu-desktop/src/lib.rs
- 项目库与文档存储:crates/koharu-app/src/commands/project.rs
【免费下载链接】koharu
ML-powered manga translator, written in Rust.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



