99 lines
2.3 KiB
Go
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
|
|
}
|