// segment.go implements imagegen.SegmentationProvider against a // GroundingDINO+SAM shim (segment-langsam) reached through llama-swap's // /upstream passthrough (ADR-0023): // // POST /upstream//v1/segment multipart file,prompt[,threshold],output=mask // // The response is a single grayscale mask PNG where WHITE marks the prompted // region — directly usable as imagegen.EditRequest.Mask (white = repaint). // The shim also offers output=cutout|boxes; this client always requests the // mask, because a cutout is derivable client-side from mask+original with no // second GPU call. package llamaswap import ( "context" "fmt" "math" "net/http" "strconv" "gitea.stevedudenhoeffer.com/steve/majordomo/imagegen" "gitea.stevedudenhoeffer.com/steve/majordomo/llm" ) // SegmentationModel implements imagegen.SegmentationProvider. The id selects // which upstream llama-swap loads. func (p *Provider) SegmentationModel(id string, opts ...imagegen.SegmentationModelOption) (imagegen.Segmenter, error) { if err := p.requireBaseURL(); err != nil { return nil, err } _ = imagegen.ApplySegmentationModelOptions(opts) return &segmentationModel{p: p, id: id}, nil } type segmentationModel struct { p *Provider id string } // Segment implements imagegen.Segmenter. func (m *segmentationModel) Segment(ctx context.Context, req imagegen.SegmentationRequest, opts ...imagegen.SegmentationOption) (*imagegen.Result, error) { req = req.Apply(opts...) if len(req.Image.Data) == 0 { return nil, fmt.Errorf("%w: segmentation requires an image", llm.ErrUnsupported) } if req.Prompt == "" { return nil, fmt.Errorf("%w: segmentation requires a prompt", llm.ErrUnsupported) } // NaN fails every comparison, so it would sail through a bare range // check and reach the upstream as the literal string "NaN". if math.IsNaN(req.Threshold) || req.Threshold < 0 || req.Threshold > 1 { return nil, fmt.Errorf("%w: segmentation threshold must be in [0,1], got %g", llm.ErrUnsupported, req.Threshold) } path, err := upstreamPath(m.id, "/v1/segment") if err != nil { return nil, err } threshold := "" if req.Threshold != 0 { threshold = strconv.FormatFloat(req.Threshold, 'g', -1, 64) } body, contentType, err := buildMultipart("build segment form", filePart{field: "file", filename: "image.png", data: req.Image.Data}, []formField{ {"prompt", req.Prompt, true}, {"threshold", threshold, false}, // Always the mask: white = prompted region, EditRequest.Mask // polarity. Cutouts are derived client-side. {"output", "mask", true}, }) if err != nil { return nil, err } raw, respType, err := m.p.doRaw(ctx, http.MethodPost, path, m.id, contentType, body, maxImageResponseBytes) if err != nil { return nil, err } return singleImageResult(m.p.name, m.id, "segmentation", raw, respType) }