diff --git a/DEVLOG.md b/DEVLOG.md
index 14c7590..cb72423 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -2,6 +2,32 @@
---
+## 2026-10-24(OCRタブ: 切り抜きの読み直し、PaddleOCR-VL系の検証、v1.62.0)
+
+**1. 他OCRモデルの実測(Ollama、漫画ページ1枚+合成7件)**
+- **glm-ocr**: bboxなし・プロンプト無視。本文は正確だが、縦書きで文字を落とす(合成 4/7)。
+- **unlimited-ocr**: `label [x1,y1,x2,y2]本文`(0〜999)を返し、`image` 枠がコマ枠とほぼ一致。縦書き文字は取りこぼす。
+- **deepseek-ocr**: `/api/generate` の `<|grounding|>Locate <|ref|>X<|/ref|>` だけがbbox。1問い合わせ=1枠で精度は低く、`/api/chat` では壊れる。日本語も弱い。
+- **PaddleOCR-VL(マンガ特化 `hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M`、1.6 `hf.co/PaddlePaddle/PaddleOCR-VL-1.6-GGUF`)**: mmprojが別ファイルでも `ollama pull hf.co/…` だけでvision対応で動く。プロンプトは `OCR:`。`Spotting:` はOllama経由だと本文のみでbboxは出ない。実画像の縦書き装飾文字(正解「アノワタクシハ」)はマンガ特化版だけが正解(LightOn「アノマリー」、glm「ルール」、1.6「ンッうやつーっ」)。合成7件は manga 7/7(「…」「!」「?」を半角化)、1.6 6/7、LightOn 5/7。
+- どのモデルも複数クラス(コマ・キャラ・フキダシ)の一括検出はできず、gemma4のJSON検出は引き続き必要。
+
+**2. アップスケール実験(Lanczos、1〜4倍)**
+- 実画像の切り抜き(49×215px・101×31px)は倍率で結果が変わらない。誤読はモデル要因。
+- 合成の極小文字(16px)では LightOnOCR だけ 1倍 1/7 → 2倍以上 5/7(1倍は極小画像でHTTP 500も出た)。マンガ特化版は1倍が最良で、拡大すると「ドドドドッ」の文字数が増える。
+- 方針: 基本は等倍、読み直しモデルがLightOnOCR系のときだけ短辺96px未満を2倍。AI超解像は未検証。
+
+**3. 実装**
+- `ocr-core.js` に `pickRereadModel` / `rereadPrompt` / `rereadScale` / `cropRect` / `normalizeJaPunct`(日本語を含む文字列の `...`→`…`、`!`→`!`、`?`→`?`)を追加。
+- OCRタブのモデル欄に「文字枠を切り抜いて別モデルで読み直す」と「読み直しモデル」(既定は manga → PaddleOCR-VL → glm-ocr の順に選ぶ。無ければ既定オフ)。実行時は文字枠を順に読み直し、失敗が2回続いたら打ち切って元のOCR文字を残す。ツールバーに「選択枠を読み直す」を追加。
+- Ollamaが一時的に500を返す(モデル入れ替え直後にランナーが落ちる)ことがあるため、1回だけ自動で再試行する。
+- 実機(Kapture): 縦書き「アノワタクシハ」が LightOnOCR単体の「アークタクシー」から正解に変わる(全体2秒)。未導入モデルは404で打ち切り、元の文字が残る。
+
+**4. 保留**
+- 読み直しの並列実行・進捗の取り消し。枠の数だけ時間が掛かる。
+- Ollama経由でPaddleOCR-VLのbbox(Spotting)を取る方法。ロケーショントークンが落ちている可能性があるが原因は未調査。
+
+---
+
## 2026-10-24(OCRタブ新規: Ollama LightOnOCR-3でbbox・文字検出、MOSS-Imageレイアウト連携、v1.61.0)
**1. 構成**
diff --git a/README.md b/README.md
index efd87a2..a123b81 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,7 @@ ComfyUI 上で動作するマンガページ作成 SPA(シングルページ
- 検出枠はキャンバス上でドラッグ移動・角で拡縮・追加・削除でき、右の一覧でクラスと本文・説明を編集可能。「全文をコピー」「JSON をコピー」で出力
- **MOSS-Image 連携** — 「レイアウトとして送る」で物体・コマ・キャラ・文字の bbox として MOSS-Image のレイアウトへ取り込む。フキダシ等の枠に入った文字は 1 つの文字枠にまとめる。再送時は前回の OCR 取り込み分だけ入れ替え
- モデル設定欄は折りたたみ式(開閉状態を保存)
+- **切り抜きの読み直し** — 検出した文字枠を元画像から切り抜き、別モデル(既定はマンガ特化の PaddleOCR-VL、`ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M`)で本文を取り直す。縦書き・装飾文字の精度が上がる。「…」「!」「?」は自動で全角化。選択した枠だけの読み直しも可能
@@ -357,4 +358,12 @@ MIT License — 詳細は [LICENSE](LICENSE) を参照。
- **[ComfyUI-Workflow-Studio](https://github.com/ketle-man/ComfyUI-Workflow-Studio)** — I2I 連携・ギャラリー埋め込み機能を提供するコンパニオンノード
- **[comfyUI-particle-pixijs](https://github.com/ketle-man/comfyUI-particle-pixijs)** — PixiJS FX(パーティクル・フィルタ効果)機能を提供するコンパニオンノード
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one)** — Image タブの Mask ツール・レイヤー機構の実装時に参考にしたノード
+- **[ComfyUI-LiveChatStream](https://github.com/ketle-man/ComfyUI-LiveChatStream)** — VRAM 調整(画像生成の前に Ollama のモデルをアンロードして空き VRAM を作る機能)の実装元となったノード
+- **[PaddleOCR-VL-For-Manga](https://huggingface.co/jzhang533/PaddleOCR-VL-For-Manga)**(jzhang533、Apache 2.0)— OCR タブの「切り抜きの読み直し」で、縦書き・装飾文字の読み取りに推奨しているマンガ特化の OCR モデル。元モデルは [PaddleOCR-VL](https://huggingface.co/PaddlePaddle/PaddleOCR-VL)(PaddlePaddle、Apache 2.0)、Ollama 用 GGUF は [LunarOilRig/PaddleOCR-VL-manga-GGUF](https://huggingface.co/LunarOilRig/PaddleOCR-VL-manga-GGUF)。アプリには同梱せず、利用者が Ollama で取得して使う
+- **[LightOnOCR-3](https://huggingface.co/lightonai/LightOnOCR-3-4B)**(LightOn、Apache 2.0)— OCR タブの文字検出(本文と位置)に使うモデル。Ollama 版は [maternion/LightOnOCR-3:4b](https://ollama.com/maternion/LightOnOCR-3:4b)。アプリには同梱せず、利用者が Ollama で取得して使う
+- **[Gemma 4](https://ollama.com/library/gemma4)**(Google)— OCR タブのレイアウト検出(コマ・キャラ・フキダシ等)の既定モデル(`gemma4:e4b`)。同梱せず、Ollama で取得して使う
+- **[MOSS-Image](https://github.com/OpenMOSS/MOSS-Image)**(OpenMOSS、Apache 2.0)— MOSS-Image タブが使う画像生成モデル。ComfyUI 本体のネイティブ対応を利用し、モデルは同梱しない
+- **[Ollama](https://ollama.com/)**(MIT)— OCR タブ・Auto タブ・VRAM 調整が利用するローカル LLM/VLM の実行環境
+- 同梱ライブラリ(`static/js/vendor/`、各ライセンスは同フォルダの README を参照): [jsPDF](https://github.com/parallax/jsPDF)(MIT、PDF 出力)、[JSZip](https://github.com/Stuk/jszip)(MIT / GPLv3 デュアル、zip 保存)、[opentype.js](https://github.com/opentypejs/opentype.js)(MIT、3D テキストのフォント読込)
+- [psd-tools](https://github.com/psd-tools/psd-tools)(MIT)— Image タブの PSD 読み書き(`requirements.txt` で導入)
- [G'MIC](https://gmic.eu/) — フィルタ編集機能(G'MIC Qt GUI、外部実行ファイル連携)
diff --git a/README_en.md b/README_en.md
index 6837279..4835f57 100644
--- a/README_en.md
+++ b/README_en.md
@@ -111,6 +111,7 @@ A manga page creation SPA (single-page application) that runs on top of ComfyUI.
- Detected boxes can be moved by dragging, resized by the corners, added and deleted on the canvas; class, text and description can be edited in the list on the right. "Copy all text" and "Copy JSON" export the result
- **MOSS-Image handoff** — "Send as layout" imports the result into the MOSS-Image layout as object / panel / character / text bboxes. Text inside a balloon etc. is merged into one text box. Sending again replaces only the boxes imported from the previous OCR
- The model settings section is collapsible (the open/closed state is remembered)
+- **Re-reading crops** — each detected text box is cropped from the original image and read again by another model (default: manga-tuned PaddleOCR-VL, `ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M`), which improves vertical and stylized text. "…", "!" and "?" are converted to full-width automatically. A single selected box can also be re-read
@@ -357,4 +358,12 @@ MIT License — see [LICENSE](LICENSE) for details.
- **[ComfyUI-Workflow-Studio](https://github.com/ketle-man/ComfyUI-Workflow-Studio)** — Companion node providing I2I integration and the embedded gallery
- **[comfyUI-particle-pixijs](https://github.com/ketle-man/comfyUI-particle-pixijs)** — Companion node providing the PixiJS FX (particle/filter effects) feature
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one)** — Node referenced when implementing the Image tab's Mask tool and layer mechanism
+- **[ComfyUI-LiveChatStream](https://github.com/ketle-man/ComfyUI-LiveChatStream)** — Node the VRAM preparation feature (unloading Ollama models to free VRAM before image generation) was ported from
+- **[PaddleOCR-VL-For-Manga](https://huggingface.co/jzhang533/PaddleOCR-VL-For-Manga)** (jzhang533, Apache 2.0) — the manga-tuned OCR model recommended for reading vertical and stylized text in the OCR tab's "Re-reading crops". Based on [PaddleOCR-VL](https://huggingface.co/PaddlePaddle/PaddleOCR-VL) (PaddlePaddle, Apache 2.0); the GGUF build for Ollama is [LunarOilRig/PaddleOCR-VL-manga-GGUF](https://huggingface.co/LunarOilRig/PaddleOCR-VL-manga-GGUF). It is not bundled with the app; users pull it with Ollama
+- **[LightOnOCR-3](https://huggingface.co/lightonai/LightOnOCR-3-4B)** (LightOn, Apache 2.0) — the model used for text detection (content and position) in the OCR tab. The Ollama build is [maternion/LightOnOCR-3:4b](https://ollama.com/maternion/LightOnOCR-3:4b). Not bundled with the app; users pull it with Ollama
+- **[Gemma 4](https://ollama.com/library/gemma4)** (Google) — the default model (`gemma4:e4b`) for the OCR tab's layout detection (panels, characters, balloons, etc.). Not bundled; pulled with Ollama
+- **[MOSS-Image](https://github.com/OpenMOSS/MOSS-Image)** (OpenMOSS, Apache 2.0) — the image generation model used by the MOSS-Image tab. It relies on ComfyUI's native support and the model is not bundled
+- **[Ollama](https://ollama.com/)** (MIT) — the local LLM/VLM runtime used by the OCR tab, the Auto tab, and VRAM preparation
+- Bundled libraries (`static/js/vendor/`; see the README in that folder for each license): [jsPDF](https://github.com/parallax/jsPDF) (MIT, PDF export), [JSZip](https://github.com/Stuk/jszip) (MIT / GPLv3 dual, zip saving), [opentype.js](https://github.com/opentypejs/opentype.js) (MIT, font loading for 3D text)
+- [psd-tools](https://github.com/psd-tools/psd-tools) (MIT) — PSD import/export in the Image tab (installed via `requirements.txt`)
- [G'MIC](https://gmic.eu/) — Filter editing (via the G'MIC Qt GUI, an external executable)
diff --git a/README_zh.md b/README_zh.md
index b14ada3..0cd6418 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -111,6 +111,7 @@
- 检测到的框可在画布上拖动移动、拖角缩放、添加和删除;右侧列表中可编辑类别、文字和说明。可用"复制全文""复制 JSON"导出
- **MOSS-Image 联动** — 点击"作为布局发送",将结果作为物体・分镜・角色・文字的 bbox 导入 MOSS-Image 布局。气泡等框内的文字会合并为一个文字框。再次发送时仅替换上次 OCR 导入的框
- 模型设置区可折叠(会记住展开/折叠状态)
+- **裁剪重新识别** — 将检测到的文字框从原图中裁剪出来,用另一个模型(默认为面向漫画的 PaddleOCR-VL,`ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M`)重新读取文字,可提升竖排・装饰文字的精度。"…""!""?"会自动转为全角。也可只重新识别所选的框
@@ -353,4 +354,12 @@ MIT License — 详见 [LICENSE](LICENSE)。
- **[ComfyUI-Workflow-Studio](https://github.com/ketle-man/ComfyUI-Workflow-Studio)** — 提供I2I联动和嵌入式图库功能的配套节点
- **[comfyUI-particle-pixijs](https://github.com/ketle-man/comfyUI-particle-pixijs)** — 提供PixiJS FX(粒子・滤镜效果)功能的配套节点
- **[comfyui-mask-editor-one](https://github.com/ketle-man/comfyui-mask-editor-one)** — 实现Image标签Mask工具及图层机制时参考的节点
+- **[ComfyUI-LiveChatStream](https://github.com/ketle-man/ComfyUI-LiveChatStream)** — VRAM 调整功能(生成图像前卸载 Ollama 模型以腾出显存)的移植来源节点
+- **[PaddleOCR-VL-For-Manga](https://huggingface.co/jzhang533/PaddleOCR-VL-For-Manga)**(jzhang533,Apache 2.0)— OCR 标签页"裁剪重新识别"中推荐用于读取竖排・装饰文字的漫画专用 OCR 模型。基于 [PaddleOCR-VL](https://huggingface.co/PaddlePaddle/PaddleOCR-VL)(PaddlePaddle,Apache 2.0),Ollama 用 GGUF 版本为 [LunarOilRig/PaddleOCR-VL-manga-GGUF](https://huggingface.co/LunarOilRig/PaddleOCR-VL-manga-GGUF)。应用不内置该模型,需用户通过 Ollama 自行获取
+- **[LightOnOCR-3](https://huggingface.co/lightonai/LightOnOCR-3-4B)**(LightOn,Apache 2.0)— OCR 标签页用于文字检测(内容和位置)的模型。Ollama 版本为 [maternion/LightOnOCR-3:4b](https://ollama.com/maternion/LightOnOCR-3:4b)。应用不内置,需用户通过 Ollama 自行获取
+- **[Gemma 4](https://ollama.com/library/gemma4)**(Google)— OCR 标签页布局检测(分镜・角色・气泡等)的默认模型(`gemma4:e4b`)。不内置,通过 Ollama 获取
+- **[MOSS-Image](https://github.com/OpenMOSS/MOSS-Image)**(OpenMOSS,Apache 2.0)— MOSS-Image 标签页使用的图像生成模型。依赖 ComfyUI 本体的原生支持,模型不内置
+- **[Ollama](https://ollama.com/)**(MIT)— OCR 标签页、Auto 标签页和 VRAM 调整使用的本地 LLM/VLM 运行环境
+- 内置库(`static/js/vendor/`,各自的许可证见该文件夹内的 README):[jsPDF](https://github.com/parallax/jsPDF)(MIT,PDF 导出)、[JSZip](https://github.com/Stuk/jszip)(MIT / GPLv3 双许可,zip 保存)、[opentype.js](https://github.com/opentypejs/opentype.js)(MIT,3D 文字的字体加载)
+- [psd-tools](https://github.com/psd-tools/psd-tools)(MIT)— Image 标签页的 PSD 读写(通过 `requirements.txt` 安装)
- [G'MIC](https://gmic.eu/) — 滤镜编辑功能(通过G'MIC Qt GUI外部可执行文件联动)
diff --git a/static/js/i18n.js b/static/js/i18n.js
index 32e5bda..7d41284 100644
--- a/static/js/i18n.js
+++ b/static/js/i18n.js
@@ -82,6 +82,14 @@ const LANGUAGES = {
'ocr.clearBoxes': '全クリア',
'ocr.imageHint': '画像をドロップ / 貼り付け(Ctrl+V)でも読み込めます。空いている所をドラッグで枠を追加、枠をドラッグで移動・角で拡縮',
'ocr.empty': 'ここに画像をドロップ',
+ 'ocr.useReread': '文字枠を切り抜いて別モデルで読み直す',
+ 'ocr.rereadModel': '読み直しモデル:',
+ 'ocr.rereadHint': '縦書き・装飾文字はマンガ特化のPaddleOCR-VLが強い傾向です(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)。枠の数だけ時間が掛かります。',
+ 'ocr.rereadBox': '選択枠を読み直す',
+ 'ocr.rereadBoxTitle': '選択中の枠を切り抜いて、読み直しモデルで文字を取り直します',
+ 'ocr.progressReread': (i, n) => `切り抜きを読み直し中… ${i}/${n}`,
+ 'ocr.rereadDone': '読み直しました',
+ 'ocr.errReread': (m) => `読み直しに失敗しました(モデルが未導入の可能性): ${m}`,
'ocr.errDrop': (m) => `画像を読み込めませんでした: ${m}`,
'ocr.modelsTitle': 'モデル(Ollama)',
'ocr.ocrModel': 'OCRモデル:',
@@ -2025,6 +2033,14 @@ const LANGUAGES = {
'ocr.clearBoxes': 'Clear all',
'ocr.imageHint': 'You can also drop or paste (Ctrl+V) an image. Drag on empty area to add a box; drag a box to move, corners to resize',
'ocr.empty': 'Drop an image here',
+ 'ocr.useReread': 'Re-read text boxes from crops with another model',
+ 'ocr.rereadModel': 'Re-read model:',
+ 'ocr.rereadHint': 'Manga-tuned PaddleOCR-VL tends to be best on vertical and stylized text (ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M). Takes time per box.',
+ 'ocr.rereadBox': 'Re-read selected',
+ 'ocr.rereadBoxTitle': 'Crop the selected box and read it again with the re-read model',
+ 'ocr.progressReread': (i, n) => `Re-reading crops… ${i}/${n}`,
+ 'ocr.rereadDone': 'Re-read done',
+ 'ocr.errReread': (m) => `Re-read failed (is the model installed?): ${m}`,
'ocr.errDrop': (m) => `Could not load the image: ${m}`,
'ocr.modelsTitle': 'Models (Ollama)',
'ocr.ocrModel': 'OCR model:',
@@ -3939,6 +3955,14 @@ const LANGUAGES = {
'ocr.clearBoxes': '全部清除',
'ocr.imageHint': '也可拖放或粘贴(Ctrl+V)图像。在空白处拖动添加框,拖动框移动,拖动角缩放',
'ocr.empty': '将图像拖放到此处',
+ 'ocr.useReread': '裁剪文字框并用另一个模型重新识别',
+ 'ocr.rereadModel': '重新识别模型:',
+ 'ocr.rereadHint': '竖排・装饰文字推荐使用面向漫画的 PaddleOCR-VL(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)。耗时与框数成正比。',
+ 'ocr.rereadBox': '重新识别所选框',
+ 'ocr.rereadBoxTitle': '裁剪所选框,并用重新识别模型再次读取文字',
+ 'ocr.progressReread': (i, n) => `正在重新识别裁剪… ${i}/${n}`,
+ 'ocr.rereadDone': '已重新识别',
+ 'ocr.errReread': (m) => `重新识别失败(模型可能未安装): ${m}`,
'ocr.errDrop': (m) => `无法加载图像: ${m}`,
'ocr.modelsTitle': '模型(Ollama)',
'ocr.ocrModel': 'OCR模型:',
diff --git a/static/js/main/22-help-tab.js b/static/js/main/22-help-tab.js
index 525e7ab..cf6c591 100644
--- a/static/js/main/22-help-tab.js
+++ b/static/js/main/22-help-tab.js
@@ -118,6 +118,7 @@ const _HELP_DATA = [
{ heading: '準備', body: '
OCRモデル: Ollamaで maternion/LightOnOCR-3:4b を用意します(文字の位置と本文を返します)。 レイアウト検出モデル(任意): 画像対応の汎用VLM(例: gemma4:e4b)。OCRモデルはコマ・キャラなどのラベル検出ができないため、これらは別のVLMで検出します。 ' },
{ heading: '使い方', body: '画像を開く(ボタン・ドロップ・Ctrl+V貼り付け)。 検出するクラス(フキダシ・擬音・キャプション・コマ・キャラ・顔・物体)を選び「検出を実行」。文字(text)は常にOCRモデルが検出します。 キャンバスの枠をドラッグで移動、角で拡縮。空きをドラッグで枠を追加、Deleteで削除。右の一覧でクラス・本文・説明を直せます。 「全文をコピー」「JSONをコピー」で結果を取り出せます。 ' },
{ heading: 'MOSS-Imageへ送る', body: '「レイアウトとして送る」で、物体・コマ・キャラ・文字のbboxとしてMOSS-Imageのレイアウトに取り込み、MOSS-Imageタブへ移動します。キャンバスは元画像の縦横比に合わせられます。 顔はキャラ、フキダシ・擬音・キャプションは文字として送られます。「枠に文字をまとめる」がONのとき、フキダシ等の枠に入っているOCR文字は1つの文字枠(枠はフキダシの輪郭)になります。文字の入っていないフキダシ等は送られません。 再送すると、前回OCRから取り込んだ枠だけが入れ替わります(手で足した枠は残ります。「取り込み全クリア」で消せます)。 ' },
+ { heading: '切り抜きの読み直し', body: '「文字枠を切り抜いて別モデルで読み直す」をONにすると、検出した文字枠を元画像から切り抜き、「読み直しモデル」で本文を取り直します(枠の位置はそのまま)。縦書き・装飾文字は、マンガ特化のPaddleOCR-VL(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)が強い傾向です。 PaddleOCR-VL系は「…」「!」「?」を半角で返すため、日本語を含む文字列は自動で全角に直します。 切り抜きは基本的に等倍で渡します(マンガ特化版は拡大で悪化)。読み直しモデルがLightOnOCR系のときだけ、極小の切り抜き(短辺96px未満)を2倍にします。 枠の数だけ時間が掛かります。モデルが未導入などで失敗したときは、元のOCR文字のまま残ります。手直しした枠は「選択枠を読み直す」で1つずつ取り直せます。 ' },
{ heading: '注意', body: 'VLMの検出枠は大まかです。必要に応じて枠を直してから送ってください。 小さなOCRモデルのため、文字を読み違えることがあります。送る前に本文を確認してください。 ' },
],
},
@@ -546,6 +547,7 @@ const _HELP_I18N = {
{ heading: 'Setup', body: 'OCR model: prepare maternion/LightOnOCR-3:4b in Ollama (it returns text positions and content). Layout detection model (optional): a general vision LLM such as gemma4:e4b. The OCR model cannot detect labels such as panels or characters, so a separate VLM detects them. ' },
{ heading: 'How to use', body: 'Open an image (button, drop, or Ctrl+V paste). Choose the classes to detect (balloon, sound effect, caption, panel, character, face, object) and press "Run detection". Text is always detected by the OCR model. Drag a box on the canvas to move it, drag a corner to resize it, drag on empty space to add one, and press Delete to remove it. Class, text and description can be edited in the list on the right. "Copy all text" and "Copy JSON" export the result. ' },
{ heading: 'Send to MOSS-Image', body: '"Send as layout" imports the result into the MOSS-Image layout as object / panel / character / text bboxes and switches to the MOSS-Image tab. The canvas is fitted to the aspect ratio of the source image. Faces are sent as characters; balloons, sound effects and captions are sent as text. With "Merge text into frames" on, OCR text inside a balloon etc. becomes one text box (the frame is the balloon outline). Balloons without text are not sent. Sending again replaces only the boxes imported from the previous OCR (hand-drawn boxes stay; "Clear Imported" removes them). ' },
+ { heading: 'Re-reading crops', body: 'With "Re-read text boxes from crops with another model" on, each detected text box is cropped from the original image and read again by the "Re-read model" (box positions are unchanged). Manga-tuned PaddleOCR-VL (ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M) tends to be strongest on vertical and stylized text. PaddleOCR-VL models return "…", "!" and "?" as half-width characters, so strings containing Japanese are converted back to full-width automatically. Crops are sent at 1x (the manga-tuned model got worse when enlarged). Only when the re-read model is LightOnOCR-based, tiny crops (short side under 96px) are enlarged 2x. It takes time per box. If it fails (for example the model is not installed), the original OCR text is kept. Use "Re-read selected" to redo a single box after adjusting it. ' },
{ heading: 'Notes', body: 'Boxes found by the VLM are approximate. Adjust them before sending if needed. The OCR model is small and can misread text. Check the text before sending. ' },
],
},
@@ -860,6 +862,7 @@ const _HELP_I18N = {
{ heading: '准备', body: 'OCR模型: 在 Ollama 中准备 maternion/LightOnOCR-3:4b(返回文字的位置和内容)。 布局检测模型(可选): 支持图像的通用VLM(例如 gemma4:e4b)。OCR模型无法检测分镜、角色等标签,因此由另一个VLM检测。 ' },
{ heading: '使用方法', body: '打开图像(按钮、拖放或 Ctrl+V 粘贴)。 选择要检测的类别(气泡・拟声词・旁白框・分镜・角色・脸部・物体),点击"执行检测"。文字始终由OCR模型检测。 在画布上拖动框可移动,拖动角可缩放,在空白处拖动可添加,按 Delete 删除。右侧列表中可修改类别、文字和说明。 可用"复制全文""复制JSON"导出结果。 ' },
{ heading: '发送到 MOSS-Image', body: '"作为布局发送"会将结果作为物体・分镜・角色・文字的bbox导入 MOSS-Image 布局,并切换到 MOSS-Image 标签页。画布会调整为与原图相同的长宽比。 脸部作为角色发送;气泡・拟声词・旁白框作为文字发送。开启"将文字合并到框"时,气泡等框内的OCR文字会合并为一个文字框(框为气泡轮廓)。不含文字的气泡等不会发送。 再次发送时,只替换上次OCR导入的框(手绘的框会保留,可用"清除导入"删除)。 ' },
+ { heading: '裁剪重新识别', body: '开启"裁剪文字框并用另一个模型重新识别"后,会将检测到的文字框从原图中裁剪出来,用"重新识别模型"重新读取文字(框的位置不变)。竖排・装饰文字推荐使用面向漫画的 PaddleOCR-VL(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)。 PaddleOCR-VL 系列会以半角返回"…""!""?",因此含日文的字符串会自动转换为全角。 裁剪图按原尺寸(1 倍)发送(漫画专用模型放大后反而变差)。仅当重新识别模型为 LightOnOCR 系列时,才会将极小的裁剪图(短边不足 96px)放大 2 倍。 耗时与框数成正比。若失败(例如模型未安装),则保留原来的OCR文字。调整某个框后可用"重新识别所选框"单独重读。 ' },
{ heading: '注意', body: 'VLM检测到的框比较粗略,发送前请根据需要调整。 OCR模型较小,可能会读错文字,发送前请确认文字。 ' },
],
},
diff --git a/static/js/main/29-ocr-tab.js b/static/js/main/29-ocr-tab.js
index 017d2e5..2c3153d 100644
--- a/static/js/main/29-ocr-tab.js
+++ b/static/js/main/29-ocr-tab.js
@@ -14,6 +14,7 @@ import { switchTab } from './01-state.js';
import {
OCR_MODEL_DEFAULT, LAYOUT_MODEL_DEFAULT, OCR_CLASSES, OCR_CLASS_KEYS, VLM_CLASS_KEYS, OCR_PROMPT,
buildLayoutPrompt, parseOcrOutput, parseLayoutOutput, mergeDetections, toMossItems, collectText, sortReading,
+ pickRereadModel, rereadPrompt, rereadScale, cropRect, normalizeJaPunct,
} from '../ocr-core.js';
const $ = (id) => document.getElementById(id);
@@ -42,6 +43,7 @@ function saveSettings() {
localStorage.setItem(STORE_KEY, JSON.stringify({
url: $('ocr-url').value, ocrModel: $('ocr-model').value, layoutModel: $('ocr-layout-model').value,
useLayout: $('ocr-use-layout').checked, classes, merge: $('ocr-merge-containers').checked,
+ useReread: $('ocr-use-reread').checked, rereadModel: $('ocr-reread-model').value,
}));
} catch (_) { /* ignore */ }
}
@@ -49,7 +51,7 @@ function saveSettings() {
// ─── Ollama ───
const baseUrl = () => ($('ocr-url').value.trim() || getBackendDefaultUrl('ollama')).replace(/\/+$/, '');
-async function ollamaChat(model, prompt, b64, numPredict) {
+async function ollamaChat(model, prompt, b64, numPredict, retry = true) {
const res = await fetch(`${baseUrl()}/api/chat`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -58,6 +60,10 @@ async function ollamaChat(model, prompt, b64, numPredict) {
options: { temperature: 0, num_predict: numPredict },
}),
});
+ if (res.status === 500 && retry) { // モデルの入れ替え直後などでランナーが一時的に落ちることがある。1回だけやり直す
+ await new Promise(r => setTimeout(r, 800));
+ return ollamaChat(model, prompt, b64, numPredict, false);
+ }
if (!res.ok) throw new Error(`HTTP ${res.status} ${(await res.text().catch(() => '')).slice(0, 200)}`);
return (await res.json()).message?.content || '';
}
@@ -78,6 +84,9 @@ async function loadModels(saved) {
const names = ((await res.json()).models || []).map(m => m.name);
fillModelSelect($('ocr-model'), names, saved?.ocrModel || OCR_MODEL_DEFAULT, [/lighton/i, /ocr/i]);
fillModelSelect($('ocr-layout-model'), names, saved?.layoutModel || LAYOUT_MODEL_DEFAULT,[/gemma|qwen.*vl|vl/i]); // 既定は LAYOUT_MODEL_DEFAULT(gemma4:e4b)。保存値が無ければ名前から推定
+ const rereadDefault = pickRereadModel(names);
+ fillModelSelect($('ocr-reread-model'), names, saved?.rereadModel || rereadDefault, []);
+ if (!rereadDefault && saved?.useReread === undefined) $('ocr-use-reread').checked = false; // 向いたモデルが無いときは既定オフ
const ok = names.includes(OCR_MODEL_DEFAULT) || names.some(n => /ocr/i.test(n));
label.textContent = ok ? t('ocr.statusReady') : t('ocr.statusNoModel');
label.className = 'comfyui-status ' + (ok ? 'connected' : 'disconnected');
@@ -111,7 +120,7 @@ async function loadImageFile(file) {
cx.fillStyle = '#fff'; cx.fillRect(0, 0, cv.width, cv.height);
cx.drawImage(im, 0, 0, cv.width, cv.height);
const b64 = cv.toDataURL('image/jpeg', 0.92).split(',')[1];
- O.img = { url, w: im.naturalWidth, h: im.naturalHeight, b64 };
+ O.img = { url, w: im.naturalWidth, h: im.naturalHeight, b64, el: im };
O.boxes = []; O.selId = null; O.nextId = 1;
$('ocr-run-btn').disabled = false;
$('ocr-progress').textContent = '';
@@ -123,6 +132,7 @@ function renderAll() { renderStage(); renderList(); updateButtons(); }
function updateButtons() {
$('ocr-box-del-btn').disabled = !selBox();
+ $('ocr-box-reread-btn').disabled = !selBox() || !TEXTY.has(selBox().cls) || O.busy;
$('ocr-send-moss-btn').disabled = !O.boxes.length || !O.img;
$('ocr-count').textContent = O.boxes.length ? `(${O.boxes.length})` : '';
}
@@ -302,6 +312,59 @@ function selectedLayoutClasses() {
return VLM_CLASS_KEYS.filter(c => document.querySelector(`[data-ocr-class="${c}"]`)?.checked);
}
+// ─── 切り抜き再OCR ───
+// 文字枠を元画像から切り抜き、別モデルで読み直す。LightOnOCR系だけ、極小の切り抜きを2倍にする(ocr-core.js の rereadScale)
+function cropForModel(box, model) {
+ const r = cropRect(box, O.img.w, O.img.h);
+ const k = rereadScale(model, r.sw, r.sh);
+ const cv = document.createElement('canvas');
+ cv.width = Math.round(r.sw * k); cv.height = Math.round(r.sh * k);
+ const cx = cv.getContext('2d');
+ cx.fillStyle = '#fff'; cx.fillRect(0, 0, cv.width, cv.height);
+ cx.imageSmoothingQuality = 'high';
+ cx.drawImage(O.img.el, r.sx, r.sy, r.sw, r.sh, 0, 0, cv.width, cv.height);
+ return cv.toDataURL('image/png').split(',')[1];
+}
+
+/** 1枠を読み直す。空の結果は null(元の文字を残す) */
+async function rereadBox(box, model) {
+ const out = normalizeJaPunct(await ollamaChat(model, rereadPrompt(model), cropForModel(box, model), 512));
+ return out || null;
+}
+
+async function rereadAll(prog) {
+ const model = $('ocr-reread-model').value;
+ const targets = O.boxes.filter(b => b.cls === 'text');
+ let done = 0, failed = 0;
+ for (const b of targets) {
+ prog.textContent = t('ocr.progressReread', ++done, targets.length);
+ try {
+ const text = await rereadBox(b, model);
+ if (text) { b.textOrig = b.text; b.text = text; }
+ } catch (e) {
+ console.warn('[ocr] reread failed', e);
+ if (++failed >= 2) throw new Error(t('ocr.errReread', e.message || String(e))); // モデル未導入などで全滅するときは打ち切る
+ }
+ }
+}
+
+async function onRereadSelected() {
+ const b = selBox();
+ if (!b || O.busy || !O.img) return;
+ O.busy = true; updateButtons();
+ const prog = $('ocr-progress');
+ try {
+ prog.textContent = t('ocr.progressReread', 1, 1);
+ const text = await rereadBox(b, $('ocr-reread-model').value);
+ if (text) { b.textOrig = b.text; b.text = text; renderList(); }
+ prog.textContent = t('ocr.rereadDone');
+ } catch (e) {
+ prog.textContent = t('ocr.errRun', e.message || String(e));
+ } finally {
+ O.busy = false; updateButtons();
+ }
+}
+
async function onRun() {
if (O.busy || !O.img) return;
if (O.boxes.length && !confirm(t('ocr.confirmReplace'))) return;
@@ -322,12 +385,17 @@ async function onRun() {
O.boxes = mergeDetections(ocrBoxes, vlmBoxes).map(b => ({ ...b, id: O.nextId++ }));
O.selId = null;
renderAll();
- prog.textContent = t('ocr.done', O.boxes.length, ((Date.now() - started) / 1000).toFixed(1));
+ let rereadError = '';
+ if ($('ocr-use-reread').checked && $('ocr-reread-model').value) {
+ try { await rereadAll(prog); } catch (e) { rereadError = e.message || String(e); } // 検出結果は残し、元のOCR文字のまま
+ renderAll();
+ }
+ prog.textContent = rereadError || t('ocr.done', O.boxes.length, ((Date.now() - started) / 1000).toFixed(1));
} catch (e) {
console.warn('[ocr] run failed', e);
prog.textContent = t('ocr.errRun', e.message || String(e));
} finally {
- O.busy = false; $('ocr-run-btn').disabled = !O.img;
+ O.busy = false; $('ocr-run-btn').disabled = !O.img; updateButtons();
}
}
@@ -370,6 +438,7 @@ async function initOcrTabOnce() {
document.querySelectorAll('[data-ocr-class]').forEach(el => { el.checked = saved.classes.includes(el.dataset.ocrClass); });
}
if (saved.useLayout === false) $('ocr-use-layout').checked = false;
+ if (saved.useReread !== undefined) $('ocr-use-reread').checked = !!saved.useReread;
if (saved.merge === false) $('ocr-merge-containers').checked = false;
const md = $('ocr-models-details'); // モデル欄の開閉状態を保存
@@ -383,6 +452,7 @@ async function initOcrTabOnce() {
if (f) loadImageFile(f).catch(err => alert(err.message));
});
$('ocr-box-del-btn').addEventListener('click', deleteSelected);
+ $('ocr-box-reread-btn').addEventListener('click', onRereadSelected);
$('ocr-box-clear-btn').addEventListener('click', () => {
if (!O.boxes.length || !confirm(t('ocr.confirmClear'))) return;
O.boxes = []; O.selId = null; renderAll();
@@ -392,7 +462,7 @@ async function initOcrTabOnce() {
$('ocr-copy-text-btn').addEventListener('click', () => copyText(collectText(O.boxes), 'ocr.copied'));
$('ocr-copy-json-btn').addEventListener('click', () => copyText(boxesAsJson(), 'ocr.copied'));
$('ocr-send-moss-btn').addEventListener('click', onSendMoss);
- for (const id of ['ocr-url', 'ocr-model', 'ocr-layout-model', 'ocr-use-layout', 'ocr-merge-containers']) $(id).addEventListener('change', saveSettings);
+ for (const id of ['ocr-url', 'ocr-model', 'ocr-layout-model', 'ocr-use-layout', 'ocr-merge-containers', 'ocr-use-reread', 'ocr-reread-model']) $(id).addEventListener('change', saveSettings);
document.querySelectorAll('[data-ocr-class]').forEach(el => el.addEventListener('change', saveSettings));
$('ocr-url').addEventListener('change', () => loadModels(loadSettings()));
window.addEventListener('resize', () => { if ($('ocr-tab')?.classList.contains('active')) renderStage(); });
diff --git a/static/js/ocr-core.js b/static/js/ocr-core.js
index 9bf7802..6db4fd1 100644
--- a/static/js/ocr-core.js
+++ b/static/js/ocr-core.js
@@ -226,3 +226,46 @@ export function collectText(boxes) {
return sortReading(boxes.filter(b => b.cls === 'text' || (OCR_CLASSES[b.cls]?.container && b.text)))
.map(b => b.text).filter(Boolean).join('\n');
}
+
+// ── 切り抜き再OCR ──
+// OCRモデルで取った文字枠を元画像から切り抜き、別モデルで読み直す(日本語の縦書き・装飾文字の精度を上げる)。
+// 実測(DEVLOG 2026-10-24): マンガ特化のPaddleOCR-VLは縦書きの装飾文字に強く等倍が最良、LightOnOCRは極小の切り抜きだけ2倍で改善する。
+
+/** 再OCRに向くモデルを一覧から選ぶ(マンガ特化 → PaddleOCR-VL → GLM-OCR の順)。無ければ '' */
+export function pickRereadModel(names) {
+ for (const re of [/paddleocr.*manga/i, /paddleocr/i, /glm-ocr/i]) {
+ const hit = names.find(n => re.test(n));
+ if (hit) return hit;
+ }
+ return '';
+}
+
+/** モデルごとのOCR指示文(PaddleOCR-VLは "OCR:"、GLM-OCRは "Text Recognition:") */
+export function rereadPrompt(model) {
+ const m = String(model || '');
+ if (/paddle/i.test(m)) return 'OCR:';
+ if (/glm-ocr/i.test(m)) return 'Text Recognition:';
+ return 'Extract all text from this image.';
+}
+
+export const UPSCALE_SHORT_SIDE = 96; // LightOnOCRに渡す切り抜きの短辺がこれ未満なら2倍に拡大
+/** 切り抜き(px)の拡大率。LightOnOCR系だけ極小のとき2倍、それ以外は等倍(マンガ特化版は拡大で悪化した) */
+export function rereadScale(model, w, h) {
+ return /lighton/i.test(String(model || '')) && Math.min(w, h) < UPSCALE_SHORT_SIDE ? 2 : 1;
+}
+
+/** 正規化bbox → 元画像の切り抜き矩形(px)。短辺の6%+4pxの余白を足す */
+export function cropRect(box, imgW, imgH) {
+ const bw = (box.x2 - box.x1) * imgW, bh = (box.y2 - box.y1) * imgH;
+ const pad = Math.round(Math.min(bw, bh) * 0.06) + 4;
+ const sx = Math.max(0, Math.floor(box.x1 * imgW - pad)), sy = Math.max(0, Math.floor(box.y1 * imgH - pad));
+ const ex = Math.min(imgW, Math.ceil(box.x2 * imgW + pad)), ey = Math.min(imgH, Math.ceil(box.y2 * imgH + pad));
+ return { sx, sy, sw: Math.max(1, ex - sx), sh: Math.max(1, ey - sy) };
+}
+
+/** PaddleOCR-VL系は「…」「!」「?」を半角(...・!・?)で返すので、日本語を含む文字列だけ全角に戻す */
+export function normalizeJaPunct(text) {
+ const s = String(text || '').trim();
+ if (!isCjk(s)) return s;
+ return s.replace(/\.{2,}/g, '…').replace(/!/g, '!').replace(/\?/g, '?');
+}
diff --git a/templates/index.html b/templates/index.html
index 3f59a74..289e9c5 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -2311,6 +2311,7 @@
描画種別:
+ 選択枠を読み直す
削除
全クリア
@@ -2331,6 +2332,11 @@
検出モデル:
OCRモデルは文字の位置と本文だけを返します。コマ・キャラ・フキダシ等は、画像対応の汎用VLM(gemma4 など)で検出します。
+
+ 文字枠を切り抜いて別モデルで読み直す
+
+ 読み直しモデル:
+ 縦書き・装飾文字はマンガ特化のPaddleOCR-VLが強い傾向です(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)。枠の数だけ時間が掛かります。