Your 4K AI video isn't TikTok-ready. Convert it to 1080x1920

ShengShu launched Vidu Q4 Preview on October 7, 2026 with 4K output, 10-bit color on the 2K and 4K tiers, up to 15 image references, 3 audio references, and API access. If you wired it into a pipeline this week, the file coming back is a 4K 10-bit MP4 with model-generated audio, and TikTok neither wants that nor fixes it for you. You get a rejection, a mushy re-compress, or a clip that looks grey on the feed.
Quick answer: To convert 4K video for TikTok, downscale to 1080x1920, convert 10-bit to 8-bit SDR H.264, crop or pad to 9:16 rather than stretching, cap video bitrate near 10 Mbps, and normalize audio to about -14 LUFS with a -1 dBTP ceiling. In FFmpeg that is-vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,format=yuv420p" -c:v libx264 -crf 20 -maxrate 10M -bufsize 20M -c:a aac -b:a 192k, plus a tone-map stage first ifffprobereports a PQ or HLG transfer. If you'd rather not host an encoder or babysit a filter graph inside a workflow timeout, send the clip to FFmpeg Micro as one API call. The free tier is 200 tokens, about 33 minutes of video, no card.
The grey, washed-out look is a color transfer problem, not a bit depth problem
Conventional wisdom on the forums is that 10-bit to 8-bit is a one-flag fix: add -pix_fmt yuv420p with libx264 or you end up with a 10-bit H.264 file most phones refuse to play. That advice is right about the failure the VideoHelp thread describes.
The flag truncates bit depth. It does not touch transfer characteristics or primaries. When the source is tagged PQ, you hand the decoder 8-bit values that still claim HDR intent, and you get the flat, desaturated frame people blame on "quality loss." The correct conversion goes to linear light, tone maps, then returns to bt709 before dropping to 8-bit, which the 32blog HDR-to-SDR writeup lays out in full.
This matters more for generated video than for camera footage, because you don't know what you got. EachLabs' Vidu Q4 Preview breakdown notes that Vidu hasn't published the export codec or color space behind the 10-bit figure, and lists launch pricing from $0.014 per second for 1 to 16 second clips at 540p through 4K. Branch on what the file says, not the model card.
Read the file before you encode it
One ffprobe call tells you which encode path you need:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,pix_fmt,color_transfer,color_primaries,r_frame_rate \
-of default=noprint_wrappers=1 generated.mp4
If color_transfer comes back smpte2084 or arib-std-b67, the clip is PQ or HLG and needs tone mapping. If it comes back bt709 or unknown with pix_fmt=yuv420p10le, you only need a dithered depth reduction. Probe the audio stream too (-select_streams a:0), because some generations come back with no audio track at all, and loudnorm on a stream that doesn't exist fails the whole job.
The finishing command for a 1080x1920 upload
For a 10-bit bt709 source, one pass does the scale, the reframe, the depth reduction, the bitrate cap, and the audio:
ffmpeg -i generated.mp4 \
-vf "scale=1080:1920:force_original_aspect_ratio=increase:flags=lanczos,\
crop=1080:1920,zscale=t=bt709:m=bt709:r=tv:dither=error_diffusion,format=yuv420p" \
-c:v libx264 -preset slow -crf 20 -maxrate 10M -bufsize 20M \
-profile:v high -level 4.1 -pix_fmt yuv420p -movflags +faststart \
-af "loudnorm=I=-14:TP=-1:LRA=11" -c:a aac -b:a 192k -ar 48000 \
tiktok_1080x1920.mp4
The zscale stage is there for the dither. AI-generated clips are full of smooth skies, skin, and studio gradients, which is where plain truncation produces visible banding; dither=error_diffusion costs nothing and hides it. Drop that stage if your output looks clean without it.
For a PQ-tagged source, put the tone map in front of everything else:
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,\
tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv:dither=error_diffusion,\
scale=1080:1920:force_original_aspect_ratio=increase:flags=lanczos,crop=1080:1920,format=yuv420p"
Check that your build has zimg first (ffmpeg -filters | grep zscale). Plenty of distro and Docker builds ship without it, and that's the most common reason this chain dies with an unknown filter error.
Crop or pad decides what you lose
A 4K generation at 3840x2160 is 16:9, the opposite of 9:16, so something has to give. Cropping to 1080x1920 keeps the center 1215 pixels of a 3840-wide frame and throws away about two thirds of the width, which is fine for a centered subject and ruinous for a two-person shot. Padding keeps the whole frame and gives you a 1080x608 strip with bars above and below, which reads as repurposed footage. The ClickyApps guide covers the mechanics: increase plus crop for one, decrease plus pad for the other, and filter order matters.
The cheapest fix is upstream. Vidu Q4's resolution parameter accepts 540p through 4K and defaults to 720p, so generate vertical when the platform is vertical. For footage you already have, Resize for Format is the one-click version of this decision: pick 9:16, 1:1, 4:5, or 16:9, pick pad or crop, get the file. We wrote up the multi-output case in one product video, four cuts.
Cap the bitrate instead of letting the platform guess
Platforms re-encode whatever you upload, so all you control is the quality of what they re-encode. A 60 Mbps 4K file is worse than a correctly specced 10 Mbps one, because you've spent the upload on data the platform discards.
| Target | Resolution | Video bitrate | Audio |
|---|---|---|---|
| TikTok | 1080x1920 H.264 | 5 to 10 Mbps | AAC-LC 128 to 192 kbps |
| Instagram Reels | 1080x1920 H.264 | 8 to 12 Mbps, 25 Mbps hard ceiling | AAC 48 kHz |
The TikTok window comes from Recurpost's size guide, which also notes upload caps by route: roughly 72 MB on Android, about 287 MB on iOS, up to 4 GB on web. The Reels numbers come from ScreenSnap's Reels guide. -crf 20 -maxrate 10M -bufsize 20M satisfies both: constant quality where the scene is easy, a hard ceiling where it isn't. Our case for encoding to one spec instead of a ladder is in video transcoding isn't a bitrate ladder.
Model-generated audio needs measuring, not guessing
TikTok and Reels both sit around -14 LUFS with roughly -1 dBTP of headroom, so loudnorm=I=-14:TP=-1 is the target. Generated audio arrives anywhere from near-silent dialogue to a music bed slammed at -6, and one-pass loudnorm guesses at the correction as it streams. On 1 to 16 second clips, measuring first is almost free:
ffmpeg -i generated.mp4 -af loudnorm=I=-14:TP=-1:LRA=11:print_format=summary -f null -
Then feed the reported measured_I, measured_TP, measured_LRA, measured_thresh, and target_offset into a second pass with linear=true. Mason's two-pass loudnorm writeup explains why the single pass drifts.
The same pass as one API call, billed per second
Everything above is five minutes of work the first time and a maintenance surface forever: a build with zimg, a box big enough to decode 4K, a queue, and a retry story. Running it inside an automation tool is worse, since n8n 3.0 drops the task runner timeout from five minutes to one, shorter than many real transcodes.
FFmpeg Micro runs the same four calls for every job: request a presigned upload URL (or hand it the URL your generation step returned), submit the processing job with your options, poll until it's done, download the output. No binaries, no servers to run, and the quickstart walks the sequence in the dashboard with a temporary key.
Jobs are billed on the duration of the video processed, not on how long our servers take, metered per second with no rounding up to the minute. One token covers 10 seconds of input, so a 16 second Vidu clip costs 1.6 tokens whether the tone map takes two seconds or twenty. Failed jobs are never billed, unlike a per-minute transcoder charging for every retry in a generation loop. One caveat: the Free plan caps input files at 100 MB, and a 4K 10-bit clip can exceed that, so generate at 1080p or 2K while you're testing. Starter raises the cap to 1,024 MB at $19.99 a month.
The step n8n's own Vidu template leaves out
n8n publishes a Vidu template that takes reference images, calls Vidu, writes a title, and pushes straight to Drive, YouTube, and TikTok. There's no transcode between the generation node and the upload node, so at the 2K and 4K tiers it uploads the exact file this post is about.
The repair is one HTTP Request node. Vidu's API is submit-then-poll: POST the task, read task_id, poll until status is success, then pull the video URL out of the result. Pass that URL into an HTTP Request node pointed at FFmpeg Micro, then a short Wait-plus-poll loop, then download and continue to the upload node. Download immediately rather than treating the generator's URL as storage; xAI's Batch API expires video result URLs after an hour. We wrote the HTTP-node pattern out step by step for n8n 3.0 in send it HTTP, and if you're publishing generated clips, TikTok's AI-generated label is a separate API field you have to set yourself.
Pitfalls that cost people an afternoon
Most broken finishing passes fail one of five ways:
-pix_fmt yuv420pon a PQ-tagged file, which produces the washed-out frame with no error.- A build without zimg, so the
zscaleandtonemapchain dies on an unknown filter. scalewithoutforce_original_aspect_ratio, which squashes the subject instead of reframing it.- An odd output dimension after cropping, which libx264 rejects for yuv420p; use
-2for the computed axis. - No
-movflags +faststart, which doesn't break the upload but does break every preview player before it.
When a single finishing pass is the wrong tool
Hard-coding one generation vendor is the real risk here, not the encode. These endpoints move fast: AceDataCloud shipped Veo and Grok n8n nodes on October 11, 2026, and OpenAI scheduled the Sora API for removal in September. Keep the finishing step vendor-neutral and branch on ffprobe output, so swapping generators changes one node.
If what you need is a timeline with keyframed text, layered graphics, and brand templates a non-technical teammate edits, a template-editor service fits better than a filter graph, and FFmpeg Micro is an API rather than an editor. And if your clip is already 1080x1920 8-bit SDR H.264 under 10 Mbps, don't re-encode it. Upload it.
FAQ
Why does my generated video look grey after converting to 8-bit?
The grey, flat look comes from truncating bit depth on an HDR-tagged source without converting the transfer function. Adding -pix_fmt yuv420p changes depth only, so the 8-bit values still carry PQ metadata and the player renders them flat. Run the zscale-to-linear, tonemap, zscale-back-to-bt709 chain before the format change. Only files whose ffprobe color_transfer is smpte2084 (PQ) or arib-std-b67 (HLG) need it; bt709 or unknown needs a dithered depth reduction to yuv420p.
What bitrate should I use so TikTok doesn't re-compress my video badly?
A 1080x1920 H.264 upload around 10 Mbps with a -maxrate 10M -bufsize 20M cap sits inside TikTok's practical 5 to 10 Mbps window and Instagram Reels' 8 to 12 Mbps window, both of which re-encode anyway. Going higher wastes upload bandwidth without improving what viewers see.
If a generation retries five times, what am I billed for on the finishing step?
You're billed only for the clips that actually got processed, on their input duration, per second with no rounding up to the minute. Four failed generations never reach the finishing step, and a failed FFmpeg Micro job is never billed, so a 16 second clip that finally renders costs 1.6 tokens.
Start with the probe, pick your branch, and keep the finishing pass in one place so the next model you try is a URL change rather than a rewrite. The free tier is 200 tokens with no card, about 100 vertical clips at 16 seconds each.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Run an ONNX Upscaler in FFmpeg 9's DNN Filter Graph
FFmpeg onnx dnn filter in 9.0: the real dnn_processing chain, NCHW float32 model limits, silent CUDA-to-CPU fallback, and what hosting it actually costs.

FFmpeg vsync Is Deprecated. -fps_mode Isn't a Drop-In Swap
FFmpeg vsync deprecated? Map -vsync 0/1/2/drop to -fps_mode, repeat it per output, pin CFR at the right pipeline stage. Plus pre-5.1 static-build fallbacks.

Video Transcoding Isn't a Bitrate Ladder. It's One File at Spec
Video transcoding is demux, decode, filter, re-encode, remux. The pipeline version: one file at platform spec as an FFmpeg command, a REST job, or n8n node.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free