Привет. Я Марат. Я сетевой инженер. Хуже того — я руководитель сетевых инженеров.

Код не пишу, на железки хожу только из любопытства.

Нейросетки использую повседневно, но под простые задачи: составить TL;DR, написать текст, вытащить данные из таблички, нарисовать картинку. Несколько раз для рабочих и домашних проектов пару функций с их помощью написал. Ну и кто sunno не баловался?

Но вот такой большой, настоящей, интересной задачки никак в поле зрения не было. А всех слушаешь — делают что-то полезное и крутое. На кухнях только и разговоров, что про вайб-кодинг. На хабре только и статьи, что про ML.

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

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

Важное условие — вайб-кодить. Не написав ни строчки кода, получить рабочую программу.

Вот прям так и писал
Вот прям так и писал

TL;DR: всё получилось, но не быстро, не так, и не то чтобы прям совсем не пришлось трогать код.

Навигатор по статье:


Окружение и начальные условия

  • VScode

  • Code Assistant

  • DeepSeek R1 Thinking

  • Она — так я называю модельку в статье. Хотя на лицо диссациативное расстройство идентичности: в ассистенте я общаюсь с ним.

  • Python

  • Нет ничего на старте

  • Я ни разу не писал код в IDE с нейро-помощниками


Первый подход

Сначала хотелка была простая. У нас есть еженедельный подкаст До Нас Дошло с очень простым сценарием публикации:

  1. Получить название

  2. Получить описание

  3. Получить mp3-файл

  4. Нарисовать обложку

  5. Загрузить на s3 файлы mp3 и обложки

  6. Опубликовать пост на сайте

С нуля часа за три удалось реализовать этот сценарий. Оно реально заработало.

Я реально не написал ни буквы кода. Просто в режиме живого не очень директивного общения в чате с моделькой.

Изначальный запрос был примерно такого вида:

Давай сделаем телеграм-бота для загрузки подкаста.

Используется библиотека telebot, токен бота в файле .env.

Сначала ты должен запросить у пользователя название подкаста по шаблону "До нас дошло SXXEYY. Название эпизода".
Потом попроси описание в формате html.
Потом попроси mp3-файл.
Далее возьми локальный файл dnd.png, в него чуть выше середины напиши "до нас дошло", чуть ниже середины короткое название эпизода, а в самом низу маленьким шрифтом SXXEYY. Используй шрифт из папки fonts.
Файл обложки нужно сделать в двух форматах: 900х900 и 1500х1500. Сохрани первый как dnd_SXXEYY_cover_900, а второй dnd_SXXEYY_cover_1500.
Загрузи обложки и mp3-файл на s3 через библиотеку boto3 в {S3_ENDPOINT}/{S3_BUCKET}/dnd. Переменные и секрет s3 возьми из файла .env.

Далее создай пост на сайт с таким параметром data

{
 "title": Название подкаста,
 "content": Описание подкаста,
 "excerpt": Описание подкаста, очищенное от html,
 "status": "draft",
 "podcast-category": [PODCAST_CATEGORIES[podcast.feed]],
 "podcast_image_url": сгенерированное изображение 900,
 "podcast_audio_url": ссылка на mp3-файл на s3,
 }

Креды для сайта возьми из .env.
Достань ссылку на опубликованный пост, она лежит в поле permalink.
После завершения пришли в чат ссылки на s3 и пост.

В целом, это всё.

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

И…

Оно заработало. Просто сразу, с первого же запуска заработало. Несколько сотен строк кода запустились и отработали без трейсбеков.
Что творится-то?? Мой код после правок с первого раза работает в 10% случаев.

Дальше был тонкий тюнинг: валидация ввода, правка текста на обложке, подготовка текста.

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

Через три часа всё было готово.

Любопытно, кстати, как нейронка реализовала конечный автомат в телеграме.

Я бы до этого не додумался..

Она описала возможные состояния

DND_TITLE = 'dnd_title'
DND_DESCRIPTION = 'dnd_description'
DND_AUDIO = 'dnd_audio'

Потом она придумала обработчик сообщений

# Основной обработчик сообщений
@bot.message_handler(func=lambda message: True)
def handle_message(message):
    chat_id = message.chat.id
    if chat_id not in user_states:
        return # Пользователь не в сценарии

    state = user_states[chat_id]

    text = message.text
    if state == DND_TITLE:
        handle_dnd_title(bot, chat_id, text)
    elif state == DND_DESCRIPTION:
        handle_dnd_description(bot, chat_id, text)

И аудио:

# Обработчик аудио
@bot.message_handler(content_types=['audio'])
def handle_audio(message):
    chat_id = message.chat.id
    state = user_states[chat_id]

    if state == DND_AUDIO:
        audio = message.audio
        handle_dnd_audio(bot, chat_id, audio 

Далее начала реализовывать каждое из них и переход между ними.
Сначала start

@bot.message_handler(func=lambda message: message.text == 'Загрузить эпизод ДНД')
def start_dnd(message):
    chat_id = message.chat.id
    logger.info(f"Начало сценария ДНД для chat_id: {chat_id}")

    # Устанавливаем начальное состояние
    user_states[chat_id] = DND_TITLE
    user_data[chat_id] = {}

    bot.send_message(
        chat_id,
        "Введите название эпизода в формате: 'До нас дошло SXXEYY. Название эпизода'",
        reply_markup=create_stop_keyboard()
    )

Потом переходим к DND_TITLE

def handle_dnd_title(bot, chat_id, title):
    logger.info(f"Обработка названия для chat_id: {chat_id}, текст: '{title}'")
    ...

    # Переходим к описанию
    user_states[chat_id] = DND_DESCRIPTION

Потом к DND_DESCRIPTION

def handle_dnd_description(bot, chat_id, description):
    logger.info(f"Обработка описания для chat_id: {chat_id}")
    ...

    # Переходим к аудио
    user_states[chat_id] = DND_AUDIO

И наконец к DND_AUDIO. В этой функции автомат завершает свою работу и возвращается в исходное состояние.

def handle_dnd_audio(bot, chat_id, audio):
    logger.info(f"Обработка аудио для chat_id: {chat_id}")
    ...

    # Возвращаем основное меню
    bot.send_message(chat_id, "Выберите действие:", reply_markup=create_main_keyboard()) 

Изящно.

Ну программисты бы так и сделали, наверно. Но я-то не программист)

Работает! 3 часа! Чего же боле?

Но всё это время меня глодало то, что мне на самом-то деле нужно больше. У меня есть другие подкасты. Их я тоже хочу публиковать, но там другие сценарии. Есть также анонсы предстоящих подкастов.

И всё прежнее было .. баловством.


Добавляем сценарии

Тут-то я и попался. При попытке добавить новый сценарий всё пошло по вайб-кодингу.

Я решил сделать возможность загружать не только еженедельные выпуски одного подкаста, но и другие рубрики.

Во-первых, нужно было как-то с самого начала разводить разные сценарии.

Делаю это через меню.

Кнопки есть, а код я не писал, ручки — вот они
Кнопки есть, а код я не писал, ручки — вот они

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

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

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

В конце концов моделька теряет контекст и начинает совсем уж дичь вытворять.


Новый подход. Моделька-архитектор

Теперь я понял: сначала архитектура приложения, потом код.
Поэтому я составил супер-ТЗ.

Подробнейшее
Давай сделаем телеграм-бота для загрузки подкастов и публикации анонсов.

Используется библиотека telebot.

В боте будет три кнопки в меню, реализующие три сценария:
- Загрузить эпизод ДНД
- Загрузить эпизод обычного подкаста
- Сделать анонс

Структура проекта:
main.py — главный файл
Папка fonts - шрифты
Папка templates - шаблоны обложек
Папка states - файлы для машин состояний разных сценариев:
- states_dnd.py - Загрузить эпизод ДНД
- states_podcasts - Загрузить эпизод обычного подкаста
- state_announce - Сделать анонс
Папка utils: утилиты, в ней:
- s3_client.py - клиент работы с s3
- site_api_client.py - клиент работы с api сайта
gen_covers.py - Модуль генерации обложек
Папка tmp - для временных файлов
constatns.py - файл с констнантами
.env - файл с переменным окружения

По команде /start выводится приветствие и ID чата. Если его ещё нет в списке ALLOWED_CHATS, нужно предложить отправить chat ID @eucariot для доступа.

По команде /start становится доступно меню с кнопками, если чат в списке разрешённых.

После нажатия на любую из кнопок начинается соответствующий диалог, а список кнопок меняется одной "stop". При нажатии на эту кнопку в чат отпроавляется команда /stop, останавливаются текущие задачи и сновая появляется список кнопок.

После завршения работы сценария на экране снова появляются три кнопки

===Общие:

====Соответствие между названием рубрики и её category:

Хранятся в constatns.py 
PODCAST_FEEDS = {
    "LTE": "lte",
    "telecom": "telecom",
    "sysadmins": "sysadmins",
    "По'уехавшие": "pouekhavshie",
    "pouekhavshie": "pouekhavshie",
    "Шоты": "shorts",
    "IRL": "irl",
    "Поjncieлки": "ccielki",
    "Поrhcaлки": "ccielki",
    "Поallелки": "ccielki",
    "Поccieлки": "ccielki",
    "По'училки": "ccielki",
    "linkmeup": "other",
    "До нас дошло": "donasdoshlo"
}

====Соответствие между category и номером на сайте:

PODCAST_CATEGORIES = {
    "irl": 9,
    "lte": 6,
    "sysadmins": 4,
    "telecom": 3,
    "по'уехавшие": 5,
    "pouekhavshie": 5,
    "поccieлки": 8,
    "шоты": 7,
    "shorts": 7,
    "other": 7,
    "donasdoshlo": 188,
}

====Для публикации поста на сайте используется библиотека http.client в следующем формате:

    conn = http.client.HTTPSConnection("linkmeup.ru")
    headers = {
        "Authorization": f"Basic {SITE_SECRET}",
        "Content-Type": "application/json",
    }

    conn.request("POST", url, payload, headers)
    res = conn.getresponse()

====s3
Используется библиотека boto3



===Как работает кнопка "Загрузить эпизод ДНД"
Переиспользуются site_api_client


1. Запрашивается полное название эпизода в формате "До нас дошло SXXEYY. Название эпизода", где "До нас дошло" не меняется, XX - номер сезона, YY - номер эпизода, "название эпизода" - переменная часть.
При получении названия SXXEYY и Название эпизода нужно сохранить в отдельных переменных episode_number и episone_name.
Есть валидация названия на совпадение с шаблоном

2. Запрашивается описание в формате html.
После получения в конце этого описания добавляется следующий статический блок: 

------------------------------------------------------------
<blockquote>
    <h5>Оставайтесь на связи</h5>
    Кто мы такие: <a href="https://linkmeup.ru/about/">https://linkmeup.ru/about/</a><br/>
    Пишите нам: <a href="mailto:info@linkmeup.ru" rel="nofollow">info@linkmeup.ru</a><br/>
    Канал в телеграме: <a href="https://t.me/donasdoshlo">https://t.me/donasdoshlo</a>. Приходите обсуждать и предлагать.<br/>
    Плейлист подкаста на <a href="https://www.youtube.com/playlist?list=PLHN9m7XN8U8Enp72zrhlYd3o0pcifFpox">Youtube</a><br/>

    <br />

    <b>Поддержите проект:</b><br />

    <a href="https://www.patreon.com/linkmeup?ty=h" target="_blank" rel="noopener"><img title="Поддержать нас на Patreon" src="https://s3.linkmeup.ru/linkmeup/images/patreon.jpg" width="300" align="middle" /></a>

    <a href="https://sponsr.ru/linkmeup/" target="_blank" rel="noopener"><img title="Поддержать нас на Sponsr" src="https://s3.linkmeup.ru/linkmeup/images/sponsr.png" width="300" align="middle" /></a>

    <a href="https://boosty.to/linkmeup" target="_blank" rel="noopener"><img title="Поддержать нас на boosty" src="https://s3.linkmeup.ru/linkmeup/images/boosty.png" width="300" align="middle" /></a>
</blockquote>
------------------------------------------------------------



3. Запрашивается mp3-файл с эпизодом.
Проверяется, что расширение файла mp3.

4. Отправляется сообщение "Все данные получены. Задача обрабатывается"

5. Файл mp3 загружается на s3 с исходным именем

6. Подготовка обложки
Используется шаблон обложки из файла templates/donasdoshlo.png.

Используется шрифт fonts/11662.ttf, цвет чёрный
Чуть выше середины статический текст крупным шрифтом "До нас дошло"

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

Внизу на 1/7 от нижнего края обложки посередине episode_number ещё меньшим размером.

Изображение машстабируется до 1500х1500 и сохраняется локально под именем donasdoshlo_<episode_number>_cover_1500.png
Изображение машстабируется до 900х900 и сохраняется локально под именем donasdoshlo_<episode_number>_cover_900.png


7. Файлы обложки загружаются на s3 по пути podcasts/donasdoshlo/<название файла>

8. Отправляется сообщение со ссылками на mp3, обложку 900 и обложку 1500

9. Публикуем эпизод подкаста на сайте:
url = "/wp-json/wp/v2/podcasts/"

http  payload
        {
            "title": полное название эпизода, как вводил пользователь
            "content": описание эпизода
            "excerpt": описание эпизода, очищенное от тегов
            
            "status": "draft",
            "podcast-category": 188,
            "podcast_image_url": ссылка на файл donasdoshlo_<episode_number>_cover_900.png,
            "podcast_audio_url": ссылка на файл mp3,
        }
    )

10. В ответе сайта, если пост успешно создан, в поле permalink_template находится ссылка на пост. Её нужно прислать отдельным сообщением.

11. Сновая появляется список изначальных кнопок в меню.



---

===Как работает кнопка "Загрузить эпизод подкаста"

Переиспользуются site_api_client

Начинается диалог публикации анонса.

1. Нужно, используя кнопки внутри чата (не меню), выбрать рубрику из следующих:
- telecom
- sysadmins
- LTE
- IRL
- Шоты
- По'уехавшие

Сохранить в episode_feed, взять из PODCAST_FEEDS соответствующий category и внести его в episode_category

2. Спросить номер эпизода
Записать его в episode_number

3. Спросить название эпизода
Записать его в episode_name

4. Спросить описание
Записать его в episode_description

5. Спросить изображение для обложки
Записать его в episode_img
Сохранить локально

6. Спросить mp3-файл эпизода
Записать его в episode_mp3
Сохранить локально

Диалог на этом заканчивается. 

7. Рисуем обложки
	- Шрифт fonts/Lato-Bold.ttf. Цвет чёрный
	- episode_img увеличивается до 1500х1500, записывается в episode_rss_cover и сохраняется локально под именем <episode_category>_<episode_number>_rss_cover.png
	- За шаблонную обложку берётся template/<episode_category>.jpg
	- Изображение episode_img масштабируется и вписывается в 480х550, углы скругляются, и вставляется в шаблонную обложку с левой стороны. Текст "<episode_feed> №<episode_number>. <episode_name> вставляется в правую часть: 1/3 по вертикали и 1/2 по горизонтали. До края изображения нужен отступ 50 пикселей. Если нужно, текст разделяется на разные строки по пробелам
	- Полученное изображение записывается в episode_cover и сохраняется локально как <episode_category>_<episode_number>_cover.png

8. Файлы загружаются на s3 
	- mp3 по пути podcasts/<episode_category>/<episode_category>_<episode_number>.mp3
	- episode_img по пути podcasts/<episode_category>/<episode_category>_<episode_number>.png
	- episode_rss_cover по пути podcasts/<episode_category>/<episode_category>_<episode_number>_rss_cover.png
	- episode_cover по пути podcasts/<episode_category>/<episode_category>_<episode_number>_cover.png

9. Отправляется сообщение со ссылками на s3 для mp3 и всех обложек

10. Далее готовится пост на сайт.
В episode description вначале добавляется <img src="ссылка на podcasts/<episode_category>/<episode_category>_episode_number_cover.png">

А в конце статический блок:

------------------------------------------------------------
<blockquote>
    <h5>Оставайтесь на связи</h5>
    Пишите нам: <a href="mailto:info@linkmeup.ru" rel="nofollow">info@linkmeup.ru</a><br />
    Канал в телеграме: <a href="https://t.me/linkmeup_podcast">t.me/linkmeup_podcast</a><br />
    Канал на youtube: <a href="https://youtube.com/c/linkmeup-podcast">youtube.com/c/linkmeup-podcast</a><br />
    Подкаст доступен в <a href="https://itunes.apple.com/ru/podcast/linkmeup.-pervyj-podkast-dla/id1065445951?mt=2">iTunes</a>, <a href="https://podcasts.google.com/feed/aHR0cHM6Ly9saW5rbWV1cC5ydS9yc3MvcG9kY2FzdHM">Google Подкастах</a>, <a href="https://music.yandex.ru/album/7060168">Яндекс Музыке</a>, <a href="https://castbox.fm/channel/linkmeup.-Подкаст-про-IT-и-про-людей-id1173801?country=ru">Castbox</a><br />
    Сообщество в вк: <a href="https://vk.com/linkmeup">vk.com/linkmeup</a><br />
    Группа в фб: <a href="https://www.facebook.com/linkmeup.sdsm/">www.facebook.com/linkmeup.sdsm</a><br />
    Добавить <a href="https://linkmeup.ru/rss/podcasts">RSS</a> в подкаст-плеер.<br />
    Пообщаться в общем чате в тг: <a href="https://t.me/linkmeup_chat">https://t.me/linkmeup_chat</a><br />
    <br />

    <b>Поддержите проект:</b><br />

    <a href="https://www.patreon.com/linkmeup?ty=h" target="_blank" rel="noopener"><img title="Поддержать нас на Patreon" src="https://s3.linkmeup.ru/linkmeup/images/patreon.jpg" width="300" align="middle" /></a>

    <a href="https://sponsr.ru/linkmeup/" target="_blank" rel="noopener"><img title="Поддержать нас на Sponsr" src="https://s3.linkmeup.ru/linkmeup/images/sponsr.png" width="300" align="middle" /></a>

    <a href="https://boosty.to/linkmeup" target="_blank" rel="noopener"><img title="Поддержать нас на boosty" src="https://s3.linkmeup.ru/linkmeup/images/boosty.png" width="300" align="middle" /></a>
</blockquote>
------------------------------------------------------------


url = "/wp-json/wp/v2/podcasts/"

http  payload
        {
            "title": "<episode_feed> №<episode_number>. <episode_name>"
            "content": episode_description
            "excerpt": episode_description, очищенное от тегов
            
            "status": "draft",
            "podcast-category": число из PODCAST_CATEGORIES на основе episode_category,
            "podcast_image_url": ссылка на файл episode_img,
            "podcast_audio_url": ссылка на episode_mp3,
        }
    )



11. В ответе сайта, если пост успешно создан, в поле permalink_template находится ссылка на пост. Её нужно прислать отдельным сообщением.

12. Сновая появляется список изначальных кнопок в меню.


---

===Как работает кнопка "Сделать анонс"
Переиспользуются site_api_client

Начинается диалог публикации анонса.

1. Нужно, используя кнопки внутри чата (не меню), выбрать рубрику из следующих:
- telecom
- sysadmins
- LTE
- IRL

Сохранить в episode_feed, взять из PODCAST_FEEDS соответствующий category и внести его в episode_category

2. Спросить номер эпизода
Записать его в episode_number

3. Спросить название эпизода
Записать его в episode_name

4. Спросить описание
Записать его в episode_description

6. Спросить дату прямого эфира
Записать его в episode_date

5. Спросить изображение для обложки
Записать его в episode_img
Сохранить локально


Диалог на этом заканчивается. 

7. Рисуем обложки
	- Шрифт fonts/Lato-Bold.ttf. Цвет чёрный
	- episode_img увеличивается до 1500х1500, записывается в episode_rss_cover и сохраняется локально как png
	- За шаблонную обложку берётся template/<episode_category>.jpg
	- Изображение episode_img масштабируется и вписывается в 480х550, углы скругляются, и вставляется в шаблонную обложку с левой стороны. Текст "<episode_feed> №<episode_number>. <episode_name> вставляется в правую часть: 1/3 по вертикали и 1/2 по горизонтали. До края изображения нужен отступ 50 пикселей. Если нужно, текст разделяется на разные строки по пробелам
	- Полученное изображение записывается в episode_cover и сохраняется локально как png

8. Файлы загружаются на s3 
	- episode_img по пути podcasts/<episode_category>/<episode_category>_episode_number.png
	- episode_rss_cover по пути podcasts/<episode_category>/<episode_category>_episode_number_rss_cover.png
	- episode_cover по пути podcasts/<episode_category>/<episode_category>_episode_number_cover.png

9. Отправляется сообщение со ссылками на s3 для всех обложек

10. Далее готовится пост на сайт.
В episode description вначале добавляется <img src="ссылка на podcasts/<episode_category>/<episode_category>_episode_number_cover.png">

А в конце статический блок:

------------------------------------------------------------
<blockquote>
    <h5>Оставайтесь на связи</h5>
    Пишите нам: <a href="mailto:info@linkmeup.ru" rel="nofollow">info@linkmeup.ru</a><br />
    Канал в телеграме: <a href="https://t.me/linkmeup_podcast">t.me/linkmeup_podcast</a><br />
    Канал на youtube: <a href="https://youtube.com/c/linkmeup-podcast">youtube.com/c/linkmeup-podcast</a><br />
    Подкаст доступен в <a href="https://itunes.apple.com/ru/podcast/linkmeup.-pervyj-podkast-dla/id1065445951?mt=2">iTunes</a>, <a href="https://podcasts.google.com/feed/aHR0cHM6Ly9saW5rbWV1cC5ydS9yc3MvcG9kY2FzdHM">Google Подкастах</a>, <a href="https://music.yandex.ru/album/7060168">Яндекс Музыке</a>, <a href="https://castbox.fm/channel/linkmeup.-Подкаст-про-IT-и-про-людей-id1173801?country=ru">Castbox</a><br />
    Сообщество в вк: <a href="https://vk.com/linkmeup">vk.com/linkmeup</a><br />
    Группа в фб: <a href="https://www.facebook.com/linkmeup.sdsm/">www.facebook.com/linkmeup.sdsm</a><br />
    Добавить <a href="https://linkmeup.ru/rss/podcasts">RSS</a> в подкаст-плеер.<br />
    Пообщаться в общем чате в тг: <a href="https://t.me/linkmeup_chat">https://t.me/linkmeup_chat</a><br />
    <br />

    <b>Поддержите проект:</b><br />

    <a href="https://www.patreon.com/linkmeup?ty=h" target="_blank" rel="noopener"><img title="Поддержать нас на Patreon" src="https://s3.linkmeup.ru/linkmeup/images/patreon.jpg" width="300" align="middle" /></a>

    <a href="https://sponsr.ru/linkmeup/" target="_blank" rel="noopener"><img title="Поддержать нас на Sponsr" src="https://s3.linkmeup.ru/linkmeup/images/sponsr.png" width="300" align="middle" /></a>

    <a href="https://boosty.to/linkmeup" target="_blank" rel="noopener"><img title="Поддержать нас на boosty" src="https://s3.linkmeup.ru/linkmeup/images/boosty.png" width="300" align="middle" /></a>
</blockquote>
------------------------------------------------------------


url = "/wp-json/wp/v2/podcasts/"


API URL: url = "/wp-json/wp/v2/blog/"


http payload = 
        {
            "title": "<episode_feed> №<episode_number>. <episode_name>",
            "content": episode_description
            "excerpt": episode_description, очищенное от тегов

            "status": "draft",
            "article-category": [13],
            "article_image_url": ссылка на файл episode_img
            "event_date": episode_date в формате %d.%m.%Y %H:%M
        }
    )


11. В ответе сайта, если пост успешно создан, в поле permalink_template находится ссылка на пост. Её нужно прислать отдельным сообщением.

12. Сновая появляется список изначальных кнопок в меню.




Теперь давай добавим сюда кнопку "Сделать анонс"

После такого ТЗ, конечно, вопросики появляются: кто архитектуру в итоге продумывал: я или нейронка? Ну да ладно.
По нему стажёрчик сел бы и сделал всё красиво за полдня-день. Без лишних вопросов. Ну я так думаю.

Садимся, архитектурим. Какие будут папочки, какие будут модули, какие будут кнопочки.

Рисует мне ридми. Красиво.

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

Чтобы оставаться на месте..
Чтобы оставаться на месте..

На этот раз быстрее — с полчасика.

К моменту встраивания второго сценария моделька уже поплыла. Она снова начала всё крушить.

Ты ей говоришь переиспользовать модуль site_api_client для всех сценариев, а она пишет новый.
Ты ей говоришь не трогать файл states_dnd, когда делаешь новые сценарии, а она всё в него напихала.
Ты ей говоришь, генерацию обложек вынеси в utils/gen_cover.py, а она размазывает их по всем файлам и функциям.

Промаяшись ещё пару часов и не получив ровно никакого нового результата, я решаю просто начать всё сначала, совсем.


Попытка номер три

Закончилась тем же, что и попытка номер два.


Попытка номер четыре

В ней я достиг дзен. Ну или просто нейронные звёзды сошлись на небосклоне.
В этот раз я был максимально последователен.

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

На шаге 2 добавляем один сценарий. Зафиксировали. Далее второй и далее третий. Зафиксировали

Только на шаге 3 начинаем оптимизации.

В общем ещё через несколько часов с горем пополам заработали все сценарии в базовой комплектации.

Ещё часок — их тонкий тюнинг.


И вот что получилось

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

Последнее я проверил при попытке добавить ещё один сценарий.
А также при добавлении отписывания статуса мне в личку.


Последующие доработки

Мне не нравятся 4 кнопки по вертикали, сделай в два столбца

Мне только обложку нарисовать

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

Дословный промпт:

Давай встроим ещё один сценарий. Добавь кнопку в меню "Мне только обложку нарисовать".
После нажатия на неё появляются inline кнопки
- ДНД
- telecom
- sysadmins
- LTE
- IRL
- Шоты
- По'уехавшие

Сохранить в episode_feed, взять из PODCAST_FEEDS соответствующий category и внести его в episode_category

Далее запрашиваем полное название эпизода
Если это днд, сразу генерируем обложку, как это было в функции generate_dnd_cover
Если это другие подкаста, запроси ещё картинку выпуска и сгенерируй обложку, как это было в handle_podcast_cover.
С теми же именами и размерами.

Точно так же их потом нужно залить на s3 и в конце прислать ссылки.

Всё сделала с тонкими правками: как правильно распарсить название, какое дать имя файлу. Заработало с первой итерации.


Админский чат

Создай переменную в .env ADMIN_CHAT=178711963
Добавь отправку туда сообщения о том, что кто-то инициировал какой-то сценарий.
Сделай пересылку в него отчётное сообщение со ссылками.
Если возникают ошибки, тоже пересылай их в этот чат.

Тоже практически без нареканий сразу заработало.


Вайб-дебаггинг

А вот это, вообще говоря, была интереснейшая часть.

Если честно посчитать, то из примерно 25 часов "разработки", около четверти — это отладка самых разных штук, про которые нейросетка знает, но понять, что у меня такая проблема есть, не может.

А ещё четверть, это когда она сама себя ломала, не передавала новые аргументы в вызов функции, забывала импортировать модули.

Кто-то написал, а ты теперь траблшуть это!

Вот кто это сделал, тот пусть и убирает!

На самом деле (я могу с вами быть откровенным?) в большинстве случае я тупо в чат сливал трейсбеки и предлагал модельке разобраться. И она разбиралась. Не всегда оптимально, иногда, чтобы починить, она расхреначивала весь проект. Но чинила. Вот такой вайб-дебаггинг.

Проблема забавная

Вставляет китайские иероглифы. Обычно замечает и в несколько итераций их удаляет. Но не всегда.

Проблема с особенностями работы telebot

Вот это, конечно, нейронка могла бы и знать. Но не знала. Пришлось ей подсказывать.

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

Проблема с API Wordpress

Я сильно не разбирался с его APIшкой. Взял как есть — а там Basic Auth — либо login/password, либо секрет, на их основе сгенерённый.

В скрипте у меня это уже сто лет работает. А в ботике ну никак, всё время 401.

Промаялся я долго.

Пришлось отложить ИИ и испачкать руки. Postman, ipython, requests. И… Тоже не работает.

Thinking…

Семён Семёныч! Я вспомнил, что года четыре назад я уже так же потратил на этот квест пару часов и понял тогда, что авторизация работает только через http.client. Именно так я  в скрипте и написал.

Дальше было легко: простой prompt Поменяй requests при публикации поста на сайт на http.client всё исправил.

Проблема: мы строили-строили и наконец построили бесполезную фигню

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

A request to the Telegram API was unsuccessful. Error code: 400. Description: Bad Request: file is too big

Стандартный Bot API телеграма не принимает файлы размером больше 20МБ.
Причём как на upload, так и на download. А средний размер подкаста ДНД несколько десятков МБ, а других — пара сотен.

И как будто бы всё напрасно. Так обидно. Так грустно.

Пошёл искать, узнал про https://github.com/tdlib/telegram-bot-api — это локальный API-сервер, запуск которого выглядит практически, как next->next->next. И он значительно задирает лимиты.

Ну я понекстал, запустил, в telebot поменял URL API — и всё заработало. Звучит, конечно, быстро, но несколько часов заняло.

И нейронки об этом точно знают (я же оттуда и узнал про локальный API-сервер). Но почему-то через ассистента мы с этим не разобрались.

Проблема с telegram-bot-api

Отлаживал я код локально — на МАКе. При этом глубокого понимания, как работает telegram-bot-api у меня не было. Оно просто завелось сразу — и клёво.

И в какой-то момент мой бот мне на попытку обратиться к изображению начал возвращать 400. Раньше всё работало!

A request to the Telegram API was unsuccessful. Error code: 400. Description: Bad Request: wrong file_id or the file is temporarily unavailable

При этом его file_id был известен, и с аудио можно было по-прежнему работать нормально.

Логика работы telegram-bot-api следующая: локальный API-сервер сохраняет файлы локально в указанную директорию с размещением в директориях photo/music/итд и именно к ним обращается telebot.
Так вот: аудио сохраняются, а фото нет, поэтому и обратиться к ним нельзя.

Разобраться, что происходит у меня настойчивости не хватило (читай: не смог починить рестартом и ребутом). Попробовал на linux-тачке, где будет крутиться сервис — там всё работает.

Вот и славно! Но ещё часок на это потрачен.


Выводы


А нет, ещё пока не выводы


Бонус-трэк: ботик с нуля почти в прямом эфире

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

Отдадим нейронке вот такой промпт и будем активно следить за происходящим.

Давай напишем telegram bot на python, в котором будет две кнопки в меню: "Узнать когда ближайшее затмение", "Узнать когда ближайшее затмение, видимое из страны"
После нажатия первой кнопки сразу покажи результат: когда и откуда видно
После нажатия на вторую предложи выбор: Россия, Испания

Составь статический список затмений до 2050 года вида:
- полное или нет
- когда
- из каких стран видно
Не нужно ходить в API, не нужно рассчитывать области видимости. Только статический список

Используй telebot. Токен бота возьми из .env

И вот как это выглядит.

Я немного обрезал и ускорил в два раза, чтобы вас не бесить. Было 13 минут.

Можете посмотреть код: https://sourcecraft.dev/linkmeupru/eclipse-bot?rev=main

Можете попробовать: https://t.me/eucaribot


И вот теперь выводы

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

Очень круты три вещи:

  1. На мой взгляд ассистент вместе с моделькой справились с проработкой архитектуры решения и его документированием.

  2. LLM прекрасно понимает человеческий (причём русский) язык и держит существенный контекст. Не требует высокой директивности.

  3. Отдельные функции и блоки кода сами по себе или в контексте больших приложений она пишет блестяще.

Есть немало вопросов к тому, как она порой делает откровенную чушь, но надо сделать всё же скидку — она не понимает, что она делает. Ну как стажёр.

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

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

Сам бы я эти два ботика писал бы существенно дольше. И большая часть времени была отведена на отладку.


Полезные (спорный эпитет) ссылки

Комментарии (2)


  1. dyadyaSerezha
    15.08.2025 05:18

    Я бегло просмотрел, но не увидел, а какой ИИ был использован? Это есть в статье?

    Я, она

    Ну и главное. Ты оглнулся посмотреть, не оглянулась ли она, чтоб посмотреть, не оглянулся ли ты?


    1. eucariot Автор
      15.08.2025 05:18

      Я бегло просмотрел, но не увидел, а какой ИИ был использован? Это есть в статье?

      Почти в самом начале: DeepSeek R1 Thinking