Кейс · май–сентябрь 2026 · Самара

Контент-завод: соцсети четырёх заведений ведёт один сервер

Ночной клуб, два ресторана и гостиница. У каждого свой бренд и своя манера говорить, площадок пять, а SMM-щик на всё это один. Я собрал систему, которая сама планирует рубрики, пишет тексты, рисует картинки, монтирует ролики и публикует их по расписанию. Ещё она выводит сообщения гостей на экран над входом, принимает брони столов, ведёт учёт концертов, отвечает на отзывы и следит за рекламой. За четыре месяца вышла 1 821 публикация, просмотров набралось около двух миллионов. Человек задаёт правила и даёт добро на рискованные шаги, остальное завод делает сам.

1 821публикация в соцсетях с 23 мая по 24 сентября
≈2 млнпросмотров по 991 посту, где собраны метрики
20+подсистем: от контент-плана до броней и экранов
100 тыс.строк кода: 61 тыс. Python и 39 тыс. панели управления
Задача

Один человек на четыре заведения и пять площадок

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

Карта завода

Что завод делает сам

Двадцать участков. Раньше это была чья-то ручная работа, а что-то не делалось вовсе. Цифры взяты из рабочей базы завода на 24 сентября.

Контент
01 · План

Рубрики и контент-план

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

02 · Конструктор

Кастомизатор постов

Новая рубрика собирается в панели без программиста: инструкция для Gemini, дни, время, площадки, свой пул фото или промптов.

32 свои рубрики · 149 пуловподробнее →
03 · Gemini

Gemini без платного API

Тексты, картинки, видео, разбор фото и модерацию завод делает через браузерную сессию Gemini на сервере, так же, как работал бы человек.

27 модулей завода работают с Geminiподробнее →
04 · Фото

Фотобанк с антиповтором

Раз в шесть часов забирает новые фото из альбомов ВК и размечает. Под пост подбирает кадр нужной ориентации, который не стоял в ленте 30 дней.

7 099 файлов в банке
05 · Статьи

Дзен, Пикабу, VC

Статьи для ресторана: темы без повторов, свой календарь выхода, отдельный лимит, чтобы не выжигать сессию Gemini.

41 статья опубликована
06 · Метрики

Что заходит, а что нет

Каждые 15 минут собирает просмотры и реакции со всех площадок. По ним правится план: какие рубрики и слоты работают.

≈2 млн просмотров
Видео и экраны
07 · Видео

Шесть режимов генерации

Шаблоны, автомонтаж с субтитрами, монтаж по чужому референсу, сборка по паттерну, кружки Telegram, ролики Veo.

34 композиции Remotionподробнее →
08 · After Effects

Видео-афиши выходных

Агент на компьютере с After Effects сам находит макет афиши, рендерит ролик и отдаёт его заводу. Ролик встаёт на пятницу и субботу.

09 · Экраны

Фасад и «стена» гостей

Афиша на фасадном экране собирается из программы вечера. Гость пишет боту, и через несколько секунд его текст уже над входом.

161 сообщение в эфиреподробнее →
Каналы и люди
10 · Кросспостинг

Один ролик на все каналы

Пост и история расходятся по VK, Telegram, Instagram, YouTube и TikTok. У каждой площадки свой API и свои правила.

835 VK · 616 TG · 287 IG · 45 YT · 38 TTподробнее →
11 · Бот

Чат-бот ВК

Отвечает гостям по сценариям, собирает базу подписчиков с тегами, запускает розыгрыши.

4 710 сообщений · 19 розыгрышей
12 · Рассылки

Рассылки по базе

Уходят сегментами в дни программы, только тем, кто разрешил сообщения.

30 729 отправок
13 · Найм

Поиск персонала

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

648 рассылок вакансий
14 · Отзывы

Яндекс и 2ГИС

Раз в полчаса собирает отзывы, на хорошие готовит ответ в тоне заведения, плохие отдаёт человеку, несправедливые обжалует.

3 094 отзыва · 237 ответов · 24 жалобы
15 · Доступы

Роли и организации

Менеджер заведения видит в панели только своё: фильтр на уровне базы и страж на входе API отдают 403 на чужую организацию.

Бизнес
16 · Брони

Своя система броней

Схемы залов, редактор, журнал и виджет для сайта. Собрал её, чтобы заменить платный сервис за 4 000 ₽ в месяц.

6 залов · 100 столовподробнее →
17 · Концерты

Учёт арендных мероприятий

Концерты раньше вели в Excel, теперь в таблице панели: оплаты, контрагенты, выгрузка обратно в тот же файл, новые даты сами подтягиваются с Яндекс.Афиши.

50 мероприятий · 26 контрагентовподробнее →
18 · Реклама

Директ и VK Реклама

Цифры из кабинетов тянет раз в три часа, правила автопилота проверяет каждый час, а раз в сутки присылает отчёт.

84 кампании под наблюдением
19 · Сайты

Мост в MODX

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

20 · Панель

Пульт на Next.js

30 разделов: календарь постов по всем заведениям, медиабанк, видеостудия, отзывы, реклама, бот, брони, экраны, концерты, найм.

39 тыс. строк TypeScript
Архитектура

Как устроено

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

Схема: источники контента, очередь постов, проверки, площадки; под ними соседние подсистемы
Python · FastAPISQLiteNext.js · TypeScriptPlaywrightGemini · Nano Banana · VeoRemotionAfter Effectsffmpeg · WhisperResolume · OSCVK APITelegram · TelethonInstagram GraphYouTube Data APIЯндекс.Директ APIMODXsystemd · nginx
Gemini

Gemini работает через браузер, как обычный сотрудник

Бесплатного лимита API на такой поток не хватает, а платить за каждый запрос четырёх заведений выходит дорого. Поэтому завод заходит в Gemini так же, как человек: через браузер на сервере, под аккаунтом Google Pro. Тексты, картинки, видео и разбор изображений получаются без оплаты за запрос, но вокруг живого браузера пришлось много чего построить.

Схема: одна браузерная сессия Gemini обслуживает восемь задач завода

Что пришлось построить вокруг браузера

  • Один браузер на всехСессию держит межпроцессный лок: посты, статьи и отзывы ждут своей очереди и не лезут в профиль одновременно. Если Chrome упал и оставил замок, тот снимается сам.
  • Временный чатВсе запросы идут во «временном чате». Так история аккаунта не забивается тысячами постов, и в ответы не протекает контекст прошлых заведений.
  • Живая мышьМеню Gemini не реагирует на программный клик, поэтому режим и соотношение сторон завод выбирает, двигая мышь по экрану.
  • Водяной знакУ картинок и роликов он всегда в одном углу. Завод срезает его сам, а в промпте просит оставить низ кадра пустым, чтобы вместе со знаком не потерять текст.
  • Отказы и сбоиОтказ модели ловится по маркерам, и запрос повторяется до трёх раз. Упавшую вкладку поднимает сторож, он проверяет её раз в 30 секунд.
  • УтечкиСторож раз в 15 минут гасит браузерные драйверы, которые остались без живого окна.
# tools/gemini_session_lock.py
LOCK_PATH   = "/tmp/gemini_session.lock"
PROFILE_DIR = "/opt/factory/browser-data"

def cleanup_stale_chrome(profile_dir=None) -> None:
    # SingletonLock указывает на мёртвый PID — удаляем,
    # иначе Chrome решит, что профиль занят
    profile = profile_dir or PROFILE_DIR
    sl = Path(profile) / "SingletonLock"
    if sl.is_symlink():
        target = os.readlink(str(sl))       # hostname-PID
        pid_part = target.rsplit("-", 1)[-1]
        try:
            pid = int(pid_part)
        except ValueError:
            pid = None
        if pid is None or not _pid_alive(pid):
            sl.unlink()
            for name in ("SingletonCookie", "SingletonSocket"):
                (Path(profile) / name).unlink(missing_ok=True)
Python · фрагмент, сокращены проверки
Где упирается. Видео Gemini даёт 2–4 ролика в сутки на аккаунт, так что Veo уходит только на самые важные кадры, остальное монтируется из банка. Опечатки и бессмыслицу маркеры не ловят, поэтому после массовой генерации тексты вычитывает человек.
Кросспостинг

Один ролик на пять каналов, и у каждого свой API

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

ПлощадкаКак публикует заводОсобенности
ВКонтактеVK API: стена, видео, клипы, историисвоя очередь вместо отложки ВК, проверка, что пост реально на стене; защита от флуда
TelegramБот API + личный премиум-аккаунт через Telethonпремиум-аккаунт нужен, чтобы в постах работали анимированные эмодзи; истории канала
InstagramGraph API: посты, карусели, Reels, историимедиа отдаётся по публичной ссылке завода, своя обложка у Reels
YouTubeData API: Shorts с докачкойсвоя обложка после загрузки
TikTokПолуручная очередьдля официального автопостинга нужен аудит приложения, поэтому завод готовит ролик, обложку и подпись с хэштегами, а человек жмёт «Залито»
Discord · MAXПодключаются в «Подключениях» по организациидля новых проектов, ключи вводятся без кода
  • ОбложкаСвоя картинка, кадр из ролика или фирменная обложка, которую завод рендерит сам в стиле заведения.
  • ПодписьСвоя или автоматическая: тезис по теме ролика плюс короткая подпись заведения.
  • ИсторииТот же ролик уходит в истории VK, Telegram и Instagram. У YouTube и TikTok историй нет, их завод пропускает.
  • Без дублейКаждый выход записывается отдельно. Если упала одна площадка, повтор добьёт только её, и на остальные пост второй раз не придёт.
# app/routers/fanout.py — POST /api/fanout/reel
VIDEO_PLATFORMS = ["vk", "tg", "ig", "yt", "tt"]
STORY_PLATFORMS = ("vk", "tg", "ig")   # у yt/tt историй нет

post = Post(id=pid, org_id=body.org_id, status="queued",
            format="video", format_label="Рилз — все каналы",
            text=caption, scheduled_at=when,
            cover_media_id=(cover.id if cover else None))
session.add(PostMedia(post_id=pid, media_id=body.media_id))
for p in plats:
    session.add(PostPlatform(post_id=pid, platform=p))

# авто-история: тот же ролик туда, где истории есть
story_plats = [p for p in plats if p in STORY_PLATFORMS]
Python · FastAPI · SQLModel
Конструктор рубрик

Новая рубрика за пять минут, без программиста

Владелец придумал конкурс, у ресторана появилось бизнес-меню, клубу понадобилась рубрика про резидентов. Раньше под каждую такую идею приходилось лезть в код. Теперь есть форма в разделе «Кастомизация постов»: заполнил её, и рубрика сама встала в контент-план.

  • ИнструкцияОбычными словами для Gemini: что писать и каким тоном. Рядом кладётся пример удачного поста, он пойдёт в ход, если генерация не удалась.
  • РасписаниеДни недели, время выхода, площадки, приоритет. Своя рубрика борется за слот в своё время и встаёт на место штатной, так что дневной лимит не растёт.
  • КартинкиПул из фото: снимки идут по кругу без повторов. Или пул из промптов, тогда картинка генерируется под конкретный пост, с программой вечера и датой.
  • Текст по картинкеGemini смотрит на фото из пула и пишет к нему подпись. Удобно для меню и интерьеров.
  • ФорматСколько фото, сетка или карусель, нужен ли ролик, ставить ли фирменную подпись с телефоном.
  • ТумблерЛюбую рубрику, свою или штатную, можно снять с ротации одной кнопкой. Что уже стоит в очереди, то выйдет, а в новые дни она больше не попадёт.
32свои рубрики у четырёх заведений
149пулов фото и промптов
413элементов в пулах
0строк кода на новую рубрику
# таблица custom_post_categories — одна рубрика
slug               # адрес рубрики
label              # название в панели
gemini_instruction # что и каким тоном писать
example_text       # запасной текст, если генерация не удалась
weekdays           "0,3,5"    # пн, чт, сб
slot_time          "19:00"
platforms          "vk,tg,ig"
default_media_count
needs_video
priority
append_footer      # ставить ли подпись с телефоном
is_active          # тумблер ротации
Поля модели в том виде, в каком они лежат в базе
Видео

Шесть способов сделать ролик

Для заведения видео обходится дороже всего: съёмка, монтаж, музыка, титры. В разделе «Видео» у завода шесть режимов под разные задачи. Самый простой делает ролик по шаблону за минуту, самый сложный повторяет монтаж чужого вирусного ролика на наших клипах.

РежимЧто на входеЧто делает завод
01 · Шаблонтексты, фото, цифрырендерит бренд-шаблон Remotion: афиша, цитата, список, обратный отсчёт, «угадай чек», история номера. В студии есть живое превью, поправил поле и сразу видишь кадр
02 · Автомонтажсырое видео с голосомрежет лишние паузы, убирает шум, выравнивает голос, распознаёт речь в субтитры, подбирает музыку и приглушает её под голос
03 · По референсучужой ролик + наши клипыИИ-режиссёр: разбирает референс по долям бита, Gemini пишет план монтажа, завод подбирает клипы по тегам, проверяет и рендерит
04 · По паттернусохранённый стиль + новые клипымонтаж, который уже разобрали один раз (крупности, энергия, переходы, длины), применяется к новым материалам
05 · Кружкивидеокруглые видео-сообщения для Telegram-каналов
06 · Veoкадр и описаниеролик 9:16 через Gemini: к фото заведения добавляются движение камеры, свет, люди

Там же есть ручной редактор с таймлайном в духе CapCut, вырез человека из кадра для текста «за спиной», банк клипов с разметкой и банк музыки. Видео-афиши выходных собирает After Effects: агент на моём ПК сам находит макет афиши, рендерит ролик и отдаёт заводу, а тот ставит его на пятницу и субботу в 12:00.

Что выходит с конвейера

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

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

Видео-афиши выходных из After Effects лежат на главной странице.

Экраны клуба

Сообщение гостя через несколько секунд уже на фасаде

Над входом в клуб висит экран 1920×576. Завод крутит на нём афишу вечера, а с августа ещё и «стену»: гость пишет боту, и его текст появляется над входом, пока он стоит на улице с телефоном. Для клуба это инфоповод, люди снимают его сами. Ручной модерации нет, так задумано, поэтому фильтр строгий.

Гость пишет ботукнопка или QR у входа
Проверкиподписка на канал, 1 сообщение в 30 минут
Модерация Geminiсомнение ниже 0,85 → отказ
Кадртекст в бренде клуба, ffmpeg
Агент Resolumeна ПК экрана, опрос каждые 4 с
30 секунд в эфиреровно один показ
Заставкаэкран возвращается к ролику
Заставка «стены». В паузах между сообщениями крутится ролик с примером и QR-кодом.
Афиша на фасаде. Собирается из программы вечера: дата, название, условия входа. Утром после события сама снимается с экрана.
222сообщения гостей с 15 августа
161вышло в эфир
61отклонила модерация
0человек на модерации
Брони

Своя система броней на замену сервису за 4 000 ₽ в месяц

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

Виджет брони клуба: схема зала Welcome со столами, сценой, DJ, танцполом и баром
Клуб. Четыре зала на выбор, схема двигается и масштабируется, свободные столы подсвечены.
Виджет брони ресторана: основной зал с шестнадцатью столами и баром
Ресторан. Тот же виджет, только в бренде ресторана: основной зал и летняя веранда.
  • Редактор заловПеретаскивание, ручки размера и поворота, инструмент стен, палитра декора: бар, сцена, DJ, танцпол, вход, зоны. Столы рисуются со стульями по числу мест.
  • ЖурналШахматка занятости по столам и часам, у клуба ось времени уходит за полночь. Поиск и правка любой брони в один клик.
  • УведомленияНовая бронь сразу падает менеджеру в Telegram. Чтобы подключить чат, достаточно добавить бота в группу.
  • База гостейФорма собирает email и согласие на рекламу, из этого складывается аудитория для ретаргета. Заявки гостиницы с сайта идут сюда же, с датами заезда и рекламными метками.
Учёт концертов

Концерты и аренда зала переехали из Excel в панель

Клуб сдаёт зал под концерты приезжих артистов, и всё это жило в таблице Excel: дата, организатор, контакты, сумма договора, три оплаты, комментарии. Я перенёс таблицу в панель так, чтобы работать с ней можно было по-привычному, а рутину забрал завод.

  • ИмпортСтарый файл заливается как есть: завод понимает строки-разделители месяцев, имя менеджера в строке над мероприятием и статус, записанный словами в комментарии («отмена», «перенос», «пока не оплатили»).
  • ОплатыПишут «Оплатили 50 тысяч», и сумма встаёт в первую свободную оплату. Как только оплаты добрали сумму договора, статус меняется на «оплачено».
  • Яндекс.АфишаРаз в сутки завод обходит страницу площадки и заводит новые концерты: дата по Самаре, артист, ссылка. Сумму не ставит никогда, в деньги завод не лезет.
  • КонтрагентыСправочник организаторов: кто возил артиста в прошлый раз, подставится сам. Контакты фирмы проливаются во все её мероприятия.
  • ВыгрузкаЭкспорт в .xlsx повторяет исходную раскладку один в один, с подписями года и месяцев. Прошедшие мероприятия уходят в архив и не удаляются.
50мероприятий в учёте
26контрагентов
7ждут оплаты
# app/rental_afisha.py
def _norm_artist(s: str) -> str:
    # «АК-47 + ТГК» и «АК-47+ТГК» — один артист
    return re.sub(r"[^0-9a-zа-яё]", "", (s or "").lower())

def _known_counterparty(session, org_id, artist):
    # Возвращает (точное, похожее). Контрагента ставим
    # ТОЛЬКО по точному — похожее уходит подсказкой.
    a = _norm_artist(artist)
    ...
    for e in rows:
        ea = _norm_artist(e.artist)
        if ea == a:
            return e, None
        # похоже — только если совпадение от 5 знаков
        if similar is None and min(len(a), len(ea)) >= 5 \
                and (a in ea or ea in a):
            similar = e
    return None, similar
Python · правило после реальной осечки
Первая версия искала организатора по вхождению строки и склеила артиста «RAM» с «Ramil», так что к концерту приписался чужой договор. Теперь подставляется только точное совпадение. Похожее падает подсказкой в комментарий, и решает человек.
Цифры

Выход по месяцам и заведениям

Сентябрь короче: 15-го закрылся один из ресторанов, 22-го я закончил работу с заведениями.

Публикации по месяцам

май3
июнь302
июль615
август647
сентябрь254

Первые посты ушли в ночь на 23 мая, в июне запустились все заведения. С июля выходило около 20 публикаций в день.

Опубликовано постов и историй

Метелица535
Chili196
гостиница143
ресторан140

Один пост выходит сразу на несколько площадок, поэтому публикаций (1 821) больше, чем постов и историй (1 014).

Код

Что пришлось решать в коде

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

01 · Рубеж перед публикацией

Модель отказалась писать, а пост чуть не ушёл в ленту

Первого сентября я нашёл в очереди гостиницы 5 постов из 30, где вместо текста стояло «Я просто языковая модель, эта задача не для меня», а под этим фирменная подпись. Автопубликация была включена, и первый такой пост вышел бы на следующий день.

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

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

Список маркеров теперь один и для генерации, и для публикации. Потом я прогнал по нему всю очередь, 266 постов четырёх заведений, и брака больше не нашлось.
# app/post_text.py
def find_refusal(text: str) -> Optional[str]:
    body = split_footer(text or "")[0].lower()   # подпись не смотрим
    if not body:
        return None
    for m in REFUSAL_MARKERS:
        if m in body:
            return m
    return None

# app/scheduler.py — _publish_post(), до вызова площадок
_ref = _ptr.find_refusal(post.text)
if _ref:
    post.status = "draft"
    post.reason = f"текст-отказ ({_ref}) — нужен свежий текст"
    session.add(post); session.commit()
    return False, [f"отказ ({_ref}) → draft (публикация отменена)"]
Python · рядом стоят такие же рубежи на длину и дословный дубль
02 · Без дублей

Пост на пять площадок, одна упала

У пяти площадок пять разных API, и какой-нибудь обязательно падает: Instagram отдаёт временную ошибку, ВК включает защиту от флуда. Если повторить пост целиком, в Telegram он выйдет второй раз.

Каждый успешный выход записывается отдельной строкой. На повторе планировщик пропускает площадки, куда пост уже ушёл, и добивает только упавшие. После трёх неудач площадка сдаётся и не держит весь пост в очереди.

Повторять можно сколько угодно, дублей в ленте не будет. Если перепубликовать надо намеренно, для этого есть отдельный флаг.
# app/scheduler.py
already_pubs = session.exec(
    select(PostPublication).where(PostPublication.post_id == post.id)
).all()
if force and already_pubs:          # осознанная перепубликация
    for _p in already_pubs:
        session.delete(_p)
    already_pubs = []
already_platforms = {p.platform for p in already_pubs}
if already_platforms:
    logger.info("post %s already published to: %s — skip those",
                post.id, already_platforms)
...
for plat in platforms:
    if plat in already_platforms:
        continue                      # сюда уже вышло
Python · SQLModel
03 · Антиповтор фото

Четыре тысячи фото, а в ленте одна и та же

Случайный выбор из большого альбома всё равно повторяется: подписчики замечают одну и ту же фотографию через неделю. Отметка «использовано» тоже врёт, потому что фото могли поставить в пост руками из панели.

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

В тематических пулах фото идут по кругу без повторов, и каждый круг перемешивается заново.
# app/media_pool_engine.py — _pick_from_library()
cutoff = datetime.utcnow() - timedelta(days=30)
# железный антиповтор: всё, что реально стояло в постах за 30 дней
posted30 = set(session.exec(
    select(_PM.media_id).join(_P, _P.id == _PM.post_id)
    .where(_P.org_id == org_id, _P.scheduled_at >= cutoff)
).all())
fresh = [m for m in rows
         if m.id not in posted30
         and (not m.last_used_at or m.last_used_at < cutoff)]
pool_rows = fresh or [m for m in rows if m.id not in posted30] or rows
pool_rows.sort(key=lambda m: (m.photo_score or 0), reverse=True)
return random.choice(pool_rows[:40]).id
Python · запасные ступени, чтобы пост не остался без фото
04 · Ротация рубрик

Понедельник не должен повторять понедельник

У гостиницы 15 рубрик и один пост в день. Простая ротация по кругу за две недели ставит одинаковые рубрики на одинаковые дни недели, и лента начинает выглядеть как расписание.

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

Очередь на месяц вперёд строится одним вызовом, без ручной раскладки.
# app/makhall_hotel_plan.py
while cur < end:
    _dn = (cur - _ANCHOR).days
    # недельный сдвиг фазы = антизеркало
    rub = hw.ROTATION[(_dn + _dn // 7) % len(hw.ROTATION)]
    # выключенная рубрика: день пропускаем, фазу НЕ сдвигаем
    if rub in disabled or _cat(rub) in disabled:
        warnings.append(f"рубрика выключена тумблером: {rub}")
        skipped += 1
        cur += timedelta(days=1)
        continue
    ...
Python · ротация: эконом · стандарт · твин · люкс · апартаменты · меню · FAQ …
Аварии

Где набил шишки

Система, которая работает без присмотра, ломается не так, как ожидаешь. Каждую аварию я закрывал исправлением причины, без заплаток, и записывал в базу знаний проекта.

КогдаЧто случилосьЧто изменил
23.07Включил генерацию гостиницы вечером «чтобы канал ожил завтра». Завод сгенерил пост на «сейчас», слот уже прошёл, и за вечер в паблик ушли два несогласованных поста.Развёл генерацию и публикацию на два разных флага. Очередь заряжается заранее отдельным вызовом, а публикатор только выпускает готовое.
11.08Автоподхват концертов приписал артисту «RAM» организатора артиста «Ramil», потому что сравнение шло по вхождению строки.Контрагент ставится только по точному совпадению, похожее уходит человеку подсказкой.
18.08Шаблонный пост вышел в ленту третий раз дословно и длиннее лимита: потолок длины жил только в копирайтере.Длина и дословный дубль за 30 дней теперь проверяются на выходе, перед площадкой, откуда бы текст ни взялся.
01.09На сервере нашлись 16 зависших браузерных драйверов, они занимали 2,1 ГБ памяти. Каждая перезагрузка сессии Gemini запускала новый и не гасила старый.Починил остановку драйвера и поставил сторожа, который раз в 15 минут гасит драйверы без живого браузера.
01.09Истории гостиницы на сентябрь стояли в очереди без площадки. Не вышли бы никуда, и никто бы этого не заметил.Исправил генератор и завёл правило: после любой массовой заливки проверять у поста площадку, медиа и парную историю.

Нужна такая система?

Соберу конвейер под ваш бизнес, от контент-плана и публикаций до броней, отзывов и рекламы. Могу и забрать соцсети целиком, буду вести их на своём заводе.

Telegram · @vitaldemort