← Журнал

От сырых фотографий к многоканальному складу: проектирование надёжного конвейера приёма

Архитектурная ретроспектива конвейера, который распознаёт предметы по фотографиям, готовит их объявления и синхронизирует их жизненный цикл на нескольких маркетплейсах.

АрхитектураИскусственный интеллектМаркетплейсыПриём данныхСобытийная архитектура

От сырых фотографий к многоканальному складу

Создать объявление из нескольких фотографий кажется простым: распознать предмет, сгенерировать заголовок, оценить цену и вызвать API маркетплейса. Такая картина работает для демонстрации. Её перестаёт хватать, как только вы обрабатываете целый ящик, фотографии приходят вразнобой или один и тот же предмет нужно выставить на нескольких каналах.

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

Статья сначала описывает рабочий конвейер: от папки с фотографиями до черновиков объявлений. Затем предлагает его развитие к многоканальной архитектуре, в которой внутренняя база остаётся источником истины, а адаптеры синхронизируют eBay, Leboncoin и других дистрибьюторов.

Ведущая идея: ИИ может производить наблюдения и предложения. Идентичность, остатки и необратимые переходы должны оставаться под контролем явных прикладных правил.

Фотография, предмет и объявление, три разные идентичности

Ящик книг хорошо иллюстрирует проблему. Одна и та же книга может быть представлена обложкой, задней стороной, корешком, титульным листом и несколькими внутренними страницами. И наоборот, два похожих издания могут иметь общего автора, серию, переплёт или почти одинаковый вид.

Поэтому нужно различать как минимум:

  • медиафайл, идентифицируемый содержимым и хешем;
  • физический предмет, здесь единственный экземпляр, у которого может быть несколько фотографий;
  • каноническую карточку, объединяющую название, состояние, атрибуты и цену;
  • удалённое объявление, свойственное конкретному поставщику и его ограничениям;
  • продажу, которая расходует или резервирует складскую единицу.

Смешение этих идентичностей порождает самые дорогие ошибки: фотографии, привязанные не к той книге, дубли объявлений, изменение товара на основе данных, специфичных для маркетплейса, или двойную продажу предмета, имеющегося в единственном экземпляре.

Первоначальный конвейер: превратить ящик в проверяемые книги

Первая версия системы корректно обрабатывает книгу, когда её фотографии уже сгруппированы. Расширение до ящика добавляет предварительный шаг: найти наборы фотографий, соответствующие каждому предмету, не полагаясь на порядок в папке, имена файлов или искусственные разделители.

1. Инвентаризировать ненадёжные входные данные

Каждое изображение сначала рассматривается как ненадёжный ввод:

  • реальный формат проверяется декодированием, а не только расширением;
  • действуют ограничения на число файлов, их размер и количество распакованных пикселей;
  • ориентация EXIF исправляется локально, без изменения оригинала;
  • вычисляется хеш для идентичности, возобновления и трассируемости;
  • читается метка времени съёмки из EXIF, если она есть;
  • оригиналы сохраняются до подтверждения результата.

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

2. Описать прежде, чем группировать

Недорогая мультимодальная модель описывает изображения пакетами по закрытой схеме. Она не выбирает путей и не перемещает файлы. Для каждой фотографии она извлекает, в частности:

  • тип ракурса: обложка, задняя сторона, корешок, титульный лист, разворот;
  • видимые библиографические элементы: название, автор, издательство, ISBN;
  • отличительные признаки: переплёт, дефекты, узоры, пометки;
  • реальное присутствие книги и уверенность наблюдения.

Эти структурированные наблюдения затем питают разреженный граф кандидатов. Два изображения могут стать кандидатами благодаря одинаковому ISBN, пересечению текста, совместимым визуальным признакам или временной близости.

Метка времени намеренно остаётся мягкой подсказкой. Две фотографии, сделанные с разницей в несколько минут, с большой вероятностью показывают один предмет, но две разные книги тоже могли быть сняты подряд. Временная близость открывает проверку; она никогда не запускает слияние сама по себе.

3. Предпочесть лишнее разделение ошибочному смешению

Каждая пара или группа кандидатов проходит целевую визуальную проверку. Слияние принимается только при положительном доказательстве. Сильный конфликт (например, два разных ISBN) блокирует сопоставление. Неуверенное решение не создаёт никакой связи.

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

Результат, полное и трассируемое разбиение:

  • группы, признанные надёжными;
  • группы, отмеченные для ревью;
  • неназначенные фотографии;
  • проигнорированные изображения, на которых не обнаружено пригодного предмета.

4. Оставить человека на необратимой границе

Интерфейс напрямую отражает эту последовательность входов и выходов: импортировать, сгруппировать, проверить, затем создать. Надёжные группы могут быть предвыбраны; неоднозначные случаи, никогда. Неназначенные и проигнорированные фотографии остаются видимыми, но не отправляются молча в следующий конвейер.

После подтверждения каждая группа становится входом единичного конвейера:

  1. подробный анализ предмета и его состояния;
  2. построение атрибутов категории;
  3. поиск аналогов и оценка цены;
  4. бережная подготовка фотографий;
  5. загрузка медиа;
  6. создание черновика без автоматической публикации.

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

МаркетплейсЕдиничный конвейерМультимодальная модельСортировщикМенеджер задачИнтерфейс ревьюМаркетплейсЕдиничный конвейерМультимодальная модельСортировщикМенеджер задачИнтерфейс ревьюloop[Для каждого полезного сопоставления]par[Метаданные и цена][Подготовка медиа]loop[Для каждого подтверждённого предмета]ОператорВыбирает папку с фотографиями1Создаёт постоянную задачу2Инвентаризирует и проверяет оригиналы3Хеш, EXIF, размеры, миниатюры4Описывает изображения пакетами5Структурированные наблюдения6Строит граф кандидатов7Проверяет два изображения или группы8тот же предмет / другой / неясно9Применяет конфликты и пороги10Группы, ревью, unassigned, ignored11Результат и прогресс12Явно подтверждает группы13Запускает единичный приём14Подробный анализ15Структурированная карточка16Поворот и бережное улучшение17Загружает фотографии18Создаёт карточку и черновик19Удалённые идентификаторы20Успех или трассируемая ошибка21Оператор

Что эта первая архитектура уже решает

Четыре свойства важнее точного выбора модели:

  1. Осторожность: никакого слабого сходства недостаточно для слияния двух предметов.
  2. Возобновляемость: дорогие наблюдения и решения кешируются по хешам, модели и версии промпта.
  3. Трассируемость: каждый медиафайл появляется ровно один раз в итоговом манифесте, с его размещением и связанными решениями.
  4. Разделение ответственности: модель наблюдает, ядро домена решает, интерфейс запрашивает подтверждение, затем адаптер разговаривает с поставщиком.

Этой архитектуры достаточно, пока единственным каналом сбыта остаётся один маркетплейс, а его черновики могут играть роль удалённого склада. Но она упирается в структурный предел, как только единственный предмет публикуется в нескольких местах.

От конвейера публикации к складской системе

В многоканальной системе eBay, Leboncoin или любой другой дистрибьютор не должны становиться источником истины о предмете. У каждой платформы свои категории, состояния и идентификаторы. Ни одна не имеет надёжного представления об остальных.

Внутренняя база должна нести каноническое состояние: идентичность предмета, складскую единицу, медиа, базовую цену, жизненный цикл, удалённые объявления и продажу. Приложение читает эту базу; поставщики получают её проекции, адаптированные к их контрактам.

Целевая архитектура сочетает три классических паттерна:

  • порты и адаптеры, чтобы изолировать особенности каждого маркетплейса;
  • транзакционные outbox/inbox, чтобы синхронизировать базу и удалённые вызовы, не теряя намерение и не воспроизводя его опасным образом;
  • периодическую сверку, чтобы исправлять неизбежное расхождение между системами.
Фотографии и импортКонвейер приёмаКаноническая базаOutboxВоркерысинхронизацииАдаптер eBayАдаптер LeboncoinДругиедистрибьюторыeBayLeboncoinДругие каналыInbox событий и опросПланировщикСверкаПриложение идашбордОповещения

Модель данных, сосредоточенная на физическом предмете

Для предметов, продаваемых поштучно, минимальная модель может оставаться простой:

СущностьОтветственность
media_assetОригинал, хеш, метаданные EXIF и производные
itemКаноническая карточка, независимая от маркетплейсов
inventory_unitФизический экземпляр и реальная доступность
listingПроекция предмета для конкретного поставщика
provider_bindingУдалённые идентификаторы, версия и последнее известное состояние
reservationВременная блокировка остатка на время транзакции
saleПринятая продажа, исходный поставщик и финансовые данные
inbox_eventДедуплицированное входящее событие
outbox_eventНамерение синхронизации к исполнению
sync_attemptПопытка, задержка, нормализованный ответ и ошибка

Коммерческое содержимое может отличаться по каналам, но идентичность и количество нельзя дублировать в каждом объявлении. Для единственной книги inventory_unit.available_quantity равно нулю или единице: это ограничение база должна гарантировать транзакционно.

Публиковать без хрупкой двойной записи

Наивная реализация выполняет две последовательные операции: обновить базу, затем вызвать маркетплейс. Если процесс падает между ними, база и поставщик расходятся. Обратный порядок ничего не решает: объявление может быть создано, тогда как локальная транзакция не удалась.

Транзакционный outbox избегает этой ловушки:

  1. локальная транзакция изменяет предмет и вставляет намерение в outbox_event;
  2. воркер читает это намерение и вызывает нужный адаптер;
  3. адаптер использует ключ идемпотентности или стабильный бизнес-идентификатор;
  4. удалённый ответ обновляет provider_binding и состояние объявления;
  5. повтор воспроизводит то же намерение, а не новое логическое создание.

Мы не гонимся за гипотетическим распределённым «exactly once». Мы принимаем доставку at least once и делаем каждую обработку идемпотентной.

Адаптер предоставляет общий словарь, например:

  • upsert_draft(item);
  • publish(listing);
  • update_price(listing, price);
  • reserve_or_pause(listing);
  • end_listing(listing, reason);
  • fetch_status(binding);
  • fetch_recent_sales(cursor).

Каждый поставщик затем переводит этот контракт в свой API (или в разрешённый платформой режим синхронизации) не поднимая своих деталей в бизнес-домен.

Управлять жизненным циклом, а не булевыми флагами

Поля published = true недостаточно. Явный конечный автомат делает переходы наблюдаемыми и исключает невозможные сочетания.

импорт подтверждённедостаточнаяуверенностьисправлениечеловекомканоническая карточкаполнанамерения в outboxхотя бы одно активноеобъявлениеошибка поставщикаповтор илиисправлениевременноеобязательстворезерв истёкоплата или продажаподтвержденаудалённая продажаподтвержденаснятие остальныхобъявленийканалы свереныснятие не завершеноIngestedNeedsReviewReadyPublishingAvailableSyncErrorReservedSoldClosingChannelsArchived

У объявлений есть собственное состояние (черновик, публикуется, активно, приостанавливается, завершено, ошибка,) но они остаются проекциями складской единицы. Активное объявление никогда не делает предмет доступным, если inventory_unit уже показывает sold.

Продажа на одном канале должна закрыть остальные

Когда маркетплейс сообщает о продаже, входящая обработка тоже идёт через идемпотентный inbox. Сырое событие сохраняется, дедуплицируется по идентификатору поставщика и применяется в локальной транзакции.

ОповещенияМаркетплейс BВоркерOutboxКаноническая базаInboxВебхук или поллерМаркетплейс AОповещенияМаркетплейс BВоркерOutboxКаноническая базаInboxВебхук или поллерМаркетплейс Aalt[Единица ещё доступна][Единица уже зарезервирована или продана]Продажа подтверждена1Записывает удалённое событие2Транзакция расхода остатка3Переводит available в sold4Создаёт продажу5Добавляет END_OTHER_LISTINGS6Событие принято7Распределяет намерение8Завершает или отключает объявление9Удалённое подтверждение10Помечает канал сверенным11Отклоняет второй расход12Создаёт оповещение о коллизии13Событие сохранено для разбора14

Переход available → sold должен использовать блокировку, сравнение версий или условное обновление. Так два одновременных события не могут израсходовать одну и ту же единицу в базе.

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

Вебхуки не заменяют сверку

Вебхук может потеряться, прийти с опозданием или несколько раз. Некоторые платформы не предоставляют его для всех событий. Поэтому поллер и задача сверки остаются необходимыми даже в событийной архитектуре.

Cron не должен содержать бизнес-логику. Он запускает идемпотентные задачи, защищённые от параллельных запусков и наблюдаемые в той же системе, что и остальная обработка.

Разумная стартовая периодичность может быть такой:

Ориентировочная периодичностьПроверкаОжидаемый результат
Каждые 1–5 минутПродажи и резервы без вебхукаБыстро закрыть остальные каналы
Каждые 10–15 минутСостояния и количества активных объявленийОбнаружить расхождения остатков
Каждый часЗависшие задачи, повторы и истёкшие резервыПочинить или оповестить
Каждую ночьПолная сверкаНайти осиротевшие объявления и пропущенные продажи
Каждое утроОперационная сводкаДать команде приоритеты ревью

Эти частоты должны соблюдать ограничения и условия использования каждой платформы.

Дашборд, ориентированный на аномалии

Хороший дашборд не ограничивается показом числа объявлений. Он должен быстро отвечать на четыре вопроса: чем мы владеем, где это опубликовано, что расходится и какое действие человека требуется?

Самые полезные метрики:

  • предметы доступные, зарезервированные, проданные и на ревью;
  • активные объявления по поставщикам;
  • удалённые объявления, осиротевшие или без канонического предмета;
  • расхождения цены, количества или статуса;
  • задержка между продажей и снятием с остальных каналов;
  • возраст самого старого необработанного события inbox/outbox;
  • доля отказов и число повторов по адаптерам;
  • доля неназначенных фотографий и групп, отправленных на ревью;
  • средняя стоимость вызовов ИИ на один принятый предмет;
  • объём продаж и маржа по каналам.

Оповещения можно ранжировать по влиянию:

  • критические: коллизия продажи, проданный предмет всё ещё активен в другом месте;
  • высокие: необработанное событие продажи, неудачное удалённое снятие;
  • средние: рассинхронизированное объявление, зависшая задача, истёкший резерв;
  • информационные: рост доли ревью или дрейф стоимости приёма.

Каждое оповещение должно указывать на предмет, затронутые объявления, последний успешный переход и рекомендуемое действие. Оповещение без контекста лишь перекладывает диагностику на оператора.

Развернуть это развитие, не переписывая конвейер

Миграция может оставаться поэтапной:

  1. Ввести каноническую модель. Сохранять предметы и складские единицы до создания черновиков уже поддерживаемого поставщика.
  2. Инкапсулировать первого поставщика. Превратить существующую интеграцию в адаптер и систематически хранить удалённые идентификаторы.
  3. Добавить outbox, inbox и задачи сверки. Сделать повторы идемпотентными до увеличения числа каналов.
  4. Подключить второго дистрибьютора. Начать с черновиков, затем публикация, наконец, обратная связь о продажах.
  5. Автоматизировать многоканальное закрытие. Измерять задержку, тестировать коллизии и сохранять режим разбора человеком.
  6. Построить оповещения и дашборд. Питать их теми же событиями и состояниями, а не параллельными счётчиками.

Конвейеру изображений не нужно знать про eBay или Leboncoin. Он производит канонический предмет и проверенные медиа. Адаптеры не должны знать, как были сгруппированы фотографии. Эта граница позволяет развивать модели ИИ, интерфейс и поставщиков независимо.

Заключение

Переход от нескольких фотографий к многоканальному сбыту не сводится к добавлению цикла по списку API. Он заставляет решить, где живёт истина, кто владеет остатками и как каждый переход можно воспроизвести, проверить или компенсировать.

Поэтому надёжный конвейер строится в два яруса:

  1. превратить неопределённые визуальные входные данные в проверенные канонические предметы;
  2. спроецировать эти предметы на несколько каналов, не отдавая им контроль над жизненным циклом.

ИИ ускоряет распознавание, описание и ценообразование. Каноническая база, транзакционные переходы, идемпотентные адаптеры и сверка защищают эксплуатацию. Именно это сочетание (а не модель сама по себе) превращает убедительную автоматизацию в настоящую складскую систему.