bugfixes
This commit is contained in:
parent
2311857661
commit
f0eef673d9
@ -44,6 +44,18 @@ func logChaturbateAutoStartError(prefix string, modelURL string, err error) {
|
|||||||
appLogln(prefix, modelURL, err)
|
appLogln(prefix, modelURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func chaturbateAutoStartCandidateIndex(queue []autoStartItem, paused bool) int {
|
||||||
|
for i, candidate := range queue {
|
||||||
|
if paused && !candidate.force {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
func normUser(s string) string {
|
func normUser(s string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(s))
|
return strings.ToLower(strings.TrimSpace(s))
|
||||||
}
|
}
|
||||||
@ -315,7 +327,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scanInterval = 1 * time.Second
|
const scanInterval = 1 * time.Second
|
||||||
const startInterval = 5 * time.Second
|
const startInterval = 500 * time.Millisecond
|
||||||
|
const pendingRefreshInterval = 5 * time.Second
|
||||||
|
|
||||||
const snapshotMaxAge = 90 * time.Second
|
const snapshotMaxAge = 90 * time.Second
|
||||||
|
|
||||||
@ -593,6 +606,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
|
|
||||||
nextWatchedPending := make([]PendingAutoStartItem, 0, len(watchedOrder))
|
nextWatchedPending := make([]PendingAutoStartItem, 0, len(watchedOrder))
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
waitingForPublic := false
|
||||||
|
|
||||||
resumePendingThisScan := map[string]bool{}
|
resumePendingThisScan := map[string]bool{}
|
||||||
|
|
||||||
@ -650,6 +664,7 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
pendingAutoStartMu.Unlock()
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
resumePendingThisScan[user] = true
|
resumePendingThisScan[user] = true
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
if verboseLogs() {
|
if verboseLogs() {
|
||||||
appLogln("⏸️ [autostart] stopped because model is no longer public:", user, show)
|
appLogln("⏸️ [autostart] stopped because model is no longer public:", user, show)
|
||||||
@ -689,6 +704,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
queued[user] = true
|
queued[user] = true
|
||||||
|
|
||||||
case "private", "hidden", "away":
|
case "private", "hidden", "away":
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
if resumePendingThisScan[user] {
|
if resumePendingThisScan[user] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -711,6 +728,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
pendingAutoStartMu.Unlock()
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
case "unknown":
|
case "unknown":
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
if resumePendingThisScan[user] {
|
if resumePendingThisScan[user] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -763,6 +782,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
queued[user] = true
|
queued[user] = true
|
||||||
|
|
||||||
case "private", "hidden", "away":
|
case "private", "hidden", "away":
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
// Wenn der User in diesem Scan gerade wegen private/hidden/away
|
// Wenn der User in diesem Scan gerade wegen private/hidden/away
|
||||||
// aus einem laufenden Download gestoppt wurde, existiert bereits
|
// aus einem laufenden Download gestoppt wurde, existiert bereits
|
||||||
// ein echter resume-Eintrag. Dann keinen normalen watched-Eintrag
|
// ein echter resume-Eintrag. Dann keinen normalen watched-Eintrag
|
||||||
@ -831,6 +852,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
queued[user] = true
|
queued[user] = true
|
||||||
|
|
||||||
case "private", "hidden", "away":
|
case "private", "hidden", "away":
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
// Manual Pending bleibt wartend, solange das Model nicht offline ist.
|
// Manual Pending bleibt wartend, solange das Model nicht offline ist.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -842,6 +865,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
waitingForPublic = true
|
||||||
|
|
||||||
// unknown nicht als offline/wartend speichern.
|
// unknown nicht als offline/wartend speichern.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -851,9 +876,13 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
_ = saveWatchedPendingAutoStartItemsForProvider("chaturbate", nextWatchedPending)
|
_ = saveWatchedPendingAutoStartItemsForProvider("chaturbate", nextWatchedPending)
|
||||||
pendingAutoStartMu.Unlock()
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
|
if waitingForPublic {
|
||||||
|
requestChaturbateSnapshotRefresh(pendingRefreshInterval)
|
||||||
|
}
|
||||||
|
|
||||||
case <-startTicker.C:
|
case <-startTicker.C:
|
||||||
s := getSettings()
|
s := getSettings()
|
||||||
if !s.UseProviderAPIs || !s.UseChaturbateAPI || !s.AutoStartAddedDownloads || isAutostartPaused() {
|
if !s.UseProviderAPIs || !s.UseChaturbateAPI {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -879,17 +908,8 @@ func startChaturbateAutoStartWorker(store *ModelStore) {
|
|||||||
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
showByUser[u] = normalizePendingShowServer(r.CurrentShow)
|
||||||
}
|
}
|
||||||
|
|
||||||
paused := isAutostartPaused()
|
paused := isAutostartPaused() || !s.AutoStartAddedDownloads
|
||||||
|
startIdx := chaturbateAutoStartCandidateIndex(queue, paused)
|
||||||
startIdx := -1
|
|
||||||
for i, candidate := range queue {
|
|
||||||
if paused && !candidate.force {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
startIdx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if startIdx < 0 {
|
if startIdx < 0 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
29
backend/chaturbate_autostart_test.go
Normal file
29
backend/chaturbate_autostart_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestChaturbateAutoStartCandidateIndex(t *testing.T) {
|
||||||
|
queue := []autoStartItem{
|
||||||
|
{userKey: "watched", source: "watched"},
|
||||||
|
{userKey: "resume", source: "resume", force: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := chaturbateAutoStartCandidateIndex(queue, false); got != 0 {
|
||||||
|
t.Fatalf("unpaused candidate index = %d, want 0", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := chaturbateAutoStartCandidateIndex(queue, true); got != 1 {
|
||||||
|
t.Fatalf("paused candidate index = %d, want forced resume at 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChaturbateAutoStartCandidateIndexPausedWithoutResume(t *testing.T) {
|
||||||
|
queue := []autoStartItem{
|
||||||
|
{userKey: "watched", source: "watched"},
|
||||||
|
{userKey: "manual", source: "manual"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := chaturbateAutoStartCandidateIndex(queue, true); got != -1 {
|
||||||
|
t.Fatalf("paused candidate index = %d, want -1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -520,6 +520,54 @@ func refreshChaturbateSnapshotNow(ctx context.Context) (time.Time, error) {
|
|||||||
return fetchedAtNow, nil
|
return fetchedAtNow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestChaturbateSnapshotRefresh(minInterval time.Duration) bool {
|
||||||
|
if !chaturbateProviderAPIEnabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cbRefreshMu.Lock()
|
||||||
|
if cbRefreshInFlight {
|
||||||
|
cbRefreshMu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cbMu.RLock()
|
||||||
|
lastAttempt := cb.LastAttempt
|
||||||
|
cbMu.RUnlock()
|
||||||
|
|
||||||
|
if minInterval > 0 && !lastAttempt.IsZero() && now.Sub(lastAttempt) < minInterval {
|
||||||
|
cbRefreshMu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cbRefreshInFlight = true
|
||||||
|
|
||||||
|
cbMu.Lock()
|
||||||
|
cb.LastAttempt = now
|
||||||
|
cbMu.Unlock()
|
||||||
|
|
||||||
|
cbRefreshMu.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
cbRefreshMu.Lock()
|
||||||
|
cbRefreshInFlight = false
|
||||||
|
cbRefreshMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, err := refreshChaturbateSnapshotNow(ctx); err != nil && verboseLogs() {
|
||||||
|
appLogln("⚠️ [autostart] chaturbate pending refresh failed:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func triggerChaturbateStoreSync(store *ModelStore, rooms []ChaturbateRoom, fetchedAt time.Time) {
|
func triggerChaturbateStoreSync(store *ModelStore, rooms []ChaturbateRoom, fetchedAt time.Time) {
|
||||||
if store == nil {
|
if store == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -45,10 +45,10 @@ var previewFileRe = regexp.MustCompile(`^(index(_hq)?\.m3u8|seg_(low|hq)_\d+\.ts
|
|||||||
var cbHlsRefreshMu sync.Mutex
|
var cbHlsRefreshMu sync.Mutex
|
||||||
var cbHlsRefreshAt = map[string]time.Time{} // key=userLower -> last refresh time
|
var cbHlsRefreshAt = map[string]time.Time{} // key=userLower -> last refresh time
|
||||||
|
|
||||||
func shouldRefreshHLS(userLower string, minInterval time.Duration) bool {
|
func beginHLSRefresh(userLower string, minInterval time.Duration) (time.Time, bool) {
|
||||||
userLower = strings.ToLower(strings.TrimSpace(userLower))
|
userLower = strings.ToLower(strings.TrimSpace(userLower))
|
||||||
if userLower == "" {
|
if userLower == "" {
|
||||||
return false
|
return time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
cbHlsRefreshMu.Lock()
|
cbHlsRefreshMu.Lock()
|
||||||
@ -56,11 +56,30 @@ func shouldRefreshHLS(userLower string, minInterval time.Duration) bool {
|
|||||||
|
|
||||||
last := cbHlsRefreshAt[userLower]
|
last := cbHlsRefreshAt[userLower]
|
||||||
if !last.IsZero() && time.Since(last) < minInterval {
|
if !last.IsZero() && time.Since(last) < minInterval {
|
||||||
return false
|
return time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
cbHlsRefreshAt[userLower] = time.Now()
|
startedAt := time.Now()
|
||||||
return true
|
cbHlsRefreshAt[userLower] = startedAt
|
||||||
|
return startedAt, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishHLSRefresh(userLower string, startedAt time.Time, success bool) {
|
||||||
|
if success || startedAt.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userLower = strings.ToLower(strings.TrimSpace(userLower))
|
||||||
|
if userLower == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cbHlsRefreshMu.Lock()
|
||||||
|
defer cbHlsRefreshMu.Unlock()
|
||||||
|
|
||||||
|
if current := cbHlsRefreshAt[userLower]; current.Equal(startedAt) {
|
||||||
|
delete(cbHlsRefreshAt, userLower)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchCurrentBestHLS lädt die Room-Seite und parsed hls_source.
|
// fetchCurrentBestHLS lädt die Room-Seite und parsed hls_source.
|
||||||
@ -193,9 +212,22 @@ func refreshPreviewLiveSourceForJob(r *http.Request, job *RecordJob) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ wichtig: doppelten Refresh verhindern
|
// ✅ wichtig: doppelten Refresh verhindern
|
||||||
if !shouldRefreshHLS(strings.ToLower(username), 10*time.Second) {
|
userLower := strings.ToLower(username)
|
||||||
|
refreshInterval := 10 * time.Second
|
||||||
|
forceRefresh := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("refresh")), "1") ||
|
||||||
|
strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("refresh")), "true")
|
||||||
|
if forceRefresh {
|
||||||
|
refreshInterval = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshStartedAt, shouldRefresh := beginHLSRefresh(userLower, refreshInterval)
|
||||||
|
if !shouldRefresh {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
refreshSucceeded := false
|
||||||
|
defer func() {
|
||||||
|
finishHLSRefresh(userLower, refreshStartedAt, refreshSucceeded)
|
||||||
|
}()
|
||||||
|
|
||||||
ctxRefresh, cancelRefresh := context.WithTimeout(r.Context(), 8*time.Second)
|
ctxRefresh, cancelRefresh := context.WithTimeout(r.Context(), 8*time.Second)
|
||||||
defer cancelRefresh()
|
defer cancelRefresh()
|
||||||
@ -225,6 +257,7 @@ func refreshPreviewLiveSourceForJob(r *http.Request, job *RecordJob) error {
|
|||||||
stopPreview(job)
|
stopPreview(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshSucceeded = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,6 +292,7 @@ func recordPreviewLive(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
_ = json.NewEncoder(w).Encode(resp{
|
_ = json.NewEncoder(w).Encode(resp{
|
||||||
OK: false,
|
OK: false,
|
||||||
Prepared: false,
|
Prepared: false,
|
||||||
|
|||||||
50
backend/live_test.go
Normal file
50
backend/live_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clearHLSRefreshForTest(user string) {
|
||||||
|
cbHlsRefreshMu.Lock()
|
||||||
|
delete(cbHlsRefreshAt, user)
|
||||||
|
cbHlsRefreshMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailedHLSRefreshCanRetryImmediately(t *testing.T) {
|
||||||
|
const user = "failed-refresh-test"
|
||||||
|
clearHLSRefreshForTest(user)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
clearHLSRefreshForTest(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
startedAt, ok := beginHLSRefresh(user, 10*time.Second)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected first refresh reservation")
|
||||||
|
}
|
||||||
|
|
||||||
|
finishHLSRefresh(user, startedAt, false)
|
||||||
|
|
||||||
|
if _, ok := beginHLSRefresh(user, 10*time.Second); !ok {
|
||||||
|
t.Fatal("failed refresh must not throttle the next attempt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSuccessfulHLSRefreshRemainsThrottled(t *testing.T) {
|
||||||
|
const user = "successful-refresh-test"
|
||||||
|
clearHLSRefreshForTest(user)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
clearHLSRefreshForTest(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
startedAt, ok := beginHLSRefresh(user, 10*time.Second)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected first refresh reservation")
|
||||||
|
}
|
||||||
|
|
||||||
|
finishHLSRefresh(user, startedAt, true)
|
||||||
|
|
||||||
|
if _, ok := beginHLSRefresh(user, 10*time.Second); ok {
|
||||||
|
t.Fatal("successful refresh should throttle duplicate attempts")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,9 +3,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ultralytics import YOLO
|
from ultralytics import YOLO
|
||||||
|
from ultralytics.models.yolo.detect.train import DetectionTrainer
|
||||||
|
|
||||||
|
|
||||||
def emit_progress(stage, progress, message="", **extra):
|
def emit_progress(stage, progress, message="", **extra):
|
||||||
@ -51,6 +53,70 @@ def safe_int(value, fallback):
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def batch_sample_id(batch):
|
||||||
|
if not isinstance(batch, dict):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
image_paths = batch.get("im_file")
|
||||||
|
if not image_paths:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(image_paths, (str, Path)):
|
||||||
|
image_path = image_paths
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
image_path = image_paths[0]
|
||||||
|
except (IndexError, KeyError, TypeError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return Path(str(image_path)).stem.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def progress_detection_trainer(train_count, val_count, train_device, fallback_epochs):
|
||||||
|
class ProgressDetectionTrainer(DetectionTrainer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._preview_epoch = 0
|
||||||
|
self._preview_batch = 0
|
||||||
|
self._preview_emitted_at = 0.0
|
||||||
|
|
||||||
|
def preprocess_batch(self, batch):
|
||||||
|
epoch = int(getattr(self, "epoch", 0)) + 1
|
||||||
|
total_epochs = int(getattr(self, "epochs", fallback_epochs) or fallback_epochs)
|
||||||
|
|
||||||
|
if epoch != self._preview_epoch:
|
||||||
|
self._preview_epoch = epoch
|
||||||
|
self._preview_batch = 0
|
||||||
|
|
||||||
|
self._preview_batch += 1
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
if self._preview_batch == 1 or now - self._preview_emitted_at >= 0.5:
|
||||||
|
sample_id = batch_sample_id(batch)
|
||||||
|
|
||||||
|
if sample_id:
|
||||||
|
total_batches = max(1, len(self.train_loader))
|
||||||
|
completed = (epoch - 1) + min(1.0, self._preview_batch / total_batches)
|
||||||
|
|
||||||
|
emit_progress(
|
||||||
|
"detector",
|
||||||
|
0.04 + 0.90 * (completed / max(1, total_epochs)),
|
||||||
|
"Object Detector trainiert…",
|
||||||
|
epoch=epoch,
|
||||||
|
epochs=total_epochs,
|
||||||
|
sampleId=sample_id,
|
||||||
|
trainSamples=train_count,
|
||||||
|
valSamples=val_count,
|
||||||
|
device=str(train_device),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._preview_emitted_at = now
|
||||||
|
|
||||||
|
return super().preprocess_batch(batch)
|
||||||
|
|
||||||
|
return ProgressDetectionTrainer
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--root", required=True)
|
parser.add_argument("--root", required=True)
|
||||||
@ -198,6 +264,12 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = model.train(
|
result = model.train(
|
||||||
|
trainer=progress_detection_trainer(
|
||||||
|
train_count,
|
||||||
|
val_count,
|
||||||
|
train_device,
|
||||||
|
epochs,
|
||||||
|
),
|
||||||
data=str(yaml_path),
|
data=str(yaml_path),
|
||||||
epochs=epochs,
|
epochs=epochs,
|
||||||
imgsz=imgsz,
|
imgsz=imgsz,
|
||||||
|
|||||||
@ -148,6 +148,7 @@ type TrainingJobStatus struct {
|
|||||||
StartedAt string `json:"startedAt,omitempty"`
|
StartedAt string `json:"startedAt,omitempty"`
|
||||||
FinishedAt string `json:"finishedAt,omitempty"`
|
FinishedAt string `json:"finishedAt,omitempty"`
|
||||||
DurationMs int64 `json:"durationMs,omitempty"`
|
DurationMs int64 `json:"durationMs,omitempty"`
|
||||||
|
PreviewURL string `json:"previewUrl,omitempty"`
|
||||||
|
|
||||||
Stage string `json:"stage,omitempty"`
|
Stage string `json:"stage,omitempty"`
|
||||||
Epoch int `json:"epoch,omitempty"`
|
Epoch int `json:"epoch,omitempty"`
|
||||||
@ -194,6 +195,7 @@ type trainingProgressEvent struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Epoch int `json:"epoch,omitempty"`
|
Epoch int `json:"epoch,omitempty"`
|
||||||
Epochs int `json:"epochs,omitempty"`
|
Epochs int `json:"epochs,omitempty"`
|
||||||
|
SampleID string `json:"sampleId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingFeedbackListResponse struct {
|
type TrainingFeedbackListResponse struct {
|
||||||
@ -686,6 +688,13 @@ func trainingHandleProgressLine(line string, start int, end int, defaultStep str
|
|||||||
if ev.Epochs > 0 {
|
if ev.Epochs > 0 {
|
||||||
s.Epochs = ev.Epochs
|
s.Epochs = ev.Epochs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sampleID := strings.TrimSpace(ev.SampleID)
|
||||||
|
if sampleID != "" &&
|
||||||
|
!strings.Contains(sampleID, "/") &&
|
||||||
|
!strings.Contains(sampleID, "\\") {
|
||||||
|
s.PreviewURL = "/api/training/frame?id=" + url.QueryEscape(sampleID)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -1059,6 +1068,7 @@ func trainingFinishCancelled(root string) {
|
|||||||
s.Error = ""
|
s.Error = ""
|
||||||
s.FinishedAt = finishedAt.Format(time.RFC3339)
|
s.FinishedAt = finishedAt.Format(time.RFC3339)
|
||||||
s.DurationMs = durationMs
|
s.DurationMs = durationMs
|
||||||
|
s.PreviewURL = ""
|
||||||
|
|
||||||
if cleanupErr != nil {
|
if cleanupErr != nil {
|
||||||
s.Message = "Training wurde abgebrochen, aber temporäre Trainingsausgaben konnten nicht vollständig gelöscht werden."
|
s.Message = "Training wurde abgebrochen, aber temporäre Trainingsausgaben konnten nicht vollständig gelöscht werden."
|
||||||
@ -2510,6 +2520,7 @@ func trainingRunJob(ctx context.Context, root string, count int) {
|
|||||||
s.Error = errorText
|
s.Error = errorText
|
||||||
s.FinishedAt = finishedAt.Format(time.RFC3339)
|
s.FinishedAt = finishedAt.Format(time.RFC3339)
|
||||||
s.DurationMs = durationMs
|
s.DurationMs = durationMs
|
||||||
|
s.PreviewURL = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
trainingClearJobCancel()
|
trainingClearJobCancel()
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default function LiveVideo({
|
|||||||
onVolumeChange,
|
onVolumeChange,
|
||||||
onLoadingStart,
|
onLoadingStart,
|
||||||
onReady,
|
onReady,
|
||||||
|
onUnavailable,
|
||||||
}: {
|
}: {
|
||||||
src: string
|
src: string
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
@ -23,13 +24,19 @@ export default function LiveVideo({
|
|||||||
onVolumeChange?: (volume: number, muted: boolean) => void
|
onVolumeChange?: (volume: number, muted: boolean) => void
|
||||||
onLoadingStart?: () => void
|
onLoadingStart?: () => void
|
||||||
onReady?: () => void
|
onReady?: () => void
|
||||||
|
onUnavailable?: () => void
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLVideoElement>(null)
|
const ref = useRef<HTMLVideoElement>(null)
|
||||||
const [broken, setBroken] = useState(false)
|
const [failure, setFailure] = useState<{
|
||||||
const [brokenReason, setBrokenReason] = useState<'private' | 'hidden' | 'away' | 'offline' | 'unknown' | null>(null)
|
src: string
|
||||||
|
reason: 'private' | 'hidden' | 'away' | 'offline' | 'unknown'
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const onLoadingStartRef = useRef(onLoadingStart)
|
const onLoadingStartRef = useRef(onLoadingStart)
|
||||||
const onReadyRef = useRef(onReady)
|
const onReadyRef = useRef(onReady)
|
||||||
|
const onUnavailableRef = useRef(onUnavailable)
|
||||||
|
const mutedRef = useRef(muted)
|
||||||
|
const volumeRef = useRef(volume)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoadingStartRef.current = onLoadingStart
|
onLoadingStartRef.current = onLoadingStart
|
||||||
@ -39,6 +46,10 @@ export default function LiveVideo({
|
|||||||
onReadyRef.current = onReady
|
onReadyRef.current = onReady
|
||||||
}, [onReady])
|
}, [onReady])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onUnavailableRef.current = onUnavailable
|
||||||
|
}, [onUnavailable])
|
||||||
|
|
||||||
const normalizeBrokenReason = (
|
const normalizeBrokenReason = (
|
||||||
status?: string
|
status?: string
|
||||||
): 'private' | 'hidden' | 'away' | 'offline' | 'unknown' => {
|
): 'private' | 'hidden' | 'away' | 'offline' | 'unknown' => {
|
||||||
@ -70,38 +81,69 @@ export default function LiveVideo({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const broken = Boolean(src && failure?.src === src)
|
||||||
|
const brokenReason = broken ? failure?.reason ?? null : null
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let watchdogTimer: number | null = null
|
let watchdogTimer: number | null = null
|
||||||
|
let retryTimer: number | null = null
|
||||||
let readySent = false
|
let readySent = false
|
||||||
let stalledRetries = 0
|
let consecutiveRetries = 0
|
||||||
|
let loadAttempt = 0
|
||||||
let attemptStartedAt = Date.now()
|
let attemptStartedAt = Date.now()
|
||||||
let lastProgressAt = Date.now()
|
let lastProgressAt = Date.now()
|
||||||
|
let retryPending = false
|
||||||
|
let failed = false
|
||||||
|
|
||||||
const video = ref.current
|
const video = ref.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
setBroken(false)
|
applyInlineVideoPolicy(video, { muted: mutedRef.current })
|
||||||
setBrokenReason(null)
|
video.volume = Math.max(0, Math.min(1, volumeRef.current))
|
||||||
|
video.muted = mutedRef.current || volumeRef.current <= 0
|
||||||
applyInlineVideoPolicy(video, { muted })
|
|
||||||
video.volume = Math.max(0, Math.min(1, volume))
|
|
||||||
video.muted = muted || volume <= 0
|
|
||||||
|
|
||||||
const hardReset = () => {
|
const hardReset = () => {
|
||||||
try {
|
try {
|
||||||
video.pause()
|
video.pause()
|
||||||
video.removeAttribute('src')
|
video.removeAttribute('src')
|
||||||
video.load()
|
video.load()
|
||||||
} catch {}
|
} catch {
|
||||||
|
// Best effort while the media element is being replaced.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markBroken = () => {
|
const markBroken = () => {
|
||||||
|
if (failed) return
|
||||||
|
failed = true
|
||||||
|
hardReset()
|
||||||
|
|
||||||
const reason = normalizeBrokenReason(roomStatus)
|
const reason = normalizeBrokenReason(roomStatus)
|
||||||
setBroken(true)
|
setFailure({ src, reason })
|
||||||
setBrokenReason(reason)
|
onUnavailableRef.current?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDefinitiveRoomFailure = () => {
|
||||||
|
const reason = normalizeBrokenReason(roomStatus)
|
||||||
|
|
||||||
|
return reason === 'private' ||
|
||||||
|
reason === 'hidden' ||
|
||||||
|
reason === 'away' ||
|
||||||
|
reason === 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptUrl = () => {
|
||||||
|
loadAttempt += 1
|
||||||
|
|
||||||
|
const separator = src.includes('?') ? '&' : '?'
|
||||||
|
const forceRefresh = loadAttempt > 1 ? '&refresh=1' : ''
|
||||||
|
|
||||||
|
return `${src}${separator}liveRetry=${Date.now()}-${loadAttempt}${forceRefresh}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const beginLoad = () => {
|
const beginLoad = () => {
|
||||||
|
if (failed) return
|
||||||
|
|
||||||
|
retryPending = false
|
||||||
readySent = false
|
readySent = false
|
||||||
attemptStartedAt = Date.now()
|
attemptStartedAt = Date.now()
|
||||||
lastProgressAt = Date.now()
|
lastProgressAt = Date.now()
|
||||||
@ -109,7 +151,7 @@ export default function LiveVideo({
|
|||||||
onLoadingStartRef.current?.()
|
onLoadingStartRef.current?.()
|
||||||
|
|
||||||
hardReset()
|
hardReset()
|
||||||
video.src = src
|
video.src = attemptUrl()
|
||||||
video.load()
|
video.load()
|
||||||
|
|
||||||
const playPromise = video.play()
|
const playPromise = video.play()
|
||||||
@ -120,11 +162,27 @@ export default function LiveVideo({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const retryOrBreak = () => {
|
||||||
|
if (failed || retryPending) return
|
||||||
|
|
||||||
|
if (hasDefinitiveRoomFailure() || consecutiveRetries >= 2) {
|
||||||
|
markBroken()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveRetries += 1
|
||||||
|
retryPending = true
|
||||||
|
onLoadingStartRef.current?.()
|
||||||
|
|
||||||
|
retryTimer = window.setTimeout(() => {
|
||||||
|
retryTimer = null
|
||||||
|
beginLoad()
|
||||||
|
}, Math.min(1_500, consecutiveRetries * 400))
|
||||||
|
}
|
||||||
|
|
||||||
hardReset()
|
hardReset()
|
||||||
|
|
||||||
if (!src) {
|
if (!src) {
|
||||||
setBroken(false)
|
|
||||||
setBrokenReason(null)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +196,7 @@ export default function LiveVideo({
|
|||||||
|
|
||||||
const onTime = () => {
|
const onTime = () => {
|
||||||
lastProgressAt = Date.now()
|
lastProgressAt = Date.now()
|
||||||
|
consecutiveRetries = 0
|
||||||
markReady()
|
markReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,11 +212,12 @@ export default function LiveVideo({
|
|||||||
|
|
||||||
const onPlaying = () => {
|
const onPlaying = () => {
|
||||||
lastProgressAt = Date.now()
|
lastProgressAt = Date.now()
|
||||||
|
consecutiveRetries = 0
|
||||||
markReady()
|
markReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
markBroken()
|
retryOrBreak()
|
||||||
}
|
}
|
||||||
|
|
||||||
video.addEventListener('timeupdate', onTime)
|
video.addEventListener('timeupdate', onTime)
|
||||||
@ -173,31 +233,21 @@ export default function LiveVideo({
|
|||||||
|
|
||||||
// Stream wurde nie ready -> nicht ewig Spinner zeigen
|
// Stream wurde nie ready -> nicht ewig Spinner zeigen
|
||||||
if (!readySent && now - attemptStartedAt > 12_000) {
|
if (!readySent && now - attemptStartedAt > 12_000) {
|
||||||
if (stalledRetries < 1) {
|
retryOrBreak()
|
||||||
stalledRetries += 1
|
|
||||||
beginLoad()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
markBroken()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream war ready, hängt aber später
|
// Stream war ready, hängt aber später
|
||||||
if (readySent && !video.paused && now - lastProgressAt > 12_000) {
|
if (readySent && !video.paused && now - lastProgressAt > 12_000) {
|
||||||
if (stalledRetries < 1) {
|
retryOrBreak()
|
||||||
stalledRetries += 1
|
|
||||||
beginLoad()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
markBroken()
|
|
||||||
}
|
}
|
||||||
}, 1_000)
|
}, 1_000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (watchdogTimer) window.clearInterval(watchdogTimer)
|
if (watchdogTimer) window.clearInterval(watchdogTimer)
|
||||||
watchdogTimer = null
|
watchdogTimer = null
|
||||||
|
if (retryTimer) window.clearTimeout(retryTimer)
|
||||||
|
retryTimer = null
|
||||||
|
|
||||||
video.removeEventListener('timeupdate', onTime)
|
video.removeEventListener('timeupdate', onTime)
|
||||||
video.removeEventListener('loadeddata', onLoadedData)
|
video.removeEventListener('loadeddata', onLoadedData)
|
||||||
@ -211,9 +261,13 @@ export default function LiveVideo({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = ref.current
|
const video = ref.current
|
||||||
if (!video) return
|
|
||||||
|
|
||||||
const safeVolume = Math.max(0, Math.min(1, volume))
|
const safeVolume = Math.max(0, Math.min(1, volume))
|
||||||
|
volumeRef.current = safeVolume
|
||||||
|
mutedRef.current = muted
|
||||||
|
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
video.volume = safeVolume
|
video.volume = safeVolume
|
||||||
video.muted = muted || safeVolume <= 0
|
video.muted = muted || safeVolume <= 0
|
||||||
}, [volume, muted])
|
}, [volume, muted])
|
||||||
|
|||||||
@ -68,16 +68,23 @@ export default function ModelPreview({
|
|||||||
|
|
||||||
const [livePreparing, setLivePreparing] = useState(false)
|
const [livePreparing, setLivePreparing] = useState(false)
|
||||||
const [liveReady, setLiveReady] = useState(false)
|
const [liveReady, setLiveReady] = useState(false)
|
||||||
|
const [liveUnavailable, setLiveUnavailable] = useState(false)
|
||||||
|
|
||||||
const [popupMuted, setPopupMuted] = useState(true)
|
const [popupMuted, setPopupMuted] = useState(true)
|
||||||
const [popupVolume, setPopupVolume] = useState(1)
|
const [popupVolume, setPopupVolume] = useState(1)
|
||||||
|
|
||||||
const handleLiveLoadingStart = useCallback(() => {
|
const handleLiveLoadingStart = useCallback(() => {
|
||||||
setLiveReady(false)
|
setLiveReady(false)
|
||||||
|
setLiveUnavailable(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLiveReady = useCallback(() => {
|
const handleLiveReady = useCallback(() => {
|
||||||
setLiveReady(true)
|
setLiveReady(true)
|
||||||
|
setLiveUnavailable(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleLiveUnavailable = useCallback(() => {
|
||||||
|
setLiveUnavailable(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleLiveVolumeChange = useCallback((nextVolume: number, nextMuted: boolean) => {
|
const handleLiveVolumeChange = useCallback((nextVolume: number, nextMuted: boolean) => {
|
||||||
@ -151,31 +158,52 @@ export default function ModelPreview({
|
|||||||
setLiveSrc('')
|
setLiveSrc('')
|
||||||
setLivePreparing(false)
|
setLivePreparing(false)
|
||||||
setLiveReady(false)
|
setLiveReady(false)
|
||||||
|
setLiveUnavailable(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setLivePreparing(true)
|
setLivePreparing(true)
|
||||||
setLiveReady(false)
|
setLiveReady(false)
|
||||||
|
setLiveUnavailable(false)
|
||||||
|
|
||||||
const cookieHeader = buildChaturbateCookieHeader()
|
const cookieHeader = buildChaturbateCookieHeader()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prepareUrl = apiUrl(
|
let prepared = false
|
||||||
`/api/preview/live?id=${encodeURIComponent(jobId)}&hover=1&play=1&prepare=1`
|
|
||||||
)
|
|
||||||
|
|
||||||
const prepareRes = await apiFetch(prepareUrl, {
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
method: 'GET',
|
const prepareUrl = apiUrl(
|
||||||
cache: 'no-store',
|
`/api/preview/live?id=${encodeURIComponent(jobId)}&hover=1&play=1&prepare=1&attempt=${attempt + 1}`
|
||||||
signal: ac.signal,
|
)
|
||||||
headers: cookieHeader
|
|
||||||
? { 'X-Chaturbate-Cookie': cookieHeader }
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (ac.signal.aborted) return
|
const prepareRes = await apiFetch(prepareUrl, {
|
||||||
await prepareRes.json().catch(() => null)
|
method: 'GET',
|
||||||
if (ac.signal.aborted) return
|
cache: 'no-store',
|
||||||
|
signal: ac.signal,
|
||||||
|
headers: cookieHeader
|
||||||
|
? { 'X-Chaturbate-Cookie': cookieHeader }
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ac.signal.aborted) return
|
||||||
|
|
||||||
|
const prepareData = await prepareRes.json().catch(() => null)
|
||||||
|
if (ac.signal.aborted) return
|
||||||
|
|
||||||
|
if (prepareRes.ok && prepareData?.ok !== false) {
|
||||||
|
prepared = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt === 0) {
|
||||||
|
await new Promise((resolve) => window.setTimeout(resolve, 500))
|
||||||
|
if (ac.signal.aborted) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prepared) {
|
||||||
|
throw new Error('Live preview preparation failed')
|
||||||
|
}
|
||||||
|
|
||||||
setLiveSrc(
|
setLiveSrc(
|
||||||
apiUrl(`/api/preview/live?id=${encodeURIComponent(jobId)}&hover=1&play=1&ts=${Date.now()}`)
|
apiUrl(`/api/preview/live?id=${encodeURIComponent(jobId)}&hover=1&play=1&ts=${Date.now()}`)
|
||||||
@ -226,6 +254,7 @@ export default function ModelPreview({
|
|||||||
setLiveSrc('')
|
setLiveSrc('')
|
||||||
setLivePreparing(false)
|
setLivePreparing(false)
|
||||||
setLiveReady(false)
|
setLiveReady(false)
|
||||||
|
setLiveUnavailable(false)
|
||||||
}, [jobId])
|
}, [jobId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -333,6 +362,7 @@ export default function ModelPreview({
|
|||||||
setLiveSrc('')
|
setLiveSrc('')
|
||||||
setLivePreparing(false)
|
setLivePreparing(false)
|
||||||
setLiveReady(false)
|
setLiveReady(false)
|
||||||
|
setLiveUnavailable(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
content={(open, { close }) =>
|
content={(open, { close }) =>
|
||||||
@ -350,11 +380,12 @@ export default function ModelPreview({
|
|||||||
roomStatus={roomStatus}
|
roomStatus={roomStatus}
|
||||||
onLoadingStart={handleLiveLoadingStart}
|
onLoadingStart={handleLiveLoadingStart}
|
||||||
onReady={handleLiveReady}
|
onReady={handleLiveReady}
|
||||||
|
onUnavailable={handleLiveUnavailable}
|
||||||
onVolumeChange={handleLiveVolumeChange}
|
onVolumeChange={handleLiveVolumeChange}
|
||||||
className="w-full h-full object-contain object-center relative z-0"
|
className="w-full h-full object-contain object-center relative z-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{livePreparing || !liveReady ? (
|
{!liveUnavailable && (livePreparing || !liveReady) ? (
|
||||||
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/85 text-white">
|
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/85 text-white">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
|
|||||||
@ -67,6 +67,7 @@ type TrainingJobStatus = {
|
|||||||
stage?: string
|
stage?: string
|
||||||
epoch?: number
|
epoch?: number
|
||||||
epochs?: number
|
epochs?: number
|
||||||
|
previewUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingPrediction = {
|
type TrainingPrediction = {
|
||||||
@ -2032,6 +2033,7 @@ function annotationToCorrectionState(item: TrainingAnnotation): CorrectionState
|
|||||||
const FEEDBACK_PAGE_SIZE = 10
|
const FEEDBACK_PAGE_SIZE = 10
|
||||||
|
|
||||||
const TRAINING_INFO_DISMISSED_STORAGE_KEY = 'training:info-dismissed-key'
|
const TRAINING_INFO_DISMISSED_STORAGE_KEY = 'training:info-dismissed-key'
|
||||||
|
const TRAINING_IMAGE_EXPANDED_STORAGE_KEY = 'training:image-expanded'
|
||||||
|
|
||||||
export default function TrainingTab(props: {
|
export default function TrainingTab(props: {
|
||||||
onTrainingRunningChange?: (running: boolean) => void
|
onTrainingRunningChange?: (running: boolean) => void
|
||||||
@ -2095,7 +2097,15 @@ export default function TrainingTab(props: {
|
|||||||
const finishingGestureRef = useRef(false)
|
const finishingGestureRef = useRef(false)
|
||||||
|
|
||||||
const [frameImageLoaded, setFrameImageLoaded] = useState(false)
|
const [frameImageLoaded, setFrameImageLoaded] = useState(false)
|
||||||
const [imageExpanded, setImageExpanded] = useState(false)
|
const [imageExpanded, setImageExpanded] = useState(() => {
|
||||||
|
if (typeof window === 'undefined') return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(TRAINING_IMAGE_EXPANDED_STORAGE_KEY) === '1'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const [frameNaturalSize, setFrameNaturalSize] = useState<{
|
const [frameNaturalSize, setFrameNaturalSize] = useState<{
|
||||||
width: number
|
width: number
|
||||||
@ -2706,6 +2716,7 @@ export default function TrainingTab(props: {
|
|||||||
stage: job.stage,
|
stage: job.stage,
|
||||||
epoch: Number(job.epoch ?? 0),
|
epoch: Number(job.epoch ?? 0),
|
||||||
epochs: Number(job.epochs ?? 0),
|
epochs: Number(job.epochs ?? 0),
|
||||||
|
previewUrl: String(job.previewUrl ?? ''),
|
||||||
}
|
}
|
||||||
: prev?.training,
|
: prev?.training,
|
||||||
}))
|
}))
|
||||||
@ -3271,6 +3282,17 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
}, [imageExpanded, props.onImageExpandedChange])
|
}, [imageExpanded, props.onImageExpandedChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
TRAINING_IMAGE_EXPANDED_STORAGE_KEY,
|
||||||
|
imageExpanded ? '1' : '0'
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [imageExpanded])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!trainingRunning) {
|
if (!trainingRunning) {
|
||||||
etaSmoothingRef.current = {
|
etaSmoothingRef.current = {
|
||||||
@ -5780,7 +5802,7 @@ export default function TrainingTab(props: {
|
|||||||
visible={stageOverlayIsVisible}
|
visible={stageOverlayIsVisible}
|
||||||
backgroundUrl={
|
backgroundUrl={
|
||||||
stageOverlayMode === 'training'
|
stageOverlayMode === 'training'
|
||||||
? imageSrc
|
? trainingStatus?.training?.previewUrl || imageSrc
|
||||||
: loadingPreviewBackgroundUrl
|
: loadingPreviewBackgroundUrl
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user