feat(imagegen): face swap (identity transfer), a separate operation from Edit
Measured against the instruction-edit models on 2026-07-31: asking a diffusion model to put a SPECIFIC person's face into a photo does not work by any route. qwen-image-edit returns the picture essentially unchanged whether asked by name, by attribute, or by supplying the portrait as a second reference image; flux-kontext replaces the face with a different generic person. Identity transfer is a detect/align/blend pipeline, not a better prompt, so it gets its own interface rather than more Edit options. imagegen.FaceSwapper is optional and type-asserted, like Editor — a provider that cannot do this must not have Edit quietly stand in for it. ListFaces is part of the interface, not a convenience: a caller asked to change "the man on the right" needs a stable way to NAME one face, and pixel boxes let it check its own choice. The llamaswap shim orders faces left to right for exactly that reason (insightface's own order is score-ranked and unstable between near-identical images), and a malformed box is a protocol error rather than a zero-filled struct, because a wrong box aims the swap at the wrong person. The provider is the first here to POST more than one file, so buildMultipart gained buildMultipartFiles and now delegates to it — one writer loop, so the two cannot drift in how they escape names or terminate the body. index and all are mutually exclusive ON THE WIRE: the shim ignores index under all=true, and sending both would imply a precedence the caller cannot see. A JSON body is refused rather than returned as image bytes — the shim answers JSON on a semantic miss (no face in the source), and handing that back as a picture would report success while delivering a file that is not one.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package imagegen
|
||||
|
||||
import "context"
|
||||
|
||||
// FaceSwapRequest transfers an identity from Source into Target.
|
||||
//
|
||||
// This is a DIFFERENT OPERATION from Edit, not a better-tuned one. Measured
|
||||
// against the instruction-edit models on 2026-07-31, asking a diffusion model
|
||||
// to put a specific person's face into a photo does not work by any route —
|
||||
// by name, by description, or by supplying the portrait as a reference image.
|
||||
// Face swapping is a dedicated detect/align/blend pipeline; a provider that
|
||||
// cannot do it should not pretend Edit is a substitute.
|
||||
type FaceSwapRequest struct {
|
||||
// Target is the photo to edit — the pose, expression, lighting and
|
||||
// everything outside the face are preserved from it.
|
||||
Target Image
|
||||
|
||||
// Source is a photo of the face to put in. Only the identity travels;
|
||||
// the source's own pose and expression do not.
|
||||
Source Image
|
||||
|
||||
// Index selects WHICH face in Target, in the provider's documented
|
||||
// ordering (llamaswap: left to right by box centre, as reported by
|
||||
// ListFaces). nil = the largest face, which is right for a portrait and
|
||||
// wrong for a group — enumerate first when it matters.
|
||||
Index *int
|
||||
|
||||
// All swaps every detected face and ignores Index.
|
||||
All bool
|
||||
}
|
||||
|
||||
// FaceSwapOption mutates a FaceSwapRequest before it is sent.
|
||||
type FaceSwapOption func(*FaceSwapRequest)
|
||||
|
||||
// WithFaceIndex selects which face in the target to replace.
|
||||
func WithFaceIndex(i int) FaceSwapOption { return func(r *FaceSwapRequest) { r.Index = &i } }
|
||||
|
||||
// WithAllFaces swaps every detected face.
|
||||
func WithAllFaces() FaceSwapOption { return func(r *FaceSwapRequest) { r.All = true } }
|
||||
|
||||
// Apply returns a copy of the request with all options applied.
|
||||
func (r FaceSwapRequest) Apply(opts ...FaceSwapOption) FaceSwapRequest {
|
||||
for _, opt := range opts {
|
||||
opt(&r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// DetectedFace is one face located in an image, in PIXEL coordinates.
|
||||
type DetectedFace struct {
|
||||
// Index is the face's position in the provider's stable ordering, and
|
||||
// the value FaceSwapRequest.Index expects.
|
||||
Index int
|
||||
// Box is [x0, y0, x1, y1].
|
||||
Box [4]int
|
||||
// Score is the detector's confidence, 0-1.
|
||||
Score float64
|
||||
// Width and Height are the box dimensions, carried so a caller can pick
|
||||
// "the big face" without recomputing them.
|
||||
Width, Height int
|
||||
}
|
||||
|
||||
// FaceSwapper is the optional face-transfer surface. Separate interface so
|
||||
// existing providers keep compiling; callers type-assert.
|
||||
type FaceSwapper interface {
|
||||
// ListFaces enumerates the faces in an image, in the SAME ordering
|
||||
// FaceSwapRequest.Index uses. Exposed because a caller asked to change
|
||||
// "the man on the right" needs a way to name one face and to check its
|
||||
// own choice against pixel boxes.
|
||||
ListFaces(ctx context.Context, img Image) ([]DetectedFace, error)
|
||||
|
||||
// FaceSwap transfers Source's identity into Target.
|
||||
FaceSwap(ctx context.Context, req FaceSwapRequest, opts ...FaceSwapOption) (*Result, error)
|
||||
}
|
||||
Reference in New Issue
Block a user