241 lines
6.4 KiB
Go
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")
|
|
}
|