471 lines
10 KiB
TypeScript
471 lines
10 KiB
TypeScript
// frontend\src\aiLabels.ts
|
|
|
|
export type AiLabelGroup =
|
|
| 'people'
|
|
| 'position'
|
|
| 'body'
|
|
| 'object'
|
|
| 'clothing'
|
|
| 'unknown'
|
|
|
|
const NO_POSITION_LABELS = new Set(['', 'keine'])
|
|
|
|
export const AI_POSITION_LABELS = [
|
|
'keine',
|
|
'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',
|
|
|
|
keine: 'Keine',
|
|
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',
|
|
bestPosition: 'Top-Position',
|
|
bestPositionRank: 'Positionsrang',
|
|
bestClothing: 'Top-Kleidung',
|
|
bestClothingRank: 'Kleidungsrang',
|
|
positionScore: 'Position',
|
|
clothingScore: 'Kleidung',
|
|
durationScore: 'Dauer',
|
|
strengthScore: 'Stärke',
|
|
coverageScore: 'Abdeckung',
|
|
contextScore: 'Kontext',
|
|
varietyScore: 'Vielfalt',
|
|
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 (NO_POSITION_LABELS.has(label)) 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 {
|
|
const label = normalizeAiLabel(value)
|
|
return !NO_POSITION_LABELS.has(label) && POSITION_SET.has(label)
|
|
}
|
|
|
|
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)
|
|
}
|