feat(videogen): LastImage — pin the trailing keyframe (first-last-frame-to-video)
videogen.Request gains LastImage alongside InitImage, so one Request covers t2v, i2v and FL2V without a mode flag. With InitImage it pins both ends of the clip; alone it pins the destination and lets the backend invent the approach. The llamaswap provider sends it as a SEPARATE `input_reference_last` part rather than a second `input_reference`. Multipart permits repeated names, but then which frame is first and which is last depends on part ORDER — an ordering contract invisible in the payload, that nothing notices breaking. A backend that does not know the new name ignores the part, the same degradation as any other unknown field. Both parts go through one writeImagePart helper so their encoding cannot drift, and an empty LastImage is rejected up front exactly as InitImage already is. Support is per-model and deliberately NOT advertised in this contract: a backend that ignores a trailing keyframe returns an ordinary clip, which is indistinguishable from success. The doc comment says so, because a caller that needs to know whether the pin took effect has to establish that out of band — and the mort side gates on a convar for exactly this reason. Motivated by mort's #1567 (long-form video): with both ends pinned, drift becomes structurally bounded inside each shot instead of compounding across an autoregressive chain. Tests break-checked: sending the last frame under the shared name fails both the distinct-name assertion and the last-alone case. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PLjgrxvHjm1sJgUu9zBPH9
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
// InitImage is a pure text prompt, a non-nil InitImage conditions generation
|
||||
// on that frame. Hybrid models (e.g. Wan 2.2 TI2V) serve both from the same
|
||||
// checkpoint, so unlike imagegen there is no separate Editor-style interface.
|
||||
// LastImage extends the same surface to the other end of the clip, so one
|
||||
// Request covers t2v, i2v, and first-last-frame-to-video without a mode flag.
|
||||
//
|
||||
// The first implementation is provider/llamaswap, which targets the blocking
|
||||
// OpenAI/vLLM-Omni-style POST /v1/videos/sync endpoint: the response body is
|
||||
@@ -51,6 +53,18 @@ type Request struct {
|
||||
// nil = pure text-to-video.
|
||||
InitImage *Image
|
||||
|
||||
// LastImage conditions generation on an ENDING frame. With InitImage it
|
||||
// pins both ends (first-last-frame-to-video); alone it pins only the
|
||||
// destination and lets the backend invent the approach.
|
||||
//
|
||||
// Support is per-model and NOT advertised anywhere in this contract: a
|
||||
// backend that does not understand a trailing keyframe ignores it and
|
||||
// returns an ordinary clip, which is indistinguishable from success. A
|
||||
// caller that needs to know whether the pin took effect must establish
|
||||
// that out of band — see the note on LastImage support in
|
||||
// provider/llamaswap.
|
||||
LastImage *Image
|
||||
|
||||
// Size is the requested resolution, e.g. "1280x704"; "" = backend default.
|
||||
Size string
|
||||
|
||||
@@ -92,6 +106,10 @@ type Option func(*Request)
|
||||
// WithInitImage conditions generation on a starting frame (image-to-video).
|
||||
func WithInitImage(img Image) Option { return func(r *Request) { r.InitImage = &img } }
|
||||
|
||||
// WithLastImage conditions generation on an ending frame. Combined with
|
||||
// WithInitImage this pins both ends of the clip.
|
||||
func WithLastImage(img Image) Option { return func(r *Request) { r.LastImage = &img } }
|
||||
|
||||
// WithSize sets the requested resolution (e.g. "1280x704").
|
||||
func WithSize(size string) Option { return func(r *Request) { r.Size = size } }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user