80 lines
2.0 KiB
Go
80 lines
2.0 KiB
Go
|
|
package intelligence
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func buildSimilarityGraph(assets []loadedAsset, fingerprints map[string]MaterialFingerprint, k int) SimilarityGraph {
|
||
|
|
if k <= 0 {
|
||
|
|
k = 5
|
||
|
|
}
|
||
|
|
graph := SimilarityGraph{
|
||
|
|
Version: "3.0",
|
||
|
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||
|
|
K: k,
|
||
|
|
Nodes: len(assets),
|
||
|
|
}
|
||
|
|
for _, source := range assets {
|
||
|
|
candidates := make([]SimilarityEdge, 0, len(assets)-1)
|
||
|
|
sourceVec, sourceMethod := comparableVector(source, fingerprints[source.Asset.SKU])
|
||
|
|
for _, target := range assets {
|
||
|
|
if source.Asset.SKU == target.Asset.SKU {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
targetVec, targetMethod := comparableVector(target, fingerprints[target.Asset.SKU])
|
||
|
|
score := cosine(sourceVec, targetVec)
|
||
|
|
method := sourceMethod
|
||
|
|
if sourceMethod != targetMethod {
|
||
|
|
method = "mixed"
|
||
|
|
}
|
||
|
|
candidates = append(candidates, SimilarityEdge{
|
||
|
|
SourceSKU: source.Asset.SKU, TargetSKU: target.Asset.SKU,
|
||
|
|
Score: round6(score), Method: method,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
sort.Slice(candidates, func(i, j int) bool { return candidates[i].Score > candidates[j].Score })
|
||
|
|
if len(candidates) > k {
|
||
|
|
candidates = candidates[:k]
|
||
|
|
}
|
||
|
|
graph.Edges = append(graph.Edges, candidates...)
|
||
|
|
}
|
||
|
|
return graph
|
||
|
|
}
|
||
|
|
|
||
|
|
func comparableVector(asset loadedAsset, fp MaterialFingerprint) ([]float64, string) {
|
||
|
|
if len(asset.Embeddings) > 0 && len(asset.Embeddings[0].Values) > 0 {
|
||
|
|
vec := make([]float64, len(asset.Embeddings[0].Values))
|
||
|
|
for i, v := range asset.Embeddings[0].Values {
|
||
|
|
vec[i] = float64(v)
|
||
|
|
}
|
||
|
|
return vec, "embedding_cosine"
|
||
|
|
}
|
||
|
|
return []float64{
|
||
|
|
fp.Brightness, fp.Contrast, fp.Saturation, fp.Variation,
|
||
|
|
fp.TextureEntropy, fp.TextureFrequency, fp.Orientation / 180,
|
||
|
|
fp.ColorVariance, fp.TextureVariance,
|
||
|
|
}, "fingerprint_cosine"
|
||
|
|
}
|
||
|
|
|
||
|
|
func cosine(a, b []float64) float64 {
|
||
|
|
n := len(a)
|
||
|
|
if len(b) < n {
|
||
|
|
n = len(b)
|
||
|
|
}
|
||
|
|
if n == 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
var dot, na, nb float64
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
dot += a[i] * b[i]
|
||
|
|
na += a[i] * a[i]
|
||
|
|
nb += b[i] * b[i]
|
||
|
|
}
|
||
|
|
if na == 0 || nb == 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
return dot / (math.Sqrt(na) * math.Sqrt(nb))
|
||
|
|
}
|