← بازگشت به یادداشت‌ها

ساخت یک ترجمه‌گر دسکتاپ فارسی‌محور برای کتاب‌های PDF و EPUB: تکه‌تکه‌سازی، کانتکست غلتان و تمپلیت ساختار هر کتاب

ساخت یک ترجمه‌گر دسکتاپ فارسی‌محور برای کتاب‌های PDF و EPUB: تکه‌تکه‌سازی، کانتکست غلتان و تمپلیت ساختار هر کتاب

ترجمهٔ یک کتاب ۳۰۰ صفحه‌ای در پنجرهٔ چت، شکست‌های قابل پیش‌بینی دارد. وقتی ترجمه خواسته‌ای، مدل خلاصه می‌کند. اصطلاحات از فصلی به فصل دیگر عوض می‌شوند و یک مفهوم، با سه واژهٔ فارسی مختلف ظاهر می‌شود. سرصفحه‌ها و شماره‌های صفحه هم مثل متن اصلی ترجمه می‌شوند. و یک نشستِ کرشی، یک ساعت کار را از بین می‌برد.

می‌خواستم بدانم برای انجام درست این کار چه چیزی لازم است، پس Kaman AI Translator را ساختم — یک اپ دسکتاپ ویندوزی که کتاب‌های PDF و EPUB را صفحه‌به‌صفحه به فارسی دانشگاهی ترجمه می‌کند و برعکس، با شکست‌های تکراری که با کد محلی و خوانا حل شده‌اند.

ریپو دو فایل پایتون و یک مجموعه تست است. بخش جالبش رابط کاربری نیست؛ خط لولهٔ زیر آن است.

محدودیتی که همه‌چیز را شکل داد

«هوش مصنوعی» اینجا یک سرویس میزبان‌شده درون اپ نیست. یک لایهٔ موتور نازک و قابل‌تعویض است — SDK رسمی گوگل جیمینی، یا هر endpoint سازگار با OpenAI (Ollama، LM Studio، llama.cpp، یا یک تجمیع‌کنندهٔ ابری).

این تفاوت مهم است چون استراتژی ترجمه — تکه‌تکه‌سازی، غلتان کانتکست، تمپلیت ساختار، کنترل کیفیت — کد محلی و قطعی است. مدل ابزاری است که این استراتژی صدایش می‌زند؛ هیچ‌وقت چیزی نیست که تصمیم بگیرد. به همین دلیل است که کل خط لوله بدون سرور، با یک موتور جعلی و بدون کلید API، قابل تست است.

تکه‌تکه‌سازی که جمله را نصف نمی‌کند

ورودی بلند اولین چیزی است که خراب می‌شود. بودجهٔ پیش‌فرض هر درخواست ۶۰۰۰ کاراکتر است و شکافنده در سه سطح کار می‌کند: اول مرز پاراگراف، بعد مرز جمله، و بعد کلمه — هرگز وسط جمله.

سپس واحدها زیر بودجه دوباره بسته‌بندی می‌شوند، پس یک درخواست صرفاً صرفِ یک پاراگراف بزرگ نمی‌شود و هیچ تکه‌ای پر از تکه‌های ناقص به مدل نمی‌رسد.

دنبالهٔ کانتکست غلتان همراه هر تکه می‌رود: ۷۰۰ کاراکتر آخر ترجمهٔ قبلی به پرامپت اضافه می‌شود تا اصطلاحات و لحن در طول یک فصل یکدست بمانند و هر صفحه از صفر شروع نشود. این تنظیم را می‌شود صفر کرد و کل قابلیتش را خاموش کرد، ولی پیش‌فرض روشن است چون ارزان‌ترین بهبود کیفیت در کل اپ است.

تمپلیت ساختار مخصوص هر کتاب

جذاب‌ترین بخش منطق محلی، پاسخ به یک مشکل مشخص است: چطور جلوی ترجمه‌شدن یک برچسب تکراری به پنج شکل مختلف در یک کتاب را بگیریم؟

اپ، برای هر کتاب، به‌صورت محلی و رایگان، دو چیز را تشخیص می‌دهد:

  • سرفصل‌ها — عنوان فصل باید مثل عنوان فصل خوانده شود، نه مثل متن بدنه.
  • برچسب‌های تکراری — چیزهایی مثل «آیا می‌دانید؟» یا «فعالیت‌ها» که ده‌ها بار می‌آیند و باید هر بار دقیقاً یکسان ترجمه شوند.

هر دو در پرامپتِ هر درخواستِ آن کتاب تزریق می‌شوند، پس هر برچسب همیشه یکسان ترجمه می‌شود. هر دو دستی هم قابل ویرایش‌اند، و یک پاس AI اختیاری می‌تواند فهرست مطالب را بخواند و ساختار عمیق‌تری استخراج کند — با یک درخواست API اضافه.

تشخیص ارزان است: خط‌هایی که دست‌کم در یک‌چهارم صفحات نمونه‌شده، نزدیک لبهٔ بالا یا پایین صفحه تکرار شوند، سرصفحهٔ تکراری در نظر گرفته می‌شوند و پیش از آنکه مدل آن‌ها را ببیند حذف می‌شوند. کلماتی که با خط تیره شکسته شده‌اند دوباره به هم چسبانده می‌شوند — exam- به‌علاوهٔ ple می‌شود example — و شمارهٔ فهرست‌ها و فرمول‌های میان‌متن محافظت می‌شوند.

پرامپت‌های الزام‌آور و یک دور دوم که کار را بررسی می‌کند

پرامپت‌ها صریح‌اند: ترجمه نباید حذف کند، نباید خلاصه کند، و باید ساختار پاراگراف، اعداد، معادلات و نشانه‌های ارجاع مثل [12] و یک اصطلاح ثابت برای هر مفهوم را حفظ کند.

بعد از آن، یک حلقهٔ QC اختیاری هست: یک دور دوم ترجمه را با متن مبدأ خودش مقایسه می‌کند، لغزش معنایی و حذف‌ها را علامت می‌زند و نسخهٔ اصلاح‌شده برمی‌گرداند. این واقعاً یک فراخوانی دوم است و رابط کاربری هم همین را می‌گوید — هزینهٔ این پاس تقریباً دوبرابر است.

یک لایهٔ ایمنی JSON هم هست. مدل‌ها گاهی با JSON خام یا با یک شیء JSON پیچیده در متن پاسخ می‌دهند. این پاسخ‌ها باز می‌شوند، ترمیم می‌شوند یا به‌عنوان متن ساده پذیرفته می‌شوند، پس کاربر هرگز آشغال JSON در کادر خروجی نمی‌بیند.

صفحه‌های اسکن‌شده رد نمی‌شوند

کتاب اسکن‌شده لایهٔ متنی ندارد. ترجمهٔ سادهٔ بی‌دقت، صفحهٔ خالی را صفحهٔ خالی می‌گیرد و کتاب اسکن‌شدهٔ ۳۰۰ صفحه‌ای می‌شود ۳۰۰ صفحهٔ گمشده.

اینجا، متن استخراج‌شدهٔ کاملاً خالی مسیر دیگری را فعال می‌کند: صفحه به‌صورت PNG با بزرگ‌نمایی ۲× (حدود ۱۴۴ DPI) رندر می‌شود و به مدلی با توانایی دیدن فرستاده می‌شود که آن را می‌خواند و در یک قدم ترجمه می‌کند.

دو محدودیت صادقانه قابل گفتن است. ارائه‌دهنده باید بتواند تصویر بگیرد — یک مدل محلی فقط‌متنی این کار را نمی‌تواند، و به همین دلیل مسیر صفحهٔ اسکن‌شده عملاً به سمت جیمینی می‌رود. و چون این صفحه متن مبدأ استخراج‌شده ندارد، حلقهٔ QC برای صفحه‌های اسکن‌شده اجرا نمی‌شود: چیزی مستقل برای راستی‌آزمایی ترجمه وجود ندارد. این صفحه‌ها بیش از صفحه‌های متنی به تیک «بازبینی شد» انسانی نیاز دارند.

هیچ‌چیز هرگز گم نمی‌شود

مهم‌ترین چیزی که برایم مهم بود این بود که کار تمام‌شده هرگز گم نشود — نه با کرش، نه با توقف سهمیه، نه با ری‌استارت.

هر کتاب یک فایل وضعیت JSON دارد که نامش از هش مسیر مطلق آن ساخته می‌شود و بعد از هر تکه صفحه، به‌صورت اتمیک نوشته می‌شود: داده اول در یک فایل موقت نوشته و بعد با os.replace جای خود را می‌گیرد، پس حتی اگر پروسه وسط ذخیره کشته شود، فایل هرگز نیمه‌نوشته دیده نمی‌شود. یک threading.Lock هم کل عملیات را محافظت می‌کند.

وقتی کتابی با صفحه‌های ذخیره‌شده را دوباره باز می‌کنی، اپ یک سؤال صریح می‌پرسد — از اولین صفحهٔ ناتمام ادامه بده یا از اول شروع کن — و ادامه‌دادن پیش‌فرض است. حتی در حالت شروع دوباره، تمپلیت ساختار حفظ می‌شود، چون کتاب تغییری نکرده و تشخیص دوباره‌اش فقط اتلاف وقت است.

هزینهٔ یک اجرای از‌دست‌رفته حداکثر همان صفحه‌ای است که در حال پردازش بود.

کلید رمز‌شده است و اپ با چیز دیگری حرف نمی‌زند

کلید API یک‌بار در دیالوگ تنظیمات وارد می‌شود، در رابط پوشانده است و با Windows DPAPI رمز می‌شود — همان Data Protection API ویندوز، مقید به حساب کاربری و به سیستم خودت. کاربر دیگری روی همان سیستم، یا همان کاربر روی سیستم دیگر، نمی‌تواند آن را رمزگشایی کند.

برای سکوهای بدون DPAPI یک fallback وجود دارد و اپ آن را پنهان نمی‌کند: کلید به ذخیره‌سازی base64 با برچسبِ صریح برمی‌گردد و دلیلش در encrypt_fallback.log نوشته می‌شود — مهر زمان و پیام خطا، و هیچ بخشی از secret. base64 رمزنگاری نیست، کدگذاری است، و پروژه این را صریح می‌گوید به‌جای اینکه وانمود کند محافظت شده.

کل سطح شبکهٔ اپ یک سطر است: ارائه‌دهنده‌ای که خودت تنظیم کرده‌ای. نه telemetry، نه بررسی به‌روزرسانی، نه گزارش کرش، نه سرویس شخص ثالث، نه آنالیتیک. دیالوگ تنظیمات مسیر دقیق پوشهٔ داده را نشان می‌دهد و دکمهٔ یک‌کلیکی برای باز کردنش دارد.

اگر می‌خواهی هیچ‌چیز از دستگاه خارج نشود، ارائه‌دهندهٔ سازگار با OpenAI را به Ollama روی لوکال‌هاست وصل کن — دقیقاً برای همین، آدرس پیش‌فرض (http://localhost:11434/v1) همین‌جاست.

رابط کاربری فارسی است، و این یعنی RTL تا ته ته

این یک چیدمان انگلیسی با رشته‌های فارسی نیست. کل برنامه راست‌چین است و در سراسر اپ از ارقام فارسی استفاده می‌شود (۰۱۲۳۴۵۶۷۸۹).

این پیامدهای واقعی دارد و به‌جای اینکه به شانس واگذار شود، در مجموعه تست assert می‌شود:

  • «صفحهٔ بعد» به چپ اشاره می‌کند و «صفحهٔ قبل» به راست، چون در RTL جهت حرکت همین است.
  • پنل مبدأ اول به چیدمان اضافه می‌شود، پس روی راست می‌نشیند — جایی که خوانندهٔ فارسی انتظار دارد متن اصلی باشد.
  • درصد پیشرفت راست‌چین است و متن داخلی نوار پیشرفت پنهان شده، چون Qt آن را سمت چپ می‌کشد.
  • مقدار spin boxها با یک فیلتر اختصاصی راست‌چین می‌شود که هنگام تغییر تم دوباره اعمال می‌شود، چون Qt چیدمان spin box را بازسازی می‌کند و این تراز را از دست می‌دهد.
  • دکمهٔ کپی وقتی صفحهٔ جاری ترجمه دارد آبی است و در غیر این صورت خاکستری، پس وضعیتش بدون نیاز به tooltip خوانده می‌شود.

در طول یک اجرای طولانی، اپ صفحه‌ای را که در حال ترجمه است زنده دنبال می‌کند — ولی ناوبری بین صفحه‌ها کاملاً آزاد می‌ماند. به‌محض اینکه خودت جایی بروی، اپ دیگر نما را برایت جابه‌جا نمی‌کند. پردازش ادامه دارد؛ فقط دنبال‌کردن خودکار کنار می‌کشد.

سه خروجی

وقتی کتاب تمام شد، منوی ذخیره سه قالب پیشنهاد می‌دهد:

  • یک صفحه به‌صورت TXT، با سرصفحه‌ای که نام کتاب و شمارهٔ صفحه را می‌نویسد، با کدگذاری utf-8-sig تا فارسی در Notepad ویندوز درست نمایش داده شود و به‌جای متن به‌هم‌ریخته، فارسی دیده شود.
  • کل کتاب به‌صورت Markdown، یک بخش ## برای هر صفحه به ترتیب عددی — مناسب برای version control و diff.
  • کل کتاب به‌صورت یک نسخهٔ HTML مستقل و راست‌چین، با تصویر صفحه کنار هر ترجمه، CSS درون‌خطی و قوانین چاپ که نمی‌گذارند یک صفحه وسط برگهٔ چاپی نصف شود. در مرورگر باز کن و Print → Save as PDF بزن تا یک PDF فارسی تمیز با تصاویر صفحه‌های اصلی بگیری.

مسیر HTML یک انتخاب آگاهانه است: ساخت PDF درستِ راست‌چین نیازمند جاسازی فونت فارسی و مدیریت درست چیدمان دوجهته است، و مرورگر این را از قبل خوب انجام می‌دهد. خروجی مستقیم PDF با رندر فونت RTL در نقشهٔ راه پروژه هست، ولی خروجی HTML امروز خوب است و از فونت‌هایی استفاده می‌کند که کاربر از قبل دارد.

قدم بعدی من

قدم بعدیِ صادقانه یک مدل بهتر نیست. دادهٔ بهتر است: یک مجموعه‌ارزیابی واقعی از کتاب‌ها، همراه با ترجمه‌هایی که یک مترجم انسانی خوب تولید می‌کند، تا کیفیت خط لوله اندازه‌گیری شود نه اینکه صرفاً درباره‌اش بحث شود. اولین ویژگی‌ای که اضافه می‌کردم، خروجی/ورودی گلاساری است — یک فهرست اصطلاح مشترک بین چند کتاب — چون یکدستی در یک مجموعه کتاب همان چیزی است که تمپلیت‌های per-book حل نمی‌کنند.

اعداد، و منبعشان

واقعیت مقدار
نسخهٔ برنامه 3.0.0
هسته kaman_core.py، حدود ۱۴۰۰ خط، بدون Qt
رابط translator_app_v3.py، حدود ۱۸۰۰ خط
مجموعه تست ۳۰۲ بررسی، همه پاس، کاملاً آفلاین
وابستگی‌های زمان اجرا ۴ — PyQt6، PyMuPDF، Pillow، google-genai
بودجهٔ پیش‌فرض تکه ۶۰۰۰ کاراکتر
کانتکست غلتان پیش‌فرض ۷۰۰ کاراکتر
رندر صفحهٔ اسکن‌شده ماتریس ۲× (حدود ۱۴۴ DPI)
محافظت از کلید Windows DPAPI، با fallback پایه۶۴ که لاگ می‌شود
انتشارها v3.0.0 (exe ویندوز + zip سورس)، v2.0.0

مجموعه تست بدون شبکه، بدون کلید API و بدون حساب ارائه‌دهنده اجرا می‌شود: موتورهای جعلی جای واقعی را می‌گیرند و پروفایل واقعی کاربر هرگز دست نمی‌خورد، چون APPDATA قبل از import شدن ماژول‌های اپ به یک پوشهٔ موقت اشاره می‌کند. یک اجرا ۳۰۲ بررسی پاس و صفر خطا چاپ می‌کند — این عدد اندازه‌گیری‌شده است، نه گِرد شده.

مستندات فنی کامل به‌صورت ویکی منتشر شده است: معماری، خط لولهٔ ترجمه، موتورها، تمپلیت‌های ساختار، مسیر صفحهٔ اسکن‌شده، ذخیرهٔ کار، خروجی‌ها، رابط کاربری، تنظیمات، مدل امنیتی، مجموعه تست و فرایند انتشار.

مرتبط: همین سایت یک ویجت مشاورهٔ فارسی و یک ربات پیام‌رسان را هم مستند کرده که بر همین ایده‌ها ساخته شده‌اند — یک موتور محلی و قابل‌ممیزی، با مدل میزبان‌شده فقط جایی که واقعاً ارزشش را دارد.

تماس
یادداشت بعدی

چرا استریم LLM من خالی است؟ راهنمای تشخیص پاسخ‌های خراب SSE

→