docs-combiner 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -100993,6 +100993,160 @@ function appendCreativeIdToCatalogLink(baseUrl, creativeId) {
100993
100993
  const sep = u.includes('?') ? '&' : '?';
100994
100994
  return `${u}${sep}creative_id=${encodeURIComponent(creativeId)}&${CATALOG_LINK_TRACKING_SUFFIX}`;
100995
100995
  }
100996
+ /** Текст ответа из OpenRouter/OpenAI chat completion (string или массив частей). */
100997
+ function extractChatCompletionText(choice) {
100998
+ const msg = choice?.message;
100999
+ if (!msg)
101000
+ return '';
101001
+ const c = msg.content;
101002
+ if (typeof c === 'string')
101003
+ return c;
101004
+ if (Array.isArray(c)) {
101005
+ return c
101006
+ .map((part) => (typeof part === 'string' ? part : part?.text ?? part?.content ?? ''))
101007
+ .filter(Boolean)
101008
+ .join('');
101009
+ }
101010
+ if (c != null && typeof c === 'object' && typeof c.text === 'string') {
101011
+ return c.text;
101012
+ }
101013
+ return '';
101014
+ }
101015
+ function stripMarkdownJsonFence(raw) {
101016
+ let s = raw.trim();
101017
+ if (!s.startsWith('```'))
101018
+ return s;
101019
+ s = s.replace(/^```(?:json)?\s*/i, '');
101020
+ const end = s.lastIndexOf('```');
101021
+ if (end >= 0)
101022
+ s = s.slice(0, end).trim();
101023
+ return s;
101024
+ }
101025
+ /** Парсит JSON-массив переводов из ответа модели (с ```json или мусором вокруг). */
101026
+ function parsePairTranslationsJson(raw) {
101027
+ const cleaned = stripMarkdownJsonFence(raw);
101028
+ try {
101029
+ const parsed = JSON.parse(cleaned);
101030
+ if (Array.isArray(parsed))
101031
+ return parsed;
101032
+ }
101033
+ catch {
101034
+ /* далее — вырезка по скобкам */
101035
+ }
101036
+ const jsonMatch = cleaned.match(/\[[\s\S]*\]/);
101037
+ if (!jsonMatch)
101038
+ return null;
101039
+ try {
101040
+ const parsed = JSON.parse(jsonMatch[0]);
101041
+ return Array.isArray(parsed) ? parsed : null;
101042
+ }
101043
+ catch {
101044
+ return null;
101045
+ }
101046
+ }
101047
+ /** Убирает повторяющиеся префиксы «ОШИБКА:» и лишние пробелы в строке из ответа валидатора. */
101048
+ function normalizeValidationErrorText(s) {
101049
+ let t = s.trim().replace(/\s+/g, ' ');
101050
+ while (/^ОШИБКА[:\s]+/i.test(t)) {
101051
+ t = t.replace(/^ОШИБКА[:\s]+/i, '').trim();
101052
+ }
101053
+ return t;
101054
+ }
101055
+ /** Одна и та же формулировка из ответа модели не дублируется в UI и в промпте переделки. */
101056
+ function dedupeValidationErrors(errors) {
101057
+ const seen = new Set();
101058
+ const out = [];
101059
+ for (const raw of errors) {
101060
+ const n = normalizeValidationErrorText(raw);
101061
+ if (!n)
101062
+ continue;
101063
+ const key = n.toLowerCase();
101064
+ if (seen.has(key))
101065
+ continue;
101066
+ seen.add(key);
101067
+ out.push(n);
101068
+ }
101069
+ return out;
101070
+ }
101071
+ /** Строка «ОШИБКА: нет» при статусе пересборки — противоречие; не считать содержательной ошибкой. */
101072
+ function isValidationNegationLine(s) {
101073
+ const t = normalizeValidationErrorText(s).toLowerCase();
101074
+ if (!t)
101075
+ return true;
101076
+ if (t === 'нет')
101077
+ return true;
101078
+ if (t.includes('нет ошибок') || t.includes('ошибок нет'))
101079
+ return true;
101080
+ if (t === 'none' || /^no errors?\.?$/i.test(t))
101081
+ return true;
101082
+ return false;
101083
+ }
101084
+ /** Все подписанные строки ОШИБКА:/ERROR: (модель иногда пишет по-английски). */
101085
+ function extractLabeledValidationErrors(content) {
101086
+ const out = [];
101087
+ const patterns = [
101088
+ /(?:^|\n)\s*ОШИБКА\s*[:\s]\s*([^\n]+)/gi,
101089
+ /(?:^|\n)\s*ERROR\s*[:\s]\s*([^\n]+)/gi,
101090
+ ];
101091
+ for (const re of patterns) {
101092
+ let m;
101093
+ const r = new RegExp(re.source, re.flags);
101094
+ while ((m = r.exec(content)) !== null) {
101095
+ const t = normalizeValidationErrorText(m[1]);
101096
+ if (t)
101097
+ out.push(t);
101098
+ }
101099
+ }
101100
+ return out;
101101
+ }
101102
+ /**
101103
+ * Если модель поставила НУЖНА ПЕРЕСБОРКА, но не оформила список как ОШИБКА: — забираем осмысленные строки
101104
+ * между статусом и блоком РЕКОМЕНДАЦИЯ (часто там остаётся описание причин).
101105
+ */
101106
+ function extractFallbackValidationErrorsBetweenStatusAndRecommendations(content) {
101107
+ const statusNeed = /СТАТУС\s*:\s*НУЖНА\s+ПЕРЕСБОРКА/i;
101108
+ const idx = content.search(statusNeed);
101109
+ if (idx < 0)
101110
+ return [];
101111
+ const tail = content.slice(idx);
101112
+ const firstNl = tail.indexOf('\n');
101113
+ const blockStart = firstNl >= 0 ? idx + firstNl + 1 : content.length;
101114
+ const rest = content.slice(blockStart);
101115
+ const recIdx = rest.search(/\nРЕКОМЕНДАЦИЯ\s*:/i);
101116
+ const block = recIdx >= 0 ? rest.slice(0, recIdx) : rest.slice(0, 3500);
101117
+ const lines = block.split('\n').map(l => l.trim()).filter(l => {
101118
+ if (l.length < 12)
101119
+ return false;
101120
+ if (/^(затем|список|каждая)\s/i.test(l))
101121
+ return false;
101122
+ if (/^ОШИБКА\s*[:\s]*нет\s*\.?$/i.test(l))
101123
+ return false;
101124
+ if (/^РЕКОМЕНДАЦИЯ\s*:/i.test(l))
101125
+ return false;
101126
+ if (/^ШАГ\s+[\dP]/i.test(l))
101127
+ return false;
101128
+ if (/^(?:HOOK|HEADLINE|PRICE|DISCOUNT|CTA|OTHER_TEXT)\s*:/i.test(l))
101129
+ return false;
101130
+ if (/^СТАТУС\s*:/i.test(l))
101131
+ return false;
101132
+ return true;
101133
+ });
101134
+ return lines
101135
+ .map(l => normalizeValidationErrorText(l.replace(/^ОШИБКА\s*[:\s]+/i, '')))
101136
+ .filter(Boolean);
101137
+ }
101138
+ /** Короткий фрагмент блока ФИНАЛ для подсказки, если структура ответа нестандартная. */
101139
+ function extractValidationFinalSnippet(content, maxLen) {
101140
+ const fi = content.search(/\bФИНАЛ\s*:/i);
101141
+ if (fi < 0)
101142
+ return null;
101143
+ let sn = content.slice(fi, fi + maxLen).replace(/\s+/g, ' ').trim();
101144
+ if (sn.length < 50)
101145
+ return null;
101146
+ if (sn.length > maxLen)
101147
+ sn = `${sn.slice(0, maxLen - 1)}…`;
101148
+ return sn;
101149
+ }
100996
101150
  /**
100997
101151
  * Разбор строк «ЗАГОЛОВОК n:» / «ТЕКСТ n:» из ответа модели.
100998
101152
  * Иногда модель пишет ZAGОЛОВОК (латиница ZAG + кириллица ОЛОВОК) или ZAGOLOVOK — без этого парсер не видит заголовки.
@@ -101448,6 +101602,7 @@ function App() {
101448
101602
  setTexts(['']);
101449
101603
  setLink('');
101450
101604
  setUploadedLink('');
101605
+ setPairTranslations({});
101451
101606
  return;
101452
101607
  }
101453
101608
  const folderId = extractFolderId(driveFolderUrl);
@@ -101463,6 +101618,7 @@ function App() {
101463
101618
  setTexts(['']);
101464
101619
  setLink('');
101465
101620
  setUploadedLink('');
101621
+ setPairTranslations({});
101466
101622
  return;
101467
101623
  }
101468
101624
  logToTerminal('log', '[Load] driveFolderUrl changed, clearing old data and loading from folderId:', folderId);
@@ -101474,6 +101630,7 @@ function App() {
101474
101630
  setTexts(['']);
101475
101631
  setLink('');
101476
101632
  setUploadedLink('');
101633
+ setPairTranslations({});
101477
101634
  setLoadingContentFromDrive(true);
101478
101635
  setDriveFilesFound({ content: false });
101479
101636
  // Load content from Google Drive
@@ -102802,22 +102959,33 @@ function App() {
102802
102959
  },
102803
102960
  body: JSON.stringify(requestBody)
102804
102961
  });
102805
- if (!response.ok)
102962
+ if (!response.ok) {
102963
+ logToTerminal('warn', '[Translate RU] HTTP', response.status, '— перевод пар пропущен');
102806
102964
  return;
102965
+ }
102807
102966
  const data = await response.json();
102808
- const raw = data.choices?.[0]?.message?.content || '';
102809
- const jsonMatch = raw.match(/\[[\s\S]*\]/);
102810
- if (!jsonMatch)
102967
+ const choice = data.choices?.[0];
102968
+ let raw = extractChatCompletionText(choice) ||
102969
+ (typeof choice?.text === 'string' ? choice.text : '') ||
102970
+ '';
102971
+ if (!raw.trim()) {
102972
+ logToTerminal('warn', '[Translate RU] Пустой ответ модели (content), перевод не выполнен');
102973
+ return;
102974
+ }
102975
+ const parsed = parsePairTranslationsJson(raw);
102976
+ if (!parsed || parsed.length === 0) {
102977
+ logToTerminal('warn', '[Translate RU] Не удалось разобрать JSON-массив переводов, превью:', raw.substring(0, 280));
102811
102978
  return;
102812
- const parsed = JSON.parse(jsonMatch[0]);
102979
+ }
102813
102980
  const translations = {};
102814
102981
  parsed.forEach((item, idx) => {
102815
102982
  translations[idx] = { titleRu: item.titleRu || '', textRu: item.textRu || '' };
102816
102983
  });
102817
102984
  setPairTranslations(translations);
102985
+ logToTerminal('log', '[Translate RU] OK, пар переведено:', parsed.length);
102818
102986
  }
102819
102987
  catch {
102820
- // тихо игнорируем ошибки переводаэто вспомогательная функция
102988
+ logToTerminal('warn', '[Translate RU] Ошибка запроса или разбора см. консоль / лог');
102821
102989
  }
102822
102990
  finally {
102823
102991
  setTranslatingPairs(false);
@@ -103339,7 +103507,21 @@ function App() {
103339
103507
  };
103340
103508
  const keywords = geoKeywords[geo.toUpperCase()] || ['продукт', 'проблема'];
103341
103509
  logMsg('log', `🔑 Ключевые слова: ${keywords.join(', ')}`);
103342
- const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords, approachName);
103510
+ const { price: briefPrice, currency: briefCurrency, currencySymbol } = parsePriceAndCurrency(generatePriceWithCurrency);
103511
+ let priceBriefForValidation = '';
103512
+ if (briefPrice && briefCurrency) {
103513
+ const nv = parseFloat(String(briefPrice).replace(',', '.'));
103514
+ const oldNum = Number.isFinite(nv) && nv > 0 ? nv * 2 : NaN;
103515
+ const oldStr = Number.isFinite(oldNum)
103516
+ ? (Number.isInteger(oldNum) ? String(oldNum) : (Math.round(oldNum * 100) / 100).toString().replace(/\.?0+$/, ''))
103517
+ : '';
103518
+ priceBriefForValidation = `Новая цена по брифу (после скидки -50%): ${briefPrice} ${briefCurrency}${currencySymbol ? `, символ валюты: ${currencySymbol}` : ''}. ${Number.isFinite(oldNum) ? `Ожидаемая старая цена до скидки: ${oldStr} ${briefCurrency} (2× новой).` : 'Старая цена на макете должна быть в 2 раза больше новой.'} Любые другие суммы — ошибка «не совпадает с брифом».`;
103519
+ logMsg('log', `💶 Эталон цен для валидации: новая ${briefPrice} ${briefCurrency}${oldStr ? `, старая ${oldStr}` : ''}`);
103520
+ }
103521
+ else {
103522
+ logMsg('log', '💶 Поле цены в приложении пустое — валидатор сверяет только визуал двух цен');
103523
+ }
103524
+ const validationPrompt = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.getValidationPrompt)(product, geo, keywords, approachName, undefined, priceBriefForValidation);
103343
103525
  const requestBody = {
103344
103526
  model: selectedValidationModel,
103345
103527
  messages: [
@@ -103478,25 +103660,28 @@ function App() {
103478
103660
  }
103479
103661
  // Update balance after successful API call
103480
103662
  fetchOpenRouterBalance();
103481
- // Парсим результат
103482
- const statusMatch = content.match(/СТАТУС:\s*(OK|НУЖНА ПЕРЕСБОРКА)/i);
103483
- const status = statusMatch ? (statusMatch[1].toUpperCase() === 'OK' ? 'ok' : 'needs_rebuild') : 'needs_rebuild';
103484
- // Извлекаем ошибки
103485
- const errors = [];
103486
- const errorMatches = content.match(/ОШИБКА[:\s]+([^\n]+)/gi);
103487
- if (errorMatches) {
103488
- errors.push(...errorMatches
103489
- .map(m => m.replace(/ОШИБКА[:\s]+/i, '').trim())
103490
- .filter(err => {
103491
- const lowerErr = err.toLowerCase();
103492
- // Игнорируем "нет", "нет ошибок", "ошибок нет" и подобные
103493
- return lowerErr !== 'нет' &&
103494
- !lowerErr.includes('нет ошибок') &&
103495
- !lowerErr.includes('ошибок нет') &&
103496
- err.length > 0;
103497
- }));
103663
+ // Парсим результат (английский STATUS — на случай ответа модели не по шаблону)
103664
+ const statusMatchRu = content.match(/СТАТУС\s*:\s*(OK|НУЖНА\s+ПЕРЕСБОРКА)/i);
103665
+ const statusMatchEn = content.match(/\bSTATUS\s*:\s*(OK|NEEDS?\s+REBUILD|FAIL|FAILED)\b/i);
103666
+ const statusToken = statusMatchRu?.[1] ?? statusMatchEn?.[1] ?? '';
103667
+ const status = (() => {
103668
+ if (!statusToken)
103669
+ return 'needs_rebuild';
103670
+ const u = statusToken.toUpperCase().replace(/\s+/g, ' ');
103671
+ if (u === 'OK')
103672
+ return 'ok';
103673
+ if (u.includes('NEED') || u === 'FAIL' || u === 'FAILED')
103674
+ return 'needs_rebuild';
103675
+ if (u.includes('НУЖНА') || u.includes('ПЕРЕСБОРКА'))
103676
+ return 'needs_rebuild';
103677
+ return 'needs_rebuild';
103678
+ })();
103679
+ let errors = extractLabeledValidationErrors(content).filter(e => !isValidationNegationLine(e));
103680
+ errors = dedupeValidationErrors(errors);
103681
+ // Противоречие: пересборка, а единственная строка была «ОШИБКА: нет» — тянем текст из хвоста ответа
103682
+ if (status === 'needs_rebuild' && errors.length === 0) {
103683
+ errors = dedupeValidationErrors(extractFallbackValidationErrorsBetweenStatusAndRecommendations(content));
103498
103684
  }
103499
- // Если статус "НУЖНА ПЕРЕСБОРКА", но ошибок нет в формате ОШИБКА, ищем в тексте
103500
103685
  if (status === 'needs_rebuild' && errors.length === 0) {
103501
103686
  const lines = content.split('\n').filter(line => line.includes('нарушено') ||
103502
103687
  line.includes('проблема') ||
@@ -103504,10 +103689,20 @@ function App() {
103504
103689
  line.includes('не хватает'));
103505
103690
  if (lines.length > 0) {
103506
103691
  errors.push(...lines.map(l => l.trim()).filter(l => l.length > 0));
103692
+ errors = dedupeValidationErrors(errors);
103507
103693
  }
103508
103694
  }
103509
103695
  const totalElapsed = Date.now() - validationStartTime;
103510
- const finalErrors = errors.length > 0 ? errors : (status === 'needs_rebuild' ? ['Обнаружены нарушения в креативе'] : []);
103696
+ const snippet = status === 'needs_rebuild' && errors.length === 0
103697
+ ? extractValidationFinalSnippet(content, 900)
103698
+ : null;
103699
+ const finalErrors = errors.length > 0
103700
+ ? errors
103701
+ : status === 'needs_rebuild'
103702
+ ? (snippet
103703
+ ? [`Валидатор не выписал строки «ОШИБКА:». Фрагмент ответа: ${snippet}`]
103704
+ : ['Валидатор указал пересборку, но не перечислил причины (нет строк «ОШИБКА:»). Откройте полный текст ответа модели в логе или повторите проверку.'])
103705
+ : [];
103511
103706
  logMsg('log', `✅ === Валидация завершена ===`);
103512
103707
  logMsg('log', `⏱️ Общее время: ${Math.round(totalElapsed / 1000)}s`);
103513
103708
  logMsg('log', `📊 Статус: ${status === 'ok' ? '✅ OK' : '❌ НУЖНА ПЕРЕСБОРКА'}`);
@@ -104172,7 +104367,7 @@ function App() {
104172
104367
  const [b1, b2, b3] = (0,_prompts__WEBPACK_IMPORTED_MODULE_1__.pickRandomBullets)(t.approach.name);
104173
104368
  return `ОБЯЗАТЕЛЬНЫЕ БУЛЛЕТЫ (используй именно эти 3, в указанном порядке; переведи на язык ${generateGeo}): 1) ${b1} 2) ${b2} 3) ${b3}`;
104174
104369
  })();
104175
- return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${bulletsLine}\n\nТекст — строго следуй правилам этого подхода (HOOK обязателен; буллиты добавляй только если они разрешены для подхода).`;
104370
+ return `🏷️ ПРОДУКТ: ${generateProduct}${additionalInfoLine}\n\n${basePromptStructure}🏷️ ПРОДУКТ (повторение для ясности): ${generateProduct}${additionalInfoLine}\n🎯 ПОДХОД: ${t.approach.name}\n\n${t.approach.prompt}\n\n${t.approach.headlineAngle}\n\n${bulletsLine}\n\nТекст — строго следуй правилам этого подхода (верхний заголовок обязателен; на макете не пиши слова HOOK/CTA/CAPS; буллиты добавляй только если они разрешены для подхода).`;
104176
104371
  });
104177
104372
  // Initialize placeholders for all images
104178
104373
  const initialPlaceholders = tasks.map((t, index) => ({
@@ -104948,8 +105143,12 @@ ${imageData.originalPrompt}
104948
105143
  try {
104949
105144
  const parsed = new URL(toParse);
104950
105145
  let path = parsed.pathname || '/';
104951
- if (!path.endsWith('/'))
104952
- path += '/';
105146
+ // Корень сайта — оставляем как `/`. Если есть «тело» пути (не один домен), завершающий `/` убираем, не добавляем.
105147
+ if (path !== '/' && path.length > 1) {
105148
+ while (path.length > 1 && path.endsWith('/')) {
105149
+ path = path.slice(0, -1);
105150
+ }
105151
+ }
104953
105152
  const result = `https://${parsed.host}${path}${parsed.search}${parsed.hash}`;
104954
105153
  setLink(result);
104955
105154
  setLinkError('');
@@ -106880,7 +107079,7 @@ function PromptManagerDialog({ open, onClose }) {
106880
107079
  case 'getImageCheckPrompt':
106881
107080
  return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getImageCheckPrompt)('${product}', '${geo}', true);
106882
107081
  case 'getValidationPrompt':
106883
- return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getValidationPrompt)('${product}', '${geo}', ['keyword1', 'keyword2'], '${approachName}', true);
107082
+ return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getValidationPrompt)('${product}', '${geo}', ['keyword1', 'keyword2'], '${approachName}', true, 'Новая по брифу: 29 EUR; старая (2×): 58 EUR — в рантайме подставляется из поля цены; в кастомном промпте плейсхолдер ${priceBrief}');
106884
107083
  case 'getImageGenerationBasePrompt':
106885
107084
  return (0,_prompts__WEBPACK_IMPORTED_MODULE_36__.getImageGenerationBasePrompt)('${generateGeo}', '${generatePrice}', '${generateCurrency}', true);
106886
107085
  case 'getLandingPageSystemPrompt':
@@ -108117,7 +108316,9 @@ function getImageCheckPrompt(product, geo, noOverride) {
108117
108316
  /**
108118
108317
  * Промпт для валидации рекламных креативов
108119
108318
  */
108120
- function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
108319
+ function getValidationPrompt(product, geo, keywords, approachName, noOverride,
108320
+ /** Краткий эталон цен из приложения (новая + ожидаемая старая при -50%); пусто — без сверки чисел */
108321
+ priceBrief) {
108121
108322
  if (!noOverride) {
108122
108323
  const override = (0,_promptOverrides__WEBPACK_IMPORTED_MODULE_0__.getPromptOverride)('getValidationPrompt');
108123
108324
  if (override?.enabled && override.customPrompt) {
@@ -108126,63 +108327,49 @@ function getValidationPrompt(product, geo, keywords, approachName, noOverride) {
108126
108327
  .replace(/\$\{product\}/g, product)
108127
108328
  .replace(/\$\{geo\}/g, geo)
108128
108329
  .replace(/\$\{keywords\}/g, keywords.join(', '))
108129
- .replace(/\$\{approachLine\}/g, approachLine);
108330
+ .replace(/\$\{approachLine\}/g, approachLine)
108331
+ .replace(/\$\{priceBrief\}/g, priceBrief?.trim() || 'В приложении бриф цены не задан — сверяй только визуально две цены и -50%.');
108130
108332
  }
108131
108333
  }
108132
108334
  const approachLine = approachName?.trim() ? `\nПОДХОД: ${approachName.trim()}\n` : '\n';
108133
- // Resolve no-bullets conditions in code — don't leave "if approach = X" for the LLM
108134
- const noBulletsApproachNames = CREO_APPROACHES.filter(a => a.noBullets).map(a => a.name);
108135
- const isNoBullets = noBulletsApproachNames.includes(approachName?.trim() ?? '');
108136
108335
  const isScreenshotReviews = (approachName?.trim() ?? '') === 'Скрин отзывов';
108137
- // ШАГ 0 hint for BULLETS depends on approach
108138
- const step0BulletsHint = isNoBullets
108139
- ? `BULLETS: [] (для данного подхода буллиты запрещены ожидается 0)`
108140
- : `BULLETS: ["...","...","..."] (если буллетов не 3перечисли сколько есть)`;
108141
- // ШАГ 2 completely different rules for no-bullets approaches vs normal
108142
- const step2Bullets = isNoBullets
108143
- ? `ШАГ 2 — BULLETS (критично):
108144
- - Данный подход НЕ допускает буллитов. Буллетов должно быть РОВНО 0.
108145
- - Если на креативе есть ЛЮБЫЕ буллиты — это ОШИБКА.`
108146
- : `ШАГ 2 — BULLETS (критично):
108147
- - РОВНО 3 буллета. Если буллетов не 3 — ОШИБКА.
108148
- - Каждый буллет должен быть читабельным (хорошо читается на телефоне, не микрошрифт)
108149
- - Буллеты: крупные, с иконками/галочками (), на контрастных подложках. Буллеты НЕ на упаковке/банке.
108150
- - Формат бенефита РАЗРЕШЁН: «Чувствуешь себя легче», «Больше энергии», «Без дискомфорта», «Лёгкость движений»это бенефиты, НЕ предложения
108151
- - ПРЕДЛОЖЕНИЕ (ошибка) = полная грамматическая конструкция с подлежащим + сказуемым + дополнением, например «Продукт быстро снижает дискомфорт». Короткие бенефиты — не считать предложениями
108152
- - Буллиты с глаголом во 2-м лице («Чувствуешь...», «Получаешь...») или безличные («Больше энергии», «Без дискомфорта») — НЕ ошибка
108153
- - Буллеты = свойства, характеристики ИЛИ бенефиты. Разрешено: «Reduce disconfortul», «Mai puține treziri», «Efect în 14 zile», цифры соц. доказательства.
108154
- - Буллиты должны быть расположены вертикально (столбиком). Если буллиты идут горизонтально в одну строкуОШИБКА: плохая читаемость на мобиле
108155
- - Цена визуально ОТЛИЧНА от буллитов (цена компактный блок без иконок; буллетыс иконками/галочками). Если цена выглядит как буллет ОШИБКА.`;
108156
- // ШАГ 3 TEXT LIMIT: PVP has no badges allowed, normal allows urgency/trust (без проверок по длине/количеству слов)
108157
- const step3TextLimit = isScreenshotReviews
108158
- ? `ШАГ 3OTHER_TEXT (критично):
108159
- - Для «Скрин отзывов» блок отзывов (аватарки, имена, возраст, 5 звёзд, текст отзывов) — это НЕ ошибка, это основной контент.
108160
- - Допускаются также trust‑печати (0–3 шт). URGENCY не рекомендуется, но не ошибка.
108161
- - На креативе допустимы: HOOK, блок отзывов, цена, скидка, CTA, опционально trust‑печати.`
108162
- : isNoBullets
108163
- ? `ШАГ 3 — OTHER_TEXT (критично):
108164
- - OTHER_TEXT должен быть ПУСТЫМ (0 элементов). Никаких дополнительных бейджей, подписей, ярлыков, trust‑печатей/urgency — НИЧЕГО.
108165
- - Любой дополнительный текст (бейджи/подписи/пояснения/urgency/trust‑печати) ОШИБКА: слишком много текста для punch‑креатива.
108166
- - На креативе допустимы ТОЛЬКО: HOOK, цена, скидка, кнопка CTA.`
108167
- : `ШАГ 3OTHER_TEXT (критично):
108168
- - По умолчанию любой другой рекламный текст (бейджи/подписи/пояснения) запрещён.
108169
- - НО допускаются (не обязательно) дополнительные бейджи ВНЕ упаковки — это НЕ ошибка, если ВСЕ элементы OTHER_TEXT подпадают под разрешённые типы:
108170
- A) URGENCY (0–1 шт): бейдж срочности/дефицита — только ясные формулировки («только сегодня», «последние штуки», «акция до конца дня»). ЗАПРЕЩЕНО: «24h» и подобные — непонятно что означает.
108171
- B) TRUST (0–3 шт, для «Минимализм / Clean Big Text» — макс 1): печати цветные, яркие, очень похожие на печать FDA. Подчёркивают: натуральность состава, премиальность, безопасность. Допустимо: качество, натуральные ингредиенты, экологичность, контроль качества. Печати — размер как минимум как у буллитов, крупные. Даже одна печать — крупная.
108172
- Правила для каждого элемента OTHER_TEXT:
108173
- * строго на языке ${geo} (без английских слов)
108174
- * без цены/скидки/процентов (кроме обязательного поля DISCOUNT), без доменов/ссылок
108175
- * TRUST‑печати — про натуральность/премиальность/безопасность (не про доставку/поддержку)
108176
- Примеры стиля (адаптируй к языку GEO, не требуются):
108177
- - URGENCY: «Ostatnie sztuki», «Tylko dziś», «Koniec dziś» (НЕ «24h» — непонятно)
108178
- - TRUST: «Naturalna formuła», «Wysoka jakość» (для PL); «Ingrediente naturale», «Calitate» (для RO). ЗАПРЕЩЕНО: «NATURAL», «QUALITY», «100% NATURAL» — английские слова.
108179
- - Если OTHER_TEXT содержит что-то вне этих типов, или URGENCY > 1, или TRUST > 3 (для Минимализма — TRUST > 1) — ОШИБКА.`;
108336
+ // Буллеты валидатором не проверяются (ни количество, ни наличие)
108337
+ const step0BulletsHint = 'BULLETS: кратко опиши, что видишь на макете (для справки). Количество и наличие буллетов НЕ валидируются.';
108338
+ const step2Bullets = `ШАГ 2БУЛЛЕТЫ:
108339
+ - Не проверяй и не оценивай количество, отсутствие или наличие буллетов. 0, 1, 2, 3 и большевсё допустимо.
108340
+ - НЕ добавляй в финальный список ошибок ни одного пункта, связанного с буллетами (вертикальность, «ровно 3», иконки, читаемость буллетов и т.п.).`;
108341
+ const priceBriefTrim = priceBrief?.trim() || '';
108342
+ const stepPriceBrief = priceBriefTrim
108343
+ ? `ШАГ P ЦЕНЫ (сначала макет, потом бриф):
108344
+ Эталон из приложения:
108345
+ ${priceBriefTrim}
108346
+
108347
+ 1) ВНУТРЕННЯЯ ЛОГИКА НА МАКЕТЕ (главное):
108348
+ - Должны быть видны две суммы: новая (акционная) и старая (зачёркнутая). Старая новой при скидке -50% (допуск до ~2% из‑за округления; «1 180» и «1180» — одно и то же).
108349
+ - Если отношение старая/новая 2 пределах допуска) это уже доказательство корректной пары -50% на макете. В этом случае ЗАПРЕЩЕНО выводить ОШИБКУ формулировками вроде «выдуманные цены», «неверные суммы», «не соответствуют брифу» даже если цифры в брифе другие или бриф кажется не тем.
108350
+ - Если пара математически согласована с -50% и процент «-50%» где‑либо читаем в зоне оффера (см. ШАГ 4) — не придумывай расхождение с брифом из‑за символа валюты (€ / EUR / MXN и т.д.) или пробелов.
108351
+
108352
+ 2) СВЕРКА С БРИФОМ (только когда сопоставимо):
108353
+ - Сравнивай число НОВОЙ цены с брифом ТОЛЬКО если валюта на макете явно та же, что в брифе (EUR↔EUR, MXN↔MXN и т.д.; символ € и код EUR одна валюта). Допускается разное оформление числа (пробелы тысяч, запятая/точка).
108354
+ - Если валюта на креативе другая, чем в брифе, или не уверен в коде валюты не требуй совпадения цифр с брифом: достаточно пункта (1).
108355
+ - Ошибку по ценам ставь только если: (а) на макете нет двух цен / нет согласованности -50%, ИЛИ (б) валюта однозначно совпадает с брифом, а число новой цены явно другое (не в пределах округления).
108356
+
108357
+ 3) Цена не должна быть оформлена как строка буллета с галочкой отдельный блок.`
108358
+ : `ШАГ P ЦЕНЫ (бриф числом не задан):
108359
+ - На макете должны быть ДВЕ цены, старая зачёркнута толсто, новая выразительна; пара должна визуально соответствовать -50% (старая ≈ 2× новой). Конкретные суммы с приложением не сверяй.`;
108360
+ const step3OtherText = isScreenshotReviews
108361
+ ? `ШАГ 3 — Подход «Скрин отзывов»:
108362
+ - Блок отзывов (аватары, имена, звёзды, текст) это нормальный контент креатива, не считай его лишним OTHER_TEXT.`
108363
+ : `ШАГ 3 Доп. текст, бейджи, печати:
108364
+ - Не валидируй объём текста и не отклоняй креатив за «слишком много элементов», punch vs не punch, количество плашек или пустоту OTHER_TEXT.
108365
+ - Не требуй отсутствия trust/urgency/подписей для любого подхода это не ошибка.
108366
+ - Язык рекламного текста (включая бейджи) по-прежнему ШАГ 5. Неясные срочности вроде «24h» — по-прежнему ШАГ 6.`;
108180
108367
  return `Ты — СТРОГИЙ валидатор рекламных креативов (1:1). Не улучшай и не предлагай идеи — только проверка и вердикт.
108181
108368
  Продукт: ${product}. GEO/язык: ${geo}.${approachLine}
108182
108369
 
108183
108370
  ВАЖНО:
108184
- - Проверяй язык ТОЛЬКО для рекламного текста на макете (заголовок/буллеты/CTA/цена/скидка/разрешённые бейджи из OTHER_TEXT). Текст на самой упаковке/этикетке игнорируй (включая название/бренд вроде "${product}").
108185
- - Если сомневаешься, где расположен текст (на упаковке или вне её) — считай, что он НА упаковке и НЕ записывай его в OTHER_TEXT.
108371
+ - Проверяй язык ТОЛЬКО для рекламного текста на макете (заголовок/CTA/цена/скидка/разрешённые бейджи из OTHER_TEXT; если есть буллеты — и их текст). Текст на самой упаковке/этикетке игнорируй (включая название/бренд вроде "${product}").
108372
+ - Если сомневаешься, мелкий текст на банке — этикетка или нет: для языка считай этикеткой и не валидируй. Исключение: **нижний оффер-блок** макета (две цены, зачёркивание, «-50%», кнопка CTA в одной полосе/ряду под картинкой продукта) — это всегда рекламный слой **вне упаковки**, даже рядом с фото банки; цены и скидку оттуда учитывай в PRICE/DISCOUNT и не считай «на упаковке».
108186
108373
  - Если текст плохо читается/микрошрифт — это ошибка.
108187
108374
 
108188
108375
  ШАГ 0 — Сначала выпиши распознанный текст (как ты его видишь на макете):
@@ -108192,9 +108379,7 @@ PRICE: "..." (если нет — "нет")
108192
108379
  DISCOUNT: "..." (если нет — "нет")
108193
108380
  CTA: "..." (если нет — "нет")
108194
108381
  OTHER_TEXT: ["..."] (любой другой рекламный текст на макете ВНЕ упаковки и ВНЕ элементов выше; название/бренд на упаковке НЕ включай)
108195
- (если можешь, для ясности — не обязательно):
108196
- URGENCY_BADGE: "..." (бейдж срочности, если есть; «24h» — ОШИБКА, непонятно что означает)
108197
- TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FDA; про натуральность/премиальность/безопасность; размер как у буллитов — крупные, если есть)
108382
+ (опционально, для ясности): URGENCY_BADGE, TRUST_BADGES перечисли, если есть
108198
108383
 
108199
108384
  ШАГ 1 — HOOK/HEADLINE (критично):
108200
108385
  - Должен быть ВЫШЕ всех элементов и читабелен (хорошо читается на телефоне)
@@ -108207,35 +108392,38 @@ TRUST_BADGES: ["..."] (печати цветные, яркие, в стиле FD
108207
108392
 
108208
108393
  ${step2Bullets}
108209
108394
 
108395
+ ${stepPriceBrief}
108396
+
108210
108397
  ШАГ 2.5 — CLAIMS CHECK:
108211
108398
  - Клеймы по результату (жёсткие/абсолютные или мягкие) — НЕ проверяются и НЕ запрещаются. Не блокируй за формулировки обещаний результата.
108212
108399
 
108213
- ${step3TextLimit}
108400
+ ${step3OtherText}
108214
108401
 
108215
- ШАГ 4 — CTA > PRICE (критично):
108402
+ ШАГ 4 — CTA и визуал цены/скидки (критично):
108216
108403
  - CTA: на языке ${geo}, хорошо заметная кнопка (позиция НЕ важна: можно вправо/влево/по центру). Главное — CTA читабельна и выглядит как кнопка.
108217
- - Цена: ОБЯЗАТЕЛЬНО ДВЕ старая (до скидки) + новая. Если только одна цена ОШИБКА. Старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Тонкое/незаметное зачёркивание — ОШИБКА.
108218
- - Скидка ОБЯЗАТЕЛЬНА: «-50%» отдельным видимым бейджем (процент должен быть явно читаем, не только в цене). Ярко, заметно. Строго НЕ на упаковке/банке.
108404
+ - Блок цен: две суммы, старая зачёркнута ТОЛСТОЙ линией, новая выразительно (детали и сверка чисел с брифом в ШАГ P). Если только одна цена — ОШИБКА.
108405
+ - Скидка ОБЯЗАТЕЛЬНА: читаемое «-50%». **Отдельный видимый бейдж** = любая заметная плашка/круг/прямоугольник с текстом «-50в **той же зоне оффера**, что и цены (сбоку от сумм, над/под ценовой строкой, между ценой и CTA) это ОК, не требуй отдельного «далёкого» элемента. «НЕ на упаковке» означает только: не напечатано на этикетке/банке товара; плашка в нижней рекламной полосе макета **не** считается упаковкой.
108406
+ - Если в ШАГ 0 в DISCOUNT ты указал «-50%» (или эквивалент) — ЗАПРЕЩЕНО ставить ошибку «скидка не отображается» / «нет отдельного бейджа».
108219
108407
  - Не считать ошибкой, если скидка/цена конкурируют с CTA по акценту — это допустимо, пока CTA остаётся хорошо заметной и читабельной.
108220
108408
  - Если скидка указана и она не равна -50% — ошибка.
108221
108409
 
108222
108410
  ШАГ 5 — ЯЗЫК (критично):
108223
- - В рекламном тексте (HOOK/HEADLINE/BULLETS/CTA/PRICE/DISCOUNT/OTHER_TEXT) НЕТ английских слов. Если есть — ошибка.
108411
+ - В рекламном тексте (HOOK/HEADLINE, текст на буллетах если есть, CTA/PRICE/DISCOUNT/OTHER_TEXT) НЕТ английских слов. Если есть — ошибка.
108412
+ - ЗАПРЕЩЁННЫЕ служебные подписи на макете (частая ошибка модели): слова HOOK, CTA, CAPS, BULLET, HEADLINE, PRICE, DISCOUNT как видимый текст на плашках или кнопке — ОШИБКА (это метки из брифа, не для читателя).
108224
108413
  - Все слова соответствуют GEO/языку ${geo}. Если смешение языков — ошибка.
108225
108414
 
108226
108415
  ШАГ 6 — КОМПОЗИЦИЯ:
108227
108416
  - Если креатив без человека (lifestyle/clean), проверь наличие контекста использования (кухня, стол, тумбочка, и т.д.). Продукт на полностью пустом/белом фоне без контекста — РЕКОМЕНДАЦИЯ к улучшению (не критичная ошибка, но слабый визуал)
108228
- - КОНТРАСТ ПЛАШЕК: если хотя бы одна текстовая плашка (HOOK, буллеты, CTA или цена) визуально сливается с фоном и текст плохо читается — это ОШИБКА.
108229
- - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание ОШИБКА. Цена не должна выглядеть как буллет.
108230
- - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (размер как минимум как у буллитов).
108231
- - СКИДКА «-50%»: ОБЯЗАТЕЛЬНО отдельным видимым бейджем (процент явно читаем). Если скидка не отображается или только в цене без отдельного «-50%» ОШИБКА. Ярко, заметно, НЕ на упаковке.
108232
- - ЦЕНА: ОБЯЗАТЕЛЬНО ДВЕ — старая зачёркнута ТОЛСТОЙ линией (не тонкой!), новая выразительно. Одна цена или тонкое зачёркивание — ОШИБКА.
108417
+ - КОНТРАСТ ПЛАШЕК: если HOOK, CTA или блок цен визуально сливается с фоном и текст плохо читается — это ОШИБКА. (Контраст буллетов не проверяй.)
108418
+ - Две цены и зачёркивание старой см. ШАГ 4 и ШАГ P. Цена не должна быть оформлена как элемент списка с галочкой в стиле бенефитов.
108419
+ - TRUST‑печати: если печати мелкие или текст нечитабелен на телефоне — ОШИБКА (должны быть крупными).
108420
+ - СКИДКА «-50%»: как в ШАГ 4 плашка рядом с ценами в оффер-блоке засчитывается; если DISCOUNT в ШАГ 0 заполненне дублируй ошибку про бейдж.
108233
108421
  - Бейджи срочности: «24h» и подобные неясные формулировки — ОШИБКА (непонятно что означает). Ясные («только сегодня», «последние штуки») — ок.
108234
108422
 
108235
108423
  ФИНАЛ:
108236
108424
  Выведи строго:
108237
108425
  СТАТУС: OK / НУЖНА ПЕРЕСБОРКА
108238
- Затем список ошибок, каждая строка начинается с "ОШИБКА:" (кратко, по делу). Если ошибок нет — напиши "ОШИБКА: нет".
108426
+ Затем список ошибок, каждая строка начинается с "ОШИБКА:" (кратко, по делу). Одну и ту же проблему не повторяй — не больше одной строки на один тип нарушения. Если СТАТУС: НУЖНА ПЕРЕСБОРКА — ОБЯЗАТЕЛЬНО минимум одна конкретная строка "ОШИБКА: ..." (что именно не так); нельзя писать только "ОШИБКА: нет". Если ошибок нет — СТАТУС: OK и "ОШИБКА: нет".
108239
108427
  Затем список рекомендаций (если есть), каждая строка начинается с "РЕКОМЕНДАЦИЯ:" (кратко, по делу). Если рекомендаций нет — напиши "РЕКОМЕНДАЦИЯ: нет".
108240
108428
  Не блокируй за клеймы по результату или формулировки обещаний.`;
108241
108429
  }
@@ -108271,6 +108459,9 @@ function getImageGenerationBasePrompt(generateGeo, generatePrice, generateCurren
108271
108459
  ВАЖНО: изображение должно быть ${ratioShape} — строго соблюдай пропорции холста.
108272
108460
  Язык текста: ${generateGeo}.
108273
108461
 
108462
+ 🚫 СЛУЖЕБНЫЕ СЛОВА НЕ РИСОВАТЬ НА КАРТИНКЕ (КРИТИЧНО):
108463
+ Слова HOOK, CTA, CAPS, BULLET, HEADLINE, PRICE, DISCOUNT, BULLETS, LAYER и любые похожие английские метки из этой инструкции (в т.ч. thumb‑stop) — только для тебя; на макете их НЕТ ни на кнопке, ни на плашках, ни мелким текстом. Кнопка: только реальный призыв на языке ${generateGeo} (1–2 слова), без префикса «CTA». Заголовок: только живой текст оффера, без слова HOOK. Заголовок делай прописными буквами на языке ${generateGeo}, но не пиши на изображении слово CAPS и не подписывай блоки ярлыками.
108464
+
108274
108465
  🚨 КРИТИЧНО — РЕЛЕВАНТНОСТЬ ПРОДУКТУ (читай ЭТО ПЕРВЫМ):
108275
108466
  - Название продукта передано в начале промпта (строка «🏷️ ПРОДУКТ: ...»). Прочитай его ПРЯМО СЕЙЧАС и определи категорию.
108276
108467
  - Категории и их проблемы: суставы/колени → боль в суставах, скованность, тугоподвижность; пищеварение → дискомфорт после еды, тяжесть; сон → бессонница, усталость; похудение → лишний вес; простата → частые позывы, дискомфорт.
@@ -108301,7 +108492,7 @@ HOOK / HEADLINE (строгое правило):
108301
108492
  - 1–4 строки; до 12 слов. Без переноса внутри слова
108302
108493
  - ОБЯЗАТЕЛЬНО содержит ключевое слово проблемы ЯВНО. Примеры по категориям: простата → простатит, простата; похудение → лишний вес, похудение; суставы → боль в суставах, колени, скованность; пищеварение → дискомфорт, вздутие, тяжесть; сон → бессонница, усталость. Без ключевого слова — ОШИБКА
108303
108494
  - HOOK ВЫШЕ всех элементов (продукт, буллеты, CTA, цена). Самый крупный текстовый элемент на креативе
108304
- - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, ВСЕ БУКВЫ ПРОПИСНЫЕ (CAPS), на яркой контрастной плашке/подложке. Это верхний главный “thumb‑stop” элемент
108495
+ - HOOK должен быть ВИЗУАЛЬНО “тяжёлым”: ОЧЕНЬ крупный, ТОЛСТЫЙ/жирный шрифт, весь текст заголовка ПРОПИСНЫМИ буквами на языке ${generateGeo}, на яркой контрастной плашке/подложке. Это верхний главный элемент, цепляющий внимание в ленте
108305
108496
  - Можно (не обязательно) включить СРОК прямо в HOOK (например «7 dni», «14 dni») как триггер
108306
108497
  - Заголовок должен быть РЕЗКИМ и призывным: короткие рубленые фразы, вопросы и предупреждения допустимы. Можно использовать обращение на «ты» (в языке GEO: «Masz…», «Twój…») и вопросительный знак
108307
108498
  - заголовок НЕ должен быть абстрактным слоганом/лозунгом или метафорой без конкретной боли. Допускается «thumb‑stop» стиль (вызов/вопрос/предупреждение), если это конкретно и релевантно категории
@@ -108352,7 +108543,7 @@ CTA > PRICE:
108352
108543
  ❗ ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → цена и «-50%» могут быть более крупными и заметными, но CTA всё равно должен оставаться очень заметным и выглядеть как кнопка (не теряется на фоне)
108353
108544
 
108354
108545
  ПОКАЗЫВАЙ ТОЛЬКО ЭТИ ТЕКСТОВЫЕ ЭЛЕМЕНТЫ:
108355
- - HOOK (1–4 строки; выше всех элементов; самый крупный; CAPS; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
108546
+ - HOOK (1–4 строки; выше всех элементов; самый крупный; прописные буквы; жирный; на яркой контрастной подложке; ключевое слово проблемы явно)
108356
108547
  - 3 буллета (крупные, с иконками/галочками ✓, на контрастных подложках, НЕ на банке/упаковке)
108357
108548
  - Цена: ОБЯЗАТЕЛЬНО ДВЕ — старая (2×${generatePrice} ${generateCurrency}) зачёркнута ТОЛСТОЙ контрастной линией + новая ${generatePrice} ${generateCurrency} выразительно. Одна цена = ОШИБКА. Тонкая линия зачёркивания = ОШИБКА. Без слов. Не на упаковке.
108358
108549
  - Скидка: «-50%» ОБЯЗАТЕЛЬНО отдельным видимым бейджем (не только в цене!). Процент скидки должен быть явно читаем. Ярко, заметно, строго НЕ на банке/упаковке, визуально слабее CTA
@@ -108388,11 +108579,11 @@ ANTI-TEMPLATE DIVERSITY (КРИТИЧНО):
108388
108579
  * 🎯 ПОДХОД: Визуализация проблемы → Инфографика/схема: слева проблема (иконка/схема зоны тела, релевантной продукту), справа решение + продукт. HOOK сверху по центру, BULLETS справа или снизу (вертикально), CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне). Красный акцент только на проблеме.
108389
108580
  * 🎯 ПОДХОД: Power / Сила решения → БЕЗ человека. ДИНАМИЧНЫЙ комикс‑кадр, диагональная композиция. Продукт как “герой” + power‑иконки (щит/молния/бёрст). HOOK сверху слева на яркой плашке, BULLETS слева ниже, CTA снизу справа, PRICE/DISCOUNT возле CTA. Trust‑печати (если есть) — компактно по низу, размер как минимум как у буллитов (крупные, читабельны на телефоне).
108390
108581
  * 🎯 ПОДХОД: Минимализм / Clean Big Text → Белый/градиентный фон, минимум элементов. ОГРОМНЫЙ HOOK занимает верх/центр, продукт крупно (центр/право), CTA снизу по центру, PRICE/DISCOUNT рядом (слабее CTA). Trust‑печати (если есть) — размер как минимум как у буллитов, крупные и читабельные.
108391
- * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный CAPS, выше всех. БЕЗ буллитов. PRICE + «-50%» крупно и заметно. CTA контрастной кнопкой
108392
- * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, CAPS, выше всех. Крупные надписи цены/скидки вокруг продукта. CTAплоская кнопка без градиентов. БЕЗ буллитов, бейджей, теней, иконок.
108582
+ * 🎯 ПОДХОД: Problem Visual Punch → БЕЗ человека. Яркий сплошной фон (красный/оранжевый) или агрессивный градиент. В центре КРУПНАЯ иконка/схема зоны тела, релевантной продукту (для суставов — колено; для пищеварения — желудок), с красным свечением. Продукт крупно рядом. HOOK 1–4 строки, до 12 слов, огромный прописной текст, выше всех. БЕЗ буллитов. Две цены + «-50%» крупно и заметно. Кнопка призыва контрастная (только текст на языке ${generateGeo}, без слова CTA)
108583
+ * 🎯 ПОДХОД: Любительский Примитивизм → БЕЗ человека. Чёрный или кислотный сплошной фон. Продукт по центру или справа без декора. HOOK сверху — 1–4 строки, до 12 слов, крупный, грубый bold/рукописный, прописные буквы, выше всех. Крупные надписи цены/скидки вокруг продукта. Кнопка призыва плоская, без градиентов (только короткая фраза на языке ${generateGeo}). БЕЗ буллитов, бейджей, теней, иконок.
108393
108584
 
108394
108585
  AUTO-CHECK (перед финалом):
108395
- - HOOK: 1–4 строки, до 12 слов, CAPS, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов
108586
+ - HOOK: 1–4 строки, до 12 слов, прописные буквы, жирный на контрастной плашке, выше всех элементов, ключевое слово проблемы явно, язык ${generateGeo} без английских слов (включая метки HOOK/CTA/CAPS)
108396
108587
  - Буллеты: по умолчанию ровно 3, каждый <= 4 слов, без запятых, не предложения. ИСКЛЮЧЕНИЕ: если 🎯 ПОДХОД = ${noBulletsCond} → буллетов 0 (запрещены)
108397
108588
  - Нет лишнего текста кроме: HOOK, буллетов, цены, скидки, кнопки (+ опционально 1 бейдж срочности + опционально 1–3 trust‑печати)
108398
108589
  - Есть скидка «-50%» (вне упаковки) и она слабее CTA
@@ -108436,37 +108627,37 @@ const CREO_APPROACHES = [
108436
108627
  {
108437
108628
  name: 'Эксперт / Авторитет',
108438
108629
  prompt: `ЭКСПЕРТ / АВТОРИТЕТ (без человека): стиль “рекомендация эксперта”, но БЕЗ человека. Профессиональный контекст — аптечные полки на фоне / медицинский планшет / стетоскоп / рецептурный блокнот как реквизит рядом с продуктом. Продукт в центре как “рекомендованное решение”. Чистый профессиональный фон, высокий контраст. Trust‑печати (1–3 шт): цветные, яркие, очень похожие на печать FDA; подчёркивают натуральность состава, премиальность, безопасность. Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная.`,
108439
- headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108630
+ headlineAngle: `HOOK: угол «предупреждение / интрига / специалисты знают». Формат: вопрос‑предупреждение ИЛИ “специалисты знают/используют” (адаптируй под GEO). 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108440
108631
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор для этого подхода — не повторяй комбинации из других креативов.`
108441
108632
  },
108442
108633
  {
108443
108634
  name: 'Lifestyle / Момент приёма',
108444
108635
  prompt: `LIFESTYLE / МОМЕНТ ПРИЁМА: продукт в контексте использования (кухня/стол/спальня/тумбочка). Без человека, но с "историей" (стакан воды, чай, тарелка, будильник/часы/календарь). Срочность: реквизит (часы/таймер‑иконка) и/или опциональный urgency‑бейдж (1–3 слова) в углу кадра — только ясные формулировки («только сегодня», «последние штуки»), ЗАПРЕЩЕНО «24h». Высокий контраст, яркий акцент на продукте. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ВАЖНО: буллиты располагаются вертикально и всегда читабельны на экране телефона без зума — даже если уходят вбок, минимальный размер шрифта буллитов не уменьшается. Если буллиты не вмещаются сбоку с нужным шрифтом — переставь их вниз.`,
108445
- headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108636
+ headlineAngle: `HOOK: угол «цифры + срочность». Формат: число/процент + ограничение времени/“сейчас/сегодня” (адаптируй под GEO), 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108446
108637
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (срок + цифра + действие/формула) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108447
108638
  },
108448
108639
  {
108449
108640
  name: 'Эмоция / Портрет',
108450
108641
  prompt: `ЭМОЦИЯ / ПОРТРЕТ: один из двух подходов с человеком в серии. Используй максимально: ОЧЕНЬ крупный план лица (thumb‑stop), прямой взгляд в камеру, сильная эмоция облегчения/надежды (без широкой стоковой улыбки). Возраст человека подбери под категорию и ЦА продукта: по умолчанию 50–60, но для категорий с более молодой аудиторией (например похудение/фитнес) допускается 35–55. Продукт в руке на уровне лица или рядом, хорошо виден. Свет "тень → свет" на лице допустим. Минимум отвлекающих деталей, высокий контраст. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. ИЕРАРХИЯ: лицо — главный и доминирующий визуальный элемент (верхние 2/3 кадра). Все текстовые блоки (HOOK исключение — сверху) уходят в нижнюю треть. Буллиты, цена, CTA НЕ перекрывают лицо и НЕ конкурируют с ним по визуальному весу — они заметно меньше и ниже.`,
108451
- headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
108642
+ headlineAngle: `HOOK: угол «персонально на “ты” + результат/облегчение». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно. Тон максимально личный и прямой.`,
108452
108643
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (качество жизни + действие + срок/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108453
108644
  },
108454
108645
  {
108455
108646
  name: 'Визуализация проблемы',
108456
108647
  prompt: `ВИЗУАЛИЗАЦИЯ ПРОБЛЕМЫ (без человека): схема/иконка проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав/позвоночник; для пищеварения — желудок; для простаты — силуэт мужчины) + мягкий красный акцент на этой зоне (не шок‑контент, без графики). Рядом продукт как решение. Можно добавить простую стрелку/переход “проблема → облегчение” как графику (без лишнего текста). Чистый фон, высокая читабельность. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека.`,
108457
- headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, вопросительный знак допустим.`,
108648
+ headlineAngle: `HOOK: прямой вопрос о боли/дискомфорте (релевантно категории). 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно, вопросительный знак допустим.`,
108458
108649
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108459
108650
  },
108460
108651
  {
108461
108652
  name: 'Power / Сила решения',
108462
108653
  prompt: `POWER / СИЛА РЕШЕНИЯ (без человека): метафоры силы и победы над проблемой: щит, молния, энергия, взрыв‑бёрст, мощные стикеры/иконки. Продукт как “герой” в центре, высокая энергия, контрастные агрессивные цвета. Можно комикс/anti‑design подачу, но всё должно быть читабельно. Без лишнего текста. Опционально: 1–3 trust‑печати по низу (цветные, яркие, очень похожие на печать FDA; натуральность/премиальность/безопасность). Размер как минимум как у буллитов — крупные, читабельны на телефоне. Даже одна печать — крупная. БЕЗ человека. Только продукт + power‑иконки.`,
108463
- headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108654
+ headlineAngle: `HOOK: угол «было → стало / победа над проблемой». Можно использовать стрелку “→” как часть перехода. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108464
108655
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + скорость + цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108465
108656
  },
108466
108657
  {
108467
108658
  name: 'Минимализм / Clean Big Text',
108468
- prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (CAPS, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
108469
- headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108659
+ prompt: `МИНИМАЛИЗМ / CLEAN BIG TEXT (без человека): белый или мягкий градиентный фон, премиальное ощущение. ОГРОМНЫЙ HOOK как главный элемент (прописные буквы, жирный, на контрастной плашке). Продукт крупно, минимум реквизита, минимум шума. Тени/премиальные материалы допустимы, но без лишнего текста. БЕЗ человека. Urgency и trust‑печати в этом подходе НЕ рекомендуются — они нарушают чистоту и ощущение премиальности. Добавляй печати только если это критически необходимо, не более 1, но крупную — размер как у буллитов.`,
108660
+ headlineAngle: `HOOK: одна максимально простая сильная фраза (коротко и ясно), 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108470
108661
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + формула/цифра) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108471
108662
  },
108472
108663
  {
@@ -108476,14 +108667,14 @@ const CREO_APPROACHES = [
108476
108667
  - Фон: яркий сплошной цвет (красный/оранжевый) или агрессивный градиент
108477
108668
  - Центр: КРУПНАЯ иконка/схема проблемной зоны тела, строго соответствующей категории продукта (для суставов — колено/сустав; для простаты — силуэт мужчины; для пищеварения — желудок), с красным свечением
108478
108669
  - Продукт: крупно рядом
108479
- - HOOK: 1–4 строки, до 12 слов, огромный, CAPS, ключевое слово проблемы явно
108670
+ - HOOK: 1–4 строки, до 12 слов, огромный, прописные буквы, ключевое слово проблемы явно
108480
108671
  - БЕЗ буллитов
108481
108672
  - Цена + скидка: крупно, заметно
108482
108673
  - CTA: контрастная яркая кнопка
108483
108674
 
108484
108675
  Цель: считывание за 1 секунду. Минимум текста, максимум визуального удара.
108485
108676
  БЕЗ человека. Никаких trust‑печатей/urgency бейджей — только HOOK + PRICE + -50% + CTA.`,
108486
- headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108677
+ headlineAngle: `HOOK: «ПРОЩАЙ/КОНЕЦ/СТОП + [проблема]!». Эмоция победы/избавления. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108487
108678
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK и крупных надписях.`
108488
108679
  },
108489
108680
  {
@@ -108498,7 +108689,7 @@ const CREO_APPROACHES = [
108498
108689
  - Крупные текстовые надписи рядом с продуктом: цена, скидка; допустимо одно короткое слово-восклицание («РАБОТАЕТ!», «ПРОВЕРЕНО!») на языке GEO
108499
108690
  - Кнопка CTA: плоская, без градиентов, контрастный сплошной цвет
108500
108691
  - НИКАКИХ буллитов, trust‑печатей, urgency‑бейджей, иконок — только продукт и текст`,
108501
- headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
108692
+ headlineAngle: `HOOK: максимально прямолинейный — короткий, грубый, без украшений. 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно, как объявление на столбе. Никакого маркетингового лоска, никаких абстрактных слоганов.`,
108502
108693
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Всё — в HOOK и крупных надписях вокруг продукта (цена, скидка, одно слово-восклицание).`
108503
108694
  },
108504
108695
  {
@@ -108510,7 +108701,7 @@ const CREO_APPROACHES = [
108510
108701
  - Фон: профессиональный (кабинет, аптечные полки, медицинский контекст). Высокий контраст, читабельность.
108511
108702
  - Опционально: 1–3 trust‑печати по низу (цветные, яркие, в стиле FDA). Размер как минимум как у буллитов.
108512
108703
  - ИЕРАРХИЯ: врач + продукт — главные элементы. HOOK сверху, BULLETS сбоку или снизу, CTA и PRICE/DISCOUNT в нижнем блоке.`,
108513
- headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108704
+ headlineAngle: `HOOK: угол «врач/специалист рекомендует» или «эксперты знают». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108514
108705
  bulletsFocus: `БУЛЛИТЫ: выбери 3 СЛУЧАЙНЫХ из пула (действие + срок + соц.доказательство) в ПРОИЗВОЛЬНОМ порядке. Уникальный набор — не повторяй комбинации из других креативов.`
108515
108706
  },
108516
108707
  {
@@ -108519,11 +108710,11 @@ const CREO_APPROACHES = [
108519
108710
  prompt: `СКРИН ОТЗЫВОВ: имитация реалистичного скриншота с положительными благодарными отзывами и оценками 5 звёзд.
108520
108711
  - Визуал: реалистичный стиль — как скриншот приложения/сайта отзывов (App Store, Google Play, или страница отзывов). Высокое качество, читабельный текст.
108521
108712
  - Отзывы: 2–4 отзыва с аватарками, именами и возрастом (формат «Имя, 45 лет»), ОБЯЗАТЕЛЬНО 5 звёзд (★★★★★) в каждом, короткий текст благодарности. Текст отзывов СТРОГО на языке GEO — никакого английского. Релевантен категории продукта.
108522
- - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, CAPS, на яркой подложке. Ключевое слово проблемы явно.
108713
+ - HOOK ОБЯЗАТЕЛЕН: крупно, выше блока отзывов или поверх, прописные буквы, на яркой подложке. Ключевое слово проблемы явно.
108523
108714
  - Продукт: виден в кадре (рядом со скрином или интегрирован в композицию).
108524
108715
  - Цена, скидка «-50%», CTA — в нижней части. БЕЗ буллитов — отзывы заменяют их.
108525
108716
  - Стиль: не мультяшный, не комикс — максимально реалистичный скрин.`,
108526
- headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, CAPS, ключевое слово проблемы явно.`,
108717
+ headlineAngle: `HOOK: угол «тысячи довольны» / «9 из 10 рекомендуют» / «реальный результат». 1–4 строки, до 12 слов, прописные буквы, ключевое слово проблемы явно.`,
108527
108718
  bulletsFocus: `БУЛЛИТЫ: ЗАПРЕЩЕНЫ. Вся информация — в HOOK, отзывах (с 5 звёздами), цене и CTA.`
108528
108719
  }
108529
108720
  ];