← Все гайды · Контент-завод

Гайд · кодовое слово КАРУСЕЛЬ

Карусель за одну строку

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

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

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

Что вышло у меня

Задание одной фразой: «собери карусель-тред на тему 6 вещей, которые ИИ делает за меня каждый день». Результат: 8 слайдов, время от фразы до готовых картинок - 23 секунды (сама отрисовка меньше секунды), затраты 0 рублей. Ни одной платной генерации: текст пишет Клод, картинки рисует библиотека Pillow на моём компьютере.

Из чего это собрано

Клод в терминале (Claude Code) - он читает скил, пишет тред и сам запускает отрисовку. Подписка та же, что для обычных задач.
Python и одна библиотека - Pillow для картинок и PyYAML для файла с текстом. Ставятся одной командой: pip install pillow pyyaml
Ваше фото - обычный портрет, из него скрипт вырезает круглую аватарку на карточку. Никаких нейро-лиц.

Шаг 1. Генератор

Файл make_thread.py. Кладите куда удобно, он ни от чего не зависит, кроме двух библиотек выше. Шрифт берётся системный Segoe UI, если его нет - подставляется запасной.

"""Генератор ТРЕД-КАРУСЕЛИ (карточки-твиты) для @sergei.pro.ai.

Второй формат карусели рядом с make_carousel.py (фото + текст). Здесь слайд =
белая карточка-твит на тёмном фоне: аватар, имя, @ник, текст. Тред пишет Клод в
YAML, техника (шрифты, карточка, перенос, тень) — на стороне скрипта.

Запуск:
    .venv\\Scripts\\python.exe scripts\\make_thread.py carousels\\<name>.yaml

Результат: output/<slug>/slide_01.png ... + сетка output/<slug>/_grid.png
"""
import argparse
import sys
import time
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont
import yaml

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

PROJECT_ROOT = Path(__file__).resolve().parents[1]
OUTPUT_DIR = PROJECT_ROOT / "output"

W, H = 1080, 1350
BG = (14, 16, 20)
CARD = (255, 255, 255)
INK = (15, 20, 25)
GREY = (110, 120, 130)
BLUE = (29, 155, 240)

WIN_FONTS = Path("C:/Windows/Fonts")
FALLBACK = PROJECT_ROOT / "assets" / "fonts"


def font(bold: bool, size: int):
    for path in ((WIN_FONTS / ("segoeuib.ttf" if bold else "segoeui.ttf")),
                 (FALLBACK / ("DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"))):
        if path.exists():
            return ImageFont.truetype(str(path), size)
    raise FileNotFoundError("Нет ни Segoe UI, ни DejaVu")


def wrap(draw, text: str, f, max_w: int):
    """Перенос по словам + сохранение переводов строк из YAML."""
    lines = []
    for para in text.split("\n"):
        if not para.strip():
            lines.append("")
            continue
        cur = ""
        for word in para.split():
            probe = f"{cur} {word}".strip()
            if draw.textlength(probe, font=f) <= max_w:
                cur = probe
            else:
                if cur:
                    lines.append(cur)
                cur = word
        lines.append(cur)
    return lines


def round_avatar(path: Path, size: int):
    img = Image.open(path).convert("RGB")
    iw, ih = img.size
    side = min(iw, ih)
    # лицо на рефах — в верхней трети кадра
    top = int(ih * 0.06)
    if top + side > ih:
        top = max(0, ih - side)
    left = (iw - side) // 2
    img = img.crop((left, top, left + side, top + side)).resize((size, size), Image.LANCZOS)
    mask = Image.new("L", (size, size), 0)
    ImageDraw.Draw(mask).ellipse((0, 0, size - 1, size - 1), fill=255)
    out = Image.new("RGBA", (size, size))
    out.paste(img, (0, 0), mask)
    return out


def draw_card(slide_text: str, meta: dict, avatar: Image.Image, idx: int, total: int):
    img = Image.new("RGB", (W, H), BG)
    d = ImageDraw.Draw(img)

    pad = 56
    card_x0, card_y0 = pad, 150
    card_x1, card_y1 = W - pad, H - 210

    # тень + карточка
    shadow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    ImageDraw.Draw(shadow).rounded_rectangle(
        (card_x0 + 8, card_y0 + 14, card_x1 + 8, card_y1 + 14), radius=34, fill=(0, 0, 0, 90))
    img.paste(Image.alpha_composite(img.convert("RGBA"), shadow).convert("RGB"), (0, 0))
    d = ImageDraw.Draw(img)
    d.rounded_rectangle((card_x0, card_y0, card_x1, card_y1), radius=34, fill=CARD)

    # шапка карточки
    ax, ay, asize = card_x0 + 48, card_y0 + 48, 104
    img.paste(avatar, (ax, ay), avatar)

    f_name = font(True, 42)
    f_handle = font(False, 36)
    name = meta.get("name", "Сергей")
    d.text((ax + asize + 26, ay + 6), name, font=f_name, fill=INK)
    nw = d.textlength(name, font=f_name)
    # синяя галочка
    cx, cy, r = ax + asize + 26 + nw + 26, ay + 6 + 21, 16
    d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=BLUE)
    d.line([(cx - 7, cy), (cx - 2, cy + 6), (cx + 8, cy - 6)], fill=(255, 255, 255), width=4)
    d.text((ax + asize + 26, ay + 58), meta.get("handle", "@sergei.pro.ai"),
           font=f_handle, fill=GREY)

    # текст твита — кегль подбирается под объём
    max_w = card_x1 - card_x0 - 96
    avail_h = card_y1 - (ay + asize + 54) - 60
    for size in (56, 52, 48, 44, 40, 36, 32):
        f_body = font(False, size)
        lines = wrap(d, slide_text, f_body, max_w)
        lh = int(size * 1.42)
        if len(lines) * lh <= avail_h:
            break
    y = ay + asize + 54
    for line in lines:
        d.text((card_x0 + 48, y), line, font=f_body, fill=INK)
        y += lh

    # подвал карточки: счётчик слайда
    f_small = font(False, 32)
    d.text((card_x0 + 48, card_y1 - 66), f"{idx}/{total}", font=f_small, fill=GREY)

    # подпись под карточкой
    f_foot = font(True, 38)
    foot = meta.get("footer", "листай →") if idx == 1 else meta.get("footer_rest", "")
    if foot:
        tw = d.textlength(foot, font=f_foot)
        d.text(((W - tw) / 2, card_y1 + 58), foot, font=f_foot, fill=(235, 238, 242))
    return img


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("yaml_path")
    args = ap.parse_args()

    t0 = time.time()
    data = yaml.safe_load(Path(args.yaml_path).read_text(encoding="utf-8"))
    slug = data.get("slug") or Path(args.yaml_path).stem
    tweets = data["tweets"]
    meta = {k: data[k] for k in ("name", "handle", "footer", "footer_rest") if k in data}

    avatar_path = Path(data["avatar"]) if data.get("avatar") else None
    if avatar_path and not avatar_path.is_absolute():
        avatar_path = (PROJECT_ROOT / avatar_path).resolve()
    avatar = round_avatar(avatar_path, 104)

    out = OUTPUT_DIR / slug
    out.mkdir(parents=True, exist_ok=True)
    imgs = []
    for i, tweet in enumerate(tweets, 1):
        img = draw_card(tweet.strip(), meta, avatar, i, len(tweets))
        img.save(out / f"slide_{i:02d}.png")
        imgs.append(img)

    # контактная сетка для показа в ролике
    cols = min(4, len(imgs))
    rows = (len(imgs) + cols - 1) // cols
    tw, th = W // 3, H // 3
    grid = Image.new("RGB", (cols * tw, rows * th), BG)
    for i, im in enumerate(imgs):
        grid.paste(im.resize((tw, th), Image.LANCZOS), ((i % cols) * tw, (i // cols) * th))
    grid.save(out / "_grid.png")

    print(f"Готово: {len(imgs)} слайдов -> {out}")
    print(f"Рендер: {time.time() - t0:.1f} с")


if __name__ == "__main__":
    main()

Шаг 2. Скил для Клода

Чтобы не объяснять заново каждый раз, правила лежат в файле-скиле. Путь: .claude/skills/karusel/SKILL.md в вашем проекте. После этого достаточно сказать «собери карусель на тему такую-то» - Клод сам вспомнит формат, напишет тред и запустит отрисовку.

---
name: karusel
description: Собрать карусель-тред (карточки в виде твитов) из ОДНОЙ фразы с темой — Клод пишет тред по виральным каркасам, генератор рендерит слайды 1080×1350. Используй, когда Сергей говорит "собери карусель", "карусель-тред на тему X", "/karusel X", "сделай слайды-твиты", "тред в карусель". Не про рилсы (это reels-pipeline) и не про фото-карусели отношенческого (это make_carousel.py).
---

# Карусель-тред за одну строку

Формат: слайд = белая карточка-твит на тёмном фоне (аватар + имя + ник + текст).
Работает для любого аккаунта — меняются только `name` / `handle` / `avatar` в YAML.

**Движок:** `carousel-pipeline/scripts/make_thread.py` (Pillow, 0 ₽, локально).
Рядом живёт старый формат «фото + текст» — `make_carousel.py`, его не трогаем.

## Как собрать (3 шага, ~20-30 секунд)

1. **Тред.** Пишешь YAML в `carousel-pipeline/carousels/thread_<slug>.yaml`:
   ```yaml
   title: "Тема карусели"
   slug: thread_<slug>
   name: "Сергей"
   handle: "@sergei.pro.ai"
   avatar: ../ai-creator-account/prompts/avatar-refs/ref-closeup.png
   footer: "листай →"
   tweets:
     - |
       Первый твит - обложка. Крючок + обещание, что будет дальше.
     - |
       1. Пункт.

       Раскрытие пункта: конкретика, цифра, инструмент по имени.
   ```
2. **Рендер.**
   ```powershell
   cd carousel-pipeline
   .venv\Scripts\python.exe scripts\make_thread.py carousels\thread_<slug>.yaml
   ```
3. **Результат.** `output/<slug>/slide_01.png …` + `output/<slug>/_grid.png`
   (сетка всех слайдов — она же кадр «результат» для ролика).

Кегль текста подбирается сам под объём (56 → 32). Пустая строка в YAML = пустая
строка на карточке. Счётчик `N/8` и «листай →» рисуются автоматически.

## Каркас треда (по нему пишем текст)

- **Слайд 1 - обложка.** Кто говорит + почему его слушать + что будет внутри.
  Заход от факта или цифры, не от вопроса-боли.
- **Слайды 2…N-1 - пункты.** Один пункт = одна мысль. Формула пункта:
  что делаю → как именно → чем подтверждаю (цифра, имя инструмента, срок).
- **Последний слайд - вывод.** Не призыв «подпишись», а честный итог, который
  можно унести. Призыв живёт в описании поста, не на слайде.

Правила текста те же, что у всего публикуемого: «КВН» вместо запретной аббревиатуры,
короткие тире « - », прямые кавычки. Название соцсети зависит от площадки: в самой
соцсети - прямо, за её пределами (сайты, телеграм, Дзен) - «Запретграм».
Финальную чистку текста гнать через скил `humaniser`.

## Проверка перед отдачей

- Открыть `_grid.png` глазами: нет обрезанных строк, кегль читается в ленте.
- Пункты не повторяют друг друга, в каждом есть конкретика.
- Аватар — реальное фото, ник совпадает с аккаунтом публикации.

## Факт (первый прогон, 04.08.2026)

Тема «6 вещей, которые ИИ делает за меня каждый день» → 8 слайдов.
От фразы с темой до готовых PNG — **23 секунды** (рендер 0.9 с), затраты 0 ₽.
Результат: `carousel-pipeline/output/thread_ii_za_menya/`.

Шаг 3. Файл с текстом

Тред живёт в YAML - это простой текстовый файл. Его пишет Клод, но полезно понимать устройство: сверху настройки карточки, ниже список твитов. Пустая строка внутри твита останется пустой строкой на картинке.

title: "6 вещей, которые ИИ делает за меня каждый день"
slug: thread_ii_za_menya
name: "Сергей"
handle: "@sergei.pro.ai"
avatar: ../ai-creator-account/prompts/avatar-refs/ref-closeup.png
footer: "листай →"
tweets:
  - |
    Мне 45. Год назад я не умел ничего, кроме как открыть браузер.

    Сейчас шесть дел в день за меня делает нейросеть - и я расскажу какие.
  - |
    1. Читает документы.

    Кидаю договор или справку - получаю пересказ на человеческом и список мест,
    где меня могут подловить.

Каркас треда - почему его дочитывают

Первый слайд - обложка. Кто говорит, почему его стоит слушать и что будет внутри. Заход от факта или цифры. Вопрос-боль в начале («устали от…») убивает досмотр.
Средние слайды - пункты. Один слайд - одна мысль. Формула: что делаю, как именно, чем подтверждаю. Подтверждение - цифра, имя инструмента или срок.
Последний слайд - вывод. Не «подпишись», а мысль, которую можно унести. Призыв живёт в описании поста, на слайде он выглядит как реклама.

Границы - честно

Тексты всё равно надо читать. Клод пишет тред за секунды, но факты и цифры в нём ваши. Я каждый раз перечитываю: где нет конкретики - переписываю руками.
Шрифт системный. Скрипт ищет Segoe UI (Windows). На другой системе подставится запасной шрифт из папки проекта - вид чуть изменится, вёрстка не поедет.
Длинный твит мельчает. Кегль подбирается автоматически от 56 до 32. Если текст ушёл в самый мелкий - это сигнал резать, а не оставлять как есть.
Публикация руками. Готовые картинки лежат папкой, автопостинга тут нет и не будет: аккаунты не любят, когда за них постит робот.

Что дальше

Этот скил - один узел моего контент-завода. Рядом такие же: клон голоса, аватар в кадре, парсер чужих выстрелов, касса. Как это всё собиралось за 14 дней - в сериале. Что происходит сейчас, включая ставку «первая продажа за 30 дней», пишу в дневнике - t.me/sergeiai90.