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") }