Files
majordomo/imagegen/faceswap.go
T
steve 6995a8dee1
Gadfly review (reusable) / review (pull_request) Successful in 4m54s
Adversarial Review (Gadfly) / review (pull_request) Successful in 4m55s
CI / Tidy (pull_request) Successful in 9m23s
CI / Build & Test (pull_request) Successful in 9m48s
feat(faceswap): expose yaw on enumeration too
ListFaces now carries head yaw, so a caller choosing WHICH face to swap can
see the thing that decides whether the swap will read — not only learn it
afterwards from the swap report. In the run that prompted this the target's
three faces sat at -82, -8 and -11 degrees; only the first was hopeless, and
nothing in a bounding box said so.
2026-07-31 17:50:27 -04:00

138 lines
5.8 KiB
Go

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
// Yaw is how far the head is turned from camera, in degrees, or nil when
// the provider does not report pose. Exposed on ENUMERATION, not just
// after the fact, because it is how a caller picks a face a swap will
// actually work on: past roughly ±45° the features carrying identity are
// edge-on, and the result reads as a generic person however good the
// transfer is. A bounding box cannot show this.
Yaw *float64
}
// Size returns the box dimensions. Derived rather than stored: carrying
// width/height alongside Box is two sources of truth for one fact, and the
// pair can disagree after any transform.
func (f DetectedFace) Size() (w, h int) {
return f.Box[2] - f.Box[0], f.Box[3] - f.Box[1]
}
// SwappedFace is the MEASURED outcome for one face the provider replaced.
//
// It exists because "the call returned an image" and "the likeness
// transferred" are different claims that look identical from outside, and a
// caller that cannot tell them apart will go looking for another way to
// check. The one it reaches for — asking a vision model who the result looks
// like — is wrong in exactly the cases that matter: a VLM shown a jogger in a
// Georgetown cap holding McDonald's cups answers "Bill Clinton" whoever's
// face is on him, so it reports failure on a correct swap.
type SwappedFace struct {
// Index is the face's position in the provider's left-to-right ordering.
Index int
// Width, Height are the replaced face's pixel size in the TARGET.
// Meaningful only against ImageWidth/ImageHeight: a 138px face is large
// in a 400px picture and nearly invisible in a 2000px one, and it is the
// ratio, not the absolute size, that decides whether a person notices.
Width, Height int
// ImageWidth, ImageHeight are the target image's dimensions, repeated on
// every entry so a single face is self-describing without the caller
// holding onto the rest of the response.
ImageWidth, ImageHeight int
// Yaw is how far the head is turned from camera, in degrees, or nil when
// the provider does not report pose. The best single predictor of whether
// a swap will READ as the source person: past roughly ±45° the features
// carrying identity are edge-on and the result looks like a generic
// person rather than a specific one.
Yaw *float64
// IdentitySimilarity is cosine similarity between the source face and the
// face actually present in the result, 0-1, or nil when the provider
// could not measure it. Above ~0.5 the identity transferred; a LOW value
// is the only evidence that a swap genuinely failed.
IdentitySimilarity *float64
}
// FractionOfImage is the swapped face's width as a share of the image's, 0-1.
// The number that predicts whether a person will SEE the change: the swap
// that prompted all this replaced a 138px face in a 1010px-wide photo — 14%,
// correct by every measure and invisible at a glance — while the same code on
// a 168px face in a 385px picture (44%) is unmistakable. Returns 0 when the
// dimensions are unknown.
func (f SwappedFace) FractionOfImage() float64 {
if f.ImageWidth <= 0 || f.Width <= 0 {
return 0
}
return float64(f.Width) / float64(f.ImageWidth)
}
// 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)
}