Вайбмаркетинг · монтаж рилс · сентябрь 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, он создаст папку и положит их туда
- Набери
claudeв том же окне, где ставил программы, чтобы запустить Claude Code - Скопируй сообщение ниже целиком, кнопкой копирования в углу блока, вставь в Claude Code и отправь
- 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.md | Claude по твоему сообщению | описание, по которому Claude узнаёт задачу «смонтируй ролик», пять шагов монтажа и частые поломки |
| style.json | Claude по твоему сообщению | твой стиль: шрифт, размер и цвет субтитров, хук, зумы, громкость музыки |
| montazh.py | Claude сам при первом монтаже | помощник для механической работы, его текст лежит в конце SKILL.md, поэтому копировать нужно два файла, а не три |
Таблица прокручивается вбок →
Проверка, что скилл на месте: закрой Claude Code, открой снова и набери /skills. В списке должна быть строка montazher. Если скилл тебе вообще в новинку, как он устроен изнутри, я разбирал в статье как сделать свой скилл в Claude
Смонтируй первый ролик: одна фраза
Сними ролик на телефон вертикально, как снимаешь обычно, со всеми паузами и повторами, и перекинь файл на Mac, например в папку «Фильмы». Музыку положи рядом, если она нужна. Потом напиши в Claude Code:
Смонтируй скиллом montazher ролик ~/Movies/rolik.mov, музыка ~/Movies/muzyka.mp3
Путь к файлу руками набирать не нужно. Напечатай фразу до слова «ролик», потом перетащи видео из Finder прямо в окно Терминала: путь вставится сам. У ролика с iPhone имя будет вроде IMG_1234.MOV, это нормально
Дальше Claude идёт по шагам сам, ты только разрешаешь ему запускать команды:
- Находит паузы по громкости и распознаёт речь. На экране появляется список кусков речи с номерами и текстом
- Сам решает, какие куски лишние. Если одна фраза сказана три раза, оставляет последний полный дубль, оговорки выкидывает
- Режет видео, чистит шум, выравнивает громкость и заново распознаёт уже смонтированную речь
- Исправляет ошибки распознавания в названиях и пишет план: текст хука, надписи и моменты для зумов
- Собирает ролик, открывает четыре контрольных кадра и сам проверяет, читаются ли субтитры
Что было на моём прогоне. Запись на 131,8 секунды с четырьмя дублями: одну фразу я начинал трижды, ещё две сказал по два раза. Claude убрал все четыре неудачных дубля и оставил последние полные, поправил название, которое распознавание услышало как «NVIDIA», и написал хук «5 сайтов: из текста готовый ролик». Итог 45,3 секунды, от фразы до готового файла 71-73 секунды в четырёх прогонах подряд на Mac с процессором M4 Max. На ноутбуке послабее будет дольше, сколько именно, я не замерял
Как понять, что всё получилось
Успех видно по трём признакам, проверь все три:
- Рядом с исходником лежит файл с
-gotovo.mp4в конце имени и папка с-montazhв конце: в ней черновики монтажа, их можно не открывать - В чате Claude отчёт: сколько секунд было и стало, какие дубли убраны (номер и текст), какой хук и какие надписи стоят
- Открой ролик двойным щелчком. Первые 2,5 секунды сверху хук, внизу субтитры меняются вместе со словами, названия сервисов всплывают надписью ровно тогда, когда ты их произносишь, музыка тише голоса
Если в отчёте 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: путь к треку по умолчанию |
Таблица прокручивается вбок →
Про стиль субтитров. На карусели был стиль 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 | установка обновила общие детали, на которые опирается старый ffmpeg | brew 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 |
Таблица прокручивается вбок →
Источники
- Extend Claude with skills, документация Claude Code - где лежит SKILL.md, переменная папки скилла, список /skills
- Set up Claude Code - установка одной строкой, нужна подписка Pro, Max, Team или Enterprise
- Тарифы Claude - Pro 20 долларов в месяц или 17 при оплате за год, Claude Code в бесплатном тарифе нет
- Homebrew - команда установки Homebrew
- Формула ffmpeg-full в Homebrew - ffmpeg с библиотекой субтитров libass, ставится отдельно от обычного
- Формула whisper.cpp в Homebrew - распознавание речи на своём компьютере, прежнее имя whisper-cpp
- whisper.cpp на GitHub - программа распознавания и её модели
- Модели whisper.cpp на Hugging Face - файл ggml-large-v3-turbo-q5_0.bin
- Фильтры ffmpeg - шумоподавление afftdn, громкость loudnorm, зумы zoompan, субтитры subtitles, приглушение музыки sidechaincompress
- Шрифт Inter в Homebrew - установка шрифта субтитров одной командой
Памятка: скилл-монтажёр в 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 и агентах
Всё, что не влезает в статью, лежит у меня в канале: свои скиллы, которые я ставлю в работу, раньше, чем о них снимают ролики, и внутрянка моих вайб-маркетинговых проектов
Открытый канал, подписка в одно нажатие