DEV Community

Sherwood
Sherwood

Posted on Fully Autonomous

Turn a list of images or prompts into short videos with one API call (no GPU, no API key)

Making one short AI video is easy: open a web playground, drop an image, type what should move, wait a minute, download the mp4. Making forty of them from inside a pipeline is a different job. The typical cases: a product catalog where every photo should become a five-second clip for a listing or an ad, a set of illustrations that need to move for slides or a blog, stills from a scraper that you want animated for social posts, or first-to-last transitions between two keyframes.

What you want there is boring: a list of images or prompts in, a list of mp4 URLs out, the real duration of each clip (because that is what you pay for), and a clear signal for the ones that failed. This post compares the usual ways to batch-generate video, then shows how to do it with one API call from curl, Python, JavaScript, n8n and an AI agent.

Why batch video generation gets annoying

Self-hosting an open-weight video model. Full control and no per-clip fee, in exchange for a large GPU, minutes of compute per clip, a queue, and a way to serve files that weigh several megabytes each. It pays off at high, steady volume, and not before.

Calling an inference API (fal, Replicate, and the like) directly. This is the closest you get to the model, and per second of video it is the cheapest hosted option. But video is slow, so every provider puts you on a queue: your pipeline now owns an API key, a submit-then-poll loop or a webhook endpoint, a retry policy, a place to store the output, and the bookkeeping of which clip belongs to which input. You are also billed for clips that come back broken or refused.

The vendor APIs of the big video models. Good models, but each one means another account, another key, another billing relationship per account rather than per run. For a no-code tool or an agent, "go create an account and wire a secret" is where the automation dies.

Looping over a single-clip endpoint. Most hosted generators take one image or one prompt per call. Forty inputs means forty calls, your own concurrency control, and your own matching of outputs to inputs.

The pattern that removes all of this: one call that takes the whole list, runs the model, stores the files, measures each clip and returns one record per clip with its URL. That is what an Apify Actor does, and the rest of this post uses one I maintain: Image to Video API (MiniMax Hailuo, h3-max-turbo, image to video and text to video).

A batch video generator behind one API call

Step 1: get a token

Create a free Apify account and copy your API token from Settings → Integrations. There is no video-model key to manage: the model runs inside the Actor.

Step 2: the input

The input is a JSON object. Everything is optional; with no input at all the Actor generates one sample clip so you can see the output shape before sending your own list.

{
  "imageUrls": ["https://example.com/product.jpg"],
  "prompts": ["Slow 360 rotation of the product, studio lighting"],
  "duration": 5,
  "resolution": "768P"
}
Enter fullscreen mode Exit fullscreen mode
  • imageUrls — the first frame of each clip, one clip per image. Public URLs, Google Drive or Dropbox share links, Drive file IDs, base64 or data URIs all work; a newline- or comma-separated string works too. Aliases: imageUrl, urls, images, startUrls, imageFiles (uploads from the console). Images must be at least 256×256 pixels.
  • prompts — what happens in each clip, in English for best results. Alone, you get one 16:9 text-to-video clip per prompt. With imageUrls, prompts and images are paired by index, and a list of one is reused for every image: "slow 360 rotation, studio lighting" across a whole catalog is one call. Without a matching prompt, a neutral motion prompt is used. Aliases: prompt, texts.
  • endImageUrls — pins the last frame of each clip. With imageUrls, that is a first-to-last keyframe transition; alone, a clip that ends on the given image.
  • datasetId — animate the image URLs found in another Actor's dataset (an image generator, a product scraper). Common image fields are detected; urlField picks one explicitly, for example images[].url.
  • duration — 1 to 15 seconds per clip, default 5.
  • resolution — 768P (default), 1080P or 480P for drafts. All three cost the same per second.
  • promptExpansion — balanced (default), disabled to use your prompt verbatim, quality for a richer rewrite that takes longer.
  • seed — fix it to reproduce a run. It applies to every clip of the run, not per clip.
  • maxVideos — hard cap per run, default 20, up to 200, so a malformed list cannot surprise you.

Step 3: call it with curl

curl -s -X POST \
  "https://api.apify.com/v2/acts/sherwood~video-generator-batch/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompts": ["A paper plane gliding over pastel mountains, flat illustration"]}'
Enter fullscreen mode Exit fullscreen mode

The call blocks until the run finishes and returns the dataset items directly. Video is slow, so keep this synchronous form for one or two clips: Apify's run-sync endpoints give up after 300 seconds. For a real batch, start the run and read the dataset when it is done, which is what the official clients do for you below.

Step 4: Python

from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")

run = client.actor("sherwood/video-generator-batch").call(run_input={
    "imageUrls": [
        "https://example.com/product-1.jpg",
        "https://example.com/product-2.jpg",
    ],
    "prompts": ["Slow 360 rotation of the product, studio lighting"],
    "duration": 5,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] == "ok":
        print(item["index"], item["durationSeconds"], "s ->", item["outputUrl"])
    else:
        print(item["index"], "FAILED:", item["error"])
Enter fullscreen mode Exit fullscreen mode

call() waits for the run to finish, however long the batch takes, then you read the dataset.

Step 5: JavaScript

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('sherwood/video-generator-batch').call({
    prompts: [
        'A paper plane gliding over pastel mountains, flat illustration',
        'Steam rising from a ceramic coffee mug, soft morning light',
    ],
    resolution: '1080P',
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
    console.log(item.index, item.status, item.outputUrl ?? item.error);
}
Enter fullscreen mode Exit fullscreen mode

What comes back

One dataset item per clip, in input order:

{
  "index": 0,
  "prompt": "Slow 360 rotation of the product, studio lighting",
  "expandedPrompt": "Cinematic, vintage film-style portrait. The scene opens on <Picture 1>, a sepia-toned photograph of a woman [...] the camera begins a slow, smooth 360-degree arc shot [...]",
  "imageUrl": "https://api.apify.com/v2/key-value-stores/MnVYERh4Bm47trebR/records/face-21",
  "endImageUrl": null,
  "outputUrl": "https://api.apify.com/v2/key-value-stores/WyquiA5e9IdYCpLbn/records/video-0000.mp4?signature=...",
  "durationSeconds": 5.184,
  "billedSeconds": 5,
  "resolution": "768P",
  "fileSize": 4541105,
  "seed": null,
  "model": "minimax/h3-max-turbo/image-to-video",
  "format": "mp4",
  "durationMs": 6618,
  "status": "ok",
  "error": null
}
Enter fullscreen mode Exit fullscreen mode

(Real output of a run made for this post: a 1900s studio portrait animated with the prompt above. expandedPrompt is the model's rewrite of your prompt, shortened here; it is useful to see what the model actually understood.)

index is the position of the input in your list, so matching clips back to your own rows is a sort, not a guess. durationSeconds is read from the delivered file, not assumed: the model produces slightly more than you asked for (5.184 s for a 5 s request, on every clip I have measured), and billedSeconds tells you what that clip actually cost, rounded to the nearest second. The image-to-video output keeps the aspect ratio of your image (a portrait photo gave a 768×1152 clip).

Failed clips come back as items, and are not charged

A dead image link, an image under 256×256, a prompt the safety checker refuses, or an upstream error all produce an item with "status": "error" and the reason in error. It costs nothing, and nothing is silently dropped: you always get as many answers as inputs. The safety checker is always on; this Actor is not a way around model content rules.

Using it from n8n

The wiring is the same as for any Actor: the official Apify node (@apify/n8n-nodes-apify, install "Apify" from the community nodes if you don't have it), resource Actors, operation "Run an Actor and Get Dataset", Actor sherwood~video-generator-batch, the JSON above as the input. The node waits for the run and hands you the dataset items. Upstream, a Code node collects the image URLs of your sheet or scraper output into one list; downstream, the dataset items come back one per clip, and a Google Sheets node writes outputUrl, durationSeconds, billedSeconds and status next to each row. Because the whole list goes in one Actor call, the workflow runs once per batch, not once per image.

The honest comparison: you can also call the inference provider straight from n8n with an HTTP Request node. It works, but video generation is queued, so you end up building submit, Wait, poll-the-status-URL, loop, download, and storing a provider key in n8n credentials, for each workflow that needs clips. The Actor is that loop, packaged, with the failures filtered out of your bill.

Using it from an AI agent (MCP)

Add the Apify MCP server to your agent and it can call the Actor like any tool: the input above, as arguments; the dataset items, as the result. Nothing in the input is required, so "animate these three images" maps onto one call. An agent that just ran an image generator or a product scraper can pass that run's datasetId and animate the outputs without copying URLs around.

The Actor is also eligible for agentic payments (x402, Skyfire, MPP): an agent can run it and pay per request without an Apify account at all.

Pricing at the time of writing

  • Video second — one event per second of delivered video, rounded to the nearest second: from $0.05 to $0.06 per second depending on your Apify plan.
  • A 5-second clip therefore costs $0.25 to $0.30. All resolutions, including 1080P, cost the same.
  • Failed or safety-flagged clips: free. No subscription, no minimum.

Forty five-second product clips cost $10 to $12. Set maxTotalChargeUsd on the run and the Actor stops cleanly at the limit, reporting which clips remain instead of failing. Apify's free plan includes $5 of platform credit each month, enough for roughly 100 seconds of video to test your motion prompts before a batch, no card required.

On price alone, calling the model at an inference provider yourself is cheaper per second (about a third of the Actor's price at the time of writing): the Actor's price covers the provider's cost plus a margin. What the margin buys is not holding a key, not writing the polling loop, not paying for the clips that failed, and getting the measured duration of every file back in one dataset.

Limits

  • The model is MiniMax Hailuo (h3-max-turbo), run via fal.ai. If your brief needs the look of a specific other video model, this is not that.
  • Clips top out at 15 seconds and 1080P. Each mp4 carries a generated audio track (ambience and music suggested by the prompt expansion); there is no control over it beyond the prompt, and no voice-over.
  • Input images must be at least 256×256 pixels; smaller ones come back as a free error item. Upscale them first if needed.
  • seed is per run, not per clip; maxVideos tops out at 200.
  • Prompts work best in English.
  • Output files live in the run's key-value store and follow your Apify account's data retention; copy them out if you need them long-term.

When I wouldn't use it

For one clip now and then, a free web playground is simpler. For a style that depends on a particular video model, or for fine control over the generation parameters, a direct inference API gives you what this Actor deliberately hides. And at very high, steady volume, thousands of clips a month, every month, a rented GPU and an open-weight model beat any per-second price. Everywhere between those: a list in, a list of mp4s out, one call.

Top comments (0)