This commit is contained in:
Linrador 2026-06-06 20:50:41 +02:00
parent 1a559fe76d
commit 6b333ee78a
7 changed files with 241 additions and 214 deletions

View File

@ -2462,6 +2462,7 @@ type analyzeLabelSegmentPoint struct {
Label string Label string
Start float64 Start float64
End float64 End float64
Time float64
Score float64 Score float64
} }
@ -2550,10 +2551,19 @@ func buildLabelContinuitySegmentsFromAnalyzeHits(
score = 1 score = 1
} }
markerTime := hit.Time
if markerTime < 0 {
markerTime = (start + end) / 2
}
if duration > 0 {
markerTime = math.Max(0, math.Min(markerTime, duration))
}
byLabel[key] = append(byLabel[key], analyzeLabelSegmentPoint{ byLabel[key] = append(byLabel[key], analyzeLabelSegmentPoint{
Label: label, Label: label,
Start: start, Start: start,
End: end, End: end,
Time: markerTime,
Score: score, Score: score,
}) })
} }
@ -2580,6 +2590,8 @@ func buildLabelContinuitySegmentsFromAnalyzeHits(
curEndMarker := points[0].End curEndMarker := points[0].End
curScoreSum := points[0].Score curScoreSum := points[0].Score
curScoreCount := 1 curScoreCount := 1
curMarkerTime := points[0].Time
curBestScore := points[0].Score
for i := 1; i < len(points); i++ { for i := 1; i < len(points); i++ {
n := points[i] n := points[i]
@ -2598,6 +2610,10 @@ func buildLabelContinuitySegmentsFromAnalyzeHits(
curScoreSum += n.Score curScoreSum += n.Score
curScoreCount++ curScoreCount++
if n.Score > curBestScore {
curBestScore = n.Score
curMarkerTime = n.Time
}
continue continue
} }
@ -2605,6 +2621,7 @@ func buildLabelContinuitySegmentsFromAnalyzeHits(
curLabel, curLabel,
curStartMarker, curStartMarker,
curEndMarker, curEndMarker,
curMarkerTime,
curScoreSum, curScoreSum,
curScoreCount, curScoreCount,
halfSample, halfSample,
@ -2620,12 +2637,15 @@ func buildLabelContinuitySegmentsFromAnalyzeHits(
curEndMarker = n.End curEndMarker = n.End
curScoreSum = n.Score curScoreSum = n.Score
curScoreCount = 1 curScoreCount = 1
curMarkerTime = n.Time
curBestScore = n.Score
} }
segment := makeLabelContinuitySegment( segment := makeLabelContinuitySegment(
curLabel, curLabel,
curStartMarker, curStartMarker,
curEndMarker, curEndMarker,
curMarkerTime,
curScoreSum, curScoreSum,
curScoreCount, curScoreCount,
halfSample, halfSample,
@ -2654,6 +2674,7 @@ func makeLabelContinuitySegment(
label string, label string,
startMarker float64, startMarker float64,
endMarker float64, endMarker float64,
markerTime float64,
scoreSum float64, scoreSum float64,
scoreCount int, scoreCount int,
halfSample float64, halfSample float64,
@ -2680,6 +2701,13 @@ func makeLabelContinuitySegment(
score = scoreSum / float64(scoreCount) score = scoreSum / float64(scoreCount)
} }
if markerTime < 0 {
markerTime = (start + end) / 2
}
if duration > 0 {
markerTime = math.Max(0, math.Min(markerTime, duration))
}
return aiSegmentMeta{ return aiSegmentMeta{
Label: label, Label: label,
StartSeconds: start, StartSeconds: start,
@ -2689,6 +2717,8 @@ func makeLabelContinuitySegment(
AutoSelected: true, AutoSelected: true,
Position: segmentPositionFromAnalyzeLabel(label), Position: segmentPositionFromAnalyzeLabel(label),
Tags: segmentTagsFromAnalyzeLabel(label), Tags: segmentTagsFromAnalyzeLabel(label),
MarkerSeconds: markerTime,
PreviewSeconds: markerTime,
} }
} }

View File

@ -104,6 +104,8 @@ type aiSegmentMeta struct {
AutoSelected bool `json:"autoSelected,omitempty"` AutoSelected bool `json:"autoSelected,omitempty"`
Position string `json:"position,omitempty"` Position string `json:"position,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
MarkerSeconds float64 `json:"markerSeconds,omitempty"`
PreviewSeconds float64 `json:"previewSeconds,omitempty"`
} }
type aiAnalysisMeta struct { type aiAnalysisMeta struct {

View File

@ -535,85 +535,6 @@ func refreshMyFreeCamsSnapshotNow(ctx context.Context, store *ModelStore) (time.
return fetchedAt, okCount, nil return fetchedAt, okCount, nil
} }
func startMyFreeCamsOnlinePoller(store *ModelStore) {
const interval = mfcOnlinePollInterval
if store != nil {
setMyFreeCamsOnlineModelStore(store)
}
lastLoggedErr := ""
refreshOnce := func(trigger string) {
if !myFreeCamsProviderAPIEnabled() {
if verboseLogs() {
appLogln(" [myfreecams] online refresh skipped: provider API disabled")
}
return
}
mfcOnlineRefreshMu.Lock()
if mfcOnlineRefreshInFlight {
mfcOnlineRefreshMu.Unlock()
return
}
mfcOnlineRefreshInFlight = true
mfcOnlineRefreshMu.Unlock()
go func() {
defer func() {
mfcOnlineRefreshMu.Lock()
mfcOnlineRefreshInFlight = false
mfcOnlineRefreshMu.Unlock()
}()
fetchedAt, count, err := refreshMyFreeCamsSnapshotNow(context.Background(), store)
if err != nil {
msg := err.Error()
if strings.Contains(msg, "usernameLookup pausiert") {
if verboseLogs() && msg != lastLoggedErr {
appLogln("⏸️ [myfreecams] online refresh skipped:", msg)
lastLoggedErr = msg
}
return
}
if msg != lastLoggedErr {
appLogln("⚠️ [myfreecams] online refresh failed:", msg)
lastLoggedErr = msg
}
return
}
if lastLoggedErr != "" {
appLogln("✅ [myfreecams] online refresh recovered")
lastLoggedErr = ""
}
if verboseLogs() {
appLogln(
"✅ [myfreecams] checked models:",
count,
"snapshot",
fetchedAt.Format(time.RFC3339),
"trigger="+trigger,
)
}
}()
}
refreshOnce("startup")
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
refreshOnce("ticker")
}
}
type mfcOnlineResponse struct { type mfcOnlineResponse struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
FetchedAt time.Time `json:"fetchedAt"` FetchedAt time.Time `json:"fetchedAt"`
@ -630,107 +551,16 @@ func myfreecamsOnlineHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
wantRefresh := false
var users []string
if r.Method == http.MethodPost {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req struct {
Q []string `json:"q"`
Refresh bool `json:"refresh"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
wantRefresh = req.Refresh
for _, u := range req.Q {
u = strings.ToLower(strings.TrimSpace(u))
if u != "" {
users = append(users, u)
}
}
} else {
qRefresh := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("refresh")))
wantRefresh = qRefresh == "1" || qRefresh == "true" || qRefresh == "yes"
qUsers := strings.TrimSpace(r.URL.Query().Get("q"))
if qUsers != "" {
for _, u := range strings.Split(qUsers, ",") {
u = strings.ToLower(strings.TrimSpace(u))
if u != "" {
users = append(users, u)
}
}
}
}
store := mfcOnlineModelStore
if store == nil {
store = getModelStore()
}
if wantRefresh && store != nil {
if !myFreeCamsProviderAPIEnabled() {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
Enabled: false,
LastError: "MyFreeCams API ist in den Einstellungen deaktiviert",
Rooms: []MyFreeCamsOnlineRoomLite{},
})
return
}
mfcOnlineRefreshMu.Lock()
if !mfcOnlineRefreshInFlight {
mfcOnlineRefreshInFlight = true
go func() {
defer func() {
mfcOnlineRefreshMu.Lock()
mfcOnlineRefreshInFlight = false
mfcOnlineRefreshMu.Unlock()
}()
_, _, _ = refreshMyFreeCamsSnapshotNow(context.Background(), store)
}()
}
mfcOnlineRefreshMu.Unlock()
}
mfcOnlineMu.RLock()
fetchedAt := mfcOnline.FetchedAt
lastAttempt := mfcOnline.LastAttempt
lastErr := mfcOnline.LastErr
liteByUser := mfcOnline.LiteByUser
mfcOnlineMu.RUnlock()
total := 0
if liteByUser != nil {
total = len(liteByUser)
}
rooms := make([]MyFreeCamsOnlineRoomLite, 0)
if len(users) > 0 {
for _, u := range users {
if rm, ok := liteByUser[u]; ok {
rooms = append(rooms, rm)
}
}
}
if fetchedAt.IsZero() && lastErr == "" {
lastErr = "warming up"
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{ _ = json.NewEncoder(w).Encode(mfcOnlineResponse{
Enabled: true, Enabled: false,
FetchedAt: fetchedAt, FetchedAt: time.Time{},
LastAttempt: lastAttempt, LastAttempt: time.Time{},
Count: len(rooms), Count: 0,
Total: total, Total: 0,
LastError: lastErr, LastError: "MyFreeCams Online-Prüfung deaktiviert; usernameLookup nur über biocontext bei Bedarf.",
Rooms: rooms, Rooms: []MyFreeCamsOnlineRoomLite{},
}) })
} }

View File

@ -23,11 +23,11 @@ type aiRatingMeta struct {
const ( const (
// Normales Zusammenziehen sehr kurzer Pausen. // Normales Zusammenziehen sehr kurzer Pausen.
ratingMaxSilentGapSec = 6.5 ratingMaxSilentGapSec = 4.5
// Größere Lücken dürfen überbrückt werden, // Größere Lücken dürfen überbrückt werden,
// wenn beide Seiten stark genug / thematisch ähnlich sind. // wenn beide Seiten stark genug / thematisch ähnlich sind.
ratingBridgeStrongGapSec = 35.0 ratingBridgeStrongGapSec = 12.0
ratingMinSegmentDurationSec = 2.5 ratingMinSegmentDurationSec = 2.5
) )
@ -743,7 +743,41 @@ func ratingBridgeStrengthFromSet(set ratingSignalSet) float64 {
return strength return strength
} }
func ratingPositionSetFromLabels(labels map[string]bool) map[string]bool {
out := map[string]bool{}
for label := range labels {
normalized := normalizeRatingSignalLabel(label)
if strings.HasPrefix(normalized, "position:") {
out[normalized] = true
}
}
return out
}
func ratingPositionSetsConflict(a map[string]bool, b map[string]bool) bool {
ap := ratingPositionSetFromLabels(a)
bp := ratingPositionSetFromLabels(b)
if len(ap) == 0 || len(bp) == 0 {
return false
}
for label := range ap {
if bp[label] {
return false
}
}
return true
}
func ratingLabelsBridgeCompatible(a map[string]bool, b map[string]bool) bool { func ratingLabelsBridgeCompatible(a map[string]bool, b map[string]bool) bool {
if ratingPositionSetsConflict(a, b) {
return false
}
for la := range a { for la := range a {
na := normalizeRatingSignalLabel(la) na := normalizeRatingSignalLabel(la)
if na == "" { if na == "" {
@ -765,9 +799,11 @@ func ratingLabelsBridgeCompatible(a map[string]bool, b map[string]bool) bool {
as := ratingSignalSetFromLabels(a) as := ratingSignalSetFromLabels(a)
bs := ratingSignalSetFromLabels(b) bs := ratingSignalSetFromLabels(b)
if as.HasPosition && bs.HasPosition { // Wichtig:
return true // Unterschiedliche Positionen NICHT pauschal verbinden.
} // Vorher hat "Doggy" + "Reverse Cowgirl" als kompatibel gegolten,
// nur weil beide irgendeine Position waren.
if as.HasBody && bs.HasBody { if as.HasBody && bs.HasBody {
return true return true
} }
@ -840,11 +876,99 @@ func mergeRatingActivitySegments(
}) })
type activityBlock struct { type activityBlock struct {
start float64 start float64
end float64 end float64
scoreSum float64 scoreSum float64
scoreWeight float64 scoreWeight float64
labels map[string]bool labels map[string]bool
positionWeights map[string]float64
marker float64
markerWeight float64
}
var addRatingPositionWeight func(weights map[string]float64, label string, weight float64)
addRatingPositionWeight = func(weights map[string]float64, label string, weight float64) {
label = strings.ToLower(strings.TrimSpace(label))
if label == "" {
return
}
if strings.HasPrefix(label, "combo:") {
raw := strings.TrimPrefix(label, "combo:")
for _, part := range strings.Split(raw, "+") {
addRatingPositionWeight(weights, part, weight)
}
return
}
normalized := normalizeRatingSignalLabel(label)
if strings.HasPrefix(normalized, "position:") {
weights[normalized] += weight
}
}
effectiveRatingLabelsForBlock := func(labels map[string]bool, positionWeights map[string]float64) map[string]bool {
out := map[string]bool{}
bestPosition := ""
bestWeight := -1.0
for label := range labels {
normalized := normalizeRatingSignalLabel(label)
if normalized == "" {
continue
}
if strings.HasPrefix(normalized, "position:") {
w := positionWeights[normalized]
if w <= 0 {
w = ratingSegmentSeverity(normalized)
}
if w > bestWeight {
bestWeight = w
bestPosition = normalized
}
continue
}
out[normalized] = true
}
if bestPosition != "" {
out[bestPosition] = true
}
return out
}
segmentMarker := func(s aiSegmentMeta) float64 {
if s.PreviewSeconds > 0 {
return s.PreviewSeconds
}
if s.MarkerSeconds > 0 {
return s.MarkerSeconds
}
if s.EndSeconds > s.StartSeconds {
return (s.StartSeconds + s.EndSeconds) / 2
}
return s.StartSeconds
}
segmentMarkerWeight := func(s aiSegmentMeta) float64 {
conf := ratingClamp01(s.Score)
if conf <= 0 {
conf = 0.50
}
sev := ratingSegmentSeverity(s.Label)
if sev <= 0 {
sev = 0.50
}
return conf * sev
} }
newBlock := func(s aiSegmentMeta) activityBlock { newBlock := func(s aiSegmentMeta) activityBlock {
@ -857,12 +981,21 @@ func mergeRatingActivitySegments(
conf = 0.50 conf = 0.50
} }
positionWeights := map[string]float64{}
addRatingPositionWeight(positionWeights, s.Label, conf*math.Max(1, dur))
marker := segmentMarker(s)
markerWeight := segmentMarkerWeight(s)
return activityBlock{ return activityBlock{
start: s.StartSeconds, start: s.StartSeconds,
end: s.EndSeconds, end: s.EndSeconds,
scoreSum: conf * dur, scoreSum: conf * dur,
scoreWeight: dur, scoreWeight: dur,
labels: labels, labels: labels,
positionWeights: positionWeights,
marker: marker,
markerWeight: markerWeight,
} }
} }
@ -872,7 +1005,8 @@ func mergeRatingActivitySegments(
return aiSegmentMeta{}, false return aiSegmentMeta{}, false
} }
label := buildRatingComboLabel(b.labels) effectiveLabels := effectiveRatingLabelsForBlock(b.labels, b.positionWeights)
label := buildRatingComboLabel(effectiveLabels)
if label == "" { if label == "" {
return aiSegmentMeta{}, false return aiSegmentMeta{}, false
} }
@ -901,6 +1035,8 @@ func mergeRatingActivitySegments(
AutoSelected: true, AutoSelected: true,
Position: segmentPositionFromAnalyzeLabel(label), Position: segmentPositionFromAnalyzeLabel(label),
Tags: segmentTagsFromAnalyzeLabel(label), Tags: segmentTagsFromAnalyzeLabel(label),
MarkerSeconds: b.marker,
PreviewSeconds: b.marker,
}, true }, true
} }
@ -956,8 +1092,17 @@ func mergeRatingActivitySegments(
if conf <= 0 { if conf <= 0 {
conf = 0.50 conf = 0.50
} }
cur.scoreSum += conf * dur cur.scoreSum += conf * dur
cur.scoreWeight += dur cur.scoreWeight += dur
addRatingPositionWeight(cur.positionWeights, n.Label, conf*math.Max(1, dur))
}
nextMarkerWeight := segmentMarkerWeight(n)
if nextMarkerWeight > cur.markerWeight {
cur.markerWeight = nextMarkerWeight
cur.marker = segmentMarker(n)
} }
addRatingLabelsFromSegment(cur.labels, n.Label) addRatingLabelsFromSegment(cur.labels, n.Label)

View File

@ -13,7 +13,7 @@
"useMyFreeCamsApi": true, "useMyFreeCamsApi": true,
"autoDeleteSmallDownloads": true, "autoDeleteSmallDownloads": true,
"autoDeleteSmallDownloadsBelowMB": 300, "autoDeleteSmallDownloadsBelowMB": 300,
"autoDeleteSmallDownloadsKeepFavorites": true, "autoDeleteSmallDownloadsKeepFavorites": false,
"lowDiskPauseBelowGB": 5, "lowDiskPauseBelowGB": 5,
"blurPreviews": false, "blurPreviews": false,
"teaserPlayback": "all", "teaserPlayback": "all",

View File

@ -715,7 +715,6 @@ func main() {
// Hintergrund-Worker // Hintergrund-Worker
go startDiskSpaceGuard() go startDiskSpaceGuard()
go startChaturbateOnlinePoller(store) go startChaturbateOnlinePoller(store)
go startMyFreeCamsOnlinePoller(store)
go startChaturbateAutoStartWorker(store) go startChaturbateAutoStartWorker(store)
go startMyFreeCamsAutoStartWorker(store) go startMyFreeCamsAutoStartWorker(store)

View File

@ -888,22 +888,13 @@ function segmentAnchorTime(
obj: Record<string, unknown>, obj: Record<string, unknown>,
durationSeconds: number durationSeconds: number
): number | undefined { ): number | undefined {
const start = firstSegmentTimeNumber(obj, [
'startSec',
'startSeconds',
'start_s',
'start',
'from',
'startMs',
'startRatio',
'startPercent',
'startPct',
], durationSeconds)
const marker = firstSegmentTimeNumber(obj, [ const marker = firstSegmentTimeNumber(obj, [
'markerTime', 'previewSeconds',
'markerSec', 'previewSec',
'previewTime',
'markerSeconds', 'markerSeconds',
'markerSec',
'markerTime',
'time', 'time',
't', 't',
'timestamp', 'timestamp',
@ -918,18 +909,41 @@ function segmentAnchorTime(
'timestampPct', 'timestampPct',
], durationSeconds) ], durationSeconds)
if (typeof start === 'number') return start
if (typeof marker === 'number') return marker if (typeof marker === 'number') return marker
return undefined const start = firstSegmentTimeNumber(obj, [
'startSec',
'startSeconds',
'start_s',
'start',
'from',
'startMs',
'startRatio',
'startPercent',
'startPct',
], durationSeconds)
return typeof start === 'number' ? start : undefined
} }
function segmentPreviewTime( function segmentPreviewTime(
obj: Record<string, unknown>, obj: Record<string, unknown>,
durationSeconds: number durationSeconds: number
): number | undefined { ): number | undefined {
const anchor = segmentAnchorTime(obj, durationSeconds) const marker = segmentAnchorTime(obj, durationSeconds)
if (typeof anchor === 'number') return anchor if (typeof marker === 'number') return marker
const start = firstSegmentTimeNumber(obj, [
'startSec',
'startSeconds',
'start_s',
'start',
'from',
'startMs',
'startRatio',
'startPercent',
'startPct',
], durationSeconds)
const end = firstSegmentTimeNumber(obj, [ const end = firstSegmentTimeNumber(obj, [
'endSec', 'endSec',
@ -943,7 +957,14 @@ function segmentPreviewTime(
'endPct', 'endPct',
], durationSeconds) ], durationSeconds)
return typeof end === 'number' ? end : undefined if (typeof start === 'number' && typeof end === 'number') {
return Math.max(0, (start + end) / 2)
}
if (typeof start === 'number') return start
if (typeof end === 'number') return end
return undefined
} }
function segmentOpenTime( function segmentOpenTime(