feat: wave-3 video surfaces — lipsync, video matte, video upscale, chain jobs (ADR-0025)
CI / Tidy (pull_request) Successful in 9m27s
CI / Build & Test (pull_request) Successful in 10m43s
Gadfly review (reusable) / review (pull_request) Successful in 10m9s
Adversarial Review (Gadfly) / review (pull_request) Successful in 10m9s

- videogen.LipSyncer/LipsyncProvider: SadTalker talking heads via
  POST /upstream/<id>/v1/talking_head (multipart image+audio parts,
  still/enhance/preprocess flags) -> mp4.
- videogen.VideoBackgroundRemover/VideoBackgroundRemovalProvider:
  POST /upstream/<id>/v1/video/matte (output greenscreen_mp4|alpha_webm).
- videogen.VideoUpscaler/VideoUpscaleProvider:
  POST /upstream/<id>/v1/video/upscale (scale 2|4).
- videogen.Chainer/ChainerProvider: async long-video chain-job client —
  SubmitChain (JSON POST /v1/video/chain, init_image_b64), ChainStatus
  (GET /v1/jobs/{id}, tolerant segment-id decode), ChainResult,
  ChainSegmentResult (partial delivery after mid-chain failure); hostile
  job-id path rejection.
- Shared singleVideoResult validation (positive video evidence) + a
  videoInputFilename hint helper; httptest contract tests per surface;
  ADR-0025 (index row deferred — MJ-A backfills the ADR index table and
  parallel edits would conflict).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-16 17:07:27 -04:00
co-authored by Claude Fable 5
parent cd43009672
commit 5f175ecf82
10 changed files with 1453 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
package videogen
import "context"
// ChainSegment is one prompt in a multi-segment ("long video") chain.
type ChainSegment struct {
// Prompt describes this segment. Required.
Prompt string
// Seconds is the segment's requested length; 0 = backend default.
Seconds float64
}
// ChainRequest asks a chain orchestrator to generate a long video as a
// sequence of segments, each continuing from the previous segment's last
// frame. Zero values mean "backend default" (ADR-0025).
type ChainRequest struct {
// Segments are the per-segment prompts in order. At least one required.
Segments []ChainSegment
// InitImage optionally conditions the FIRST segment on a starting frame
// (image-to-video); nil = pure text-to-video.
InitImage []byte
// SmoothJoins asks the orchestrator to interpolate across segment
// boundaries (RIFE-style) so cuts don't pop.
SmoothJoins bool
// Size is the requested resolution, e.g. "1280x704"; "" = backend
// default.
Size string
}
// ChainJob is a chain job's progress snapshot.
type ChainJob struct {
// Status is the backend's job state, passed through verbatim
// (e.g. "queued", "running", "done", "failed").
Status string
// Segment is the segment currently being generated (1-based); Total is
// the segment count.
Segment int
Total int
// SegmentIDs are the COMPLETED per-segment artifacts, fetchable via
// ChainSegmentResult — a mid-chain failure still leaves these
// retrievable, so multi-minute GPU output is never discarded.
SegmentIDs []string
// Raw is the provider-native job payload. May be nil.
Raw any
}
// Chainer drives a multi-segment video-chain job. Unlike Model.Generate it
// is deliberately ASYNC — a chain runs through multiple GPU loads for many
// minutes, so callers submit, poll, and fetch instead of holding one
// blocking call open.
type Chainer interface {
// SubmitChain starts a chain job and returns its job id.
SubmitChain(ctx context.Context, req ChainRequest) (string, error)
// ChainStatus reports the job's progress. Polling also signals liveness
// to backends that unload idle orchestrators.
ChainStatus(ctx context.Context, jobID string) (*ChainJob, error)
// ChainResult fetches the finished, concatenated clip.
ChainResult(ctx context.Context, jobID string) (*Result, error)
// ChainSegmentResult fetches one completed segment's clip (n indexes the
// job's segment list) — the partial-delivery path when a chain dies
// mid-run.
ChainSegmentResult(ctx context.Context, jobID string, n int) (*Result, error)
}
// ChainerModelOption configures a Chainer at construction time. Reserved for
// future per-model settings.
type ChainerModelOption func(*ChainerModelConfig)
// ChainerModelConfig carries per-model construction settings.
type ChainerModelConfig struct{}
// ApplyChainerModelOptions folds options into a config.
func ApplyChainerModelOptions(opts []ChainerModelOption) ChainerModelConfig {
var cfg ChainerModelConfig
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// ChainerProvider mints Chainers bound to one backend.
type ChainerProvider interface {
// Name is the registry identifier for the provider.
Name() string
// ChainerModel returns a Chainer bound to the given id (passed through
// to the backend verbatim; no catalog validation).
ChainerModel(id string, opts ...ChainerModelOption) (Chainer, error)
}
+93
View File
@@ -0,0 +1,93 @@
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)
}
+80
View File
@@ -0,0 +1,80 @@
package videogen
import "context"
// VideoBackgroundRemovalRequest asks a video-matting backend (Robust Video
// Matting style) to separate the foreground subject from the background of a
// clip. Zero values mean "backend default" (ADR-0025).
type VideoBackgroundRemovalRequest struct {
// Video is the encoded clip to matte. Required. Carried as bytes (never
// a URL).
Video []byte
// MIME is the video MIME type (e.g. "video/mp4"); "" = let the backend
// sniff it.
MIME string
// Filename is the multipart filename hint some backends key their format
// detection on; "" derives one from MIME ("video.mp4") or falls back to
// "video".
Filename string
// Output selects the delivery container: "greenscreen_mp4" (subject over
// solid green, universally playable) or "alpha_webm" (true transparency,
// VP9 alpha channel); "" = backend default.
Output string
}
// VideoBackgroundRemovalOption mutates a VideoBackgroundRemovalRequest
// before it is sent.
type VideoBackgroundRemovalOption func(*VideoBackgroundRemovalRequest)
// WithVideoBackgroundOutput selects the delivery container
// ("greenscreen_mp4" or "alpha_webm").
func WithVideoBackgroundOutput(o string) VideoBackgroundRemovalOption {
return func(r *VideoBackgroundRemovalRequest) { r.Output = o }
}
// Apply returns a copy of the request with all options applied.
func (r VideoBackgroundRemovalRequest) Apply(opts ...VideoBackgroundRemovalOption) VideoBackgroundRemovalRequest {
for _, opt := range opts {
opt(&r)
}
return r
}
// VideoBackgroundRemover mattes the subject out of video clips — the moving-
// picture sibling of imagegen.BackgroundRemover.
type VideoBackgroundRemover interface {
// RemoveVideoBackground returns the matted clip. Matting is slow
// (per-frame inference); bound the call with a context deadline.
RemoveVideoBackground(ctx context.Context, req VideoBackgroundRemovalRequest, opts ...VideoBackgroundRemovalOption) (*Result, error)
}
// VideoBackgroundRemoverModelOption configures a VideoBackgroundRemover at
// construction time. Reserved for future per-model settings.
type VideoBackgroundRemoverModelOption func(*VideoBackgroundRemoverModelConfig)
// VideoBackgroundRemoverModelConfig carries per-model construction settings.
type VideoBackgroundRemoverModelConfig struct{}
// ApplyVideoBackgroundRemoverModelOptions folds options into a config.
func ApplyVideoBackgroundRemoverModelOptions(opts []VideoBackgroundRemoverModelOption) VideoBackgroundRemoverModelConfig {
var cfg VideoBackgroundRemoverModelConfig
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// VideoBackgroundRemovalProvider mints VideoBackgroundRemovers bound to one
// backend.
type VideoBackgroundRemovalProvider interface {
// Name is the registry identifier for the provider.
Name() string
// VideoBackgroundRemoverModel returns a VideoBackgroundRemover bound to
// the given id (passed through to the backend verbatim; no catalog
// validation).
VideoBackgroundRemoverModel(id string, opts ...VideoBackgroundRemoverModelOption) (VideoBackgroundRemover, error)
}
+74
View File
@@ -0,0 +1,74 @@
package videogen
import "context"
// VideoUpscaleRequest asks a super-resolution backend (per-frame Real-ESRGAN
// style) to enlarge a clip. Zero values mean "backend default" (ADR-0025).
type VideoUpscaleRequest struct {
// Video is the encoded clip to upscale. Required. Carried as bytes
// (never a URL).
Video []byte
// MIME is the video MIME type (e.g. "video/mp4"); "" = let the backend
// sniff it.
MIME string
// Filename is the multipart filename hint some backends key their format
// detection on; "" derives one from MIME ("video.mp4") or falls back to
// "video".
Filename string
// Scale is the enlargement factor (2 or 4 on the reference backend);
// 0 = backend default.
Scale int
}
// VideoUpscaleOption mutates a VideoUpscaleRequest before it is sent.
type VideoUpscaleOption func(*VideoUpscaleRequest)
// WithVideoUpscaleScale sets the enlargement factor.
func WithVideoUpscaleScale(s int) VideoUpscaleOption {
return func(r *VideoUpscaleRequest) { r.Scale = s }
}
// Apply returns a copy of the request with all options applied.
func (r VideoUpscaleRequest) Apply(opts ...VideoUpscaleOption) VideoUpscaleRequest {
for _, opt := range opts {
opt(&r)
}
return r
}
// VideoUpscaler enlarges video clips frame by frame — the moving-picture
// sibling of imagegen.Upscaler.
type VideoUpscaler interface {
// UpscaleVideo returns the enlarged clip. Upscaling is slow (per-frame
// inference); bound the call with a context deadline.
UpscaleVideo(ctx context.Context, req VideoUpscaleRequest, opts ...VideoUpscaleOption) (*Result, error)
}
// VideoUpscalerModelOption configures a VideoUpscaler at construction time.
// Reserved for future per-model settings.
type VideoUpscalerModelOption func(*VideoUpscalerModelConfig)
// VideoUpscalerModelConfig carries per-model construction settings.
type VideoUpscalerModelConfig struct{}
// ApplyVideoUpscalerModelOptions folds options into a config.
func ApplyVideoUpscalerModelOptions(opts []VideoUpscalerModelOption) VideoUpscalerModelConfig {
var cfg VideoUpscalerModelConfig
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// VideoUpscaleProvider mints VideoUpscalers bound to one backend.
type VideoUpscaleProvider interface {
// Name is the registry identifier for the provider.
Name() string
// VideoUpscalerModel returns a VideoUpscaler bound to the given id
// (passed through to the backend verbatim; no catalog validation).
VideoUpscalerModel(id string, opts ...VideoUpscalerModelOption) (VideoUpscaler, error)
}