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

View File

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

View File

@ -535,85 +535,6 @@ func refreshMyFreeCamsSnapshotNow(ctx context.Context, store *ModelStore) (time.
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 {
Enabled bool `json:"enabled"`
FetchedAt time.Time `json:"fetchedAt"`
@ -630,107 +551,16 @@ func myfreecamsOnlineHandler(w http.ResponseWriter, r *http.Request) {
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("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(mfcOnlineResponse{
Enabled: true,
FetchedAt: fetchedAt,
LastAttempt: lastAttempt,
Count: len(rooms),
Total: total,
LastError: lastErr,
Rooms: rooms,
Enabled: false,
FetchedAt: time.Time{},
LastAttempt: time.Time{},
Count: 0,
Total: 0,
LastError: "MyFreeCams Online-Prüfung deaktiviert; usernameLookup nur über biocontext bei Bedarf.",
Rooms: []MyFreeCamsOnlineRoomLite{},
})
}

View File

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

View File

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

View File

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

View File

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