bugfixes
This commit is contained in:
parent
2d5ef7741a
commit
de4bfe8cef
@ -23,7 +23,7 @@ import (
|
|||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
||||||
const (
|
const (
|
||||||
diskGuardInterval = 5 * time.Second
|
diskGuardInterval = 1 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var diskEmergency int32 // 0=false, 1=true
|
var diskEmergency int32 // 0=false, 1=true
|
||||||
@ -47,18 +47,7 @@ func diskStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
pauseGB, resumeGB, _, _, _ := computeDiskThresholds()
|
pauseGB, resumeGB, _, _, _ := computeDiskThresholds()
|
||||||
|
|
||||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
free, dir, _ := minFreeBytesForDiskGuard(s)
|
||||||
dir := strings.TrimSpace(recordDirAbs)
|
|
||||||
if dir == "" {
|
|
||||||
dir = strings.TrimSpace(s.RecordDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
free := uint64(0)
|
|
||||||
if dir != "" {
|
|
||||||
if u, err := godisk.Usage(dir); err == nil && u != nil {
|
|
||||||
free = u.Free
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := diskStatusResp{
|
resp := diskStatusResp{
|
||||||
Emergency: atomic.LoadInt32(&diskEmergency) == 1,
|
Emergency: atomic.LoadInt32(&diskEmergency) == 1,
|
||||||
@ -175,60 +164,61 @@ func inFlightBytesForJob(j *RecordJob) uint64 {
|
|||||||
if j == nil {
|
if j == nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
// Prefer live-tracked bytes if available (accurate & cheap).
|
|
||||||
|
// Dateisystem ist die harte Wahrheit. SizeBytes kann bei laufenden Downloads
|
||||||
|
// verzögert oder gar nicht aktualisiert sein.
|
||||||
|
statSize := sizeOfPathBestEffort(j.Output)
|
||||||
|
|
||||||
|
liveSize := uint64(0)
|
||||||
if j.SizeBytes > 0 {
|
if j.SizeBytes > 0 {
|
||||||
return uint64(j.SizeBytes)
|
liveSize = uint64(j.SizeBytes)
|
||||||
}
|
}
|
||||||
return sizeOfPathBestEffort(j.Output)
|
|
||||||
|
if statSize > liveSize {
|
||||||
|
return statSize
|
||||||
|
}
|
||||||
|
return liveSize
|
||||||
}
|
}
|
||||||
|
|
||||||
const giB = uint64(1024 * 1024 * 1024)
|
const giB = uint64(1024 * 1024 * 1024)
|
||||||
|
|
||||||
// computeDiskThresholds:
|
// computeDiskThresholds:
|
||||||
// Pause = max(lowDiskPauseBelowGB, ceil(relevantInFlightBytes / GiB))
|
|
||||||
// Resume = Pause + 3 GB (Hysterese)
|
|
||||||
//
|
//
|
||||||
// relevantInFlightBytes = Summe aller laufenden Downloads,
|
// Wir brauchen:
|
||||||
// deren aktuelle Dateigröße über AutoDeleteSmallDownloadsBelowMB liegt.
|
// 1. die konfigurierte Mindestreserve
|
||||||
|
// 2. zusätzlich ungefähr die Größe aller aktiven/queued TS-Dateien,
|
||||||
|
// weil TS -> MP4 temporär nochmal ungefähr dieselbe Größe benötigt.
|
||||||
//
|
//
|
||||||
// Damit greift immer mindestens die konfigurierte Mindestschwelle,
|
// PauseNeed = configReserve + conversionReserve
|
||||||
// zusätzlich aber auch eine dynamische Schwelle basierend auf den
|
// ResumeNeed = PauseNeed + 3 GiB Hysterese
|
||||||
// "relevanten" Downloads, die nicht automatisch gelöscht würden.
|
|
||||||
func computeDiskThresholds() (pauseGB int, resumeGB int, relevantInFlight uint64, pauseNeed uint64, resumeNeed uint64) {
|
func computeDiskThresholds() (pauseGB int, resumeGB int, relevantInFlight uint64, pauseNeed uint64, resumeNeed uint64) {
|
||||||
s := getSettings()
|
s := getSettings()
|
||||||
|
|
||||||
runningBytes := sumInFlightBytesAboveAutoDeleteThreshold()
|
|
||||||
queuedPostworkBytes := reservedQueuedPostworkBytesAboveAutoDeleteThreshold()
|
|
||||||
|
|
||||||
// Gesamtrelevanz = laufende Schreiblast + reservierter Bedarf der anstehenden Nacharbeit
|
|
||||||
relevantInFlight = runningBytes + queuedPostworkBytes
|
|
||||||
|
|
||||||
configPauseGB := s.LowDiskPauseBelowGB
|
configPauseGB := s.LowDiskPauseBelowGB
|
||||||
if configPauseGB <= 0 {
|
if configPauseGB <= 0 {
|
||||||
configPauseGB = 5
|
configPauseGB = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicPauseGB := 0
|
runningBytes := sumActiveRecordingBytes()
|
||||||
if relevantInFlight > 0 {
|
postworkBytes := reservedPostworkBytes()
|
||||||
dynamicPauseGB = int((relevantInFlight + giB - 1) / giB) // ceil
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseGB = dynamicPauseGB
|
relevantInFlight = runningBytes + postworkBytes
|
||||||
if pauseGB < configPauseGB {
|
|
||||||
pauseGB = configPauseGB
|
configReserve := uint64(configPauseGB) * giB
|
||||||
}
|
|
||||||
|
pauseNeed = configReserve + relevantInFlight
|
||||||
|
resumeNeed = pauseNeed + 3*giB
|
||||||
|
|
||||||
|
pauseGB = int((pauseNeed + giB - 1) / giB)
|
||||||
|
resumeGB = int((resumeNeed + giB - 1) / giB)
|
||||||
|
|
||||||
if pauseGB > 10_000 {
|
if pauseGB > 10_000 {
|
||||||
pauseGB = 10_000
|
pauseGB = 10_000
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeGB = pauseGB + 3
|
|
||||||
if resumeGB > 10_000 {
|
if resumeGB > 10_000 {
|
||||||
resumeGB = 10_000
|
resumeGB = 10_000
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseNeed = uint64(pauseGB) * giB
|
|
||||||
resumeNeed = uint64(resumeGB) * giB
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,16 +244,7 @@ func sumInFlightBytes() uint64 {
|
|||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
func sumActiveRecordingBytes() uint64 {
|
||||||
s := getSettings()
|
|
||||||
|
|
||||||
thresholdMB := s.AutoDeleteSmallDownloadsBelowMB
|
|
||||||
if thresholdMB < 0 {
|
|
||||||
thresholdMB = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
thresholdBytes := uint64(thresholdMB) * 1024 * 1024
|
|
||||||
|
|
||||||
var sum uint64
|
var sum uint64
|
||||||
|
|
||||||
jobsMu.RLock()
|
jobsMu.RLock()
|
||||||
@ -273,7 +254,9 @@ func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
|||||||
if j == nil {
|
if j == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if j.Status != JobRunning {
|
|
||||||
|
// Nur echte aktive Recorder. Postwork wird separat gezählt.
|
||||||
|
if !isActiveRecordingJob(j) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,44 +265,29 @@ func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nur Downloads berücksichtigen, die über der Auto-Delete-Grenze liegen.
|
|
||||||
// Kleine Dateien würden später ohnehin automatisch entfernt.
|
|
||||||
if size <= thresholdBytes {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sum += size
|
sum += size
|
||||||
}
|
}
|
||||||
|
|
||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldReserveQueuedPostworkJob(j *RecordJob) bool {
|
func shouldReservePostworkJob(j *RecordJob) bool {
|
||||||
if j == nil {
|
if j == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bereits laufende Jobs werden schon über sumInFlight... erfasst.
|
|
||||||
if j.Status == JobRunning {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur Jobs mit PostWork-Bezug
|
|
||||||
if !isPostworkJob(j) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminale Jobs nicht berücksichtigen
|
|
||||||
if isTerminalJobStatus(j.Status) {
|
if isTerminalJobStatus(j.Status) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nur wenn wirklich noch ein PostWorkKey existiert
|
if !isPostworkJob(j) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(j.PostWorkKey) == "" {
|
if strings.TrimSpace(j.PostWorkKey) == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// queued / running aus der Queue-Info berücksichtigen
|
|
||||||
if j.PostWork != nil {
|
if j.PostWork != nil {
|
||||||
st := strings.ToLower(strings.TrimSpace(j.PostWork.State))
|
st := strings.ToLower(strings.TrimSpace(j.PostWork.State))
|
||||||
if st == "queued" || st == "running" {
|
if st == "queued" || st == "running" {
|
||||||
@ -330,27 +298,18 @@ func shouldReserveQueuedPostworkJob(j *RecordJob) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback-Heuristik
|
|
||||||
state := getEffectivePostworkState(j)
|
state := getEffectivePostworkState(j)
|
||||||
return state == "queued" || state == "running"
|
return state == "queued" || state == "running"
|
||||||
}
|
}
|
||||||
|
|
||||||
func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
func reservedPostworkBytes() uint64 {
|
||||||
s := getSettings()
|
|
||||||
|
|
||||||
thresholdMB := s.AutoDeleteSmallDownloadsBelowMB
|
|
||||||
if thresholdMB < 0 {
|
|
||||||
thresholdMB = 0
|
|
||||||
}
|
|
||||||
thresholdBytes := uint64(thresholdMB) * 1024 * 1024
|
|
||||||
|
|
||||||
var sum uint64
|
var sum uint64
|
||||||
|
|
||||||
jobsMu.RLock()
|
jobsMu.RLock()
|
||||||
defer jobsMu.RUnlock()
|
defer jobsMu.RUnlock()
|
||||||
|
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if !shouldReserveQueuedPostworkJob(j) {
|
if !shouldReservePostworkJob(j) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,17 +318,72 @@ func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kleine Dateien ignorieren, wenn sie ohnehin auto-gelöscht würden
|
|
||||||
if size <= thresholdBytes {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sum += size
|
sum += size
|
||||||
}
|
}
|
||||||
|
|
||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveDiskGuardDirs(s RecorderSettings) []string {
|
||||||
|
rawDirs := []string{s.RecordDir, s.DoneDir}
|
||||||
|
|
||||||
|
seen := map[string]bool{}
|
||||||
|
out := make([]string, 0, len(rawDirs))
|
||||||
|
|
||||||
|
for _, raw := range rawDirs {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, _ := resolvePathRelativeToApp(raw)
|
||||||
|
dir = strings.TrimSpace(dir)
|
||||||
|
if dir == "" {
|
||||||
|
dir = raw
|
||||||
|
}
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
key := strings.ToLower(dir)
|
||||||
|
|
||||||
|
if seen[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
out = append(out, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func minFreeBytesForDiskGuard(s RecorderSettings) (uint64, string, bool) {
|
||||||
|
dirs := resolveDiskGuardDirs(s)
|
||||||
|
if len(dirs) == 0 {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var minFree uint64
|
||||||
|
var minDir string
|
||||||
|
okAny := false
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
u, err := godisk.Usage(dir)
|
||||||
|
if err != nil || u == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !okAny || u.Free < minFree {
|
||||||
|
minFree = u.Free
|
||||||
|
minDir = dir
|
||||||
|
okAny = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minFree, minDir, okAny
|
||||||
|
}
|
||||||
|
|
||||||
// startDiskSpaceGuard läuft im Backend und reagiert auch ohne offenen Browser.
|
// startDiskSpaceGuard läuft im Backend und reagiert auch ohne offenen Browser.
|
||||||
// Bei wenig freiem Platz:
|
// Bei wenig freiem Platz:
|
||||||
// - diskEmergency aktivieren (Autostart blockieren)
|
// - diskEmergency aktivieren (Autostart blockieren)
|
||||||
@ -378,58 +392,31 @@ func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
|||||||
// Bei Erholung (Resume-Schwelle):
|
// Bei Erholung (Resume-Schwelle):
|
||||||
// - diskEmergency automatisch wieder freigeben
|
// - diskEmergency automatisch wieder freigeben
|
||||||
func startDiskSpaceGuard() {
|
func startDiskSpaceGuard() {
|
||||||
t := time.NewTicker(diskGuardInterval)
|
checkOnce := func() {
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
for range t.C {
|
|
||||||
s := getSettings()
|
s := getSettings()
|
||||||
|
|
||||||
// Pfad bestimmen, auf dem wir freien Speicher prüfen
|
free, dir, ok := minFreeBytesForDiskGuard(s)
|
||||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
if !ok {
|
||||||
dir := strings.TrimSpace(recordDirAbs)
|
return
|
||||||
if dir == "" {
|
|
||||||
dir = strings.TrimSpace(s.RecordDir)
|
|
||||||
}
|
|
||||||
if dir == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := godisk.Usage(dir)
|
|
||||||
if err != nil || u == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
free := u.Free
|
|
||||||
|
|
||||||
// ✅ Schwellen:
|
|
||||||
// Pause = max(config lowDiskPauseBelowGB, ceil((2 * inFlight) / GiB))
|
|
||||||
// Resume = Pause + 3 GB
|
|
||||||
// pauseNeed/resumeNeed sind die benötigten freien Bytes
|
|
||||||
pauseGB, resumeGB, relevantInFlight, pauseNeed, resumeNeed := computeDiskThresholds()
|
pauseGB, resumeGB, relevantInFlight, pauseNeed, resumeNeed := computeDiskThresholds()
|
||||||
|
|
||||||
// ✅ diskEmergency NICHT sticky behalten.
|
|
||||||
// Stattdessen dynamisch mit Hysterese setzen/löschen:
|
|
||||||
//
|
|
||||||
// - triggern bei free < pauseNeed
|
|
||||||
// - freigeben erst bei free >= resumeNeed
|
|
||||||
//
|
|
||||||
// So kann die Notbremse später erneut greifen.
|
|
||||||
|
|
||||||
wasEmergency := atomic.LoadInt32(&diskEmergency) == 1
|
wasEmergency := atomic.LoadInt32(&diskEmergency) == 1
|
||||||
|
|
||||||
isLowForPause := free < pauseNeed
|
isLowForPause := free < pauseNeed
|
||||||
isHighEnoughForResume := free >= resumeNeed
|
isHighEnoughForResume := free >= resumeNeed
|
||||||
|
|
||||||
if !wasEmergency {
|
if !wasEmergency {
|
||||||
// Normalzustand: nur triggern, wenn unter Pause-Schwelle
|
|
||||||
if !isLowForPause {
|
if !isLowForPause {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic.StoreInt32(&diskEmergency, 1)
|
atomic.StoreInt32(&diskEmergency, 1)
|
||||||
broadcastAutostartPaused()
|
broadcastAutostartPaused()
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"🛑 [disk] Low space: free=%s (%dB) (< %s, %dB, pause=%dGB resume=%dGB, relevantBytes=%s, %dB) -> stop jobs + block autostart via diskEmergency (path=%s)\n",
|
"🛑 [disk] Low space: free=%s (%dB) (< need=%s, %dB, pause=%dGB resume=%dGB, reservedForTS2MP4=%s, %dB) -> stop downloads + block autostart (path=%s)\n",
|
||||||
formatBytesSI(u64ToI64(free)), free,
|
formatBytesSI(u64ToI64(free)), free,
|
||||||
formatBytesSI(u64ToI64(pauseNeed)), pauseNeed,
|
formatBytesSI(u64ToI64(pauseNeed)), pauseNeed,
|
||||||
pauseGB, resumeGB,
|
pauseGB, resumeGB,
|
||||||
@ -439,18 +426,18 @@ func startDiskSpaceGuard() {
|
|||||||
|
|
||||||
stopped := stopAllStoppableJobs()
|
stopped := stopAllStoppableJobs()
|
||||||
if stopped > 0 {
|
if stopped > 0 {
|
||||||
fmt.Printf("🛑 [disk] Stop requested for %d job(s)\n", stopped)
|
fmt.Printf("🛑 [disk] Stop requested for %d download job(s)\n", stopped)
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Emergency ist aktiv: nur freigeben, wenn Resume-Schwelle erreicht ist
|
|
||||||
if isHighEnoughForResume {
|
if isHighEnoughForResume {
|
||||||
atomic.StoreInt32(&diskEmergency, 0)
|
atomic.StoreInt32(&diskEmergency, 0)
|
||||||
broadcastAutostartPaused()
|
broadcastAutostartPaused()
|
||||||
|
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"✅ [disk] Space recovered: free=%s (%dB) (>= %s, %dB, resume=%dGB, relevantBytes=%s, %dB) -> unblock autostart (path=%s)\n",
|
"✅ [disk] Space recovered: free=%s (%dB) (>= resume=%s, %dB, resume=%dGB, reservedForTS2MP4=%s, %dB) -> unblock autostart (path=%s)\n",
|
||||||
formatBytesSI(u64ToI64(free)), free,
|
formatBytesSI(u64ToI64(free)), free,
|
||||||
formatBytesSI(u64ToI64(resumeNeed)), resumeNeed,
|
formatBytesSI(u64ToI64(resumeNeed)), resumeNeed,
|
||||||
resumeGB,
|
resumeGB,
|
||||||
@ -459,4 +446,48 @@ func startDiskSpaceGuard() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sofort beim Start prüfen, nicht erst nach dem ersten Tick.
|
||||||
|
checkOnce()
|
||||||
|
|
||||||
|
t := time.NewTicker(diskGuardInterval)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for range t.C {
|
||||||
|
checkOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureDiskGuardAllowsStart() error {
|
||||||
|
if atomic.LoadInt32(&diskEmergency) == 1 {
|
||||||
|
return fmt.Errorf("Speicherplatz-Notbremse aktiv: neuer Download wird blockiert")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := getSettings()
|
||||||
|
|
||||||
|
free, dir, ok := minFreeBytesForDiskGuard(s)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseGB, resumeGB, relevantInFlight, pauseNeed, _ := computeDiskThresholds()
|
||||||
|
if free >= pauseNeed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.StoreInt32(&diskEmergency, 1)
|
||||||
|
broadcastAutostartPaused()
|
||||||
|
|
||||||
|
stopped := stopAllStoppableJobs()
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Speicherplatz-Notbremse aktiv: frei=%s, benötigt=%s, pause=%dGB, resume=%dGB, reserve=%s, gestoppt=%d, pfad=%s",
|
||||||
|
formatBytesSI(u64ToI64(free)),
|
||||||
|
formatBytesSI(u64ToI64(pauseNeed)),
|
||||||
|
pauseGB,
|
||||||
|
resumeGB,
|
||||||
|
formatBytesSI(u64ToI64(relevantInFlight)),
|
||||||
|
stopped,
|
||||||
|
dir,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -436,6 +436,10 @@ func startRecordingInternal(req RecordRequest) (*RecordJob, error) {
|
|||||||
return nil, fmt.Errorf("unsupported provider: %q", url)
|
return nil, fmt.Errorf("unsupported provider: %q", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ensureDiskGuardAllowsStart(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
defer jobsMu.Unlock()
|
defer jobsMu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@ -1182,6 +1182,43 @@ function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null {
|
|||||||
const normalized = normalizeSegmentLabel(label)
|
const normalized = normalizeSegmentLabel(label)
|
||||||
if (!normalized) return null
|
if (!normalized) return null
|
||||||
|
|
||||||
|
const labelKey = normalizeLabelKey(normalized)
|
||||||
|
|
||||||
|
// Wichtig:
|
||||||
|
// Personen-Labels exakt vor jedem Fuzzy-Matching behandeln.
|
||||||
|
// Sonst kann "person" auch "person_male" / "person_female" matchen.
|
||||||
|
if (labelKey === 'person_male' || labelKey === 'male_person') {
|
||||||
|
return {
|
||||||
|
match: ['person_male', 'male_person'],
|
||||||
|
text: 'Männliche Person',
|
||||||
|
icon: MalePersonIcon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelKey === 'person_female' || labelKey === 'female_person') {
|
||||||
|
return {
|
||||||
|
match: ['person_female', 'female_person'],
|
||||||
|
text: 'Weibliche Person',
|
||||||
|
icon: FemalePersonIcon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelKey === 'person' || labelKey === 'person_unknown') {
|
||||||
|
return {
|
||||||
|
match: ['person_unknown', 'person'],
|
||||||
|
text: 'Person',
|
||||||
|
icon: UnknownContentIcon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erst exakte Matches prüfen.
|
||||||
|
for (const item of SEGMENT_LABEL_META) {
|
||||||
|
if (item.match.some((key) => labelKey === normalizeLabelKey(key))) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Danach erst weiche Matches prüfen.
|
||||||
for (const item of SEGMENT_LABEL_META) {
|
for (const item of SEGMENT_LABEL_META) {
|
||||||
if (item.match.some((key) => labelMatches(normalized, key))) {
|
if (item.match.some((key) => labelMatches(normalized, key))) {
|
||||||
return item
|
return item
|
||||||
|
|||||||
@ -636,7 +636,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load()
|
load()
|
||||||
const t = window.setInterval(load, 5000) // alle 5s aktualisieren
|
const t = window.setInterval(load, 1000) // jede Sekunde aktualisieren
|
||||||
return () => {
|
return () => {
|
||||||
alive = false
|
alive = false
|
||||||
window.clearInterval(t)
|
window.clearInterval(t)
|
||||||
|
|||||||
@ -1235,17 +1235,14 @@ export default function TrainingTab() {
|
|||||||
}, [sample?.prediction.clothingPresent])
|
}, [sample?.prediction.clothingPresent])
|
||||||
|
|
||||||
const peopleScores = useMemo(() => {
|
const peopleScores = useMemo(() => {
|
||||||
const entries = (sample?.prediction.boxes ?? [])
|
|
||||||
.filter((box) => box.label && labels.people.includes(box.label))
|
|
||||||
.map((box) => [box.label, box.score])
|
|
||||||
|
|
||||||
const bestScores = new Map<string, number>()
|
const bestScores = new Map<string, number>()
|
||||||
|
|
||||||
for (const [label, score] of entries) {
|
for (const box of sample?.prediction.boxes ?? []) {
|
||||||
const cleanLabel = String(label || '').trim()
|
const cleanLabel = String(box.label || '').trim()
|
||||||
const n = Number(score)
|
const n = Number(box.score)
|
||||||
|
|
||||||
if (!cleanLabel || !Number.isFinite(n)) continue
|
if (!cleanLabel || !labels.people.includes(cleanLabel)) continue
|
||||||
|
if (!Number.isFinite(n)) continue
|
||||||
|
|
||||||
const current = bestScores.get(cleanLabel)
|
const current = bestScores.get(cleanLabel)
|
||||||
if (current === undefined || n > current) {
|
if (current === undefined || n > current) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user