[Inference Providers] image-text-to-video: reference images, videos and audio on fal and wavespeed - #2394
Draft
hanouticelina wants to merge 13 commits into
Draft
[Inference Providers] image-text-to-video: reference images, videos and audio on fal and wavespeed#2394hanouticelina wants to merge 13 commits into
hanouticelina wants to merge 13 commits into
Conversation
`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>
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ 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.
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Context
MiniMaxAI/MiniMax-H3can 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.
reference_image_urlsreference_video_urlsreference_audio_urlsreference_imagesreference_videosreference_audiosreference_image_urlsreference_video_urlsreference_audio_urlsimage_url, separate endpointfirst_frame_image, same schemaThe task owns the concept
ImageTextToVideoParametersgains three optional fields, typed and generated for every language:Callers write those names whoever serves the model — so anything built on the task, the Hub's
ImageTextToVideoWidgetincluded, needs no provider-specific code:The schema types the lists as URLs, which is what reaches the provider; the client additionally accepts
Blob, so a browserFilegoes straight through the wayinputsalready does.One mechanism, three configurations
lib/referenceInputs.tsdoes 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:One call, each provider's native payload:
Adding replicate later is a
ReferenceInputsSpecand 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
Blobbeing serialised into the request body as{}.Only fal's
image-to-videoand wavespeed's are registered; no reference-to-video endpoint is, and the router exact-matches the path against the registered providerId:For fal and wavespeed, repointing trades away prompt-only and keyframing — verified live:
reference-to-videorejects a call with no reference (At least one reference image, video, or audio must be provided) and has noend_image_url. Worth weighing against the follow-up: replicate's singleminimax/h3schema 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.../loraendpoints require alorasentry, but only the image tasks built one, sominimax/h3/image-to-video/lorawas unreachable. Four copies folded into one helper, now also used by text-to-video.COMPLETEDeven for a rejected request and the result fetch never checkedresultResponse.ok, so a 422 surfaced asProviderOutputError: expected { video: { url: string } }. That is how the reference requirement above was found. Affects every fal queue task.dropEndpointSegmentOnDirectCallsfrom image-text-to-video — it assumes the text-only variant sits at the parent path, butminimax/h3is 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:
ArrayBufferrefs, untyped blobs, lora on both paths) — byte-identical.images[], lora) — byte-identical.replicate.tsandgetProviderHelper.tsare untouched by this PR.New fields are optional and both task types already carried an index signature.
Testing
45 tests green;
tsc,eslint,oxfmtclean acrosspackages/inferenceandpackages/tasks. Live-tested againstminimax/h3/reference-to-videowith a fal key, which is how the reference requirement and the error-reporting bug surfaced.Follow-ups
getInferenceSnippets.tsroutesimage-text-to-videothroughprepareImageToImageInput, so the model page can only show an image+prompt snippet.huggingface_hubparity.🤖 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-videoso callers can passreference_images/reference_videos/reference_audio(URLs or Blobs) regardless of provider.Shared
buildReferenceInputsvalidates counts, inlines binaries, and remaps field names. fal and wavespeed only send those lists on…/reference-to-videoendpoints; other video endpoints error instead of silently dropping extras. The primaryinputsimage 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/tasksdocument the new optional fields.Reviewed by Cursor Bugbot for commit 8d57e2d. Bugbot is set up for automated code reviews on this repo. Configure here.