Merge branch 'feature/ocr-reread'
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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. 構成**
|
||||
|
||||
@@ -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`)で本文を取り直す。縦書き・装飾文字の精度が上がる。「…」「!」「?」は自動で全角化。選択した枠だけの読み直しも可能
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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、外部実行ファイル連携)
|
||||
|
||||
@@ -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
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`)重新读取文字,可提升竖排・装饰文字的精度。"…""!""?"会自动转为全角。也可只重新识别所选的框
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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外部可执行文件联动)
|
||||
|
||||
@@ -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模型:',
|
||||
|
||||
@@ -118,6 +118,7 @@ const _HELP_DATA = [
|
||||
{ heading: '準備', body: '<ul><li>OCRモデル: Ollamaで <code>maternion/LightOnOCR-3:4b</code> を用意します(文字の位置と本文を返します)。</li><li>レイアウト検出モデル(任意): 画像対応の汎用VLM(例: <code>gemma4:e4b</code>)。OCRモデルはコマ・キャラなどのラベル検出ができないため、これらは別のVLMで検出します。</li></ul>' },
|
||||
{ heading: '使い方', body: '<ol><li>画像を開く(ボタン・ドロップ・Ctrl+V貼り付け)。</li><li>検出するクラス(フキダシ・擬音・キャプション・コマ・キャラ・顔・物体)を選び「検出を実行」。文字(text)は常にOCRモデルが検出します。</li><li>キャンバスの枠をドラッグで移動、角で拡縮。空きをドラッグで枠を追加、Deleteで削除。右の一覧でクラス・本文・説明を直せます。</li><li>「全文をコピー」「JSONをコピー」で結果を取り出せます。</li></ol>' },
|
||||
{ heading: 'MOSS-Imageへ送る', body: '<ul><li>「レイアウトとして送る」で、物体・コマ・キャラ・文字のbboxとしてMOSS-Imageのレイアウトに取り込み、MOSS-Imageタブへ移動します。キャンバスは元画像の縦横比に合わせられます。</li><li>顔はキャラ、フキダシ・擬音・キャプションは文字として送られます。「枠に文字をまとめる」がONのとき、フキダシ等の枠に入っているOCR文字は1つの文字枠(枠はフキダシの輪郭)になります。文字の入っていないフキダシ等は送られません。</li><li>再送すると、前回OCRから取り込んだ枠だけが入れ替わります(手で足した枠は残ります。「取り込み全クリア」で消せます)。</li></ul>' },
|
||||
{ heading: '切り抜きの読み直し', body: '<ul><li>「文字枠を切り抜いて別モデルで読み直す」をONにすると、検出した文字枠を元画像から切り抜き、「読み直しモデル」で本文を取り直します(枠の位置はそのまま)。縦書き・装飾文字は、マンガ特化のPaddleOCR-VL(<code>ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M</code>)が強い傾向です。</li><li>PaddleOCR-VL系は「…」「!」「?」を半角で返すため、日本語を含む文字列は自動で全角に直します。</li><li>切り抜きは基本的に等倍で渡します(マンガ特化版は拡大で悪化)。読み直しモデルがLightOnOCR系のときだけ、極小の切り抜き(短辺96px未満)を2倍にします。</li><li>枠の数だけ時間が掛かります。モデルが未導入などで失敗したときは、元のOCR文字のまま残ります。手直しした枠は「選択枠を読み直す」で1つずつ取り直せます。</li></ul>' },
|
||||
{ heading: '注意', body: '<ul><li>VLMの検出枠は大まかです。必要に応じて枠を直してから送ってください。</li><li>小さなOCRモデルのため、文字を読み違えることがあります。送る前に本文を確認してください。</li></ul>' },
|
||||
],
|
||||
},
|
||||
@@ -546,6 +547,7 @@ const _HELP_I18N = {
|
||||
{ heading: 'Setup', body: '<ul><li>OCR model: prepare <code>maternion/LightOnOCR-3:4b</code> in Ollama (it returns text positions and content).</li><li>Layout detection model (optional): a general vision LLM such as <code>gemma4:e4b</code>. The OCR model cannot detect labels such as panels or characters, so a separate VLM detects them.</li></ul>' },
|
||||
{ heading: 'How to use', body: '<ol><li>Open an image (button, drop, or Ctrl+V paste).</li><li>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.</li><li>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.</li><li>"Copy all text" and "Copy JSON" export the result.</li></ol>' },
|
||||
{ heading: 'Send to MOSS-Image', body: '<ul><li>"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.</li><li>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.</li><li>Sending again replaces only the boxes imported from the previous OCR (hand-drawn boxes stay; "Clear Imported" removes them).</li></ul>' },
|
||||
{ heading: 'Re-reading crops', body: '<ul><li>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 (<code>ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M</code>) tends to be strongest on vertical and stylized text.</li><li>PaddleOCR-VL models return "…", "!" and "?" as half-width characters, so strings containing Japanese are converted back to full-width automatically.</li><li>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.</li><li>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.</li></ul>' },
|
||||
{ heading: 'Notes', body: '<ul><li>Boxes found by the VLM are approximate. Adjust them before sending if needed.</li><li>The OCR model is small and can misread text. Check the text before sending.</li></ul>' },
|
||||
],
|
||||
},
|
||||
@@ -860,6 +862,7 @@ const _HELP_I18N = {
|
||||
{ heading: '准备', body: '<ul><li>OCR模型: 在 Ollama 中准备 <code>maternion/LightOnOCR-3:4b</code>(返回文字的位置和内容)。</li><li>布局检测模型(可选): 支持图像的通用VLM(例如 <code>gemma4:e4b</code>)。OCR模型无法检测分镜、角色等标签,因此由另一个VLM检测。</li></ul>' },
|
||||
{ heading: '使用方法', body: '<ol><li>打开图像(按钮、拖放或 Ctrl+V 粘贴)。</li><li>选择要检测的类别(气泡・拟声词・旁白框・分镜・角色・脸部・物体),点击"执行检测"。文字始终由OCR模型检测。</li><li>在画布上拖动框可移动,拖动角可缩放,在空白处拖动可添加,按 Delete 删除。右侧列表中可修改类别、文字和说明。</li><li>可用"复制全文""复制JSON"导出结果。</li></ol>' },
|
||||
{ heading: '发送到 MOSS-Image', body: '<ul><li>"作为布局发送"会将结果作为物体・分镜・角色・文字的bbox导入 MOSS-Image 布局,并切换到 MOSS-Image 标签页。画布会调整为与原图相同的长宽比。</li><li>脸部作为角色发送;气泡・拟声词・旁白框作为文字发送。开启"将文字合并到框"时,气泡等框内的OCR文字会合并为一个文字框(框为气泡轮廓)。不含文字的气泡等不会发送。</li><li>再次发送时,只替换上次OCR导入的框(手绘的框会保留,可用"清除导入"删除)。</li></ul>' },
|
||||
{ heading: '裁剪重新识别', body: '<ul><li>开启"裁剪文字框并用另一个模型重新识别"后,会将检测到的文字框从原图中裁剪出来,用"重新识别模型"重新读取文字(框的位置不变)。竖排・装饰文字推荐使用面向漫画的 PaddleOCR-VL(<code>ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M</code>)。</li><li>PaddleOCR-VL 系列会以半角返回"…""!""?",因此含日文的字符串会自动转换为全角。</li><li>裁剪图按原尺寸(1 倍)发送(漫画专用模型放大后反而变差)。仅当重新识别模型为 LightOnOCR 系列时,才会将极小的裁剪图(短边不足 96px)放大 2 倍。</li><li>耗时与框数成正比。若失败(例如模型未安装),则保留原来的OCR文字。调整某个框后可用"重新识别所选框"单独重读。</li></ul>' },
|
||||
{ heading: '注意', body: '<ul><li>VLM检测到的框比较粗略,发送前请根据需要调整。</li><li>OCR模型较小,可能会读错文字,发送前请确认文字。</li></ul>' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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(); });
|
||||
|
||||
@@ -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, '?');
|
||||
}
|
||||
|
||||
@@ -2311,6 +2311,7 @@
|
||||
<input type="file" id="ocr-file" accept="image/*" style="display:none" />
|
||||
<span style="font-size:12px;" data-i18n="ocr.drawType">描画種別:</span>
|
||||
<select id="ocr-draw-class" class="comfyui-select" style="width:auto"></select>
|
||||
<button id="ocr-box-reread-btn" class="btn small secondary" disabled data-i18n="ocr.rereadBox" data-i18n-title="ocr.rereadBoxTitle">選択枠を読み直す</button>
|
||||
<button id="ocr-box-del-btn" class="btn small danger" disabled data-i18n="common.delete">削除</button>
|
||||
<button id="ocr-box-clear-btn" class="btn small danger" data-i18n="ocr.clearBoxes">全クリア</button>
|
||||
</div>
|
||||
@@ -2331,6 +2332,11 @@
|
||||
</div>
|
||||
<div class="comfyui-field"><label class="comfyui-label" data-i18n="ocr.layoutModel">検出モデル:</label><select id="ocr-layout-model" class="comfyui-select"></select></div>
|
||||
<div style="font-size:11px; color:#888;" data-i18n="ocr.layoutHint">OCRモデルは文字の位置と本文だけを返します。コマ・キャラ・フキダシ等は、画像対応の汎用VLM(gemma4 など)で検出します。</div>
|
||||
<div class="comfyui-field" style="margin-top:8px;">
|
||||
<label class="comfyui-checkbox-label" style="display:inline-flex; align-items:center; gap:4px;"><input type="checkbox" id="ocr-use-reread" checked> <span data-i18n="ocr.useReread">文字枠を切り抜いて別モデルで読み直す</span></label>
|
||||
</div>
|
||||
<div class="comfyui-field"><label class="comfyui-label" data-i18n="ocr.rereadModel">読み直しモデル:</label><select id="ocr-reread-model" class="comfyui-select"></select></div>
|
||||
<div style="font-size:11px; color:#888;" data-i18n="ocr.rereadHint">縦書き・装飾文字はマンガ特化のPaddleOCR-VLが強い傾向です(ollama pull hf.co/LunarOilRig/PaddleOCR-VL-manga-GGUF:Q4_K_M)。枠の数だけ時間が掛かります。</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="comfyui-field-group" style="margin-top:10px">
|
||||
|
||||
Reference in New Issue
Block a user