From d4560fa9e8d9ebb5a738d81daea4c2dadce173a3 Mon Sep 17 00:00:00 2001 From: xin61 Date: Thu, 9 Jul 2026 11:01:28 +0800 Subject: [PATCH] feat(editor): add inline review mode --- tools/epub_review_editor/MAINTENANCE.md | 5 +- tools/epub_review_editor/README.md | 16 ++-- tools/epub_review_editor/static/app.js | 88 +++++++++++++++++----- tools/epub_review_editor/static/index.html | 8 +- tools/epub_review_editor/static/style.css | 33 +++++++- tools/epub_review_editor/version.py | 2 +- 6 files changed, 117 insertions(+), 35 deletions(-) diff --git a/tools/epub_review_editor/MAINTENANCE.md b/tools/epub_review_editor/MAINTENANCE.md index f19cd45..40b656a 100644 --- a/tools/epub_review_editor/MAINTENANCE.md +++ b/tools/epub_review_editor/MAINTENANCE.md @@ -83,6 +83,7 @@ epub_review_sessions/ - 书架删除默认是软删除:只在 session manifest/state 中写入 `hidden` / `deleted_at`,从 `/api/sessions` 与 `/api/library` 隐藏,不删除原始 EPUB、导出 EPUB 或反馈文件;删除 active session 时必须清空 `active_session_id`。 - 选中同系列分类后,必须能打开“系列设置”,设置该系列共享的术语表路径、翻译内容提示词、格式提示词和角色口吻提示词。 - 初始进入审校时,左侧侧栏隐藏,正文占主要空间。 +- 阅读页顶栏提供“校”审校模式按钮;默认关闭时阅读流保持纯阅读,不因点击段落打开审校面板,开启后才允许在原阅读界面中选择中日双语段落并打开右侧审校面板。 - 顶部“目录”只打开目录,“检索”只打开搜索/段落列表,不合并成一个常驻面板。 - 目录可展开/收起章节,插图作为所属章节的次级目录条目可点击阅读,文字 part 可点击定位。 - 点击目录章节、part 或插图条目后,侧栏不得自动收起;用户仍可点“隐藏”或按 `Escape` 关闭。 @@ -92,7 +93,7 @@ epub_review_sessions/ - 审校页顶部标题栏与阅读标题栏默认收起,鼠标移到页面上方或键盘焦点进入标题栏时再显示;开始页保持正常显示;展开后不得遮住侧栏“隐藏”或审校工具“关闭”等面板操作。 - 鼠标停留在顶栏或阅读标题栏时,沉浸顶栏不得因自动计时过早收起;前端脚本和样式发布时应带版本参数或等效缓存失效机制,避免浏览器继续执行旧入口代码。 - 阅读区“上一节 / 下一节”必须按 spine/目录内的小章节顺序移动,覆盖文字 part 与插图条目,不得只跳顶层大章节。 -- 点击正文段落打开右侧精修与重翻面板。 +- “校”审校模式开启后,点击正文段落打开右侧精修与重翻面板;关闭“校”或默认进入页面时,点击正文不应改变阅读宽度或弹出面板。 - 右侧面板打开或关闭导致正文宽度变化时,应恢复用户原来正在看的段落位置。 - 刷新页面、重启服务或从已有会话重新打开 EPUB 时,应恢复到上次阅读的章节/part/插图、段落和滚动位置。 - 移动窄屏下,侧栏和右侧面板应以抽屉/底部面板方式显示,不遮死主要操作。 @@ -249,6 +250,7 @@ Prompt 硬规则: - 首个插图 `/api/asset/...` 返回 200 且浏览器可显示 - 非白名单或非 raster asset 不能被 `/api/asset/...` 读取 - 目录按钮、检索按钮、隐藏侧栏、点击段落打开右侧面板 +- “校”按钮默认关闭,关闭时点击阅读段落不打开右侧面板;开启后点击日文或中文所在段落可打开右侧面板,关闭时沿用未保存编辑保护并恢复阅读宽度 - 阅读段落下方没有编号和“快速编辑/快速标记” - 未配置 API Key 时重翻返回清晰错误 - 配置保存后 `/api/gpt/config` 不包含密钥字段 @@ -256,6 +258,7 @@ Prompt 硬规则: - 重翻候选事件包含 `glossary_path` 与 `glossary_matches` - 无关段落的重翻 messages 不得包含 `大砍刀`、`Falchion` 或 `ファルシオン`;含 `ファルシオン` 或 ruby `Falchion` 的段落必须命中“大砍刀” - 顶栏复杂操作收敛在“审校工具”入口;未选择段落时仍可配置 AI、术语表并执行反馈与交付,段落编辑/重翻按钮禁用 +- 右侧审校面板复用原有审校工具能力:中文编辑、问题类型、严重程度、标签、备注、长期规则、标记选区、AI 重翻/应用候选、AI 设置、术语表、反馈、写回与导出;不要为阅读融合另造功能残缺的新面板 - `/api/reading-position` 能保存/读取位置;刷新或重启后回到上次阅读处 - 阅读外观默认/护眼/夜间、字号、行距切换即时生效,刷新后仍保留;夜间模式下正文、原文、按钮和侧栏文本可读 - 审校页顶部栏和阅读标题栏默认不遮挡正文,鼠标移到页面上方后可操作;开始页顶栏不隐藏;目录侧栏和审校工具打开时,其顶部按钮仍可直接点击 diff --git a/tools/epub_review_editor/README.md b/tools/epub_review_editor/README.md index 24ea65d..a471e51 100644 --- a/tools/epub_review_editor/README.md +++ b/tools/epub_review_editor/README.md @@ -1,6 +1,6 @@ # 双语 EPUB 审校编辑器 -当前版本:`0.13.1` +当前版本:`0.14.0` 这个工具用于把生成后的中日双语 EPUB 打开成浏览器审校界面。用户可以直接修改中文译文,也可以标记“哪里翻得不好”,并把所有记录沉淀为可交给 Codex 的反馈文件。 @@ -50,6 +50,8 @@ `0.13.1` 修复整书 AI 翻译重复正文块:已是中日双语的 EPUB 再翻译时会识别旧的日文源文/中文译文 pair,并替换中文层,不再把旧译文当成源文追加新译文;审校行识别也收敛为只接受显式灰色/source 标记。翻译页删除冗余段落预览和诊断提示,统一使用“正文块”口径,书架封面悬停菜单新增“删除”,可从书架软删除会话且不删除原始 EPUB。 +`0.14.0` 新增阅读界面内嵌审校模式:阅读页顶栏新增“校”按钮,默认关闭时保持纯阅读,开启后才允许在原阅读流中选中中日双语段落并打开右侧审校面板;原审校工具的中文编辑、问题类型、严重程度、标签、备注、长期规则、AI 重翻、术语表、反馈与导出能力均在该面板中复用。 + ## 独立安装 这个审校器现在可以脱离 Codex 使用,只需要 Python 和浏览器。把 `tools/epub_review_editor` 目录复制到其他电脑或服务器后,在该目录里安装依赖: @@ -125,10 +127,10 @@ python .\server.py --host 0.0.0.0 --port 5177 --review-root .\epub_review_sessio - 目录优先按 EPUB 内部目录页或 nav/toc 组织章节;章节下显示内部 `part0000` 形式的 part 与次级插图条目,可展开/收起,点击 part 后只看该 part,点击章节则按原 spine 顺序连续阅读正文和插图。 - 插图页会作为所属章节的次级条目出现在目录里,点击后可在阅读模式中直接查看原 EPUB 图片;点击目录项不会自动隐藏左侧侧栏。 - 检索支持“当前范围”和“全书全局”两种范围,适合在单章精查和整本书排查术语时切换。 -- 在阅读模式中点击段落即可打开右侧精修与重翻面板,直接编辑中文译文、标记问题或调用 GPT 重翻;打开/关闭面板时会尽量保持原来的阅读位置。 +- 在阅读模式中点击顶栏“校”可进入审校模式;开启后点击日文或中文所在段落会在原阅读界面右侧打开审校面板,直接编辑中文译文、标记问题或调用 GPT 重翻;关闭“校”后恢复纯阅读点击行为。面板打开/关闭时会尽量保持原来的阅读位置。 - 精修模式保留原有逐段编辑界面,用于较细的逐段复审。 -- 可从“审校工具”统一配置 OpenAI 兼容的 `base_url`、`model`、API Key、重翻提示词与术语表,并对当前段落生成重翻候选;候选不会自动覆盖正文,需要点击“应用重翻”并保存。 -- 可在“审校工具”的“AI 设置与术语表”里配置并读取正式术语表,实时搜索、增删改术语并保存;单段重翻会按当前段落命中术语表条目,而不是只在提示词里泛泛要求遵守术语。 +- 可在“校”打开的审校面板中统一配置 OpenAI 兼容的 `base_url`、`model`、API Key、重翻提示词与术语表,并对当前段落生成重翻候选;候选不会自动覆盖正文,需要点击“应用重翻”并保存。 +- 可在审校面板的“AI 设置与术语表”里配置并读取正式术语表,实时搜索、增删改术语并保存;单段重翻会按当前段落命中术语表条目,而不是只在提示词里泛泛要求遵守术语。 - 可从书架进入“AI 翻译 EPUB”页面,对日语 EPUB 设置翻译内容提示词、格式与标点提示词、角色口吻提示词、术语表路径、翻译范围、输出为中日双语或纯中文 EPUB;同系列书籍会优先套用系列提示词和术语表配置,任务在后台运行,页面实时显示进度与日志。 - 直接编辑中文译文。 - 标记问题段落或选中文本。 @@ -167,16 +169,16 @@ epub_review_sessions\_\ ## 使用建议 -- 正式长篇 EPUB 优先使用“阅读模式”:先在左侧点章节/part 连续读,看到问题再点段落快速编辑或标记。 +- 正式长篇 EPUB 优先使用“阅读模式”:先在左侧点章节/part 连续读,看到问题再点顶栏“校”,随后点段落快速编辑或标记。 - 同时审校多本书时,优先从顶栏左上角进入“书架”,再选择目标书籍;从书架进入会恢复该书上次阅读位置。 - 需要集中逐段复审时切到“精修模式”,左侧段落列表会跟随当前章节筛选。 - 只想记录问题时:勾选“标记为翻译问题”,写备注,保存本段。 - 想直接修正译文时:编辑中文译文后保存本段。 - 想让 Codex 学习你的偏好时:把“可沉淀为长期规则”写具体,例如“某个术语只在原文实际出现时才进入重翻提示”。 -- 想单段重翻时:点击段落打开“审校工具”,先在“AI 设置与术语表”中保存 GPT 配置;提示词分为翻译内容、格式标点、角色口吻三块,可恢复预设。术语表路径会从当前项目中可唯一识别的系列 `mingcibiao.json` 推断,也可以改成其他 `mingcibiao.json`。再点“重翻本段”;满意后点“应用重翻”,最后保存本段记录。 +- 想单段重翻时:先点顶栏“校”进入审校模式,再点击段落打开审校面板。先在“AI 设置与术语表”中保存 GPT 配置;提示词分为翻译内容、格式标点、角色口吻三块,可恢复预设。术语表路径会从当前项目中可唯一识别的系列 `mingcibiao.json` 推断,也可以改成其他 `mingcibiao.json`。再点“重翻本段”;满意后点“应用重翻”,最后保存本段记录。 - 想整书 AI 翻译时:先上传或打开日语 EPUB,让它出现在书架里;在书架封面悬停后点击“翻译”,保存 API 设置,并在系列设置里保存该系列术语表和提示词。默认先做前 20 段试译,确认效果后再把范围切换为全书。中日双语输出可直接进入审校;纯中文输出会入书架,但没有逐段双语审校行。 - 术语表路径默认会从当前项目中可唯一识别的系列 `mingcibiao.json` 推断;如果无法唯一推断,请在页面里填写目标术语表路径。 -- 想调整术语时:打开“审校工具”的“AI 设置与术语表”,在术语表区域搜索、修改或新增条目,点击“保存术语表”;下一次重翻会重新读取已保存的最新术语。 +- 想调整术语时:打开“校”审校面板里的“AI 设置与术语表”,在术语表区域搜索、修改或新增条目,点击“保存术语表”;下一次重翻会重新读取已保存的最新术语。 - 审校结束后点击“生成反馈”,再在聊天里告诉 Codex 读取 `feedback_for_codex.md` 和 `translation_feedback.jsonl`。 ## 当前限制 diff --git a/tools/epub_review_editor/static/app.js b/tools/epub_review_editor/static/app.js index b94b7d9..98cab36 100644 --- a/tools/epub_review_editor/static/app.js +++ b/tools/epub_review_editor/static/app.js @@ -10,6 +10,7 @@ let currentMode = "reading"; let showTouchedOnly = false; let dirty = false; let quickDirty = false; +let reviewModeEnabled = false; let hasSession = false; let expandedChapters = new Set(); let sidePanelMode = null; @@ -527,6 +528,50 @@ function closeSidePanel() { if (dom.searchPanelBtn) dom.searchPanelBtn.classList.remove("active"); } +function syncReviewModeUi() { + document.body.classList.toggle("reviewModeEnabled", reviewModeEnabled); + if (!dom.reviewToolsBtn) return; + dom.reviewToolsBtn.classList.toggle("active", reviewModeEnabled); + dom.reviewToolsBtn.setAttribute("aria-pressed", reviewModeEnabled ? "true" : "false"); + dom.reviewToolsBtn.setAttribute("aria-label", reviewModeEnabled ? "退出审校模式" : "审校模式"); + dom.reviewToolsBtn.title = reviewModeEnabled ? "退出审校模式" : "审校模式"; +} + +async function setReviewMode(enabled, options = {}) { + const next = Boolean(enabled); + if (!next && options.closePanel !== false && dom.quickEditor && !dom.quickEditor.hidden) { + const closed = await closeQuickEditor(Boolean(options.savePanel)); + if (!closed) { + syncReviewModeUi(); + return false; + } + } + if (reviewModeEnabled === next) { + syncReviewModeUi(); + return true; + } + reviewModeEnabled = next; + syncReviewModeUi(); + if (options.render !== false && hasSession) { + withReaderAnchor(() => renderAll()); + } + if (!options.silent) { + toast(next ? "审校模式已开启。点击日文或中文段落即可编辑。" : "审校模式已关闭。"); + } + return true; +} + +async function toggleReviewMode() { + if (!hasSession) return; + if (reviewModeEnabled) { + await setReviewMode(false); + return; + } + const ok = await maybeSaveBeforeSwitch(); + if (!ok) return; + await setReviewMode(true); +} + function resetReviewState() { rows = []; filteredRows = []; @@ -572,13 +617,12 @@ function resetReviewState() { } document.body.classList.remove("quickOpen"); if (dom.reviewToolsBtn) dom.reviewToolsBtn.classList.remove("active"); + setReviewMode(false, { closePanel: false, render: false, silent: true }).catch(() => {}); } -function closeReviewPanels() { +async function closeReviewPanels() { closeSidePanel(); - if (dom.quickEditor) dom.quickEditor.hidden = true; - if (dom.reviewToolsBtn) dom.reviewToolsBtn.classList.remove("active"); - document.body.classList.remove("quickOpen"); + await setReviewMode(false, { closePanel: true, render: false, silent: true, savePanel: true }); } function clearTranslationPoll() { @@ -623,7 +667,7 @@ async function leaveReviewShell() { if (!hasSession) return true; if (!(await maybeSaveBeforeSwitch())) return false; await flushReadingPosition().catch(() => {}); - closeReviewPanels(); + await closeReviewPanels(); return true; } @@ -1956,6 +2000,11 @@ function appendReadBlock(frag, row) { const block = document.createElement("section"); block.className = "readBlock"; block.dataset.rowId = row.id; + if (reviewModeEnabled) { + block.tabIndex = 0; + block.setAttribute("role", "button"); + block.setAttribute("aria-label", `审校段落 ${row.id}`); + } const part = partForRow(row); if (part) block.dataset.readingScopeId = readingScopeId(chapterForPart(part), part); if (currentRow && row.id === currentRow.id) block.classList.add("active"); @@ -1969,6 +2018,12 @@ function appendReadBlock(frag, row) { `; block.addEventListener("click", () => { + if (!reviewModeEnabled) return; + maybeSelectRow(row, { openQuick: true, scroll: false }); + }); + block.addEventListener("keydown", (event) => { + if (!reviewModeEnabled || (event.key !== "Enter" && event.key !== " ")) return; + event.preventDefault(); maybeSelectRow(row, { openQuick: true, scroll: false }); }); frag.appendChild(block); @@ -2309,6 +2364,8 @@ async function setMode(mode, saveBefore = true) { function openQuickEditor(row) { withReaderAnchor(() => { + reviewModeEnabled = true; + syncReviewModeUi(); if (row) { currentRow = row; if (isImageChapter(currentChapter) && !currentPart) currentChapter = null; @@ -2320,7 +2377,6 @@ function openQuickEditor(row) { fillQuickEditor(row); dom.quickEditor.hidden = false; document.body.classList.add("quickOpen"); - dom.reviewToolsBtn.classList.add("active"); renderAll(); }); loadGptConfig().catch(() => {}); @@ -2331,19 +2387,20 @@ async function closeQuickEditor(save = false) { if (quickDirty && currentRow) { if (save) { const ok = await saveCurrent(true); - if (!ok) return; + if (!ok) return false; } else if (!window.confirm("当前段有未保存修改,关闭审校工具会放弃这些修改。继续吗?")) { - return; + return false; } } withReaderAnchor(() => { dom.quickEditor.hidden = true; document.body.classList.remove("quickOpen"); - dom.reviewToolsBtn.classList.remove("active"); + syncReviewModeUi(); quickDirty = false; renderAll(); }); saveReadingPositionSoon(); + return true; } function markDirty() { @@ -2831,15 +2888,8 @@ function initEvents() { openSidePanel("search"); } }); - dom.reviewToolsBtn.addEventListener("click", async () => { - if (!hasSession) return; - if (!dom.quickEditor.hidden) { - await closeQuickEditor(false); - return; - } - const ok = await maybeSaveBeforeSwitch(); - if (!ok) return; - openQuickEditor(null); + dom.reviewToolsBtn.addEventListener("click", () => { + toggleReviewMode().catch((error) => toast(`切换审校模式失败:${error.message}`, 7000)); }); dom.closeSidebarBtn.addEventListener("click", closeSidePanel); dom.bookshelfBtn.addEventListener("click", () => { @@ -3036,7 +3086,7 @@ async function main() { if (!session || !session.has_session) { return; } - await loadActiveSession({ message: "阅读模式已就绪。可连续阅读,点击段落即可打开精修与重翻面板。" }); + await loadActiveSession({ message: "阅读模式已就绪。可连续阅读,需要修改时点顶栏“校”进入审校模式。" }); } catch (error) { await showStartScreen(); toast(`启动时未载入 EPUB:${error.message}`, 7000); diff --git a/tools/epub_review_editor/static/index.html b/tools/epub_review_editor/static/index.html index fd2b0f8..783f71d 100644 --- a/tools/epub_review_editor/static/index.html +++ b/tools/epub_review_editor/static/index.html @@ -4,13 +4,13 @@ 双语 EPUB 审校编辑器 - +
-

双语 EPUB 审校编辑器 v0.13.1

+

双语 EPUB 审校编辑器 v0.14.0

正在载入……

@@ -21,7 +21,7 @@ - +
@@ -509,6 +509,6 @@ - + diff --git a/tools/epub_review_editor/static/style.css b/tools/epub_review_editor/static/style.css index 7d9265e..17a293f 100644 --- a/tools/epub_review_editor/static/style.css +++ b/tools/epub_review_editor/static/style.css @@ -189,6 +189,14 @@ h1 { color: var(--accent-strong); } +.reviewModeButton { + min-width: 38px; + padding-inline: 10px; + font-size: 18px; + font-weight: 800; + line-height: 1; +} + .toolbar { display: flex; gap: 8px; @@ -1371,7 +1379,7 @@ body.chromeVisible .readerHeader, position: relative; padding: 18px 0 22px 12px; border-left: 4px solid transparent; - cursor: pointer; + cursor: default; } .partDivider { @@ -1382,11 +1390,21 @@ body.chromeVisible .readerHeader, font-size: 17px; } -.readBlock:hover { +body.reviewModeEnabled .readBlock { + cursor: pointer; +} + +body.reviewModeEnabled .readBlock:hover, +body.reviewModeEnabled .readBlock:focus-visible { background: linear-gradient(90deg, rgba(29, 111, 95, 0.055), transparent 42%); } -.readBlock.active { +body.reviewModeEnabled .readBlock:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +body.reviewModeEnabled .readBlock.active { border-left-color: var(--accent); } @@ -1615,6 +1633,11 @@ textarea { border-radius: 0; box-shadow: var(--shadow); padding: 14px; + transition: top 160ms ease; +} + +body.chromeVisible .quickEditor { + top: var(--topbar-live-height); } .quickHeader { @@ -1941,6 +1964,10 @@ details.gptConfig { border-radius: 8px; } + body.chromeVisible .quickEditor { + top: auto; + } + .startScreen, .bookshelfScreen, .translationScreen { diff --git a/tools/epub_review_editor/version.py b/tools/epub_review_editor/version.py index c2d5d34..ad00b4b 100644 --- a/tools/epub_review_editor/version.py +++ b/tools/epub_review_editor/version.py @@ -1,4 +1,4 @@ -version = "0.13.1" +version = "0.14.0" VERSION_RULES = { "PATCH": "修复 bug 或兼容性小修",