Увидел в ТГ видеокружок — винил-пластинку с аудио, и захотел также. И сделал своего бота

в 18:57, , рубрики: ffmpeg, telegrambot
  • Реальная история полета мысли и рождения продукта

  • Примеры создания бота с нуля

  • Готовый скрипт для рендера кружочков с музыкой (ну почти)

  • Готовый бот с неприлично простым функционалом: t.me/Wjooh_bot

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

Мой фаворит в видео редакторах YouCut, который обычно позволяет создать кат по любой ситуативной идеи, и быстро, в этот раз немного подвел: всё что можно от него добиться это использовать эффект вращения с одной заданной скоростью, а эта скорость задана весьма выше чем у вайба винила.

=(

Второй кочкой, на которой споткнулся полет моей идеи, стала сама механика видеокружочков. Юзер не может загрузить их с устройства — только записать.. Тут я вспомнил что разрабатываю ботов на Python, и в сущностях сообщений явно видел поле VideoNote — видеокружок. И стало ясно что загружать их из галереи нельзя только в реализации приложения — само API телеграмма естественно не против.

Для надежности загуглил «как делают видео-сообщения из обычных видео», и нашел кучу примеров зреющей идеи — телеграмм боты на всяко-разных условиях предлагают сделать кружочек из любого вашего видео. А раз такое делает бот, почему бы боту и не закручивать его, и не накладывать аудио самому? Вот и ни почему.

Поехали

Начинаем с центральной функции — монтажа и рендера.

  • Входные данные — картинка и аудио файл.

  • На выходе нужно mp4 видео с вращающейся картинкой под музыку.

Сразу как то интуитивно было, что вращение проще оформить на этапе подготовки картинки. Использовать самый банальный инструмент, встроенный в Python — PIL Image, создать раскадровку будущего видео и сохранить на диске.

Делаем цикл, оставляем на перспективу множитель скорости(им также можно менять направление вращения знаком +-), поворачиваем картинку на шаг*скорость, и сохраняем в массив кадров

def rotate_set(f_imgpath, f_speed,f_id):
    f_step = int(360 / f_speed)
    f_res = []
    # умножаем на минус потому что интуитивнее когда плюс крутит по часовой
    f_speed = -f_speed
    f_img = Image.open(f_imgpath)
    for i in range(0, f_step):
        q_img = f_img.rotate(i * f_speed)
        f_res.append(q_img)

    return f_res

Сразу скажу, получится урод. Надо сначала сделать из картинки квадрат: считаем точки краев картинки и получаем новую

def crop_img(f_imgpath):
    img = Image.open(f_imgpath)
    f_size = min(img.size)Готовый набор картинок собираем в контейнер видео-либы, накладываем аудио и го рендерить на старом офисном ноуте это дело адски долгое.
    f_crop_size = (max(img.size))

    f_dif = int((f_crop_size - f_size) / 2)
    if img.height >= img.width:
        f_crop_img = img.crop((0,f_dif,img.width,img.height - f_dif))
    else:
        f_crop_img = img.crop((f_dif, 0, img.width - f_dif,img.height))

    return f_crop_img

Готовый набор картинок собираем в контейнер видео-либы, накладываем аудио и го рендерить на старом офисном ноуте это дело адски долгое.

def spin_imag(f_len=59, f_speed=2, f_img='low.jpg'):
  j = 0
  clips = []
  f_img_obj = crop_img(f_img)
  f_frames = rotate_set(f_img_obj, f_speed)
  for i in range(0, f_len * 24):
    # тут гоняем массив кадров полного вращения f_frames
    # пока не получим массив на всю длинну видео f_len * 24
    clips.append(ImageSequenceClip(f_frames[j]))
    j += 1
    if j >= len(f_frames):
      j = 0

  result_clip = concatenate_videoclips(clips, method="compose")
  audio_clip = AudioFileClip(f_audio)
  result_clip.audio = new_audioclip
  f_result_file = f'{s_files_path}.mp4'
  result_clip.write_videofile(f_result_file,
                              fps=24,
                              )

  return f_result_file

Сразу тестим на VPS с убунтой и 300mb ОЗУ: Процесс убивается еще на закручивании картинок

:D

:D

Оптимизируем

Ладно, если не торопиться, то во первых надо делать входные картинки одного небольшого размера, всё-таки в кружочке нет приоритета на ХайРес, а видео рендерится по размеру большего из слоев. Заодно внимательнее следим за закрытием ненужных файлов/потоков



def crop_img(f_imgpath):
  ...
  else:
    f_crop_img = img.crop((f_dif, 0, img.width - f_dif, img.height))

  f_crop_img = f_crop_img.resize((s_img_size, s_img_size))
  img.close()
  return f_crop_img

Из любопытства глядим на нагрузку системы

Увидел в ТГ видеокружок — винил-пластинку с аудио, и захотел также. И сделал своего бота - 2

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

# сохраняем каждый кадр в файл, возвращаем путь к файлу
def rotate(f_img,f_angle,f_result_path):
    f_res_path = f_result_path
    rotate_img = f_img.rotate(f_angle)
    rotate_img.save(f_result_path)
    return f_res_path

def rotate_set(f_img, f_speed, f_id):
  ...
  for i in range(0, f_step):
    q_img = rotate(f_img, i * f_speed, f'{s_work_dir}{i}_rotate.jpg')
    f_res.append(q_img)

  return f_res

def spin_image(f_id=0, f_len=59, f_speed=2, f_img='low.jpg'):
  ...
  # теперь здесь мы получаем массив адресов файлов
  f_frames = rotate_set(f_img_obj, f_speed, f_id)
  f_img_obj.close()
  for i in range(0, f_len * 24):
    # гоняем массив адресов файлов так же как раньше картинки
    clips.append(f_frames[j])
    j += 1
    if j >= len(f_frames):
      j = 0
  # этот объект будет загружать кадры из файлов только когда они
  # потребуются на рендере
  result_clip = ImageSequenceClip(clips, fps=24)
  ...

Ощутимая оптимизации. Надо больше читать доки

Где то в это время под тест попадет кейс когда входное аудио короче минуты, и хочется эту ситуацию быстро пусть топорно решить.

Применяем вуду-программирование:

def spin_image(f_id=0, f_len=59, f_speed=2, f_img='low.jpg'):
  ...
  audio_clip = AudioFileClip(f_audio)
  if audio_clip.duration < result_clip.duration:
    f_count = int(result_clip.duration / audio_clip.duration) + 1

    f_clip_list = []
    for i in range(0, f_count):
      f_clip_list.append(audio_clip.copy().set_start(audio_clip.duration * i))

    f_clip_list[f_count - 1] = f_clip_list[f_count - 1].copy().set_duration(
      result_clip.duration - ((f_count - 1) * audio_clip.duration))
    audio_clip = f_clip_list
  else:
    audio_clip = [audio_clip.set_duration(result_clip.duration)]
  new_audioclip = CompositeAudioClip(audio_clip)
  ...

С божей помощью оно работает с первого раза, едем дальше.

Увидел в ТГ видеокружок — винил-пластинку с аудио, и захотел также. И сделал своего бота - 3

Друг (ссылка: Разработка кроссплатформенных приложений, интерактивных экскурсий, презентаций, AR, VR, MR для выставок, музеев, рекламы и веба) советует для оптимизации потыкаться в кодеки рендера и битрейт, так что мы снижаем битрейт аудио до 100k, меняем кодек на новомодный h264 видео до 200k (дефолт выдавал 400, дефолт х254 вобще 4500! sic), врубаем режим оптимизации ultrafast, и все ради бедной убунты-300-озу, дай ей бог сил.

def spin_image(f_id=0, f_len=59, f_speed=2, f_img='low.jpg'):
  ...
  result_clip.write_videofile(f_result_file,
                              fps=24,
                              codec='libx264',
                              preset='ultrafast',
                              bitrate='200k'
                              audio_bitrate='100k')
  ...

Убунта справляется. Но такое качество даже в кружочке неприемлемо

Если использовать как посоветовал мой друг (ссылка: Разработка кроссплатформенных приложений, интерактивных экскурсий, презентаций, AR, VR, MR для выставок, музеев, рекламы и веба) кодек h265, который ещё более современный и оптимизированный, то результирующее видео вообще не воспроизводится на мобильном ТГ, на десктопе выглядит выразительно (скорее всего проблема в моем невежестве, но тратить на это время не целесообразно, ИДЕЯ ГОРИТ)

Увидел в ТГ видеокружок — винил-пластинку с аудио, и захотел также. И сделал своего бота - 4

Молодец друг, если что он занимается Разработка кроссплатформенных приложений, интерактивных экскурсий, презентаций, AR, VR, MR для выставок, музеев, рекламы и веба и вот его профиль ссылка

Дальше начинаем подбирать разрешение и битрейт что бы железо тянуло, картинка удовлетворяла и самое интересное — какие критерии для видео существуют что бы ТГ его сделал кружочком. Дело в том что загрузка VideoNote работает загадочным образом, она отправляет на сервер ТГ любое твое видео, но они там его каким то образом оценивают и решают — выдать в чат каноничный кружок, или если что то не понравилось — вкинуть его как обычное видео. В документации написаны требования, только забыли упомянуть разрешение:

  • Разрешение видео не более 640р

  • Расширение файла .mp4

  • Длинна не более минуты

  • Квадратное

Кружок начинает выглядеть прекрасно уже на 600к битрейте

def get_mask(f_name, f_size=s_img_size):
  f_path = f'{f_name}{f_size}.png'
  if not os.path.exists(f_path):
    with Image.open(f_name) as og:
      with og.resize((f_size, f_size)) as rs:
        rs.save(f_path)
  return f_path

def spin_image():
  ...
  result_clip = ImageSequenceClip(clips,fps=24)

  # сначала загружаем пнг как обычный кадр
  logo = ImageClip(get_mask(mask.png,f_img_size),duration=result_clip.duration)
  # потом хитро загружаем его же но фильтром маской, и накладываем на предыдущий
  f_mask = ImageClip(get_mask(mask.png,f_img_size),ismask=True).to_mask()
  logo = logo.set_mask(f_mask)
  result_clip = CompositeVideoClip([result_clip,logo])
  ...

Конечный код скрипта доступен на https://github.com/Yellastro2/image_spin_and_music/blob/main/spin_video_sc.py

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

Короче немного магии Aiogram, про него контента итак хватает, придумал социальный элемент — показывать юзерам чужие кружочки! и оценивать! и формировать топ!!! Добавил выбор маски-пластинки и запустил на копеечном VPS

Увидел в ТГ видеокружок — винил-пластинку с аудио, и захотел также. И сделал своего бота - 5

Я назвал своего бота Вжух

t.me/Wjooh_bot

Автор: Yellastro2

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js