// 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 = { 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 = { 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(AI_POSITION_LABELS) const BODY_SET = new Set(AI_BODY_LABELS) const OBJECT_SET = new Set(AI_OBJECT_LABELS) const CLOTHING_SET = new Set(AI_CLOTHING_LABELS) const PERSON_SET = new Set(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 ): 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): 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) }