Gadfly on #23, blocking, 2/2 agreement — and it is the exact defect this whole line of work has been about: a call that succeeds while handing back the wrong bytes. sniffImageMIME falls back to image/png when detection is inconclusive, and the guard only consulted Content-Type. A response with NO Content-Type therefore skipped the check entirely and was labelled a PNG. The shim answers JSON on a semantic miss (no face found in the source or target), which is precisely the body that would have sailed through as a successful image. The check now validates the BYTES — http.DetectContentType must say image/ — and the reported MIME prefers the server's own label only when that label is itself an image type. Break-checked by restoring the header-only condition, which fails the new test. Also from that review: - index is documented as ignored under all=true, so a negative one is no longer rejected there; it is still rejected when it would actually be sent, and both halves are tested. - initImageFilename (video.go) was imageFilename with the base fixed to "frame" and now delegates to it — two copies of one extension table is how they drift. - DetectedFace carried Width/Height alongside Box, two sources of truth for one fact that can disagree after any transform. Now a Size() method derived from Box. - a dead `apiErr` in the test (declared, then `_ = apiErr`) was an abandoned errors.As check; it is wired up and now asserts callers can classify the error. - swapImg duplicated editInit verbatim; removed. Not taken: adding a FaceSwapProvider/ModelOption surface to match the other optional imagegen capabilities (single-model finding). There are no options to carry yet, and inventing an empty option type to look symmetrical would be API surface with nothing behind it. Worth revisiting when a real knob exists.
79 lines
3.0 KiB
Go
79 lines
3.0 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
|
|
}
|
|
|
|
// 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]
|
|
}
|
|
|
|
// 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)
|
|
}
|