persona-community-3/pkg/persona/data_loader.go
jordan f53b908499
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 11:10:35 +00:00

241 lines
6.4 KiB
Go

package persona
import (
"embed"
"log"
"sync"
"gopkg.in/yaml.v3"
)
//go:embed data/*.yaml
var dataFS embed.FS
// DataStore holds loaded pool data.
type DataStore struct {
BodyDescriptions map[BodyBuildCategory][]string
VoiceDescriptions map[string][]string
EthnicityDistribution map[EthnicityCode]EthnicityFeatureDistribution
MorphologyFeatures MorphologyFeaturePool
}
// EthnicityFeatureDistribution holds feature probabilities for an ethnicity.
type EthnicityFeatureDistribution struct {
EyeColors map[EyeColorCategory]float64 `yaml:"eye_colors"`
HairColors map[HairColorCategory]float64 `yaml:"hair_colors"`
HairTextures map[HairTextureCategory]float64 `yaml:"hair_textures"`
SkinTones []SkinToneCategory `yaml:"skin_tones"`
}
// MorphologyFeaturePool holds valid morphology feature values.
type MorphologyFeaturePool struct {
EarTypes []string `yaml:"ear_types"`
TailTypes []string `yaml:"tail_types"`
FangTypes []string `yaml:"fang_types"`
WingTypes []string `yaml:"wing_types"`
HornTypes []string `yaml:"horn_types"`
ClawTypes []string `yaml:"claw_types"`
}
var (
dataStore *DataStore
dataStoreOnce sync.Once
dataStoreErr error
)
// GetDataStore returns the singleton data store.
// Data is loaded lazily on first access.
func GetDataStore() (*DataStore, error) {
dataStoreOnce.Do(func() {
dataStore, dataStoreErr = loadDataStore()
})
return dataStore, dataStoreErr
}
// MustGetDataStore returns the data store or panics on error.
func MustGetDataStore() *DataStore {
ds, err := GetDataStore()
if err != nil {
log.Fatalf("failed to load persona data: %v", err)
}
return ds
}
func loadDataStore() (*DataStore, error) {
ds := &DataStore{
BodyDescriptions: make(map[BodyBuildCategory][]string),
VoiceDescriptions: make(map[string][]string),
EthnicityDistribution: make(map[EthnicityCode]EthnicityFeatureDistribution),
}
// Load body descriptions
if err := loadYAML("data/body_descriptions.yaml", &ds.BodyDescriptions); err != nil {
// Use defaults if file doesn't exist
ds.BodyDescriptions = defaultBodyDescriptions()
}
// Load voice descriptions
if err := loadYAML("data/voice_descriptions.yaml", &ds.VoiceDescriptions); err != nil {
ds.VoiceDescriptions = defaultVoiceDescriptions()
}
// Load ethnicity distributions
if err := loadYAML("data/ethnicity_distributions.yaml", &ds.EthnicityDistribution); err != nil {
ds.EthnicityDistribution = defaultEthnicityDistributions()
}
// Load morphology features
if err := loadYAML("data/morphology_features.yaml", &ds.MorphologyFeatures); err != nil {
ds.MorphologyFeatures = defaultMorphologyFeatures()
}
return ds, nil
}
func loadYAML[T any](path string, target *T) error {
data, err := dataFS.ReadFile(path)
if err != nil {
return err
}
return yaml.Unmarshal(data, target)
}
// defaultBodyDescriptions provides fallback body descriptions.
func defaultBodyDescriptions() map[BodyBuildCategory][]string {
return map[BodyBuildCategory][]string{
BodyBuildSlender: {
"slim and graceful figure",
"lean, elegant frame",
"willowy silhouette",
},
BodyBuildAthletic: {
"toned, athletic physique",
"fit and active build",
"athletic frame with visible muscle tone",
},
BodyBuildCurvy: {
"curvy, feminine figure",
"shapely silhouette with soft curves",
"hourglass proportions",
},
BodyBuildMuscular: {
"muscular, powerful build",
"strong, well-defined physique",
"athletic frame with prominent muscles",
},
BodyBuildAverage: {
"average build",
"balanced proportions",
"typical physique",
},
BodyBuildPlusCurvy: {
"full-figured and curvy",
"generous curves",
"voluptuous silhouette",
},
BodyBuildPetite: {
"petite and delicate",
"small-framed",
"compact, graceful build",
},
}
}
// defaultVoiceDescriptions provides fallback voice descriptions.
func defaultVoiceDescriptions() map[string][]string {
return map[string][]string{
"warm": {
"warm and inviting",
"comforting timbre",
"friendly warmth",
},
"cool": {
"cool and measured",
"calm, collected tone",
"composed delivery",
},
"bright": {
"bright and energetic",
"lively, upbeat quality",
"animated expression",
},
}
}
// defaultEthnicityDistributions provides fallback distributions.
func defaultEthnicityDistributions() map[EthnicityCode]EthnicityFeatureDistribution {
return map[EthnicityCode]EthnicityFeatureDistribution{
EthnicityEastAsian: {
EyeColors: map[EyeColorCategory]float64{
EyeColorDarkBrown: 0.85,
EyeColorBrown: 0.15,
},
HairColors: map[HairColorCategory]float64{
HairColorBlack: 0.85,
HairColorDarkBrown: 0.15,
},
HairTextures: map[HairTextureCategory]float64{
HairTextureStraight: 0.85,
HairTextureWavy: 0.15,
},
SkinTones: []SkinToneCategory{
SkinToneFair, SkinToneLight, SkinToneMedium,
},
},
EthnicityCaucasian: {
EyeColors: map[EyeColorCategory]float64{
EyeColorBlue: 0.30,
EyeColorBrown: 0.25,
EyeColorGreen: 0.15,
EyeColorHazel: 0.15,
EyeColorGray: 0.15,
},
HairColors: map[HairColorCategory]float64{
HairColorBrown: 0.35,
HairColorLightBrown: 0.20,
HairColorBlonde: 0.20,
HairColorDarkBrown: 0.15,
HairColorRed: 0.10,
},
HairTextures: map[HairTextureCategory]float64{
HairTextureStraight: 0.40,
HairTextureWavy: 0.40,
HairTextureCurly: 0.20,
},
SkinTones: []SkinToneCategory{
SkinToneFair, SkinToneLight, SkinToneMedium,
},
},
}
}
// defaultMorphologyFeatures provides fallback morphology pools.
func defaultMorphologyFeatures() MorphologyFeaturePool {
return MorphologyFeaturePool{
EarTypes: ValidEarTypes,
TailTypes: ValidTailTypes,
FangTypes: ValidFangTypes,
WingTypes: ValidWingTypes,
HornTypes: ValidHornTypes,
ClawTypes: ValidClawTypes,
}
}
// GetBodyDescription returns a description for a body build type.
func (ds *DataStore) GetBodyDescription(build BodyBuildCategory, characterID string) string {
pool, ok := ds.BodyDescriptions[build]
if !ok || len(pool) == 0 {
return string(build) + " build"
}
return SelectDescription(pool, characterID, "body_description")
}
// GetVoiceDescription returns a description for a voice tone.
func (ds *DataStore) GetVoiceDescription(tone string, characterID string) string {
pool, ok := ds.VoiceDescriptions[tone]
if !ok || len(pool) == 0 {
return tone + " voice"
}
return SelectDescription(pool, characterID, "voice_description")
}