DEV Community

liveavabot
liveavabot

Posted on

HeyGen + Telegram Video Avatar: Fixing the Format Mismatch

Why HeyGen Output Fails as a Telegram Video Avatar

I've been using HeyGen to generate talking-head videos, and the exports look good. But when I tried to use one as a Telegram video avatar, it failed silently. No error message, the bot just did nothing.

After debugging, the problem is straightforward: HeyGen and Telegram have incompatible format requirements on four separate properties.

Property HeyGen output Telegram avatar spec
Aspect ratio 16:9 or 9:16 1:1 exactly
Duration 10-120s max 10s
Codec H.264 or HEVC H.264 only
Audio stereo none (stream removed)

Telegram's mobile and desktop clients handle format errors differently. Mobile silently rejects the upload. Desktop shows a vague error. Neither tells you which parameter caused the failure.

What Telegram Actually Requires

The video avatar spec is stricter than the official docs suggest. The constraints:

  • MP4 container, H.264 video codec
  • Exactly 800x800 pixels (square)
  • Maximum 10 seconds
  • Maximum 2 MB file size
  • Audio stream must be absent entirely

The aspect ratio constraint is the main trap with HeyGen output. A 16:9 clip scaled to 800x800 without cropping first produces a horizontally-stretched face. The fix is to crop to a square first, then scale.

HEVC (H.265) is the other common failure mode. iPhones record in HEVC by default. HeyGen's higher-quality export presets also use HEVC. Telegram's avatar pipeline only accepts H.264, so HEVC files need a full transcode.

The ffmpeg Command

This handles the full conversion:

ffmpeg -y -i heygen_output.mp4 -t 10 \
  -vf "crop=min(iw\,ih):min(iw\,ih),scale=800:800,fps=30,format=yuv420p" \
  -c:v libx264 -preset medium -b:v 900k \
  -an -movflags +faststart \
  telegram_avatar.mp4
Enter fullscreen mode Exit fullscreen mode

-t 10 trims to the first 10 seconds before any video processing. crop=min(iw,ih):min(iw,ih) takes the largest centered square from the input, which works correctly for both landscape and portrait HeyGen exports. scale=800:800 hits the target resolution. fps=30 normalizes the frame rate. format=yuv420p ensures H.264 compatibility across all players including older Android clients.

-c:v libx264 -preset medium -b:v 900k encodes H.264 at a bitrate that keeps a 10-second clip under 2 MB. At 900k, a 10s clip is about 1.1 MB. -an removes the audio stream entirely rather than just muting it. -movflags +faststart moves the MOOV atom to the front of the file for faster progressive playback in Telegram.

The backslashes before commas in -vf are shell escaping. In a Python subprocess call with a list argument, you pass crop=min(iw,ih):min(iw,ih) directly without them.

Handling This in a Telegram Bot

Here's the aiogram 3 handler pattern:

import asyncio
import tempfile
from pathlib import Path

from aiogram import Bot, Dispatcher, F
from aiogram.types import FSInputFile, Message

dp = Dispatcher()

@dp.message(F.video | F.animation | F.document)
async def handle_video(message: Message, bot: Bot):
    media = message.video or message.animation or message.document
    file_id = media.file_id

    with tempfile.TemporaryDirectory() as tmp:
        in_path = Path(tmp) / "input.mp4"
        out_path = Path(tmp) / "output.mp4"

        await bot.download(file_id, destination=in_path)

        cmd = [
            "ffmpeg", "-y", "-i", str(in_path), "-t", "10",
            "-vf", "crop=min(iw,ih):min(iw,ih),scale=800:800,fps=30,format=yuv420p",
            "-c:v", "libx264", "-preset", "medium", "-b:v", "900k",
            "-an", "-movflags", "+faststart",
            str(out_path),
        ]
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.DEVNULL,
            stderr=asyncio.subprocess.PIPE,
        )
        await proc.communicate()

        if proc.returncode != 0 or not out_path.exists():
            await message.reply("Conversion failed.")
            return

        if out_path.stat().st_size > 2 * 1024 * 1024:
            await message.reply("Output exceeds 2 MB. Try a shorter source.")
            return

        await message.reply_video(
            video=FSInputFile(out_path),
            caption="Ready. Set it as your Telegram video avatar.",
        )
Enter fullscreen mode Exit fullscreen mode

F.video | F.animation | F.document catches all video types. HeyGen exports come through as video or document. iPhone HEVC recordings always arrive as document because Telegram doesn't recognize the codec client-side.

Using create_subprocess_exec with a list avoids shell injection and skips the backslash-escaping that the shell command requires.

How @liveavabot Packages This

I wrapped this pipeline into a Telegram bot: @LiveAvaBot.

Beyond the base ffmpeg command, real usage pushed me to add a few things.

A cropdetect pre-pass removes black bars before the square crop. Some HeyGen exports have letterboxing baked in. Without this step, crop=min(iw,ih) squares the letterboxed frame, not the actual video content.

A codec probe via ffprobe detects HEVC inputs and forces a full transcode. Without it, ffmpeg sometimes passes through a codec stream copy that Telegram rejects.

A retry loop drops the bitrate if the first encode exceeds 2 MB. At 900k, a typical 10s talking-head clip is around 1.1 MB. High-motion clips can push past 2 MB. The retry drops to 600k, which covers most cases.

The bot currently has 511 users, mostly uploading iPhone videos and HeyGen exports.

Edge Cases Worth Knowing

The crop=min(iw,ih):min(iw,ih) formula crops from the center. If the talking head is off-center in a 16:9 frame (common with HeyGen's default camera framing), the crop might cut part of the face. HeyGen's 1:1 export option sidesteps this if your plan includes it.

The 2 MB ceiling is easier to hit than it looks. A 900kbps H.264 stream at 30fps for 10 seconds is about 1.1 MB. That's comfortable for talking-head content but tight for screen recordings or high-motion clips. If you hit the ceiling, drop to 600k or 400k. At 800x800, the quality difference is small.

HeyGen's free tier adds a watermark at a fixed corner. Depending on the aspect ratio, the centered crop might cut it out or leave it in. I didn't add watermark detection since it's a plan decision for the user, not a format problem.

Built by me: @LiveAvaBot.

Top comments (0)