persona-community-5/pkg/persona/plausibility.go
jordan bd2f591b98
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-24 07:39:46 +00:00

509 lines
12 KiB
Go

package persona
import (
"fmt"
"math"
)
// PlausibilityLevel represents how biologically plausible a feature combination is.
type PlausibilityLevel string
const (
PlausibilityHighlyPlausible PlausibilityLevel = "highly_plausible" // 50%+ probability
PlausibilityPlausible PlausibilityLevel = "plausible" // 15-50% probability
PlausibilityUnusual PlausibilityLevel = "unusual" // 5-15% probability
PlausibilityRare PlausibilityLevel = "rare" // 1-5% probability
PlausibilityImplausible PlausibilityLevel = "implausible" // < 1% probability
)
// PlausibilityViolation represents a single biological plausibility issue.
type PlausibilityViolation struct {
Field string // e.g., "face.eye_color"
Value interface{} // The actual value
Ethnicity EthnicityCode // The ethnicity being validated against
Level PlausibilityLevel // How plausible this is
Probability float64 // Actual probability (0.0-1.0)
Message string // Human-readable explanation
}
// PlausibilityResult contains the validation results for DNA.
type PlausibilityResult struct {
Valid bool // True if no implausible violations
Violations []PlausibilityViolation // All violations found
Score float64 // Overall plausibility score (0-1)
}
// ValidateHumanPlausibility checks if face/hair features are biologically plausible
// for the given ethnicity. Returns violations for implausible combinations.
func ValidateHumanPlausibility(dna *DNA) *PlausibilityResult {
if dna == nil {
return &PlausibilityResult{
Valid: true,
Score: 1.0,
}
}
result := &PlausibilityResult{
Valid: true,
Violations: []PlausibilityViolation{},
Score: 1.0,
}
ethnicity := dna.Identity.Ethnicity
if ethnicity == "" || ethnicity == EthnicityMixed {
// Mixed or unspecified ethnicity - skip plausibility checks
return result
}
// Validate eye color
if violation := validateEyeColor(dna.Face.EyeColor, ethnicity); violation != nil {
result.Violations = append(result.Violations, *violation)
if violation.Level == PlausibilityImplausible {
result.Valid = false
}
}
// Validate hair color
if violation := validateHairColor(dna.Face.HairColor, ethnicity); violation != nil {
result.Violations = append(result.Violations, *violation)
if violation.Level == PlausibilityImplausible {
result.Valid = false
}
}
// Validate hair texture
if violation := validateHairTexture(dna.Face.HairTexture, ethnicity); violation != nil {
result.Violations = append(result.Violations, *violation)
if violation.Level == PlausibilityImplausible {
result.Valid = false
}
}
// Validate skin tone
if violation := validateSkinTone(dna.Face.SkinTone, ethnicity); violation != nil {
result.Violations = append(result.Violations, *violation)
if violation.Level == PlausibilityImplausible {
result.Valid = false
}
}
// Calculate overall plausibility score (geometric mean of probabilities)
if len(result.Violations) > 0 {
product := 1.0
for _, v := range result.Violations {
product *= v.Probability
}
numFeatures := 4.0 // eye_color, hair_color, hair_texture, skin_tone
result.Score = math.Pow(product, 1.0/numFeatures)
}
return result
}
// validateEyeColor checks if eye color is plausible for the ethnicity.
func validateEyeColor(eyeColor EyeColorCategory, ethnicity EthnicityCode) *PlausibilityViolation {
if eyeColor == "" {
return nil
}
prob := getEyeColorProbability(eyeColor, ethnicity)
level := getProbabilityLevel(prob)
if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible {
return &PlausibilityViolation{
Field: "face.eye_color",
Value: eyeColor,
Ethnicity: ethnicity,
Level: level,
Probability: prob,
Message: fmt.Sprintf("Eye color %s is %s for %s ethnicity (%.1f%% probability)", eyeColor, level, ethnicity, prob*100),
}
}
return nil
}
// validateHairColor checks if hair color is plausible for the ethnicity.
func validateHairColor(hairColor HairColorCategory, ethnicity EthnicityCode) *PlausibilityViolation {
if hairColor == "" {
return nil
}
prob := getHairColorProbability(hairColor, ethnicity)
level := getProbabilityLevel(prob)
if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible {
return &PlausibilityViolation{
Field: "face.hair_color",
Value: hairColor,
Ethnicity: ethnicity,
Level: level,
Probability: prob,
Message: fmt.Sprintf("Hair color %s is %s for %s ethnicity (%.1f%% probability)", hairColor, level, ethnicity, prob*100),
}
}
return nil
}
// validateHairTexture checks if hair texture is plausible for the ethnicity.
func validateHairTexture(hairTexture HairTextureCategory, ethnicity EthnicityCode) *PlausibilityViolation {
if hairTexture == "" {
return nil
}
prob := getHairTextureProbability(hairTexture, ethnicity)
level := getProbabilityLevel(prob)
if level == PlausibilityUnusual || level == PlausibilityRare || level == PlausibilityImplausible {
return &PlausibilityViolation{
Field: "face.hair_texture",
Value: hairTexture,
Ethnicity: ethnicity,
Level: level,
Probability: prob,
Message: fmt.Sprintf("Hair texture %s is %s for %s ethnicity (%.1f%% probability)", hairTexture, level, ethnicity, prob*100),
}
}
return nil
}
// validateSkinTone checks if skin tone is plausible for the ethnicity.
func validateSkinTone(skinTone SkinToneCategory, ethnicity EthnicityCode) *PlausibilityViolation {
if skinTone == "" {
return nil
}
if !isSkinTonePlausible(skinTone, ethnicity) {
return &PlausibilityViolation{
Field: "face.skin_tone",
Value: skinTone,
Ethnicity: ethnicity,
Level: PlausibilityImplausible,
Probability: 0.005,
Message: fmt.Sprintf("Skin tone %s is implausible for %s ethnicity", skinTone, ethnicity),
}
}
return nil
}
// getProbabilityLevel converts a probability to a PlausibilityLevel.
func getProbabilityLevel(prob float64) PlausibilityLevel {
if prob >= 0.5 {
return PlausibilityHighlyPlausible
} else if prob >= 0.15 {
return PlausibilityPlausible
} else if prob >= 0.05 {
return PlausibilityUnusual
} else if prob >= 0.01 {
return PlausibilityRare
} else {
return PlausibilityImplausible
}
}
// getEyeColorProbability returns the probability of an eye color for an ethnicity.
func getEyeColorProbability(eyeColor EyeColorCategory, ethnicity EthnicityCode) float64 {
switch ethnicity {
case EthnicityEastAsian, EthnicitySoutheastAsian:
switch eyeColor {
case EyeColorDarkBrown:
return 0.85
case EyeColorBrown:
return 0.15
case EyeColorHazel:
return 0.005
default:
return 0.001
}
case EthnicitySouthAsian:
switch eyeColor {
case EyeColorDarkBrown:
return 0.75
case EyeColorBrown:
return 0.20
case EyeColorHazel:
return 0.03
case EyeColorAmber:
return 0.02
default:
return 0.005
}
case EthnicityAfrican:
switch eyeColor {
case EyeColorDarkBrown:
return 0.85
case EyeColorBrown:
return 0.13
case EyeColorAmber:
return 0.02
default:
return 0.002
}
case EthnicityHispanic:
switch eyeColor {
case EyeColorBrown:
return 0.55
case EyeColorDarkBrown:
return 0.25
case EyeColorHazel:
return 0.12
case EyeColorGreen:
return 0.05
case EyeColorAmber:
return 0.03
default:
return 0.01
}
case EthnicityMiddleEastern:
switch eyeColor {
case EyeColorBrown:
return 0.50
case EyeColorDarkBrown:
return 0.30
case EyeColorHazel:
return 0.10
case EyeColorGreen:
return 0.07
case EyeColorAmber:
return 0.03
default:
return 0.005
}
case EthnicityCaucasian:
switch eyeColor {
case EyeColorBlue:
return 0.30
case EyeColorBrown:
return 0.25
case EyeColorGreen:
return 0.15
case EyeColorHazel:
return 0.15
case EyeColorGray:
return 0.10
case EyeColorDarkBrown:
return 0.05
default:
return 0.02
}
default:
return 0.15 // Mixed/unknown - moderate probability
}
}
// getHairColorProbability returns the probability of a hair color for an ethnicity.
func getHairColorProbability(hairColor HairColorCategory, ethnicity EthnicityCode) float64 {
switch ethnicity {
case EthnicityEastAsian, EthnicitySoutheastAsian:
switch hairColor {
case HairColorBlack:
return 0.85
case HairColorDarkBrown:
return 0.12
case HairColorBrown:
return 0.03
default:
return 0.001
}
case EthnicitySouthAsian:
switch hairColor {
case HairColorBlack:
return 0.85
case HairColorDarkBrown:
return 0.12
case HairColorBrown:
return 0.03
default:
return 0.001
}
case EthnicityAfrican:
switch hairColor {
case HairColorBlack:
return 0.92
case HairColorDarkBrown:
return 0.08
default:
return 0.001
}
case EthnicityHispanic:
switch hairColor {
case HairColorBlack:
return 0.50
case HairColorDarkBrown:
return 0.35
case HairColorBrown:
return 0.12
case HairColorLightBrown:
return 0.03
default:
return 0.005
}
case EthnicityMiddleEastern:
switch hairColor {
case HairColorBlack:
return 0.65
case HairColorDarkBrown:
return 0.30
case HairColorBrown:
return 0.05
default:
return 0.005
}
case EthnicityCaucasian:
switch hairColor {
case HairColorBrown:
return 0.35
case HairColorLightBrown:
return 0.20
case HairColorBlonde:
return 0.20
case HairColorDarkBrown:
return 0.12
case HairColorAuburn:
return 0.08
case HairColorRed:
return 0.05
default:
return 0.02
}
default:
return 0.10
}
}
// getHairTextureProbability returns the probability of a hair texture for an ethnicity.
func getHairTextureProbability(hairTexture HairTextureCategory, ethnicity EthnicityCode) float64 {
switch ethnicity {
case EthnicityEastAsian, EthnicitySoutheastAsian:
switch hairTexture {
case HairTextureStraight:
return 0.85
case HairTextureWavy:
return 0.15
case HairTextureCurly:
return 0.005
case HairTextureCoily, HairTextureKinky:
return 0.001
default:
return 0.01
}
case EthnicitySouthAsian:
switch hairTexture {
case HairTextureWavy:
return 0.40
case HairTextureStraight:
return 0.35
case HairTextureCurly:
return 0.25
default:
return 0.01
}
case EthnicityAfrican:
switch hairTexture {
case HairTextureCoily:
return 0.50
case HairTextureKinky:
return 0.30
case HairTextureCurly:
return 0.20
case HairTextureWavy:
return 0.005
case HairTextureStraight:
return 0.001
default:
return 0.01
}
case EthnicityHispanic:
switch hairTexture {
case HairTextureWavy:
return 0.40
case HairTextureStraight:
return 0.30
case HairTextureCurly:
return 0.30
default:
return 0.01
}
case EthnicityMiddleEastern:
switch hairTexture {
case HairTextureWavy:
return 0.45
case HairTextureStraight:
return 0.30
case HairTextureCurly:
return 0.25
default:
return 0.01
}
case EthnicityCaucasian:
switch hairTexture {
case HairTextureStraight:
return 0.40
case HairTextureWavy:
return 0.40
case HairTextureCurly:
return 0.20
default:
return 0.01
}
default:
return 0.15
}
}
// isSkinTonePlausible checks if a skin tone is plausible for an ethnicity.
func isSkinTonePlausible(skinTone SkinToneCategory, ethnicity EthnicityCode) bool {
switch ethnicity {
case EthnicityEastAsian, EthnicitySoutheastAsian:
return skinTone == SkinToneFair || skinTone == SkinToneLight ||
skinTone == SkinToneMedium || skinTone == SkinToneTan
case EthnicitySouthAsian:
return skinTone == SkinToneLight || skinTone == SkinToneMedium ||
skinTone == SkinToneTan || skinTone == SkinToneBrown || skinTone == SkinToneDeep
case EthnicityAfrican:
return skinTone == SkinToneMedium || skinTone == SkinToneTan ||
skinTone == SkinToneBrown || skinTone == SkinToneDarkBrown || skinTone == SkinToneDeep
case EthnicityHispanic:
return skinTone == SkinToneLight || skinTone == SkinToneMedium ||
skinTone == SkinToneTan || skinTone == SkinToneBrown ||
skinTone == SkinToneDeep || skinTone == SkinToneOlive
case EthnicityMiddleEastern:
return skinTone == SkinToneLight || skinTone == SkinToneMedium ||
skinTone == SkinToneTan || skinTone == SkinToneOlive
case EthnicityCaucasian:
return skinTone == SkinToneFair || skinTone == SkinToneLight || skinTone == SkinToneMedium
case EthnicityMixed:
return true // Mixed allows all skin tones
default:
return true // Unknown allows all
}
}