How to Lip Sync AI Videos with Audio Using an API In 2026

A Python and JavaScript build of an API lip sync pipeline on fal: voice with Eleven v3, picture with Seedance 2.5, sync with VEED Lipsync v2, plus dubs and queued H3 Max Lip Sync greetings.

John Ozuysal19 min read
How to Lip Sync AI Videos with Audio Using an API In 2026

One fal API key reaches every model in this guide, from Seedance 2.5 and ElevenLabs Eleven v3 to the lip sync endpoints that finish the clips. VEED Lipsync v2 bills $0.07 per second of output, so a 10-second presenter sync costs $0.70, while most of the spend goes to the Seedance 2.5 source clip. The voice is generated first and the clip sized to the measured read, Sync Lipsync 2 Pro handles dubbing through sync_mode, and H3 Max Lip Sync animates one portrait for queued personalized greetings at $0.08 per second of 768p output.

I'll show you how to lip-sync AI videos with audio using an API, and since the request takes just one video URL and one audio URL, most of the work is in making both inputs good before it goes out.

I build the chain on fal, with Seedance 2.5 making the picture and ElevenLabs Eleven v3 the voice before VEED Lipsync v2 syncs one to the other.

TL;DR

  • One fal API key reaches all the models in this guide, from Seedance 2.5 and ElevenLabs Eleven v3 to the lip sync endpoints that finish the clips.
  • VEED Lipsync v2 bills $0.07 per second of output video, which puts a 10-second presenter sync at $0.70.
  • Most of the spend goes to the Seedance 2.5 source clip, about $0.46 per second of 720p video in the aspect ratios used here by our token formula.
  • I generate the voice before the video and size the clip to the measured read, since Seedance 2.5 sets duration in whole seconds from 4 to 30.
  • Sync Lipsync 2 Pro is my pick for dubbing, because its sync_mode setting covers reads that overrun or undershoot the clip.
  • H3 Max Lip Sync handles my short personalized greetings sent through the queue, animating one portrait to a read of at least 5 seconds at $0.08 per second of 768p output.

Where can you lip sync AI videos through an API?

fal is the best place to lip sync AI videos through an API, as the models that make the inputs run behind the same key as the lip sync endpoints that consume them.

From the more than 1,000 hosted models in our catalog, I lead with VEED Lipsync v2 here and bring in Sync Lipsync 2 Pro and H3 Max Lip Sync for dubbing and short batches.

VEED Lipsync v2 and Sync Lipsync 2 Pro take a video_url and an audio_url, H3 Max Lip Sync takes an image_url with the audio_url, and all three hand back the synced video under video.url once the job finishes.

Seedance 2.5 and Eleven v3 return fal CDN URLs as well, which means a file from one model can go straight into the next request with no download in between.

When does a video need a separate lip sync step?

A video needs a separate lip sync step when its words change after the picture exists, as with a translated read or a new hook on a take the client has already approved.

Seedance 2.5 can generate dialogue in the same pass as the picture, provided the spoken lines are wrapped in double quotes as its schema asks.

Whenever the script is locked before the render starts, I'm happy to let that single pass handle the dialogue.

A voice track that exists before the render can also go in as an audio reference on the Seedance 2.5 reference-to-video endpoint, and our Seedance 2.5 page shows performers lip-syncing to a supplied track that way.

Once the picture exists, though, I keep it as it is and send a new audio track through a lip sync endpoint.

The three endpoints here charge between $0.07 and about $0.083 per second, compared with roughly $0.46 per second for a fresh 720p Seedance 2.5 render.

How does an API lip sync pipeline fit together?

When you render a new source clip for a read, the pipeline runs in a fixed order, with the voice track generated first and both URLs sent to the lip sync endpoint last.

It's ideal to start by writing the script so each read stays inside the 4 to 30 seconds one Seedance 2.5 clip can cover.

ElevenLabs Eleven v3 turns that script into an MP3, and ffprobe reads its duration straight from the returned URL.

Dreamina Seedance 2.5 renders a silent 720p clip whose duration is the read length rounded up to a whole second.

VEED Lipsync v2 takes the clip URL with the audio URL and returns the synced MP4.

The voice goes ahead of the picture because Seedance 2.5 takes durations as whole seconds from 4 to 30, while a TTS read can end at any fraction of a second.

Once the read is measured, the code can ask for the shortest duration that still covers it, keeping the most expensive call in the chain as short as possible.

fal documents no duration setting for VEED Lipsync v2, which is why the VEED format in this guide sizes its source clip from a measured read.

Setting up the clients and ffprobe

To run the scripts in this guide, you'll want a fal API key exported as FAL_KEY and FFmpeg's ffprobe installed on the same machine.

Keys are created in the fal dashboard, though requests only run once the account holds credits, starting from a $1 top-up.

bash
export FAL_KEY="your-api-key-here"

pip install fal-client
npm install --save @fal-ai/client

ffprobe comes with FFmpeg and reads the duration of a remote MP3 or MP4 from its URL without downloading the file first.

For apps that call fal from a browser, our proxy setup guide covers keeping FAL_KEY on the server.

What makes a source clip easy to lip sync?

You'll get the cleanest sync from a source clip with a front-facing, well-lit speaker whose mouth stays visible for the whole shot, which is the input our VEED Lipsync v2 page recommends.

Since that page describes the input as footage of someone already talking, my Seedance 2.5 prompts always show the person mid-speech with natural head movement.

The spoken words themselves stay out of those prompts, because the Seedance 2.5 schema uses double quotes to mark dialogue for the model to voice and lip-sync.

Based on my experience, it's cleaner to set generate_audio to false on source clips, as the voice track is the only sound I want in the final cut.

According to our Seedance 2.5 model page, generate_audio has no effect on the token count, so switching it off costs nothing.

720p is the resolution I use for every Seedance 2.5 source clip in this guide.

Hands and props stay below the chin in my prompts to keep the mouth unobstructed, as VEED's guidance recommends.

For wider prompting habits on this model, our Seedance 2.5 workflows guide and Seedance 2.5 prompt examples go deeper into text-to-video and reference-driven prompts.

How do you lip sync an AI video in Python?

In Python, you lip sync an AI video with three fal_client.subscribe() calls in a row.

I'll build them around a 16:9 presenter announcing a feature in a budgeting app.

A talking-head explainer makes a good first build, with a steady, front-facing shot that lets you judge sync quality frame by frame.

Step 1: Voice the script with Eleven v3

The first call sends the script to fal-ai/elevenlabs/tts/eleven-v3 and times the MP3 it returns with ffprobe.

Eleven v3 runs at $0.10 per 1,000 characters on fal, putting this 149-character script at about $0.015.

I went with the Charlotte voice from the schema's example list and left stability at its 0.5 default.

python
import math
import subprocess
import urllib.request

import fal_client

SCRIPT = (
    "Our new budgeting dashboard pulls your cards and accounts into one weekly view. "
    "Open the app this morning and tap Insights to see your first summary."
)


def seconds(url):
    probe = subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", url],
        capture_output=True, text=True, check=True,
    )
    return float(probe.stdout.strip())


voice = fal_client.subscribe(
    "fal-ai/elevenlabs/tts/eleven-v3",
    arguments={"text": SCRIPT, "voice": "Charlotte", "stability": 0.5},
)
audio_url = voice["audio"]["url"]
read_length = seconds(audio_url)
print(audio_url, read_length)

Generated using ElevenLabs Eleven v3 on fal, an AI model from ElevenLabs.

Step 2: Generate a silent presenter clip with Seedance 2.5

Next, the picture renders at 720p in 16:9, with its duration set to the read length rounded up and held within the 4 to 30 seconds Seedance 2.5 accepts.

Seedance 2.5's schema types duration as a string, hence the str() conversion in the request below.

A 10-second clip at 1280×720 works out to 216,000 tokens, or $4.62 at our rate of $0.0214 per 1,000 tokens.

The presenter prompt below is the exact text that went into this request:

Prompt: A woman in her early thirties with shoulder-length dark hair and a rust-colored knit sweater talks directly to the camera in a bright home office, with relaxed head movement and an occasional open-hand gesture below her chin. A large window to her left lights her face evenly, and her mouth stays clear of her hands for the whole shot. Static medium close-up at eye level on a 50mm lens, with a softly blurred bookshelf behind her.

python
PRESENTER = (
    "A woman in her early thirties with shoulder-length dark hair and a rust-colored knit sweater "
    "talks directly to the camera in a bright home office, with relaxed head movement and an "
    "occasional open-hand gesture below her chin. A large window to her left lights her face evenly, "
    "and her mouth stays clear of her hands for the whole shot. Static medium close-up at eye level "
    "on a 50mm lens, with a softly blurred bookshelf behind her."
)

clip = fal_client.subscribe(
    "bytedance/seedance-2.5/text-to-video",
    arguments={
        "prompt": PRESENTER,
        "aspect_ratio": "16:9",
        "resolution": "720p",
        "duration": str(min(30, max(4, math.ceil(read_length)))),
        "generate_audio": False,
    },
)
video_url = clip["video"]["url"]
print(video_url, clip["seed"])

Generated using Dreamina Seedance 2.5 on fal, an AI model from ByteDance.

It's worth storing the seed that Seedance 2.5 returns with the video, so a later re-render can start from the same value.

Step 3: Sync the clip to the voice track with VEED Lipsync v2

The last call passes both URLs to veed/lipsync/v2 with no other fields required, then saves the synced MP4 to disk.

For a 10-second result, VEED Lipsync v2 adds $0.70 to the run at its rate of $0.07 per second of output video.

python
synced = fal_client.subscribe(
    "veed/lipsync/v2",
    arguments={"video_url": video_url, "audio_url": audio_url},
)
urllib.request.urlretrieve(synced["video"]["url"], "presenter-en.mp4")
print(synced["video"]["url"])

Generated using VEED Lipsync v2 on fal, an AI model from VEED.

I recheck single syncs in the VEED Lipsync v2 playground, which takes the same two URLs.

falMODEL APIs

The fastest, cheapest and most reliable way to run genAI models. 1 API, 100s of models

falSERVERLESS

Scale custom models and apps to thousands of GPUs instantly

falCOMPUTE

A fully controlled GPU cloud for enterprise AI training + research

What changes when you dub a clip into another language?

When you dub a clip, the translated read can overrun or fall short of the original line, so the lip sync request needs an instruction for the gap.

Sync Lipsync 2 Pro takes that instruction through sync_mode, with cut_off as the default and loop, bounce, silence and remap as the alternatives.

In Sync Labs' own documentation, cut_off trims the output to whichever input is shorter, while remap changes the video's playback speed until its duration equals the audio's.

For this format I reuse the silent Seedance 2.5 clip from the Python build and voice Spanish and German reads with the same Charlotte voice, setting language_code on each Eleven v3 request.

The script times both reads against the clip, choosing remap when a read runs past it and cut_off when the read finishes first.

Sync Labs also warns that large mismatches can make remap noticeable.

My rule of thumb is to render a longer source clip once a read overshoots by more than a second or two.

python
import subprocess

import fal_client

SOURCE_CLIP = "https://v3b.fal.media/files/<path>.mp4"  # the silent presenter clip from Step 2

DUBS = {
    "es": (
        "Nuestro nuevo panel de presupuesto reúne tus tarjetas y cuentas en una sola vista semanal. "
        "Abre la app esta mañana y toca Insights para ver tu primer resumen."
    ),
    "de": (
        "Unser neues Budget-Dashboard bündelt deine Karten und Konten in einer Wochenansicht. "
        "Öffne heute Morgen die App und tippe auf Insights, um deine erste Zusammenfassung zu sehen."
    ),
}


def seconds(url):
    probe = subprocess.run(
        ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", url],
        capture_output=True, text=True, check=True,
    )
    return float(probe.stdout.strip())


clip_length = seconds(SOURCE_CLIP)

for lang, text in DUBS.items():
    voice = fal_client.subscribe(
        "fal-ai/elevenlabs/tts/eleven-v3",
        arguments={"text": text, "voice": "Charlotte", "language_code": lang},
    )
    audio_url = voice["audio"]["url"]
    mode = "remap" if seconds(audio_url) > clip_length else "cut_off"

    dubbed = fal_client.subscribe(
        "fal-ai/sync-lipsync/v2/pro",
        arguments={"video_url": SOURCE_CLIP, "audio_url": audio_url, "sync_mode": mode},
    )
    print(lang, mode, dubbed["video"]["url"])

Generated using Sync Lipsync 2 Pro on fal, an AI model from Sync Labs.

Generated using Sync Lipsync 2 Pro on fal, an AI model from Sync Labs.

The pair of 10-second dubs costs about $1.67 on Sync Lipsync 2 Pro, which bills $5 per minute on fal.

If you'd prefer not to write translations yourself, ElevenLabs Dubbing on fal translates an existing audio track into languages that include Spanish and German, at $0.90 per minute.

Batching personalized greetings with H3 Max Lip Sync and webhooks

For batches of short personalized clips, I submit the lip sync jobs to the queue with a webhook URL and let a small Express handler record the finished videos as they land.

The format here is a 5-second square welcome clip for a coffee subscription, in which a roaster greets new members by first name.

H3 Max Lip Sync fits this job well, as it animates a single portrait to a supplied audio file and returns a video that runs as long as the audio.

Its schema accepts an image with an aspect ratio between 0.4 and 2.5 and audio from 5 seconds up to 15 minutes, with output from 480p to 2K.

Our H3 Max Lip Sync page prices 768p output at $0.08 per second, so a 5-second greeting costs $0.40 to sync.

A greeting of about a dozen words reads in four to five seconds, which is why the submit script pads each read with silence up to the 5-second minimum.

Longer reads pass through untouched and simply make a longer greeting, billed by the second.

I generate the roaster once with H3 Max text-to-video and keep its first frame as the portrait.

Seedance 2.5's reference-to-video endpoint declined photoreal portraits as image or video references in my tests, so the portrait goes to H3 Max Lip Sync.

The roaster came from the prompt below, which I sent at 5 seconds, 768p and 1:1 with prompt expansion switched off so the model used the text as written:

Prompt: Square video of a man in his forties with a trimmed grey beard and a navy canvas apron, standing behind the counter of a small coffee roastery and talking warmly to the camera. He keeps facing the lens while he speaks, with one small nod partway through and shelves of coffee bags softly blurred behind him. Static camera at chest height under warm light from pendant lamps.

H3 Max text-to-video bills 768p at $0.052 per second until October 31 and $0.08 per second after that, so the clip costs about $0.26, or $0.40 at the standard rate.

The script saves the first frame with ffmpeg and uploads it to fal storage, so one portrait URL serves every greeting:

javascript
// make-roaster.mjs
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import { fal } from "@fal-ai/client";

const ROASTER = [
  "Square video of a man in his forties with a trimmed grey beard and a navy canvas apron,",
  "standing behind the counter of a small coffee roastery and talking warmly to the camera.",
  "He keeps facing the lens while he speaks, with one small nod partway through",
  "and shelves of coffee bags softly blurred behind him.",
  "Static camera at chest height under warm light from pendant lamps.",
].join(" ");

const { data: clip } = await fal.subscribe("minimax/h3-max/text-to-video", {
  input: {
    prompt: ROASTER,
    prompt_expansion_mode: "disabled",
    aspect_ratio: "1:1",
    resolution: "768P",
    duration: 5,
  },
});

// H3 Max Lip Sync animates a still image, so the clip's first frame becomes the portrait.
execFileSync("ffmpeg", ["-v", "error", "-y", "-i", clip.video.url, "-frames:v", "1", "roaster.png"]);
const portraitUrl = await fal.storage.upload(new Blob([await readFile("roaster.png")], { type: "image/png" }));

console.log(clip.video.url);
console.log(portraitUrl);

Generated using MiniMax H3 Max on fal, an AI model from MiniMax.

On every pass, the submit script voices one greeting with Eleven v3, pads the read to five seconds and queues its H3 Max Lip Sync job, and the request_id that comes back goes onto the member's record:

javascript
// submit-greetings.mjs
import { execFileSync } from "node:child_process";
import { readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fal } from "@fal-ai/client";
import { db } from "./db.js"; // your own data layer

const PORTRAIT_URL = "https://v3b.fal.media/files/<path>.png"; // the roaster frame from make-roaster.mjs
const WEBHOOK_URL = "https://roastery.example.com/api/fal/webhook";

const members = [
  { id: "m_1042", firstName: "Priya" },
  { id: "m_1043", firstName: "Mateo" },
  { id: "m_1044", firstName: "Hana" },
  { id: "m_1045", firstName: "Jonas" },
];

// H3 Max Lip Sync needs at least 5 seconds of audio, so shorter reads get trailing silence.
async function padToFiveSeconds(audioUrl, name) {
  const file = join(tmpdir(), `${name}-5s.mp3`);
  execFileSync("ffmpeg", ["-v", "error", "-y", "-i", audioUrl, "-af", "apad=whole_dur=5", file]);
  const url = await fal.storage.upload(new Blob([await readFile(file)], { type: "audio/mpeg" }));
  await rm(file);
  return url;
}

for (const member of members) {
  const { data: voice } = await fal.subscribe("fal-ai/elevenlabs/tts/eleven-v3", {
    input: {
      text: `Hi ${member.firstName}, welcome to the coffee club. Your first bag ships tomorrow.`,
      voice: "Daniel",
    },
  });
  const audioUrl = await padToFiveSeconds(voice.audio.url, member.id);

  const { request_id } = await fal.queue.submit("minimax/h3-max/lip-sync/image-to-video", {
    input: { image_url: PORTRAIT_URL, audio_url: audioUrl, resolution: "768P", enable_transcription: true },
    webhookUrl: WEBHOOK_URL,
  });

  await db.greetings.insert({ requestId: request_id, memberId: member.id, status: "queued" });
}

I switch on enable_transcription, which has the model transcribe the read to guide the mouth shapes.

The handler below checks the signature on each delivery before writing the result onto the matching row:

javascript
// server.mjs
import express from "express";
import { db } from "./db.js";
import { verifyWebhookSignature } from "./verify-webhook.js";

const app = express();

app.post("/api/fal/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const valid = await verifyWebhookSignature(
    req.get("x-fal-webhook-request-id"),
    req.get("x-fal-webhook-user-id"),
    req.get("x-fal-webhook-timestamp"),
    req.get("x-fal-webhook-signature"),
    req.body,
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  const update =
    event.status === "OK" && event.payload
      ? { status: "ready", videoUrl: event.payload.video.url }
      : { status: "failed", error: event.error ?? event.payload_error };

  await db.greetings.update({ requestId: event.request_id }, update);
  res.sendStatus(200);
});

app.listen(3000);

Generated using H3 Max Lip Sync on fal, an AI model from MiniMax.

verifyWebhookSignature follows the steps in our webhooks documentation, using Node's built-in Ed25519 support so the handler needs no extra packages:

javascript
// verify-webhook.js
import { createHash, createPublicKey, verify } from "node:crypto";

// Implements the verification steps in fal's webhooks documentation with Node's built-in Ed25519 support.
const JWKS_URL = "https://rest.fal.ai/.well-known/jwks.json";
const JWKS_TTL_MS = 24 * 60 * 60 * 1000; // fal's keys can change, so never cache them longer than 24 hours

let cachedKeys = null;
let cachedAt = 0;

async function falPublicKeys() {
  if (!cachedKeys || Date.now() - cachedAt > JWKS_TTL_MS) {
    const res = await fetch(JWKS_URL, { signal: AbortSignal.timeout(10_000) });
    if (!res.ok) throw new Error(`JWKS fetch failed: ${res.status}`);
    const { keys = [] } = await res.json();
    cachedKeys = keys
      .filter((key) => typeof key.x === "string")
      // fal pads x; Node's JWK import expects unpadded base64url, so re-encode it
      .map((key) => Buffer.from(key.x, "base64url").toString("base64url"))
      .map((x) => createPublicKey({ key: { kty: "OKP", crv: "Ed25519", x }, format: "jwk" }));
    cachedAt = Date.now();
  }
  return cachedKeys;
}

export async function verifyWebhookSignature(requestId, userId, timestamp, signatureHex, body) {
  if (!requestId || !userId || !timestamp || !signatureHex) return false;

  // Reject deliveries more than 5 minutes from now to block replays.
  const sentAt = Number(timestamp);
  if (!Number.isFinite(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > 300) return false;

  const bodyHash = createHash("sha256").update(body).digest("hex");
  const message = Buffer.from([requestId, userId, timestamp, bodyHash].join("\n"), "utf8");
  const signature = Buffer.from(signatureHex, "hex");

  const keys = await falPublicKeys();
  return keys.some((key) => {
    try {
      return verify(null, message, key, signature);
    } catch {
      return false;
    }
  });
}

The signature covers the raw body, which is why express.raw sits on this route and the JSON is parsed only once the signature passes.

fal retries a delivery that fails or returns an error with growing backoff, for up to about an hour after the job completes, or about six minutes for results of 10 KB or more.

With the update keyed on request_id, a repeat delivery writes the same values to the same row.

Redirects aren't followed, so the webhook URL should be the final public HTTPS address.

If a delivery never lands, the finished job can still be read from the queue while fal keeps the result, which is what this fallback does for rows still marked queued:

javascript
// recover-greetings.mjs
import { readFile } from "node:fs/promises";
import { fal } from "@fal-ai/client";
import { db } from "./db.js";

// Fallback for deliveries that never arrived: read finished jobs straight from the queue.
const rows = JSON.parse(await readFile(new URL("./greetings.json", import.meta.url), "utf8"));

for (const row of rows.filter((r) => r.status === "queued")) {
  const endpoint = "minimax/h3-max/lip-sync/image-to-video";
  const { status } = await fal.queue.status(endpoint, { requestId: row.requestId });
  if (status !== "COMPLETED") continue;

  const { data } = await fal.queue.result(endpoint, { requestId: row.requestId });
  await db.greetings.update({ requestId: row.requestId }, { status: "ready", videoUrl: data.video.url });
}

All four greetings cost $1.60 to sync on H3 Max Lip Sync at 5 seconds apiece, and the four Eleven v3 reads add about $0.03.

With the $0.26 portrait clip, the whole batch comes to about $1.89.

How many of those jobs run at once depends on the concurrency limits on your account, and the rest wait in the queue.

How much does an API lip sync pipeline cost on fal?

A 10-second lip-synced presenter clip costs about $5.34 on fal as of October 2026, at the rates on our model pages and pricing page.

Of that total, $4.62 goes to the Seedance 2.5 source clip and $0.70 to VEED Lipsync v2.

The Eleven v3 read adds about $0.015 on top, at $0.10 per 1,000 characters.

Seedance 2.5 bills $0.0214 per 1,000 tokens at 480p and 720p, with tokens counted as height × width × seconds × 24 ÷ 1,024.

By that formula, 1280×720 comes to 21,600 tokens per second.

Each lip sync rate is listed in the section for the format that uses it.

My total for the three formats in this guide is $8.93 at the planned lengths, assuming the reads fit inside their clips and VEED Lipsync v2 bills on the full clip length:

FormatEndpointsSettingsCost
Presenter clip (Python)Eleven v3, Seedance 2.5, VEED Lipsync v216:9, 720p, 10 s$5.34
Spanish and German dubs (Python)Eleven v3 ×2, Sync Lipsync 2 Pro ×2Reuses the 10 s presenter clip$1.70
Four member greetings (JavaScript)H3 Max text-to-video, Eleven v3 ×4, H3 Max Lip Sync ×41:1, 768p, 5 s$1.89

What breaks a lip sync pipeline on fal?

Most of what breaks a lip sync pipeline on fal is an input outside a limit on the model page, along with a few source-clip choices that weaken the sync without raising an error.

I've grouped those checks below by the pipeline stage where each input is made.

StageWhat you seeLikely causeFix
Voice trackH3 Max Lip Sync rejects the audioThe read runs under 5 secondsTrailing silence up to 5 seconds, as the submit script adds
Source clipThe source clip carries its own soundtrackgenerate_audio stayed at its default of truegenerate_audio set to false
Source clipMouth movement looks off in part of the synced clipThe face turns away or a hand covers the mouth in the sourceA new source with a front-facing, evenly lit speaker
Lip syncH3 Max Lip Sync rejects the portraitThe image's aspect ratio falls outside 0.4 to 2.5A portrait cropped to a ratio inside that range
Lip syncA dub ends before the line finishessync_mode stayed at cut_off with a read longer than the clipremap, or a longer source clip
Lip syncA model can't fetch an input fileThe host blocks automated requests or the link needs a loginA fal CDN URL for the same file
DeliveryWebhook deliveries never arriveThe webhookUrl redirects or resolves to a private IPThe final public HTTPS address

Validation and content errors that models return are documented in our model errors reference.

Recently Added

Build your lip sync pipeline on fal

In the presenter format, which renders a clip for a known read, the voice came first and the picture was sized to fit it.

The dubs reuse an existing clip and the greetings share one portrait, so those formats rely on sync_mode and a padded read.

On fal, the whole chain runs behind one API key and bills per use, with nothing for you to host between the steps.

For the same chain without code, fal Agent takes the brief in a chat and lays out the model runs as a plan you approve.

If you'd like to audition voices beyond Eleven v3, our ranking of the best AI voice generators compares the other text-to-speech models on fal side by side.

Video models that could stand in for Seedance 2.5 as the source are ranked in our best AI video generators guide.

Check out fal to get started.

Lip sync API FAQ

Can VEED Lipsync v2 sync a recorded voiceover?

Yes, VEED Lipsync v2 handles a recorded voiceover the same way as a TTS track, as its audio_url accepts any spoken audio in MP3, OGG, WAV, M4A or AAC format.

For a local recording, I upload the file first with fal_client.upload_file() (or fal.storage.upload() from JavaScript) and pass the CDN URL it returns as audio_url.

How is lip syncing a video different from turning a photo into a talking video?

When you lip sync a video, the footage keeps its motion and only the speech and mouth movement change, while a photo-to-talking-video model generates all of the motion from one still image.

For photo-based jobs, our VEED Lipsync v2 page points to VEED Fabric 1.0, which takes an image with an audio file and returns a talking video.

Does lip sync work on animated or illustrated characters?

VEED Lipsync v2 works with animated characters as well as real footage, according to our model page, provided the character faces the camera with the mouth clearly visible.

Our blog post announcing Sync Labs' Lipsync 2.0 on fal also lists animated and AI-generated characters among the inputs that model handles.

Can you chain a lip sync pipeline into one fal endpoint?

A lip sync pipeline can run as a single endpoint through fal Workflows, which pass one input through several models in sequence.

I script it myself whenever the clip length depends on the measured read, since my own code then handles that duration check.

How long can a lip-synced clip be?

A single Seedance 2.5 source clip in this pipeline runs up to 30 seconds, whereas H3 Max Lip Sync takes audio from 5 seconds up to 15 minutes and returns a video as long as the audio.

Neither our VEED Lipsync v2 page nor our Sync Lipsync 2 Pro page lists a duration cap.

Before building a batch of longer pieces, I'd test one full-length run in the playground.

About the author
John Ozuysal

Founder of House of Growth. 2x entrepreneur, 1x exit, mentor at 500, Plug and Play, and Techstars.

Build with generative media on fal

Hundreds of production-ready image, video, and audio models behind one API.