Вайбмаркетинг · монтаж рилс · сентябрь 2026

Скилл монтажа рилс для Claude Code: 7 правок одной фразой

Скилл монтажа для Claude Code это два файла в папке ~/.claude/skills/montazher: инструкция для Claude и твой стиль. Ты пишешь Claude одну фразу, и он делает семь правок: вырезает паузы и дубли, чистит шум, выравнивает громкость, ставит субтитры, хук с надписями, зумы и музыку тише голоса

Шаг 1. Открой Терминал (Cmd + Пробел, набери «Терминал»), вставь строку и нажми Enter. Она поставит программы монтажа и распознавания речи, шрифт Inter и скачает модель распознавания на 547 МБ. Программа монтажа тянет за собой 47 библиотек, так что это займёт несколько минут:

brew install ffmpeg-full whisper-cpp && brew install --cask font-inter && mkdir -p ~/whisper-models && curl -L -o ~/whisper-models/ggml-large-v3-turbo-q5_0.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin

Шаг 2. Набери claude, вставь сообщение из следующего раздела (в нём оба файла целиком) и дождись ответа, что файлы созданы. Шаг 3. Запусти монтаж, путь замени на свой:

Смонтируй скиллом montazher ролик ~/Movies/rolik.mov

Мою двухминутную запись (131,8 секунды) с четырьмя неудачными дублями Claude смонтировал в ролик на 45,3 секунды чуть больше чем за минуту. Готовый файл ложится рядом с исходником, в конце имени стоит -gotovo.mp4

Первый ролик смонтирован? Открой style.json в папке скилла и подгони три строки под себя: размер субтитров (size), сколько слов в строке (words_per_line) и насколько тише голоса музыка (quieter_than_voice_db). Claude перечитывает файл перед каждым монтажом, поэтому следующий ролик выйдет уже в твоём стиле, без повторных объяснений

Поставь скилл: одно сообщение для Claude

Установка скилла это два текстовых файла в нужной папке. Руками её искать не надо: папка .claude на Mac скрыта. Проще отдать файлы самому Claude, он создаст папку и положит их туда

  1. Набери claude в том же окне, где ставил программы, чтобы запустить Claude Code
  2. Скопируй сообщение ниже целиком, кнопкой копирования в углу блока, вставь в Claude Code и отправь
  3. Claude попросит разрешение создать файлы, соглашайся

Что увидишь: Claude ответит, что создал папку ~/.claude/skills/montazher с двумя файлами и что в SKILL.md 406 строк. Я ставил скилл таким сообщением трижды, последний раз ровно этим текстом: установка занимала около полутора минут, и каждый раз оба файла совпадали с оригиналом байт в байт

Создай скилл montazher: папку ~/.claude/skills/montazher и в ней два файла, SKILL.md и style.json. Текст каждого файла стоит ниже между строками НАЧАЛО и КОНЕЦ. Перепиши его в файл дословно, ничего не меняя, сами строки НАЧАЛО и КОНЕЦ в файл не входят. Потом проверь, что в SKILL.md 406 строк.

===== НАЧАЛО SKILL.md =====
---
name: montazher
description: Монтирует снятый на телефон вертикальный ролик (рилс, шортс, клип) целиком в одном стиле - вырезает паузы и неудачные дубли, чистит шум, выравнивает громкость, ставит субтитры слово в слово, хук и надписи поверх видео, зумы и фоновую музыку тише голоса. Применяй, когда просят «смонтируй ролик», «смонтируй рилс», «сделай монтаж этого видео» и дают видеофайл.
---

# Монтажёр рилсов

Ты монтируешь говорящее видео. Решения по смыслу (какие дубли убрать, какой хук, где надписи и зумы) принимаешь ты. Механическую работу делает помощник montazh.py из папки этого скилла. Исходное видео не трогаешь, ничего не удаляешь.

## Шаг 0. Помощник и программы

1. Если файла `${CLAUDE_SKILL_DIR}/montazh.py` нет, создай его: возьми из этого файла всё, что стоит между строкой `#### НАЧАЛО montazh.py` и строкой `#### КОНЕЦ montazh.py` (сами эти строки не входят), и сохрани как есть, не меняя ни одного символа.
2. Проверь программы:

    python3 "${CLAUDE_SKILL_DIR}/montazh.py" --help && "$(brew --prefix ffmpeg-full)/bin/ffmpeg" -hide_banner -filters | grep -c " subtitles " && which whisper-cli && ls ~/whisper-models/

Чего-то нет - скажи человеку ОДНОЙ строкой, что выполнить: `brew install ffmpeg-full`, `brew install whisper-cpp` или скачать модель распознавания (команду печатает помощник). Дальше не иди.

## Шаг 1. Паузы и дубли

Папка работы лежит рядом с видео и называется `<имя видео>-montazh`.

    python3 "${CLAUDE_SKILL_DIR}/montazh.py" analiz "<видео>" "<папка работы>"

Помощник печатает куски речи с номерами. Прочитай их и реши сам:
- несколько кусков начинаются одинаково = дубли одной фразы: оставь ПОСЛЕДНИЙ полный, остальные убери
- оговорка («ой», «сейчас заново», оборванная фраза, которую дальше сказали целиком) - убери
- всё остальное оставь, порядок не меняй

Убери лишние записи из `<папка работы>/kuski.json`, остальные поля не трогай.

## Шаг 2. Нарезка и звук

    python3 "${CLAUDE_SKILL_DIR}/montazh.py" rez "<видео>" "<папка работы>"

Помощник вырезает паузы и дубли, чистит шум, выравнивает громкость, заново распознаёт речь и печатает текст со временем. Открой `<папка работы>/titry.json` и исправь ошибки распознавания: названия сервисов, имена, числа. Меняй только поле "slovo", время не трогай, одно слово = одна запись.

## Шаг 3. План: хук, надписи, зумы

Запиши `<папка работы>/plan.json`:

    {"hook": "...", "labels": [{"text": "...", "s": 6.9, "e": 8.4}], "zooms": [[1.8, 3.2]]}

- hook: до 7 слов на первые 2,5 секунды, суть или выгода ролика словами автора; не обещай того, чего в ролике нет
- labels: 1-3 надписи по 1-3 слова (название, число, ключевое слово) ровно в тот момент, когда их произносят, на 1,2-2,5 секунды
- zooms: примерно zooms.per_minute из style.json на минуту ролика, на смысловых ударениях, каждый 1,5-4 секунды, в первые 2,5 секунды зума нет
- все времена берёшь из titry.json: это время уже смонтированного ролика

## Шаг 4. Сборка

Музыка: если человек дал файл музыки или в style.json заполнено music.file - передай путь третьим аргументом. Музыки нет - собирай без неё и скажи об этом в отчёте.

    python3 "${CLAUDE_SKILL_DIR}/montazh.py" render "<папка работы>" "<папка видео>/<имя видео>-gotovo.mp4" "<музыка, если есть>"

## Шаг 5. Проверка глазами и отчёт

Открой каждый кадр proverka-*.jpg, который напечатал помощник. Проверь: субтитры читаются и не вылезают за край, хук виден и не закрывает лицо целиком, надпись совпадает со сказанным. Что-то не так - поправь plan.json или style.json и повтори шаг 4.

Отчёт человеку коротко: путь к готовому файлу, длительность было и стало, какие дубли убрал (номер и текст), текст хука и надписей, есть ли музыка.

## Если что-то сломалось

- помощник пишет «НЕТ ffmpeg-full» - нужен ffmpeg с субтитрами: `brew install ffmpeg-full` (обычный ffmpeg субтитры не умеет)
- после установки ffmpeg-full обычный ffmpeg пишет `Library not loaded: ... libx265` - Homebrew обновил общие библиотеки, почини: `brew upgrade ffmpeg`
- субтитры не тем шрифтом - шрифт из style.json не установлен: `brew install --cask font-inter` или поменяй font на установленный
- вырезались начала слов - уменьши pauses.silence_threshold в style.json (0.35 вместо 0.45); остались паузы - увеличь
- видео горизонтальное - помощник обрежет края под 1080x1920; скажи об этом человеку до сборки

## Помощник montazh.py

#### НАЧАЛО montazh.py
#!/usr/bin/env python3
# Помощник скилла «монтажёр». Три шага: analiz -> rez -> render.
# Только стандартная библиотека Python, внешние программы: ffmpeg-full и whisper-cli.
import json, math, os, shutil, struct, subprocess, sys, wave
from array import array

VERSIYA = "montazh.py 1.0"
PAPKA_SKILLA = os.path.dirname(os.path.abspath(__file__))

def ffmpeg_put(imya="ffmpeg"):
    """Нужен ffmpeg-full: в обычном ffmpeg из brew нет субтитров. Он не в PATH (keg-only)."""
    try:
        pref = subprocess.run(["brew", "--prefix", "ffmpeg-full"], capture_output=True, text=True).stdout.strip()
    except FileNotFoundError:
        sys.exit("НЕТ Homebrew. Поставь его с сайта brew.sh, потом: brew install ffmpeg-full")
    put = os.path.join(pref, "bin", imya)
    if not pref or not os.path.exists(put):
        sys.exit("НЕТ ffmpeg-full. Установи: brew install ffmpeg-full")
    return put

FF = ffmpeg_put("ffmpeg")
FP = ffmpeg_put("ffprobe")

def stil():
    with open(os.path.join(PAPKA_SKILLA, "style.json"), encoding="utf-8") as f:
        return json.load(f)

def model_put():
    if os.environ.get("WHISPER_MODEL"):
        return os.environ["WHISPER_MODEL"]
    for p in [os.path.expanduser("~/whisper-models/ggml-large-v3-turbo-q5_0.bin"),
              os.path.expanduser("~/whisper-models/ggml-large-v3-turbo.bin")]:
        if os.path.exists(p):
            return p
    sys.exit("НЕТ модели распознавания. Скачай: mkdir -p ~/whisper-models && curl -L -o "
             "~/whisper-models/ggml-large-v3-turbo-q5_0.bin "
             "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin")

def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        sys.stderr.write(r.stderr[-3000:])
        sys.exit(f"ОШИБКА команды: {cmd[0]} (код {r.returncode})")
    return r

def raspoznat(wav, vyhod_bez_rasshireniya):
    if not shutil.which("whisper-cli"):
        sys.exit("НЕТ whisper-cli. Установи: brew install whisper-cpp")
    st = stil()
    run(["whisper-cli", "-m", model_put(), "-l", st.get("language", "ru"), "-f", wav,
         "-ml", "1", "-sow", "-oj", "-of", vyhod_bez_rasshireniya, "-np"])
    with open(vyhod_bez_rasshireniya + ".json", encoding="utf-8") as f:
        d = json.load(f)
    slova = []
    for s in d.get("transcription", []):
        t = s.get("text", "").strip()
        if not t or t.startswith("["):
            continue
        slova.append({"s": s["offsets"]["from"] / 1000, "e": s["offsets"]["to"] / 1000, "slovo": t})
    return slova

def urovni(wav, shag=0.05):
    """Громкость каждого кусочка по 0.05 с, в децибелах."""
    with wave.open(wav, "rb") as w:
        sr = w.getframerate()
        raw = w.readframes(w.getnframes())
    a = array("h")
    a.frombytes(raw)
    n = int(sr * shag)
    out = []
    for i in range(0, len(a) - n + 1, n):
        kus = a[i:i + n]
        rms = math.sqrt(sum(x * x for x in kus) / n) or 1e-9
        out.append(20 * math.log10(rms / 32768))
    return out, shag

def analiz(video, papka):
    os.makedirs(papka, exist_ok=True)
    st = stil()
    p = st["pauses"]
    fps = st["frame"]["fps"]
    wav = os.path.join(papka, "zvuk.wav")
    # мерим тишину по звуку без фонового шума: так речь отличается от фона сильнее
    run([FF, "-y", "-v", "error", "-i", video, "-vn", "-ac", "1", "-ar", "16000",
         "-af", f"highpass=f=80,afftdn=nf={st['sound']['noise_floor_db']}", wav])
    db, shag = urovni(wav)
    dlina, dur = len(db), len(db) * shag
    # порог между фоном (тихие 10%) и речью (громкие 10%), а не число с потолка
    srt = sorted(db)
    p10, p90 = srt[int(dlina * 0.1)], srt[int(dlina * 0.9)]
    verh = p10 + (p90 - p10) * p["silence_threshold"]
    niz = p10 + (p90 - p10) * p["silence_threshold"] * 0.5
    # ядро речи громче верхнего порога, края слов дотягиваем до нижнего
    rech = [x > verh for x in db]
    for i in range(1, dlina):
        if not rech[i] and rech[i - 1] and db[i] > niz:
            rech[i] = True
    for i in range(dlina - 2, -1, -1):
        if not rech[i] and rech[i + 1] and db[i] > niz:
            rech[i] = True
    # куски речи = всё, кроме тишины длиннее cut_longer_than_sec
    min_tish = int(round(p["cut_longer_than_sec"] / shag))
    kuski, i, nachalo = [], 0, None
    while i < dlina:
        if rech[i]:
            if nachalo is None:
                nachalo = i
            i += 1
            continue
        j = i
        while j < dlina and not rech[j]:
            j += 1
        if j - i >= min_tish or j == dlina:
            if nachalo is not None:
                kuski.append([nachalo * shag, i * shag])
                nachalo = None
        i = j
    if nachalo is not None:
        kuski.append([nachalo * shag, dur])
    pad = p["keep_around_speech_sec"]
    itog = []
    for s, e in kuski:
        if e - s < 0.15:  # щелчок или вдох, не речь
            continue
        s, e = max(0, s - pad), min(dur, e + pad)
        s, e = math.floor(s * fps) / fps, math.ceil(e * fps) / fps
        if itog and s <= itog[-1]["e"]:
            itog[-1]["e"] = e
        else:
            itog.append({"s": round(s, 4), "e": round(e, 4)})
    # распознаём склейку без пауз: на ней время слов точнее, чем на сыром звуке
    usl = "+".join(f"gte(t,{k['s']})*lt(t,{k['e']})" for k in itog)
    skleyka = os.path.join(papka, "zvuk-bez-pauz.wav")
    run([FF, "-y", "-v", "error", "-i", wav, "-af", f"aselect='{usl}',asetpts=N/SR/TB", skleyka])
    slova = raspoznat(skleyka, os.path.join(papka, "slova-ishodnik"))
    sdvig = 0.0
    for n, k in enumerate(itog, 1):
        d = k["e"] - k["s"]
        k["n"] = n
        k["tekst"] = " ".join(w["slovo"] for w in slova if sdvig - 0.05 <= (w["s"] + min(w["e"], w["s"] + 0.3)) / 2 < sdvig + d)
        sdvig += d
    with open(os.path.join(papka, "kuski.json"), "w", encoding="utf-8") as f:
        json.dump(itog, f, ensure_ascii=False, indent=1)
    dlit_rechi = sum(k["e"] - k["s"] for k in itog)
    print(f"{VERSIYA}: исходник {dur:.1f} с, после вырезания пауз останется {dlit_rechi:.1f} с, кусков {len(itog)}")
    print("Куски речи (номер | начало-конец | что сказано):")
    for k in itog:
        print(f"{k['n']:>3} | {k['s']:7.2f}-{k['e']:7.2f} | {k['tekst']}")
    print("\nДальше: убери из kuski.json дубли и оговорки (оставь последний удачный дубль), потом шаг rez")

def rez(video, papka):
    st = stil()
    with open(os.path.join(papka, "kuski.json"), encoding="utf-8") as f:
        kuski = json.load(f)
    fr = st["frame"]
    fps, W, H = fr["fps"], fr["width"], fr["height"]
    usl = "+".join(f"gte(t,{k['s']})*lt(t,{k['e']})" for k in kuski)
    vf = (f"setpts=PTS-STARTPTS,fps={fps},scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H},"
          f"select='{usl}',setpts=N/{fps}/TB")
    zv = st["sound"]
    af = (f"asetpts=PTS-STARTPTS,aselect='{usl}',asetpts=N/SR/TB,highpass=f=80,"
          f"afftdn=nf={zv['noise_floor_db']},loudnorm=I={zv['loudness_lufs']}:TP=-1.5:LRA=11,aresample=48000")
    cut = os.path.join(papka, "narezka.mov")
    run([FF, "-y", "-v", "error", "-i", video, "-vf", vf, "-af", af,
         "-c:v", "libx264", "-crf", "16", "-preset", "veryfast", "-pix_fmt", "yuv420p",
         "-c:a", "pcm_s16le", cut])
    wav = os.path.join(papka, "zvuk-narezki.wav")
    run([FF, "-y", "-v", "error", "-i", cut, "-vn", "-ac", "1", "-ar", "16000", wav])
    slova = raspoznat(wav, os.path.join(papka, "slova-narezki"))
    for a, b in zip(slova, slova[1:]):
        a["e"] = min(a["e"], b["s"])
    with open(os.path.join(papka, "titry.json"), "w", encoding="utf-8") as f:
        json.dump(slova, f, ensure_ascii=False, indent=1)
    dur = float(run([FP, "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", cut]).stdout.strip())
    print(f"{VERSIYA}: нарезка готова, {dur:.1f} с. Слова для субтитров (titry.json):")
    stroka, t0 = [], None
    for w in slova:
        if t0 is None:
            t0 = w["s"]
        stroka.append(w["slovo"])
        if w["slovo"][-1:] in ".?!" or len(stroka) >= 12:
            print(f"{t0:6.2f} | {' '.join(stroka)}")
            stroka, t0 = [], None
    if stroka:
        print(f"{t0:6.2f} | {' '.join(stroka)}")
    print("\nДальше: поправь в titry.json ошибки распознавания, напиши plan.json (хук, надписи, зумы), потом шаг render")

def ass_cvet(hex_, prozrachnost=0.0):
    h = hex_.lstrip("#")
    r, g, b = h[0:2], h[2:4], h[4:6]
    a = int(round(prozrachnost * 255))
    return f"&H{a:02X}{b}{g}{r}".upper()

def ass_vremya(t):
    t = max(0, t)
    h, t = int(t // 3600), t % 3600
    m, s = int(t // 60), t % 60
    return f"{h}:{m:02d}:{s:05.2f}"

def ass_tekst(t):
    return t.replace("\\", "").replace("{", "(").replace("}", ")").replace("\n", " ")

def sobrat_ass(papka, dur):
    st = stil()
    W, H = st["frame"]["width"], st["frame"]["height"]
    s, h, n = st["subtitles"], st["hook"], st["labels"]
    with open(os.path.join(papka, "titry.json"), encoding="utf-8") as f:
        slova = json.load(f)
    with open(os.path.join(papka, "plan.json"), encoding="utf-8") as f:
        plan = json.load(f)

    def stil_ass(imya, c):
        bold = -1 if c.get("bold") else 0
        plashka = c.get("box_color")
        border = 3 if plashka else 1
        back = ass_cvet(plashka, 1 - c.get("box_opacity", 1)) if plashka else ass_cvet("#000000", 1 - c.get("shadow_opacity", 0))
        outline_c = back if plashka else ass_cvet(c.get("outline_color", "#000000"))
        outline = c.get("box_padding", 0) if plashka else c.get("outline", 0)
        return (f"Style: {imya},{c['font']},{c['size']},{ass_cvet(c['color'])},{ass_cvet(c['color'])},"
                f"{outline_c},{back},{bold},0,0,0,100,100,{c.get('letter_spacing', 0)},0,{border},{outline},"
                f"{c.get('shadow', 0)},{c.get('align', 2)},60,60,{c.get('margin_v', 0)},1")

    zag = ["[Script Info]", "ScriptType: v4.00+", f"PlayResX: {W}", f"PlayResY: {H}", "WrapStyle: 0",
           "ScaledBorderAndShadow: yes", "", "[V4+ Styles]",
           "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, "
           "Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, "
           "MarginL, MarginR, MarginV, Encoding",
           stil_ass("Titry", dict(s, align=2, margin_v=int(H * s["bottom_margin_pct"] / 100))),
           stil_ass("Huk", dict(h, align=8, margin_v=int(H * h["top_margin_pct"] / 100))),
           stil_ass("Nadpis", dict(n, align=8, margin_v=int(H * n["top_margin_pct"] / 100))),
           "", "[Events]", "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"]
    sob = []
    # субтитры: по N слов, новый кусок после точки/вопроса или паузы
    gruppy, tek = [], []
    for w in slova:
        if tek and (len(tek) >= s["words_per_line"] or w["s"] - tek[-1]["e"] > 0.3):
            gruppy.append(tek)
            tek = []
        tek.append(w)
        if w["slovo"][-1:] in ".?!,":
            gruppy.append(tek)
            tek = []
    if tek:
        gruppy.append(tek)
    for i, g in enumerate(gruppy):
        nach = g[0]["s"]
        kon = gruppy[i + 1][0]["s"] if i + 1 < len(gruppy) else min(dur, g[-1]["e"] + 0.3)
        kon = min(kon, g[-1]["e"] + 0.6)
        tekst = " ".join(w["slovo"] for w in g).rstrip(".,")
        if s.get("uppercase"):
            tekst = tekst.upper()
        if s.get("soft_shadow"):  # мягкая тень отдельным слоем под текстом, сам текст остаётся чётким
            a = int(round((1 - s.get("soft_shadow_opacity", 0.5)) * 255))
            ten = "{\\1a&HFF&\\bord%s\\3c&H000000&\\3a&H%02X&\\blur%s}" % (s["soft_shadow"] // 2, a, s["soft_shadow"])
            sob.append(f"Dialogue: 0,{ass_vremya(nach)},{ass_vremya(kon)},Titry,,0,0,0,,{ten}{ass_tekst(tekst)}")
        sob.append(f"Dialogue: 1,{ass_vremya(nach)},{ass_vremya(kon)},Titry,,0,0,0,,{ass_tekst(tekst)}")
    if plan.get("hook"):
        sob.append(f"Dialogue: 2,{ass_vremya(0)},{ass_vremya(h['seconds'])},Huk,,0,0,0,,{ass_tekst(plan['hook'])}")
    for nd in plan.get("labels", []):
        sob.append(f"Dialogue: 2,{ass_vremya(nd['s'])},{ass_vremya(nd['e'])},Nadpis,,0,0,0,,{ass_tekst(nd['text'])}")
    put = os.path.join(papka, "titry.ass")
    with open(put, "w", encoding="utf-8") as f:
        f.write("\n".join(zag + sob) + "\n")
    return put, len(gruppy)

def render(papka, vyhod, muzyka=None):
    st = stil()
    fr = st["frame"]
    W, H, fps = fr["width"], fr["height"], fr["fps"]
    cut = os.path.join(papka, "narezka.mov")
    dur = float(run([FP, "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", cut]).stdout.strip())
    with open(os.path.join(papka, "plan.json"), encoding="utf-8") as f:
        plan = json.load(f)
    ass, n_titrov = sobrat_ass(papka, dur)
    z = st["zooms"]["scale"]
    zumy = plan.get("zooms", [])
    if zumy:
        usl = "+".join(f"gte(it,{a})*lt(it,{b})" for a, b in zumy)
        zoom = (f"zoompan=z='if({usl},{z},1)':x='(iw-iw/zoom)/2':y='(ih-ih/zoom)*{st['zooms']['focus_y']}'"
                f":d=1:s={W}x{H}:fps={fps},")
    else:
        zoom = ""
    ass_esc = ass.replace("\\", "/").replace(":", "\\:").replace("'", "\\'")
    fonts = os.path.expanduser("~/Library/Fonts").replace(":", "\\:")
    vf = f"[0:v]{zoom}subtitles=filename='{ass_esc}':fontsdir='{fonts}'[v]"
    vhody = ["-i", cut]
    m = st["music"]
    muzyka = muzyka or m.get("file") or None
    if muzyka:
        muzyka = os.path.expanduser(muzyka)
        vhody += ["-stream_loop", "-1", "-i", muzyka]
        af = (f"[0:a]asplit=2[g][sc];"
              f"[1:a]aresample=48000,loudnorm=I={st['sound']['loudness_lufs'] - m['quieter_than_voice_db']}:TP=-6,"
              f"afade=t=out:st={max(0, dur - 1.5):.2f}:d=1.5[m0];"
              f"[m0][sc]sidechaincompress=threshold=0.05:ratio=6:attack=30:release=400[m];"
              f"[g][m]amix=inputs=2:duration=first:normalize=0,alimiter=limit=0.9[a]")
    else:
        af = "[0:a]anull[a]"
    run([FF, "-y", "-v", "error", *vhody, "-filter_complex", vf + ";" + af, "-map", "[v]", "-map", "[a]",
         "-t", f"{dur:.3f}", "-c:v", "libx264", "-crf", "19", "-preset", "medium", "-pix_fmt", "yuv420p",
         "-r", str(fps), "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", vyhod])
    # проверка: размер, длительность, громкость, кадры для глаз
    info = json.loads(run([FP, "-v", "error", "-show_entries", "stream=width,height:format=duration",
                           "-of", "json", vyhod]).stdout)
    gr = run([FF, "-hide_banner", "-i", vyhod, "-af", "ebur128", "-f", "null", "-"]).stderr
    lufs = [l for l in gr.splitlines() if l.strip().startswith("I:")]
    kadry = []
    moments = [min(1.0, dur / 2), dur / 2] + [(nd["s"] + nd["e"]) / 2 for nd in plan.get("labels", [])[:1]] \
        + [(a + b) / 2 for a, b in zumy[:1]]
    for i, t in enumerate(moments, 1):
        k = os.path.join(papka, f"proverka-{i}.jpg")
        run([FF, "-y", "-v", "error", "-ss", f"{t:.2f}", "-i", vyhod, "-frames:v", "1", "-vf", "scale=540:-1", k])
        kadry.append(k)
    v = [s for s in info["streams"] if s.get("width")][0]
    print(f"{VERSIYA}: ГОТОВО {vyhod}")
    print(f"  кадр {v['width']}x{v['height']}, длительность {float(info['format']['duration']):.1f} с, "
          f"громкость {lufs[-1].strip() if lufs else '?'}, субтитров {n_titrov}, зумов {len(zumy)}, "
          f"надписей {len(plan.get('labels', []))}, музыка {'да' if muzyka else 'нет'}")
    print("  кадры для проверки глазами: " + " ".join(kadry))

if __name__ == "__main__":
    if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
        print("analiz <видео> <папка> | rez <видео> <папка> | render <папка> <готовый.mp4> [музыка]")
        sys.exit(0)
    cmd = sys.argv[1]
    if cmd == "analiz":
        analiz(os.path.expanduser(sys.argv[2]), os.path.expanduser(sys.argv[3]))
    elif cmd == "rez":
        rez(os.path.expanduser(sys.argv[2]), os.path.expanduser(sys.argv[3]))
    elif cmd == "render":
        render(os.path.expanduser(sys.argv[2]), os.path.expanduser(sys.argv[3]),
               sys.argv[4] if len(sys.argv) > 4 else None)
    else:
        sys.exit("неизвестный шаг: " + cmd)
#### КОНЕЦ montazh.py
===== КОНЕЦ SKILL.md =====

===== НАЧАЛО style.json =====
{
  "language": "ru",
  "frame": { "width": 1080, "height": 1920, "fps": 30 },
  "pauses": {
    "cut_longer_than_sec": 0.35,
    "keep_around_speech_sec": 0.08,
    "silence_threshold": 0.45
  },
  "sound": { "noise_floor_db": -25, "loudness_lufs": -14 },
  "subtitles": {
    "font": "Inter",
    "size": 84,
    "bold": false,
    "color": "#F3F1EE",
    "outline": 0,
    "shadow": 0,
    "soft_shadow": 8,
    "soft_shadow_opacity": 0.55,
    "letter_spacing": 0,
    "words_per_line": 3,
    "uppercase": false,
    "bottom_margin_pct": 28
  },
  "hook": {
    "font": "Inter",
    "size": 88,
    "bold": true,
    "color": "#141414",
    "box_color": "#F3F1EE",
    "box_opacity": 1,
    "box_padding": 22,
    "seconds": 2.5,
    "top_margin_pct": 12
  },
  "labels": {
    "font": "Inter",
    "size": 68,
    "bold": true,
    "color": "#F3F1EE",
    "box_color": "#141414",
    "box_opacity": 0.85,
    "box_padding": 16,
    "top_margin_pct": 30
  },
  "zooms": { "scale": 1.12, "per_minute": 5, "focus_y": 0.4 },
  "music": { "file": "", "quieter_than_voice_db": 18 }
}
===== КОНЕЦ style.json =====

Что где лежит после установки и первого монтажа:

ФайлКто кладётЗачем он
SKILL.mdClaude по твоему сообщениюописание, по которому Claude узнаёт задачу «смонтируй ролик», пять шагов монтажа и частые поломки
style.jsonClaude по твоему сообщениютвой стиль: шрифт, размер и цвет субтитров, хук, зумы, громкость музыки
montazh.pyClaude сам при первом монтажепомощник для механической работы, его текст лежит в конце SKILL.md, поэтому копировать нужно два файла, а не три

Таблица прокручивается вбок →

Проверка, что скилл на месте: закрой Claude Code, открой снова и набери /skills. В списке должна быть строка montazher. Если скилл тебе вообще в новинку, как он устроен изнутри, я разбирал в статье как сделать свой скилл в Claude

Смонтируй первый ролик: одна фраза

Сними ролик на телефон вертикально, как снимаешь обычно, со всеми паузами и повторами, и перекинь файл на Mac, например в папку «Фильмы». Музыку положи рядом, если она нужна. Потом напиши в Claude Code:

Смонтируй скиллом montazher ролик ~/Movies/rolik.mov, музыка ~/Movies/muzyka.mp3

Путь к файлу руками набирать не нужно. Напечатай фразу до слова «ролик», потом перетащи видео из Finder прямо в окно Терминала: путь вставится сам. У ролика с iPhone имя будет вроде IMG_1234.MOV, это нормально

Дальше Claude идёт по шагам сам, ты только разрешаешь ему запускать команды:

  1. Находит паузы по громкости и распознаёт речь. На экране появляется список кусков речи с номерами и текстом
  2. Сам решает, какие куски лишние. Если одна фраза сказана три раза, оставляет последний полный дубль, оговорки выкидывает
  3. Режет видео, чистит шум, выравнивает громкость и заново распознаёт уже смонтированную речь
  4. Исправляет ошибки распознавания в названиях и пишет план: текст хука, надписи и моменты для зумов
  5. Собирает ролик, открывает четыре контрольных кадра и сам проверяет, читаются ли субтитры
Кадр сырой записи и тот же момент в готовом ролике: сверху хук на плашке, внизу субтитр по словам
Слева моя сырая запись, справа первая секунда ролика после скилла: хук «5 сайтов: из текста готовый ролик» и субтитр «надо запретить»

Что было на моём прогоне. Запись на 131,8 секунды с четырьмя дублями: одну фразу я начинал трижды, ещё две сказал по два раза. Claude убрал все четыре неудачных дубля и оставил последние полные, поправил название, которое распознавание услышало как «NVIDIA», и написал хук «5 сайтов: из текста готовый ролик». Итог 45,3 секунды, от фразы до готового файла 71-73 секунды в четырёх прогонах подряд на Mac с процессором M4 Max. На ноутбуке послабее будет дольше, сколько именно, я не замерял

Как понять, что всё получилось

Успех видно по трём признакам, проверь все три:

  1. Рядом с исходником лежит файл с -gotovo.mp4 в конце имени и папка с -montazh в конце: в ней черновики монтажа, их можно не открывать
  2. В чате Claude отчёт: сколько секунд было и стало, какие дубли убраны (номер и текст), какой хук и какие надписи стоят
  3. Открой ролик двойным щелчком. Первые 2,5 секунды сверху хук, внизу субтитры меняются вместе со словами, названия сервисов всплывают надписью ровно тогда, когда ты их произносишь, музыка тише голоса
Три кадра готового ролика с надписями ZSky AI, Pictory и D-ID сверху и субтитрами внизу
Надписи всплывают в момент, когда название звучит: ZSky AI, Pictory, D-ID. Субтитры внизу идут по 1-3 слова

Если в отчёте Claude написал, что в чём-то не уверен (так у меня было с InVideo), проверь это место в ролике и ответь ему словами: «там не InVideo, а X, пересобери». Он поправит субтитры и соберёт заново, не повторяя нарезку

Что скилл делает с роликом и где это поменять

Каждое из семи действий управляется строкой в файле стиля. Менять его проще через Claude: «открой файл стиля скилла montazher и поставь размер субтитров 96»

ДействиеКак делаетсяЧто поменять в файле стиля
Паузывырезается тишина длиннее 0,35 секунды, по краям слов остаётся 0,08 секунды воздухаcut_longer_than_sec: больше число, больше пауз останется
Дубли и оговоркиClaude читает текст кусков и оставляет последний полный дубльв файле нет, скажи Claude словами, что оставить
Шумфильтр шумоподавления ffmpeg убирает ровный фон: гул, шипениеnoise_floor_db: -30 чистит мягче, -20 сильнее
Громкостьголос выравнивается до -14 LUFS, это одинаковая громкость от ролика к роликуloudness_lufs
Субтитрыраспознавание пословно, по 3 слова в строке, Inter, цвет #F3F1EE, без обводки и плашкиsize, color, words_per_line, uppercase, bottom_margin_pct
Хук и надписитекст пишет Claude по смыслу ролика, хук висит первые 2,5 секундыблоки hook и labels: шрифт, цвет, плашка, seconds
Зумырезкое приближение в 1,12 раза на смысловых ударениях, около пяти в минутуscale, per_minute
Музыкаидёт на 18 дБ тише голоса и сама приглушается, когда ты говоришь, в конце плавно стихаетquieter_than_voice_db, file: путь к треку по умолчанию

Таблица прокручивается вбок →

Один и тот же момент ролика до зума и после: справа лицо крупнее, внизу субтитр
Один момент до и после: слева нарезка без эффектов, справа тот же кадр с зумом в 1,12 раза и субтитром

Про стиль субтитров. На карусели был стиль Inter Regular, цвет #F3F1EE, без обводки и плашки, так он и стоит в файле стиля. Одно я добавил после прогона: мягкую размытую тень под буквами. На моей светлой рубашке белые субтитры без неё терялись. Снимаешь на тёмном фоне, поставь soft_shadow в 0. Размер на слайде был 30 для экрана телефона, в кадре ролика 1080 на 1920 это 84. Межстрочного шага в файле нет: субтитр идёт одной строкой по 1-3 слова, переносить нечего

Если у тебя другой случай

Claude Code ещё не стоит. На Mac он ставится одной строкой в Терминале, что нажать при первом входе, разобрано в статье как установить Claude Code. Для входа нужна подписка: Pro стоит 20 долларов в месяц (17 при оплате за год), на бесплатном тарифе Claude Code нет. Из России нужны VPN и оплата зарубежной картой, варианты в разборе оплата Claude из России. Строка установки для Терминала:

curl -fsSL https://claude.ai/install.sh | bash

Первая команда пишет command not found: brew. Значит, на Mac нет Homebrew, установщика программ. Он ставится одной строкой с его сайта. Вставь её в то же окно, введи пароль от Mac, дождись конца и повтори первую команду статьи:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

У тебя Windows. Скилл писался и прогонялся на Mac, на Windows я его не запускал. Сам Claude Code на Windows ставится, как в разборе Claude Code на Windows, но команды установки ffmpeg и распознавания там другие, и помощник ищет ffmpeg через Homebrew. Здесь проще попросить Claude переписать шаг 0 под Windows и проверить на коротком ролике

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

Хочешь только нарезку без субтитров и эффектов. Так и скажи: «смонтируй скиллом montazher, только паузы и дубли, без субтитров, хука и зумов». Claude пропустит план и соберёт чистую нарезку

Что ломается и что делать

Здесь то, что случилось на моём прогоне, с текстом с экрана:

Что видишьПочему такЧто сделать
НЕТ ffmpeg-full. Установи: brew install ffmpeg-fullстоит обычный ffmpeg, а он субтитры не умеетвыполнить brew install ffmpeg-full
после установки обычный ffmpeg пишет Library not loaded: /opt/homebrew/opt/x265/lib/libx265.216.dylibустановка обновила общие детали, на которые опирается старый ffmpegbrew upgrade ffmpeg, у меня это починило за минуту
НЕТ модели распознаваниямодель не скачалась или лежит в другой папкеповторить строку curl из начала статьи, модель должна лежать в ~/whisper-models
в /skills нет montazherфайлы легли не в ту папку или Claude Code не перезапускалиспросить Claude «где лежит скилл montazher» и перезапустить Claude Code
в ролике срезаны начала словпорог тишины слишком строгий для твоей записив файле стиля silence_threshold 0.35 вместо 0.45
паузы осталисьзапись шумная, фон громкийsilence_threshold 0.55, noise_floor_db -20
название сервиса в субтитрах с ошибкойраспознавание не знает редких названийнаписать Claude правильное название, он поправит и пересоберёт
субтитры не тем шрифтомшрифт Inter не установленbrew install --cask font-inter или другой шрифт в font

Таблица прокручивается вбок →

Источники

Памятка: скилл-монтажёр в Claude Code

Всё, что понадобится со второго ролика, в одном месте

Сохрани себе

  • Программы (один раз): brew install ffmpeg-full whisper-cpp, brew install --cask font-inter, модель ggml-large-v3-turbo-q5_0.bin в папку ~/whisper-models
  • Скилл лежит в ~/.claude/skills/montazher: SKILL.md и style.json, помощник montazh.py Claude создаёт сам
  • Запуск: «Смонтируй скиллом montazher ролик <путь к видео>, музыка <путь к треку>»
  • Результат: <имя>-gotovo.mp4 рядом с исходником, 1080 на 1920, отчёт в чате
  • Не тот дубль оставлен: «верни кусок 7, убери кусок 9, пересобери»
  • Субтитры крупнее или мельче: size в блоке subtitles, по умолчанию 84
  • Музыка громче или тише: quieter_than_voice_db, по умолчанию 18
  • Срезает начала слов: silence_threshold 0.35. Оставляет паузы: 0.55
  • Обычный ffmpeg сломался после установки: brew upgrade ffmpeg

Монтаж собирается одной фразой

Ролики стали быстрыми. Теперь им нужно куда-то вести

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

Забрать путёвку в ИИ-Лагерь →

Кнопка ведёт в мой закрытый канал. Жмёшь «Подать заявку», впускаю сразу, бот пишет в личку и отдаёт путёвку

Частые вопросы

Нужен ли CapCut или Premiere?

Нет. Монтирует ffmpeg, бесплатная программа для работы с видео, а решения по смыслу принимает Claude. Готовый файл открывается в любом плеере и сразу выкладывается

Сколько стоит такой монтаж?

Программы монтажа и распознавания бесплатные и работают на твоём компьютере. Платный только Claude Code: подписка Pro стоит 20 долларов в месяц или 17 при оплате за год, монтаж расходует её лимит, отдельной платы за ролик нет

Видео куда-то загружается?

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

Можно поменять стиль субтитров?

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

Подойдёт ли горизонтальное видео?

Скилл собирает вертикальный ролик 1080 на 1920, поэтому у горизонтального видео обрежет края. Claude предупредит об этом до сборки

Читать дальше

Я соло-предприниматель на Claude, Claude Code и агентах

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

Читать канал

Открытый канал, подписка в одно нажатие