diff --git a/Icons/boobjob.png b/Icons/boobjob.png new file mode 100644 index 0000000..8346c8b Binary files /dev/null and b/Icons/boobjob.png differ diff --git a/Icons/penis.png b/Icons/penis.png index 3c4b4f4..9b01da1 100644 Binary files a/Icons/penis.png and b/Icons/penis.png differ diff --git a/backend/ai_server.py b/backend/ai_server.py index 1e27733..7e042e7 100644 --- a/backend/ai_server.py +++ b/backend/ai_server.py @@ -1,4 +1,4 @@ -# backend/ai_server.py +# backend\ai_server.py import json import os diff --git a/backend/analyze.go b/backend/analyze.go index b72c76c..6dc7d60 100644 --- a/backend/analyze.go +++ b/backend/analyze.go @@ -1509,7 +1509,7 @@ func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []anal continue } - // Schwache Positions-Kontexte wie standing/sitting nicht alleine als Segment anzeigen. + // Schwache Positions-Kontexte wie standing nicht alleine als Segment anzeigen. if sig.Group == "position" && segmentSeverityWeight(label) < 0.70 { continue } diff --git a/backend/data/pending-autostart/admin.json b/backend/data/pending-autostart/admin.json deleted file mode 100644 index fc69ce2..0000000 --- a/backend/data/pending-autostart/admin.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "items": [] -} \ No newline at end of file diff --git a/backend/embed.go b/backend/embed.go index 2885023..68c3a01 100644 --- a/backend/embed.go +++ b/backend/embed.go @@ -8,7 +8,7 @@ import ( "path/filepath" ) -//go:embed ml/*.py ml/*.json ai_server.py .env recorder-cert.pem recorder-key.pem +//go:embed ml/*.py ai_server.py .env recorder-cert.pem recorder-key.pem generated/training/detection_labels.json var embeddedMLFiles embed.FS func trainingEmbeddedMLDir() (string, error) { @@ -33,6 +33,10 @@ func trainingEmbeddedMLDir() (string, error) { for _, name := range append(files, optionalFiles...) { srcPath := filepath.ToSlash(filepath.Join("ml", name)) + if name == "detection_labels.json" { + srcPath = "generated/training/detection_labels.json" + } + b, err := embeddedMLFiles.ReadFile(srcPath) if err != nil { // Pflichtdateien müssen vorhanden sein. diff --git a/backend/ml/detection_labels.json b/backend/ml/detection_labels.json deleted file mode 100644 index 286ffe8..0000000 --- a/backend/ml/detection_labels.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "people": [ - "person_female", - "person_male" - ], - "sexPositions": [ - "unknown", - "missionary", - "doggy", - "cowgirl", - "reverse_cowgirl", - "cunnilingus", - "prone_bone", - "standing", - "standing_doggy", - "spooning", - "facesitting", - "handjob", - "blowjob", - "boobjob", - "toy_play", - "fingering", - "69" - ], - "bodyParts": [ - "anus", - "ass", - "breasts", - "penis", - "tongue", - "pussy" - ], - "objects": [ - "blindfold", - "buttplug", - "collar", - "dildo", - "handcuffs", - "shower", - "strapon", - "towel", - "vibrator" - ], - "clothing": [ - "bikini", - "bra", - "dress", - "heels", - "hotpants", - "lingerie", - "panties", - "skirt", - "stockings", - "croptop" - ] -} \ No newline at end of file diff --git a/backend/rating.go b/backend/rating.go index cf90c7c..c589ffb 100644 --- a/backend/rating.go +++ b/backend/rating.go @@ -451,7 +451,7 @@ func contextualSegmentSeverityWeight(label string) float64 { score += 0.015 } - // Standing/Sitting ohne Kontext bleibt schwach. + // Standing ohne Kontext bleibt schwach. if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing { score = math.Min(score, 0.42) } diff --git a/backend/server.go b/backend/server.go index 1a3c6d4..f803a14 100644 --- a/backend/server.go +++ b/backend/server.go @@ -464,9 +464,22 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) { trainingRoot, appBaseDir := findTrainingRootDir(scriptDir) + mlDir, mlErr := trainingEmbeddedMLDir() + if mlErr != nil { + appLogln("⚠️ embedded ML-Dateien konnten nicht extrahiert werden:", mlErr) + } else { + appLogln("✅ embedded ML-Dateien extrahiert:", mlDir) + } + detectionLabelsPath := strings.TrimSpace(os.Getenv("DETECTION_LABELS_PATH")) if detectionLabelsPath == "" { - detectionLabelsPath = filepath.Join(trainingRoot, "detection_labels.json") + if mlErr == nil && strings.TrimSpace(mlDir) != "" { + // ai_server.py soll die entpackte embedded detection_labels.json verwenden. + detectionLabelsPath = filepath.Join(mlDir, "detection_labels.json") + } else { + // Fallback für Entwicklung / falls embedded extraction fehlschlägt. + detectionLabelsPath = filepath.Join(trainingRoot, "detection_labels.json") + } } else { detectionLabelsPath = filepath.Clean(detectionLabelsPath) } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dccd439..8ee2c07 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4301,7 +4301,7 @@ export default function App() { setSplitModalOpen(false) setSplitJob(null) }} - onApply={({ job, splits, segments, deleteOriginal }) => { + onApply={async ({ job, splits, segments, deleteOriginal, onProgress }) => { const file = baseName(job.output || '') if (!file) { throw new Error('Kein Dateiname vorhanden.') @@ -4338,13 +4338,18 @@ export default function App() { return segments[pos - 1] ?? null } - const publish = ( - progress: Omit - ) => { - pushSplitProgress(file, progress) + type ModalSplitProgress = Parameters>[0] + + const publish = (progress: ModalSplitProgress) => { + pushSplitProgress( + file, + progress as Omit + ) + + onProgress?.(progress) } - void (async () => { + return await (async () => { let buffer = '' let finalData: any = null let total = totalFallback @@ -4562,6 +4567,7 @@ export default function App() { }) notify.error('Split fehlgeschlagen', `${file} — ${message}`) + throw err } })() }} diff --git a/frontend/src/aiLabels.ts b/frontend/src/aiLabels.ts index 8d0b222..90e8830 100644 --- a/frontend/src/aiLabels.ts +++ b/frontend/src/aiLabels.ts @@ -20,7 +20,6 @@ export const AI_POSITION_LABELS = [ 'standing', 'standing_doggy', 'spooning', - 'sitting', 'facesitting', 'handjob', 'blowjob', @@ -121,7 +120,6 @@ export const AI_LABEL_FALLBACKS: Record = { standing: 'Stehend', standing_doggy: 'Standing Doggy', spooning: 'Spooning', - sitting: 'Sitzend', facesitting: 'Facesitting', handjob: 'Handjob', blowjob: 'Blowjob', diff --git a/frontend/src/components/ui/FinishedDownloadsCardsView.tsx b/frontend/src/components/ui/FinishedDownloadsCardsView.tsx index a9252dd..7b580f8 100644 --- a/frontend/src/components/ui/FinishedDownloadsCardsView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsCardsView.tsx @@ -1324,10 +1324,6 @@ function FinishedDownloadsCardsView({ doubleTapMaxMovePx={48} pressDelayMs={220} enablePressHold={canSpeedHold} - verticalThresholdPx={92} - verticalThresholdRatio={0.20} - upAction={{ label: 'Öffnen' }} - downAction={{ label: 'Weiter' }} onDoubleTap={async () => { if (isHot) return await onToggleHot?.(j) @@ -1361,16 +1357,6 @@ function FinishedDownloadsCardsView({ setMobileTopTeaserEnabled(false) return await keepVideo(j) }} - onSwipeUp={() => { - setMobileTopTeaserEnabled(false) - openPlayer(j) - return false - }} - onSwipeDown={() => { - setMobileTopTeaserEnabled(false) - skipTopMobileCard() - return true - }} onPressStart={(info) => { if (info.pointerType === 'mouse' && info.button !== 0) return if (!canSpeedHold) return @@ -1397,10 +1383,6 @@ function FinishedDownloadsCardsView({ {mobileTopRow ? (
-
- ← Löschen · → Behalten · ↑ Öffnen · ↓ Weiter · Doppeltipp HOT · Halten schneller -
- {mobileOrderedRows.length > 1 ? ( ) : null} - +
{!isDraft ? ( diff --git a/frontend/src/components/ui/VideoSplitModal.tsx b/frontend/src/components/ui/VideoSplitModal.tsx index 7d42ff3..c2870d7 100644 --- a/frontend/src/components/ui/VideoSplitModal.tsx +++ b/frontend/src/components/ui/VideoSplitModal.tsx @@ -860,6 +860,143 @@ function normalizeAiSegmentsFromHits( }) } +type SegmentVisualKind = 'position' | 'toy' | 'clothing' | 'body' | 'person' | 'default' + +function segmentVisualKindFromText(value: unknown): SegmentVisualKind { + const clean = String(value ?? '').trim().toLowerCase() + if (!clean) return 'default' + + if ( + clean.includes('person_male') || + clean.includes('male_person') || + clean.includes('person_female') || + clean.includes('female_person') || + clean === 'person' || + clean.startsWith('person:') + ) { + return 'person' + } + + if ( + clean.startsWith('position:') || + clean.includes('missionary') || + clean.includes('doggy') || + clean.includes('doggystyle') || + clean.includes('cowgirl') || + clean.includes('reverse cowgirl') || + clean.includes('reverse_cowgirl') || + clean.includes('cunnilingus') || + clean.includes('prone bone') || + clean.includes('prone_bone') || + clean.includes('standing') || + clean.includes('spooning') || + clean.includes('facesitting') || + clean.includes('handjob') || + 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('toy play') || + clean.includes('toy_play') || + clean === '69' + ) { + return 'position' + } + + if ( + clean.includes('dildo') || + clean.includes('vibrator') || + clean.includes('buttplug') || + clean.includes('strapon') || + clean.includes('toy') + ) { + return 'toy' + } + + if ( + clean.includes('lingerie') || + clean.includes('bikini') || + clean.includes('bra') || + clean.includes('bh') || + clean.includes('panties') || + clean.includes('slip') || + clean.includes('stocking') || + clean.includes('strümpfe') || + clean.includes('skirt') || + clean.includes('rock') || + clean.includes('dress') || + clean.includes('kleid') || + clean.includes('heels') + ) { + return 'clothing' + } + + if ( + clean.includes('vagina') || + clean.includes('vulva') || + clean.includes('pussy') || + clean.includes('penis') || + clean.includes('anus') || + clean.includes('ass') || + clean.includes('hintern') || + clean.includes('breasts') || + clean.includes('brüste') || + clean.includes('buttocks') + ) { + return 'body' + } + + return 'default' +} + +function segmentVisualKind(seg: Segment): SegmentVisualKind { + const rawLabel = String(seg.label || '').trim() + const prettyLabel = getSegmentLabelText(rawLabel) + + const values = [rawLabel, prettyLabel] + + for (const value of values) { + const kind = segmentVisualKindFromText(value) + if (kind !== 'default') return kind + } + + return 'default' +} + +function reindexSegments(input: Segment[]): Segment[] { + return input.map((seg, idx) => ({ + ...seg, + index: idx + 1, + })) +} + +function filterSegmentsLikeRatingOverlay(input: Segment[]): Segment[] { + const withoutPeople = input.filter((seg) => segmentVisualKind(seg) !== 'person') + + const positionSegments = withoutPeople.filter((seg) => ( + segmentVisualKind(seg) === 'position' + )) + + if (positionSegments.length > 0) { + return reindexSegments(positionSegments) + } + + const bodyOrClothingSegments = withoutPeople.filter((seg) => { + const kind = segmentVisualKind(seg) + return kind === 'clothing' || kind === 'body' + }) + + if (bodyOrClothingSegments.length > 0) { + return reindexSegments(bodyOrClothingSegments) + } + + return reindexSegments(withoutPeople) +} + function shouldAutoSelectAiHit(hit: { label: string }): boolean { @@ -1085,7 +1222,7 @@ export default function VideoSplitModal({ mapAiTimes ) - const metaAiSegments = + const rawMetaAiSegments = metaAiSegmentsFromMeta.length > 0 ? metaAiSegmentsFromMeta : normalizeAiSegmentsFromHits( @@ -1094,6 +1231,8 @@ export default function VideoSplitModal({ mapAiTimes ) + const metaAiSegments = filterSegmentsLikeRatingOverlay(rawMetaAiSegments) + if (!cancelled) { setMediaDuration(metaDuration > 0 ? metaDuration : 0) setAiHits(metaAiHits) @@ -1610,6 +1749,31 @@ export default function VideoSplitModal({ ) }, [job?.output, anyJob?.startedAt, anyJob?.endedAt, anyJob?.createdAt, anyJob?.fileCreatedAt, anyJob?.ctime]) + const showSplitProgress = Boolean( + splitProgress && + ( + busy || + splitProgress.phase === 'done' || + splitProgress.phase === 'error' + ) + ) + + const splitOverallPercent = Math.round( + clamp(splitProgress?.overallProgress ?? 0, 0, 1) * 100 + ) + + const splitSegmentPercent = Math.round( + clamp(splitProgress?.segmentProgress ?? 0, 0, 1) * 100 + ) + + const splitCurrentLabel = splitProgress?.segment?.label + ? getSegmentLabelText(splitProgress.segment.label) + : '' + + const splitCurrentTime = splitProgress?.segment + ? `${formatTime(splitProgress.segment.start)} → ${formatTime(splitProgress.segment.end)}` + : '' + return ( - {segmentsToApply.length === 1 - ? '1 Datei erzeugen' - : `${segmentsToApply.length} Dateien erzeugen`} + {busy + ? 'Wird erzeugt…' + : segmentsToApply.length === 1 + ? '1 Datei erzeugen' + : `${segmentsToApply.length} Dateien erzeugen`} } @@ -2105,6 +2271,73 @@ export default function VideoSplitModal({ + {showSplitProgress ? ( +
+
+
+
+ {splitProgress?.phase === 'done' + ? 'Split abgeschlossen' + : splitProgress?.phase === 'error' + ? 'Split fehlgeschlagen' + : 'Split läuft…'} +
+ +
+ {splitProgress?.message || 'Bitte warten…'} +
+ + {splitCurrentLabel ? ( +
+ {splitCurrentLabel} + {splitCurrentTime ? ` · ${splitCurrentTime}` : ''} +
+ ) : null} +
+ +
+
+ {splitOverallPercent}% +
+ +
+ {splitProgress?.completed ?? 0}/{splitProgress?.total ?? segmentsToApply.length} +
+
+
+ +
+
+
+ + {splitProgress?.phase === 'splitting' ? ( +
+ Aktuelles Segment + {splitSegmentPercent}% +
+ ) : null} +
+ ) : null} +
{displaySegmentGroups.map((group, groupIdx) => { const firstSeg = group.segments[0]