bugfixes
This commit is contained in:
parent
19faed7a40
commit
34df62aa13
@ -5,7 +5,6 @@
|
|||||||
],
|
],
|
||||||
"sexPositions": [
|
"sexPositions": [
|
||||||
"unknown",
|
"unknown",
|
||||||
"solo",
|
|
||||||
"missionary",
|
"missionary",
|
||||||
"doggy",
|
"doggy",
|
||||||
"cowgirl",
|
"cowgirl",
|
||||||
@ -33,13 +32,11 @@
|
|||||||
"pussy"
|
"pussy"
|
||||||
],
|
],
|
||||||
"objects": [
|
"objects": [
|
||||||
"bath",
|
|
||||||
"blindfold",
|
"blindfold",
|
||||||
"buttplug",
|
"buttplug",
|
||||||
"collar",
|
"collar",
|
||||||
"dildo",
|
"dildo",
|
||||||
"handcuffs",
|
"handcuffs",
|
||||||
"rope",
|
|
||||||
"shower",
|
"shower",
|
||||||
"strapon",
|
"strapon",
|
||||||
"towel",
|
"towel",
|
||||||
@ -50,11 +47,11 @@
|
|||||||
"bra",
|
"bra",
|
||||||
"dress",
|
"dress",
|
||||||
"fishnet",
|
"fishnet",
|
||||||
|
"harness",
|
||||||
"heels",
|
"heels",
|
||||||
"hotpants",
|
"hotpants",
|
||||||
"lingerie",
|
"lingerie",
|
||||||
"panties",
|
"panties",
|
||||||
"shirt",
|
|
||||||
"skirt",
|
"skirt",
|
||||||
"stockings",
|
"stockings",
|
||||||
"croptop"
|
"croptop"
|
||||||
|
|||||||
@ -405,28 +405,6 @@ func appendFileAndRemove(dstPath, srcPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupHLSRetryChunks(outFile string) {
|
|
||||||
dir := filepath.Dir(outFile)
|
|
||||||
ext := filepath.Ext(outFile)
|
|
||||||
base := strings.TrimSuffix(filepath.Base(outFile), ext)
|
|
||||||
|
|
||||||
canonical := canonicalAssetIDFromName(base)
|
|
||||||
if canonical == "" {
|
|
||||||
canonical = base
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern := filepath.Join(dir, fmt.Sprintf(".%s.retry.*%s", canonical, ext))
|
|
||||||
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range matches {
|
|
||||||
_ = removeWithRetry(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleM3U8Mode(
|
func handleM3U8Mode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
stream *selectedHLSStream,
|
stream *selectedHLSStream,
|
||||||
@ -460,20 +438,15 @@ func handleM3U8Mode(
|
|||||||
return errors.New("output file path leer")
|
return errors.New("output file path leer")
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupHLSRetryChunks(outFile)
|
|
||||||
defer cleanupHLSRetryChunks(outFile)
|
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(outFile))
|
ext := strings.ToLower(filepath.Ext(outFile))
|
||||||
if ext != ".ts" && ext != ".mp4" {
|
if ext != ".ts" && ext != ".mp4" {
|
||||||
return fmt.Errorf("nicht unterstützte output-endung: %q", ext)
|
return fmt.Errorf("nicht unterstützte output-endung: %q", ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wichtig: ffmpeg schreibt in temporäre Retry-Datei.
|
// Wichtig:
|
||||||
// Danach wird sie an outFile angehängt und gelöscht.
|
// handleM3U8Mode schreibt immer in den übergebenen outFile.
|
||||||
retryOut := hlsRetryChunkPath(outFile, 1)
|
// Wenn der Caller einen Retry-Chunk möchte, muss er bereits
|
||||||
defer func() {
|
// einen Retry-Pfad als outFile übergeben.
|
||||||
_ = removeWithRetry(retryOut)
|
|
||||||
}()
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-y",
|
"-y",
|
||||||
@ -534,12 +507,12 @@ func handleM3U8Mode(
|
|||||||
case ".ts":
|
case ".ts":
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-f", "mpegts",
|
"-f", "mpegts",
|
||||||
retryOut,
|
outFile,
|
||||||
)
|
)
|
||||||
case ".mp4":
|
case ".mp4":
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-movflags", "+faststart",
|
"-movflags", "+faststart",
|
||||||
retryOut,
|
outFile,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,9 +541,7 @@ func handleM3U8Mode(
|
|||||||
case <-stopStat:
|
case <-stopStat:
|
||||||
return
|
return
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
mainSize := safeFileSize(outFile)
|
sz := safeFileSize(outFile)
|
||||||
retrySize := safeFileSize(retryOut)
|
|
||||||
sz := mainSize + retrySize
|
|
||||||
|
|
||||||
if sz > 0 && sz != last {
|
if sz > 0 && sz != last {
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
@ -596,10 +567,6 @@ func handleM3U8Mode(
|
|||||||
return fmt.Errorf("ffmpeg m3u8 failed: %w", err)
|
return fmt.Errorf("ffmpeg m3u8 failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := appendFileAndRemove(outFile, retryOut); err != nil {
|
|
||||||
return fmt.Errorf("retry chunk übernehmen fehlgeschlagen: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if job != nil {
|
if job != nil {
|
||||||
if fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() {
|
if fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() {
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
|
|||||||
@ -171,7 +171,11 @@ func publishTaskState() {
|
|||||||
TS: time.Now().UnixMilli(),
|
TS: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := json.Marshal(ev)
|
b, err := json.Marshal(ev)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
publishSSE("taskState", b)
|
publishSSE("taskState", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -127,6 +127,11 @@ type TrainingJobStatus struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
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"`
|
||||||
|
|
||||||
|
Stage string `json:"stage,omitempty"`
|
||||||
|
Epoch int `json:"epoch,omitempty"`
|
||||||
|
Epochs int `json:"epochs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingConfidence struct {
|
type TrainingConfidence struct {
|
||||||
@ -209,12 +214,38 @@ func trainingHandleProgressLine(line string, start int, end int, defaultStep str
|
|||||||
if progress > s.Progress {
|
if progress > s.Progress {
|
||||||
s.Progress = progress
|
s.Progress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Step = step
|
s.Step = step
|
||||||
|
|
||||||
|
if strings.TrimSpace(ev.Stage) != "" {
|
||||||
|
s.Stage = strings.TrimSpace(ev.Stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.Epoch > 0 {
|
||||||
|
s.Epoch = ev.Epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
if ev.Epochs > 0 {
|
||||||
|
s.Epochs = ev.Epochs
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trainingPublishJobStatus(status TrainingJobStatus) {
|
||||||
|
b, err := json.Marshal(map[string]any{
|
||||||
|
"type": "training_status",
|
||||||
|
"training": status,
|
||||||
|
"ts": time.Now().UnixMilli(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSSE("training", b)
|
||||||
|
}
|
||||||
|
|
||||||
func trainingRunCommandStreaming(
|
func trainingRunCommandStreaming(
|
||||||
python string,
|
python string,
|
||||||
script string,
|
script string,
|
||||||
@ -306,8 +337,11 @@ var trainingJob = struct {
|
|||||||
|
|
||||||
func trainingSetJobStatus(update func(*TrainingJobStatus)) {
|
func trainingSetJobStatus(update func(*TrainingJobStatus)) {
|
||||||
trainingJob.mu.Lock()
|
trainingJob.mu.Lock()
|
||||||
defer trainingJob.mu.Unlock()
|
|
||||||
update(&trainingJob.status)
|
update(&trainingJob.status)
|
||||||
|
snapshot := trainingJob.status
|
||||||
|
trainingJob.mu.Unlock()
|
||||||
|
|
||||||
|
trainingPublishJobStatus(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func trainingGetJobStatus() TrainingJobStatus {
|
func trainingGetJobStatus() TrainingJobStatus {
|
||||||
@ -901,12 +935,23 @@ func trainingRunJob(root string, count int) {
|
|||||||
errorText := strings.Join(errorParts, " ")
|
errorText := strings.Join(errorParts, " ")
|
||||||
|
|
||||||
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
||||||
|
finishedAt := time.Now().UTC()
|
||||||
|
|
||||||
|
var durationMs int64
|
||||||
|
if startedAt, err := time.Parse(time.RFC3339, strings.TrimSpace(s.StartedAt)); err == nil {
|
||||||
|
durationMs = finishedAt.Sub(startedAt).Milliseconds()
|
||||||
|
if durationMs < 0 {
|
||||||
|
durationMs = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.Running = false
|
s.Running = false
|
||||||
s.Progress = 100
|
s.Progress = 100
|
||||||
s.Step = "Training abgeschlossen."
|
s.Step = "Training abgeschlossen."
|
||||||
s.Message = message
|
s.Message = message
|
||||||
s.Error = errorText
|
s.Error = errorText
|
||||||
s.FinishedAt = time.Now().UTC().Format(time.RFC3339)
|
s.FinishedAt = finishedAt.Format(time.RFC3339)
|
||||||
|
s.DurationMs = durationMs
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1528,6 +1573,10 @@ func trainingDeleteAllHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trainingSetJobStatus(func(s *TrainingJobStatus) {
|
||||||
|
*s = TrainingJobStatus{}
|
||||||
|
})
|
||||||
|
|
||||||
trainingWriteJSON(w, http.StatusOK, map[string]any{
|
trainingWriteJSON(w, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"message": "Alle Trainingsdaten wurden gelöscht.",
|
"message": "Alle Trainingsdaten wurden gelöscht.",
|
||||||
|
|||||||
@ -121,6 +121,7 @@ type TaskStateEvent = {
|
|||||||
type?: 'task_state'
|
type?: 'task_state'
|
||||||
generateAssetsRunning?: boolean
|
generateAssetsRunning?: boolean
|
||||||
regenerateAssetsRunning?: boolean
|
regenerateAssetsRunning?: boolean
|
||||||
|
checkVideosRunning?: boolean
|
||||||
cleanupRunning?: boolean
|
cleanupRunning?: boolean
|
||||||
anyRunning?: boolean
|
anyRunning?: boolean
|
||||||
ts?: number
|
ts?: number
|
||||||
@ -1109,13 +1110,6 @@ export default function App() {
|
|||||||
|
|
||||||
void loadTrainingStatus()
|
void loadTrainingStatus()
|
||||||
|
|
||||||
const timer = window.setInterval(
|
|
||||||
() => {
|
|
||||||
void loadTrainingStatus()
|
|
||||||
},
|
|
||||||
trainingTabRunning ? 1500 : 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
const onVisibilityChange = () => {
|
const onVisibilityChange = () => {
|
||||||
if (!document.hidden) {
|
if (!document.hidden) {
|
||||||
void loadTrainingStatus()
|
void loadTrainingStatus()
|
||||||
@ -1126,10 +1120,9 @@ export default function App() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
window.clearInterval(timer)
|
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
}
|
}
|
||||||
}, [authed, trainingTabRunning])
|
}, [authed])
|
||||||
|
|
||||||
const loadPendingAutoStarts = useCallback(async (opts?: { force?: boolean }) => {
|
const loadPendingAutoStarts = useCallback(async (opts?: { force?: boolean }) => {
|
||||||
const force = Boolean(opts?.force)
|
const force = Boolean(opts?.force)
|
||||||
@ -2837,12 +2830,25 @@ export default function App() {
|
|||||||
msg?.anyRunning ||
|
msg?.anyRunning ||
|
||||||
msg?.generateAssetsRunning ||
|
msg?.generateAssetsRunning ||
|
||||||
msg?.regenerateAssetsRunning ||
|
msg?.regenerateAssetsRunning ||
|
||||||
|
msg?.checkVideosRunning ||
|
||||||
msg?.cleanupRunning
|
msg?.cleanupRunning
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onTraining = (ev: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(String(ev.data ?? 'null'))
|
||||||
|
|
||||||
|
if (data?.type !== 'training_status') return
|
||||||
|
|
||||||
|
setTrainingTabRunning(Boolean(data?.training?.running))
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void loadJobs()
|
void loadJobs()
|
||||||
void loadDoneCount()
|
void loadDoneCount()
|
||||||
void loadTaskStateOnce()
|
void loadTaskStateOnce()
|
||||||
@ -2880,6 +2886,7 @@ export default function App() {
|
|||||||
es.addEventListener('autostart', onAutostart as any)
|
es.addEventListener('autostart', onAutostart as any)
|
||||||
es.addEventListener('finishedPostwork', onFinishedPostwork as any)
|
es.addEventListener('finishedPostwork', onFinishedPostwork as any)
|
||||||
es.addEventListener('taskState', onTaskState as any)
|
es.addEventListener('taskState', onTaskState as any)
|
||||||
|
es.addEventListener('training', onTraining as any)
|
||||||
|
|
||||||
const onVis = () => {
|
const onVis = () => {
|
||||||
if (document.hidden) return
|
if (document.hidden) return
|
||||||
@ -2904,6 +2911,7 @@ export default function App() {
|
|||||||
es.removeEventListener('autostart', onAutostart as any)
|
es.removeEventListener('autostart', onAutostart as any)
|
||||||
es.removeEventListener('finishedPostwork', onFinishedPostwork as any)
|
es.removeEventListener('finishedPostwork', onFinishedPostwork as any)
|
||||||
es.removeEventListener('taskState', onTaskState as any)
|
es.removeEventListener('taskState', onTaskState as any)
|
||||||
|
es.removeEventListener('training', onTraining as any)
|
||||||
|
|
||||||
const handler = onModelJobEventRef.current
|
const handler = onModelJobEventRef.current
|
||||||
if (handler) {
|
if (handler) {
|
||||||
|
|||||||
@ -512,6 +512,39 @@ export function DressIcon(props: IconProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HarnessIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconBase {...props}>
|
||||||
|
{/* Hals-/Brustband */}
|
||||||
|
<path d="M8.2 5.2C9.3 4.5 10.6 4.1 12 4.1C13.4 4.1 14.7 4.5 15.8 5.2" />
|
||||||
|
<path d="M8.6 5.4L7.2 9.2" />
|
||||||
|
<path d="M15.4 5.4L16.8 9.2" />
|
||||||
|
|
||||||
|
{/* Schulter-/Brustgurte */}
|
||||||
|
<path d="M7.2 9.2C8.5 8.4 10.1 8 12 8C13.9 8 15.5 8.4 16.8 9.2" />
|
||||||
|
<path d="M7.2 9.2L5.8 15.2" />
|
||||||
|
<path d="M16.8 9.2L18.2 15.2" />
|
||||||
|
|
||||||
|
{/* Mittelgurt */}
|
||||||
|
<path d="M12 8V19.8" />
|
||||||
|
|
||||||
|
{/* Taillen-/Hüftband */}
|
||||||
|
<path d="M5.8 15.2C7.5 16.2 9.6 16.7 12 16.7C14.4 16.7 16.5 16.2 18.2 15.2" />
|
||||||
|
<path d="M7.2 19.8H16.8" />
|
||||||
|
|
||||||
|
{/* untere Verbindungsgurte */}
|
||||||
|
<path d="M5.8 15.2L7.2 19.8" />
|
||||||
|
<path d="M18.2 15.2L16.8 19.8" />
|
||||||
|
|
||||||
|
{/* Ringe / Metallösen */}
|
||||||
|
<circle cx="12" cy="8" r="0.9" />
|
||||||
|
<circle cx="12" cy="16.7" r="0.9" />
|
||||||
|
<circle cx="7.2" cy="9.2" r="0.7" />
|
||||||
|
<circle cx="16.8" cy="9.2" r="0.7" />
|
||||||
|
</IconBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function ShoesIcon(props: IconProps) {
|
export function ShoesIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<IconBase {...props}>
|
<IconBase {...props}>
|
||||||
@ -663,22 +696,6 @@ export function TongueIcon(props: IconProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BathIcon(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<IconBase {...props}>
|
|
||||||
<path d="M5 11.5H20" />
|
|
||||||
<path d="M6 11.5V14.5C6 17.3 8.2 19.5 11 19.5H14C16.8 19.5 19 17.3 19 14.5V11.5" />
|
|
||||||
<path d="M8 19.5L7 21" />
|
|
||||||
<path d="M17 19.5L18 21" />
|
|
||||||
<path d="M5 11.5V7.5C5 5.6 6.4 4.2 8.2 4.2C9.8 4.2 11 5.4 11 7" />
|
|
||||||
<path d="M9.8 7H12.2" />
|
|
||||||
<circle cx="9" cy="9" r="0.5" fill="currentColor" stroke="none" />
|
|
||||||
<circle cx="12" cy="9.5" r="0.5" fill="currentColor" stroke="none" />
|
|
||||||
<circle cx="15" cy="9" r="0.5" fill="currentColor" stroke="none" />
|
|
||||||
</IconBase>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShowerIcon(props: IconProps) {
|
export function ShowerIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<IconBase {...props}>
|
<IconBase {...props}>
|
||||||
@ -883,11 +900,6 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
|
|||||||
text: 'Fingering',
|
text: 'Fingering',
|
||||||
icon: FingeringIcon,
|
icon: FingeringIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
match: ['solo', 'masturbation'],
|
|
||||||
text: 'Solo',
|
|
||||||
icon: HandIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
match: ['standing_doggy', 'standing-doggy', 'standing doggy'],
|
match: ['standing_doggy', 'standing-doggy', 'standing doggy'],
|
||||||
text: 'Standing Doggy',
|
text: 'Standing Doggy',
|
||||||
@ -983,11 +995,6 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Objects
|
// Objects
|
||||||
{
|
|
||||||
match: ['bath', 'bathtub', 'tub'],
|
|
||||||
text: 'Badewanne',
|
|
||||||
icon: BathIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
match: ['blindfold'],
|
match: ['blindfold'],
|
||||||
text: 'Augenbinde',
|
text: 'Augenbinde',
|
||||||
@ -1013,11 +1020,6 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
|
|||||||
text: 'Handschellen',
|
text: 'Handschellen',
|
||||||
icon: RopeIcon,
|
icon: RopeIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
match: ['rope', 'tie', 'restraint', 'restraints'],
|
|
||||||
text: 'Seil',
|
|
||||||
icon: RopeIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
match: ['shower'],
|
match: ['shower'],
|
||||||
text: 'Dusche',
|
text: 'Dusche',
|
||||||
@ -1095,6 +1097,11 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
|
|||||||
text: 'Fishnet',
|
text: 'Fishnet',
|
||||||
icon: FishnetIcon,
|
icon: FishnetIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: ['harness', 'body_harness', 'body-harness'],
|
||||||
|
text: 'Harness',
|
||||||
|
icon: HarnessIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
match: ['heels', 'heel', 'high_heels', 'high-heels'],
|
match: ['heels', 'heel', 'high_heels', 'high-heels'],
|
||||||
text: 'High Heels',
|
text: 'High Heels',
|
||||||
@ -1115,11 +1122,6 @@ const SEGMENT_LABEL_META: SegmentLabelMeta[] = [
|
|||||||
text: 'Panties',
|
text: 'Panties',
|
||||||
icon: UnderwearIcon,
|
icon: UnderwearIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
match: ['shirt', 'tshirt', 't-shirt', 'blouse', 'hoodie', 'sweater', 'jacket', 'coat'],
|
|
||||||
text: 'Shirt',
|
|
||||||
icon: ClothingIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
match: ['skirt'],
|
match: ['skirt'],
|
||||||
text: 'Rock',
|
text: 'Rock',
|
||||||
@ -1262,10 +1264,6 @@ function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null {
|
|||||||
return { match: [], text: 'Mund', icon: MouthIcon }
|
return { match: [], text: 'Mund', icon: MouthIcon }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.includes('bath') || normalized.includes('tub')) {
|
|
||||||
return { match: [], text: 'Badewanne', icon: BathIcon }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized.includes('shower')) {
|
if (normalized.includes('shower')) {
|
||||||
return { match: [], text: 'Dusche', icon: ShowerIcon }
|
return { match: [], text: 'Dusche', icon: ShowerIcon }
|
||||||
}
|
}
|
||||||
@ -1302,10 +1300,6 @@ function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null {
|
|||||||
return { match: [], text: 'Handschellen', icon: RopeIcon }
|
return { match: [], text: 'Handschellen', icon: RopeIcon }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.includes('rope') || normalized.includes('restraint') || normalized.includes('tie')) {
|
|
||||||
return { match: [], text: 'Seil', icon: RopeIcon }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized.includes('towel')) {
|
if (normalized.includes('towel')) {
|
||||||
return { match: [], text: 'Handtuch', icon: TowelIcon }
|
return { match: [], text: 'Handtuch', icon: TowelIcon }
|
||||||
}
|
}
|
||||||
@ -1362,17 +1356,17 @@ function findSegmentLabelMeta(label?: string): SegmentLabelMeta | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
normalized.includes('shirt') ||
|
normalized.includes('harness') ||
|
||||||
normalized.includes('tshirt') ||
|
normalized.includes('clothing') ||
|
||||||
normalized.includes('t-shirt') ||
|
normalized.includes('clothes') ||
|
||||||
normalized.includes('blouse') ||
|
normalized.includes('garment') ||
|
||||||
normalized.includes('hoodie') ||
|
normalized.includes('outfit')
|
||||||
normalized.includes('sweater') ||
|
|
||||||
normalized.includes('jacket') ||
|
|
||||||
normalized.includes('coat') ||
|
|
||||||
normalized.includes('clothing')
|
|
||||||
) {
|
) {
|
||||||
return { match: [], text: 'Oberteil', icon: ClothingIcon }
|
return {
|
||||||
|
match: [],
|
||||||
|
text: normalized.includes('harness') ? 'Harness' : 'Kleidung',
|
||||||
|
icon: HarnessIcon,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -1446,14 +1440,12 @@ function prettifyUnknownLabel(label?: string): string {
|
|||||||
.replace(/\bpussy\b/g, 'Vagina')
|
.replace(/\bpussy\b/g, 'Vagina')
|
||||||
.replace(/\bpenis\b/g, 'Penis')
|
.replace(/\bpenis\b/g, 'Penis')
|
||||||
.replace(/\btongue\b/g, 'Zunge')
|
.replace(/\btongue\b/g, 'Zunge')
|
||||||
.replace(/\bbath\b/g, 'Badewanne')
|
|
||||||
.replace(/\bblindfold\b/g, 'Augenbinde')
|
.replace(/\bblindfold\b/g, 'Augenbinde')
|
||||||
.replace(/\bbuttplug\b/g, 'Buttplug')
|
.replace(/\bbuttplug\b/g, 'Buttplug')
|
||||||
.replace(/\bbutt plug\b/g, 'Buttplug')
|
.replace(/\bbutt plug\b/g, 'Buttplug')
|
||||||
.replace(/\bcollar\b/g, 'Halsband')
|
.replace(/\bcollar\b/g, 'Halsband')
|
||||||
.replace(/\bdildo\b/g, 'Dildo')
|
.replace(/\bdildo\b/g, 'Dildo')
|
||||||
.replace(/\bhandcuffs\b/g, 'Handschellen')
|
.replace(/\bhandcuffs\b/g, 'Handschellen')
|
||||||
.replace(/\brope\b/g, 'Seil')
|
|
||||||
.replace(/\bshower\b/g, 'Dusche')
|
.replace(/\bshower\b/g, 'Dusche')
|
||||||
.replace(/\bstrapon\b/g, 'Strap-on')
|
.replace(/\bstrapon\b/g, 'Strap-on')
|
||||||
.replace(/\bstrap on\b/g, 'Strap-on')
|
.replace(/\bstrap on\b/g, 'Strap-on')
|
||||||
@ -1463,12 +1455,12 @@ function prettifyUnknownLabel(label?: string): string {
|
|||||||
.replace(/\bbra\b/g, 'BH')
|
.replace(/\bbra\b/g, 'BH')
|
||||||
.replace(/\bdress\b/g, 'Kleid')
|
.replace(/\bdress\b/g, 'Kleid')
|
||||||
.replace(/\bfishnet\b/g, 'Fishnet')
|
.replace(/\bfishnet\b/g, 'Fishnet')
|
||||||
|
.replace(/\bharness\b/g, 'Harness')
|
||||||
.replace(/\bheels\b/g, 'High Heels')
|
.replace(/\bheels\b/g, 'High Heels')
|
||||||
.replace(/\bheel\b/g, 'High Heel')
|
.replace(/\bheel\b/g, 'High Heel')
|
||||||
.replace(/\bhotpants\b/g, 'Hotpants')
|
.replace(/\bhotpants\b/g, 'Hotpants')
|
||||||
.replace(/\blingerie\b/g, 'Lingerie')
|
.replace(/\blingerie\b/g, 'Lingerie')
|
||||||
.replace(/\bpanties\b/g, 'Panties')
|
.replace(/\bpanties\b/g, 'Panties')
|
||||||
.replace(/\bshirt\b/g, 'Shirt')
|
|
||||||
.replace(/\bskirt\b/g, 'Rock')
|
.replace(/\bskirt\b/g, 'Rock')
|
||||||
.replace(/\bstockings\b/g, 'Stockings')
|
.replace(/\bstockings\b/g, 'Stockings')
|
||||||
.replace(/\bstocking\b/g, 'Stocking')
|
.replace(/\bstocking\b/g, 'Stocking')
|
||||||
|
|||||||
@ -22,6 +22,10 @@ type TrainingJobStatus = {
|
|||||||
error?: string
|
error?: string
|
||||||
startedAt?: string
|
startedAt?: string
|
||||||
finishedAt?: string
|
finishedAt?: string
|
||||||
|
durationMs?: number
|
||||||
|
stage?: string
|
||||||
|
epoch?: number
|
||||||
|
epochs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrainingStatus = {
|
type TrainingStatus = {
|
||||||
@ -148,6 +152,7 @@ type TrainingNotice = {
|
|||||||
title: string
|
title: string
|
||||||
message: string
|
message: string
|
||||||
detail?: string
|
detail?: string
|
||||||
|
progress?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function trainingNoticeClass(kind: TrainingNoticeKind) {
|
function trainingNoticeClass(kind: TrainingNoticeKind) {
|
||||||
@ -215,8 +220,14 @@ function TrainingNoticeCard(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-semibold">
|
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold">
|
||||||
{props.notice.title}
|
<span>{props.notice.title}</span>
|
||||||
|
|
||||||
|
{typeof props.notice.progress === 'number' ? (
|
||||||
|
<span className="rounded-full bg-black/5 px-2 py-0.5 text-[11px] font-bold ring-1 ring-black/10 dark:bg-white/10 dark:ring-white/10">
|
||||||
|
{Math.round(clampPercent(props.notice.progress))}%
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-0.5 break-words text-xs leading-snug">
|
<div className="mt-0.5 break-words text-xs leading-snug">
|
||||||
@ -266,6 +277,23 @@ function backendText(data: any, fallback: string) {
|
|||||||
).trim()
|
).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trainingDurationMs(job?: TrainingJobStatus | null) {
|
||||||
|
const direct = Number(job?.durationMs)
|
||||||
|
|
||||||
|
if (Number.isFinite(direct) && direct > 0) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = job?.startedAt ? Date.parse(job.startedAt) : NaN
|
||||||
|
const finished = job?.finishedAt ? Date.parse(job.finishedAt) : NaN
|
||||||
|
|
||||||
|
if (!Number.isFinite(started) || !Number.isFinite(finished)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0, finished - started)
|
||||||
|
}
|
||||||
|
|
||||||
function countPercent(count: number, total: number) {
|
function countPercent(count: number, total: number) {
|
||||||
if (!Number.isFinite(count) || !Number.isFinite(total) || total <= 0) return '0%'
|
if (!Number.isFinite(count) || !Number.isFinite(total) || total <= 0) return '0%'
|
||||||
return `${Math.round((count / total) * 100)}%`
|
return `${Math.round((count / total) * 100)}%`
|
||||||
@ -1883,6 +1911,7 @@ export default function TrainingTab(props: {
|
|||||||
const [labels, setLabels] = useState<TrainingLabels>(emptyLabels)
|
const [labels, setLabels] = useState<TrainingLabels>(emptyLabels)
|
||||||
const [sample, setSample] = useState<TrainingSample | null>(null)
|
const [sample, setSample] = useState<TrainingSample | null>(null)
|
||||||
const [correction, setCorrection] = useState<CorrectionState>(() => predictionToCorrection(null))
|
const [correction, setCorrection] = useState<CorrectionState>(() => predictionToCorrection(null))
|
||||||
|
const [hasManualCorrection, setHasManualCorrection] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [analysisProgress, setAnalysisProgress] = useState(0)
|
const [analysisProgress, setAnalysisProgress] = useState(0)
|
||||||
const [analysisStep, setAnalysisStep] = useState('')
|
const [analysisStep, setAnalysisStep] = useState('')
|
||||||
@ -1899,12 +1928,32 @@ export default function TrainingTab(props: {
|
|||||||
const [trainingStatsLoading, setTrainingStatsLoading] = useState(false)
|
const [trainingStatsLoading, setTrainingStatsLoading] = useState(false)
|
||||||
const [trainingStatsError, setTrainingStatsError] = useState<string | null>(null)
|
const [trainingStatsError, setTrainingStatsError] = useState<string | null>(null)
|
||||||
const wasTrainingRunningRef = useRef(false)
|
const wasTrainingRunningRef = useRef(false)
|
||||||
|
const shownTrainingCompletionRef = useRef<string | null>(null)
|
||||||
|
|
||||||
const imageBoxRef = useRef<HTMLDivElement | null>(null)
|
const imageBoxRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const detectorBoxesScrollRef = useRef<HTMLDivElement | null>(null)
|
const detectorBoxesScrollRef = useRef<HTMLDivElement | null>(null)
|
||||||
const detectorBoxItemRefs = useRef<Array<HTMLDivElement | null>>([])
|
const detectorBoxItemRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||||
|
|
||||||
|
const epochTimingRef = useRef<{
|
||||||
|
lastEpoch: number
|
||||||
|
lastAt: number
|
||||||
|
}>({
|
||||||
|
lastEpoch: 0,
|
||||||
|
lastAt: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const etaSmoothingRef = useRef<{
|
||||||
|
lastAt: number
|
||||||
|
}>({
|
||||||
|
lastAt: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [trainingNowMs, setTrainingNowMs] = useState(() => Date.now())
|
||||||
|
const [smoothedTrainingEtaMs, setSmoothedTrainingEtaMs] = useState(0)
|
||||||
|
|
||||||
|
const [estimatedEpochMs, setEstimatedEpochMs] = useState(0)
|
||||||
|
|
||||||
const [drawingBox, setDrawingBox] = useState<TrainingBox | null>(null)
|
const [drawingBox, setDrawingBox] = useState<TrainingBox | null>(null)
|
||||||
const [boxInteraction, setBoxInteraction] = useState<BoxInteraction | null>(null)
|
const [boxInteraction, setBoxInteraction] = useState<BoxInteraction | null>(null)
|
||||||
const [touchMagnifier, setTouchMagnifier] = useState<MagnifierState | null>(null)
|
const [touchMagnifier, setTouchMagnifier] = useState<MagnifierState | null>(null)
|
||||||
@ -2024,6 +2073,67 @@ export default function TrainingTab(props: {
|
|||||||
if (data) setLabels(sortTrainingLabels(data))
|
if (data) setLabels(sortTrainingLabels(data))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const applyTrainingStatus = useCallback((data: any) => {
|
||||||
|
if (!data) return
|
||||||
|
|
||||||
|
const job = data.training || null
|
||||||
|
|
||||||
|
setTrainingStatus((prev) => ({
|
||||||
|
feedbackCount: Number(data.feedbackCount ?? prev?.feedbackCount ?? 0),
|
||||||
|
requiredCount: Number(data.requiredCount ?? prev?.requiredCount ?? 5),
|
||||||
|
canTrain: Boolean(data.canTrain ?? prev?.canTrain ?? false),
|
||||||
|
training: job
|
||||||
|
? {
|
||||||
|
running: Boolean(job.running),
|
||||||
|
progress: Number(job.progress ?? 0),
|
||||||
|
step: String(job.step ?? ''),
|
||||||
|
message: job.message,
|
||||||
|
error: job.error,
|
||||||
|
startedAt: job.startedAt,
|
||||||
|
finishedAt: job.finishedAt,
|
||||||
|
durationMs: Number(job.durationMs ?? 0),
|
||||||
|
stage: job.stage,
|
||||||
|
epoch: Number(job.epoch ?? 0),
|
||||||
|
epochs: Number(job.epochs ?? 0),
|
||||||
|
}
|
||||||
|
: prev?.training,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setTraining(Boolean(job?.running))
|
||||||
|
|
||||||
|
if (job?.message && !job.running) {
|
||||||
|
setTrainingProgress(100)
|
||||||
|
setTrainingStep('Training abgeschlossen.')
|
||||||
|
|
||||||
|
const finishedAt = String(job.finishedAt || '').trim()
|
||||||
|
const completionKey =
|
||||||
|
finishedAt || `${String(job.startedAt || '')}:${String(job.message || '')}`
|
||||||
|
|
||||||
|
if (completionKey && shownTrainingCompletionRef.current !== completionKey) {
|
||||||
|
shownTrainingCompletionRef.current = completionKey
|
||||||
|
|
||||||
|
const duration = trainingDurationMs(job)
|
||||||
|
const durationText = duration > 0
|
||||||
|
? ` Dauer: ${formatDuration(duration)}.`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
setMessage(`${String(job.message)}${durationText}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job?.error && !job.running) {
|
||||||
|
const finishedAt = String(job.finishedAt || '').trim()
|
||||||
|
const errorKey = finishedAt
|
||||||
|
? `${finishedAt}:error`
|
||||||
|
: `${String(job.startedAt || '')}:error`
|
||||||
|
|
||||||
|
if (errorKey && shownTrainingCompletionRef.current !== errorKey) {
|
||||||
|
shownTrainingCompletionRef.current = errorKey
|
||||||
|
setError(String(job.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const loadNext = useCallback(async (opts?: {
|
const loadNext = useCallback(async (opts?: {
|
||||||
forceNew?: boolean
|
forceNew?: boolean
|
||||||
refreshPrediction?: boolean
|
refreshPrediction?: boolean
|
||||||
@ -2046,6 +2156,8 @@ export default function TrainingTab(props: {
|
|||||||
if (value < 35) return Math.min(35, value + 4)
|
if (value < 35) return Math.min(35, value + 4)
|
||||||
if (value < 65) return Math.min(65, value + 2)
|
if (value < 65) return Math.min(65, value + 2)
|
||||||
if (value < 95) return Math.min(95, value + 1)
|
if (value < 95) return Math.min(95, value + 1)
|
||||||
|
|
||||||
|
setAnalysisStep('Analyse läuft noch… Ergebnis wird erwartet.')
|
||||||
return value
|
return value
|
||||||
})
|
})
|
||||||
}, 350)
|
}, 350)
|
||||||
@ -2086,6 +2198,7 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
setSample(data)
|
setSample(data)
|
||||||
setCorrection(nextCorrection)
|
setCorrection(nextCorrection)
|
||||||
|
setHasManualCorrection(false)
|
||||||
|
|
||||||
const currentLabels = labelsRef.current
|
const currentLabels = labelsRef.current
|
||||||
const personLabels = new Set(currentLabels.people)
|
const personLabels = new Set(currentLabels.people)
|
||||||
@ -2137,37 +2250,8 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
if (!res.ok || !data) return
|
if (!res.ok || !data) return
|
||||||
|
|
||||||
const job = data.training || null
|
applyTrainingStatus(data)
|
||||||
|
}, [applyTrainingStatus])
|
||||||
setTrainingStatus({
|
|
||||||
feedbackCount: Number(data.feedbackCount ?? 0),
|
|
||||||
requiredCount: Number(data.requiredCount ?? 5),
|
|
||||||
canTrain: Boolean(data.canTrain),
|
|
||||||
training: job
|
|
||||||
? {
|
|
||||||
running: Boolean(job.running),
|
|
||||||
progress: Number(job.progress ?? 0),
|
|
||||||
step: String(job.step ?? ''),
|
|
||||||
message: job.message,
|
|
||||||
error: job.error,
|
|
||||||
startedAt: job.startedAt,
|
|
||||||
finishedAt: job.finishedAt,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
setTraining(Boolean(job?.running))
|
|
||||||
|
|
||||||
if (job?.message && !job.running) {
|
|
||||||
setTrainingProgress(100)
|
|
||||||
setTrainingStep('Training abgeschlossen.')
|
|
||||||
setMessage((prev) => prev || String(job.message))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (job?.error && !job.running) {
|
|
||||||
setError((prev) => prev || String(job.error))
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const loadTrainingStats = useCallback(async () => {
|
const loadTrainingStats = useCallback(async () => {
|
||||||
setTrainingStatsLoading(true)
|
setTrainingStatsLoading(true)
|
||||||
@ -2204,6 +2288,35 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const es = new EventSource('/api/events/stream')
|
||||||
|
|
||||||
|
const onTraining = (ev: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(String(ev.data ?? 'null'))
|
||||||
|
|
||||||
|
if (data?.type !== 'training_status') return
|
||||||
|
|
||||||
|
applyTrainingStatus({
|
||||||
|
training: data.training,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
es.addEventListener('training', onTraining as EventListener)
|
||||||
|
|
||||||
|
es.onerror = () => {
|
||||||
|
// Optional: Polling-Fallback bleibt separat bestehen.
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
es.removeEventListener('training', onTraining as EventListener)
|
||||||
|
es.close()
|
||||||
|
}
|
||||||
|
}, [applyTrainingStatus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!statsModalOpen) return
|
if (!statsModalOpen) return
|
||||||
|
|
||||||
@ -2216,6 +2329,25 @@ export default function TrainingTab(props: {
|
|||||||
onTrainingRunningChange?.(trainingRunning)
|
onTrainingRunningChange?.(trainingRunning)
|
||||||
}, [trainingRunning, onTrainingRunningChange])
|
}, [trainingRunning, onTrainingRunningChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!trainingRunning) {
|
||||||
|
etaSmoothingRef.current = {
|
||||||
|
lastAt: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmoothedTrainingEtaMs(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrainingNowMs(Date.now())
|
||||||
|
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
setTrainingNowMs(Date.now())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [trainingRunning])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!boxLabel) return
|
if (!boxLabel) return
|
||||||
|
|
||||||
@ -2287,14 +2419,6 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
}, [loadLabels, loadNext, loadTrainingStatus])
|
}, [loadLabels, loadNext, loadTrainingStatus])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
void loadTrainingStatus()
|
|
||||||
}, trainingRunning ? 1500 : 5000)
|
|
||||||
|
|
||||||
return () => window.clearInterval(timer)
|
|
||||||
}, [loadTrainingStatus, trainingRunning])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!trainingRunning) return
|
if (!trainingRunning) return
|
||||||
|
|
||||||
@ -2332,6 +2456,57 @@ export default function TrainingTab(props: {
|
|||||||
trainingStatus?.training?.step,
|
trainingStatus?.training?.step,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const job = trainingStatus?.training
|
||||||
|
const epoch = Number(job?.epoch ?? 0)
|
||||||
|
const epochs = Number(job?.epochs ?? 0)
|
||||||
|
|
||||||
|
if (!trainingRunning || epoch <= 0 || epochs <= 0) {
|
||||||
|
epochTimingRef.current = {
|
||||||
|
lastEpoch: 0,
|
||||||
|
lastAt: 0,
|
||||||
|
}
|
||||||
|
setEstimatedEpochMs(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const previous = epochTimingRef.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous.lastEpoch > 0 &&
|
||||||
|
previous.lastAt > 0 &&
|
||||||
|
epoch > previous.lastEpoch
|
||||||
|
) {
|
||||||
|
const measuredMsPerEpoch = (now - previous.lastAt) / (epoch - previous.lastEpoch)
|
||||||
|
|
||||||
|
if (Number.isFinite(measuredMsPerEpoch) && measuredMsPerEpoch > 0) {
|
||||||
|
setEstimatedEpochMs((old) => {
|
||||||
|
if (!Number.isFinite(old) || old <= 0) return measuredMsPerEpoch
|
||||||
|
|
||||||
|
const clampedMeasured = Math.max(
|
||||||
|
old * 0.5,
|
||||||
|
Math.min(old * 2, measuredMsPerEpoch)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stärker glätten, damit Restzeit/Ø-Zeit nicht springt.
|
||||||
|
return old * 0.85 + clampedMeasured * 0.15
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (epoch !== previous.lastEpoch) {
|
||||||
|
epochTimingRef.current = {
|
||||||
|
lastEpoch: epoch,
|
||||||
|
lastAt: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
trainingRunning,
|
||||||
|
trainingStatus?.training?.epoch,
|
||||||
|
trainingStatus?.training?.epochs,
|
||||||
|
])
|
||||||
|
|
||||||
const saveFeedback = useCallback(
|
const saveFeedback = useCallback(
|
||||||
async (accepted: boolean) => {
|
async (accepted: boolean) => {
|
||||||
if (!sample) return
|
if (!sample) return
|
||||||
@ -2390,6 +2565,8 @@ export default function TrainingTab(props: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const startTraining = useCallback(async () => {
|
const startTraining = useCallback(async () => {
|
||||||
|
shownTrainingCompletionRef.current = null
|
||||||
|
|
||||||
setTraining(true)
|
setTraining(true)
|
||||||
setTrainingProgress(5)
|
setTrainingProgress(5)
|
||||||
setTrainingStep('Training wird gestartet…')
|
setTrainingStep('Training wird gestartet…')
|
||||||
@ -2407,8 +2584,6 @@ export default function TrainingTab(props: {
|
|||||||
throw new Error(backendText(data, `HTTP ${res.status}`))
|
throw new Error(backendText(data, `HTTP ${res.status}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessage(backendText(data, 'Training wurde gestartet.'))
|
|
||||||
|
|
||||||
await loadTrainingStatus()
|
await loadTrainingStatus()
|
||||||
|
|
||||||
// WICHTIG:
|
// WICHTIG:
|
||||||
@ -2585,10 +2760,16 @@ export default function TrainingTab(props: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const correctedNextBox = boxGeometryChanged(original, nextBox)
|
const geometryChanged = boxGeometryChanged(original, nextBox)
|
||||||
|
|
||||||
|
const correctedNextBox = geometryChanged
|
||||||
? markBoxCorrected(nextBox)
|
? markBoxCorrected(nextBox)
|
||||||
: nextBox
|
: nextBox
|
||||||
|
|
||||||
|
if (geometryChanged) {
|
||||||
|
setHasManualCorrection(true)
|
||||||
|
}
|
||||||
|
|
||||||
setCorrection((prev) => ({
|
setCorrection((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
boxes: (prev.boxes ?? []).map((box, index) =>
|
boxes: (prev.boxes ?? []).map((box, index) =>
|
||||||
@ -2636,6 +2817,8 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
if (box.w < 0.01 || box.h < 0.01) return
|
if (box.w < 0.01 || box.h < 0.01) return
|
||||||
|
|
||||||
|
setHasManualCorrection(true)
|
||||||
|
|
||||||
setCorrection((prev) => {
|
setCorrection((prev) => {
|
||||||
const next: CorrectionState = {
|
const next: CorrectionState = {
|
||||||
...prev,
|
...prev,
|
||||||
@ -2647,6 +2830,8 @@ export default function TrainingTab(props: {
|
|||||||
}, [boxInteraction, drawingBox])
|
}, [boxInteraction, drawingBox])
|
||||||
|
|
||||||
const removeBox = useCallback((index: number) => {
|
const removeBox = useCallback((index: number) => {
|
||||||
|
setHasManualCorrection(true)
|
||||||
|
|
||||||
let removedLabel = ''
|
let removedLabel = ''
|
||||||
let shouldClearBoxLabel = false
|
let shouldClearBoxLabel = false
|
||||||
|
|
||||||
@ -2678,12 +2863,21 @@ export default function TrainingTab(props: {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const changeBoxLabel = useCallback((index: number, nextLabel: string) => {
|
const changeBoxLabel = useCallback((index: number, nextLabel: string) => {
|
||||||
|
const currentLabel = String(correction.boxes?.[index]?.label || '').trim()
|
||||||
|
const cleanNextLabel = String(nextLabel || '').trim()
|
||||||
|
|
||||||
|
if (currentLabel !== cleanNextLabel) {
|
||||||
|
setHasManualCorrection(true)
|
||||||
|
}
|
||||||
|
|
||||||
setCorrection((prev) =>
|
setCorrection((prev) =>
|
||||||
changeBoxLabelInCorrection(prev, index, nextLabel, labelsRef.current)
|
changeBoxLabelInCorrection(prev, index, nextLabel, labelsRef.current)
|
||||||
)
|
)
|
||||||
}, [])
|
}, [correction.boxes])
|
||||||
|
|
||||||
const clearBoxes = useCallback(() => {
|
const clearBoxes = useCallback(() => {
|
||||||
|
setHasManualCorrection(true)
|
||||||
|
|
||||||
setBoxLabel('')
|
setBoxLabel('')
|
||||||
setActiveBoxIndex(null)
|
setActiveBoxIndex(null)
|
||||||
|
|
||||||
@ -2710,6 +2904,117 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
const showImageBoxes = !loading && !trainingRunning
|
const showImageBoxes = !loading && !trainingRunning
|
||||||
|
|
||||||
|
const shownTrainingDurationMs = useMemo(() => {
|
||||||
|
const job = trainingStatus?.training
|
||||||
|
|
||||||
|
if (!job) return 0
|
||||||
|
|
||||||
|
if (job.running && job.startedAt) {
|
||||||
|
const started = Date.parse(job.startedAt)
|
||||||
|
if (Number.isFinite(started)) {
|
||||||
|
return Math.max(0, trainingNowMs - started)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trainingDurationMs(job)
|
||||||
|
}, [trainingStatus?.training, trainingNowMs])
|
||||||
|
|
||||||
|
const rawTrainingEtaMs = useMemo(() => {
|
||||||
|
if (!trainingRunning) return 0
|
||||||
|
|
||||||
|
const job = trainingStatus?.training
|
||||||
|
const epoch = Number(job?.epoch ?? 0)
|
||||||
|
const epochs = Number(job?.epochs ?? 0)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Number.isFinite(epoch) &&
|
||||||
|
Number.isFinite(epochs) &&
|
||||||
|
Number.isFinite(estimatedEpochMs) &&
|
||||||
|
epoch > 0 &&
|
||||||
|
epochs > 0 &&
|
||||||
|
epoch < epochs &&
|
||||||
|
estimatedEpochMs > 0
|
||||||
|
) {
|
||||||
|
return Math.max(0, (epochs - epoch) * estimatedEpochMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = clampPercent(Number(shownTrainingProgress))
|
||||||
|
|
||||||
|
if (
|
||||||
|
shownTrainingDurationMs > 0 &&
|
||||||
|
progress >= 5 &&
|
||||||
|
progress < 99
|
||||||
|
) {
|
||||||
|
return Math.max(0, shownTrainingDurationMs * ((100 - progress) / progress))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}, [
|
||||||
|
trainingRunning,
|
||||||
|
trainingStatus?.training,
|
||||||
|
estimatedEpochMs,
|
||||||
|
shownTrainingDurationMs,
|
||||||
|
shownTrainingProgress,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!trainingRunning || rawTrainingEtaMs <= 0) {
|
||||||
|
etaSmoothingRef.current = {
|
||||||
|
lastAt: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmoothedTrainingEtaMs(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmoothedTrainingEtaMs((previous) => {
|
||||||
|
const lastAt = etaSmoothingRef.current.lastAt || trainingNowMs
|
||||||
|
const elapsed = Math.max(0, trainingNowMs - lastAt)
|
||||||
|
|
||||||
|
// Anzeige zählt zwischen Backend-Updates weiter runter.
|
||||||
|
const countedDown = previous > 0
|
||||||
|
? Math.max(0, previous - elapsed)
|
||||||
|
: rawTrainingEtaMs
|
||||||
|
|
||||||
|
const diff = rawTrainingEtaMs - countedDown
|
||||||
|
|
||||||
|
// Nach oben sehr vorsichtig glätten, nach unten etwas schneller.
|
||||||
|
const factor = diff > 0 ? 0.08 : 0.18
|
||||||
|
|
||||||
|
let next = countedDown + diff * factor
|
||||||
|
|
||||||
|
// Harte Sprünge zusätzlich begrenzen.
|
||||||
|
if (diff > 0) {
|
||||||
|
next = Math.min(next, countedDown + 10_000)
|
||||||
|
} else {
|
||||||
|
next = Math.max(next, countedDown - 20_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = Math.max(0, next)
|
||||||
|
|
||||||
|
etaSmoothingRef.current = {
|
||||||
|
lastAt: trainingNowMs,
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [trainingRunning, rawTrainingEtaMs, trainingNowMs])
|
||||||
|
|
||||||
|
const shownTrainingEtaMs = smoothedTrainingEtaMs
|
||||||
|
|
||||||
|
const shownTrainingEpochText = useMemo(() => {
|
||||||
|
const epoch = Number(trainingStatus?.training?.epoch ?? 0)
|
||||||
|
const epochs = Number(trainingStatus?.training?.epochs ?? 0)
|
||||||
|
|
||||||
|
if (!Number.isFinite(epoch) || !Number.isFinite(epochs)) return ''
|
||||||
|
if (epoch <= 0 || epochs <= 0) return ''
|
||||||
|
|
||||||
|
return `Epoche ${epoch}/${epochs}`
|
||||||
|
}, [
|
||||||
|
trainingStatus?.training?.epoch,
|
||||||
|
trainingStatus?.training?.epochs,
|
||||||
|
])
|
||||||
|
|
||||||
const activeNotice = useMemo<TrainingNotice | null>(() => {
|
const activeNotice = useMemo<TrainingNotice | null>(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return {
|
return {
|
||||||
@ -2732,15 +3037,47 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trainingRunning) {
|
if (trainingRunning) {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
if (shownTrainingDurationMs > 0) {
|
||||||
|
parts.push(`Laufzeit ${formatDuration(shownTrainingDurationMs)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shownTrainingEtaMs > 0) {
|
||||||
|
parts.push(`Restzeit ca. ${formatDuration(shownTrainingEtaMs)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shownTrainingEpochText) {
|
||||||
|
parts.push(shownTrainingEpochText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estimatedEpochMs > 0) {
|
||||||
|
parts.push(`Ø ${formatDuration(estimatedEpochMs)} pro Epoche`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'info',
|
kind: 'info',
|
||||||
title: 'Training läuft',
|
title: 'Trainingsprognose',
|
||||||
message: shownTrainingStep || 'Training läuft im Hintergrund.',
|
progress: shownTrainingProgress,
|
||||||
|
message: parts.length > 0
|
||||||
|
? parts.join(' · ')
|
||||||
|
: shownTrainingStep || 'Training läuft…',
|
||||||
|
detail: shownTrainingStep || undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}, [error, message, trainingRunning, shownTrainingStep])
|
}, [
|
||||||
|
error,
|
||||||
|
message,
|
||||||
|
trainingRunning,
|
||||||
|
shownTrainingStep,
|
||||||
|
shownTrainingDurationMs,
|
||||||
|
shownTrainingEtaMs,
|
||||||
|
shownTrainingEpochText,
|
||||||
|
estimatedEpochMs,
|
||||||
|
shownTrainingProgress,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!message) return
|
if (!message) return
|
||||||
@ -3455,13 +3792,15 @@ export default function TrainingTab(props: {
|
|||||||
<Button
|
<Button
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="emerald"
|
color="emerald"
|
||||||
disabled={uiLocked || !sample || !sample.prediction.modelAvailable}
|
disabled={uiLocked || !sample || !sample.prediction.modelAvailable || hasManualCorrection}
|
||||||
onClick={() => void saveFeedback(true)}
|
onClick={() => void saveFeedback(true)}
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
title={
|
title={
|
||||||
sample?.prediction.modelAvailable
|
hasManualCorrection
|
||||||
? 'Die Erkennung stimmt. Prediction wird als korrekt gespeichert.'
|
? 'Du hast Korrekturen vorgenommen. Bitte mit „Speichern & weiter“ speichern.'
|
||||||
: 'Erst verfügbar, wenn ein Modell trainiert wurde.'
|
: sample?.prediction.modelAvailable
|
||||||
|
? 'Die Erkennung stimmt. Prediction wird als korrekt gespeichert.'
|
||||||
|
: 'Erst verfügbar, wenn ein Modell trainiert wurde.'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Passt so & weiter
|
Passt so & weiter
|
||||||
@ -3469,10 +3808,14 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={uiLocked || !sample}
|
disabled={uiLocked || !sample || !hasManualCorrection}
|
||||||
onClick={() => void saveFeedback(false)}
|
onClick={() => void saveFeedback(false)}
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
title="Die Werte rechts werden als richtige Korrektur gespeichert."
|
title={
|
||||||
|
hasManualCorrection
|
||||||
|
? 'Die Werte rechts werden als richtige Korrektur gespeichert.'
|
||||||
|
: 'Erst verfügbar, wenn du eine Korrektur vorgenommen hast.'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Speichern & weiter
|
Speichern & weiter
|
||||||
</Button>
|
</Button>
|
||||||
@ -3553,10 +3896,16 @@ export default function TrainingTab(props: {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setCorrection((p) => ({
|
setCorrection((p) => {
|
||||||
...p,
|
if (p.sexPosition !== value) {
|
||||||
sexPosition: value,
|
setHasManualCorrection(true)
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
sexPosition: value,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
disabled={uiLocked}
|
disabled={uiLocked}
|
||||||
gridClassName="grid grid-cols-3 gap-2"
|
gridClassName="grid grid-cols-3 gap-2"
|
||||||
@ -3595,11 +3944,15 @@ export default function TrainingTab(props: {
|
|||||||
bodyParts: expanded,
|
bodyParts: expanded,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onToggle={(value) =>
|
onToggle={(value) =>
|
||||||
setCorrection((p) => ({
|
setCorrection((p) => {
|
||||||
...p,
|
setHasManualCorrection(true)
|
||||||
bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
|
|
||||||
}))
|
return {
|
||||||
|
...p,
|
||||||
|
bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={boxLabel}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
@ -3620,10 +3973,14 @@ export default function TrainingTab(props: {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onToggle={(value) =>
|
onToggle={(value) =>
|
||||||
setCorrection((p) => ({
|
setCorrection((p) => {
|
||||||
...p,
|
setHasManualCorrection(true)
|
||||||
objectsPresent: toggleArrayValue(p.objectsPresent, value),
|
|
||||||
}))
|
return {
|
||||||
|
...p,
|
||||||
|
objectsPresent: toggleArrayValue(p.objectsPresent, value),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={boxLabel}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
@ -3644,10 +4001,14 @@ export default function TrainingTab(props: {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onToggle={(value) =>
|
onToggle={(value) =>
|
||||||
setCorrection((p) => ({
|
setCorrection((p) => {
|
||||||
...p,
|
setHasManualCorrection(true)
|
||||||
clothingPresent: toggleArrayValue(p.clothingPresent, value),
|
|
||||||
}))
|
return {
|
||||||
|
...p,
|
||||||
|
clothingPresent: toggleArrayValue(p.clothingPresent, value),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
drawLabel={boxLabel}
|
drawLabel={boxLabel}
|
||||||
onDrawLabelChange={setBoxLabel}
|
onDrawLabelChange={setBoxLabel}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user