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