added boobjob position

This commit is contained in:
Linrador 2026-06-09 20:10:03 +02:00
parent d55c6b5854
commit 98e7bc70f2
8 changed files with 125 additions and 18 deletions

View File

@ -195,7 +195,16 @@ func ensureAnalyzeAllGoalsForVideoCtxForce(
return false, err return false, err
} }
return hasAIAnalysisForAllOutputGoals(videoPath, requiredAnalyzeGoals()), nil analysisReady := hasAIAnalysisForAllOutputGoals(videoPath, requiredAnalyzeGoals())
if !analysisReady {
return false, nil
}
// Direkt nach erfolgreicher Analyse löschen.
// Wichtig: hier rating übergeben, nicht nil.
autoDeleteLowRatedDownloadAfterAnalysis(actx, videoPath, rating)
return true, nil
} }
func assetsTruthForVideo(videoPath string) finishedPhaseTruth { func assetsTruthForVideo(videoPath string) finishedPhaseTruth {
@ -897,10 +906,18 @@ func ensureAnalyzeForVideoCtx(
return false, perr return false, perr
} }
if _, err := ensureAnalyzeAllGoalsForVideoCtx(ctx, videoPath, sourceURL); err != nil { analyzed, err := ensureAnalyzeAllGoalsForVideoCtx(ctx, videoPath, sourceURL)
if err != nil {
return false, err return false, err
} }
if analyzed {
if _, statErr := os.Stat(videoPath); os.IsNotExist(statErr) {
// Analyse war erfolgreich, Datei wurde danach absichtlich durch AutoDelete gelöscht.
return true, nil
}
}
return hasAIAnalysisForOutputGoal(videoPath, goal), nil return hasAIAnalysisForOutputGoal(videoPath, goal), nil
} }
@ -926,10 +943,18 @@ func ensureDeferredAssetsAndAI(ctx context.Context, videoPath, sourceURL string,
} }
// Analyse genau einmal für alle Standard-Ziele. // Analyse genau einmal für alle Standard-Ziele.
if _, err := ensureAnalyzeAllGoalsForVideoCtx(ctx, videoPath, sourceURL); err != nil { analyzed, err := ensureAnalyzeAllGoalsForVideoCtx(ctx, videoPath, sourceURL)
if err != nil {
return appErrorf("deferred analyze failed for %s: %w", videoPath, err) return appErrorf("deferred analyze failed for %s: %w", videoPath, err)
} }
if analyzed {
if _, statErr := os.Stat(videoPath); os.IsNotExist(statErr) {
// Analyse war erfolgreich, Datei wurde danach absichtlich durch AutoDelete gelöscht.
return nil
}
}
goal = "highlights" goal = "highlights"
if !hasAIAnalysisForOutputGoal(videoPath, goal) { if !hasAIAnalysisForOutputGoal(videoPath, goal) {

View File

@ -17,6 +17,7 @@
"facesitting", "facesitting",
"handjob", "handjob",
"blowjob", "blowjob",
"boobjob",
"toy_play", "toy_play",
"fingering", "fingering",
"69" "69"

View File

@ -125,6 +125,7 @@ func isKnownPositionLabel(label string) bool {
"facesitting", "facesitting",
"handjob", "handjob",
"blowjob", "blowjob",
"boobjob",
"toy_play", "toy_play",
"fingering", "fingering",
"69": "69":
@ -144,7 +145,7 @@ func positionSeverityWeight(label string) float64 {
return 0.98 return 0.98
case "missionary", "prone_bone": case "missionary", "prone_bone":
return 0.95 return 0.95
case "blowjob", "cunnilingus", "69", "facesitting": case "blowjob", "cunnilingus", "69", "facesitting", "boobjob":
return 0.94 return 0.94
case "toy_play": case "toy_play":
return 0.88 return 0.88

View File

@ -942,9 +942,6 @@ func runGenerateMissingAssetsJob(jobID string, ctx context.Context) {
publishAssetsTaskPhase(it.name, "enrich", "analyze", "error", "Analyse fehlgeschlagen") publishAssetsTaskPhase(it.name, "enrich", "analyze", "error", "Analyse fehlgeschlagen")
} else if assetsTaskTruthForVideo(id, it.path).AnalyzeReady { } else if assetsTaskTruthForVideo(id, it.path).AnalyzeReady {
publishAssetsTaskPhase(it.name, "enrich", "analyze", "done", "Analyse") publishAssetsTaskPhase(it.name, "enrich", "analyze", "done", "Analyse")
// Nach erfolgreicher Hintergrund-Analyse optional nach Rating löschen.
autoDeleteLowRatedDownloadAfterAnalysis(fileCtx, it.path, nil)
} else { } else {
publishAssetsTaskPhase(it.name, "enrich", "analyze", "error", "Analyse fehlgeschlagen") publishAssetsTaskPhase(it.name, "enrich", "analyze", "error", "Analyse fehlgeschlagen")
} }

View File

@ -739,6 +739,11 @@ func autoDeleteLowRatedDownloadAfterAnalysis(ctx context.Context, videoPath stri
default: default:
} }
videoPath = strings.TrimSpace(videoPath)
if videoPath == "" {
return
}
s := getSettings() s := getSettings()
if !s.AutoDeleteLowRatedDownloads { if !s.AutoDeleteLowRatedDownloads {
return return
@ -747,6 +752,7 @@ func autoDeleteLowRatedDownloadAfterAnalysis(ctx context.Context, videoPath stri
maxStars := clampAutoDeleteRatingStars(s.AutoDeleteLowRatedDownloadsMaxStars) maxStars := clampAutoDeleteRatingStars(s.AutoDeleteLowRatedDownloadsMaxStars)
stars := 0 stars := 0
if rating != nil { if rating != nil {
stars = rating.Stars stars = rating.Stars
if stars <= 0 && rating.Score > 0 { if stars <= 0 && rating.Score > 0 {
@ -754,21 +760,34 @@ func autoDeleteLowRatedDownloadAfterAnalysis(ctx context.Context, videoPath stri
} }
} }
// Falls kein Rating direkt übergeben wurde, aus der gespeicherten meta.json lesen. // Wichtig für Background-/Regenerate-Analyse:
// Das brauchen wir für Asset-/Regenerate-Jobs, weil dort die Analyse erst geprüft // Dort wird rating teilweise als nil übergeben, obwohl es kurz vorher
// und danach gelöscht werden soll. // in meta.json gespeichert wurde. Dann lesen wir die Sterne aus der Meta.
if stars < 1 || stars > 5 { if stars <= 0 {
metaStars, ok, err := ratingStarsForVideoPath(videoPath) if metaStars, ok, err := ratingStarsForVideoPath(videoPath); err != nil {
if err != nil { appLogln("⚠️ [rating-delete] rating lookup failed:", filepath.Base(videoPath), err)
appLogln("⚠️ [rating-delete] rating aus meta konnte nicht gelesen werden:", filepath.Base(videoPath), err) } else if ok {
return
}
if ok {
stars = metaStars stars = metaStars
} }
} }
if stars < 1 || stars > 5 || stars > maxStars { if stars < 1 || stars > 5 {
appLogf(
" [rating-delete] übersprungen, kein gültiges Rating: %s stars=%d threshold<=%d",
filepath.Base(videoPath),
stars,
maxStars,
)
return
}
if stars > maxStars {
appLogf(
" [rating-delete] behalten: %s stars=%d threshold<=%d",
filepath.Base(videoPath),
stars,
maxStars,
)
return return
} }

View File

@ -1,3 +1,5 @@
// frontend\src\aiLabels.ts
export type AiLabelGroup = export type AiLabelGroup =
| 'people' | 'people'
| 'position' | 'position'
@ -22,6 +24,7 @@ export const AI_POSITION_LABELS = [
'facesitting', 'facesitting',
'handjob', 'handjob',
'blowjob', 'blowjob',
'boobjob',
'toy_play', 'toy_play',
'fingering', 'fingering',
'69', '69',
@ -122,6 +125,7 @@ export const AI_LABEL_FALLBACKS: Record<string, string> = {
facesitting: 'Facesitting', facesitting: 'Facesitting',
handjob: 'Handjob', handjob: 'Handjob',
blowjob: 'Blowjob', blowjob: 'Blowjob',
boobjob: 'Boobjob',
toy_play: 'Toy Play', toy_play: 'Toy Play',
fingering: 'Fingering', fingering: 'Fingering',
'69': '69', '69': '69',

View File

@ -63,6 +63,42 @@ export function FemaleBreastIcon(props: IconProps) {
) )
} }
export function BoobjobIcon(props: IconProps) {
const { title, className, ...rest } = props
return (
<svg
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={iconClassName(className, 'origin-center scale-[1.12]')}
aria-hidden={title ? undefined : true}
role={title ? 'img' : undefined}
{...rest}
>
{title ? <title>{title}</title> : null}
<FemaleBreastIcon
x="3"
y="20"
width="94"
height="58"
aria-hidden="true"
className="opacity-100"
/>
<MaleGenitaliaIcon
x="35"
y="25"
width="30"
height="54"
aria-hidden="true"
className="opacity-100"
/>
</svg>
)
}
export function ButtocksIcon(props: IconProps) { export function ButtocksIcon(props: IconProps) {
return ( return (
<IconBase <IconBase
@ -1010,6 +1046,24 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
text: 'Blowjob', text: 'Blowjob',
icon: BlowjobIcon, icon: BlowjobIcon,
}, },
{
match: [
'boobjob',
'boob_job',
'boob-job',
'boob job',
'titjob',
'tit_job',
'tit-job',
'tit job',
'titfuck',
'tit_fuck',
'tit-fuck',
'tit fuck',
],
text: 'Boobjob',
icon: BoobjobIcon,
},
{ {
match: ['cunnilingus'], match: ['cunnilingus'],
text: 'Cunnilingus', text: 'Cunnilingus',

View File

@ -1076,6 +1076,12 @@ function segmentKindFromText(value: unknown): SegmentVisualKind {
clean.includes('facesitting') || clean.includes('facesitting') ||
clean.includes('handjob') || clean.includes('handjob') ||
clean.includes('blowjob') || clean.includes('blowjob') ||
clean.includes('boobjob') ||
clean.includes('boob job') ||
clean.includes('boob_job') ||
clean.includes('titjob') ||
clean.includes('tit job') ||
clean.includes('tit_job') ||
clean.includes('fingering') || clean.includes('fingering') ||
clean.includes('toy play') || clean.includes('toy play') ||
clean.includes('toy_play') || clean.includes('toy_play') ||