Loading...
← Всі проекти

AI Humanizer для Joomla: детектор ШІ-тексту без LM Studio

AI Humanizer для Joomla — це пакет із компонента й кнопки редактора, який оцінює, наскільки текст схожий на згенерований, і пропонує правки. Працює з українськими й англійськими текстами на Joomla 5 і 6. Основа — мій десктопний AI Humanizer на Electron, тільки без LM Studio.

Звідки взявся компонент

Десктопна версія (Electron 33, React 19, TypeScript) вміє дві речі. Стилометричні евристики оцінюють текст локально, а глибокий аналіз і переписування йдуть через модель у LM Studio. Для звичайного сайту така схема не годиться: редактор не ставитиме LM Studio й не завантажуватиме модель на 7B, щоб перевірити статтю.

Тож задача звучала так: той самий детектор, але як звичайне розширення Joomla, яке ставиться одним zip-файлом. LM Studio прибрати зовсім, а хмарну модель (OpenAI, Anthropic, Gemini) залишити опцією, яку адміністратор вмикає сам, зі своїм ключем.

Що перейшло з десктопу без змін

Коли я сів розбирати код Electron-версії, виявилось, що половина роботи вже зроблена. shared/detector і src/lib не торкаються Node: евристики, лексикони схожості на ChatGPT, Claude, Gemini, DeepSeek і Grok, читання DOCX через fflate і DOMParser, PDF через pdf.js. Там же лежить найскладніший механізм десктопної версії: TextMap, що записує відредагований текст назад у DOCX, не чіпаючи форматування.

Ці модулі я скопіював майже без правок. Змінилися дві речі: тип LoadedFile втратив поле path, а адреса воркера pdf.js тепер приходить із PHP через Joomla.getOptions(). Усе, що трималось на Electron (IPC, трей, глобальна гаряча клавіша, printToPDF), пішло під ніж.

Головний наслідок архітектурний. Аналіз, правки, збереження файлу і звіт працюють у браузері відвідувача. Текст і файли на сервер не надходять, і хостинг не отримує жодного навантаження від самої перевірки.

Правки без мовної моделі

Найбільша діра після видалення LM Studio — переписування. У десктопі підозрілі речення переписувала модель. Тут потрібен був варіант, що працює взагалі без неї.

Я пішов на компроміс. Правило змінює лише фразу і зберігає її граматичне місце: відмінок, число, те, що стоїть після неї. Тоді речення лишається правильним без синтаксичного розбору. Шаблонні зв’язки на початку речення видаляються, якщо не несуть сенсу, або замінюються простішими, якщо несуть:

ts
// Openers with no meaning of their own are dropped; ones that carry logic
// (therefore, таким чином) are swapped for a plainer word rather than lost.
// The replacement carries its own punctuation: "Also, " but "So " and «Тож ».
const OPENERS: Record<Lang, Pair[]> = {
  en: [
    ['moreover', ''], ['furthermore', ''], ['additionally', 'Also, '],
    // …
  ],
  uk: [
    ['крім того', ''], ['більше того', ''], ['таким чином', 'Тож '],
    // …
  ],
};

Окремо працюють словник кліше («дозволяють» → «дають змогу», «відіграє ключову роль» → «багато важить»), розгортання «не лише X, а й Y» в «X і Y» та, для англійської, заміна вставок через тире. Перший прогін на англійському прикладі видав «remains a important aspect». Довелося дописати fixArticles, яка виправляє артикль лише перед словом, що з’явилося після заміни, щоб не зачепити «a user» в іншій частині речення.

Чого правила не вміють: розбити перелік із трьох, змінити рівний ритм речень, підібрати конкретику замість кліше без готової заміни. Для таких речень компонент нічого не вигадує, а показує список «варто переписати вручну» з підказкою.

На вбудованих прикладах після прийняття всіх правок оцінка українського тексту падає з 95 до 73%, англійського — з 98 до 66%. Тобто текст стає менш шаблонним, але «людським» за евристиками не стає. Я вважаю це чесним результатом для правил без моделі.

Хмарна модель через PHP-проксі

Опційний глибокий аналіз робить те саме, що в десктопі: модель повертає ймовірність, вердикт, причини з цитатами й номери підозрілих речень, а потім може переписати ці речення. Різниця в тому, хто з ким говорить.

Ключ API зберігається в налаштуваннях компонента, і браузер його не бачить. Браузер надсилає на сайт лише масив речень, а промпт збирає PHP. Обидва промпти я переніс із TypeScript у Prompts.php слово в слово. Це навмисно: якби промпт приходив із браузера, будь-хто міг би використати сайт як безкоштовний чат за рахунок власника ключа.

Відповідь моделі PHP не розбирає. Він повертає сирий текст, а в браузері працює той самий поблажливий парсер, що й у десктопі: він вирізає <think>-блоки й огорожі ```json і закриває обрізаний JSON. Він уже враховує всі формати відповідей, які траплялися з локальними моделями, і дублювати цю логіку на PHP не було сенсу.

У провайдерів виявились свої дрібниці. OpenAI для власних моделей хоче max_completion_tokens, а сумісні сервіси на кшталт OpenRouter досі чекають max_tokens, тож проксі дивиться на базову адресу. Схему для OpenAI я надсилаю з strict: false, щоб не підганяти її під вимоги строгого режиму. Для Anthropic і Gemini схема йде текстом у системному промпті.

Щоб рахунок не здивував власника, є добові ліміти: на сайт, на користувача і на гостя. Гостя рахую за хешем IP-адреси з сіллю із secret Joomla, сама адреса ніде не зберігається. Кожен запит пишеться в таблицю #__aihumanizer_usage, а в адмінці є сторінка зі статистикою за 30 днів.

Кнопка в редакторі, яка не ламає HTML

Кнопка «Check for AI» відкриває перевірку в діалозі над редактором матеріалу. Найпростіше було б узяти текст, виправити його й вставити назад як простий текст. Тоді зникли б посилання, жирний шрифт і абзаци.

Натомість HTML матеріалу потрапляє в аналізатор як файл .html. Його читає htmlMap із TextMap, після прийняття правок той самий механізм записує зміни лише у вузли, що змінилися, і компонент повертає в редактор body.innerHTML. У тесті речення з <strong>штучний інтелект</strong> після правки лишилося з тим самим <strong>.

Дрібниці, на які пішов час

Окремий клас проблем — Joomla-специфіка, яку не видно, доки не відкриєш реальний сайт.

  • У Joomla 5 і 6 TinyMCE ховає кнопки editors-xtd у випадне меню «CMS Content». Тест шукав кнопку на панелі й падав.
  • Joomla компілює Bootstrap без префікса, тому змінні називаються --body-bg, а не --bs-body-bg. Через це в темній темі Atum картки компонента лишались білими, а назви вкладок зливались із фоном.
  • На Joomla 6 ключ JGLOBAL_SHOW_PAGE_HEADING_LABEL виводився в налаштуваннях як є, без перекладу. Замінив власним рядком.
  • Воркер pdf.js лежить як .js, а не .mjs: Apache в XAMPP не завжди віддає .mjs з JavaScript MIME, і модульний воркер не стартує.
  • Session::checkToken() для нової сесії робить редирект. Для fetch() це HTML замість JSON, тому AJAX-контролер сам відповідає JSON із кодом 403.

Ще одна дрібниця: плашка з назвою файлу мала display: inline-block, і через це атрибут hidden на ній не працював. Помітив її лише на скріншоті з живого сайту.

Як я це перевіряв

Для тестів я ставив чисту Joomla 5.3.3 і чисту 6.1.3 з MariaDB і запускав окремий PHP-сервер, що імітує OpenAI-сумісний API. Playwright проходить сторінку сайту (25 перевірок: правила, проксі, ліміт гостя, запит без CSRF-токена, DOCX, PDF, TXT у cp1251, друк звіту, ширина 390 px) і адмінку з кнопкою в редакторі.

Окремо я перевіряв DOCX: розпакував оригінал і виправлений файл і порівняв частини пакета. Змінився лише word/document.xml, а заміна «У сучасному світі» → «Сьогодні» лишилась жирною, бо успадкувала форматування фрагмента (run), у якому стояла.

Як я працював з ШІ-агентом

Цей порт я робив разом із Claude в агентному режимі. Розподіл був такий. Я поставив рамки: без LM Studio, хмара лише опційно через PHP-проксі з ключем в адмінці, Joomla 5, потім ще й 6. Агент прочитав вихідний код Electron-версії, запропонував архітектуру, написав TypeScript і PHP, підняв тестові Joomla і прогнав на них Playwright. Пакет на свій локальний сайт я ставив сам, через адмінку.

Найкорисніше, що дав такий формат, — перевірка на живому сайті. Біла картка в темній темі, неперекладений ключ на Joomla 6 і плашка, що не ховається, знайшлися, коли агент знімав скріншоти для інструкції. На чистих тестових інсталяціях їх не було видно. Інструкцію в PDF, DOCX і Markdown зроблено тим самим способом.

Що не так і що далі

Статус — beta, версія 1.0.1. Компонент працює на моєму локальному сайті з Joomla 6.1.3, публічного релізу ще немає.

Відомі обмеження:

  • точність тримається на лексиконах, які я відбирав вручну, а не на навченій моделі; оцінка — привід придивитися до тексту, а не доказ;
  • правила прибирають маркери, але не переписують ритм і не додають автору голосу;
  • скановані PDF без текстового шару не читаються, OCR немає;
  • SQL лише для MySQL і MariaDB, PostgreSQL поки не підтримується.

Найочевидніші кандидати на наступну версію — лексикони в базі з редагуванням в адмінці та SQL для PostgreSQL.