Як я зробив AI-застосунок, що «читає» склад косметики: OCR, SSE-стрім, реклама поверх стріму й анонімізація брендів

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт, DOU.

Я фронт-розробник, люблю Angular, а з беком дружу постільки-поскільки. Пишу цей застосунок вечорами й по вихідних. Ще одна деталь про мене: до того як піти в розробку, я кілька років працював з косметикою — у нас із дружиною було кілька невеликих магазинчиків. Косметику я люблю — не в сенсі користуватися (для себе маю мінімальний набір: гель-для-душа-і-всього та дезодорант), а в сенсі запахів, текстур, упаковок. І трохи розумію болі покупця косметики — тобто маю, так би мовити, мінімальну компетенцію в темі.

А почалося все з банального особистого болю: дружина просить перевірити крем для обличчя на наявність небезпечних компонентів. Гуглити кожен компонент окремо — довго. Гугл об’єктив + gpt краще, але... Виникла ідея зробити щось зі зручним інтерфейсом. Сфоткав — і отримав чесну розкладку.

Власне, ідея не нова навіть для мене: колись я з нею навіть виграв хакатон (dou.ua/forums/topic/44813). А потім — закинув, як це зазвичай і буває. Згадав про неї пару місяців тому й подумав: з Claude та Cursor цю ідею цілком реально довести до більш-менш робочого продукту, не вбивши на це рік життя. Так і вийшло.

Так народився What’s Inside — застосунок, який бере список інгредієнтів — косметика переважно, але також БАДи й інше — все що має складний склад (вибачте), проганяє його через AI, виставляє оцінку 0–10 за релевантними параметрами, людською мовою пояснює кожен інгредієнт, дає вердикт і вміє порівнювати кілька продуктів поруч. Стек, як на pet-проєкт, доволі буденний: Angular + Ionic + Capacitor, стейт на Signals, графіки на Chart.js, 13 локалей через ngx-translate. Бекенд — Supabase (Edge Functions на Deno + Postgres + Auth) і OpenAI зверху.





Гортайте вбік, щоб подивитися всі зображення

Нижче — кілька технічних рішень, якими я реально задоволений і які, можливо, будуть комусь корисні. Плюс чесно про економіку в кінці.

  • AI-аналіз їде до користувача стрімом, а не «чекай півхвилини на спінер»

Аналіз — це насправді чотири окремі AI-задачі: швидкий огляд, розбір по інгредієнтах, оцінка за параметрами і фінальний вердикт/порівняння. Найгірше, що можна зробити — зібрати все докупи й віддати одним JSON після довгої мовчанки.

Тому edge-функція стрімить кожен шматок через Server-Sent Events одразу, щойно він готовий. Задачі летять паралельно; лише фінальний вердикт через Promise.all чекає на (огляд + оцінку), бо без них немає з чого робити висновок. На клієнті свідомо без бібліотек: голий fetch(), response.body.getReader(), TextDecoder({stream:true}) і ручне склеювання буфера з розбиттям по \n\n.

  • Рекламу показую ПОВЕРХ екрана результату, поки AI ще стрімить

Класична монетизація мобілок: показав інтерстишл — потім пустив у контент. Дві окремі паузи, обидві дратують.

Я зробив навпаки. На кроці налаштування застосунок заздалегідь префетчить рекламу (з коротким таймаутом — не завантажилась, їдемо без неї). По кліку «Аналізувати» одразу, ще до старту SSE, навігую на екран результату. Екран монтується, і Angular-івський effect(), який стежить за статусом флоу, показує інтерстишл поверх часткових результатів — а AI у цей час спокійно домелює у фоні. Сам SSE-сабскрайб живе в кореневому сервісі застосунку, тому переходи між сторінками його не вбивають — саме тому аналіз реально працює «у фоні», а не тримається на одному екрані.

У підсумку рекламу користувач дивиться рівно в ту мить, коли він однаково чекав би на спінер. Час очікування монетизується замість того, щоб бути мертвим.

  • Назви брендів не доходять до моделі (боротьба з упередженістю)

Це рішення мені найбільше подобається концептуально. Якщо відправити в LLM «Dior Foundation», у скоринг інгредієнтів протече упередження «дорогий бренд = добре». Модель оцінює вже не хімію — вона згадує цінник.

Тому в промпт продукти йдуть знеособлено: «Product 1», «Product 2», з явною інструкцією не фантазувати про бренд чи походження. А вже у відповіді проста swap-функція (регулярка по /Product\s+(\d+)/) повертає реальні назви назад у текст. Анонімізація на вході, деанонімізація на виході — і оцінка лишається «сліпою». Поруч живе окремий санітайзер, що вирізає спроби prompt-injection у дусі «ignore previous instructions» зі складу (бо склад — це поле, яке вводить користувач, і туди можна напхати що завгодно).

  • Не лише AI: аналіз комбінований, з реальними довідниками

Чистий AI на складі — це гарно, але він іноді «забуває» або фантазує. Тому поверх моделі працює детермінований шар: курувані довідники інгредієнтів на основі списків ЄС — зокрема 26 алергенів ароматизаторів з Annex III Регламенту (EC) № 1223/2009, позначки про заборонені в ЄС компоненти, фотосенсибілізатори, обмеження для вагітних. Сирий текст складу сканується по цих довідниках напряму, незалежно від того, що згадала модель, і результати зливаються докупи.

Виходить така собі комбінація «AI + аналог вікі по інгрідієнтах»: модель дає пояснення й контекст людською мовою, а довідник гарантує, що відомий алерген чи заборонений компонент не загубиться, бо LLM «не в гуморі». Для теми, де ціна помилки — чиєсь здоров’я, мені так спокійніше.

  • Розпізнавання складу з фото — дрібниці, які рятують

Склад рідко вміщується в один акуратний кадр: його друкують на зламі тюбика, по колу банки, дрібним шрифтом у три рядки. Тому розпізнавання приймає кілька фото одразу і склеює фрагменти складу в один список, якщо вони належать одному продукту — не треба ловити все в один ідеальний знімок.

Бонусом: якщо докинути ще й фото лицьового боку упаковки, модель сама підхоплює назву продукту й пропонує його категорію та параметри, за якими логічно оцінювати (для крему — одні, для бальзаму для волосся — інші). Запропоновані параметри можна лишити, прибрати або додати свої. Дрібниця, але прибирає купу ручної мороки перед аналізом.

Чесно про економіку (біль)

Собівартість одного аналізу — копійки. Безкоштовний тариф обмежений кількома аналізами на тиждень і рахується по device_id, а не user_id, щоб обнуляти квоту було не так зручно. Дохід з реклами покриває вартість безкоштовного юзера лише приблизно на третину чи менше, особливо в наших геолокаціях. Реальна монетизація — підписка, до якої людину підштовхує стіна тижневого ліміту й банальна втома від реклами.

Тобто кожен безкоштовний аналіз — трохи в мінус, а отже й весь проєкт поки що збитковий. Та мені не шкода: він тішить моє его більше, ніж коштує. Поки що цього достатньо.

Була ідея дати преміум юзерам розумнішу модель. Цей важіль закладений у коді, але поки що вимкнений. Тобто сьогодні преміум — це не «краща якість», а «без реклами + вищий ліміт». Чи це достатня цінність, щоб за неї платили — питання, на яке я ще не маю чесної відповіді.

Передбачаю питання

  • Таке вже є
  • Так, я не перший у цій ніші — є кілька застосунків зі схожою ідеєю: PureScan AI, Safety INCI та інші. Але, придивившись, я побачив закономірність: або функціонал помітно простіший (просто «є алерген / немає алергену», без оцінок і пояснень), або ціна різко вища — подеколи при такому ж або навіть урізанішому наборі можливостей. Власне, це й додало мені сміливості робити своє: місце для застосунку, який дає глибший розбір і при цьому не коштує як підписка на умовний стрімінг, схоже, є.
  • Та кому воно взагалі треба?
  • Судячи з того, що аналоги існують і мають чимало завантажень — комусь таки треба.
  • Навіщо застосунок, якщо те саме можна зробити через GPT?
  • Можна, якщо нормально спитати. Але на кожну дію треба писати повноцінний промпт: склей фото, знайди склад, прибери зайве, виділи що обіцяють. Потім — проаналізуй, вистав оцінки, порівняй із іншим складом, познач небезпеки, і так далі. Хотілося зручний інтерфейс під конкретну ціль, а не діалог із чистим листом щоразу. Плюс, як писав вище, застосунок звіряється з реальними списками заборонених і нерекомендованих компонентів — страховка на випадок, коли LLM глюкне.
  • Дизайн, UI, UX — недосконалий, поганий, жахливий
  • Так, UI/UX я поки що не задоволений. Зробити щось, що б мене повністю влаштовувало — поки не виходить . І навіть всемогутній claude.design тут не дуже допоміг))

Де воно і що я прошу

Застосунок уже живий у Google Play: play.google.com/...​ls?id=com.tellwhatsinside

Шукаю перших користувачів і чесний фідбек — особливо там, де погано — помиляється ai, невірно розпізнає склад з фото, незрозумілий інтерфейс. Обґрунтований хейт для мене зараз цінніші за похвалу.

Дякую, що дочитали.

👍ПодобаєтьсяСподобалось13
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Цікаво читати такі розбори. Я теж зараз розробляю AI-бота і що разу розумію, що сам написання кода — це найпростіша частина. UX, утримання користувачів і монетизація набагато складніші.

Згоден. Особливо коли є курсор, claude та ін — найскладніші моменти саме організаційні (сервіси, доступи, ключі), девопсівські (де, як, за скільки), та ux — там всіх цих агентів штормить дуже сильно, і прийняти якесь гарне рішення доволі складно

Вітаю.
Чудова ідея, обовʼязково протестую.
Чи можливо ще додати розшифровку коду дати випуску і терміна придатності у різних брендів?

Дякую, чудова ідея. І справді, деякі бренди вказують не дату виробництва, а зашифрований код партії. Формально це означає «без терміну придатності», але на практиці мені як покупцю все одно хочеться взяти свіжіше, навіть якщо на упаковці немає прямої дати.
Одразу в мій власний список доробок, дякую!

Привіт, дякую за статтю.
Чи є плани релізити додаток в AppStore?

Так, плани є. Технічно стек (Capacitor) дозволяє зібрати під iOS доволі швидко — кодова база спільна, переписувати власне нічого. Проблема радше організаційна: з Google без досвіду довелось повоювати, щоб опублікувати апку, і підозрюю, що з Apple буде не простіше, а радше навпаки.
Плюс потрібен айфон, щоб усе нормально протестувати на реальному пристрої. А єдиний власник айфона в родині — дружина, тож доведеться домовлятися)) Так що реліз буде, питання лише в тому, скільки нервів і дипломатії на це піде.

Дякую за цікаву статтю!

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

І ще виникла думка щодо архітектури. Ви не розглядали варіант локального OCR та локального базового аналізу по довідниках, а LLM використовувати вже для розширених пояснень, рекомендацій і порівняння продуктів? З опису виглядає, що у вас і так є досить серйозний детермінований шар із правилами та базою знань. Мені здалося, що такий підхід міг би зменшити кількість запитів до LLM, частково дати офлайн-функціональність і знизити собівартість аналізів.

Також із точки зору UX було б зручно:
1. Дозволити комбінувати фотографії з камери та галереї в межах одного аналізу. Зараз, якщо я правильно зрозумів, після вибору джерела всі наступні зображення додаються лише з нього;
2. Також звернув увагу, що вже проаналізовані фотографії можна запускати на аналіз повторно, навіть якщо зображення та параметри не змінювалися
3. За можливості не вмикати спалах за замовчуванням або запам’ятовувати останній вибір користувача, оскільки для глянцевих упаковок він іноді створює сильні відблиски та може погіршувати якість розпізнавання.

Загалом цікава ідея продукту, яка вирішує реальну практичну проблему, а не просто використовує AI заради AI. Бажаю успіхів у розвитку проєкту.

Так, відповім по FIFO

Зараз і OCR, і довідники працюють на сервері. Фото йдуть на бекенд у base64, і розпізнавання робить не класичний OCR, а vision-модель (та сама LLM). Перша версія була на локальному Tesseract, але я від нього свідомо відмовився — занадто багато помилок на реальних етикетках — дрібний шрифт, склад по колу банки, відблиски, кілька мов в одному рядку та ін. А llm і фрагменти з кількох фото зшиває, і розбірливо віддає там, де старий OCR давав кашу. Були побоювання щодо того щоб llm нічого не додумувала сама там де не впевнена — але це вирішував більш жорстким промтом та, власне, звіркою з довідниками. Тестування показало що помилки бувають, але більше коли склад на кількох фото — і деякі погані, деякі перегорнуті та ін. Але принаймні справді нічого не додумує сама — лише помічає що склад розпізнаний не повністю. Набагато краще раз звичайний OCR.

Детерміновані довідники — також на сервері, проганяються вже по розпізнаному тексту.

Щодо базового аналізу по довідниках. Детермінований шар наразі не аналізує, а скоріш каже «тут EU-алерген», «це заборонено в ЄС», «фотосенсибілізатор», але не виставляє оцінку і не пояснює контекст. Тут же цікаво не тільки компонент сам по собі, але й його контекст у складі — концентрація, позиція в списку, поєднання з іншими інгредієнтами. Саме цей контекст поки що дає LLM, а не довідник. Тож як самодостатній базовий аналіз без моделі довідник наразі замалий — це база сигналів, а не движок скорингу. Можливо, треба подумати і про розширення функціоналу довідника, щоб частину висновків він робив сам. Сама ідея гібридної схеми (локальний шар по довідниках, а LLM — лише для розширених пояснень і порівняння) — можливо, думав саме туди і почав рухатись в порівнянні з першими версіями. Це дає і швидкий сигнал «є/немає тривожних компонентів» одразу, і нижчу собівартість. Головна причина, чому зараз усе на сервері — швидкість розробки соло: тримати один контракт і одну копію довідника простіше, ніж синхронізувати базу між клієнтом і бекендом. Поки що шукаю баланс в цьому питанні.

Дозволити комбінувати фото з камери і галереї. Можливо. Хочеться і максимально спрощений UX — тому прибрав таку можливість. Але зараз розумію що так — такі сценарії теж можуть бути

Повторний аналіз вже проаналізованих фото — якщо Ви кажете саме про роззпізнавання — то це рішення свідоме. Це ж ллм — можливо я просто помітив неточність в розпізнаному складі та просто хочу виконати розпізнавання ще раз. Кеш буде тільки заважати. Або акуратно продумати гарний ui для таких кейсів (але це — найскладніше для мене).
А якщо саме про аналіз — гм, мабуть це гарна ідея. Але орієнтуватись саме на розпізнаний або введений склад, а не на фото — так, думаю зроблю. Дякую.

Не вмикати спалах — дякую, відмітив в ту-ду.

За побажання дякую. Справді сподіваюсь що це комусь стане в нагоді

Підписатись на коментарі