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()
|
POSITION_LABELS = set()
|
||||||
|
|
||||||
PERSON_LABELS = {
|
PERSON_LABELS = {
|
||||||
"person",
|
|
||||||
"person_male",
|
"person_male",
|
||||||
"person_female",
|
"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_PATH = ""
|
||||||
_MODEL_ERROR = ""
|
_MODEL_ERROR = ""
|
||||||
_LABEL_ERROR = ""
|
_LABEL_ERROR = ""
|
||||||
@ -161,12 +153,9 @@ def empty_prediction(source: str = "model_missing") -> dict:
|
|||||||
return {
|
return {
|
||||||
"modelAvailable": False,
|
"modelAvailable": False,
|
||||||
"source": source,
|
"source": source,
|
||||||
"peopleCount": 0,
|
|
||||||
"maleCount": 0,
|
|
||||||
"femaleCount": 0,
|
|
||||||
"unknownCount": 0,
|
|
||||||
"sexPosition": "unknown",
|
"sexPosition": "unknown",
|
||||||
"sexPositionScore": 0.0,
|
"sexPositionScore": 0.0,
|
||||||
|
"peoplePresent": [],
|
||||||
"bodyPartsPresent": [],
|
"bodyPartsPresent": [],
|
||||||
"objectsPresent": [],
|
"objectsPresent": [],
|
||||||
"clothingPresent": [],
|
"clothingPresent": [],
|
||||||
@ -248,13 +237,6 @@ def load_label_groups_safe() -> None:
|
|||||||
PERSON_LABELS = {
|
PERSON_LABELS = {
|
||||||
label for label in LABEL_GROUPS["people"]
|
label for label in LABEL_GROUPS["people"]
|
||||||
if label
|
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 {}
|
names = result.names or {}
|
||||||
|
|
||||||
boxes_out = []
|
boxes_out = []
|
||||||
|
people_present = []
|
||||||
body_parts = []
|
body_parts = []
|
||||||
objects = []
|
objects = []
|
||||||
clothing = []
|
clothing = []
|
||||||
@ -333,6 +316,21 @@ def prediction_from_result(result) -> dict:
|
|||||||
w = max(0.0, min(1.0 - x, w))
|
w = max(0.0, min(1.0 - x, w))
|
||||||
h = max(0.0, min(1.0 - y, h))
|
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({
|
boxes_out.append({
|
||||||
"label": label,
|
"label": label,
|
||||||
"score": score,
|
"score": score,
|
||||||
@ -342,33 +340,24 @@ def prediction_from_result(result) -> dict:
|
|||||||
"h": h,
|
"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)
|
best_score(body_parts, label, score)
|
||||||
|
|
||||||
if label in OBJECT_LABELS:
|
if is_object:
|
||||||
best_score(objects, label, score)
|
best_score(objects, label, score)
|
||||||
|
|
||||||
if label in CLOTHING_LABELS:
|
if is_clothing:
|
||||||
best_score(clothing, label, score)
|
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 {
|
return {
|
||||||
"modelAvailable": True,
|
"modelAvailable": True,
|
||||||
"source": f"yolo-server:{Path(_MODEL_PATH).name}",
|
"source": f"yolo-server:{Path(_MODEL_PATH).name}",
|
||||||
"peopleCount": people_count,
|
|
||||||
"maleCount": male_count,
|
|
||||||
"femaleCount": female_count,
|
|
||||||
"unknownCount": unknown_count,
|
|
||||||
"sexPosition": sex_position,
|
"sexPosition": sex_position,
|
||||||
"sexPositionScore": sex_position_score,
|
"sexPositionScore": sex_position_score,
|
||||||
|
"peoplePresent": people_present,
|
||||||
"bodyPartsPresent": body_parts,
|
"bodyPartsPresent": body_parts,
|
||||||
"objectsPresent": objects,
|
"objectsPresent": objects,
|
||||||
"clothingPresent": clothing,
|
"clothingPresent": clothing,
|
||||||
|
|||||||
@ -54,8 +54,13 @@ type videoFrameSample struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
analyzeSegmentMergeGapSeconds = 8.0
|
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.
|
// Video-Modus: extrahiert 1 Frame alle N Sekunden.
|
||||||
// 1 = jeder Sekunde, 3 = alle 3 Sekunden, 5 = alle 5 Sekunden.
|
// 1 = jeder Sekunde, 3 = alle 3 Sekunden, 5 = alle 5 Sekunden.
|
||||||
@ -109,26 +114,15 @@ func shouldAutoSelectAnalyzeHit(label string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
autoSelectedAILabelsOnce.Do(func() {
|
labels := autoSelectedAILabelSet()
|
||||||
autoSelectedAILabelsCache = autoSelectedAILabelSet()
|
_, ok := labels[label]
|
||||||
})
|
|
||||||
|
|
||||||
_, ok := autoSelectedAILabelsCache[label]
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
var nsfwIgnoredLabels = map[string]struct{}{
|
var nsfwIgnoredLabels = map[string]struct{}{
|
||||||
// Personen sollen nicht als interessante Segmente auftauchen.
|
// Personen sollen nicht als interessante Segmente auftauchen.
|
||||||
"person": {},
|
"person_male": {},
|
||||||
"person_male": {},
|
"person_female": {},
|
||||||
"person_female": {},
|
|
||||||
"person_unknown": {},
|
|
||||||
"male_person": {},
|
|
||||||
"female_person": {},
|
|
||||||
|
|
||||||
// Falls dein Detector irgendwann diese Varianten liefert:
|
|
||||||
"people_male": {},
|
|
||||||
"people_female": {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIgnoredNSFWLabel(label string) bool {
|
func isIgnoredNSFWLabel(label string) bool {
|
||||||
@ -1084,26 +1078,43 @@ func appendNSFWHitFromPrediction(
|
|||||||
t float64,
|
t float64,
|
||||||
) []analyzeHit {
|
) []analyzeHit {
|
||||||
if !pred.ModelAvailable {
|
if !pred.ModelAvailable {
|
||||||
|
appLogln("⚠️ nsfw: modelAvailable=false bei", t)
|
||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
|
|
||||||
nsfwResults := trainingPredictionToNSFWResults(pred)
|
nsfwResults := trainingPredictionToNSFWResults(pred)
|
||||||
bestLabel, bestScore := pickBestNSFWResult(nsfwResults)
|
if len(nsfwResults) == 0 {
|
||||||
if bestLabel == "" {
|
|
||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
|
|
||||||
if bestScore < nsfwThresholdForLabel(bestLabel) {
|
for _, r := range nsfwResults {
|
||||||
return hits
|
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{
|
return hits
|
||||||
Time: t,
|
|
||||||
Label: bestLabel,
|
|
||||||
Score: bestScore,
|
|
||||||
Start: t,
|
|
||||||
End: t,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type highlightSignal struct {
|
type highlightSignal struct {
|
||||||
@ -1351,15 +1362,14 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
|||||||
addHighlightSignal(best, "detector:"+label, box.Score)
|
addHighlightSignal(best, "detector:"+label, box.Score)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(best) < 2 {
|
|
||||||
return analyzeHit{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
signals := make([]highlightSignal, 0, len(best))
|
signals := make([]highlightSignal, 0, len(best))
|
||||||
groupSeen := map[string]bool{}
|
groupSeen := map[string]bool{}
|
||||||
nonPositionCount := 0
|
nonPositionCount := 0
|
||||||
hasPosition := false
|
hasPosition := false
|
||||||
|
|
||||||
|
var bestSingle highlightSignal
|
||||||
|
var bestSingleQuality float64
|
||||||
|
|
||||||
for _, sig := range best {
|
for _, sig := range best {
|
||||||
if sig.Label == "" {
|
if sig.Label == "" {
|
||||||
continue
|
continue
|
||||||
@ -1369,27 +1379,62 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
|||||||
hasPosition = true
|
hasPosition = true
|
||||||
} else {
|
} else {
|
||||||
nonPositionCount++
|
nonPositionCount++
|
||||||
|
|
||||||
|
sev := segmentSeverityWeight(sig.Label)
|
||||||
|
quality := sig.Score * sev
|
||||||
|
|
||||||
|
if quality > bestSingleQuality {
|
||||||
|
bestSingle = sig
|
||||||
|
bestSingleQuality = quality
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupSeen[sig.Group] = true
|
groupSeen[sig.Group] = true
|
||||||
signals = append(signals, sig)
|
signals = append(signals, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nur echte interessante Kombis:
|
// Fallback: starke Einzel-Treffer sollen auch Highlights werden.
|
||||||
// - Position + mindestens ein weiteres Signal
|
// Sonst verschwinden viele explizite Stellen, wenn sie nicht zufällig
|
||||||
// - oder mindestens zwei Nicht-Positions-Signale
|
// im selben Frame mit Position/Object/Clothing kombiniert werden.
|
||||||
// - oder mindestens zwei unterschiedliche Signalgruppen
|
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 {
|
if len(signals) < 2 {
|
||||||
return analyzeHit{}, false
|
return returnSingleIfGoodEnough()
|
||||||
}
|
}
|
||||||
if hasPosition && nonPositionCount < 1 {
|
if hasPosition && nonPositionCount < 1 {
|
||||||
return analyzeHit{}, false
|
return returnSingleIfGoodEnough()
|
||||||
}
|
}
|
||||||
if !hasPosition && nonPositionCount < 2 {
|
if !hasPosition && nonPositionCount < 2 {
|
||||||
return analyzeHit{}, false
|
return returnSingleIfGoodEnough()
|
||||||
}
|
}
|
||||||
if len(groupSeen) < 2 && len(signals) < 3 {
|
if len(groupSeen) < 2 && len(signals) < 3 {
|
||||||
return analyzeHit{}, false
|
return returnSingleIfGoodEnough()
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.SliceStable(signals, func(i, j int) bool {
|
sort.SliceStable(signals, func(i, j int) bool {
|
||||||
@ -1464,17 +1509,84 @@ func buildCombinedHighlightHitFromPrediction(pred TrainingPrediction, t float64)
|
|||||||
}, true
|
}, 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(
|
func appendHighlightHitsFromPrediction(
|
||||||
hits []analyzeHit,
|
hits []analyzeHit,
|
||||||
pred TrainingPrediction,
|
pred TrainingPrediction,
|
||||||
t float64,
|
t float64,
|
||||||
) []analyzeHit {
|
) []analyzeHit {
|
||||||
hit, ok := buildCombinedHighlightHitFromPrediction(pred, t)
|
next := buildHighlightHitsFromPrediction(pred, t)
|
||||||
if !ok {
|
if len(next) == 0 {
|
||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(hits, hit)
|
return append(hits, next...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func analyzeVideoFromFrames(ctx context.Context, outPath, goal string) ([]analyzeHit, error) {
|
func analyzeVideoFromFrames(ctx context.Context, outPath, goal string) ([]analyzeHit, error) {
|
||||||
@ -2191,18 +2303,27 @@ func segmentTagsFromAnalyzeLabel(label string) []string {
|
|||||||
return out
|
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 {
|
func mergeAnalyzeHits(in []analyzeHit) []analyzeHit {
|
||||||
if len(in) == 0 {
|
if len(in) == 0 {
|
||||||
return []analyzeHit{}
|
return []analyzeHit{}
|
||||||
}
|
}
|
||||||
|
|
||||||
cp := make([]analyzeHit, 0, len(in))
|
maxGap := analyzeHitContinuationGapSeconds()
|
||||||
|
|
||||||
|
byLabel := map[string][]analyzeHit{}
|
||||||
|
|
||||||
for _, h := range in {
|
for _, h := range in {
|
||||||
label := strings.ToLower(strings.TrimSpace(h.Label))
|
label := strings.ToLower(strings.TrimSpace(h.Label))
|
||||||
if label == "" {
|
if label == "" || label == "unknown" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isIgnoredNSFWLabel(label) {
|
if isIgnoredNSFWLabel(label) || isPersonSegmentLabel(label) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2221,62 +2342,84 @@ func mergeAnalyzeHits(in []analyzeHit) []analyzeHit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if start > end {
|
||||||
|
start, end = end, start
|
||||||
|
}
|
||||||
|
|
||||||
h.Label = label
|
h.Label = label
|
||||||
h.Start = start
|
h.Start = start
|
||||||
h.End = end
|
h.End = end
|
||||||
cp = append(cp, h)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cp) == 0 {
|
key := normalizeSegmentLabel(label)
|
||||||
return []analyzeHit{}
|
if key == "" {
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
out = append(out, cur)
|
byLabel[key] = append(byLabel[key], h)
|
||||||
cur = n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func inferAnalyzePointSpanSeconds(hits []analyzeHit, duration float64) float64 {
|
func inferAnalyzePointSpanSeconds(hits []analyzeHit, duration float64) float64 {
|
||||||
const fallback = 3.0
|
const fallback = 10.0
|
||||||
|
|
||||||
if len(hits) < 2 {
|
if len(hits) < 2 {
|
||||||
return fallback
|
return fallback
|
||||||
@ -2338,8 +2481,8 @@ func inferAnalyzePointSpanSeconds(hits []analyzeHit, duration float64) float64 {
|
|||||||
// aber wir deckeln, damit Sparse-Hits nicht riesig werden.
|
// aber wir deckeln, damit Sparse-Hits nicht riesig werden.
|
||||||
span := median * 0.90
|
span := median * 0.90
|
||||||
|
|
||||||
if span < 2 {
|
if span < 6 {
|
||||||
span = 2
|
span = 6
|
||||||
}
|
}
|
||||||
if span > 12 {
|
if span > 12 {
|
||||||
span = 12
|
span = 12
|
||||||
@ -2387,17 +2530,56 @@ func expandAnalyzePointToSpan(t, span, duration float64) (float64, float64) {
|
|||||||
return start, end
|
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 {
|
if len(hits) == 0 || duration <= 0 {
|
||||||
return []aiSegmentMeta{}
|
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 {
|
for _, hit := range hits {
|
||||||
if !shouldAutoSelectAnalyzeHit(hit.Label) {
|
label := strings.ToLower(strings.TrimSpace(hit.Label))
|
||||||
|
if !isAllowedAnalyzeSegmentLabel(label) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2420,201 +2602,174 @@ func buildSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegme
|
|||||||
start, end = end, start
|
start, end = end, start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if start < 0 {
|
||||||
|
start = hit.Time
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = start
|
||||||
|
}
|
||||||
|
|
||||||
start = math.Max(0, math.Min(start, duration))
|
start = math.Max(0, math.Min(start, duration))
|
||||||
end = math.Max(0, math.Min(end, duration))
|
end = math.Max(0, math.Min(end, duration))
|
||||||
|
|
||||||
// Wichtig:
|
key := normalizeSegmentLabel(label)
|
||||||
// Einzelne AI-Treffer sind oft Punkt-Treffer: Start == End.
|
if key == "" {
|
||||||
// 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 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
label := strings.ToLower(strings.TrimSpace(hit.Label))
|
score := hit.Score
|
||||||
|
if score <= 0 {
|
||||||
|
score = 1
|
||||||
|
}
|
||||||
|
|
||||||
out = append(out, aiSegmentMeta{
|
byLabel[key] = append(byLabel[key], analyzeLabelSegmentPoint{
|
||||||
Label: label,
|
Label: label,
|
||||||
StartSeconds: start,
|
Start: start,
|
||||||
EndSeconds: end,
|
End: end,
|
||||||
DurationSeconds: end - start,
|
Score: score,
|
||||||
Score: hit.Score,
|
|
||||||
AutoSelected: true,
|
|
||||||
Position: segmentPositionFromAnalyzeLabel(label),
|
|
||||||
Tags: segmentTagsFromAnalyzeLabel(label),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(out) == 0 {
|
out := make([]aiSegmentMeta, 0)
|
||||||
return []aiSegmentMeta{}
|
|
||||||
|
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 {
|
if out[i].StartSeconds != out[j].StartSeconds {
|
||||||
return out[i].StartSeconds < out[j].StartSeconds
|
return out[i].StartSeconds < out[j].StartSeconds
|
||||||
}
|
}
|
||||||
if out[i].EndSeconds != out[j].EndSeconds {
|
if out[i].EndSeconds != out[j].EndSeconds {
|
||||||
return 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))
|
return out
|
||||||
cur := out[0]
|
}
|
||||||
|
|
||||||
for i := 1; i < len(out); i++ {
|
func makeLabelContinuitySegment(
|
||||||
n := out[i]
|
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
|
start := startMarker - halfSample
|
||||||
if gap < 0 {
|
end := endMarker + halfSample
|
||||||
gap = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if sameAnalyzeSegmentLabel(cur.Label, n.Label) && gap <= analyzeSegmentMergeGapSeconds {
|
if start < 0 {
|
||||||
cur.Label = preferAnalyzeSegmentLabel(cur.Label, n.Label)
|
start = 0
|
||||||
if n.StartSeconds < cur.StartSeconds {
|
}
|
||||||
cur.StartSeconds = n.StartSeconds
|
if duration > 0 && end > duration {
|
||||||
}
|
end = duration
|
||||||
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)
|
if end <= start {
|
||||||
return mergeAdjacentAISegments(merged, analyzeSegmentMergeGapSeconds)
|
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 {
|
func buildHighlightSegmentsFromAnalyzeHits(hits []analyzeHit, duration float64) []aiSegmentMeta {
|
||||||
if len(hits) == 0 || duration <= 0 {
|
return buildLabelContinuitySegmentsFromAnalyzeHits(hits, duration)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAnalyzeSegmentsForGoal(
|
func buildAnalyzeSegmentsForGoal(
|
||||||
|
|||||||
@ -1433,29 +1433,13 @@ func hasAIAnalysisForOutputGoal(outPath string, goal string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawHits, hasHits := aiMap["hits"].([]any)
|
||||||
|
rawSegs, hasSegs := aiMap["segments"].([]any)
|
||||||
|
|
||||||
// Wichtig:
|
// Wichtig:
|
||||||
// Auch eine Analyse ohne Treffer ist fertig, solange sie gespeichert wurde.
|
// Eine leere Analyse zählt NICHT mehr als fertig.
|
||||||
if rawGoal, ok := aiMap["goal"].(string); ok && normalizeAIGoal(rawGoal) == goal {
|
// Sonst bleibt ein kaputter 0-Treffer-Run für immer gecached.
|
||||||
return true
|
return (hasHits && len(rawHits) > 0) || (hasSegs && len(rawSegs) > 0)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasAIAnalysisForAllOutputGoals(outPath string, goals []string) bool {
|
func hasAIAnalysisForAllOutputGoals(outPath string, goals []string) bool {
|
||||||
|
|||||||
@ -847,9 +847,9 @@ func computeNSFWRating(segments []aiSegmentMeta, durationSec float64) *aiRatingM
|
|||||||
0.04*confNorm
|
0.04*confNorm
|
||||||
|
|
||||||
// Sicherheits-Caps:
|
// 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 {
|
if positionEffectiveWeighted <= 0 {
|
||||||
raw = math.Min(raw, 0.58)
|
raw = math.Min(raw, 0.72)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sehr kurze/spärliche Treffer nicht überbewerten.
|
// Sehr kurze/spärliche Treffer nicht überbewerten.
|
||||||
|
|||||||
@ -50,12 +50,9 @@ type TrainingPrediction struct {
|
|||||||
ModelAvailable bool `json:"modelAvailable"`
|
ModelAvailable bool `json:"modelAvailable"`
|
||||||
Source string `json:"source,omitempty"`
|
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"`
|
SexPosition string `json:"sexPosition"`
|
||||||
SexPositionScore float64 `json:"sexPositionScore"`
|
SexPositionScore float64 `json:"sexPositionScore"`
|
||||||
|
PeoplePresent []TrainingScoredLabel `json:"peoplePresent"`
|
||||||
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
|
BodyPartsPresent []TrainingScoredLabel `json:"bodyPartsPresent"`
|
||||||
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
|
ObjectsPresent []TrainingScoredLabel `json:"objectsPresent"`
|
||||||
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
|
ClothingPresent []TrainingScoredLabel `json:"clothingPresent"`
|
||||||
@ -63,11 +60,8 @@ type TrainingPrediction struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TrainingCorrection struct {
|
type TrainingCorrection struct {
|
||||||
PeopleCount int `json:"peopleCount"`
|
|
||||||
MaleCount int `json:"maleCount"`
|
|
||||||
FemaleCount int `json:"femaleCount"`
|
|
||||||
UnknownCount int `json:"unknownCount"`
|
|
||||||
SexPosition string `json:"sexPosition"`
|
SexPosition string `json:"sexPosition"`
|
||||||
|
PeoplePresent []string `json:"peoplePresent"`
|
||||||
BodyPartsPresent []string `json:"bodyPartsPresent"`
|
BodyPartsPresent []string `json:"bodyPartsPresent"`
|
||||||
ObjectsPresent []string `json:"objectsPresent"`
|
ObjectsPresent []string `json:"objectsPresent"`
|
||||||
ClothingPresent []string `json:"clothingPresent"`
|
ClothingPresent []string `json:"clothingPresent"`
|
||||||
@ -1271,11 +1265,8 @@ func trainingEffectiveCorrection(annotation TrainingAnnotation) TrainingCorrecti
|
|||||||
p := annotation.Prediction
|
p := annotation.Prediction
|
||||||
|
|
||||||
return TrainingCorrection{
|
return TrainingCorrection{
|
||||||
PeopleCount: p.PeopleCount,
|
|
||||||
MaleCount: p.MaleCount,
|
|
||||||
FemaleCount: p.FemaleCount,
|
|
||||||
UnknownCount: p.UnknownCount,
|
|
||||||
SexPosition: p.SexPosition,
|
SexPosition: p.SexPosition,
|
||||||
|
PeoplePresent: trainingScoredLabelsToStrings(p.PeoplePresent),
|
||||||
BodyPartsPresent: trainingScoredLabelsToStrings(p.BodyPartsPresent),
|
BodyPartsPresent: trainingScoredLabelsToStrings(p.BodyPartsPresent),
|
||||||
ObjectsPresent: trainingScoredLabelsToStrings(p.ObjectsPresent),
|
ObjectsPresent: trainingScoredLabelsToStrings(p.ObjectsPresent),
|
||||||
ClothingPresent: trainingScoredLabelsToStrings(p.ClothingPresent),
|
ClothingPresent: trainingScoredLabelsToStrings(p.ClothingPresent),
|
||||||
@ -2034,12 +2025,9 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
|||||||
pred := TrainingPrediction{
|
pred := TrainingPrediction{
|
||||||
ModelAvailable: det.Available,
|
ModelAvailable: det.Available,
|
||||||
Source: det.Source,
|
Source: det.Source,
|
||||||
PeopleCount: 0,
|
|
||||||
MaleCount: 0,
|
|
||||||
FemaleCount: 0,
|
|
||||||
UnknownCount: 0,
|
|
||||||
SexPosition: "unknown",
|
SexPosition: "unknown",
|
||||||
SexPositionScore: 0,
|
SexPositionScore: 0,
|
||||||
|
PeoplePresent: []TrainingScoredLabel{},
|
||||||
BodyPartsPresent: []TrainingScoredLabel{},
|
BodyPartsPresent: []TrainingScoredLabel{},
|
||||||
ObjectsPresent: []TrainingScoredLabel{},
|
ObjectsPresent: []TrainingScoredLabel{},
|
||||||
ClothingPresent: []TrainingScoredLabel{},
|
ClothingPresent: []TrainingScoredLabel{},
|
||||||
@ -2133,17 +2121,6 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
|||||||
}
|
}
|
||||||
|
|
||||||
if peopleSet[label] {
|
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)
|
visibleBoxes = append(visibleBoxes, box)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -2158,9 +2135,9 @@ func trainingPredictionFromDetector(det TrainingDetectorPrediction) TrainingPred
|
|||||||
pred.SexPositionScore = bestSexPositionScore
|
pred.SexPositionScore = bestSexPositionScore
|
||||||
}
|
}
|
||||||
|
|
||||||
pred.PeopleCount = pred.MaleCount + pred.FemaleCount + pred.UnknownCount
|
|
||||||
pred.Boxes = visibleBoxes
|
pred.Boxes = visibleBoxes
|
||||||
|
|
||||||
|
pred.PeoplePresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.People)
|
||||||
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.BodyParts)
|
pred.BodyPartsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.BodyParts)
|
||||||
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Objects)
|
pred.ObjectsPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Objects)
|
||||||
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Clothing)
|
pred.ClothingPresent = trainingScoredLabelsFromDetectorBoxes(visibleBoxes, grouped.Clothing)
|
||||||
@ -2526,12 +2503,9 @@ func trainingEmptyPrediction(source string) TrainingPrediction {
|
|||||||
return TrainingPrediction{
|
return TrainingPrediction{
|
||||||
ModelAvailable: false,
|
ModelAvailable: false,
|
||||||
Source: source,
|
Source: source,
|
||||||
PeopleCount: 0,
|
|
||||||
MaleCount: 0,
|
|
||||||
FemaleCount: 0,
|
|
||||||
UnknownCount: 0,
|
|
||||||
SexPosition: "unknown",
|
SexPosition: "unknown",
|
||||||
SexPositionScore: 0,
|
SexPositionScore: 0,
|
||||||
|
PeoplePresent: []TrainingScoredLabel{},
|
||||||
BodyPartsPresent: []TrainingScoredLabel{},
|
BodyPartsPresent: []TrainingScoredLabel{},
|
||||||
ObjectsPresent: []TrainingScoredLabel{},
|
ObjectsPresent: []TrainingScoredLabel{},
|
||||||
ClothingPresent: []TrainingScoredLabel{},
|
ClothingPresent: []TrainingScoredLabel{},
|
||||||
|
|||||||
@ -2627,23 +2627,23 @@ export default function App() {
|
|||||||
const onlineLikedCount = headerStats.onlineLikedCount
|
const onlineLikedCount = headerStats.onlineLikedCount
|
||||||
|
|
||||||
const runningTabCount = useMemo(() => {
|
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 (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
|
if (Boolean((j as any)?.endedAt)) return false
|
||||||
return true
|
|
||||||
|
// Nur wirklich laufende Downloads zählen
|
||||||
|
return status === 'running' && (phase === '' || phase === 'recording')
|
||||||
}).length
|
}).length
|
||||||
|
}, [jobs])
|
||||||
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])
|
|
||||||
|
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -299,6 +299,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saveSuccessUntilMs, setSaveSuccessUntilMs] = useState<number>(0)
|
const [saveSuccessUntilMs, setSaveSuccessUntilMs] = useState<number>(0)
|
||||||
const saveSuccessTimerRef = useRef<number | null>(null)
|
const saveSuccessTimerRef = useRef<number | null>(null)
|
||||||
|
const appLogPreRef = useRef<HTMLPreElement | null>(null)
|
||||||
const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
|
const [browsing, setBrowsing] = useState<'record' | 'done' | 'ffmpeg' | null>(null)
|
||||||
const [msg, setMsg] = useState<string | null>(null)
|
const [msg, setMsg] = useState<string | null>(null)
|
||||||
const [err, setErr] = 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'
|
type SaveUiState = 'idle' | 'saving' | 'success' | 'error'
|
||||||
const saveUiState: SaveUiState = saving ? 'saving' : saveSucceeded ? 'success' : saveFailed ? 'error' : 'idle'
|
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 = (() => {
|
const saveButton = (() => {
|
||||||
if (saveUiState === 'saving') {
|
if (saveUiState === 'saving') {
|
||||||
return {
|
return {
|
||||||
@ -749,6 +759,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setAppLogLoading(false)
|
setAppLogLoading(false)
|
||||||
|
scrollAppLogToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -757,6 +768,12 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
void loadAppLog()
|
void loadAppLog()
|
||||||
}, [appLogOpen])
|
}, [appLogOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!appLogOpen) return
|
||||||
|
|
||||||
|
scrollAppLogToBottom()
|
||||||
|
}, [appLogOpen, appLog?.log, appLogLoading])
|
||||||
|
|
||||||
async function browse(target: 'record' | 'done' | 'ffmpeg') {
|
async function browse(target: 'record' | 'done' | 'ffmpeg') {
|
||||||
setErr(null)
|
setErr(null)
|
||||||
setMsg(null)
|
setMsg(null)
|
||||||
@ -1743,7 +1760,10 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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
|
{appLogLoading
|
||||||
? 'Log wird geladen…'
|
? 'Log wird geladen…'
|
||||||
: appLog?.log?.trim()
|
: appLog?.log?.trim()
|
||||||
|
|||||||
@ -65,12 +65,9 @@ type TrainingPrediction = {
|
|||||||
modelAvailable: boolean
|
modelAvailable: boolean
|
||||||
source?: string
|
source?: string
|
||||||
|
|
||||||
peopleCount: number
|
|
||||||
maleCount: number
|
|
||||||
femaleCount: number
|
|
||||||
unknownCount: number
|
|
||||||
sexPosition: string
|
sexPosition: string
|
||||||
sexPositionScore: number
|
sexPositionScore: number
|
||||||
|
peoplePresent: ScoredLabel[]
|
||||||
bodyPartsPresent: ScoredLabel[]
|
bodyPartsPresent: ScoredLabel[]
|
||||||
objectsPresent: ScoredLabel[]
|
objectsPresent: ScoredLabel[]
|
||||||
clothingPresent: ScoredLabel[]
|
clothingPresent: ScoredLabel[]
|
||||||
@ -97,11 +94,8 @@ type TrainingLabels = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CorrectionState = {
|
type CorrectionState = {
|
||||||
peopleCount: number
|
|
||||||
maleCount: number
|
|
||||||
femaleCount: number
|
|
||||||
unknownCount: number
|
|
||||||
sexPosition: string
|
sexPosition: string
|
||||||
|
peoplePresent: string[]
|
||||||
bodyPartsPresent: string[]
|
bodyPartsPresent: string[]
|
||||||
objectsPresent: string[]
|
objectsPresent: string[]
|
||||||
clothingPresent: string[]
|
clothingPresent: string[]
|
||||||
@ -604,11 +598,6 @@ function toggleArrayValue(arr: string[], value: string) {
|
|||||||
: [...arr, value]
|
: [...arr, value]
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeCount(value: unknown) {
|
|
||||||
const n = Number(value)
|
|
||||||
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp01(v: number) {
|
function clamp01(v: number) {
|
||||||
if (!Number.isFinite(v)) return 0
|
if (!Number.isFinite(v)) return 0
|
||||||
return Math.max(0, Math.min(1, v))
|
return Math.max(0, Math.min(1, v))
|
||||||
@ -668,32 +657,35 @@ function uniqStrings(values: string[]) {
|
|||||||
return out
|
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 {
|
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
||||||
const p = sample?.prediction
|
const p = sample?.prediction
|
||||||
|
|
||||||
const maleCount = safeCount(p?.maleCount)
|
const boxes = (p?.boxes ?? [])
|
||||||
const femaleCount = safeCount(p?.femaleCount)
|
.map((box) => ({
|
||||||
const unknownCount = safeCount(p?.unknownCount)
|
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 {
|
return {
|
||||||
peopleCount: maleCount + femaleCount + unknownCount,
|
|
||||||
maleCount,
|
|
||||||
femaleCount,
|
|
||||||
unknownCount,
|
|
||||||
sexPosition: p?.sexPosition || 'unknown',
|
sexPosition: p?.sexPosition || 'unknown',
|
||||||
|
peoplePresent: (p?.peoplePresent ?? []).map((x) => x.label),
|
||||||
bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
|
bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
|
||||||
objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
|
objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
|
||||||
clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
|
clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
|
||||||
boxes: (p?.boxes ?? [])
|
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,39 +698,12 @@ function applyBoxLabelToCorrection(
|
|||||||
if (!clean) return state
|
if (!clean) return state
|
||||||
|
|
||||||
if (labels.people.includes(clean)) {
|
if (labels.people.includes(clean)) {
|
||||||
if (clean === 'person_male' || clean === 'male_person') {
|
return {
|
||||||
return {
|
...state,
|
||||||
...state,
|
peoplePresent: state.peoplePresent.includes(clean)
|
||||||
maleCount: safeCount(state.maleCount) + 1,
|
? state.peoplePresent
|
||||||
unknownCount: 0,
|
: [...state.peoplePresent, clean],
|
||||||
peopleCount:
|
|
||||||
safeCount(state.maleCount) + 1 + safeCount(state.femaleCount),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
if (labels.bodyParts.includes(clean)) {
|
||||||
@ -786,6 +751,13 @@ function removeBoxLabelFromCorrection(
|
|||||||
// Badge nur abwählen, wenn keine weitere Box mit diesem Label existiert.
|
// Badge nur abwählen, wenn keine weitere Box mit diesem Label existiert.
|
||||||
if (remainingBoxesWithSameLabel) return state
|
if (remainingBoxesWithSameLabel) return state
|
||||||
|
|
||||||
|
if (labels.people.includes(clean)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
peoplePresent: state.peoplePresent.filter((x) => x !== clean),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (labels.bodyParts.includes(clean)) {
|
if (labels.bodyParts.includes(clean)) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -822,42 +794,11 @@ function removeBoxFromCorrection(
|
|||||||
|
|
||||||
const removedLabel = String(removed.label || '').trim()
|
const removedLabel = String(removed.label || '').trim()
|
||||||
|
|
||||||
let next: CorrectionState = {
|
const next: CorrectionState = {
|
||||||
...state,
|
...state,
|
||||||
boxes: boxes.filter((_, i) => i !== index),
|
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)
|
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)
|
next = removeBoxLabelFromCorrection(next, oldLabel, labels)
|
||||||
|
|
||||||
// Neues Label anwenden.
|
|
||||||
next = applyBoxLabelToCorrection(next, cleanNextLabel, labels)
|
next = applyBoxLabelToCorrection(next, cleanNextLabel, labels)
|
||||||
|
|
||||||
next = {
|
|
||||||
...next,
|
|
||||||
peopleCount:
|
|
||||||
safeCount(next.maleCount) +
|
|
||||||
safeCount(next.femaleCount) +
|
|
||||||
safeCount(next.unknownCount),
|
|
||||||
}
|
|
||||||
|
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1503,8 +1401,12 @@ function CollapsibleLabelSection(props: {
|
|||||||
singleDrawMode?: boolean
|
singleDrawMode?: boolean
|
||||||
gridClassName?: string
|
gridClassName?: string
|
||||||
}) {
|
}) {
|
||||||
|
const cleanDrawLabel = String(props.drawLabel || '').trim()
|
||||||
|
const hasDrawLabelInSection =
|
||||||
|
cleanDrawLabel !== '' && props.values.includes(cleanDrawLabel)
|
||||||
|
|
||||||
const activeCount = props.singleDrawMode
|
const activeCount = props.singleDrawMode
|
||||||
? Math.max(props.selected.length, props.drawLabel ? 1 : 0)
|
? Math.max(props.selected.length, hasDrawLabelInSection ? 1 : 0)
|
||||||
: props.selected.length
|
: props.selected.length
|
||||||
|
|
||||||
const hasActiveItems = activeCount > 0
|
const hasActiveItems = activeCount > 0
|
||||||
@ -2262,12 +2164,13 @@ export default function TrainingTab(props: {
|
|||||||
}, [sample?.prediction.boxes, labels.people])
|
}, [sample?.prediction.boxes, labels.people])
|
||||||
|
|
||||||
const selectedPeopleLabels = useMemo(() => {
|
const selectedPeopleLabels = useMemo(() => {
|
||||||
return uniqStrings(
|
return correction.peoplePresent
|
||||||
correctionBoxes
|
}, [correction.peoplePresent])
|
||||||
.map((box) => String(box.label || '').trim())
|
|
||||||
.filter((label) => labels.people.includes(label))
|
const drawLabelForSection = useCallback((values: string[]) => {
|
||||||
)
|
const clean = String(boxLabel || '').trim()
|
||||||
}, [correctionBoxes, labels.people])
|
return values.includes(clean) ? clean : ''
|
||||||
|
}, [boxLabel])
|
||||||
|
|
||||||
const imageSrc = useMemo(() => {
|
const imageSrc = useMemo(() => {
|
||||||
if (!sample?.frameUrl) return ''
|
if (!sample?.frameUrl) return ''
|
||||||
@ -2445,18 +2348,11 @@ export default function TrainingTab(props: {
|
|||||||
setCorrection(nextCorrection)
|
setCorrection(nextCorrection)
|
||||||
setHasManualCorrection(false)
|
setHasManualCorrection(false)
|
||||||
|
|
||||||
const currentLabels = labelsRef.current
|
|
||||||
const personLabels = new Set(currentLabels.people)
|
|
||||||
|
|
||||||
const hasPersonBoxes = nextCorrection.boxes.some((box) =>
|
|
||||||
personLabels.has(box.label)
|
|
||||||
)
|
|
||||||
|
|
||||||
setExpandedCorrectionSections({
|
setExpandedCorrectionSections({
|
||||||
sexPosition:
|
sexPosition:
|
||||||
Boolean(nextCorrection.sexPosition) &&
|
Boolean(nextCorrection.sexPosition) &&
|
||||||
nextCorrection.sexPosition !== 'unknown',
|
nextCorrection.sexPosition !== 'unknown',
|
||||||
people: hasPersonBoxes || nextCorrection.peopleCount > 0,
|
people: nextCorrection.peoplePresent.length > 0,
|
||||||
bodyParts: nextCorrection.bodyPartsPresent.length > 0,
|
bodyParts: nextCorrection.bodyPartsPresent.length > 0,
|
||||||
objects: nextCorrection.objectsPresent.length > 0,
|
objects: nextCorrection.objectsPresent.length > 0,
|
||||||
clothing: nextCorrection.clothingPresent.length > 0,
|
clothing: nextCorrection.clothingPresent.length > 0,
|
||||||
@ -2805,19 +2701,14 @@ export default function TrainingTab(props: {
|
|||||||
setMessage(null)
|
setMessage(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const maleCount = safeCount(correction.maleCount)
|
const normalizedBoxes = (correction.boxes ?? [])
|
||||||
const femaleCount = safeCount(correction.femaleCount)
|
.map(normalizeBox)
|
||||||
const unknownCount = safeCount(correction.unknownCount)
|
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
||||||
|
|
||||||
const correctionPayload: CorrectionState = {
|
const correctionPayload: CorrectionState = {
|
||||||
...correction,
|
...correction,
|
||||||
maleCount,
|
peoplePresent: peopleLabelsFromBoxes(normalizedBoxes, labelsRef.current),
|
||||||
femaleCount,
|
boxes: normalizedBoxes,
|
||||||
unknownCount,
|
|
||||||
peopleCount: maleCount + femaleCount + unknownCount,
|
|
||||||
boxes: (correction.boxes ?? [])
|
|
||||||
.map(normalizeBox)
|
|
||||||
.filter((box) => box.label && box.w > 0 && box.h > 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -3230,10 +3121,7 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
setCorrection((prev) => ({
|
setCorrection((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
maleCount: 0,
|
peoplePresent: [],
|
||||||
femaleCount: 0,
|
|
||||||
unknownCount: 0,
|
|
||||||
peopleCount: 0,
|
|
||||||
bodyPartsPresent: [],
|
bodyPartsPresent: [],
|
||||||
objectsPresent: [],
|
objectsPresent: [],
|
||||||
clothingPresent: [],
|
clothingPresent: [],
|
||||||
@ -4647,7 +4535,7 @@ export default function TrainingTab(props: {
|
|||||||
toggleMobileCorrectionSection('people', expanded)
|
toggleMobileCorrectionSection('people', expanded)
|
||||||
}
|
}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.people)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
singleDrawMode
|
singleDrawMode
|
||||||
@ -4680,7 +4568,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.bodyParts)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
@ -4711,7 +4599,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.objects)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
@ -4742,7 +4630,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.clothing)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
@ -4838,7 +4726,7 @@ export default function TrainingTab(props: {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onToggle={() => {}}
|
onToggle={() => {}}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.people)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
singleDrawMode
|
singleDrawMode
|
||||||
@ -4868,7 +4756,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.bodyParts)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
@ -4896,7 +4784,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.objects)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
@ -4924,7 +4812,7 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={drawLabelForSection(labels.clothing)}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user