bugfixes
This commit is contained in:
parent
2d5ef7741a
commit
de4bfe8cef
@ -23,7 +23,7 @@ import (
|
||||
// -------------------------
|
||||
|
||||
const (
|
||||
diskGuardInterval = 5 * time.Second
|
||||
diskGuardInterval = 1 * time.Second
|
||||
)
|
||||
|
||||
var diskEmergency int32 // 0=false, 1=true
|
||||
@ -47,18 +47,7 @@ func diskStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
pauseGB, resumeGB, _, _, _ := computeDiskThresholds()
|
||||
|
||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
||||
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
|
||||
}
|
||||
}
|
||||
free, dir, _ := minFreeBytesForDiskGuard(s)
|
||||
|
||||
resp := diskStatusResp{
|
||||
Emergency: atomic.LoadInt32(&diskEmergency) == 1,
|
||||
@ -175,60 +164,61 @@ func inFlightBytesForJob(j *RecordJob) uint64 {
|
||||
if j == nil {
|
||||
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 {
|
||||
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)
|
||||
|
||||
// computeDiskThresholds:
|
||||
// Pause = max(lowDiskPauseBelowGB, ceil(relevantInFlightBytes / GiB))
|
||||
// Resume = Pause + 3 GB (Hysterese)
|
||||
//
|
||||
// relevantInFlightBytes = Summe aller laufenden Downloads,
|
||||
// deren aktuelle Dateigröße über AutoDeleteSmallDownloadsBelowMB liegt.
|
||||
// Wir brauchen:
|
||||
// 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,
|
||||
// zusätzlich aber auch eine dynamische Schwelle basierend auf den
|
||||
// "relevanten" Downloads, die nicht automatisch gelöscht würden.
|
||||
// PauseNeed = configReserve + conversionReserve
|
||||
// ResumeNeed = PauseNeed + 3 GiB Hysterese
|
||||
func computeDiskThresholds() (pauseGB int, resumeGB int, relevantInFlight uint64, pauseNeed uint64, resumeNeed uint64) {
|
||||
s := getSettings()
|
||||
|
||||
runningBytes := sumInFlightBytesAboveAutoDeleteThreshold()
|
||||
queuedPostworkBytes := reservedQueuedPostworkBytesAboveAutoDeleteThreshold()
|
||||
|
||||
// Gesamtrelevanz = laufende Schreiblast + reservierter Bedarf der anstehenden Nacharbeit
|
||||
relevantInFlight = runningBytes + queuedPostworkBytes
|
||||
|
||||
configPauseGB := s.LowDiskPauseBelowGB
|
||||
if configPauseGB <= 0 {
|
||||
configPauseGB = 5
|
||||
}
|
||||
|
||||
dynamicPauseGB := 0
|
||||
if relevantInFlight > 0 {
|
||||
dynamicPauseGB = int((relevantInFlight + giB - 1) / giB) // ceil
|
||||
}
|
||||
runningBytes := sumActiveRecordingBytes()
|
||||
postworkBytes := reservedPostworkBytes()
|
||||
|
||||
pauseGB = dynamicPauseGB
|
||||
if pauseGB < configPauseGB {
|
||||
pauseGB = configPauseGB
|
||||
}
|
||||
relevantInFlight = runningBytes + postworkBytes
|
||||
|
||||
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 {
|
||||
pauseGB = 10_000
|
||||
}
|
||||
|
||||
resumeGB = pauseGB + 3
|
||||
if resumeGB > 10_000 {
|
||||
resumeGB = 10_000
|
||||
}
|
||||
|
||||
pauseNeed = uint64(pauseGB) * giB
|
||||
resumeNeed = uint64(resumeGB) * giB
|
||||
return
|
||||
}
|
||||
|
||||
@ -254,16 +244,7 @@ func sumInFlightBytes() uint64 {
|
||||
return sum
|
||||
}
|
||||
|
||||
func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
||||
s := getSettings()
|
||||
|
||||
thresholdMB := s.AutoDeleteSmallDownloadsBelowMB
|
||||
if thresholdMB < 0 {
|
||||
thresholdMB = 0
|
||||
}
|
||||
|
||||
thresholdBytes := uint64(thresholdMB) * 1024 * 1024
|
||||
|
||||
func sumActiveRecordingBytes() uint64 {
|
||||
var sum uint64
|
||||
|
||||
jobsMu.RLock()
|
||||
@ -273,7 +254,9 @@ func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
if j.Status != JobRunning {
|
||||
|
||||
// Nur echte aktive Recorder. Postwork wird separat gezählt.
|
||||
if !isActiveRecordingJob(j) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -282,44 +265,29 @@ func sumInFlightBytesAboveAutoDeleteThreshold() uint64 {
|
||||
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
|
||||
}
|
||||
|
||||
return sum
|
||||
}
|
||||
|
||||
func shouldReserveQueuedPostworkJob(j *RecordJob) bool {
|
||||
func shouldReservePostworkJob(j *RecordJob) bool {
|
||||
if j == nil {
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Nur wenn wirklich noch ein PostWorkKey existiert
|
||||
if !isPostworkJob(j) {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.TrimSpace(j.PostWorkKey) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// queued / running aus der Queue-Info berücksichtigen
|
||||
if j.PostWork != nil {
|
||||
st := strings.ToLower(strings.TrimSpace(j.PostWork.State))
|
||||
if st == "queued" || st == "running" {
|
||||
@ -330,27 +298,18 @@ func shouldReserveQueuedPostworkJob(j *RecordJob) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback-Heuristik
|
||||
state := getEffectivePostworkState(j)
|
||||
return state == "queued" || state == "running"
|
||||
}
|
||||
|
||||
func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
||||
s := getSettings()
|
||||
|
||||
thresholdMB := s.AutoDeleteSmallDownloadsBelowMB
|
||||
if thresholdMB < 0 {
|
||||
thresholdMB = 0
|
||||
}
|
||||
thresholdBytes := uint64(thresholdMB) * 1024 * 1024
|
||||
|
||||
func reservedPostworkBytes() uint64 {
|
||||
var sum uint64
|
||||
|
||||
jobsMu.RLock()
|
||||
defer jobsMu.RUnlock()
|
||||
|
||||
for _, j := range jobs {
|
||||
if !shouldReserveQueuedPostworkJob(j) {
|
||||
if !shouldReservePostworkJob(j) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -359,17 +318,72 @@ func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Kleine Dateien ignorieren, wenn sie ohnehin auto-gelöscht würden
|
||||
if size <= thresholdBytes {
|
||||
continue
|
||||
}
|
||||
|
||||
sum += size
|
||||
}
|
||||
|
||||
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.
|
||||
// Bei wenig freiem Platz:
|
||||
// - diskEmergency aktivieren (Autostart blockieren)
|
||||
@ -378,58 +392,31 @@ func reservedQueuedPostworkBytesAboveAutoDeleteThreshold() uint64 {
|
||||
// Bei Erholung (Resume-Schwelle):
|
||||
// - diskEmergency automatisch wieder freigeben
|
||||
func startDiskSpaceGuard() {
|
||||
t := time.NewTicker(diskGuardInterval)
|
||||
defer t.Stop()
|
||||
|
||||
for range t.C {
|
||||
checkOnce := func() {
|
||||
s := getSettings()
|
||||
|
||||
// Pfad bestimmen, auf dem wir freien Speicher prüfen
|
||||
recordDirAbs, _ := resolvePathRelativeToApp(s.RecordDir)
|
||||
dir := strings.TrimSpace(recordDirAbs)
|
||||
if dir == "" {
|
||||
dir = strings.TrimSpace(s.RecordDir)
|
||||
}
|
||||
if dir == "" {
|
||||
continue
|
||||
free, dir, ok := minFreeBytesForDiskGuard(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// ✅ 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
|
||||
|
||||
isLowForPause := free < pauseNeed
|
||||
isHighEnoughForResume := free >= resumeNeed
|
||||
|
||||
if !wasEmergency {
|
||||
// Normalzustand: nur triggern, wenn unter Pause-Schwelle
|
||||
if !isLowForPause {
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
atomic.StoreInt32(&diskEmergency, 1)
|
||||
broadcastAutostartPaused()
|
||||
|
||||
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(pauseNeed)), pauseNeed,
|
||||
pauseGB, resumeGB,
|
||||
@ -439,18 +426,18 @@ func startDiskSpaceGuard() {
|
||||
|
||||
stopped := stopAllStoppableJobs()
|
||||
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 {
|
||||
atomic.StoreInt32(&diskEmergency, 0)
|
||||
broadcastAutostartPaused()
|
||||
|
||||
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(resumeNeed)), resumeNeed,
|
||||
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)
|
||||
}
|
||||
|
||||
if err := ensureDiskGuardAllowsStart(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
|
||||
|
||||
@ -1182,6 +1182,43 @@ function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null {
|
||||
const normalized = normalizeSegmentLabel(label)
|
||||
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) {
|
||||
if (item.match.some((key) => labelMatches(normalized, key))) {
|
||||
return item
|
||||
|
||||
@ -636,7 +636,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
||||
}
|
||||
|
||||
load()
|
||||
const t = window.setInterval(load, 5000) // alle 5s aktualisieren
|
||||
const t = window.setInterval(load, 1000) // jede Sekunde aktualisieren
|
||||
return () => {
|
||||
alive = false
|
||||
window.clearInterval(t)
|
||||
|
||||
@ -1235,17 +1235,14 @@ export default function TrainingTab() {
|
||||
}, [sample?.prediction.clothingPresent])
|
||||
|
||||
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>()
|
||||
|
||||
for (const [label, score] of entries) {
|
||||
const cleanLabel = String(label || '').trim()
|
||||
const n = Number(score)
|
||||
for (const box of sample?.prediction.boxes ?? []) {
|
||||
const cleanLabel = String(box.label || '').trim()
|
||||
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)
|
||||
if (current === undefined || n > current) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user