Узнайте, как создать open-source редактор видео с ИИ на Python. VibeClip режет длинные видео в shorts с субтитрами через чат. Попробуйте сами!
Представьте: у вас папка с длинными видео — записи подкастов, лекции, скринкасты. Вы хотите превратить их в короткие вертикальные клипы с субтитрами за минуты, а не часы. Существующие инструменты либо заставляют вас вручную таскать клипы по таймлайну, либо требуют загружать сырые файлы в облако и платить за каждый экспорт. Я столкнулся с этой проблемой и решил её, создав VibeClip — open-source инструмент, который позволяет редактировать видео, просто описывая желаемые изменения в чате. В этой статье я расскажу, как он устроен, и покажу ключевые архитектурные решения на Python.
VibeClip состоит из трёх частей, каждая из которых работает локально (кроме LLM). Это гарантирует приватность ваших данных и отсутствие привязки к облачным сервисам.
Всё начинается с точной покадровой транскрипции. Мы используем faster-whisper — локальную модель, которая не требует API-ключа и выдаёт временные метки для каждого слова. Эти метки — основа для синхронизации субтитров и обнаружения тишины.
from faster_whisper import WhisperModel
# Загружаем модель (можно выбрать размер: tiny, base, small, medium, large)
model = WhisperModel("base", device="cpu", compute_type="int8")
# Транскрибируем видеофайл
segments, info = model.transcribe("input_video.mp4", beam_size=5)
# Выводим текст с временными метками
for segment in segments:
print(f"[{segment.start:.2f}s - {segment.end:.2f}s] {segment.text}")
for word in segment.words:
print(f" Слово: {word.word}, время: {word.start:.2f}s - {word.end:.2f}s")Этот код запускается один раз для каждого видео. Результат — список слов с метками, который мы сохраняем в JSON. На его основе LLM будет принимать решения о нарезке.
Единственный компонент, который обращается к сети, — это языковая модель. Вы подключаете свой ключ (OpenAI, Gemini, Claude, DeepSeek или локальный Ollama). LLM выполняет две задачи:
Пример вызова LLM через OpenAI API:
import openai
# Устанавливаем ключ из переменной окружения
openai.api_key = os.getenv("OPENAI_API_KEY")
def analyze_transcript(transcript_json):
# Отправляем транскрипт модели для оценки
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system", "content": "Ты — редактор видео. Оценивай транскрипт на силу моментов (1-10). Выдай JSON: [{'start': float, 'end': float, 'score': int, 'reason': str}]."},
{"role": "user", "content": f"Транскрипт: {transcript_json}"}
],
response_format={"type": "json_object"}
)
return response.choices[0].message.content
# Пример вызова
scored_moments = analyze_transcript(transcript_data)
print(scored_moments)Важно: мы отправляем только текст транскрипта, а не само видео. Это сохраняет приватность.
Все операции — нарезка, переформатирование в 9:16, наложение субтитров, зум и изменение громкости — выполняются через ffmpeg локально. Никаких облачных ферм рендеринга.
import subprocess
import json
def render_clip(input_file, start, end, output_file, captions):
# Формируем команду ffmpeg для нарезки и добавления субтитров
cmd = [
"ffmpeg",
"-i", input_file,
"-ss", str(start),
"-to", str(end),
"-vf", f"crop=ih*9/16:ih,scale=1080:1920,drawtext=text='{captions}':fontsize=24:fontcolor=white:x=(w-text_w)/2:y=h-th-100",
"-c:a", "aac",
output_file
]
subprocess.run(cmd, check=True)
# Пример: клип с 10 по 20 секунду с субтитрами
render_clip("input.mp4", 10, 20, "clip_1.mp4", "Привет, это тестовый клип")Чтобы избежать повторной обработки при отмене, мы используем кэширование промежуточных результатов. Каждое изменение — это снимок состояния, который можно откатить.
Метки времени от Whisper точны, но не идеальны. Когда мы вырезаем тишину, оригинальные временные метки сбиваются. Решение: сделать транскрипт единственным источником истины, а видео — производным от него. То есть мы сначала определяем, какие слова оставить, а потом генерируем видео, которое точно соответствует этим словам.
def align_captions_after_silence_removal(original_words, silence_intervals):
"""Корректируем временные метки слов после удаления тишины."""
adjusted_words = []
total_silence_removed = 0
for word in original_words:
# Вычитаем всю тишину, которая была до этого слова
for interval in silence_intervals:
if interval['end'] <= word['start']:
total_silence_removed += interval['end'] - interval['start']
adjusted_words.append({
'text': word['text'],
'start': word['start'] - total_silence_removed,
'end': word['end'] - total_silence_removed
})
return adjusted_wordsПроблема: OpenAI использует response_format={"type": "json_object"}, но другие провайдеры (например, Ollama) игнорируют этот параметр или выдают ошибку. Решение: отправлять параметр только для поддерживаемых провайдеров и толерантно парсить JSON из ответа, даже если он обёрнут в markdown-блок.
def parse_llm_response(response_text):
"""Извлекаем JSON из ответа LLM, даже если он обёрнут в ```json ... ```."""
import re
# Ищем блок кода с JSON
match = re.search(r'```(?:json)?\n(.*?)\n```', response_text, re.DOTALL)
if match:
json_str = match.group(1)
else:
json_str = response_text
return json.loads(json_str)VibeClip — это рабочий open-source инструмент, который вы можете запустить одной командой docker compose up или через uv/pip. Вам нужен только ключ от LLM. Попробуйте его для своих видео:
pip install -r requirements.txt.env с вашим API-ключомpython main.pyЕсли вы создаёте короткий контент или интересуетесь AI-инструментами, это отличная возможность внести свой вклад. Присылайте PR и issues — проект ещё сырой, и ваша обратная связь очень важна.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →