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 — переписування. У десктопі підозрілі речення переписувала модель. Тут потрібен був варіант, що працює взагалі без неї.
Я пішов на компроміс. Правило змінює лише фразу і зберігає її граматичне місце: відмінок, число, те, що стоїть після неї. Тоді речення лишається правильним без синтаксичного розбору. Шаблонні зв’язки на початку речення видаляються, якщо не несуть сенсу, або замінюються простішими, якщо несуть:
// 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.