FloorMaterialAnalyzer/internal/intelligence/load.go

141 lines
3.7 KiB
Go
Raw Normal View History

2026-07-27 03:03:51 +00:00
package intelligence
import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"sort"
"strings"
"materialanalyzer/internal/model"
)
type loadedAsset struct {
Asset model.MaterialAsset
AssetDir string
Histogram model.ColorHistogram
Embeddings []EmbeddingRecord
}
func loadAssets(assetsDir string) ([]loadedAsset, error) {
entries, err := os.ReadDir(assetsDir)
if err != nil {
return nil, fmt.Errorf("read assets dir: %w", err)
}
out := make([]loadedAsset, 0, len(entries))
for _, entry := range entries {
if !entry.IsDir() {
continue
}
assetDir := filepath.Join(assetsDir, entry.Name())
asset, err := readMaterialAsset(filepath.Join(assetDir, "material.json"))
if err != nil {
continue
}
hist, _ := readHistogram(filepath.Join(assetDir, "histogram.json"))
embeddings, _ := readEmbeddings(filepath.Join(assetDir, "embedding.bin"), asset.Embeddings)
out = append(out, loadedAsset{Asset: asset, AssetDir: assetDir, Histogram: hist, Embeddings: embeddings})
}
sort.Slice(out, func(i, j int) bool { return out[i].Asset.SKU < out[j].Asset.SKU })
return out, nil
}
func readMaterialAsset(path string) (model.MaterialAsset, error) {
file, err := os.Open(path)
if err != nil {
return model.MaterialAsset{}, err
}
defer file.Close()
var asset model.MaterialAsset
if err := json.NewDecoder(file).Decode(&asset); err != nil {
return model.MaterialAsset{}, err
}
if strings.TrimSpace(asset.SKU) == "" {
return model.MaterialAsset{}, fmt.Errorf("asset %s has no SKU", path)
}
migrateAssetDefaults(&asset)
return asset, nil
}
func migrateAssetDefaults(asset *model.MaterialAsset) {
if asset.Semantic.MaterialType == "" {
asset.Semantic.MaterialType = firstNonEmpty(familyFromSpecific(asset.MaterialSpecific), asset.Category, asset.Material)
}
if asset.Semantic.GrainType == "" {
asset.Semantic.GrainType = asset.Semantic.Grain
}
if asset.Semantic.GlossLevel == "" {
asset.Semantic.GlossLevel = glossFromFinish(asset.Semantic.SurfaceFinish)
}
if asset.Semantic.Confidence == 0 {
asset.Semantic.Confidence = 0.65
}
if asset.AnalyzerVersion == "" {
asset.AnalyzerVersion = "1.x"
}
if asset.FeatureSchema == "" {
asset.FeatureSchema = "legacy"
}
}
func glossFromFinish(finish string) string {
lower := strings.ToLower(finish)
switch {
case strings.Contains(lower, "gloss"):
return "High"
case strings.Contains(lower, "satin"):
return "Medium"
case strings.Contains(lower, "matte"):
return "Low"
default:
return "Unknown"
}
}
func readHistogram(path string) (model.ColorHistogram, error) {
file, err := os.Open(path)
if err != nil {
return model.ColorHistogram{}, err
}
defer file.Close()
var hist model.ColorHistogram
return hist, json.NewDecoder(file).Decode(&hist)
}
func readEmbeddings(path string, manifest model.EmbeddingManifest) ([]EmbeddingRecord, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return nil, err
}
records := make([]EmbeddingRecord, 0, len(manifest.Vectors))
for _, vector := range manifest.Vectors {
start := int(vector.OffsetBytes)
byteLen := int(vector.ByteLength)
if byteLen == 0 && vector.Dimensions > 0 {
byteLen = vector.Dimensions * 4
}
end := start + byteLen
if start < 0 || end > len(data) || byteLen%4 != 0 {
continue
}
values := make([]float32, byteLen/4)
for i := range values {
values[i] = math.Float32frombits(binary.LittleEndian.Uint32(data[start+i*4 : start+i*4+4]))
}
records = append(records, EmbeddingRecord{
Name: vector.Name, Model: vector.Model, Provider: vector.Provider,
Dimensions: len(values), Values: values,
})
}
return records, nil
}