- SubmitChain rejects NaN/±Inf segment seconds with ErrUnsupported (previously an obscure json.Marshal error; NaN fails every comparison and +Inf passed the >= 0 check). - ChainStatus skips segment entries with no usable id — JSON null (which no-op-unmarshals into a string, previously appending ""), empty strings, and id-less objects; the unfiltered list survives in Raw. ChainJob.SegmentIDs doc now also says ChainSegmentResult takes the segment index, not an id string. - jobPath rejects '%' in job ids — %2F/%2E%2E percent-escapes decode back into path structure server-side, bypassing the literal check on this upstream-echoed value. - singleVideoResult moves to video.go next to videoMIME, and the two remaining hand-rolled copies of the video-result validation (videoModel.Generate, Interpolate) now use it — one validation, one message shape. - videogen.LipSyncer renamed to videogen.Lipsyncer for consistency with the rest of the surface's Lipsync* naming (LipsyncProvider, LipsyncModel, LipsyncRequest); not yet consumed downstream, so the rename is free now and never again. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWCQcYStWXBYUy5sZWnbLT
94 lines
3.2 KiB
Go
94 lines
3.2 KiB
Go
package videogen
|
|
|
|
import "context"
|
|
|
|
// LipsyncRequest asks a talking-head backend (SadTalker style) to animate a
|
|
// still portrait so it speaks the given audio. Zero values mean "backend
|
|
// default" (ADR-0025).
|
|
type LipsyncRequest struct {
|
|
// Image is the portrait to animate. Required.
|
|
Image Image
|
|
|
|
// Audio is the encoded speech the head lip-syncs to. Required. Carried
|
|
// as bytes (never a URL), mirroring audio.TranscriptionRequest.
|
|
Audio []byte
|
|
|
|
// AudioMIME is the audio MIME type (e.g. "audio/wav"); "" = let the
|
|
// backend sniff it.
|
|
AudioMIME string
|
|
|
|
// AudioFilename is the multipart filename hint some backends key their
|
|
// format detection on; "" derives one from AudioMIME or falls back to
|
|
// "audio".
|
|
AudioFilename string
|
|
|
|
// Still reduces head motion to blinks and lip movement (less uncanny on
|
|
// formal portraits); false = backend default motion.
|
|
Still bool
|
|
|
|
// Enhance runs the backend's face enhancer over the output frames.
|
|
Enhance bool
|
|
|
|
// Preprocess selects how the backend frames the face: "crop" (animate
|
|
// the face crop) or "full" (paste the animated face back into the whole
|
|
// image); "" = backend default.
|
|
Preprocess string
|
|
}
|
|
|
|
// LipsyncOption mutates a LipsyncRequest before it is sent.
|
|
type LipsyncOption func(*LipsyncRequest)
|
|
|
|
// WithLipsyncStill reduces head motion to blinks and lip movement.
|
|
func WithLipsyncStill() LipsyncOption { return func(r *LipsyncRequest) { r.Still = true } }
|
|
|
|
// WithLipsyncEnhance runs the backend's face enhancer over the output.
|
|
func WithLipsyncEnhance() LipsyncOption { return func(r *LipsyncRequest) { r.Enhance = true } }
|
|
|
|
// WithLipsyncPreprocess selects the face framing ("crop" or "full").
|
|
func WithLipsyncPreprocess(p string) LipsyncOption {
|
|
return func(r *LipsyncRequest) { r.Preprocess = p }
|
|
}
|
|
|
|
// Apply returns a copy of the request with all options applied.
|
|
func (r LipsyncRequest) Apply(opts ...LipsyncOption) LipsyncRequest {
|
|
for _, opt := range opts {
|
|
opt(&r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Lipsyncer animates still portraits into talking-head clips. Its own small
|
|
// interface rather than a method on Model: lip-syncers are not text-to-video
|
|
// generators — they bind to a different backend id entirely.
|
|
type Lipsyncer interface {
|
|
// Lipsync returns the talking-head clip. Generation is slow (minutes);
|
|
// bound the call with a context deadline.
|
|
Lipsync(ctx context.Context, req LipsyncRequest, opts ...LipsyncOption) (*Result, error)
|
|
}
|
|
|
|
// LipsyncModelOption configures a Lipsyncer at construction time. Reserved
|
|
// for future per-model settings.
|
|
type LipsyncModelOption func(*LipsyncModelConfig)
|
|
|
|
// LipsyncModelConfig carries per-model construction settings.
|
|
type LipsyncModelConfig struct{}
|
|
|
|
// ApplyLipsyncModelOptions folds options into a config.
|
|
func ApplyLipsyncModelOptions(opts []LipsyncModelOption) LipsyncModelConfig {
|
|
var cfg LipsyncModelConfig
|
|
for _, opt := range opts {
|
|
opt(&cfg)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// LipsyncProvider mints Lipsyncers bound to one backend.
|
|
type LipsyncProvider interface {
|
|
// Name is the registry identifier for the provider.
|
|
Name() string
|
|
|
|
// LipsyncModel returns a Lipsyncer bound to the given id (passed through
|
|
// to the backend verbatim; no catalog validation).
|
|
LipsyncModel(id string, opts ...LipsyncModelOption) (Lipsyncer, error)
|
|
}
|