From Two Character Portraits to a Web-Ready Animation: An AI Video + FFmpeg Workflow
An eight-second video can be surprisingly awkward to publish. The generated clip looks usable, but the editor expects an image. Converting it to a GIF solves that problemβuntil the GIF becomes larger than the original v
An eight-second video can be surprisingly awkward to publish.
The generated clip looks usable, but the editor expects an image. Converting it to a GIF solves that problemβuntil the GIF becomes larger than the original video.
This walkthrough follows one small project: turning portraits of Sherlock Holmes and Dr. Watson into a short dance clip, then preparing different versions for publishing.
The workflow has two stages:
- Generate the character video through a web interface.
- Inspect and process the downloaded MP4 with FFmpeg.
The useful part for developers is the second stage: understanding what was exported, choosing a delivery format, and controlling the trade-off between animation quality and file size.
The source material
The inputs were two separate character portraits with a consistent visual style.
Holmes wears a deerstalker hat and holds a magnifying glass. Watson wears a dark hat and carries a walking stick. Both images use muted clothing, warm lighting, and a Victorian London setting.
Input reference: Sherlock Holmes.
Input reference: Dr. Watson.
The resulting clip places the characters in a modern dance sequence. Holmes appears in the foreground first; Watson later takes the foreground position. Their gestures include raised arms and crouching movements.
A silent GIF preview of the generated eight-second dance clip.
That contrast makes the example easy to understand: recognizable character designs performing an unexpected action.
The video was generated using VisualGPTβs Viral Studio, with the two portraits supplied as character references. The commands below begin with the downloaded file, named cp-video.mp4.
1. Inspect the downloaded video
Before converting anything, inspect the media:
ffprobe -v error \
-show_entries \
format=duration,size:stream=codec_type,codec_name,width,height,avg_frame_rate \
-of json \
cp-video.mp4
For this file, the relevant properties were:
| Property | Value |
|---|---|
| Video codec | H.264 |
| Dimensions | 480 Γ 854 |
| Video frame rate | 30 fps |
| Container duration | Approximately 8.03 seconds |
| Audio codec | AAC |
| File size | 6,523,280 bytes |
This establishes a baseline. Every derivative can now be compared against a known source.
It also separates two questions: whether the video is visually usable, and whether its encoding suits the destination.
2. Check the character replacement
A recognizable first frame is only one part of the review.
For a two-character video, I would check these moments:
- When both characters are clearly visible.
- When one passes behind the other.
- When hands move across faces.
- When the foreground position changes.
- When a character crouches or changes pose quickly.
Sampled frames at approximately 0, 2, 4 and 6 seconds. This montage illustrates the sequence; it does not establish frame-by-frame consistency.
Sampled frames from this clip show both characters performing the sequence and changing their positions in the composition. Hats, clothing, and facial features provide useful visual cues for distinguishing them.
That is a limited visual check. It does not establish consistent identity across every frame or across other templates. A production review would examine the transitions in motion as well.
Keeping that distinction clear makes a case study more useful: readers can see what was checked and what remains to be tested.
3. Make an initial GIF
The first GIF used a target frame rate of 12 fps and a width of 400 pixels:
ffmpeg -i cp-video.mp4 \
-filter_complex \
"[0:v]fps=12,scale=400:-1:flags=lanczos,split[a][b]; \
[a]palettegen=stats_mode=diff[p]; \
[b][p]paletteuse=dither=sierra2_4a" \
-loop 0 \
cp-preview.gif
The filter chain:
- Samples the video at the requested frame rate.
- Resizes it while preserving its aspect ratio.
- Splits the stream into two branches.
- Generates a palette from one branch and applies it to the other.
FFmpeg documents the palette options in its filter reference.
The resulting file was 16,017,169 bytes, or about 16.02 MB.
That was substantially larger than the 6.52 MB MP4 source, even though it had fewer frames and smaller dimensions.
For this clip, converting to GIF increased the delivery cost.
4. Produce a smaller publishing version
The next export changed several settings together:
- Width: 400 β 280 pixels.
- Target frame rate: 12 β 8 fps.
- Palette color limit: 96.
- Dithering: Bayer.
ffmpeg -i cp-video.mp4 \
-filter_complex \
"[0:v]fps=8,scale=280:-1:flags=lanczos,split[a][b]; \
[a]palettegen=max_colors=96:stats_mode=diff[p]; \
[b][p]paletteuse=dither=bayer:bayer_scale=3" \
-loop 0 \
cp-note.gif
Here are the actual outputs:
| File | Dimensions | Frame count or source frame rate | Size, decimal MB |
|---|---|---|---|
| Source MP4 | 480 Γ 854 | 30 fps video | 6.52 |
| Initial GIF | 400 Γ 712 | 96 frames | 16.02 |
| Smaller GIF | 280 Γ 498 | 64 frames | 4.28 |
Both GIFs retain approximately eight seconds of animation. Neither contains audio.
Measured outputs from this example. Dimensions, frame rate and palette settings changed together; this is not an equal-quality codec benchmark.
The smaller GIF is approximately 73% smaller than the initial GIF.
Several parameters changed, so this result cannot be attributed to dithering alone. It is the combined outcome of reducing dimensions, frame count, and palette complexity.
The trade-off is visible detail and motion smoothness. A 280-pixel-wide preview can serve a compact article illustration, but it needs inspection at the size readers will actually see.
These measurements describe this particular clip. They are a starting point for another export, rather than a universal compression recipe.
5. Keep the MP4 for pages you control
For a website where you control the markup, the original MP4 remains useful. It retains the source resolution, frame rate, and audio.
First, extract a poster image:
ffmpeg -ss 0.5 \
-i cp-video.mp4 \
-frames:v 1 \
poster.jpg
Then place the poster and video alongside your page:
<figure>
<video
controls
playsinline
preload="metadata"
poster="./poster.jpg"
width="480"
height="854"
style="display: block; max-width: 100%; height: auto;"
>
<source src="./cp-video.mp4" type="video/mp4">
<a href="./cp-video.mp4">Download the video</a>
</video>
<figcaption>
An eight-second AI-generated dance clip featuring
Sherlock Holmes and Dr. Watson.
</figcaption>
</figure>
This example gives the viewer playback controls and avoids automatic playback.
The poster supplies a preview image. playsinline requests playback within the page, while preload="metadata" gives the browser a loading preference rather than a strict instruction. See the MDN video element reference for the attribute behavior.
This markup is for a page you control. Publishing platforms may expose different embedding options.
6. Choose the export for its destination
The project ended with three useful assets:
- MP4: the source video, including audio.
- GIF: a smaller, silent animation for image-based publishing.
- Poster: a static preview for the video player.
Keeping these separate makes later publishing easier. The character-generation step does not need to be repeated just because another destination needs a different format.
There are also two independent review tasks:
Content review: Are the characters recognizable throughout the action? Do fast movements or overlaps introduce distracting artifacts?
Delivery review: Does the file load, display at the intended size, and preserve the parts of the experience that matter?
A successful upload answers only part of the second question.
What I would measure next
A follow-up experiment could change one export setting at a time:
- Hold dimensions and palette settings constant while changing frame rate.
- Hold frame rate and palette settings constant while changing width.
- Compare dithering modes at identical dimensions and frame rates.
That would make it possible to understand which changes provide the most useful savings for this footage.
For this first pass, the practical finding was straightforward: an eight-second source MP4 produced a 16.02 MB GIF, and a smaller export configuration brought the GIF down to 4.28 MB.
The generation created the scene. Inspection and format preparation made it usable for publishing.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.



