I saw a video circle in TG – a vinyl record with audio, and I wanted one too. And made my own bot

  • The real story of the flight of thought and the birth of the product

  • Examples of creating a bot from scratch

  • Ready-made script for rendering circles with music (well, almost)

  • A ready-made bot with indecently simple functionality: t.me/Wjooh_bot

In one Telegram channel with memes I saw such a video note, and this idea struck me so much that I immediately wanted to make the same one. And generally do it regularly depending on your mood, when you come across a suitable picture or want to hook a friend on a top song. I started feeling out my pocket resources. Pocket – because doing according to your mood means doing from your phone

My favorite video editor is YouCut, which usually allows you to create a cut based on any situational idea, and quickly, this time it let me down a little: all you can achieve from it is to use the rotation effect with one set speed, and this speed is set very higher than that of the vibe vinyl.

=(

The second bump on which the flight of my idea stumbled was the mechanics of video circles themselves. The user cannot download them from the device – only write them down.. Then I remembered that I develop bots in Pythonand in the message entities I clearly saw the VideoNote field – a video circle. And it became clear that downloading them from the gallery is impossible only in the implementation of the application – the telegram API itself naturally does not mind.

To be on the safe side, I googled “how video messages are made from regular videos” and found a bunch of examples of a ripening idea – telegram bots, under various conditions, offer to make a circle from any of your videos. And since the bot does this, why shouldn’t the bot twist it and overlay the audio itself? No reason why.

Let's go

We start with the central function – editing and rendering.

  • Input data – picture and audio file.

  • The output is an mp4 video with a rotating image accompanied by music.

It was immediately intuitive that rotation was easier to arrange at the stage of preparing the picture. Use the most commonplace tool built into Python – PIL Image, create a storyboard for the future video and save it to disk.

We make a cycle, leave the speed multiplier for the future (you can also change the direction of rotation with the +- sign), rotate the image by step * speed, and save it to an array of frames

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

Let me tell you right away, it will turn out to be a freak. You must first make a square out of the picture: count the points of the edges of the picture and get a new one

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

We collect the finished set of images into a video-libs container, add audio and render it on an old office laptop – this is a hellishly time-consuming task.

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

We immediately test it on a VPS with Ubuntu and 300mb of RAM: The process is killed even when scrolling the images

:D

😀

We optimize

Okay, if you don’t rush, then firstly you need to make input images of one small size, after all, the circle does not have priority in HighRes, and the video is rendered according to the size of the larger layer. At the same time, we take a closer look at closing unnecessary files/streams



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

Out of curiosity, we look at the system load

After reading the docs a little, we find a method just for gluing pictures of frames into a clip, to which you just need to send the addresses of the files, and it will intelligently download and glue them.

# сохраняем каждый кадр в файл, возвращаем путь к файлу
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)
  ...

Tangible optimization. Need to read more docs

Somewhere around this time, a case will come under test when the input audio is shorter than a minute, and I want to solve this situation quickly, even clumsily.

We use voodoo programming:

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)
  ...

With God's help it works the first time, let's move on.

A friend (link: Development of cross-platform applications, interactive excursions, presentations, AR, VR, MR for exhibitions, museums, advertising and the web) advises to optimize the rendering codecs and bitrate, so we reduce the audio bitrate to 100k, change the codec to a newfangled one h264 video up to 200k (the default output was 400, the default x254 was just 4500! sic), we turned on the ultrafast optimization mode, and all for the sake of poor Ubuntu-300-RAM, God bless it.

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")
  ...

Ubuntu copes. But such quality, even in a circle, is unacceptable

If you use, as my friend advised (link: Development of cross-platform applications, interactive excursions, presentations, AR, VR, MR for exhibitions, museums, advertising and the web), the h265 codec, which is even more modern and optimized, then the resulting video is not played at all on mobile TG, it looks expressive on the desktop (most likely the problem is my ignorance, but it’s not advisable to waste time on this, THE IDEA IS BURNING)

Well done friend, if what he is doing is developing cross-platform applications, interactive excursions, presentations, AR, VR, MR for exhibitions, museums, advertising and the web and here is his profile link

Next, we begin to select the resolution and bitrate so that the hardware can handle it, the picture satisfies, and the most interesting thing is what criteria exist for a video so that the TG would make it a circle. The fact is that the VideoNote download works in a mysterious way, it sends any of your videos to the TG server, but they somehow evaluate it there and decide whether to issue a canonical circle in the chat, or if they don’t like something, throw it in as a regular video. The documentation contains the requirements, but they forgot to mention the permission:

  • Video resolution no more than 640p

  • File extension .mp4

  • Length no more than a minute

  • Square

The circle starts to look great already at 600k bitrate

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])
  ...

The final script code is available at https://github.com/Yellastro2/image_spin_and_music/blob/main/spin_video_sc.py

Until recently, I didn’t believe that no one had ever made such a bot, and recently I found one implementation of such an idea, you can find it by the word vinylizerI ended up looking at the max resolution for the circle. Seeing someone else’s implementation of your secret idea is of course painful, but I immediately decided that my bot would have a feature of simplicity, so chick-hop and the circle is ready. And the competitor’s trick is complexity (:

In short, a little bit of the magic of Aiogram, there is already enough content about it, I came up with a social element – to show users other people’s circles! and evaluate! and form the top!!! Added a selection of plate masks and launched it on a cheap VPS

I named my bot Vzhukh

t.me/Wjooh_bot

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *