nsfwapp/frontend/src/aiLabels.ts
2026-06-13 23:22:15 +02:00

457 lines
9.7 KiB
TypeScript

// frontend\src\aiLabels.ts
export type AiLabelGroup =
| 'people'
| 'position'
| 'body'
| 'object'
| 'clothing'
| 'unknown'
export const AI_POSITION_LABELS = [
'unknown',
'missionary',
'doggy',
'doggystyle',
'cowgirl',
'reverse_cowgirl',
'cunnilingus',
'prone_bone',
'standing',
'standing_doggy',
'spooning',
'facesitting',
'handjob',
'blowjob',
'boobjob',
'toy_play',
'fingering',
'69',
] as const
export const AI_BODY_LABELS = [
'anus',
'ass',
'breasts',
'penis',
'tongue',
'pussy',
'vulva',
'buttocks',
] as const
export const AI_OBJECT_LABELS = [
'blindfold',
'buttplug',
'collar',
'dildo',
'handcuffs',
'shower',
'strapon',
'towel',
'vibrator',
] as const
export const AI_CLOTHING_LABELS = [
'bikini',
'bra',
'dress',
'heels',
'hotpants',
'lingerie',
'panties',
'skirt',
'stockings',
'croptop',
] as const
export const AI_PERSON_LABELS = [
'person',
'person_unknown',
'person_male',
'person_female',
'male_person',
'female_person',
'people_male',
'people_female',
'frau',
'mann',
] as const
export const AI_LABEL_FALLBACKS: Record<string, string> = {
vulva: 'Vagina',
pussy: 'Vagina',
penis: 'Penis',
anus: 'Anus',
ass: 'Hintern',
buttocks: 'Hintern',
breasts: 'Brüste',
tongue: 'Zunge',
blindfold: 'Augenbinde',
buttplug: 'Buttplug',
collar: 'Halsband',
dildo: 'Dildo',
handcuffs: 'Handschellen',
shower: 'Dusche',
strapon: 'Strapon',
towel: 'Handtuch',
vibrator: 'Vibrator',
bikini: 'Bikini',
bra: 'BH',
dress: 'Kleid',
heels: 'High Heels',
hotpants: 'Hotpants',
lingerie: 'Lingerie',
panties: 'Slip',
skirt: 'Rock',
stockings: 'Strümpfe',
croptop: 'Crop Top',
unknown: 'Unbekannt',
missionary: 'Missionary',
doggy: 'Doggy',
doggystyle: 'Doggy',
cowgirl: 'Cowgirl',
reverse_cowgirl: 'Reverse Cowgirl',
cunnilingus: 'Cunnilingus',
prone_bone: 'Prone Bone',
standing: 'Stehend',
standing_doggy: 'Standing Doggy',
spooning: 'Spooning',
facesitting: 'Facesitting',
handjob: 'Handjob',
blowjob: 'Blowjob',
boobjob: 'Boobjob',
toy_play: 'Toy Play',
fingering: 'Fingering',
'69': '69',
person: 'Person',
person_unknown: 'Person',
person_male: 'Mann',
person_female: 'Frau',
}
export const RATING_VALUE_LABELS: Record<string, string> = {
stars: 'Sterne',
score: 'Score',
actionScore: 'Action',
confidence: 'Konfidenz',
probability: 'Wahrscheinlichkeit',
nsfw: 'NSFW',
adult: 'Adult',
explicit: 'Explizit',
explicitness: 'Explizitheit',
nudity: 'Nacktheit',
sexual: 'Sexuell',
sexualActivity: 'Sexuelle Aktivität',
visibility: 'Sichtbarkeit',
quality: 'Qualität',
reason: 'Begründung',
summary: 'Zusammenfassung',
label: 'Label',
category: 'Kategorie',
model: 'Modell',
version: 'Version',
}
const POSITION_SET = new Set<string>(AI_POSITION_LABELS)
const BODY_SET = new Set<string>(AI_BODY_LABELS)
const OBJECT_SET = new Set<string>(AI_OBJECT_LABELS)
const CLOTHING_SET = new Set<string>(AI_CLOTHING_LABELS)
const PERSON_SET = new Set<string>(AI_PERSON_LABELS)
export function normalizeAiLabel(value: unknown): string {
return String(value ?? '')
.trim()
.toLowerCase()
.replace(/^detector:/, '')
.replace(/^position:/, '')
.replace(/^body:/, '')
.replace(/^object:/, '')
.replace(/^clothing:/, '')
}
export function titleCaseLabel(value: string): string {
return value
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.replace(/\b\w/g, (m) => m.toUpperCase())
}
export function prettyAiLabel(value: unknown): string {
const raw = String(value ?? '').trim()
if (!raw) return ''
const key = raw.toLowerCase()
if (key.startsWith('position:')) {
return prettyAiLabel(key.slice('position:'.length))
}
if (key.startsWith('object:')) {
return prettyAiLabel(key.slice('object:'.length))
}
if (key.startsWith('clothing:')) {
return prettyAiLabel(key.slice('clothing:'.length))
}
if (key.startsWith('body:')) {
return prettyAiLabel(key.slice('body:'.length))
}
if (key.startsWith('detector:')) {
return prettyAiLabel(key.slice('detector:'.length))
}
if (key.startsWith('combo:')) {
const rest = key.slice('combo:'.length)
return rest
.split('+')
.map((part) => prettyAiLabel(part))
.filter(Boolean)
.join(' + ')
}
const direct = AI_LABEL_FALLBACKS[key]
if (direct) return direct
return isAiLikeLabel(raw) ? titleCaseLabel(raw) : raw
}
export function aiLabelGroup(value: unknown): AiLabelGroup {
const label = normalizeAiLabel(value)
if (!label || label === 'unknown') return 'unknown'
if (PERSON_SET.has(label)) return 'people'
if (POSITION_SET.has(label)) return 'position'
if (BODY_SET.has(label)) return 'body'
if (OBJECT_SET.has(label)) return 'object'
if (CLOTHING_SET.has(label)) return 'clothing'
return 'unknown'
}
export function isKnownFrontendPositionLabel(value: unknown): boolean {
return POSITION_SET.has(normalizeAiLabel(value))
}
export function isKnownAiContentLabel(value: unknown): boolean {
const group = aiLabelGroup(value)
return (
group === 'position' ||
group === 'body' ||
group === 'object' ||
group === 'clothing'
)
}
export function isPersonSegmentLabel(value: unknown): boolean {
return PERSON_SET.has(normalizeAiLabel(value))
}
export function splitAiComboOrList(value: unknown): string[] {
return String(value ?? '')
.split(/[+,;|]+/g)
.map((part) => part.trim())
.filter(Boolean)
}
export function rawAiLabelParts(value: unknown): string[] {
const raw = String(value ?? '').trim().toLowerCase()
if (!raw) return []
if (raw.startsWith('combo:')) {
return raw
.slice('combo:'.length)
.split('+')
.map((part) => part.trim())
.filter(Boolean)
}
return [raw]
}
export function isAiLikeLabel(value: string): boolean {
const s = value.trim()
if (!s) return false
return (
s.toLowerCase() in AI_LABEL_FALLBACKS ||
/^[a-z0-9]+(?:_[a-z0-9]+)+$/i.test(s) ||
/^[a-z0-9]+(?:-[a-z0-9]+)+$/i.test(s)
)
}
export function isNonPersonHighlightLabel(value: unknown): boolean {
const parts = splitAiComboOrList(value)
for (const part of parts) {
const normalized = normalizeAiLabel(part)
if (!normalized) continue
if (!isPersonSegmentLabel(normalized)) {
return true
}
}
return false
}
export function segmentObjectHasNonPersonContext(
obj: Record<string, unknown>
): boolean {
const values: unknown[] = [
obj.tags,
obj.labels,
obj.classes,
obj.flags,
obj.summary,
obj.reason,
obj.description,
obj.text,
obj.caption,
obj.note,
]
for (const value of values) {
if (Array.isArray(value)) {
if (value.some((item) => isNonPersonHighlightLabel(item))) {
return true
}
continue
}
if (typeof value === 'string' && isNonPersonHighlightLabel(value)) {
return true
}
}
return false
}
export function isPersonSegmentObject(obj: Record<string, unknown>): boolean {
const rawLabel =
obj.label ??
obj.title ??
obj.category ??
obj.type ??
obj.kind ??
obj.name ??
''
if (!isPersonSegmentLabel(rawLabel)) {
return false
}
if (segmentObjectHasNonPersonContext(obj)) {
return false
}
return true
}
export function tagsFromAiLabel(
value: unknown,
opts?: {
prefixPositions?: boolean
}
): string[] {
const out: string[] = []
const prefixPositions = opts?.prefixPositions ?? true
for (const part of rawAiLabelParts(value)) {
const clean = part.trim()
if (!clean) continue
const normalized = normalizeAiLabel(clean)
if (clean.startsWith('position:') || isKnownFrontendPositionLabel(normalized)) {
const label = prettyAiLabel(clean)
if (label) out.push(prefixPositions ? `Position: ${label}` : label)
continue
}
if (
clean.startsWith('body:') ||
clean.startsWith('object:') ||
clean.startsWith('clothing:') ||
clean.startsWith('detector:')
) {
const label = prettyAiLabel(clean)
if (label) out.push(label)
continue
}
const label = prettyAiLabel(clean)
if (label) out.push(label)
}
return out
}
export function segmentTitleFromLabel(raw: string): string {
const parts = rawAiLabelParts(raw)
if (parts.length <= 1) {
return prettyAiLabel(raw)
}
const positionPart = parts.find((part) => {
const clean = normalizeAiLabel(part)
return part.startsWith('position:') || isKnownFrontendPositionLabel(clean)
})
if (positionPart) return prettyAiLabel(positionPart)
const objectPart = parts.find((part) => part.startsWith('object:'))
if (objectPart) return prettyAiLabel(objectPart)
const bodyPart = parts.find((part) => part.startsWith('body:'))
if (bodyPart) return prettyAiLabel(bodyPart)
const clothingPart = parts.find((part) => part.startsWith('clothing:'))
if (clothingPart) return prettyAiLabel(clothingPart)
return prettyAiLabel(parts[0])
}
export function normalizedPlainTagKey(tag: unknown): string {
return String(tag ?? '')
.trim()
.toLowerCase()
.replace(/^detector:/, '')
.replace(/^position:/, '')
.replace(/^body:/, '')
.replace(/^object:/, '')
.replace(/^clothing:/, '')
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
export function isDerivedFilterTag(tag: unknown): boolean {
const key = normalizedPlainTagKey(tag)
if (!key) return true
if (/^[1-5]\s*sterne?$/.test(key)) return true
if (/^[1-5]\s*stars?$/.test(key)) return true
if (/^[1-5]\s*\/\s*5$/.test(key)) return true
return [
'high rating',
'low rating',
'4k',
'full hd',
'hd',
'sd',
'kurz',
'lang',
].includes(key)
}