Перейти к основному содержимому

Голос покупателя — анализ отзывов для ТЗ на съёмку

Как превратить тысячи отзывов покупателей в конкретное техническое задание на фотосъёмку, которое решает реальные проблемы карточки, а не «делает красиво».

Проблема

Фотостудии создают контент, ориентируясь на собственное представление о том, что выглядит хорошо. Стандартный подход: студийный свет, белый фон, красивая раскладка. Результат — карточки, которые нравятся фотографу, но не отвечают на вопросы покупателя.

А покупатели буквально пишут, что им нужно:

  • «Фото не соответствует» — цвет, размер или текстура в реальности отличаются
  • «Не понятен размер» — заказали не тот, вернули, оставили 1 звезду
  • «В жизни выглядит иначе» — ожидание vs. реальность, возврат + негативный отзыв
  • «Хотелось бы видеть упаковку» — покупают в подарок, но не знают, как выглядит коробка
  • «Материал не тот, что на фото» — глянцевый вместо матового, тонкий вместо плотного

Проблема в том, что никто не читает тысячи отзывов системно. Менеджер пролистает 20-30, составит ТЗ «от себя», а студия снимет «как обычно». Итог — новые фото не решают реальных болей покупателей, возвраты остаются высокими.

Почему отзывы -- золотая жила для контент-агентства?

Отзывы содержат прямую речь покупателя — то, что он хотел увидеть, но не увидел. Это готовое ТЗ на съёмку, написанное целевой аудиторией. Систематический анализ 500+ отзывов по SKU выявляет паттерны, которые невозможно заметить при ручном просмотре.

Пайплайн данных

flowchart LR
A["MPStats API\nGET /wb/get/item/{sku}/comments\nОтзывы клиента + ТОП-5"] --> B["NLP-экстракция\nВизуальные аспекты:\nразмер, цвет, текстура,\nупаковка, соответствие"]
B --> C["Категоризация\nПозитив vs Негатив\nпо каждому аспекту"]
C --> D["Частотный анализ\nТОП-проблемы\nТОП-похвалы"]
D --> E["NLP-бриф\nТЗ на фотосъёмку\ndata-driven"]

Используемые эндпоинты

ЭндпоинтКлючевые поляДля чего
GET /wb/get/item/{sku}/commentstext, rating, date, photosПолный массив отзывов по конкретному SKU — основа NLP-анализа
POST /wb/get/categoryid, revenue, comments, ratingОпределение ТОП-5 конкурентов по выручке для сравнительного анализа
Как работает эндпоинт отзывов

GET /wb/get/item/{sku}/comments возвращает все отзывы по артикулу: текст, рейтинг (1-5), дату, наличие фото покупателя. Для одного SKU с 200+ отзывами это полноценная выборка для статистического анализа. Для NLP-брифа собираем отзывы по SKU клиента и по 5 конкурентам — получаем 500-2000 отзывов для анализа.

Анализ

Шаг 1. Сбор отзывов клиента и конкурентов

import httpx
from collections import Counter, defaultdict

MPSTATS_TOKEN = "your_token"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": MPSTATS_TOKEN,
"Content-Type": "application/json",
}

def fetch_reviews(sku: int) -> list[dict]:
"""Загружает все отзывы по SKU."""
resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/comments",
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
return resp.json() # list[dict] — отзывы с text, rating, date


# SKU клиента
client_sku = 12345678
client_reviews = fetch_reviews(client_sku)

# ТОП-5 конкурентов (из предварительного анализа категории)
competitor_skus = [23456789, 34567890, 45678901, 56789012, 67890123]
competitor_reviews = []
for sku in competitor_skus:
competitor_reviews.extend(fetch_reviews(sku))

print(f"Отзывов клиента: {len(client_reviews)}")
print(f"Отзывов конкурентов: {len(competitor_reviews)}")
print(f"Всего для анализа: {len(client_reviews) + len(competitor_reviews)}")

Шаг 2. NLP-экстракция визуальных аспектов

Ключевая часть — извлечение из текста отзывов упоминаний визуальных характеристик товара. Для каждого упоминания определяем: позитивное оно или негативное.

import re

# === Словарь визуальных аспектов ===
# Каждый аспект: список ключевых фраз + контекстные индикаторы
VISUAL_ASPECTS = {
"размер_масштаб": {
"keywords": [
"размер", "масштаб", "больше чем", "меньше чем",
"крупнее", "мельче", "огромный", "маленький",
"не тот размер", "размер не соответствует",
"в руках", "на ладони", "рядом с",
],
"action": "Фото с масштабной линейкой + в руках модели",
},
"цвет_реальность": {
"keywords": [
"цвет", "оттенок", "в жизни", "на самом деле",
"не соответствует", "другой цвет", "темнее", "светлее",
"ярче", "бледнее", "цветопередача", "фото врёт",
"отличается от фото",
],
"action": "Съёмка при дневном свете с калибровкой цвета",
},
"упаковка": {
"keywords": [
"упаковка", "коробка", "пакет", "подарок",
"подарочная", "красиво упакован", "пришло в пакете",
"комплект", "что в коробке", "комплектация",
],
"action": "Отдельное фото упаковки + анбоксинг-кадр",
},
"текстура_материал": {
"keywords": [
"текстура", "материал", "ткань", "на ощупь",
"качество", "тонкий", "плотный", "мягкий",
"жёсткий", "глянцевый", "матовый", "прозрачный",
"просвечивает", "приятный на ощупь",
],
"action": "Макро-съёмка текстуры + фото при боковом свете",
},
"детали_фурнитура": {
"keywords": [
"детали", "фурнитура", "молния", "пуговицы",
"швы", "строчка", "подкладка", "карманы",
"застёжка", "замок", "ручка", "ремешок",
],
"action": "Детальные фото каждого элемента (крупный план)",
},
"посадка_вид_на_фигуре": {
"keywords": [
"сидит", "посадка", "на фигуре", "на модели",
"фигура", "рост", "размер модели", "полнит",
"стройнит", "мешковатый", "обтягивает",
"как выглядит на", "на мне",
],
"action": "Фото на 3+ типах фигур + указание размеров модели",
},
}

# === Индикаторы тональности ===
POSITIVE_MARKERS = [
"отлично", "супер", "красив", "качествен", "соответствует",
"как на фото", "рекомендую", "доволен", "порадовал",
"лучше чем", "приятно удивил", "идеально",
]
NEGATIVE_MARKERS = [
"не соответствует", "разочарован", "плохо", "ужасн",
"обман", "не тот", "отличается", "врёт", "фейк",
"возврат", "хуже чем", "не рекомендую", "жалею",
]


def classify_sentiment(text: str) -> str:
"""Определяет тональность упоминания: positive / negative / neutral."""
text_lower = text.lower()
pos_count = sum(1 for m in POSITIVE_MARKERS if m in text_lower)
neg_count = sum(1 for m in NEGATIVE_MARKERS if m in text_lower)

if neg_count > pos_count:
return "negative"
elif pos_count > neg_count:
return "positive"
return "neutral"


def extract_visual_mentions(reviews: list[dict]) -> dict:
"""
Извлекает упоминания визуальных аспектов из отзывов.
Возвращает: {aspect: {"positive": N, "negative": N, "neutral": N, "examples": [...]}}
"""
results = {}
for aspect, config in VISUAL_ASPECTS.items():
results[aspect] = {
"positive": 0,
"negative": 0,
"neutral": 0,
"total": 0,
"examples_neg": [],
"examples_pos": [],
"action": config["action"],
}

for review in reviews:
text = review.get("text", "")
text_lower = text.lower()

for aspect, config in VISUAL_ASPECTS.items():
# Проверяем, упоминается ли аспект
mentioned = any(kw in text_lower for kw in config["keywords"])
if not mentioned:
continue

sentiment = classify_sentiment(text)
results[aspect][sentiment] += 1
results[aspect]["total"] += 1

# Сохраняем примеры (до 5 на категорию)
if sentiment == "negative" and len(results[aspect]["examples_neg"]) < 5:
results[aspect]["examples_neg"].append(text[:200])
elif sentiment == "positive" and len(results[aspect]["examples_pos"]) < 5:
results[aspect]["examples_pos"].append(text[:200])

return results

Шаг 3. Частотный анализ и приоритизация

def analyze_and_prioritize(
client_mentions: dict,
competitor_mentions: dict,
) -> list[dict]:
"""
Сравнивает визуальные проблемы клиента с конкурентами.
Возвращает приоритизированный список действий.
"""
priorities = []

for aspect, data in client_mentions.items():
if data["total"] == 0:
continue

total = data["total"]
neg_pct = data["negative"] / total * 100 if total > 0 else 0
pos_pct = data["positive"] / total * 100 if total > 0 else 0

# Данные конкурентов для сравнения
comp = competitor_mentions.get(aspect, {})
comp_total = comp.get("total", 0)
comp_neg_pct = (
comp.get("negative", 0) / comp_total * 100
if comp_total > 0
else 0
)

# Приоритет = частота упоминаний * % негатива * коэффициент
# Если у конкурентов негатива меньше -- значит, они решили проблему
priority_score = total * (neg_pct / 100)
if comp_neg_pct < neg_pct:
# Конкуренты справляются лучше -- нужно догонять
priority_score *= 1.5

priorities.append({
"aspect": aspect,
"total_mentions": total,
"positive_pct": round(pos_pct, 1),
"negative_pct": round(neg_pct, 1),
"comp_negative_pct": round(comp_neg_pct, 1),
"priority_score": round(priority_score, 1),
"action": data["action"],
"examples_neg": data["examples_neg"][:3],
"examples_pos": data["examples_pos"][:3],
})

# Сортируем: самые болезненные проблемы наверху
priorities.sort(key=lambda x: x["priority_score"], reverse=True)
return priorities


# --- Запуск анализа ---
client_mentions = extract_visual_mentions(client_reviews)
competitor_mentions = extract_visual_mentions(competitor_reviews)

priorities = analyze_and_prioritize(client_mentions, competitor_mentions)

print("Приоритизация визуальных аспектов для ТЗ на съёмку:\n")
for i, p in enumerate(priorities, 1):
print(f"{i}. [{p['aspect']}] Score: {p['priority_score']}")
print(f" Упоминаний: {p['total_mentions']}, "
f"Негатив: {p['negative_pct']}%, "
f"У конкурентов: {p['comp_negative_pct']}%")
print(f" Действие: {p['action']}")
print()

Шаг 4. Сводная таблица визуальных аспектов

Результат анализа для конкретного клиента (пример — женская сумка, 487 отзывов клиента + 1230 отзывов конкурентов):

Визуальный аспектУпоминанийПозитивныхНегативныхУ конкурентов (негатив)Действие
Размер / масштаб23445%55%28%Добавить фото с линейкой + в руках модели
Цвет в реальности18930%70%35%Снять при дневном свете с калибровкой цвета
Упаковка15680%20%22%Показать упаковку крупно (покупатели хвалят!)
Детали / текстура9860%40%18%Макро-съёмка деталей и фурнитуры
Посадка на фигуре7435%65%40%Фото на 3 типах фигур с указанием параметров
Материал6350%50%30%Видео с демонстрацией плотности и текстуры
Как читать таблицу
  • Высокий негатив у клиента + низкий у конкурентов = конкуренты уже решили проблему фотоконтентом, клиент отстаёт. Пример: «Цвет в реальности» -- 70% негатива у клиента vs. 35% у конкурентов. Значит, конкуренты лучше снимают цветопередачу.
  • Высокий позитив = сильная сторона клиента, сохранить и усилить. Пример: «Упаковка» -- 80% позитива, покупатели хвалят. Добавляем это как USP в карточку.
  • Высокая частота + высокий негатив = приоритет #1 для пересъёмки.

Действие Fotofactor

На основе NLP-анализа Fotofactor формирует data-driven ТЗ на съёмку — бриф, в котором каждый кадр обоснован данными из отзывов.

NLP-бриф: структура

def generate_photo_brief(priorities: list, client_sku: int) -> str:
"""Генерирует ТЗ на съёмку на основе NLP-анализа отзывов."""

brief_sections = []
brief_sections.append(f"# ТЗ на фотосъёмку — SKU {client_sku}")
brief_sections.append(f"# Основание: NLP-анализ {len(client_reviews)} отзывов\n")

for i, p in enumerate(priorities, 1):
if p["priority_score"] < 5:
continue # Пропускаем незначительные аспекты

section = f"## Приоритет #{i}: {p['aspect']}\n"
section += f"- Упоминаний в отзывах: {p['total_mentions']}\n"
section += f"- Негативных: {p['negative_pct']}%\n"
section += f"- У конкурентов негатива: {p['comp_negative_pct']}%\n"
section += f"- **Требование к съёмке**: {p['action']}\n"

if p["examples_neg"]:
section += "\nПримеры негативных отзывов (цитаты):\n"
for ex in p["examples_neg"]:
section += f' > "{ex}"\n'

brief_sections.append(section)

return "\n".join(brief_sections)

Пример NLP-брифа

Пример: бриф для женской кожаной сумки (SKU 12345678)

Приоритет #1 — Цвет (70% негатива)

Проблема: покупатели массово жалуются, что цвет не соответствует фото. «Заказывала бордовую, пришла коричневая», «На фото выглядит ярко, в жизни тусклый».

Требования к съёмке:

  • Снять при дневном свете (не студийном!) для максимальной цветопередачи
  • Сделать кадры при 3 типах освещения: дневной, тёплый вечерний, офисный
  • Добавить сравнительный кадр «на экране vs. в руках» для управления ожиданиями
  • Цветокоррекция — минимальная, ближе к реальности

Приоритет #2 — Размер (55% негатива)

Проблема: «Думала будет больше», «Не помещается А4», «На фото казалась вместительнее».

Требования к съёмке:

  • Фото с масштабной линейкой (рулетка рядом с сумкой)
  • Фото в руках модели (видно пропорцию)
  • Фото с типичным содержимым: телефон, кошелёк, косметичка, A4 блокнот
  • Инфографика: размеры в см наложены прямо на фото

Приоритет #3 — Посадка на фигуре (65% негатива)

Проблема: «На модели смотрится одним образом, на мне — другим», «Длина ремешка не регулируется, но на фото не видно».

Требования к съёмке:

  • Фото на 3 типах фигур (42, 46, 50 размер) с указанием роста модели
  • Демонстрация регулировки ремешка (минимальная и максимальная длина)
  • Видео 10 секунд: модель надевает сумку, показывает посадку в движении
Пример: бриф для электрочайника (SKU 87654321)

Приоритет #1 — Размер / объём (62% негатива)

«Выглядит компактным на фото, а на кухне занимает полстола», «1.7 литра — это сколько чашек?»

Требования к съёмке:

  • Фото на кухонной столешнице рядом с привычными предметами (чашка, тарелка)
  • Инфографика: «1.7 л = 8 чашек» наложена на фото
  • Кадр с человеческой рукой на ручке — чтобы был понятен масштаб

Приоритет #2 — Качество / материал (58% негатива)

«Думал металл, оказался пластик», «Крышка хлипкая», «Колба внутри — дешёвый пластик, хотя на фото выглядит как стекло».

Требования к съёмке:

  • Макро-кадр материала корпуса (текстура пластика/металла)
  • Фото внутренней колбы крупным планом
  • Видео: открытие крышки (показать механизм и качество)

Адресация ТОП-жалоб в инфографике

На основе NLP-анализа Fotofactor создаёт инфографику, которая упреждающе отвечает на типичные жалобы:

# Маппинг: жалоба из отзыва → элемент инфографики
COMPLAINT_TO_INFOGRAPHIC = {
"цвет не соответствует": {
"element": "Плашка с текстом",
"text": "Фото сделано при дневном свете без фильтров",
"placement": "Фото #2 (общий план)",
},
"не понятен размер": {
"element": "Размерная сетка на фото",
"text": "34 × 25 × 12 см | Вмещает А4",
"placement": "Фото #3 (инфографика)",
},
"не видно деталей": {
"element": "Выноски с зумом",
"text": "Стрелки указывают на ключевые элементы",
"placement": "Фото #5 (детали)",
},
"на модели иначе": {
"element": "Параметры модели",
"text": "Модель: рост 170 см, размер 44",
"placement": "Фото #4 (на модели)",
},
}

Видеоконтент на основе отзывов

Если анализ показывает высокий процент жалоб типа «фото не соответствует действительности», Fotofactor создаёт видео реалистичной демонстрации:

  • Анбоксинг — покупатель видит, как товар выглядит «из коробки»
  • 360-обзор — товар со всех сторон при естественном освещении
  • В контексте использования — как товар выглядит в реальной жизни, а не в студии
  • Сравнение с ожиданиями — «Вот что вы увидите, когда получите посылку»

Результат для клиента

Снижение возвратов

Главный финансовый эффект — уменьшение процента возвратов. На WB/Ozon возврат стоит продавцу 200-500 рублей за единицу (логистика + обработка). При 1000 продаж в месяц:

МетрикаДо NLP-брифаПосле NLP-брифаЭкономия
Процент возвратов18%8-12%5-10%
Возвратов в месяц (1000 продаж)180 шт80-120 шт60-100 шт
Стоимость возвратов (по 350 руб)63 000 руб28-42 000 руб21-35 000 руб/мес
Экономика снижения возвратов

При среднем чеке товара 3 000 руб и 1 000 продаж в месяц:

  • 5% снижение возвратов = 50 меньше возвратов = 17 500 руб/мес экономии только на логистике
  • Плюс 150 000 руб/мес — эти 50 единиц остаются проданными, а не возвращёнными
  • Итого экономический эффект: ~170 000 руб/мес с одного SKU
  • Окупаемость съёмки (80 000 руб) — за 2 недели

Рост рейтинга и позиций

  • Меньше возвратов = меньше негативных отзывов
  • Новые покупатели видят, что «фото соответствует» — оставляют положительные отзывы
  • Рейтинг растёт с 4.2 до 4.5-4.7 → карточка поднимается в выдаче
  • Эффект накопительный: лучшие фото → меньше возвратов → лучший рейтинг → больше продаж

Для Fotofactor: премиальная услуга

МетрикаСтандартная съёмкаСъёмка с NLP-брифомРазница
Цена за SKU25 000 руб40 000 руб+15 000 руб
Время подготовки ТЗ30 мин (на глаз)2 часа (анализ + бриф)+1.5 часа
Обоснование цены«Красивые фото»«Данные из 500 отзывов»Data-driven
Конверсия в повторный заказ30%60%x2

Уникальное позиционирование для Fotofactor:

«Мы снимаем то, что хотят видеть ваши покупатели, а не то, что красиво выглядит в портфолио»

Это не абстрактное обещание, а конкретный процесс с измеримым результатом: бриф показывает клиенту цитаты из его отзывов, и клиент сам видит, что контент решает реальные проблемы.

Полный скрипт

Минимальный рабочий пайплайн: от загрузки отзывов до генерации брифа.

import httpx
import json
from datetime import datetime

TOKEN = "YOUR_MPSTATS_TOKEN"
BASE_URL = "https://mpstats.io/api"
HEADERS = {
"X-Mpstats-TOKEN": TOKEN,
"Content-Type": "application/json",
}

# --- Конфигурация ---
CLIENT_SKU = 12345678
COMPETITOR_SKUS = [23456789, 34567890, 45678901, 56789012, 67890123]

# Визуальные аспекты и ключевые слова
VISUAL_KEYWORDS = {
"размер": ["размер", "масштаб", "больше", "меньше", "огромный", "маленький"],
"цвет": ["цвет", "оттенок", "в жизни", "не соответствует", "темнее", "светлее"],
"упаковка": ["упаковка", "коробка", "подарок", "комплект"],
"текстура": ["текстура", "материал", "ткань", "на ощупь", "качество", "тонкий"],
"детали": ["детали", "фурнитура", "молния", "швы", "карманы", "застёжка"],
"посадка": ["сидит", "посадка", "на фигуре", "полнит", "мешковатый"],
}

NEGATIVE_WORDS = [
"не соответствует", "разочарован", "плохо", "обман",
"отличается", "возврат", "хуже", "не рекомендую",
]
POSITIVE_WORDS = [
"отлично", "супер", "красив", "как на фото",
"рекомендую", "доволен", "идеально",
]


def fetch_reviews(sku: int) -> list[dict]:
"""Загружает отзывы по SKU."""
resp = httpx.get(
f"{BASE_URL}/wb/get/item/{sku}/comments",
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
return resp.json()


def get_sentiment(text: str) -> str:
"""Определяет тональность: positive / negative / neutral."""
t = text.lower()
pos = sum(1 for w in POSITIVE_WORDS if w in t)
neg = sum(1 for w in NEGATIVE_WORDS if w in t)
if neg > pos:
return "negative"
if pos > neg:
return "positive"
return "neutral"


def analyze_reviews(reviews: list[dict]) -> dict:
"""Извлекает визуальные аспекты и считает статистику."""
stats = {
aspect: {"positive": 0, "negative": 0, "neutral": 0, "total": 0}
for aspect in VISUAL_KEYWORDS
}

for review in reviews:
text = review.get("text", "").lower()
for aspect, keywords in VISUAL_KEYWORDS.items():
if any(kw in text for kw in keywords):
sentiment = get_sentiment(review.get("text", ""))
stats[aspect][sentiment] += 1
stats[aspect]["total"] += 1

return stats


def generate_brief(client_stats: dict, comp_stats: dict, sku: int) -> str:
"""Генерирует ТЗ на съёмку."""
lines = [
f"# NLP-БРИФ НА ФОТОСЪЁМКУ",
f"# SKU: {sku}",
f"# Дата: {datetime.now().strftime('%Y-%m-%d')}",
"",
]

# Сортируем по % негатива
aspects = []
for aspect, data in client_stats.items():
if data["total"] == 0:
continue
neg_pct = data["negative"] / data["total"] * 100
comp_data = comp_stats.get(aspect, {"negative": 0, "total": 1})
comp_neg = (
comp_data["negative"] / comp_data["total"] * 100
if comp_data["total"] > 0
else 0
)
aspects.append((aspect, data, neg_pct, comp_neg))

aspects.sort(key=lambda x: x[2], reverse=True)

for i, (aspect, data, neg_pct, comp_neg) in enumerate(aspects, 1):
lines.append(f"## Приоритет #{i}: {aspect.upper()}")
lines.append(f" Упоминаний: {data['total']}")
lines.append(f" Негатив: {neg_pct:.0f}% (у конкурентов: {comp_neg:.0f}%)")

gap = neg_pct - comp_neg
if gap > 15:
lines.append(f" *** КРИТИЧНО: отставание от конкурентов на {gap:.0f}% ***")

lines.append("")

return "\n".join(lines)


# --- Основной пайплайн ---
if __name__ == "__main__":
# 1. Сбор отзывов
print(f"Загрузка отзывов SKU {CLIENT_SKU}...")
client_reviews = fetch_reviews(CLIENT_SKU)
print(f" Получено: {len(client_reviews)} отзывов")

comp_reviews = []
for sku in COMPETITOR_SKUS:
reviews = fetch_reviews(sku)
comp_reviews.extend(reviews)
print(f" Конкурент {sku}: {len(reviews)} отзывов")

print(f"\nВсего для анализа: {len(client_reviews) + len(comp_reviews)}")

# 2. NLP-анализ
client_stats = analyze_reviews(client_reviews)
comp_stats = analyze_reviews(comp_reviews)

# 3. Генерация брифа
brief = generate_brief(client_stats, comp_stats, CLIENT_SKU)
print("\n" + brief)

# 4. Сохранение
output = {
"sku": CLIENT_SKU,
"date": datetime.now().isoformat(),
"client_reviews_count": len(client_reviews),
"competitor_reviews_count": len(comp_reviews),
"client_stats": client_stats,
"competitor_stats": comp_stats,
"brief": brief,
}

filename = f"nlp_brief_{CLIENT_SKU}_{datetime.now():%Y%m%d}.json"
with open(filename, "w", encoding="utf-8") as f:
json.dump(output, f, ensure_ascii=False, indent=2)
print(f"\nСохранено: {filename}")

Что дальше