Use Cases & Solutions

Make Product Short Videos Automatically: Photos, AI Voiceover and Subtitles into 9:16 Videos with ffmpeg

6 steps 14 min read 67 views 0
On this page

Short product videos sell on TikTok Shop, Shopee Video, Instagram Reels and YouTube Shorts, but editing one video per product by hand does not scale to a catalogue. This guide turns a folder containing product photos and a short narration script into a finished vertical 9:16 video with an AI voiceover, burned-in subtitles and optional background music, using a small Python script and ffmpeg on your server, and then produces videos for hundreds of products in one batch.

Use only photos, music and fonts you have the rights to use. Label the AI-generated voice according to each platform's rules; TikTok, YouTube and others require disclosure of realistic synthetic content.

Step 1: Prepare the Server

This guide builds on AI voiceover with Kokoro: set up /opt/tts and tts.py as described there first. Then install fonts for Chinese and Japanese subtitles:

apt install -y ffmpeg fonts-noto-cjk
fc-list | grep -i "Noto Sans CJK" | head -3

Step 2: Prepare One Folder per Product

/data/videos/LS-01/
├── 01.jpg          # photos, shown in name order
├── 02.jpg
├── 03.jpg
├── script.txt      # narration, one subtitle line per line
└── music.mp3       # optional background music

Step 3: Create the Video Script

cat > /opt/tts/makevideo.py <<'EOF2'
import os, subprocess, sys
import soundfile as sf

W, H, FPS = 1080, 1920, 30
FONT = os.environ.get("SUB_FONT", "Noto Sans CJK SC")
os.chdir(sys.argv[1])
images = sorted(f for f in os.listdir(".") if f.lower().endswith((".jpg", ".jpeg", ".png", ".webp")))
if not images:
    sys.exit("No photos found in " + os.getcwd())
per = sf.info("script.wav").duration / len(images)       # each photo stays on screen for an equal share

def ffmpeg(*args):
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *args], check=True)

with open("clips.txt", "w") as lst:
    for i, img in enumerate(images):
        clip = f"clip{i:03d}.mp4"                           # photo centred on a blurred copy of itself, 9:16
        ffmpeg("-loop", "1", "-t", f"{per:.3f}", "-i", img, "-filter_complex",
               f"[0:v]split[a][b];[a]scale={W}:{H}:force_original_aspect_ratio=increase,crop={W}:{H},boxblur=20[bg];"
               f"[b]scale={W}:{H}:force_original_aspect_ratio=decrease[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,format=yuv420p",
               "-r", str(FPS), "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", clip)
        lst.write(f"file '{clip}'\n")

style = f"FontName={FONT},FontSize=11,Bold=1,Outline=2,Shadow=0,Alignment=2,MarginV=50"
inputs = ["-f", "concat", "-safe", "0", "-i", "clips.txt", "-i", "script.wav"]
if os.path.exists("music.mp3"):                             # optional background music, quietly under the voice
    inputs += ["-stream_loop", "-1", "-i", "music.mp3"]
    audio = "[2:a]volume=0.15[m];[1:a][m]amix=inputs=2:duration=first,loudnorm=I=-14:TP=-1.5[a]"
else:
    audio = "[1:a]loudnorm=I=-14:TP=-1.5[a]"
ffmpeg(*inputs, "-filter_complex", f"[0:v]subtitles=script.srt:force_style='{style}'[v];{audio}",
       "-map", "[v]", "-map", "[a]", "-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
       "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "160k", "-ar", "48000", "-ac", "2", "-shortest", "-movflags", "+faststart", "video.mp4")
for f in ["clips.txt"] + [f"clip{i:03d}.mp4" for i in range(len(images))]:
    os.remove(f)
print("done:", os.path.abspath("video.mp4"))
EOF2

Each photo stays on screen for an equal share of the voiceover. It is centred on a blurred, full-screen copy of itself, so landscape and portrait photos both fill the vertical frame. The script then joins the clips, adds the voice and, if present, the music at low volume, burns in the subtitles, normalises loudness to about -14 LUFS and writes video.mp4.

Step 4: Make the First Video

cd /data/videos/LS-01
TTS_LANG=z TTS_VOICE=zf_xiaobei /opt/tts/bin/python /opt/tts/tts.py script.txt
/opt/tts/bin/python /opt/tts/makevideo.py /data/videos/LS-01

The result is a 1080x1920, 30 fps H.264 video with AAC audio, the format all major short-video platforms accept. Subtitles sit above the bottom of the frame, clear of most platforms' buttons. For Japanese subtitles, run the second command with SUB_FONT="Noto Sans CJK JP". To change the subtitle size or position, adjust FontSize and MarginV in the style line of the script.

Step 5: Produce a Whole Catalogue

for d in /data/videos/*/; do
  [ -f "$d/video.mp4" ] && continue                    # skip products that are already done
  TTS_LANG=z TTS_VOICE=zf_xiaobei /opt/tts/bin/python /opt/tts/tts.py "$d/script.txt" &&
  /opt/tts/bin/python /opt/tts/makevideo.py "$d" || echo "FAILED: $d"
done

Run long batches with nohup or inside screen so they continue after you log out. In our test, a 16-second video took about five seconds to render; most of the time is video encoding, so a multi-core CPU helps.

Step 6: Check Before You Publish

  1. Watch every video once: wrong pronunciation, typos in subtitles and badly cropped photos are easy to spot.
  2. Check that the music does not drown out the voice; lower volume=0.15 in the script if needed.
  3. Check each platform's current requirements for length, captions and the AI content label before uploading.

FAQ

Can I use short video clips instead of photos?

Yes, but the clips need to be cut to length and scaled the same way, which needs a modified script. A simpler option is to generate the voice and SRT with the voiceover guide and combine them with your clips in a video editor.

How do I make the same video in other languages?

Translate script.txt, then run both commands with a voice for that language. For videos that already exist, see dubbing videos into another language with AI.

Chinese subtitles show empty boxes

The font is missing. Install fonts-noto-cjk as in Step 1 and check that fc-list shows "Noto Sans CJK SC".

Still stuck after following these steps? Open a support ticket and the IMIDC 24/7 technical team will help. Include the server IP, OS version, the folder layout and the full error output so we can pinpoint the issue faster.

Was this answer helpful?

Related Tutorials