509 lines
12 KiB
Go
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
|
|
}
|
|
}
|