Skip to content

[Inference Providers] image-text-to-video: reference images, videos and audio on fal and wavespeed - #2394

Draft
hanouticelina wants to merge 13 commits into
mainfrom
fal-ai-minimax-h3-reference-to-video
Draft

[Inference Providers] image-text-to-video: reference images, videos and audio on fal and wavespeed#2394
hanouticelina wants to merge 13 commits into
mainfrom
fal-ai-minimax-h3-reference-to-video

Conversation

@hanouticelina

@hanouticelina hanouticelina commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Context

MiniMaxAI/MiniMax-H3 can be driven by more than one image and a prompt: extra subject or style images, motion video clips, an audio track to sync to — each addressed from the prompt by position ("Image 1", "Video 1", "Audio 1"). Today the client can send none of that.

Three providers serve it, with three different vocabularies. This PR wires the two that are live on the Hub; replicate follows separately.

fal wavespeed replicate (follow-up)
structure 6 endpoints (3 + 3 lora) 6 endpoints (3 + 3 lora) one model, all modalities
images / videos / audio reference_image_urls
reference_video_urls
reference_audio_urls
reference_images
reference_videos
reference_audios
reference_image_urls
reference_video_urls
reference_audio_urls
caps 9 / 3 / 3, 12 total 9 / 3 / 3 none published
first frame image_url, separate endpoint separate endpoint first_frame_image, same schema

The task owns the concept

ImageTextToVideoParameters gains three optional fields, typed and generated for every language:

reference_images?: string[];   // "Image 1", "Image 2", …
reference_videos?: string[];   // "Video 1", …
reference_audio?:  string[];   // "Audio 1", …

Callers write those names whoever serves the model — so anything built on the task, the Hub's ImageTextToVideoWidget included, needs no provider-specific code:

await client.imageTextToVideo({
  model: "MiniMaxAI/MiniMax-H3",
  inputs: subjectImage,
  parameters: {
    prompt: "Image 1 moves like Video 1, synced to Audio 1",
    reference_images: [styleFile, "https://example.com/style.png"],
    reference_videos: [motionFile],
    reference_audio:  [voiceFile],
  },
});

The schema types the lists as URLs, which is what reaches the provider; the client additionally accepts Blob, so a browser File goes straight through the way inputs already does.

One mechanism, three configurations

lib/referenceInputs.ts does what every provider shares — read the three parameters, coerce, check counts, inline binary as data URLs, emit under the provider's names. What differs is data:

const FAL_AI_REFERENCES: ReferenceInputsSpec = {
  fields: { images: "reference_image_urls", videos: "reference_video_urls", audio: "reference_audio_urls" },
  maxItems: { images: 9, videos: 3, audio: 3 },
  maxTotal: 12,
  rejectAudioAlone: true,
  encode: (blob, modality) => modality === "audio" ? buildFalAiAudioDataUrl(blob) : dataUrlFromBlob(blob, ),
};

One call, each provider's native payload:

fal-ai     → reference_image_urls: ["data:image/png;base64,AQ==", "https://…/style.png"]
             reference_audio_urls: ["data:audio/x-wav;base64,Aw=="]     ← fal's MIME remap
wavespeed  → reference_images: [...], reference_videos: [...], reference_audios: [...]

Adding replicate later is a ReferenceInputsSpec and a subclass — nothing here changes.

Errors name the task's field, not the provider's, because that is what the caller wrote.

Endpoint shape stays per-provider: both fal and wavespeed split the model across endpoints, and both refuse references an endpoint cannot honour rather than sending keys it will ignore — for wavespeed that also stops a Blob being serialised into the request body as {}.

⚠️ Still needs Hub mappings

Only fal's image-to-video and wavespeed's are registered; no reference-to-video endpoint is, and the router exact-matches the path against the registered providerId:

POST router.huggingface.co/fal-ai/minimax/h3/image-to-video      → 200 IN_QUEUE
POST router.huggingface.co/fal-ai/minimax/h3/reference-to-video  → 400 Model not supported by provider fal-ai

For fal and wavespeed, repointing trades away prompt-only and keyframing — verified live: reference-to-video rejects a call with no reference (At least one reference image, video, or audio must be provided) and has no end_image_url. Worth weighing against the follow-up: replicate's single minimax/h3 schema carries the first frame and the references together, so one mapping there would serve the whole surface with nothing given up.

Also fixed

  • applyLoraWeights — the .../lora endpoints require a loras entry, but only the image tasks built one, so minimax/h3/image-to-video/lora was unreachable. Four copies folded into one helper, now also used by text-to-video.
  • fal queue errors — the queue reports COMPLETED even for a rejected request and the result fetch never checked resultResponse.ok, so a 422 surfaced as ProviderOutputError: expected { video: { url: string } }. That is how the reference requirement above was found. Affects every fal queue task.
  • Removed dropEndpointSegmentOnDirectCalls from image-text-to-video — it assumes the text-only variant sits at the parent path, but minimax/h3 is not an endpoint, so prompt-only direct calls 404'd.

Backward compatibility — checked, not assumed

Every pre-existing call shape replayed against the previous commit and diffed:

  • fal, 23 shapes (each modality, both endpoints, every limit, scalar and ArrayBuffer refs, untyped blobs, lora on both paths) — byte-identical.
  • wavespeed, 6 shapes (image+prompt, prompt-only placeholder image, caller-supplied images[], lora) — byte-identical.

replicate.ts and getProviderHelper.ts are untouched by this PR.

New fields are optional and both task types already carried an index signature.

Testing

45 tests green; tsc, eslint, oxfmt clean across packages/inference and packages/tasks. Live-tested against minimax/h3/reference-to-video with a fal key, which is how the reference requirement and the error-reporting bug surfaced.

Follow-ups

  • Replicate, which needs a Hub mapping for MiniMax-H3 before a helper is reachable at all.
  • Hub mappings for fal's and wavespeed's reference endpoints.
  • The Hub widget — this PR supplies the task-level contract it needs.
  • Snippets: getInferenceSnippets.ts routes image-text-to-video through prepareImageToImageInput, so the model page can only show an image+prompt snippet.
  • Python huggingface_hub parity.

🤖 Generated with Claude Code


Note

Medium Risk
Changes provider request payloads and validation for image-text-to-video (and fal queue error handling). Wrong endpoint mapping or payload shape can reject previously valid calls or send unusable fields to providers.

Overview
Adds optional reference images, videos, and audio to image-text-to-video so callers can pass reference_images / reference_videos / reference_audio (URLs or Blobs) regardless of provider.

Shared buildReferenceInputs validates counts, inlines binaries, and remaps field names. fal and wavespeed only send those lists on …/reference-to-video endpoints; other video endpoints error instead of silently dropping extras. The primary inputs image is treated as “Image 1” on reference endpoints.

Also: LoRA weights now apply on fal text-to-video (and a shared helper), fal queue result fetches surface HTTP errors instead of “malformed response”, and image-text-to-video no longer rewrites fal URLs by dropping a path segment. Schema/types in @huggingface/tasks document the new optional fields.

Reviewed by Cursor Bugbot for commit 8d57e2d. Bugbot is set up for automated code reviews on this repo. Configure here.

hanouticelina and others added 3 commits August 20, 2026 13:37
`MiniMaxAI/MiniMax-H3` is mapped to `minimax/h3/image-to-video`, which only
takes a single first-frame `image_url`. fal exposes the model's full input
surface on `minimax/h3/reference-to-video`: up to 9 subject/style images, 3
motion video clips and 3 audio clips in one call, each addressed from the
prompt as "Image 1", "Video 1", "Audio 1".

A Hub mapping holds exactly one providerId per (model, provider), and the
router exact-matches the request path against it — `POST /fal-ai/minimax/h3/
reference-to-video` today returns `Model not supported by provider fal-ai`.
So reaching the endpoint means repointing the mapping, not rewriting the URL
client-side. This is the client half of that change; it keeps working against
either endpoint so the two can land independently.

`image-text-to-video` now normalizes every input — the task's own `inputs`
image plus any `reference_*_urls` passed through `parameters` — into the three
reference lists, inlining Blobs and ArrayBuffers as data URLs (audio via the
MIME remapping fal's data-URL decoder requires). `preparePayload` then emits
the reference lists for a `reference-to-video` provider id, or the single
`image_url` first frame otherwise. fal's caps (9/3/3, 12 combined, audio never
on its own) are checked client-side so callers get an error instead of a 422.

Also:

- Drop the `dropEndpointSegmentOnDirectCalls` rewrite from image-text-to-video.
  It assumes the text-only variant sits at the parent path, which holds for
  `fal-ai/flux-2/edit` but not for MiniMax-H3: `minimax/h3` is not an endpoint,
  so prompt-only direct calls 404'd. Both mapped endpoints already accept a
  prompt-only call. image-text-to-image keeps the rewrite.
- Build `loras` from a tag-filter adapter mapping in the video tasks. The
  `.../lora` endpoints require it, and only the image tasks did it, so
  `minimax/h3/image-to-video/lora` was unreachable. Extracted the four copies
  into `applyLoraWeights`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live-testing `minimax/h3/reference-to-video` turned up two things.

The endpoint requires at least one reference. Its schema marks only `prompt`
required, but the app validates further:

  {"type":"value_error","loc":["body"],
   "msg":"Value error, At least one reference image, video, or audio must be provided"}

So reference-to-video is *not* a superset of image-to-video — a prompt-only
call works on the latter and is rejected by the former. Fail that fast, naming
the fields that would satisfy it, instead of round-tripping to a 422.

And that 422 was reported as `ProviderOutputError: ... expected { video: { url:
string } }`, which points at the wrong thing entirely. The queue reports
COMPLETED even for a request fal rejected, so the failure only surfaces when
fetching the result — and unlike the status poll, that fetch never checked
`resultResponse.ok`, so an error body was parsed as a result. Check it and
raise the provider's own status and body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
While the mapping still points at minimax/h3/image-to-video, that endpoint
takes a single first frame, so any further reference image and every reference
video or audio clip was quietly discarded - the caller got a video that ignored
half its input with nothing to explain why. Warn, naming the count and the
mapping change that would make them usable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread packages/inference/src/providers/fal-ai.ts
hanouticelina and others added 5 commits August 20, 2026 14:25
The reference fields ride through `parameters` untyped, so anything building an
input UI on top of `imageTextToVideo` — the Hub's ImageTextToVideoWidget first —
would have to hardcode fal's field names, per-list caps, clip durations and
accepted content types, and re-derive which models take references at all.

Publish that instead. `FAL_AI_REFERENCE_INPUTS` describes each modality (field,
maxItems, an `accept` string ready for a file picker, duration bounds, and
whether it may stand alone), `FAL_AI_MAX_REFERENCE_FILES` the combined cap, and
`supportsFalAiReferenceInputs(providerId)` answers whether to offer the inputs
for a given model — the plain image-to-video endpoints ignore everything past a
single first frame. `FalAiReferenceParameters` types the values.

These are the same constants the payload builder validates against, so the two
cannot drift; a test asserts each advertised cap is the one enforced. The fal
module had no public exports at all, so these go through the package index
alongside FAL_AI_SUPPORTED_BLOB_TYPES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference lists were fal's own field names riding through `parameters`
untyped, with the caps and content types published as fal-specific exports for
a UI to read. That put the provider's identity in the caller: the Hub widget
would have had to import fal constants and ask whether this model happened to
be mapped to a fal reference endpoint before deciding what inputs to render.

Declare them on the task instead. `ImageTextToVideoParameters` gains optional
`reference_images`, `reference_videos` and `reference_audio` — arrays of URLs or
data URLs, addressed from the prompt by position ("Image 1", "Video 1") — so
they are typed, documented, and generated for every language. A widget renders
them because the task has them, and knows nothing about who serves it.

The fal helper becomes one implementation of that contract, translating the
task's fields to its own `reference_*_urls` and keeping the limits it enforces
private. Its public exports are withdrawn.

Values are URLs in the schema, which is what reaches the provider. The client
additionally accepts binary for those fields, so a browser `File` can be passed
straight through the way `inputs` already is, and the provider inlines it.

A non-reference endpoint now refuses references it cannot honour instead of
warning and dropping them: with these inputs offered on every image-text-to-
video model, a silently ignored attachment is worse than a clear error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Declaring reference_images / reference_videos / reference_audio on
ImageTextToVideoParameters bought nothing the client needed: the generated
interface already carries an index signature, and the element types come from
the widening in imageTextToVideo.ts, so both the runtime and TypeScript behave
identically without it. What it did do was put video and audio inputs on a task
named image-text-to-video that only one provider, on one endpoint, can honour.

Drop it. Callers use the same field names whoever serves the model, so nothing
consuming the task learns which provider is behind it — the point of the
exercise — and the schema stays honest about what the task is. Worth revisiting
if a second provider implements references, or when Python needs the types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cleanup pass over the new code, behaviour unchanged — verified by replaying 23
call shapes (every modality, both endpoints, each limit, scalar and ArrayBuffer
refs, untyped blobs, lora on both paths) against the previous commit and
diffing the payloads and error messages. One line differs, and it is a fix:

  `Omit<ImageTextToVideoParameters, keyof ReferenceInputs>` erased every named
  parameter. Both task types carry an index signature, so `keyof` them is
  `string | number` and the Omit laundered the whole type back into that
  signature: `parameters.prompt` and `parameters.seed` had become `unknown` for
  every caller of imageTextToVideo, on every provider. A plain intersection is
  both shorter and correctly typed.

  applyLoraWeights gated the `fal-ai/lora` model_name case on `payload.loras`,
  which preparePayload also fills from caller-supplied `parameters.loras`. It
  now reports whether the mapping carried an adapter, restoring the original
  gate.

The rest is subtraction. preparePayloadAsync built three records and two casts
to reach one payload; it now builds one typed record and fills the payload in a
loop. preparePayload derived a parallel `counts` record to ask questions the
lists could answer themselves, and re-omitted `inputs`/`parameters` and respread
`parameters` — all three dead, since the async step already flattened them.
`asReferenceList` reimplemented `toArray` and took `unknown`, forcing two casts;
it delegates and takes a real type. `requiresCompanion` meant "needs a companion"
in one place and "is the audio one" in another, read inverted in a third — it is
`isAudio`. The single-use `ReferenceField` alias and the separate regex constant
are gone. Net: three Object.fromEntries and four casts removed.

Tests: fold mappingFor into its one caller, stop rebuilding a Blob that is
re-readable, turn a loop inside it.each into named cases so a failure says which
one. Adds the missing coverage for loras on text-to-video, which this PR started
building and nothing exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fal is not alone after all. wavespeed serves the same six MiniMax-H3 endpoints
including `reference-to-video`, with the same 9/3/3 caps and the same
"at least one reference" rule; replicate exposes one `minimax/h3` model whose
schema carries the reference lists beside `first_frame_image`. Three providers,
one concept, and three different sets of field names for it.

So the concept belongs to the task. `ImageTextToVideoParameters` gains optional
`reference_images`, `reference_videos` and `reference_audio` — arrays of URLs or
data URLs, addressed from the prompt by position — typed and generated for every
language. Callers write those names whoever serves the model.

`lib/referenceInputs.ts` does the work every provider shares: read the three
parameters, coerce, check the counts, inline binary as data URLs, and emit the
lists under the provider's own names. What differs is data, declared per
provider as a ReferenceInputsSpec:

  fal        reference_image_urls / _video_urls / _audio_urls, 9/3/3, 12 total,
             audio never alone, audio re-typed for fal's data-URL decoder
  wavespeed  reference_images / reference_videos / reference_audios, 9/3/3
  replicate  reference_*_urls, no caps declared, first frame stays separate

Errors name the task's field, not the provider's, because that is what the
caller wrote.

Endpoint shape is still per-provider, since only fal and wavespeed split the
model across endpoints: both refuse references an endpoint cannot honour rather
than sending keys it will ignore — for wavespeed that also stops a Blob being
serialised into the body as `{}`. Replicate needs no such branch.

Backward compatible, and checked rather than assumed: every pre-existing call
shape on fal (23 of them) and on wavespeed (6, including the placeholder-image
and lora paths) produces a byte-identical payload. The new fields are optional
and the task types already carried an index signature.

Replicate gains an image-text-to-video helper; it needs a Hub mapping before it
is reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hanouticelina hanouticelina changed the title [Inference Providers] fal-ai: support MiniMax-H3 reference-to-video [Inference Providers] image-text-to-video: reference images, videos and audio across fal, wavespeed and replicate Aug 21, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fd79980. Configure here.

Comment thread packages/inference/src/providers/fal-ai.ts
Comment thread packages/inference/src/providers/wavespeed.ts
Comment thread packages/tasks/src/tasks/image-text-to-video/inference.ts
Its image-text-to-video helper is the only part of the cross-provider work that
nothing can reach yet: replicate has no Hub mapping for MiniMax-H3 at all, so
the helper would ship untested and unused. Drop it and keep this PR to the two
providers that are live.

The shared mechanism stays as it is — fal and wavespeed already justify it, and
adding replicate later is a ReferenceInputsSpec and a subclass, no change here.
Worth doing: replicate exposes one `minimax/h3` model carrying the first frame
and the reference lists in a single schema, so unlike the other two it needs no
endpoint switch and gives up neither prompt-only nor keyframing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hanouticelina hanouticelina changed the title [Inference Providers] image-text-to-video: reference images, videos and audio across fal, wavespeed and replicate [Inference Providers] image-text-to-video: reference images, videos and audio on fal and wavespeed Aug 21, 2026
hanouticelina and others added 4 commits August 21, 2026 15:49
A file of its own under lib/ was the wrong shelf twice over: lib/ is the request
pipeline — mapping resolution, request options, provider selection — and this is
called by providers, not by the pipeline; and providers had to reach for a
second module to implement one interface.

providers/providerHelper.ts already declares ImageTextToVideoTaskHelper, the
contract these providers implement, and already imports toArray, omit and the
error types this needs. Providers already import it at runtime for the base
class, so folding the machinery in costs no new import.

The task module would have read better still — imageTextToVideo.ts already owns
the caller-facing ReferenceInputs type — but providers import nothing from
tasks/ at runtime today, only types, and a value import would close a cycle
through getProviderHelper.

Pure move: fal's 23 call shapes and wavespeed's 6 are byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Leaves one edit in the existing spec, which is not new coverage: the
"drops the endpoint segment when calling fal directly" case asserted the
rewrite this PR removes, so it fails as written. It now asserts the URL is
left alone, with a note on why the rewrite was wrong here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Predates this PR, removed on request. Its image-text-to-video half asserted the
endpoint-segment rewrite this PR deletes; the image-text-to-image half covered
the `fal-ai/flux-2/edit` rewrite, which still stands but is now uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live-testing against wavespeed-ai/minimax-h3/reference-to-video turned this up.
A call with no references falls through to the path the other video endpoints
need, which substitutes a 1x1 transparent PNG for a missing first frame — so a
prompt-only call arrived at the reference endpoint carrying `image` and
`images`, fields it does not have. wavespeed accepted it and generated a video
anyway, which is the unhelpful kind of working.

Drop those two fields when the mapped endpoint is a reference one; the
placeholder still stands in everywhere it is wanted.

Worth recording while confirming the fix: wavespeed's reference endpoint
happily generates from a bare prompt, where fal's rejects the same call with
"At least one reference image, video, or audio must be provided". The rule is
one provider's, not the model's, which is why it lives in fal's spec and not in
the shared builder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant