bugfixes
This commit is contained in:
parent
19859b15be
commit
5679d19b30
Binary file not shown.
@ -123,18 +123,10 @@ CLOTHING_LABELS = LABEL_GROUPS["clothing"]
|
||||
POSITION_LABELS = set()
|
||||
|
||||
PERSON_LABELS = {
|
||||
"person",
|
||||
"person_male",
|
||||
"person_female",
|
||||
"person_unknown",
|
||||
"male_person",
|
||||
"female_person",
|
||||
}
|
||||
|
||||
MALE_LABELS = {"person_male", "male_person"}
|
||||
FEMALE_LABELS = {"person_female", "female_person"}
|
||||
UNKNOWN_PERSON_LABELS = {"person", "person_unknown"}
|
||||
|
||||
_MODEL_PATH = ""
|
||||
_MODEL_ERROR = ""
|
||||
_LABEL_ERROR = ""
|
||||
@ -161,12 +153,9 @@ def empty_prediction(source: str = "model_missing") -> dict:
|
||||
return {
|
||||
"modelAvailable": False,
|
||||
"source": source,
|
||||
"peopleCount": 0,
|
||||
"maleCount": 0,
|
||||
"femaleCount": 0,
|
||||
"unknownCount": 0,
|
||||
"sexPosition": "unknown",
|
||||
"sexPositionScore": 0.0,
|
||||
"peoplePresent": [],
|
||||
"bodyPartsPresent": [],
|
||||
"objectsPresent": [],
|
||||
"clothingPresent": [],
|
||||
@ -248,13 +237,6 @@ def load_label_groups_safe() -> None:
|
||||
PERSON_LABELS = {
|
||||
label for label in LABEL_GROUPS["people"]
|
||||
if label
|
||||
} | {
|
||||
"person",
|
||||
"person_male",
|
||||
"person_female",
|
||||
"person_unknown",
|
||||
"male_person",
|
||||
"female_person",
|
||||
}
|
||||
|
||||
|
||||
@ -307,6 +289,7 @@ def prediction_from_result(result) -> dict:
|
||||
names = result.names or {}
|
||||
|
||||
boxes_out = []
|
||||
people_present = []
|
||||
body_parts = []
|
||||
objects = []
|
||||
clothing = []
|
||||
@ -333,6 +316,21 @@ def prediction_from_result(result) -> dict:
|
||||
w = max(0.0, min(1.0 - x, w))
|
||||
h = max(0.0, min(1.0 - y, h))
|
||||
|
||||
is_person = label in PERSON_LABELS
|
||||
is_body = label in BODY_LABELS
|
||||
is_object = label in OBJECT_LABELS
|
||||
is_clothing = label in CLOTHING_LABELS
|
||||
is_position = label in POSITION_LABELS
|
||||
|
||||
if is_position:
|
||||
if score > sex_position_score:
|
||||
sex_position = label
|
||||
sex_position_score = score
|
||||
continue
|
||||
|
||||
if not (is_person or is_body or is_object or is_clothing):
|
||||
continue
|
||||
|
||||
boxes_out.append({
|
||||
"label": label,
|
||||
"score": score,
|
||||
@ -342,33 +340,24 @@ def prediction_from_result(result) -> dict:
|
||||
"h": h,
|
||||
})
|
||||
|
||||
if label in BODY_LABELS:
|
||||
if is_person:
|
||||
best_score(people_present, label, score)
|
||||
|
||||
if is_body:
|
||||
best_score(body_parts, label, score)
|
||||
|
||||
if label in OBJECT_LABELS:
|
||||
if is_object:
|
||||
best_score(objects, label, score)
|
||||
|
||||
if label in CLOTHING_LABELS:
|
||||
if is_clothing:
|
||||
best_score(clothing, label, score)
|
||||
|
||||
if label in POSITION_LABELS and score > sex_position_score:
|
||||
sex_position = label
|
||||
sex_position_score = score
|
||||
|
||||
people_count = sum(1 for box in boxes_out if box["label"] in PERSON_LABELS)
|
||||
male_count = sum(1 for box in boxes_out if box["label"] in MALE_LABELS)
|
||||
female_count = sum(1 for box in boxes_out if box["label"] in FEMALE_LABELS)
|
||||
unknown_count = sum(1 for box in boxes_out if box["label"] in UNKNOWN_PERSON_LABELS)
|
||||
|
||||
return {
|
||||
"modelAvailable": True,
|
||||
"source": f"yolo-server:{Path(_MODEL_PATH).name}",
|
||||
"peopleCount": people_count,
|
||||
"maleCount": male_count,
|
||||
"femaleCount": female_count,
|
||||
"unknownCount": unknown_count,
|
||||
"sexPosition": sex_position,
|
||||
"sexPositionScore": sex_position_score,
|
||||
"peoplePresent": people_present,
|
||||
"bodyPartsPresent": body_parts,
|
||||
"objectsPresent": objects,
|
||||
"clothingPresent": clothing,
|
||||
|
||||
@ -54,8 +54,13 @@ type videoFrameSample struct {
|
||||
|
||||
const (
|
||||
analyzeSegmentMergeGapSeconds = 8.0
|
||||
nsfwThresholdModerate = 0.35
|
||||
nsfwThresholdStrong = 0.60
|
||||
|
||||
// Ein Label darf kurz verschwinden, ohne dass ein neues Segment entsteht.
|
||||
// Bei 3s Frame-Intervall heißt das: ein fehlender Frame wird überbrückt.
|
||||
analyzeLabelInvisibleGraceSeconds = 3.0
|
||||
|
||||
nsfwThresholdModerate = 0.35
|
||||
nsfwThresholdStrong = 0.60
|
||||
|
||||
// Video-Modus: extrahiert 1 Frame alle N Sekunden.
|
||||
// 1 = jeder Sekunde, 3 = alle 3 Sekunden, 5 = alle 5 Sekunden.
|
||||
@ -109,26 +114,15 @@ func shouldAutoSelectAnalyzeHit(label string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
autoSelectedAILabelsOnce.Do(func() {
|
||||
autoSelectedAILabelsCache = autoSelectedAILabelSet()
|
||||
})
|
||||
|
||||
_, ok := autoSelectedAILabelsCache[label]
|
||||
labels := autoSelectedAILabelSet()
|
||||
_, ok := labels[label]
|
||||
return ok
|
||||
}
|
||||
|
||||
var nsfwIgnoredLabels = map[string]struct{}{
|
||||
// Personen sollen nicht als interessante Segmente auftauchen.
|
||||
"person": {},
|
||||
"person_male": {},
|
||||
"person_female": {},
|
||||
"person_unknown": {},
|
||||
"male_person": {},
|
||||
"female_person": {},
|
||||
|
||||
// Falls dein Detector irgendwann diese Varianten liefert:
|
||||
"people_male": {},
|
||||
"people_female": {},
|
||||
"person_male": {},
|
||||
"person_female": {},
|
||||
}
|
||||
|
||||
func isIgnoredNSFWLabel(label string) bool {
|
||||
@ -1084,26 +1078,43 @@ func appendNSFWHitFromPrediction(
|
||||
t float64,
|
||||
) []analyzeHit {
|
||||
if !pred.ModelAvailable {
|
||||
appLogln("⚠️ nsfw: modelAvailable=false bei", t)
|
||||
return hits
|
||||
}
|
||||
|
||||
nsfwResults := trainingPredictionToNSFWResults(pred)
|
||||
bestLabel, bestScore := pickBestNSFWResult(nsfwResults)
|
||||
if bestLabel == "" {
|
||||
if len(nsfwResults) == 0 {
|
||||
return hits
|
||||
}
|
||||
|
||||
if bestScore < nsfwThresholdForLabel(bestLabel) {
|
||||
return hits
|
||||
for _, r := range nsfwResults {
|
||||
label := strings.ToLower(strings.TrimSpace(r.Label))
|
||||
if label == "" || label == "unknown" {
|
||||
continue
|
||||
}
|
||||
if isIgnoredNSFWLabel(label) || isPersonSegmentLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
score := r.Score
|
||||
if score <= 0 {
|
||||
score = 1
|
||||
}
|
||||
|
||||
if score < nsfwThresholdForLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
hits = append(hits, analyzeHit{
|
||||
Time: t,
|
||||
Label: label,
|
||||
Score: score,
|
||||
Start: t,
|
||||
End: t,
|
||||
})
|
||||
}
|
||||
|
||||
return append(hits, analyzeHit{
|
||||
Time: t,
|
||||
Label: bestLabel,
|
||||
Score: bestScore,
|
||||
Start: t,
|
||||
End: t,
|
||||
})
|
||||
return hits
|
||||
}
|
||||
|
||||
type highlightSignal struct {
|
||||
@ -1351,15 +1362,14 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
||||
addHighlightSignal(best, "detector:"+label, box.Score)
|
||||
}
|
||||
|
||||
if len(best) < 2 {
|
||||
return analyzeHit{}, false
|
||||
}
|
||||
|
||||
signals := make([]highlightSignal, 0, len(best))
|
||||
groupSeen := map[string]bool{}
|
||||
nonPositionCount := 0
|
||||
hasPosition := false
|
||||
|
||||
var bestSingle highlightSignal
|
||||
var bestSingleQuality float64
|
||||
|
||||
for _, sig := range best {
|
||||
if sig.Label == "" {
|
||||
continue
|
||||
@ -1369,27 +1379,62 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
||||
hasPosition = true
|
||||
} else {
|
||||
nonPositionCount++
|
||||
|
||||
sev := segmentSeverityWeight(sig.Label)
|
||||
quality := sig.Score * sev
|
||||
|
||||
if quality > bestSingleQuality {
|
||||
bestSingle = sig
|
||||
bestSingleQuality = quality
|
||||
}
|
||||
}
|
||||
|
||||
groupSeen[sig.Group] = true
|
||||
signals = append(signals, sig)
|
||||
}
|
||||
|
||||
// Nur echte interessante Kombis:
|
||||
// - Position + mindestens ein weiteres Signal
|
||||
// - oder mindestens zwei Nicht-Positions-Signale
|
||||
// - oder mindestens zwei unterschiedliche Signalgruppen
|
||||
// Fallback: starke Einzel-Treffer sollen auch Highlights werden.
|
||||
// Sonst verschwinden viele explizite Stellen, wenn sie nicht zufällig
|
||||
// im selben Frame mit Position/Object/Clothing kombiniert werden.
|
||||
returnSingleIfGoodEnough := func() (analyzeHit, bool) {
|
||||
if bestSingle.Label == "" {
|
||||
return analyzeHit{}, false
|
||||
}
|
||||
|
||||
sev := segmentSeverityWeight(bestSingle.Label)
|
||||
|
||||
// Nur wirklich relevante Einzel-Signale übernehmen.
|
||||
if sev < 0.65 {
|
||||
return analyzeHit{}, false
|
||||
}
|
||||
if bestSingle.Score < 0.35 {
|
||||
return analyzeHit{}, false
|
||||
}
|
||||
if bestSingleQuality < 0.32 {
|
||||
return analyzeHit{}, false
|
||||
}
|
||||
|
||||
return analyzeHit{
|
||||
Time: t,
|
||||
Label: bestSingle.Label,
|
||||
Score: bestSingle.Score,
|
||||
Start: t,
|
||||
End: t,
|
||||
}, true
|
||||
}
|
||||
|
||||
// Combo nur, wenn wirklich genug Kontext da ist.
|
||||
if len(signals) < 2 {
|
||||
return analyzeHit{}, false
|
||||
return returnSingleIfGoodEnough()
|
||||
}
|
||||
if hasPosition && nonPositionCount < 1 {
|
||||
return analyzeHit{}, false
|
||||
return returnSingleIfGoodEnough()
|
||||
}
|
||||
if !hasPosition && nonPositionCount < 2 {
|
||||
return analyzeHit{}, false
|
||||
return returnSingleIfGoodEnough()
|
||||
}
|
||||
if len(groupSeen) < 2 && len(signals) < 3 {
|
||||
return analyzeHit{}, false
|
||||
return returnSingleIfGoodEnough()
|
||||
}
|
||||
|
||||
sort.SliceStable(signals, func(i, j int) bool {
|
||||
@ -1464,17 +1509,84 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
||||
}, true
|
||||
}
|
||||
|
||||
func buildHighlightHitsFromPrediction(pred TrainingPrediction, t float64) []analyzeHit {
|
||||
if !pred.ModelAvailable {
|
||||
return nil
|
||||
}
|
||||
|
||||
best := map[string]highlightSignal{}
|
||||
|
||||
sexPosition := strings.ToLower(strings.TrimSpace(pred.SexPosition))
|
||||
if sexPosition != "" && sexPosition != "unknown" && isKnownPositionLabel(sexPosition) {
|
||||
addHighlightSignal(best, "position:"+sexPosition, pred.SexPositionScore)
|
||||
}
|
||||
|
||||
addHighlightSignalsFromScoredLabels(best, "body", pred.BodyPartsPresent)
|
||||
addHighlightSignalsFromScoredLabels(best, "object", pred.ObjectsPresent)
|
||||
addHighlightSignalsFromScoredLabels(best, "clothing", pred.ClothingPresent)
|
||||
|
||||
for _, box := range pred.Boxes {
|
||||
label := strings.ToLower(strings.TrimSpace(box.Label))
|
||||
if label == "" || label == "unknown" {
|
||||
continue
|
||||
}
|
||||
if isIgnoredNSFWLabel(label) {
|
||||
continue
|
||||
}
|
||||
if !shouldAutoSelectAnalyzeHit(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
addHighlightSignal(best, "detector:"+label, box.Score)
|
||||
}
|
||||
|
||||
out := make([]analyzeHit, 0, len(best))
|
||||
|
||||
for _, sig := range best {
|
||||
label := strings.ToLower(strings.TrimSpace(sig.Label))
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Schwache Positions-Kontexte wie standing/sitting nicht alleine als Segment anzeigen.
|
||||
if sig.Group == "position" && segmentSeverityWeight(label) < 0.70 {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, analyzeHit{
|
||||
Time: t,
|
||||
Label: label,
|
||||
Score: sig.Score,
|
||||
Start: t,
|
||||
End: t,
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
wi := segmentSeverityWeight(out[i].Label) * out[i].Score
|
||||
wj := segmentSeverityWeight(out[j].Label) * out[j].Score
|
||||
|
||||
if wi != wj {
|
||||
return wi > wj
|
||||
}
|
||||
|
||||
return out[i].Label < out[j].Label
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func appendHighlightHitsFromPrediction(
|
||||
hits []analyzeHit,
|
||||
pred TrainingPrediction,
|
||||
t float64,
|
||||
) []analyzeHit {
|
||||
hit, ok := buildCombinedHighlightHitFromPrediction(pred, t)
|
||||
if !ok {
|
||||
next := buildHighlightHitsFromPrediction(pred, t)
|
||||
if len(next) == 0 {
|
||||
return hits
|
||||
}
|
||||
|
||||
return append(hits, hit)
|
||||
return append(hits, next...)
|
||||
}
|
||||
|
||||
func analyzeVideoFromFrames(ctx context.Context, outPath, goal string) ([]analyzeHit, error) {
|
||||
@ -2191,18 +2303,27 @@ func segmentTagsFromAnalyzeLabel(label string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func analyzeHitContinuationGapSeconds() float64 {
|
||||
// Treffer bei 0s und 6s sollen bei 3s Sampling noch zusammengehören:
|
||||
// 0s erkannt, 3s nicht erkannt, 6s wieder erkannt.
|
||||
return float64(analyzeVideoFrameIntervalSeconds) + analyzeLabelInvisibleGraceSeconds + 0.25
|
||||
}
|
||||
|
||||
func mergeAnalyzeHits(in []analyzeHit) []analyzeHit {
|
||||
if len(in) == 0 {
|
||||
return []analyzeHit{}
|
||||
}
|
||||
|
||||
cp := make([]analyzeHit, 0, len(in))
|
||||
maxGap := analyzeHitContinuationGapSeconds()
|
||||
|
||||
byLabel := map[string][]analyzeHit{}
|
||||
|
||||
for _, h := range in {
|
||||
label := strings.ToLower(strings.TrimSpace(h.Label))
|
||||
if label == "" {
|
||||
if label == "" || label == "unknown" {
|
||||
continue
|
||||
}
|
||||
if isIgnoredNSFWLabel(label) {
|
||||
if isIgnoredNSFWLabel(label) || isPersonSegmentLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -2221,62 +2342,84 @@ func mergeAnalyzeHits(in []analyzeHit) []analyzeHit {
|
||||
}
|
||||
}
|
||||
|
||||
if start > end {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
h.Label = label
|
||||
h.Start = start
|
||||
h.End = end
|
||||
cp = append(cp, h)
|
||||
}
|
||||
|
||||
if len(cp) == 0 {
|
||||
return []analyzeHit{}
|
||||
}
|
||||
|
||||
sort.Slice(cp, func(i, j int) bool {
|
||||
if cp[i].Start != cp[j].Start {
|
||||
return cp[i].Start < cp[j].Start
|
||||
}
|
||||
if cp[i].End != cp[j].End {
|
||||
return cp[i].End < cp[j].End
|
||||
}
|
||||
return cp[i].Label < cp[j].Label
|
||||
})
|
||||
|
||||
out := make([]analyzeHit, 0, len(cp))
|
||||
cur := cp[0]
|
||||
|
||||
for i := 1; i < len(cp); i++ {
|
||||
n := cp[i]
|
||||
|
||||
// Direkt aufeinanderfolgende Treffer mit gleichem Label immer zusammenfassen.
|
||||
// Sobald ein anderes Label dazwischen liegt, wird automatisch nicht gemergt.
|
||||
sameLabel := sameAnalyzeSegmentLabel(cur.Label, n.Label)
|
||||
gap := n.Start - cur.End
|
||||
|
||||
if sameLabel && gap >= -0.25 && gap <= analyzeSegmentMergeGapSeconds {
|
||||
cur.Label = preferAnalyzeSegmentLabel(cur.Label, n.Label)
|
||||
if n.Start < cur.Start {
|
||||
cur.Start = n.Start
|
||||
}
|
||||
if n.End > cur.End {
|
||||
cur.End = n.End
|
||||
}
|
||||
if n.Score > cur.Score {
|
||||
cur.Score = n.Score
|
||||
}
|
||||
cur.Time = (cur.Start + cur.End) / 2
|
||||
key := normalizeSegmentLabel(label)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, cur)
|
||||
cur = n
|
||||
byLabel[key] = append(byLabel[key], h)
|
||||
}
|
||||
|
||||
out = append(out, cur)
|
||||
out := make([]analyzeHit, 0, len(in))
|
||||
|
||||
for _, items := range byLabel {
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].Start != items[j].Start {
|
||||
return items[i].Start < items[j].Start
|
||||
}
|
||||
if items[i].End != items[j].End {
|
||||
return items[i].End < items[j].End
|
||||
}
|
||||
return items[i].Label < items[j].Label
|
||||
})
|
||||
|
||||
cur := items[0]
|
||||
|
||||
for i := 1; i < len(items); i++ {
|
||||
n := items[i]
|
||||
gap := n.Start - cur.End
|
||||
|
||||
if gap >= -0.25 && gap <= maxGap {
|
||||
cur.Label = preferAnalyzeSegmentLabel(cur.Label, n.Label)
|
||||
|
||||
if n.Start < cur.Start {
|
||||
cur.Start = n.Start
|
||||
}
|
||||
if n.End > cur.End {
|
||||
cur.End = n.End
|
||||
}
|
||||
if n.Score > cur.Score {
|
||||
cur.Score = n.Score
|
||||
}
|
||||
|
||||
cur.Time = (cur.Start + cur.End) / 2
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, cur)
|
||||
cur = n
|
||||
}
|
||||
|
||||
out = append(out, cur)
|
||||
}
|
||||
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Start != out[j].Start {
|
||||
return out[i].Start < out[j].Start
|
||||
}
|
||||
if out[i].End != out[j].End {
|
||||
return out[i].End < out[j].End
|
||||
}
|
||||
return normalizeSegmentLabel(out[i].Label) < normalizeSegmentLabel(out[j].Label)
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func inferAnalyzePointSpanSeconds(hits []analyzeHit, duration float64) float64 {
|
||||
const fallback = 3.0
|
||||
const fallback = 10.0
|
||||
|
||||
if len(hits) < 2 {
|
||||
return fallback
|
||||
@ -2338,8 +2481,8 @@ func inferAnalyzePointSpanSeconds(hits []analyzeHit, duration float64) float64 {
|
||||
// aber wir deckeln, damit Sparse-Hits nicht riesig werden.
|
||||
span := median * 0.90
|
||||
|
||||
if span < 2 {
|
||||
span = 2
|
||||
if span < 6 {
|
||||
span = 6
|
||||
}
|
||||
if span > 12 {
|
||||
span = 12
|
||||
@ -2387,17 +2530,56 @@ func expandAnalyzePointToSpan(t, span, duration float64) (float64, float64) {
|
||||
return start, end
|
||||
}
|
||||
|
||||
func buildSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegmentMeta {
|
||||
type analyzeLabelSegmentPoint struct {
|
||||
Label string
|
||||
Start float64
|
||||
End float64
|
||||
Score float64
|
||||
}
|
||||
|
||||
func isAllowedAnalyzeSegmentLabel(label string) bool {
|
||||
label = strings.ToLower(strings.TrimSpace(label))
|
||||
if label == "" || label == "unknown" {
|
||||
return false
|
||||
}
|
||||
if isIgnoredNSFWLabel(label) || isPersonSegmentLabel(label) {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(label, "combo:") {
|
||||
for _, part := range segmentLabelParts(label) {
|
||||
if isAllowedAnalyzeSegmentLabel(part) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
raw := normalizeSegmentLabel(label)
|
||||
if raw == "" || raw == "unknown" {
|
||||
return false
|
||||
}
|
||||
|
||||
return shouldAutoSelectAnalyzeHit(raw) || isKnownPositionLabel(raw)
|
||||
}
|
||||
|
||||
func buildLabelContinuitySegmentsFromAnalyzeHits(
|
||||
hits []analyzeHit,
|
||||
duration float64,
|
||||
) []aiSegmentMeta {
|
||||
if len(hits) == 0 || duration <= 0 {
|
||||
return []aiSegmentMeta{}
|
||||
}
|
||||
|
||||
pointSpan := inferAnalyzePointSpanSeconds(hits, duration)
|
||||
sampleSpan := math.Max(1.0, float64(analyzeVideoFrameIntervalSeconds))
|
||||
halfSample := sampleSpan / 2.0
|
||||
maxGap := analyzeHitContinuationGapSeconds()
|
||||
|
||||
out := make([]aiSegmentMeta, 0, len(hits))
|
||||
byLabel := map[string][]analyzeLabelSegmentPoint{}
|
||||
|
||||
for _, hit := range hits {
|
||||
if !shouldAutoSelectAnalyzeHit(hit.Label) {
|
||||
label := strings.ToLower(strings.TrimSpace(hit.Label))
|
||||
if !isAllowedAnalyzeSegmentLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -2420,201 +2602,174 @@ func buildSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegme
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
if start < 0 {
|
||||
start = hit.Time
|
||||
}
|
||||
if end < 0 {
|
||||
end = start
|
||||
}
|
||||
|
||||
start = math.Max(0, math.Min(start, duration))
|
||||
end = math.Max(0, math.Min(end, duration))
|
||||
|
||||
// Wichtig:
|
||||
// Einzelne AI-Treffer sind oft Punkt-Treffer: Start == End.
|
||||
// Für Segmente und Rating brauchen sie aber eine kleine Dauer.
|
||||
if end <= start {
|
||||
marker := hit.Time
|
||||
if marker < 0 {
|
||||
marker = start
|
||||
}
|
||||
|
||||
start, end = expandAnalyzePointToSpan(marker, pointSpan, duration)
|
||||
}
|
||||
|
||||
if end <= start {
|
||||
key := normalizeSegmentLabel(label)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
label := strings.ToLower(strings.TrimSpace(hit.Label))
|
||||
score := hit.Score
|
||||
if score <= 0 {
|
||||
score = 1
|
||||
}
|
||||
|
||||
out = append(out, aiSegmentMeta{
|
||||
Label: label,
|
||||
StartSeconds: start,
|
||||
EndSeconds: end,
|
||||
DurationSeconds: end - start,
|
||||
Score: hit.Score,
|
||||
AutoSelected: true,
|
||||
Position: segmentPositionFromAnalyzeLabel(label),
|
||||
Tags: segmentTagsFromAnalyzeLabel(label),
|
||||
byLabel[key] = append(byLabel[key], analyzeLabelSegmentPoint{
|
||||
Label: label,
|
||||
Start: start,
|
||||
End: end,
|
||||
Score: score,
|
||||
})
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return []aiSegmentMeta{}
|
||||
out := make([]aiSegmentMeta, 0)
|
||||
|
||||
for _, points := range byLabel {
|
||||
if len(points) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sort.SliceStable(points, func(i, j int) bool {
|
||||
if points[i].Start != points[j].Start {
|
||||
return points[i].Start < points[j].Start
|
||||
}
|
||||
if points[i].End != points[j].End {
|
||||
return points[i].End < points[j].End
|
||||
}
|
||||
return points[i].Label < points[j].Label
|
||||
})
|
||||
|
||||
curLabel := points[0].Label
|
||||
curStartMarker := points[0].Start
|
||||
curEndMarker := points[0].End
|
||||
curScoreSum := points[0].Score
|
||||
curScoreCount := 1
|
||||
|
||||
for i := 1; i < len(points); i++ {
|
||||
n := points[i]
|
||||
|
||||
gap := n.Start - curEndMarker
|
||||
|
||||
if gap >= -0.25 && gap <= maxGap {
|
||||
curLabel = preferAnalyzeSegmentLabel(curLabel, n.Label)
|
||||
|
||||
if n.Start < curStartMarker {
|
||||
curStartMarker = n.Start
|
||||
}
|
||||
if n.End > curEndMarker {
|
||||
curEndMarker = n.End
|
||||
}
|
||||
|
||||
curScoreSum += n.Score
|
||||
curScoreCount++
|
||||
continue
|
||||
}
|
||||
|
||||
segment := makeLabelContinuitySegment(
|
||||
curLabel,
|
||||
curStartMarker,
|
||||
curEndMarker,
|
||||
curScoreSum,
|
||||
curScoreCount,
|
||||
halfSample,
|
||||
duration,
|
||||
)
|
||||
|
||||
if segment.DurationSeconds > 0 {
|
||||
out = append(out, segment)
|
||||
}
|
||||
|
||||
curLabel = n.Label
|
||||
curStartMarker = n.Start
|
||||
curEndMarker = n.End
|
||||
curScoreSum = n.Score
|
||||
curScoreCount = 1
|
||||
}
|
||||
|
||||
segment := makeLabelContinuitySegment(
|
||||
curLabel,
|
||||
curStartMarker,
|
||||
curEndMarker,
|
||||
curScoreSum,
|
||||
curScoreCount,
|
||||
halfSample,
|
||||
duration,
|
||||
)
|
||||
|
||||
if segment.DurationSeconds > 0 {
|
||||
out = append(out, segment)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].StartSeconds != out[j].StartSeconds {
|
||||
return out[i].StartSeconds < out[j].StartSeconds
|
||||
}
|
||||
if out[i].EndSeconds != out[j].EndSeconds {
|
||||
return out[i].EndSeconds < out[j].EndSeconds
|
||||
}
|
||||
return out[i].Label < out[j].Label
|
||||
return normalizeSegmentLabel(out[i].Label) < normalizeSegmentLabel(out[j].Label)
|
||||
})
|
||||
|
||||
merged := make([]aiSegmentMeta, 0, len(out))
|
||||
cur := out[0]
|
||||
return out
|
||||
}
|
||||
|
||||
for i := 1; i < len(out); i++ {
|
||||
n := out[i]
|
||||
func makeLabelContinuitySegment(
|
||||
label string,
|
||||
startMarker float64,
|
||||
endMarker float64,
|
||||
scoreSum float64,
|
||||
scoreCount int,
|
||||
halfSample float64,
|
||||
duration float64,
|
||||
) aiSegmentMeta {
|
||||
label = strings.ToLower(strings.TrimSpace(label))
|
||||
|
||||
gap := n.StartSeconds - cur.EndSeconds
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
start := startMarker - halfSample
|
||||
end := endMarker + halfSample
|
||||
|
||||
if sameAnalyzeSegmentLabel(cur.Label, n.Label) && gap <= analyzeSegmentMergeGapSeconds {
|
||||
cur.Label = preferAnalyzeSegmentLabel(cur.Label, n.Label)
|
||||
if n.StartSeconds < cur.StartSeconds {
|
||||
cur.StartSeconds = n.StartSeconds
|
||||
}
|
||||
if n.EndSeconds > cur.EndSeconds {
|
||||
cur.EndSeconds = n.EndSeconds
|
||||
}
|
||||
cur.DurationSeconds = cur.EndSeconds - cur.StartSeconds
|
||||
if n.Score > cur.Score {
|
||||
cur.Score = n.Score
|
||||
}
|
||||
cur.AutoSelected = cur.AutoSelected || n.AutoSelected
|
||||
continue
|
||||
}
|
||||
|
||||
merged = append(merged, cur)
|
||||
cur = n
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if duration > 0 && end > duration {
|
||||
end = duration
|
||||
}
|
||||
|
||||
merged = append(merged, cur)
|
||||
return mergeAdjacentAISegments(merged, analyzeSegmentMergeGapSeconds)
|
||||
if end <= start {
|
||||
end = math.Min(duration, start+math.Max(1, halfSample*2))
|
||||
}
|
||||
|
||||
score := 0.0
|
||||
if scoreCount > 0 {
|
||||
score = scoreSum / float64(scoreCount)
|
||||
}
|
||||
|
||||
return aiSegmentMeta{
|
||||
Label: label,
|
||||
StartSeconds: start,
|
||||
EndSeconds: end,
|
||||
DurationSeconds: math.Max(0, end-start),
|
||||
Score: score,
|
||||
AutoSelected: true,
|
||||
Position: segmentPositionFromAnalyzeLabel(label),
|
||||
Tags: segmentTagsFromAnalyzeLabel(label),
|
||||
}
|
||||
}
|
||||
|
||||
func buildSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegmentMeta {
|
||||
return buildLabelContinuitySegmentsFromAnalyzeHits(hits, duration)
|
||||
}
|
||||
|
||||
func buildHighlightSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegmentMeta {
|
||||
if len(hits) == 0 || duration <= 0 {
|
||||
return []aiSegmentMeta{}
|
||||
}
|
||||
|
||||
pointSpan := inferAnalyzePointSpanSeconds(hits, duration)
|
||||
|
||||
out := make([]aiSegmentMeta, 0, len(hits))
|
||||
|
||||
for _, hit := range hits {
|
||||
label := strings.ToLower(strings.TrimSpace(hit.Label))
|
||||
if label == "" || label == "unknown" {
|
||||
continue
|
||||
}
|
||||
|
||||
if isIgnoredNSFWLabel(label) {
|
||||
continue
|
||||
}
|
||||
|
||||
start := hit.Start
|
||||
end := hit.End
|
||||
|
||||
if start < 0 && end < 0 {
|
||||
start = hit.Time
|
||||
end = hit.Time
|
||||
} else {
|
||||
if start < 0 {
|
||||
start = hit.Time
|
||||
}
|
||||
if end < 0 {
|
||||
end = hit.Time
|
||||
}
|
||||
}
|
||||
|
||||
if start > end {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
start = math.Max(0, math.Min(start, duration))
|
||||
end = math.Max(0, math.Min(end, duration))
|
||||
|
||||
if end <= start {
|
||||
marker := hit.Time
|
||||
if marker < 0 {
|
||||
marker = start
|
||||
}
|
||||
|
||||
start, end = expandAnalyzePointToSpan(marker, pointSpan, duration)
|
||||
}
|
||||
|
||||
if end <= start {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, aiSegmentMeta{
|
||||
Label: label,
|
||||
StartSeconds: start,
|
||||
EndSeconds: end,
|
||||
DurationSeconds: end - start,
|
||||
Score: hit.Score,
|
||||
AutoSelected: true,
|
||||
Position: segmentPositionFromAnalyzeLabel(label),
|
||||
Tags: segmentTagsFromAnalyzeLabel(label),
|
||||
})
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return []aiSegmentMeta{}
|
||||
}
|
||||
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].StartSeconds != out[j].StartSeconds {
|
||||
return out[i].StartSeconds < out[j].StartSeconds
|
||||
}
|
||||
if out[i].EndSeconds != out[j].EndSeconds {
|
||||
return out[i].EndSeconds < out[j].EndSeconds
|
||||
}
|
||||
return out[i].Label < out[j].Label
|
||||
})
|
||||
|
||||
merged := make([]aiSegmentMeta, 0, len(out))
|
||||
cur := out[0]
|
||||
|
||||
for i := 1; i < len(out); i++ {
|
||||
n := out[i]
|
||||
|
||||
gap := n.StartSeconds - cur.EndSeconds
|
||||
if gap < 0 {
|
||||
gap = 0
|
||||
}
|
||||
|
||||
if sameAnalyzeSegmentLabel(cur.Label, n.Label) && gap <= analyzeSegmentMergeGapSeconds {
|
||||
cur.Label = preferAnalyzeSegmentLabel(cur.Label, n.Label)
|
||||
if n.StartSeconds < cur.StartSeconds {
|
||||
cur.StartSeconds = n.StartSeconds
|
||||
}
|
||||
if n.EndSeconds > cur.EndSeconds {
|
||||
cur.EndSeconds = n.EndSeconds
|
||||
}
|
||||
cur.DurationSeconds = cur.EndSeconds - cur.StartSeconds
|
||||
if n.Score > cur.Score {
|
||||
cur.Score = n.Score
|
||||
}
|
||||
cur.AutoSelected = cur.AutoSelected || n.AutoSelected
|
||||
continue
|
||||
}
|
||||
|
||||
merged = append(merged, cur)
|
||||
cur = n
|
||||
}
|
||||
|
||||
merged = append(merged, cur)
|
||||
return mergeAdjacentAISegments(merged, analyzeSegmentMergeGapSeconds)
|
||||
return buildLabelContinuitySegmentsFromAnalyzeHits(hits, duration)
|
||||
}
|
||||
|
||||
func buildAnalyzeSegmentsForGoal(
|
||||
|
||||
@ -1433,29 +1433,13 @@ func hasAIAnalysisForOutputGoal(outPath string, goal string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
rawHits, hasHits := aiMap["hits"].([]any)
|
||||
rawSegs, hasSegs := aiMap["segments"].([]any)
|
||||
|
||||
// Wichtig:
|
||||
// Auch eine Analyse ohne Treffer ist fertig, solange sie gespeichert wurde.
|
||||
if rawGoal, ok := aiMap["goal"].(string); ok && normalizeAIGoal(rawGoal) == goal {
|
||||
return true
|
||||
}
|
||||
|
||||
if rawMode, ok := aiMap["mode"].(string); ok && strings.TrimSpace(rawMode) != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := aiMap["hits"]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := aiMap["segments"]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := aiMap["analyzedAtUnix"]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
// Eine leere Analyse zählt NICHT mehr als fertig.
|
||||
// Sonst bleibt ein kaputter 0-Treffer-Run für immer gecached.
|
||||
return (hasHits && len(rawHits) > 0) || (hasSegs && len(rawSegs) > 0)
|
||||
}
|
||||
|
||||
func hasAIAnalysisForAllOutputGoals(outPath string, goals []string) bool {
|
||||
|
||||
@ -847,9 +847,9 @@ func computeNSFWRating(segments []aiSegmentMeta, durationSec float64) *aiRatingM
|
||||
0.04*confNorm
|
||||
|
||||
// Sicherheits-Caps:
|
||||
// Ohne Positionssignal soll Kontext alleine nicht auf 4–5 Sterne kippen.
|
||||
// Ohne Positionssignal soll Kontext alleine nicht auf 5 Sterne kippen.
|
||||
if positionEffectiveWeighted <= 0 {
|
||||
raw = math.Min(raw, 0.58)
|
||||
raw = math.Min(raw, 0.72)
|
||||
}
|
||||
|
||||
// Sehr kurze/spärliche Treffer nicht überbewerten.
|
||||
|
||||
@ -50,12 +50,9 @@ type TrainingPrediction struct {
|
||||
ModelAvailable bool `json:"modelAvailable"`
|
||||
Source string `json:"source,omitempty"`
|
||||
|
||||
PeopleCount int `json:"peopleCount"`
|
||||
MaleCount int `json:"maleCount"`
|
||||
FemaleCount int `json:"femaleCount"`
|
||||
UnknownCount int `json:"unknownCount"`
|
||||
SexPosition string `json:"sexPosition"`
|
||||
SexPositionScore float64 `json:"sexPositionScore"`
|
||||
PeoplePresent []TrainingScoredLabel `json:"peoplePresent"`
|
||||
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
|
||||
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
|
||||
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
|
||||
@ -63,11 +60,8 @@ type TrainingPrediction struct {
|
||||
}
|
||||
|
||||
type TrainingCorrection struct {
|
||||
PeopleCount int `json:"peopleCount"`
|
||||
MaleCount int `json:"maleCount"`
|
||||
FemaleCount int `json:"femaleCount"`
|
||||
UnknownCount int `json:"unknownCount"`
|
||||
SexPosition string `json:"sexPosition"`
|
||||
PeoplePresent []string `json:"peoplePresent"`
|
||||
BodyPartsPresent []string `json:"bodyPartsPresent"`
|
||||
ObjectsPresent []string `json:"objectsPresent"`
|
||||
ClothingPresent []string `json:"clothingPresent"`
|
||||
@ -1271,11 +1265,8 @@ func trainingEffectiveCorrection(annotation TrainingAnnotation) TrainingCorrecti
|
||||
p := annotation.Prediction
|
||||
|
||||
return TrainingCorrection{
|
||||
PeopleCount: p.PeopleCount,
|
||||
MaleCount: p.MaleCount,
|
||||
FemaleCount: p.FemaleCount,
|
||||
UnknownCount: p.UnknownCount,
|
||||
SexPosition: p.SexPosition,
|
||||
PeoplePresent: trainingScoredLabelsToStrings(p.PeoplePresent),
|
||||
BodyPartsPresent: trainingScoredLabelsToStrings(p.BodyPartsPresent),
|
||||
ObjectsPresent: trainingScoredLabelsToStrings(p.ObjectsPresent),
|
||||
ClothingPresent: trainingScoredLabelsToStrings(p.ClothingPresent),
|
||||
@ -2034,12 +2025,9 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
||||
pred := TrainingPrediction{
|
||||
ModelAvailable: det.Available,
|
||||
Source: det.Source,
|
||||
PeopleCount: 0,
|
||||
MaleCount: 0,
|
||||
FemaleCount: 0,
|
||||
UnknownCount: 0,
|
||||
SexPosition: "unknown",
|
||||
SexPositionScore: 0,
|
||||
PeoplePresent: []TrainingScoredLabel{},
|
||||
BodyPartsPresent: []TrainingScoredLabel{},
|
||||
ObjectsPresent: []TrainingScoredLabel{},
|
||||
ClothingPresent: []TrainingScoredLabel{},
|
||||
@ -2133,17 +2121,6 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
||||
}
|
||||
|
||||
if peopleSet[label] {
|
||||
switch label {
|
||||
case "person_male", "male_person":
|
||||
pred.MaleCount++
|
||||
|
||||
case "person_female", "female_person":
|
||||
pred.FemaleCount++
|
||||
|
||||
case "person", "person_unknown":
|
||||
pred.UnknownCount++
|
||||
}
|
||||
|
||||
visibleBoxes = append(visibleBoxes, box)
|
||||
continue
|
||||
}
|
||||
@ -2158,9 +2135,9 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
||||
pred.SexPositionScore = bestSexPositionScore
|
||||
}
|
||||
|
||||
pred.PeopleCount = pred.MaleCount + pred.FemaleCount + pred.UnknownCount
|
||||
pred.Boxes = visibleBoxes
|
||||
|
||||
pred.PeoplePresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.People)
|
||||
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.BodyParts)
|
||||
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Objects)
|
||||
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Clothing)
|
||||
@ -2526,12 +2503,9 @@ func trainingEmptyPrediction(source string) TrainingPrediction {
|
||||
return TrainingPrediction{
|
||||
ModelAvailable: false,
|
||||
Source: source,
|
||||
PeopleCount: 0,
|
||||
MaleCount: 0,
|
||||
FemaleCount: 0,
|
||||
UnknownCount: 0,
|
||||
SexPosition: "unknown",
|
||||
SexPositionScore: 0,
|
||||
PeoplePresent: []TrainingScoredLabel{},
|
||||
BodyPartsPresent: []TrainingScoredLabel{},
|
||||
ObjectsPresent: []TrainingScoredLabel{},
|
||||
ClothingPresent: []TrainingScoredLabel{},
|
||||
|
||||
@ -2627,23 +2627,23 @@ export default function App() {
|
||||
const onlineLikedCount = headerStats.onlineLikedCount
|
||||
|
||||
const runningTabCount = useMemo(() => {
|
||||
const activeDownloads = jobs.filter((j) => {
|
||||
return jobs.filter((j) => {
|
||||
const status = String((j as any)?.status ?? '').trim().toLowerCase()
|
||||
const phase = String((j as any)?.phase ?? '').trim().toLowerCase()
|
||||
|
||||
// Nacharbeit / Postwork nicht mitzählen
|
||||
if (isPostworkJobForCount(j)) return false
|
||||
if (isTerminalJobStatusForCount((j as any)?.status)) return false
|
||||
|
||||
// Fertige/abgebrochene/fehlgeschlagene Jobs nicht mitzählen
|
||||
if (isTerminalJobStatusForCount(status)) return false
|
||||
|
||||
// Sobald endedAt gesetzt ist, ist die aktive Aufnahme vorbei
|
||||
if (Boolean((j as any)?.endedAt)) return false
|
||||
return true
|
||||
|
||||
// Nur wirklich laufende Downloads zählen
|
||||
return status === 'running' && (phase === '' || phase === 'recording')
|
||||
}).length
|
||||
|
||||
const activePostwork = jobs.filter((j) => {
|
||||
if (!isPostworkJobForCount(j)) return false
|
||||
if (isTerminalJobStatusForCount((j as any)?.status)) return false
|
||||
return true
|
||||
}).length
|
||||
|
||||
const pendingCount = pendingWatchedRooms.length
|
||||
|
||||
return activeDownloads + activePostwork + pendingCount
|
||||
}, [jobs, pendingWatchedRooms])
|
||||
}, [jobs])
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{
|
||||
|
||||
@ -299,6 +299,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saveSuccessUntilMs, setSaveSuccessUntilMs] = useState<number>(0)
|
||||
const saveSuccessTimerRef = useRef<number | null>(null)
|
||||
const appLogPreRef = useRef<HTMLPreElement | null>(null)
|
||||
const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
|
||||
const [msg, setMsg] = useState<string | null>(null)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
@ -317,6 +318,15 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
type SaveUiState = 'idle' | 'saving' | 'success' | 'error'
|
||||
const saveUiState: SaveUiState = saving ? 'saving' : saveSucceeded ? 'success' : saveFailed ? 'error' : 'idle'
|
||||
|
||||
function scrollAppLogToBottom() {
|
||||
window.requestAnimationFrame(() => {
|
||||
const el = appLogPreRef.current
|
||||
if (!el) return
|
||||
|
||||
el.scrollTop = el.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
const saveButton = (() => {
|
||||
if (saveUiState === 'saving') {
|
||||
return {
|
||||
@ -749,6 +759,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
})
|
||||
} finally {
|
||||
setAppLogLoading(false)
|
||||
scrollAppLogToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
@ -757,6 +768,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
void loadAppLog()
|
||||
}, [appLogOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!appLogOpen) return
|
||||
|
||||
scrollAppLogToBottom()
|
||||
}, [appLogOpen, appLog?.log, appLogLoading])
|
||||
|
||||
async function browse(target: 'record' | 'done' | 'ffmpeg') {
|
||||
setErr(null)
|
||||
setMsg(null)
|
||||
@ -1743,7 +1760,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-xl bg-gray-950 p-3 font-mono text-[11px] leading-relaxed text-gray-100 ring-1 ring-black/10 dark:ring-white/10">
|
||||
<pre
|
||||
ref={appLogPreRef}
|
||||
className="max-h-96 overflow-auto whitespace-pre-wrap rounded-xl bg-gray-950 p-3 font-mono text-[11px] leading-relaxed text-gray-100 ring-1 ring-black/10 dark:ring-white/10"
|
||||
>
|
||||
{appLogLoading
|
||||
? 'Log wird geladen…'
|
||||
: appLog?.log?.trim()
|
||||
|
||||
@ -65,12 +65,9 @@ type TrainingPrediction = {
|
||||
modelAvailable: boolean
|
||||
source?: string
|
||||
|
||||
peopleCount: number
|
||||
maleCount: number
|
||||
femaleCount: number
|
||||
unknownCount: number
|
||||
sexPosition: string
|
||||
sexPositionScore: number
|
||||
peoplePresent: ScoredLabel[]
|
||||
bodyPartsPresent: ScoredLabel[]
|
||||
objectsPresent: ScoredLabel[]
|
||||
clothingPresent: ScoredLabel[]
|
||||
@ -97,11 +94,8 @@ type TrainingLabels = {
|
||||
}
|
||||
|
||||
type CorrectionState = {
|
||||
peopleCount: number
|
||||
maleCount: number
|
||||
femaleCount: number
|
||||
unknownCount: number
|
||||
sexPosition: string
|
||||
peoplePresent: string[]
|
||||
bodyPartsPresent: string[]
|
||||
objectsPresent: string[]
|
||||
clothingPresent: string[]
|
||||
@ -604,11 +598,6 @@ function toggleArrayValue(arr: string[], value: string) {
|
||||
: [...arr, value]
|
||||
}
|
||||
|
||||
function safeCount(value: unknown) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
|
||||
}
|
||||
|
||||
function clamp01(v: number) {
|
||||
if (!Number.isFinite(v)) return 0
|
||||
return Math.max(0, Math.min(1, v))
|
||||
@ -668,32 +657,35 @@ function uniqStrings(values: string[]) {
|
||||
return out
|
||||
}
|
||||
|
||||
function peopleLabelsFromBoxes(boxes: TrainingBox[], labels: TrainingLabels) {
|
||||
return uniqStrings(
|
||||
boxes
|
||||
.map((box) => String(box.label || '').trim())
|
||||
.filter((label) => labels.people.includes(label))
|
||||
)
|
||||
}
|
||||
|
||||
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
||||
const p = sample?.prediction
|
||||
|
||||
const maleCount = safeCount(p?.maleCount)
|
||||
const femaleCount = safeCount(p?.femaleCount)
|
||||
const unknownCount = safeCount(p?.unknownCount)
|
||||
const boxes = (p?.boxes ?? [])
|
||||
.map((box) => ({
|
||||
label: String(box.label || '').trim(),
|
||||
score: box.score,
|
||||
x: clamp01(Number(box.x)),
|
||||
y: clamp01(Number(box.y)),
|
||||
w: clamp01(Number(box.w)),
|
||||
h: clamp01(Number(box.h)),
|
||||
}))
|
||||
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
||||
|
||||
return {
|
||||
peopleCount: maleCount + femaleCount + unknownCount,
|
||||
maleCount,
|
||||
femaleCount,
|
||||
unknownCount,
|
||||
sexPosition: p?.sexPosition || 'unknown',
|
||||
peoplePresent: (p?.peoplePresent ?? []).map((x) => x.label),
|
||||
bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
|
||||
objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
|
||||
clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
|
||||
boxes: (p?.boxes ?? [])
|
||||
.map((box) => ({
|
||||
label: String(box.label || '').trim(),
|
||||
score: box.score,
|
||||
x: clamp01(Number(box.x)),
|
||||
y: clamp01(Number(box.y)),
|
||||
w: clamp01(Number(box.w)),
|
||||
h: clamp01(Number(box.h)),
|
||||
}))
|
||||
.filter((box) => box.label && box.w > 0 && box.h > 0),
|
||||
boxes,
|
||||
}
|
||||
}
|
||||
|
||||
@ -706,39 +698,12 @@ function applyBoxLabelToCorrection(
|
||||
if (!clean) return state
|
||||
|
||||
if (labels.people.includes(clean)) {
|
||||
if (clean === 'person_male' || clean === 'male_person') {
|
||||
return {
|
||||
...state,
|
||||
maleCount: safeCount(state.maleCount) + 1,
|
||||
unknownCount: 0,
|
||||
peopleCount:
|
||||
safeCount(state.maleCount) + 1 + safeCount(state.femaleCount),
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
peoplePresent: state.peoplePresent.includes(clean)
|
||||
? state.peoplePresent
|
||||
: [...state.peoplePresent, clean],
|
||||
}
|
||||
|
||||
if (clean === 'person_female' || clean === 'female_person') {
|
||||
return {
|
||||
...state,
|
||||
femaleCount: safeCount(state.femaleCount) + 1,
|
||||
unknownCount: 0,
|
||||
peopleCount:
|
||||
safeCount(state.maleCount) + safeCount(state.femaleCount) + 1,
|
||||
}
|
||||
}
|
||||
|
||||
if (clean === 'person' || clean === 'person_unknown') {
|
||||
return {
|
||||
...state,
|
||||
unknownCount: safeCount(state.unknownCount) + 1,
|
||||
peopleCount:
|
||||
safeCount(state.maleCount) +
|
||||
safeCount(state.femaleCount) +
|
||||
safeCount(state.unknownCount) +
|
||||
1,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
if (labels.bodyParts.includes(clean)) {
|
||||
@ -786,6 +751,13 @@ function removeBoxLabelFromCorrection(
|
||||
// Badge nur abwählen, wenn keine weitere Box mit diesem Label existiert.
|
||||
if (remainingBoxesWithSameLabel) return state
|
||||
|
||||
if (labels.people.includes(clean)) {
|
||||
return {
|
||||
...state,
|
||||
peoplePresent: state.peoplePresent.filter((x) => x !== clean),
|
||||
}
|
||||
}
|
||||
|
||||
if (labels.bodyParts.includes(clean)) {
|
||||
return {
|
||||
...state,
|
||||
@ -822,42 +794,11 @@ function removeBoxFromCorrection(
|
||||
|
||||
const removedLabel = String(removed.label || '').trim()
|
||||
|
||||
let next: CorrectionState = {
|
||||
const next: CorrectionState = {
|
||||
...state,
|
||||
boxes: boxes.filter((_, i) => i !== index),
|
||||
}
|
||||
|
||||
if (removedLabel === 'person_male' || removedLabel === 'male_person') {
|
||||
next = {
|
||||
...next,
|
||||
maleCount: Math.max(0, safeCount(next.maleCount) - 1),
|
||||
unknownCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if (removedLabel === 'person_female' || removedLabel === 'female_person') {
|
||||
next = {
|
||||
...next,
|
||||
femaleCount: Math.max(0, safeCount(next.femaleCount) - 1),
|
||||
unknownCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if (removedLabel === 'person' || removedLabel === 'person_unknown') {
|
||||
next = {
|
||||
...next,
|
||||
unknownCount: Math.max(0, safeCount(next.unknownCount) - 1),
|
||||
}
|
||||
}
|
||||
|
||||
next = {
|
||||
...next,
|
||||
peopleCount:
|
||||
safeCount(next.maleCount) +
|
||||
safeCount(next.femaleCount) +
|
||||
safeCount(next.unknownCount),
|
||||
}
|
||||
|
||||
return removeBoxLabelFromCorrection(next, removedLabel, labels)
|
||||
}
|
||||
|
||||
@ -890,52 +831,9 @@ function changeBoxLabelInCorrection(
|
||||
}),
|
||||
}
|
||||
|
||||
// Alte Personen-Zählung entfernen.
|
||||
if (oldLabel === 'person_male' || oldLabel === 'male_person') {
|
||||
next = {
|
||||
...next,
|
||||
maleCount: Math.max(0, safeCount(next.maleCount) - 1),
|
||||
unknownCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if (oldLabel === 'person_female' || oldLabel === 'female_person') {
|
||||
next = {
|
||||
...next,
|
||||
femaleCount: Math.max(0, safeCount(next.femaleCount) - 1),
|
||||
unknownCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if (oldLabel === 'person' || oldLabel === 'person_unknown') {
|
||||
next = {
|
||||
...next,
|
||||
unknownCount: Math.max(0, safeCount(next.unknownCount) - 1),
|
||||
}
|
||||
}
|
||||
|
||||
next = {
|
||||
...next,
|
||||
peopleCount:
|
||||
safeCount(next.maleCount) +
|
||||
safeCount(next.femaleCount) +
|
||||
safeCount(next.unknownCount),
|
||||
}
|
||||
|
||||
// Alte Bodypart/Object/Clothing-Badges entfernen, falls keine weitere Box damit existiert.
|
||||
next = removeBoxLabelFromCorrection(next, oldLabel, labels)
|
||||
|
||||
// Neues Label anwenden.
|
||||
next = applyBoxLabelToCorrection(next, cleanNextLabel, labels)
|
||||
|
||||
next = {
|
||||
...next,
|
||||
peopleCount:
|
||||
safeCount(next.maleCount) +
|
||||
safeCount(next.femaleCount) +
|
||||
safeCount(next.unknownCount),
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
@ -1503,8 +1401,12 @@ function CollapsibleLabelSection(props: {
|
||||
singleDrawMode?: boolean
|
||||
gridClassName?: string
|
||||
}) {
|
||||
const cleanDrawLabel = String(props.drawLabel || '').trim()
|
||||
const hasDrawLabelInSection =
|
||||
cleanDrawLabel !== '' && props.values.includes(cleanDrawLabel)
|
||||
|
||||
const activeCount = props.singleDrawMode
|
||||
? Math.max(props.selected.length, props.drawLabel ? 1 : 0)
|
||||
? Math.max(props.selected.length, hasDrawLabelInSection ? 1 : 0)
|
||||
: props.selected.length
|
||||
|
||||
const hasActiveItems = activeCount > 0
|
||||
@ -2262,12 +2164,13 @@ export default function TrainingTab(props: {
|
||||
}, [sample?.prediction.boxes, labels.people])
|
||||
|
||||
const selectedPeopleLabels = useMemo(() => {
|
||||
return uniqStrings(
|
||||
correctionBoxes
|
||||
.map((box) => String(box.label || '').trim())
|
||||
.filter((label) => labels.people.includes(label))
|
||||
)
|
||||
}, [correctionBoxes, labels.people])
|
||||
return correction.peoplePresent
|
||||
}, [correction.peoplePresent])
|
||||
|
||||
const drawLabelForSection = useCallback((values: string[]) => {
|
||||
const clean = String(boxLabel || '').trim()
|
||||
return values.includes(clean) ? clean : ''
|
||||
}, [boxLabel])
|
||||
|
||||
const imageSrc = useMemo(() => {
|
||||
if (!sample?.frameUrl) return ''
|
||||
@ -2445,18 +2348,11 @@ export default function TrainingTab(props: {
|
||||
setCorrection(nextCorrection)
|
||||
setHasManualCorrection(false)
|
||||
|
||||
const currentLabels = labelsRef.current
|
||||
const personLabels = new Set(currentLabels.people)
|
||||
|
||||
const hasPersonBoxes = nextCorrection.boxes.some((box) =>
|
||||
personLabels.has(box.label)
|
||||
)
|
||||
|
||||
setExpandedCorrectionSections({
|
||||
sexPosition:
|
||||
Boolean(nextCorrection.sexPosition) &&
|
||||
nextCorrection.sexPosition !== 'unknown',
|
||||
people: hasPersonBoxes || nextCorrection.peopleCount > 0,
|
||||
people: nextCorrection.peoplePresent.length > 0,
|
||||
bodyParts: nextCorrection.bodyPartsPresent.length > 0,
|
||||
objects: nextCorrection.objectsPresent.length > 0,
|
||||
clothing: nextCorrection.clothingPresent.length > 0,
|
||||
@ -2805,19 +2701,14 @@ export default function TrainingTab(props: {
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const maleCount = safeCount(correction.maleCount)
|
||||
const femaleCount = safeCount(correction.femaleCount)
|
||||
const unknownCount = safeCount(correction.unknownCount)
|
||||
const normalizedBoxes = (correction.boxes ?? [])
|
||||
.map(normalizeBox)
|
||||
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
||||
|
||||
const correctionPayload: CorrectionState = {
|
||||
...correction,
|
||||
maleCount,
|
||||
femaleCount,
|
||||
unknownCount,
|
||||
peopleCount: maleCount + femaleCount + unknownCount,
|
||||
boxes: (correction.boxes ?? [])
|
||||
.map(normalizeBox)
|
||||
.filter((box) => box.label && box.w > 0 && box.h > 0),
|
||||
peoplePresent: peopleLabelsFromBoxes(normalizedBoxes, labelsRef.current),
|
||||
boxes: normalizedBoxes,
|
||||
}
|
||||
|
||||
const payload = {
|
||||
@ -3230,10 +3121,7 @@ export default function TrainingTab(props: {
|
||||
|
||||
setCorrection((prev) => ({
|
||||
...prev,
|
||||
maleCount: 0,
|
||||
femaleCount: 0,
|
||||
unknownCount: 0,
|
||||
peopleCount: 0,
|
||||
peoplePresent: [],
|
||||
bodyPartsPresent: [],
|
||||
objectsPresent: [],
|
||||
clothingPresent: [],
|
||||
@ -4647,7 +4535,7 @@ export default function TrainingTab(props: {
|
||||
toggleMobileCorrectionSection('people', expanded)
|
||||
}
|
||||
onToggle={() => {}}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.people)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
singleDrawMode
|
||||
@ -4680,7 +4568,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.bodyParts)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
@ -4711,7 +4599,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.objects)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
@ -4742,7 +4630,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.clothing)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
@ -4838,7 +4726,7 @@ export default function TrainingTab(props: {
|
||||
}))
|
||||
}
|
||||
onToggle={() => {}}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.people)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
singleDrawMode
|
||||
@ -4868,7 +4756,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.bodyParts)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
@ -4896,7 +4784,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.objects)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
@ -4924,7 +4812,7 @@ export default function TrainingTab(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
drawLabel={boxLabel}
|
||||
drawLabel={drawLabelForSection(labels.clothing)}
|
||||
onDrawLabelChange={setBoxLabel}
|
||||
disabled={uiLocked}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user