← Все проекты

Редакция, которая живёт в системнике

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

  • Python · Temporal · PostgreSQL
  • llama.cpp: Gemma 26B + 4B
  • Stable Diffusion
  • WordPress
  • 2× NVIDIA GPU
  • 0строк Python в ядре
  • 0тестов в pytest-сьюте
  • 13 + 1стадий конвейера (одна спит за флагом)
  • 0Telegram-источников на мониторинге
  • 15редакционных рубрик
  • 0облачных API — всё локально

// машинное отделение

Дата-центр: шкаф в коридоре

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

uname -a && lscpu && nvidia-smi · живой прод
CPU
Intel Core i7-3770 @ 3.40GHz · 4 ядра / 8 потоков · год выпуска 2012 — ветеран четырнадцати лет
RAM
32 ГБ DDR3 · и ни гигабайтом больше — платформа не умеет
GPU #1
NVIDIA RTX 5060 Ti 16 ГБ · 2025 год: видеокарта на 13 лет моложе процессора
GPU #2
NVIDIA GTX 1060 · живёт в виртуалке — почему, см. «Зал славы костылей»
Диски
≈ 14 ТБ: SSD Kingston 480 ГБ + 3× WD Red 3 ТБ + WD Purple 4 ТБ
ОС
Ubuntu 24.04 LTS · ядро 6.14
Корпус
GameTiger T-Rex FR-16 · с дополнительно вкорячёнными кулерами: нейросети греют лучше батареи
Стойка
Шкаф в коридоре · пожарный надзор — на фото справа
Сервер ПедОбраза — корпус GameTiger с красной подсветкой в бельевом шкафу, рядом пылесос и пакет с пакетами
Прод в естественной среде. Слева — пакет с пакетами (обязательный элемент серверной), справа — система пылеудаления.
Корпус GameTiger T-Rex FR-16 — каталожное фото
Как в каталоге. GameTiger T-Rex FR-16.
Огнетушитель ОП-2 на стене рядом с электрощитком — пожарная безопасность домашнего дата-центра
Пожарная сертификация. ОП-2 над щитком — на всякий случай.

// карта системы

Одна новость, шесть машинных цехов

Кликните на любой узел — расскажу, что он делает. Оранжевый пунктир — путь новости; всё внутри рамки крутится на одном домашнем сервере (плюс виртуалка со старенькой GTX 1060, которой нашлась работа).

ДОМАШНИЙ СЕРВЕР · RTX 5060 Ti 16GB + VM GTX 1060 ИНТЕРНЕТ Telegram-каналы~240 источников · Telethon Schedulerсвежий поллинг + бэкфилл PostgreSQLконвейер: Task + PipelineItem TemporalMainIngestion-Workflow · ретраи,heartbeat, очереди news.cpuпарсинг · дедуп · роутинг news.gpu.1060gemma3:4b · SEO · QA news.gpu.5060gemma 26B · SD-обложки news.publishWP REST · брейкер · темп news.notifyTelegram-бот оператора педобраз.рфWordPress-портал Операторкнопки: ок / перегенерить VK + ДзенRSS-импорт из WP
Нажмите на узел схемы

…или начните с Temporal — это сердце системы.

// интерактив

Игра: проведи новость по пайплайну

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

pedobraz-pipeline — интерактивная сменаочки: 0
📨
@источниквходящий

    // конвейер подробно

    Четырнадцать стадий — и все по делу

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

    • parse_and_normalizenews.cpu
      Разбор поста: текст, медиа, ссылки, чистка мусора. Здесь же три сторожа: детектор бессодержательных источников («подпишись на канал», копирайт-заглушки), рекламный стоп-лист и жёсткий отсев военной тематики — не «переписать нейтрально», а отклонить и внести источник в стоп-лист навсегда.

      Если ошибиться: на портал уезжает пост «Контент недоступен, подпишитесь» с сгенерированной обложкой. Проходили. Больше не повторится.

    • exact_dedupnews.cpu
      Точный дедуп: одна новость прилетает из пяти каналов за час. Побеждает первый, остальные помечаются дублями и до нейросетей не доезжают — GPU-время слишком дорого, чтобы переписывать одно и то же.
    • research_contextnews.gpu.1060gemma3:4b
      Лёгкая модель на GTX 1060 собирает контекст: о чём вообще новость, какие факты главные, чего не хватает. Черновику потом легче держаться сути, а не фантазировать.
    • generate_draftnews.gpu.5060gemma 26B
      Тяжёлая модель пишет материал в фирменном голосе «уставшего учителя» — саркастичном, но по делу. Длина адаптивная: из заметки в три строки не раздувается лонгрид. На чувствительных темах сарказм автоматически отключается.

      Если ошибиться: LLM любит отвечать «Как языковая модель, я не могу…» — для таких случаев дальше стоит мета-гард.

    • generate_seo_metadatanews.gpu.1060gemma3:4b
      Заголовок, описание, ключевые слова, слаг — лёгкая модель на виртуалке, пока большая карта занята следующим черновиком. Конвейер, а не очередь к одному станку.
    • generate_covernews.gpu.5060Stable Diffusion
      Редакционная фурри-обложка 1024×1024 по YAML-профилю: единый стиль карточек, персонаж-антропоморф по теме материала. Готовую картинку проверяет SFW-судья — не по промпту, а по пикселям. Самая долгая стадия: именно она задаёт темп публикаций.
    • qa_reviewnews.gpu.1060gemma3:4b
      LLM-судья оценивает материал: фактура, тон, структура, заголовок. Плюс детерминированный мета-гард — ловит «ответы ассистента» и огрызки, которые судья-нейросеть пропускает. Завёрнутый материал уходит на перегенерацию (ограниченное число попыток) или к оператору.
    • generate_interactive💤 за фиче-флагом
      Спящая стадия: генерация мини-игр к материалам. Выключена — маленькая модель выдавала заглушки вместо игр, а публиковать заглушки редакция отказывается. Проснётся, когда появится честный гейт интерактивности.
    • routenews.cpu
      Маршрутизация по 15 рубрикам — от «Дошколят» до «Грантов и карьеры». У каждой рубрики свой архетип подачи; таксономия на портале создаётся автоматически.
    • wait_publication_turnnews.cpu
      Ворота публикации: материалы выходят по очереди, с минимальным интервалом — портал не любит очередей из ботов, а читатели не любят простыни из десяти постов разом.
    • publish_or_enqueuenews.publish
      Публикация через WordPress REST. Хостинг портала капризный, поэтому: DB-брейкер (после серии отказов запросы замирают и ждут), лимитер частоты и никаких ретраев в лоб. Недоступен — материал честно ждёт в очереди.
    • update_derived_pagesnews.publish
      Обновление производных страниц: подборки, дайджесты, рубрикаторы — всё, что зависит от свежепубликованного материала.
    • crosspost_to_vknews.publishвыкл. по умолчанию
      Кросспост в VK. После боёв с их API редакция выбрала дзен-путь: отдельная RSS-лента, которую VK забирает родным импортёром. Стадия в конвейере осталась — за флагом, на всякий случай.
    • notify_operatornews.notify
      Карточка в Telegram оператору: заголовок, обложка, ссылка и кнопки — «ок», «перегенерить черновик/заголовок/обложку», «отложить». Последняя стадия, после которой воркфлоу закрывается зелёненьким.

    // редакционная политика

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

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

    Обычный материал · сарказм разрешён

    «Пока ведомства придумывают новые отчёты, телефонные комбинаторы решили, что пересдача ЕГЭ — это услуга. Платная. Спойлер: нет».

    Ирония, разговорные обороты, позиция. Заголовки-крючки без жёлтизны. Именно за этим голосом читатели и приходят.

    Чувствительная тема · safety override

    «В регионе объявлен траур по погибшим. Занятия в школах отменены до конца недели; работают психологи».

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

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

    // обложки

    Фурри-редакция: у каждой новости — морда

    Все 15 рубрик рисуются в едином «редакционном» стиле: антропоморфные персонажи в человеческих ситуациях — медведь-чиновник в костюме, барсук над учебниками, лис у доски. Локальный Stable Diffusion, 1024×1024, параметры в YAML-профилях (не в коде!), а готовые пиксели проверяет SFW-судья. Вот настоящие обложки с портала:

    • Обложка ПедОбраза: антропоморфный персонаж, редакционный стиль
    • Обложка ПедОбраза
    • Обложка ПедОбраза
    • Обложка ПедОбраза
    • Обложка ПедОбраза
    • Обложка ПедОбраза

    Генерация одной обложки на серийной видеокарте — несколько минут. Пока она рисуется, тяжёлая языковая модель засыпает и отдаёт видеопамять; со стороны это выглядит как «всё зависло», и однажды дежурный инженер чуть не «починил» здоровый конвейер. Теперь про это знает даже мониторинг.

    // контракт размещения

    Каждой стадии — своё железо

    Размещение — не соглашение на словах, а контракт, который проверяется на старте воркера: не та модель в не той очереди — RuntimeError до того, как что-то сломается в проде.

    СтадииОчередьЖелезоМодель
    parse · dedup · route · wait_turnnews.cpuхост, CPU— (детерминированный код)
    research_context · seo_metadata · qa_reviewnews.gpu.1060VM · GTX 1060gemma3:4b (llama.cpp)
    generate_draft · generate_covernews.gpu.5060хост · RTX 5060 Ti 16GBGemma 26B + Stable Diffusion
    publish · update_derived · crosspostnews.publishхостWordPress REST / RSS
    notify_operatornews.notifyхостTelegram Bot API

    // код напоказ

    Четыре фрагмента, которые всё объясняют

    Настоящие идеи из кодовой базы — в упрощённом, безопасном пересказе: без секретов, адресов и внутренних деталей.

    news/pipeline/constants.py — порядок стадий (настоящий)
    ACTIVE_PRODUCTION_STAGE_ORDER: Final[tuple[str, ...]] = (
        STAGE_PARSE_AND_NORMALIZE,   STAGE_EXACT_DEDUP,        STAGE_RESEARCH_CONTEXT,
        STAGE_GENERATE_DRAFT,        STAGE_GENERATE_SEO_METADATA,
        STAGE_GENERATE_COVER,        STAGE_QA_REVIEW,          STAGE_GENERATE_INTERACTIVE,  # ← спит за флагом
        STAGE_ROUTE,                 STAGE_WAIT_PUBLICATION_TURN,
        STAGE_PUBLISH_OR_ENQUEUE,    STAGE_UPDATE_DERIVED_PAGES,
        STAGE_CROSSPOST_TO_VK,       STAGE_NOTIFY_OPERATOR,
    )
    контракт размещения — идея
    STAGE_PLACEMENT = {
        "generate_draft": Placement(
            queue="news.gpu.5060",
            model="gemma-27b",
        ),
        "qa_review": Placement(
            queue="news.gpu.1060",
            model="gemma3:4b",
        ),
    }
    
    def assert_placement(stage, runtime):
        want = STAGE_PLACEMENT[stage]
        if runtime.model != want.model:
            # не та модель в не той очереди —
            # падаем сразу, а не в 3 часа ночи
            raise RuntimeError("wrong model")
    мета-гард — идея
    META_PATTERNS = (
        "как языковая модель",
        "как ии, я не могу",
        "вот переписанный текст",
        "надеюсь, это поможет",
    )
    
    def looks_like_meta_response(draft: str) -> bool:
        # LLM-судья такое пропускает: для него
        # это «вежливый, связный текст».
        # Детерминированный гард — нет.
        low = draft.lower()
        return any(p in low for p in META_PATTERNS)
    воркфлоу — идея
    async def run(self, item_id):
        for stage in PIPELINE_STAGE_ORDER:
            result = await workflow.execute_activity(
                "run_stage", stage, item_id,
                task_queue=queue_for(stage),   # своё железо
                retry_policy=RETRY[stage],
            )
            if result.status in TERMINAL:      # dedup, блок…
                return result                  # честно выходим
            stage = result.next_stage or stage # QA-петля
    брейкер публикации — идея
    def before_publish(state):
        if state.consecutive_failures >= THRESHOLD:
            if now() < state.reopen_at:
                # хостинг лежит — не добиваем его
                # ретраями, а тихо ждём очереди
                raise CircuitOpen(state.reopen_at)
        throttle.acquire()   # ≥60 c между постами

    // эксплуатация

    Прод у батареи: как это не падает

    Домашний сервер — это прод без SLA от дата-центра, поэтому за живучесть отвечает многослойная страховка.

    • 🧭Temporal + systemd

      Шесть воркеров под systemd, каждый фильтрует ровно одну очередь. Упал — рестартовал, воркфлоу продолжился с той же стадии.

    • 🫀Честные heartbeat'ы

      После случая, когда зависший воркер часами притворялся живым, пульс шлёт только реально крутящийся цикл — LoopLivenessToken, а не «процесс существует».

    • 🐶Monit + watchdog

      Внешний надзор за сервисами, GPU-памятью и диском. Знает про сиесту тяжёлой модели во время рисования обложек — и не паникует.

    • Брейкер к хостингу

      Портал живёт на капризном шаред-хостинге: треть REST-запросов может падать. Серия отказов — и публикация замирает до тайм-аута, без единого лишнего запроса.

    • ⏸️Глобальная пауза

      Одна команда оператора останавливает всё: воркеров, бота, тяжёлую модель. Маркер паузы уважают даже сторожевые скрипты.

    • 🧹Ночная уборка

      Таймер в 03:00 подметает докер-кэши и журналы. Однажды диск дошёл до 96% — теперь не дойдёт.

    Temporal UI: сотни воркфлоу конвейера ПедОбраза — выполняющиеся и завершённые
    Живой Temporal UI: параллельный веер бэкфилл-воркфлоу и основные конвейеры. 361 воркфлоу в окне — обычный вечер.

    // вынужденная инженерия

    Зал славы костылей

    Красивая архитектура — это половина системы. Вторая половина — решения, которые пришлось изобрести из-за внешних обстоятельств: чужих банов, несовместимых драйверов и железа 2012 года. Мы ими гордимся не меньше.

    • Внешний факторДрайверы против времени

      RTX 5060 Ti (2025) и GTX 1060 (2016) не живут в одной системе: новой карте нужен свежий драйвер NVIDIA, ветерану Pascal — legacy-ветка. Вместе они не устанавливаются.

      Наш костыльВиртуалка для пенсионера

      GTX 1060 проброшена целиком в виртуальную машину со своим старым драйвером и собственным llama-сервером. Пикантность: воркер gpu1060-vm физически крутится на хосте — в виртуалку он ходит только за нейросетью, по сети.

    • Внешний факторХостинг банит за скорость

      Панель хостинга портала блокирует весь аккаунт за всплеск запросов с одного IP. Банили не раз: и за браузер в wp-admin, и за бодрые ретраи после таймаута.

      Наш костыльРучной метроном на 60 секунд

      Между любыми операциями к хостингу — пауза ≥60 секунд, руками. Скрипт доступа сам себя тормозит, а в репозитории живёт хук, который бьёт по рукам ИИ-агентам за лишние запросы. Анти-DDoS против самих себя.

    • Внешний факторSSH, который не SSH

      На хостинге настроен ForceCommand: разрешён только интерактивный шелл. Обычные ssh host 'команда', scp, sftp и rsync не падают с ошибкой — они просто вечно висят.

      Наш костыльPTY + base64 + маркеры

      Самописный tw-run.sh: принудительный псевдотерминал, команды заезжают heredoc'ом в base64, полезный вывод вырезается между маркерами. А файлы едут по FTP. Обычному. Без TLS. На дворе 2026 год.

    • Внешний фактор16 ГБ VRAM на всех не хватит

      Gemma 26B и Stable Diffusion вместе в память RTX 5060 Ti не помещаются — а обе живут на одной карте, потому что вторая занята лёгкой моделью.

      Наш костыльСиеста по расписанию

      На время рендера обложки тяжёлая модель выгружается и «спит», после — просыпается с прогревом. Со стороны выглядит как зависание, поэтому знание о сиесте зашито в мониторинг, инструкции и головы.

    • Внешний факторГотовые обёртки не пережили наш зоопарк

      Нужен точный контроль видеопамяти, контекста и живучести на двух очень разных картах — от Blackwell до Pascal в виртуалке.

      Наш костыльГолый llama.cpp + честные пробы

      Свои llama-server'ы с фиксированным контекстом на каждую карту. Здоровье меряется не «списком моделей», а реальной пробой генерации: если /completion отвечает — живём, что бы ни говорили обёртки.

    • Внешний факторVPN нужен, но прод от него зависеть не должен

      Однажды «деградация хостинга» оказалась мёртвым узлом VPN: туннель на хосте молча умер, и вся диагностика смотрела не туда.

      Наш костыльТуннели — на роутер

      VPN переехал на OpenWrt-роутер с точечной маршрутизацией; сам сервер ходит в сеть напрямую. У роутера — собственная самолечилка по крону: честный пинг через прокси, мёртвый узел переключается без человека.

    • Внешний факторПроцессор 2012 года

      i7-3770 прекрасен, но ему четырнадцать лет: тяжёлую математику на нём не покрутишь, а расширить платформу некуда — 32 ГБ это потолок.

      Наш костыльCPU-стадии без нейросетей вообще

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

    // шрамы

    Боевые истории (все — правда)

    Система, которая год работает в проде, — это музей побеждённых багов. Экспонаты:

    • 06.2026Телеграм забанил нас на 12 часов

      Жадный резолвинг юзернеймов на каждый поллинг — и FloodWait на 12 часов на ровном месте. Лечение: постоянный клиент вместо «подключился-отключился», кэш резолвинга и прогрев диалогов на старте. ingest

    • 06.2026Два клиента, одна сессия, ноль работы

      Отдельный бэкфилл-воркер делил SQLite-сессию Telethon с основным клиентом. «database is locked», и оба стоят — а выглядело как проблемы сети. Лечение: бэкфилл переехал внутрь процесса шедулера; сессия — строго одна на процесс. ingest

    • 06.2026Воркер-зомби с идеальным пульсом

      После обрыва связи с Temporal процессы зависали навсегда, но мониторингу слали бодрый heartbeat — «жив-здоров». Конвейер молча стоял. Лечение: пульс отправляет только реально работающий цикл; рядом — скрипт аккуратного рестарта. оркестрация

    • 07.2026Бот четверо суток грел комнату

      Форвард-бот попал в вечный цикл усечения подписи к посту: длина не уменьшалась, цикл не кончался, ядро CPU — 99% на четыре дня. Диагностика без отладчика, по счётчикам переключений контекста. Лечение: guard прогресса в цикле. И да, комната реально была тёплая. bot

    • 04–07.2026Хостинг банит за энтузиазм

      Панель хостинга блокирует весь аккаунт за всплеск запросов с одного IP. Инструмент, бодро ретраящий таймауты, превращает лёгкий троттлинг в многочасовой бан. Лечение: железное правило «таймаут — это сигнал СТОП», паузы ≥60 секунд между операциями и запрет браузерной автоматизации в сторону прода. Теперь это соблюдает даже ИИ-ассистент, который писал эту страницу. публикация

    • 06.2026«Всё зависло» (нет)

      Во время рисования обложки тяжёлая языковая модель отдаёт видеопамять и засыпает. Со стороны — идеальная картина зависания: процесс есть, ответа нет. Однажды её чуть не «вылечили» рестартом посреди генерации. Лечение: знание о сиесте зашито в мониторинг и инструкции; GPU-стадии дособирают друг за другом, а не дерутся за память. gpu

    // вокруг конвейера

    Один репозиторий — целая экосистема

    • Тема «Ember Newsroom»

      Самописная WordPress-тема портала: чистый PHP, ноль билдеров, View Transitions, мегаменю, тёмная тема. Версия 1.8.0 — и эта страница тоже она.

    • RSS-мосты

      Плагины лент для Дзена и VK: соцсети забирают материалы родными импортёрами — без хрупких API-интеграций.

    • Подкаст-фид

      iTunes-совместимая RSS-лента подкаста — Яндекс.Музыка и приложения подхватывают сами.

    • Страницы проектов →

      Эта витрина и страницы четырёх инструментов — тоже часть репозитория.

    • /bormotoon/ →

      Персональная страница автора с тумблером «приличный ↔ монгольский».

    • Песочница local-wp

      Локальная копия портала в докере — всё проверяется там, прод от браузерных экспериментов огорожен железным правилом.

    Лучший способ понять ПедОбраз — почитать его

    Каждый материал на портале прошёл все четырнадцать стадий, мимо всех сторожей, через две видеокарты — и получил морду. Серьёзно, посмотрите на этих барсуков.

    Поддержать ПедОбраз Целая редакция на домашнем сервере — и ей нужен корм для видеокарт. donationalerts.com/r/bormotoon