go-places/api/internal/analysis/composite.go
2026-10-07 15:14:50 +02:00

99 lines
2.3 KiB
Go

package analysis
import (
"context"
"fmt"
"go-places-api/internal/domain"
)
// Composite runs the local analyzer first and then, when a remote service is
// configured, feeds the locally derived scene labels to it as hints. Running
// the local pass first is what makes the hints available, and it also means the
// picture still gets badges if the remote service is unreachable.
type Composite struct {
local Analyzer
remote Analyzer
}
var _ Analyzer = (*Composite)(nil)
// NewComposite chains two analyzers. Either may be nil, in which case it is
// skipped.
func NewComposite(local, remote Analyzer) *Composite {
return &Composite{local: local, remote: remote}
}
// Name implements Analyzer.
func (c *Composite) Name() string {
if c.remote == nil {
return SourceLocal
}
return SourceLocal + "+" + SourceRemote
}
// Analyze measures locally, then augments with model labels when available.
//
// A failure in the remote service is reported as a non-fatal note rather than
// an error: the picture was still understood, just less precisely. Callers can
// inspect Note to surface the degradation.
func (c *Composite) Analyze(ctx context.Context, in Input) (*Result, error) {
var (
base *Result
local []domain.Badge
note string
)
if c.local != nil {
res, err := c.local.Analyze(ctx, in)
if err != nil {
return nil, err
}
base = res
local = res.Badges
} else {
base = &Result{}
}
if c.remote != nil {
hinted := in
hinted.Hints = labelsOf(local)
res, err := c.remote.Analyze(ctx, hinted)
switch {
case err != nil:
note = fmt.Sprintf("%s analyzer unavailable: %v", SourceRemote, err)
if ctx.Err() != nil {
return nil, ctx.Err()
}
default:
base.Width, base.Height = pickDimension(base.Width, res.Width), pickDimension(base.Height, res.Height)
base.Badges = Combine(local, res.Badges)
}
} else {
base.Badges = local
}
base.Note = note
return base, nil
}
// labelsOf extracts badge labels to use as upstream hints.
func labelsOf(badges []domain.Badge) []string {
if len(badges) == 0 {
return nil
}
out := make([]string, 0, len(badges))
for _, b := range badges {
if b.Confidence >= 0.5 {
out = append(out, b.Label)
}
}
return out
}
func pickDimension(current, candidate int) int {
if candidate > 0 {
return candidate
}
return current
}