diff --git a/README.fa.md b/README.fa.md new file mode 100644 index 0000000..ade63fc --- /dev/null +++ b/README.fa.md @@ -0,0 +1,413 @@ +# پنل کلینر (Panel Cleaner) + +[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/voxelcubes/PanelCleaner?logo=GitHub)](https://github.com/voxelcubes/PanelCleaner/releases) +[![PyPI version](https://img.shields.io/pypi/v/pcleaner)](https://pypi.org/project/pcleaner/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Crowdin](https://badges.crowdin.net/panel-cleaner/localized.svg)](https://crowdin.com/project/panel-cleaner) + +> **فهرست** +> +> [ویژگی‌ها](#ویژگی‌ها) \ +> [محدودیت‌ها](#محدودیت‌ها) \ +> [چرا از این برنامه استفاده کنیم؟](#چرا-از-این-برنامه-استفاده-کنیم) \ +> [نصب](#نصب) \ +> [استفاده](#استفاده) \ +> [پروفایل‌ها](#پروفایل‌ها) \ +> [OCR](#ocr) \ +> [OCR هوشمند (اصلاح با LLM)](#ocr-هوشمند-اصلاح-با-llm) \ +> [نمونه‌ها](#نمونه‌هایی-از-حباب‌های-پیچیده) \ +> [تشکر و قدردانی](#تشکر-و-قدردانی) \ +> [پروانه](#پروانه) \ +> [نقشه راه](#نقشه-راه) \ +> [سؤالات متداول](https://github.com/VoxelCubes/PanelCleaner/blob/master/docs/faq.md) \ +> [ترجمه](https://github.com/VoxelCubes/PanelCleaner/blob/master/translations/TRANSLATING.md) + +--- + +این ابزار با استفاده از یادگیری ماشین، متن را پیدا کرده و سپس با بالاترین دقت ممکن، ماسک‌هایی برای پوشاندن آن تولید می‌کند. هدف آن پاک کردن حباب‌های ساده است؛ هیچ عملیات inpainting یا حذفِ متنِ خارج از حباب انجام نمی‌شود. این برنامه طراحی شده تا حجم زیادی از کارهای تکراری را برای افرادی که باید پنل‌های زیادی را پاک کنند، کاهش دهد، ضمن اینکه مطمئن شود چیزی را که نباید پاک کند، پوشش نمی‌دهد. + +![نمونه](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/spread.png) + +![نمونه](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/screenshots/cleaning_done.png) + +![نمونه](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/screenshots/details.png) + +تصویر بالا (صفحه بالا-راست) نشان می‌دهد: + +- جعبه‌های مختلفی در جایی که هوش مصنوعی متن پیدا کرده رسم شده است. + +- (سبز) هوش مصنوعی همچنین یک ماسک دقیق در جایی که متن را تشخیص داده، تولید می‌کند. + +- (بنفش) این ماسک‌ها بسط داده شده‌اند تا هر متن مجاوری که تشخیص داده نشده و همچنین artifacts مربوط به فشرده‌سازی jpeg را پوشش دهند. + +- (آبی) برای ماسک‌هایی که تنگ هستند، حاشیه اطراف لبه ماسک برای پاک‌سازی نهایی نویزگیری می‌شود، بدون اینکه بقیه تصویر تحت تأثیر قرار گیرد. + +دو صفحه پایین، خروجی‌های برنامه هستند: یا فقط لایه ماسک شفاف و/یا ماسک اعمال‌شده روی تصویر اصلی که آن را پاک می‌کند. + +--- + +## ویژگی‌ها + +- حباب‌های متنی را بدون برجای گذاشتن artifact پاک می‌کند. + +- از پوشش دادن بخش‌هایی از تصویر که متن نیستند، اجتناب می‌کند. + +- حباب‌هایی که نمی‌توان با ماسک پوشاند را (با LaMa و یادگیری ماشین) inpaint می‌کند. + +- حباب‌هایی که فقط شامل نمادها یا اعداد هستند را نادیده می‌گیرد، زیرا نیازی به ترجمه ندارند. + +- یک رابط گرافیکی (GUI) برای استفاده آسان ارائه می‌دهد؛ تم‌های تاریک، روشن و سیستمی پشتیبانی می‌شوند. + +- پس از نصب داده‌های مدل، به اتصال اینترنت نیاز ندارد. + +- مجموعه‌ای گسترده از گزینه‌ها برای سفارشی‌سازی فرآیند پاک‌سازی و امکان ذخیره چندین پیش‌تنظیم به‌عنوان پروفایل را ارائه می‌دهد. + برای مشاهده فهرست تمام گزینه‌ها، [پروفایل پیش‌فرض](https://github.com/VoxelCubes/PanelCleaner/blob/master/media/default.conf) را ببینید. + +- تحلیل‌های دقیقی از فرآیند پاک‌سازی ارائه می‌دهد تا ببینید تنظیماتتان چه تأثیری روی نتایج دارند. + +- در صورتی که به‌صورت بسته پایتون نصب شود و سخت‌افزار شما پشتیبانی کند، از شتاب‌دهی CUDA بهره می‌برد. + +- از پردازش دسته‌ای (batch) تصاویر و پوشه‌ها پشتیبانی می‌کند. + +- می‌تواند با حباب‌هایی روی هر رنگ پس‌زمینه یکدست کار کند. + +- می‌تواند متن را از بقیه تصویر برش دهد، مثلاً برای جایگذاری روی یک نسخه رنگی. + +- می‌تواند روی صفحات OCR اجرا کند و متن را در یک فایل ذخیره کند. + +- به‌صورت اختیاری خروجی OCR را از طریق یک مدل زبانی بزرگ (با [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm)) عبور می‌دهد تا متن‌های نامفهوم را اصلاح کند — «اصلاح هوشمند OCR». + +- مرور خروجی پاک‌سازی و OCR، از جمله ویرایش تعاملی خروجی OCR پیش از ذخیره آن. + +- رابط کاربری به زبان‌های: انگلیسی، آلمانی، بلغاری، اسپانیایی + (برای زبان‌های بیشتر به [ترجمه](https://github.com/VoxelCubes/PanelCleaner/blob/master/translations/TRANSLATING.md) مراجعه کنید) + +--- + +## محدودیت‌ها + +- برای پاک‌سازی فقط از متن ژاپنی و انگلیسی پشتیبانی می‌کند (موفقیت با زبان‌های دیگر ممکن است متفاوت باشد)؛ برای OCR فقط ژاپنی. + +- انواع فایل پشتیبانی‌شده: ‎.jpeg، ‎.jpg، ‎.png، ‎.bmp، ‎.tiff، ‎.tif، ‎.jp2، ‎.dib، ‎.webp، ‎.ppm +- انواع فایل پشتیبانی‌شده (فقط خروجی): ‎.psd + +- برنامه برای تشخیص اولیه متن به هوش مصنوعی تکیه می‌کند که ذاتاً بی‌نقص نیست. گاهی ممکن است بخش‌های کوچکی از متن را از قلم بیندازد یا بخشی از حباب را به‌اشتباه به‌عنوان متن تشخیص دهد که مانع پاک‌سازی آن حباب می‌شود. از آزمایش‌ها، این معمولاً بین ۲ تا ۸ درصد حباب‌ها را بسته به تنظیمات شما تحت تأثیر قرار می‌دهد. + +- به دلیل رویکرد محافظه‌کارانه در انتخاب ماسک‌ها، اگر برنامه نتواند حباب را با رضایت‌بخشی پاک کند، آن حباب را رها می‌کند. البته این امر از مثبت کاذب (false positive) نیز جلوگیری می‌کند. + +- برای ماسک‌ها، در حال حاضر فقط مقیاس خاکستری (grayscale) پشتیبانی می‌شود. یعنی می‌تواند متن را در حباب‌های سفید، سیاه یا خاکستری پوشش دهد، اما نه حباب‌های رنگی. + +--- + +## چرا از این برنامه استفاده کنیم؟ + +این برنامه برای پاک‌سازی دقیق و کامل حباب‌های متنی طراحی شده، بدون اینکه artifact‌ای برجای بگذارد. +هدف آن صرفه‌جویی در زمان پاک‌کننده‌ها و انجام کارهای تکراری است. +[هوش مصنوعی](https://github.com/dmMaze/comic-text-detector) که برای تشخیص متن و تولید ماسک اولیه استفاده شده، بخشی از این پروژه *نیست*؛ این پروژه فقط از آن به‌عنوان نقطه شروع استفاده کرده و خروجی‌اش را بهبود می‌بخشد. + +| تصویر اصلی | خروجی هوش مصنوعی | پنل کلینر | +|:-----------------------------------:|:-----------------------------------:|:------------------------------------:| +| ![Original](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_original.png) | ![AI Output](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_ai_raw.png) | ![Panel Cleaner](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_clean.png) | + +همان‌طور که می‌بینید، با کمی پاک‌سازی اضافی روی خروجی هوش مصنوعی، مقداری متن باقی‌مانده و artifact‌های فشرده‌سازی jpeg حذف می‌شوند و حباب کاملاً پاک می‌شود. \ +وقتی پاک‌سازی کامل ممکن نباشد، پنل کلینر آن حباب را رها می‌کند تا وقت شما با یک پاک‌سازی نامناسب هدر نرود. رفتار دقیق پاک‌سازی به‌شدت قابل تنظیم است؛ برای جزئیات بیشتر به [پروفایل‌ها](#پروفایل‌ها) مراجعه کنید. + +--- + +## نصب + +می‌توانید بین نصب یک باینری از پیش ساخته‌شده (exe یا elf) از [بخش انتشارها](https://github.com/VoxelCubes/PanelCleaner/releases/latest) (**توصیه‌شده برای اغلب کاربران**)، یا نصب روی مفسر پایتون محلی خود با pip، یکی را انتخاب کنید. + +توجه: همه نسخه‌ها در اولین اجرا نیاز به دانلود داده‌های مدل دارند (حدود ۵۰۰ مگابایت). این داده‌های مدل در صورت به‌روزرسانی پنل کلینر نیازی به دانلود مجدد ندارند. + +مهم: باینری‌های از پیش ساخته‌شده از شتاب‌دهی CUDA پشتیبانی نمی‌کنند. برای استفاده از CUDA، باید برنامه را با pip نصب کنید و نسخه مناسب [pytorch](https://pytorch.org/get-started/locally/) را برای سیستم خود نصب کنید. + +### نصب با Pip + +برنامه به **پایتون ۳.۱۰ یا جدیدتر** نیاز دارد. + +برنامه را با هم رابط خط فرمان و رابط گرافیکی با pip از [PyPI](https://pypi.org/project/pcleaner/) نصب کنید: +```bash +pip install pcleaner +``` + +یا اگر فقط می‌خواهید از رابط خط فرمان استفاده کنید: +```bash +pip install pcleaner-cli +``` +توجه: `pcleaner` و `pcleaner-cli` می‌توانند در کنار هم نصب شوند، اما بسته فقط-خط‌فرمان افزاینده خواهد بود. + +توجه: آزمایش شده که برنامه روی لینوکس، مک‌اواس و ویندوز با سطوح مختلفی از تنظیمات اولیه کار می‌کند. برای راهنمایی به [سؤالات متداول](https://github.com/VoxelCubes/PanelCleaner/blob/master/docs/faq.md) مراجعه کنید. + +### نصب از AUR (آرچ لینوکس) + +این روش برنامه را در یک محیط `pipx` نصب می‌کند که به pytorch اجازه می‌دهد نسخه مناسب CUDA را برای سیستم شما دانلود کند؛ بنابراین بهترین روش نصب است. + +می‌توانید بسته را این‌جا پیدا کنید: [panelcleaner](https://aur.archlinux.org/packages/panelcleaner) + +این دستور `pcleaner` و `pcleaner-gui` را همراه با یک فایل دسکتاپ برای GUI فراهم می‌کند. + +با helper مورد علاقه AUR خود نصب کنید، مثلاً با `yay`: +```bash +yay -S panelcleaner +``` + +### نصب با Flatpak + +این روش باینری از پیش ساخته‌شده را در یک کانتینر flatpak نصب می‌کند که از شتاب‌دهی CUDA پشتیبانی نمی‌کند. + +[دریافت از Flathub](https://flathub.org/apps/io.github.voxelcubes.panelcleaner) + +### نصب با Docker + +تصویر را با buildx بسازید: +```bash +docker buildx build -t pcleaner:v1 . +``` +یا با builder قدیمی: +```bash +docker image build -t pcleaner:v1 . +``` + +سپس تصویر داکر را با تعیین یک پوشه ریشه برای دسترسی کانتینر راه‌اندازی کنید. +در این مثال، پوشه جاری (`pwd`) استفاده شده است: +```bash +docker run -it --name pcleaner -v $(pwd):/app pcleaner:v1 +``` +این کار همچنین یک شل تعاملی در کانتینر باز می‌کند. + +بعداً می‌توانید با این دستور یکی دیگر باز کنید: +```bash +docker start pcleaner +docker exec -it pcleaner bash +``` + +--- + +## استفاده + +برنامه از خط فرمان قابل اجراست و در رایج‌ترین حالت، هر تعداد تصویر یا پوشه را به‌عنوان ورودی می‌پذیرد. برنامه یک پوشه جدید به نام `cleaned` در همان پوشه فایل‌های ورودی می‌سازد و تصاویر و/یا ماسک‌های پاک‌شده را آن‌جا قرار می‌دهد. اغلب مفیدتر است که فقط لایه ماسک را خروجی بگیرید، که می‌توانید با اضافه کردن گزینه `--save-only-mask` یا به‌اختصار `-m` این کار را انجام دهید. + +نمونه‌ها: +```bash +pcleaner clean image1.png image2.png image3.png + +pcleaner clean -m folder1 image1.png +``` + +نمایش با ۴۶ تصویر، در زمان واقعی، با شتاب‌دهی CUDA. +![نمایش](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/pcleaner_demo.gif) + +گزینه‌های بسیار بیشتری وجود دارد که با اجرای دستور زیر قابل مشاهده است: +```bash +pcleaner --help +``` + +### راه‌اندازی GUI با خط فرمان + +GUI با دستور `gui` از خط فرمان قابل راه‌اندازی است: +```bash +pcleaner gui +``` +یا مستقیماً با +```bash +pcleaner-gui +``` + +اگر pcleaner پیدا نمی‌شود، مطمئن شوید در متغیر PATH شماست، یا این را امتحان کنید: +```bash +python -m pcleaner +``` + +--- + +## پروفایل‌ها + +برنامه هر تنظیم ممکن را در یک پروفایل پیکربندی که به‌صورت فایل‌های متنی ساده ذخیره می‌شوند و از طریق GUI نیز قابل دسترسی هستند، expos می‌کند. هر گزینه پیکربندی در خود فایل توضیح داده شده و به شما اجازه می‌دهد هر پارامتر فرآیند پاک‌سازی را برای نیازهای خاص خود بهینه کنید. \ +کافی است یک پروفایل جدید بسازید: +```bash +pcleaner profile new my_profile_name_here +``` + +و پروفایل جدیدتان در یک ویرایشگر متن برایتان باز می‌شود. \ +در اینجا یک قطعه کوچک از پروفایل پیش‌فرض آمده است: +```ini +# Number of pixels to grow the mask by each step. +# This bulks up the outline of the mask, so smaller values will be more accurate but slower. +mask_growth_step_pixels = 2 + +# Number of steps to grow the mask by. +# A higher number will make more and larger masks, ultimately limited by the reference box size. +mask_growth_steps = 11 +``` + +با اضافه کردن `--profile=my_profile_name_here` یا +`-p my_profile_name_here` به دستور، کلینر را با پروفایل مشخص‌شده اجرا کنید. + +اگر در دیدن اینکه تنظیمات چه تأثیری روی نتایج دارند مشکل دارید، می‌توانید از +گزینه `--cache-masks` برای ذخیره تصویرسازی مراحل میانی در پوشه کش استفاده کنید. + +| پروفایل پیش‌فرض | پروفایل سفارشی | +| ---------------------------------------------- | ------------------------------------------- | +| ![Default Profile](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/profile_original.png) | ![Custom Profile](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/profile_modded.png) | +| mask_growth_step_pixels = 2 | mask_growth_step_pixels = 4 | +| mask_growth_steps = 11 | mask_growth_steps = 4 | + +علاوه بر این، برای هر مرحله پردازش در ترمینال تحلیل‌هایی ارائه می‌شود تا ببینید تنظیماتتان در مجموع چه تأثیری روی نتایج دارند. + +برای فهرست تمام گزینه‌ها، [پروفایل پیش‌فرض](https://github.com/VoxelCubes/PanelCleaner/blob/master/media/default.conf) را ببینید. + +توجه: پروفایل پیش‌فرض برای تصاویری با ابعاد تقریبی ۱۱۰۰×۱۶۰۰ پیکسل بهینه شده است. +اگر از تصاویر با وضوح به‌مراتب کمتر یا بیشتر استفاده می‌کنید، پارامترهای ابعاد را در یک پروفایل متناسب تنظیم کنید. + +پیش از خروجی گرفتن از تصاویر پاک‌شده، تنظیماتتان را با چند حالت نمایش مرور کنید. +![مرور](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/flatpak/Screenshot_review.png) + +--- + +## OCR + +همچنین می‌توانید از پنل کلینر برای انجام تشخیص کاراکتر نوری (OCR) روی صفحات استفاده کنید +و متن را در یک فایل ذخیره کنید. این می‌تواند برای کمک به ترجمه یا استخراج متن برای اهداف تحلیلی مفید باشد. \ +می‌توانید OCR را با این دستور اجرا کنید: +```bash +pcleaner ocr myfolder --output-path=output.txt +``` + +پنل کلینر برای OCR ژاپنی به‌صورت پیش‌فرض از [MangaOCR](https://github.com/kha-white/manga-ocr) استفاده می‌کند که روش ترجیحی برای OCR متن ژاپنی است. +در صورت موجود بودن، پنل کلینر می‌تواند از Tesseract نیز برای OCR استفاده کند، به‌ویژه برای پردازش متن انگلیسی و +ژاپنی — تنها دو زبانی که در حال حاضر پشتیبانی می‌شوند. + +این قابلیت در GUI نیز به‌عنوان گزینه خروجی OCR موجود است. +همچنین می‌توانید خروجی OCR را آن‌جا به‌صورت تعاملی مرور و ویرایش کنید. +![مرور](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/flatpak/Screenshot_ocr.png) + +برای نصب Tesseract روی سیستم خود، دستورالعمل‌های زیر را دنبال کنید. + +### نصب Tesseract + +#### ویندوز + +1. نصب‌کننده را از [مخزن رسمی GitHub تی Tesseract](https://github.com/tesseract-ocr/tessdoc?tab=readme-ov-file#releases-and-changelog) دانلود کنید. +توصیه می‌شود آخرین نسخه از UB Mannheim لینک‌شده را دریافت کنید (۶۴ بیت). +2. نصب‌کننده را اجرا کنید و دستورالعمل‌های روی صفحه را برای نصب سیستم‌کلی دنبال کنید. +3. پوشه نصب Tesseract را به متغیر محیطی PATH خود اضافه کنید. +اگر نصب سیستم‌کلی انجام داده‌اید، این یعنی اضافه کردن پوشه `C:\Program Files\Tesseract-OCR` [به PATH خود](https://www.computerhope.com/issues/ch000549.htm). +4. رایانه خود را راه‌اندازی مجدد کنید. + +#### مک‌اواس + +از Homebrew برای نصب Tesseract استفاده کنید: + +```bash +brew install tesseract +``` + +#### لینوکس + +برای توزیع‌های مبتنی بر دبیان، از apt استفاده کنید: + +```bash +sudo apt install tesseract-ocr +``` + +برای سایر توزیع‌ها، به package manager خود و [مستندات رسمی Tesseract](https://tesseract-ocr.github.io/tessdoc/Home.html) مراجعه کنید. + +برای دستورالعمل‌های نصب دقیق و اطلاعات بیشتر، لطفاً به [مستندات رسمی Tesseract](https://tesseract-ocr.github.io/tessdoc/) مراجعه کنید. + +> توجه: اگرچه Tesseract از زبان‌های اضافی پشتیبانی می‌کند، پنل کلینر فقط از Tesseract برای تشخیص متن انگلیسی و ژاپنی استفاده می‌کند. انگلیسی به‌صورت پیش‌فرض نصب است. برای نصب بسته زبان ژاپنی، دستورالعمل‌های [نصب بسته‌های زبان اضافی](https://ocrmypdf.readthedocs.io/en/latest/languages.html) را دنبال کنید. + +--- + +## OCR هوشمند (اصلاح با LLM) + +پنل کلینر می‌تواند به‌صورت اختیاری خروجی OCR را از طریق یک مدل زبانی بزرگ (LLM) عبور دهد تا متن نامفهوم را اصلاح کند — اصلاح کاراکترهای نادرست خوانده‌شده، کلمات شکسته و علائم نگارشی اضافی — ضمن حفظ زبان و معنای اصلی. این برنامه **ترجمه نمی‌کند**. این قابلیت زمانی مفید است که خروجی خام manga-ocr یا Tesseract پرنویز باشد و متن تمیزتری برای ترجمه یا بایگانی بخواهید. + +این قابلیت توسط [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm) تأمین می‌شود که با offloading لایه‌به‌لایه به دیسک، اجازه می‌دهد مدل‌های بسیار بزرگی (مثلاً Llama-3-70B) با فقط چند گیگابایت RAM اجرا شوند، به‌بهای کندی تولید توکن. + +### فعال‌سازی اصلاح LLM + +این قابلیت **به‌صورت پیش‌فرض خاموش است**. ابتدا افزونه اختیاری `[llm]` را نصب کنید: + +```bash +pip install pcleaner[llm] +``` + +سپس آن را برای یک اجرای OCR با پرچم `--use-llm` فعال کنید: + +```bash +pcleaner ocr myfolder --output-path=output.txt --use-llm +``` + +یا آن را به‌صورت دائمی با تنظیم `llm_enabled = True` در بخش `[LLM]` از [پروفایل](#پروفایل‌ها) خود فعال کنید: + +```ini +[LLM] +llm_enabled = True +llm_model = meta-llama/Meta-Llama-3-8B-Instruct +llm_max_bubbles_per_prompt = 40 +llm_max_new_tokens = 1024 +llm_compression = # خالی بگذارید، یا "4bit" / "8bit" +llm_hf_token = # برای مدل‌های gated مثل meta-llama/* الزامی است +``` + +### نحوه کار + +1. پنل کلینر OCR را به‌طور معمول اجرا می‌کند و متن هر حباب تشخیص‌داده‌شده را جمع‌آوری می‌کند. +2. متن OCR از حباب‌های متعدد در یک prompt واحد LLM دسته‌بندی می‌شود (چون تولید airllm کند است، دسته‌بندی بسیار سریع‌تر از یک prompt به ازای هر حباب است). +3. LLM یک آرایه JSON از رشته‌های اصلاح‌شده برمی‌گرداند که جایگزین خروجی خام OCR می‌شوند. +4. اگر یک دسته شکست بخورد یا مدل تعداد اشتباهی آیتم برگرداند، متن OCR اصلی برای آن دسته حفظ می‌شود — بنابراین یک دسته بد هرگز کل اجرا را قطع نمی‌کند. + +### نکات + +- **با طراحی کند است:** airllm لایه‌های مدل را به دیسک offload می‌کند، بنابراین تولید بسیار کندتر از استنتاج معمول GPU است. این بهای اجرای مدل‌های بزرگ با RAM کم است. + +- **مدل‌های instruct-tuned** (مثلاً `Meta-Llama-3-8B-Instruct`) به‌شدت توصیه می‌شوند. + +- **مدل‌های gated** (مانند مخازن `meta-llama/*`) به یک [توکن Hugging Face](https://huggingface.co/settings/tokens) نیاز دارند. آن را از طریق `llm_hf_token` در پروفایل یا متغیر محیطی `HF_TOKEN` تنظیم کنید. + +- **فشرده‌سازی:** تنظیم `llm_compression` روی `4bit` یا `8bit` کوانتیزاسیون بلوکی را برای شتاب تا ~۳ برابر با افت دقت کم فعال می‌کند (نیازمند بسته `bitsandbytes`). + +- **GPU توصیه می‌شود:** airllm بر CUDA هدف‌گیری شده است. روی سیستم‌های فقط-CPU ممکن است مدل بارگذاری نشود — در این صورت اجرا به‌طور خودکار به خروجی خام OCR برمی‌گردد. + +--- + +## نمونه‌هایی از حباب‌های پیچیده + +| تصویر اصلی | پاک‌شده | +|:--------:|:-------:| +| ![Square bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/square_bubble_raw.png) | ![Square bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/square_bubble_clean.png) | +| ![Handwritten bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/handwritten_bubble_raw.png) | ![Handwritten bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/handwritten_bubble_clean.png) | +| ![Black bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/black_bubble_raw.png) | ![Black bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/black_bubble_clean.png) | +| ![Ray bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/ray_bubble_raw.png) | ![Ray bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/ray_bubble_clean.png) | +| ![Nightmare bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/nightmare_bubble_raw.png) | ![Nightmare bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/nightmare_bubble_clean.png) | +| ![Spikey bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/spikey_bubble_raw.png) | ![Spikey bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/spikey_bubble_clean.png) | +| ![Darkrays bubble raw](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/darkrays_bubble_raw.png) | ![Darkrays bubble clean](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/media/demo_bubbles/darkrays_bubble_clean.png) | + +--- + +## تشکر و قدردانی + +- [Comic Text Detector](https://github.com/dmMaze/comic-text-detector) برای یافتن حباب‌های متنی و تولید ماسک اولیه. + +- [Manga OCR](https://github.com/kha-white/manga-ocr) برای تشخیص حباب‌هایی که فقط شامل نمادها یا اعداد هستند، + و اجرای دستور اختصاصی OCR. + +- [Simple Lama Inpainting](https://github.com/enesmsahin/simple-lama-inpainting) برای inpaint کردن حباب‌هایی که نمی‌توان با ماسک پوشاند. + با استفاده از [مدل fine-tune‌شده توسط dreMaz](https://huggingface.co/dreMaz/AnimeMangaInpainting). + +- [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm) برای اجرای مدل‌های زبانی بزرگ با RAM کم از طریق offloading لایه‌ای به دیسک، که در قابلیت اصلاح هوشمند OCR استفاده می‌شود. + +--- + +## پروانه + +این پروژه تحت پروانه GNU General Public License v3.0 منتشر شده است — برای جزئیات به +فایل [LICENSE](https://raw.githubusercontent.com/VoxelCubes/PanelCleaner/master/LICENSE) مراجعه کنید. + +--- + +## نقشه راه + +- در حال حاضر هیچ قابلیت جدیدی برنامه‌ریزی نشده است. diff --git a/README.md b/README.md index 98e10f3..e663861 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ The two bottom pages are what the program can output: either just the transparen > [Usage](#usage) \ > [Profiles](#profiles) \ > [OCR](#ocr) \ +> [Smart OCR (LLM Post-Correction)](#smart-ocr-llm-post-correction) \ > [Examples](#examples-of-tricky-bubbles) \ > [Acknowledgements](#acknowledgements) \ > [License](#license) \ @@ -72,6 +73,8 @@ The two bottom pages are what the program can output: either just the transparen - Can also run OCR on the pages and output the text to a file. +- Optionally runs the OCR output through a large language model (via [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm)) to fix garbled text — "smart" OCR post-correction. + - Review cleaning and OCR output, including editing the OCR output interactively before saving it. - Interface available in: English, German, Bulgarian, Spanish @@ -308,6 +311,53 @@ For detailed installation instructions and additional information, please refer > Note: While Tesseract supports additional languages, Panel Cleaner will only utilize Tesseract for English and Japanese text recognition. English is installed by default. Follow the instructions here [Installing additional language packs](https://ocrmypdf.readthedocs.io/en/latest/languages.html) to install the Japanese language pack. +## Smart OCR (LLM Post-Correction) + +Panel Cleaner can optionally run the OCR output through a large language model (LLM) to fix garbled text — correcting misread characters, broken words, and stray punctuation — while preserving the original language and meaning. It does **not** translate. This is useful when the raw manga-ocr or Tesseract output is noisy and you want cleaner text for translation or archival. + +This feature is powered by [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm), which performs layer-wise disk offloading so that very large models (e.g. Llama-3-70B) can run on just a few gigabytes of RAM, at the cost of slow token generation. + +### Enabling LLM Correction + +The feature is **off by default**. First, install the optional `[llm]` extra: + +```bash +pip install pcleaner[llm] +``` + +Then enable it for a single OCR run with the `--use-llm` flag: + +```bash +pcleaner ocr myfolder --output-path=output.txt --use-llm +``` + +Or enable it permanently by setting `llm_enabled = True` in the `[LLM]` section of your [profile](#profiles): + +```ini +[LLM] +llm_enabled = True +llm_model = meta-llama/Meta-Llama-3-8B-Instruct +llm_max_bubbles_per_prompt = 40 +llm_max_new_tokens = 1024 +llm_compression = # leave empty, or use "4bit" / "8bit" +llm_hf_token = # required for gated models like meta-llama/* +``` + +### How It Works + +1. Panel Cleaner runs OCR as usual, collecting the text from every detected bubble. +2. The OCR text from many bubbles is batched into a single LLM prompt (since airllm generation is slow, batching is much faster than one prompt per bubble). +3. The LLM returns a JSON array of corrected strings, which replace the raw OCR output. +4. If a batch fails or the model returns the wrong number of items, the original OCR text is kept for that batch — so a single bad batch never aborts the whole run. + +### Notes + +- **Slow by design:** airllm offloads model layers to disk, so generation is far slower than a normal GPU inference. This is the trade-off for running large models on minimal RAM. +- **Instruct-tuned models** (e.g. `Meta-Llama-3-8B-Instruct`) are strongly recommended. +- **Gated models** (such as the `meta-llama/*` repos) require a [Hugging Face token](https://huggingface.co/settings/tokens). Set it via `llm_hf_token` in the profile or the `HF_TOKEN` environment variable. +- **Compression:** setting `llm_compression` to `4bit` or `8bit` enables block-wise quantization for up to ~3x faster inference at a small accuracy cost (requires the `bitsandbytes` package). +- **GPU recommended:** airllm targets CUDA. On CPU-only machines the model may fail to load — in that case the run falls back to raw OCR output automatically. + ## Examples of Tricky Bubbles | Original | Cleaned | @@ -332,6 +382,8 @@ For detailed installation instructions and additional information, please refer - [Simple Lama Inpainting](https://github.com/enesmsahin/simple-lama-inpainting) for inpainting bubbles that can't be masked out. Using the fine-tuned [Model by dreMaz](https://huggingface.co/dreMaz/AnimeMangaInpainting). +- [airllm](https://github.com/lyogavin/Anima/tree/main/air_llm) for running large language models on minimal RAM via layer-wise disk offloading, used by the optional smart OCR post-correction feature. + ## License diff --git a/pcleaner/config.py b/pcleaner/config.py index b3b8c59..0aba9d0 100644 --- a/pcleaner/config.py +++ b/pcleaner/config.py @@ -932,6 +932,109 @@ def fix(self) -> None: self.max_inpainting_radius = max(self.min_inpainting_radius, self.max_inpainting_radius) +@define +class LLMConfig: + # EXPERIMENTAL: Optional "smart" OCR post-correction via a large language model. + # Requires the optional [llm] extra: pip install pcleaner[llm] + # airllm performs layer-wise disk offloading so very large models run on minimal RAM. + llm_enabled: bool = False + # Hugging Face repo id or local path of the model to load with airllm. + llm_model: str = "meta-llama/Meta-Llama-3-8B-Instruct" + # How many OCR bubbles to pack into a single LLM prompt. Higher saves on the very + # slow generation passes; too high may exceed the model's context window. + llm_max_bubbles_per_prompt: int = 40 + # Maximum new tokens to generate per prompt. Must be large enough for the JSON array + # of corrections for one batch. + llm_max_new_tokens: int = 1024 + # Optional block-wise quantization for ~3x speedup at a small accuracy cost. + # One of: "", "4bit", "8bit". Empty/none disables it. + llm_compression: str = "" + # Optional Hugging Face token, needed for gated models such as meta-llama/Llama-*. + llm_hf_token: str = "" + + def export_to_conf( + self, config_updater: cu.ConfigUpdater, add_after_section: str, gui_mode: bool = False + ) -> None: + """ + Write the config to the config updater object. + + :param config_updater: An existing config updater object. + :param add_after_section: The section to add the new section after. + :param gui_mode: Whether to format the config for the GUI. + """ + config_str = f"""\ + [LLM] + + # EXPERIMENTAL FEATURE: Optional "smart" OCR post-correction using a large language model. + # When enabled, the OCR text from the `pcleaner ocr` command is passed through an LLM that + # fixes garbled manga_ocr / tesseract output (misread characters, broken words, stray + # punctuation) while preserving the original language and meaning. It does not translate. + # [CLI: Use the --use-llm flag on the `pcleaner ocr` command to enable this for a single run.] + # This requires the optional [llm] extra to be installed: pip install pcleaner[llm] + # airllm performs layer-wise disk offloading, so large models (e.g. Llama-3-70B) can run on + # a few GB of RAM, at the cost of slow token generation. + llm_enabled = {self.llm_enabled} + + # The Hugging Face repository id (e.g. "meta-llama/Meta-Llama-3-8B-Instruct") or a local path + # of the model to load with airllm. Instruct-tuned models are strongly recommended. + # Note: gated models (like the meta-llama ones) require a Hugging Face token, see below. + llm_model = {self.llm_model} + + # How many OCR bubbles to pack into a single LLM prompt. Since airllm generation is slow, + # batching many bubbles into one prompt is much faster than one prompt per bubble. + # Lower this if you exceed the model's context window or run out of memory. + llm_max_bubbles_per_prompt = {self.llm_max_bubbles_per_prompt} + + # The maximum number of new tokens to generate per prompt. This needs to be large enough to + # hold the JSON array of corrections for one batch. + llm_max_new_tokens = {self.llm_max_new_tokens} + + # Optional block-wise quantization for up to ~3x faster inference at a small accuracy cost. + # Leave empty for no compression, or set to "4bit" or "8bit". Requires the bitsandbytes package. + llm_compression = {self.llm_compression} + + # Optional Hugging Face token, required to download gated models such as the meta-llama ones. + # Leave empty if your model is not gated. You can also set the HF_TOKEN environment variable. + llm_hf_token = {self.llm_hf_token} + + """ + llm_conf = cu.ConfigUpdater() + llm_conf.read_string(multi_left_strip(format_for_version(config_str, gui_mode))) + llm_section = llm_conf["LLM"] + config_updater[add_after_section].add_after.space(2).section(llm_section.detach()) + + def import_from_conf(self, config_updater: cu.ConfigUpdater) -> None: + """ + Read the config from the config updater object. + + :param config_updater: An existing config updater object. + """ + section = "LLM" + if not config_updater.has_section(section): + logger.debug(f"No {section} section found in the profile, using defaults.") + return + + try_to_load(self, config_updater, section, bool, "llm_enabled") + try_to_load(self, config_updater, section, str, "llm_model") + try_to_load(self, config_updater, section, int, "llm_max_bubbles_per_prompt") + try_to_load(self, config_updater, section, int, "llm_max_new_tokens") + try_to_load(self, config_updater, section, str, "llm_compression") + try_to_load(self, config_updater, section, str, "llm_hf_token") + + def fix(self) -> None: + if self.llm_max_bubbles_per_prompt < 1: + self.llm_max_bubbles_per_prompt = 1 + if self.llm_max_new_tokens < 1: + self.llm_max_new_tokens = 1 + if self.llm_compression not in ("", "4bit", "8bit"): + logger.warning( + f"Invalid llm_compression '{self.llm_compression}', disabling compression." + ) + self.llm_compression = "" + if not self.llm_model.strip(): + self.llm_model = "meta-llama/Meta-Llama-3-8B-Instruct" + + @define class Profile: """ @@ -944,6 +1047,7 @@ class Profile: masker: MaskerConfig = field(factory=MaskerConfig) denoiser: DenoiserConfig = field(factory=DenoiserConfig) inpainter: InpainterConfig = field(factory=InpainterConfig) + llm: LLMConfig = field(factory=LLMConfig) def bundle_config(self, gui_mode: bool = False) -> cu.ConfigUpdater: """ @@ -959,6 +1063,7 @@ def bundle_config(self, gui_mode: bool = False) -> cu.ConfigUpdater: self.masker.export_to_conf(config_updater, "Preprocessor", gui_mode=gui_mode) self.denoiser.export_to_conf(config_updater, "Masker", gui_mode=gui_mode) self.inpainter.export_to_conf(config_updater, "Denoiser", gui_mode=gui_mode) + self.llm.export_to_conf(config_updater, "Inpainter", gui_mode=gui_mode) return config_updater def hash_current_values(self) -> int: @@ -1027,6 +1132,7 @@ def load(cls, path: Path) -> "Profile": profile.masker.import_from_conf(config) profile.denoiser.import_from_conf(config) profile.inpainter.import_from_conf(config) + profile.llm.import_from_conf(config) profile.fix() except Exception: logger.exception(f"Failed to load profile from {path}") @@ -1061,6 +1167,7 @@ def fix(self) -> None: self.masker.fix() self.denoiser.fix() self.inpainter.fix() + self.llm.fix() @define diff --git a/pcleaner/gui/profile_parser.py b/pcleaner/gui/profile_parser.py index d3e1866..729530d 100644 --- a/pcleaner/gui/profile_parser.py +++ b/pcleaner/gui/profile_parser.py @@ -288,23 +288,33 @@ def _get_text() -> str | None: name_mapper = { LayeredExport.NONE: self.tr("None", "Layered export option"), LayeredExport.PSD_BULK: self.tr("PSD Bulk", "Layered export option"), - LayeredExport.PSD_PER_IMAGE: self.tr("PSD Per Image", "Layered export option"), + LayeredExport.PSD_PER_IMAGE: self.tr( + "PSD Per Image", "Layered export option" + ), } case EntryTypes.LanguageCode: enum_class = LanguageCode name_mapper = { LanguageCode.detect_box: self.tr("Detect per box", "Language code option"), - LanguageCode.detect_page: self.tr("Detect per page", "Language code option"), + LanguageCode.detect_page: self.tr( + "Detect per page", "Language code option" + ), LanguageCode.jpn: self.tr("Japanese", "Language code option"), LanguageCode.eng: self.tr("English", "Language code option"), LanguageCode.kor: self.tr("Korean", "Language code option"), LanguageCode.kor_vert: self.tr("Korean (vertical)", "Language code option"), - LanguageCode.chi_sim: self.tr("Chinese - Simplified", "Language code option"), - LanguageCode.chi_tra: self.tr("Chinese - Traditional", "Language code option"), + LanguageCode.chi_sim: self.tr( + "Chinese - Simplified", "Language code option" + ), + LanguageCode.chi_tra: self.tr( + "Chinese - Traditional", "Language code option" + ), LanguageCode.sqi: self.tr("Albanian", "Language code option"), LanguageCode.ara: self.tr("Arabic", "Language code option"), LanguageCode.aze: self.tr("Azerbaijani", "Language code option"), - LanguageCode.aze_cyrl: self.tr("Azerbaijani - Cyrilic", "Language code option"), + LanguageCode.aze_cyrl: self.tr( + "Azerbaijani - Cyrilic", "Language code option" + ), LanguageCode.ben: self.tr("Bengali", "Language code option"), LanguageCode.bul: self.tr("Bulgarian", "Language code option"), LanguageCode.mya: self.tr("Burmese", "Language code option"), @@ -769,4 +779,5 @@ def to_display_name(name: str) -> str: " ".join(word.capitalize() for word in s2.split(" ")) .replace("Ai ", "AI ") .replace("Ocr", "OCR") + .replace("Llm", "LLM") ) diff --git a/pcleaner/llm_correction.py b/pcleaner/llm_correction.py new file mode 100644 index 0000000..eb08634 --- /dev/null +++ b/pcleaner/llm_correction.py @@ -0,0 +1,226 @@ +"""Smart OCR post-correction using a large language model via airllm. + +airllm performs layer-wise disk offloading so very large models (e.g. Llama-3-70B) +can run on a few GB of RAM, at the cost of slow token generation. Because generation +is slow, the OCR text from many bubbles is batched into a single prompt and the model +is asked to return a JSON list of corrected snippets in one pass. + +This is an optional feature. Install the extra with:: + + pip install pcleaner[llm] + +and enable it with the ``--use-llm`` flag on the ``pcleaner ocr`` command (or by setting +``llm_enabled = True`` in the profile's ``[LLM]`` section). +""" + +import json +import re +from typing import Sequence + +from loguru import logger +from tqdm import tqdm + +import pcleaner.structures as st + +# Marker the model is told to place around the JSON list so we can locate it +# reliably even when the model adds chatter around it. +_JSON_FENCE = "```" +_SYSTEM_PROMPT = ( + "You are an OCR post-correction engine for manga and comic speech bubbles. " + "You receive a JSON array of raw OCR strings, in reading order. Fix obvious OCR " + "errors: misread characters, broken/garbled text, stray punctuation, and missing or " + "duplicate characters, while preserving the original language, meaning, numbers, and " + "spacing. Do not translate. Do not add commentary. If a string is already correct or " + "unintelligible, return it unchanged. Output ONLY a JSON array of the same length and " + "order as the input." +) + + +def _import_airllm(): + """Lazily import airllm so the rest of the program works without the [llm] extra.""" + try: + from airllm import AutoModel # type: ignore + except ImportError as exc: + raise ImportError( + "airllm is not installed. Install the optional LLM extra with: " + "pip install pcleaner[llm]" + ) from exc + return AutoModel + + +class LLMCorrector: + """Loads an airllm model once and corrects batches of OCR text. + + airllm generation is slow (layer-wise disk offloading), so callers should batch + many bubbles into one :meth:`correct_texts` call rather than one per bubble. + """ + + def __init__( + self, + model_name: str, + *, + max_new_tokens: int = 1024, + max_bubbles_per_prompt: int = 40, + compression: str | None = None, + hf_token: str | None = None, + ) -> None: + self.model_name = model_name + self.max_new_tokens = max_new_tokens + self.max_bubbles_per_prompt = max(1, max_bubbles_per_prompt) + self.compression = compression + self.hf_token = hf_token + + AutoModel = _import_airllm() + logger.info(f"Loading airllm model '{model_name}' (this may take a while)...") + kwargs = {} + if compression: + kwargs["compression"] = compression + if hf_token: + kwargs["hf_token"] = hf_token + self.model = AutoModel.from_pretrained(model_name, **kwargs) + logger.info("airllm model loaded.") + + def correct_texts(self, texts: Sequence[str]) -> list[str]: + """Correct a list of OCR strings, returning a list of the same length. + + Bubbles are processed in chunks of ``max_bubbles_per_prompt``. On any error + for a chunk, the original strings are returned unchanged for that chunk, + so a single bad batch never aborts the whole run. + """ + results: list[str] = list(texts) + for start in range(0, len(texts), self.max_bubbles_per_prompt): + chunk = list(texts[start : start + self.max_bubbles_per_prompt]) + if not any(text.strip() for text in chunk): + # Skip purely-empty batches; nothing to correct. + continue + try: + corrected = self._correct_chunk(chunk) + except Exception: + logger.exception( + f"LLM correction failed for bubbles {start}-{start + len(chunk)}; " + "keeping original OCR output." + ) + continue + # Guard against the model returning the wrong number of items. + if len(corrected) == len(chunk): + results[start : start + len(chunk)] = corrected + else: + logger.warning( + f"LLM returned {len(corrected)} corrections for {len(chunk)} bubbles; " + "keeping original OCR output for this batch." + ) + return results + + def _correct_chunk(self, chunk: list[str]) -> list[str]: + prompt = self._build_prompt(chunk) + raw_output = self._generate(prompt) + return self._parse(raw_output, chunk) + + def _build_prompt(self, chunk: list[str]) -> str: + payload = json.dumps(chunk, ensure_ascii=False) + return ( + f"{_SYSTEM_PROMPT}\n\n" + f"Input JSON array:\n{payload}\n\n" + f"Output the corrected JSON array wrapped in {_JSON_FENCE} fences." + ) + + def _generate(self, prompt: str) -> str: + tokenizer = self.model.tokenizer + + # Prefer the model's chat template for instruct models (e.g. Llama-3-Instruct). + input_text = self._apply_chat_template(prompt) + + input_tokens = tokenizer( + [input_text], + return_tensors="pt", + return_attention_mask=False, + truncation=True, + padding=False, + ) + input_ids = input_tokens["input_ids"] + if hasattr(input_ids, "cuda"): + input_ids = input_ids.cuda() + + generation_output = self.model.generate( + input_ids, + max_new_tokens=self.max_new_tokens, + use_cache=True, + return_dict_in_generate=True, + ) + # Strip the prompt tokens, then decode only the newly generated part. + prompt_len = input_ids.shape[-1] + new_tokens = generation_output.sequences[0][prompt_len:] + return tokenizer.decode(new_tokens, skip_special_tokens=True) + + def _apply_chat_template(self, prompt: str) -> str: + tokenizer = self.model.tokenizer + apply = getattr(tokenizer, "apply_chat_template", None) + if callable(apply): + try: + messages = [ + {"role": "system", "content": _SYSTEM_PROMPT}, + {"role": "user", "content": prompt[len(_SYSTEM_PROMPT) :].lstrip()}, + ] + templated = apply(messages, tokenize=False, add_generation_prompt=True) + if isinstance(templated, str) and templated.strip(): + return templated + except Exception: + logger.debug("Chat template not applicable; falling back to raw prompt.") + return prompt + + @staticmethod + def _parse(raw_output: str, original: list[str]) -> list[str]: + """Extract the JSON array of corrections from the model output. + + Tolerates code fences and surrounding chatter. Always returns a list + whose length matches ``original`` on success; mismatches bubble up to + the caller (which keeps the originals). + """ + text = raw_output.strip() + + # Strip ```json ... ``` or ``` ... ``` fences if present. + fence_match = re.search(r"```(?:json)?\s*(.*?)```", text, re.DOTALL) + if fence_match: + text = fence_match.group(1).strip() + + # Find the outermost JSON array if there's chatter around it. + array_match = re.search(r"\[.*\]", text, re.DOTALL) + if array_match: + text = array_match.group(0) + + parsed = json.loads(text) + if not isinstance(parsed, list): + raise ValueError("LLM did not return a JSON array.") + # Coerce every item to a string (in case the model emitted bare numbers/bools). + return ["" if item is None else str(item) for item in parsed] + + +def correct_ocr_analytics( + ocr_analytics: list[st.OCRAnalytic], + corrector: LLMCorrector, +) -> list[st.OCRAnalytic]: + """Run LLM correction over the OCR analytics, returning new analytics. + + The box coordinates and other analytics are preserved; only the OCR text in + ``removed_box_data`` is replaced with the corrected version. The input list + is not modified (``OCRAnalytic`` is frozen). + """ + corrected_analytics: list[st.OCRAnalytic] = [] + for analytic in tqdm(ocr_analytics, desc="LLM correcting OCR"): + texts = [text for text, _ in analytic.removed_box_data] + boxes = [box for _, box in analytic.removed_box_data] + if not texts: + corrected_analytics.append(analytic) + continue + corrected_texts = corrector.correct_texts(texts) + corrected_box_data = list(zip(corrected_texts, boxes)) + corrected_analytics.append( + st.OCRAnalytic( + path=analytic.path, + num_boxes=analytic.num_boxes, + box_sizes_ocr=analytic.box_sizes_ocr, + box_sizes_removed=analytic.box_sizes_removed, + removed_box_data=corrected_box_data, + ) + ) + return corrected_analytics diff --git a/pcleaner/main.py b/pcleaner/main.py index 26d72d1..ec7c237 100644 --- a/pcleaner/main.py +++ b/pcleaner/main.py @@ -10,7 +10,7 @@ open | delete | set-default | repair | purge-missing) [--debug] pcleaner gui [ ...] [--debug] - pcleaner ocr [ ...] [--output-path=] [--csv] [--profile=] [--cache-masks] [--debug] + pcleaner ocr [ ...] [--output-path=] [--csv] [--profile=] [--cache-masks] [--use-llm] [--debug] pcleaner config (show | open) pcleaner cache clear (all | models | cleaner) pcleaner load models [--cuda | --cpu | --both] [--force] @@ -81,6 +81,10 @@ The saved name of the profile to open, delete, or set as default. --output-path= The path to save the OCR output file to. --csv Save the output of the OCR as a CSV file + --use-llm Run the OCR output through a large language model (via airllm) to + fix garbled text before saving. Off by default. Requires the + optional [llm] extra: pip install pcleaner[llm] + Configure the model in the [LLM] section of the profile. --cuda Load the torch models that support CUDA. They will only be used if supported. --cpu Load the open cv2 models that are optimized for CPU. They will only be used as a fallback, unless specified in the config. @@ -146,6 +150,7 @@ import pcleaner.image_export as ie import pcleaner.inpainting as ip import pcleaner.image_ops as ops +import pcleaner.llm_correction as llm_corr import pcleaner.masker as ma import pcleaner.memory_watcher as mw import pcleaner.model_downloader as md @@ -234,7 +239,7 @@ def main() -> None: except OSError as e: print(f"Error: {e}") sys.exit(1) - run_ocr(config, image_paths, args.output_path, args.cache_masks, args.csv) + run_ocr(config, image_paths, args.output_path, args.cache_masks, args.csv, args.use_llm) elif args.cache and args.clear: config = cfg.load_config() @@ -767,6 +772,7 @@ def run_ocr( output_path: str | None, cache_masks: bool, csv_output: bool, + use_llm: bool = False, ): """ Run OCR on the given images. This is a byproduct of the pre-processing step, @@ -777,6 +783,7 @@ def run_ocr( :param output_path: The path to output the results to. :param cache_masks: Whether to cache the masks. :param csv_output: Whether to output CSV data + :param use_llm: Whether to run the OCR output through an LLM for correction. """ if config.show_oom_warnings: mw.start_memory_watcher() @@ -882,6 +889,11 @@ def run_ocr( handle_merging_ocr_splits(cache_dir, profile.general.merge_after_split, ocr_analytics) + # Optionally run the OCR output through a large language model (via airllm) to + # fix garbled manga_ocr / tesseract output. This is slow but "smarter". + if use_llm or profile.llm.llm_enabled: + ocr_analytics = run_llm_correction(profile, ocr_analytics) + print("\nDetected Text:") # Output the OCRed text from the analytics. text_out = ocr.format_output( @@ -910,6 +922,46 @@ def run_ocr( logger.exception(e) +def run_llm_correction( + profile: cfg.Profile, ocr_analytics: list[st.OCRAnalytic] +) -> list[st.OCRAnalytic]: + """ + Run the OCR analytics through a large language model (via airllm) to correct garbled + OCR output. If airllm isn't installed or the model fails to load, the original analytics + are returned unchanged so the rest of the OCR run still completes. + + :param profile: The profile, carrying the [LLM] configuration. + :param ocr_analytics: The OCR analytics produced by the preprocessor. + :return: New analytics with corrected text, or the originals on failure. + """ + llm_conf = profile.llm + try: + corrector = llm_corr.LLMCorrector( + llm_conf.llm_model, + max_new_tokens=llm_conf.llm_max_new_tokens, + max_bubbles_per_prompt=llm_conf.llm_max_bubbles_per_prompt, + compression=llm_conf.llm_compression or None, + hf_token=llm_conf.llm_hf_token or None, + ) + except ImportError as e: + print(f"\n{e}") + print("Skipping LLM correction; saving raw OCR output.") + logger.warning(f"LLM correction skipped: {e}") + return ocr_analytics + except Exception: + logger.exception("Failed to load airllm model; skipping LLM correction.") + print("\nFailed to load the airllm model. Skipping LLM correction; saving raw OCR output.") + return ocr_analytics + + print("\nRunning LLM OCR post-correction (this is slow)...") + try: + return llm_corr.correct_ocr_analytics(ocr_analytics, corrector) + except Exception: + logger.exception("LLM correction failed; saving raw OCR output.") + print("\nLLM correction failed. Saving raw OCR output.") + return ocr_analytics + + def clear_cache(config: cfg.Config, all_cache: bool, models: bool, images: bool) -> None: """ Clear the cache. diff --git a/pyproject.toml b/pyproject.toml index b8840f0..64bf0f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,13 @@ runtime-torch = [ runtime-gui = [ "PySide6", ] +runtime-llm = [ + # Optional "smart" OCR post-correction via a large LLM run on minimal RAM. + # Enable with: pip install pcleaner[llm] (or the runtime-llm dependency group). + # airllm performs layer-wise disk offloading so very large models (e.g. Llama-3-70B) + # can run on a few GB of RAM, at the cost of slow token generation. + "airllm", +] runtime-dbus = [ "dbus-python; platform_system == \"Linux\"", ] diff --git a/setup-cli-gui.cfg b/setup-cli-gui.cfg index 426afa5..705edc2 100644 --- a/setup-cli-gui.cfg +++ b/setup-cli-gui.cfg @@ -59,6 +59,10 @@ packages=find: [options.extras_require] DBUS = dbus-python; platform_system == "Linux" +LLM = + # Optional "smart" OCR post-correction. Enables the --use-llm flag for the `pcleaner ocr` command. + # Large models are loaded with airllm's layer-wise disk offloading, so they run on minimal RAM. + airllm [options.package_data] pcleaner= diff --git a/setup-cli.cfg b/setup-cli.cfg index d6b7a2e..a8bc8b4 100644 --- a/setup-cli.cfg +++ b/setup-cli.cfg @@ -58,6 +58,10 @@ packages=find: [options.extras_require] DBUS = dbus-python; platform_system == "Linux" +LLM = + # Optional "smart" OCR post-correction. Enables the --use-llm flag for the `pcleaner ocr` command. + # Large models are loaded with airllm's layer-wise disk offloading, so they run on minimal RAM. + airllm ; No data needed. ;[options.package_data]