bugfixes
This commit is contained in:
parent
ab9f1f79b5
commit
5e6c841b26
BIN
backend/dist/nsfwapp-linux-amd64
vendored
BIN
backend/dist/nsfwapp-linux-amd64
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp.exe
vendored
BIN
backend/dist/nsfwapp.exe
vendored
Binary file not shown.
BIN
backend/dist/nsfwapp_amd64.deb
vendored
BIN
backend/dist/nsfwapp_amd64.deb
vendored
Binary file not shown.
@ -140,6 +140,8 @@ type importResult struct {
|
||||
type modelsOverviewRequest struct {
|
||||
Keys []string `json:"keys"`
|
||||
IncludeWatched bool `json:"includeWatched"`
|
||||
IncludeStats *bool `json:"includeStats,omitempty"`
|
||||
IncludeModels *bool `json:"includeModels,omitempty"`
|
||||
}
|
||||
|
||||
type modelsOverviewModel struct {
|
||||
@ -187,6 +189,13 @@ func normalizeModelKeySet(in []string) map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
func overviewBool(v *bool, fallback bool) bool {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func isOnlineRoomStatus(status string, isOnline bool) bool {
|
||||
s := strings.ToLower(strings.TrimSpace(status))
|
||||
if s == "offline" {
|
||||
@ -213,9 +222,15 @@ func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverview
|
||||
|
||||
keySet := normalizeModelKeySet(req.Keys)
|
||||
|
||||
includeStats := overviewBool(req.IncludeStats, true)
|
||||
includeModels := overviewBool(req.IncludeModels, true)
|
||||
|
||||
resp := modelsOverviewResponse{
|
||||
TotalCount: len(list),
|
||||
Models: make([]modelsOverviewModel, 0, len(keySet)+32),
|
||||
Models: make([]modelsOverviewModel, 0, len(keySet)+32),
|
||||
}
|
||||
|
||||
if includeStats {
|
||||
resp.TotalCount = len(list)
|
||||
}
|
||||
|
||||
for _, m := range list {
|
||||
@ -224,27 +239,30 @@ func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverview
|
||||
continue
|
||||
}
|
||||
|
||||
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
|
||||
if includeStats {
|
||||
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
|
||||
|
||||
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
|
||||
if online {
|
||||
resp.OnlineModelsCount++
|
||||
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
|
||||
if online {
|
||||
resp.OnlineModelsCount++
|
||||
}
|
||||
if m.Watching && online {
|
||||
resp.OnlineWatchedModelsCount++
|
||||
}
|
||||
}
|
||||
if m.Watching && online {
|
||||
resp.OnlineWatchedModelsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if m.Favorite && online {
|
||||
resp.OnlineFavCount++
|
||||
}
|
||||
if m.Liked != nil && *m.Liked && online {
|
||||
resp.OnlineLikedCount++
|
||||
if m.Favorite && online {
|
||||
resp.OnlineFavCount++
|
||||
}
|
||||
if m.Liked != nil && *m.Liked && online {
|
||||
resp.OnlineLikedCount++
|
||||
}
|
||||
}
|
||||
|
||||
needModel :=
|
||||
keySet[key] ||
|
||||
(req.IncludeWatched && m.Watching)
|
||||
includeModels &&
|
||||
(keySet[key] ||
|
||||
(req.IncludeWatched && m.Watching))
|
||||
|
||||
if needModel {
|
||||
resp.Models = append(resp.Models, m)
|
||||
@ -259,10 +277,17 @@ func loadModelsOverview(store *ModelStore, req modelsOverviewRequest) (modelsOve
|
||||
return modelsOverviewResponse{}, err
|
||||
}
|
||||
|
||||
onlineExpr := `(lower(trim(COALESCE(room_status,''))) <> 'offline' AND (COALESCE(is_online,false) = true OR trim(COALESCE(room_status,'')) <> ''))`
|
||||
includeStats := overviewBool(req.IncludeStats, true)
|
||||
includeModels := overviewBool(req.IncludeModels, true)
|
||||
|
||||
var totalCount, onlineCount, onlineWatchedCount, onlineFavCount, onlineLikedCount int64
|
||||
err := store.db.QueryRow(`
|
||||
var resp modelsOverviewResponse
|
||||
resp.Models = make([]modelsOverviewModel, 0, len(req.Keys)+32)
|
||||
|
||||
if includeStats {
|
||||
onlineExpr := `(lower(trim(COALESCE(room_status,''))) <> 'offline' AND (COALESCE(is_online,false) = true OR trim(COALESCE(room_status,'')) <> ''))`
|
||||
|
||||
var totalCount, onlineCount, onlineWatchedCount, onlineFavCount, onlineLikedCount int64
|
||||
err := store.db.QueryRow(`
|
||||
SELECT
|
||||
COUNT(*),
|
||||
COALESCE(SUM(CASE WHEN lower(COALESCE(host,'')) LIKE '%chaturbate%' AND `+onlineExpr+` THEN 1 ELSE 0 END), 0),
|
||||
@ -271,17 +296,19 @@ SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(liked,false) = true AND `+onlineExpr+` THEN 1 ELSE 0 END), 0)
|
||||
FROM models;
|
||||
`).Scan(&totalCount, &onlineCount, &onlineWatchedCount, &onlineFavCount, &onlineLikedCount)
|
||||
if err != nil {
|
||||
return modelsOverviewResponse{}, err
|
||||
if err != nil {
|
||||
return modelsOverviewResponse{}, err
|
||||
}
|
||||
|
||||
resp.TotalCount = int(totalCount)
|
||||
resp.OnlineModelsCount = int(onlineCount)
|
||||
resp.OnlineWatchedModelsCount = int(onlineWatchedCount)
|
||||
resp.OnlineFavCount = int(onlineFavCount)
|
||||
resp.OnlineLikedCount = int(onlineLikedCount)
|
||||
}
|
||||
|
||||
resp := modelsOverviewResponse{
|
||||
TotalCount: int(totalCount),
|
||||
OnlineModelsCount: int(onlineCount),
|
||||
OnlineWatchedModelsCount: int(onlineWatchedCount),
|
||||
OnlineFavCount: int(onlineFavCount),
|
||||
OnlineLikedCount: int(onlineLikedCount),
|
||||
Models: make([]modelsOverviewModel, 0, len(req.Keys)+32),
|
||||
if !includeModels {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
keySet := normalizeModelKeySet(req.Keys)
|
||||
@ -578,6 +605,23 @@ func RegisterModelAPI(mux *http.ServeMux, store *ModelStore) {
|
||||
modelsWriteJSON(w, http.StatusOK, resp)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/models/list", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
store := getModelStore()
|
||||
if store == nil {
|
||||
modelsWriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "model store nicht verfügbar"})
|
||||
return
|
||||
}
|
||||
|
||||
modelsWriteJSON(w, http.StatusOK, map[string]any{
|
||||
"items": store.ListForModelTab(),
|
||||
})
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/models", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
|
||||
@ -341,6 +341,10 @@ func (s *ModelStore) profileImagePresenceExpr() string {
|
||||
return fmt.Sprintf("CASE WHEN trim(COALESCE(profile_image_path,'')) <> '' OR (profile_image_blob IS NOT NULL AND %s > 0) THEN 1 ELSE 0 END", s.blobLengthExpr())
|
||||
}
|
||||
|
||||
func (s *ModelStore) profileImagePathPresenceExpr() string {
|
||||
return "CASE WHEN trim(COALESCE(profile_image_path,'')) <> '' THEN 1 ELSE 0 END"
|
||||
}
|
||||
|
||||
func (s *ModelStore) emptyTextExpr() string {
|
||||
return "''::text"
|
||||
}
|
||||
@ -1577,6 +1581,144 @@ ORDER BY updated_at DESC;
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *ModelStore) ListForModelTab() []StoredModel {
|
||||
if err := s.ensureInit(); err != nil {
|
||||
return []StoredModel{}
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(fmt.Sprintf(`
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(input,'') as input,
|
||||
is_url,
|
||||
COALESCE(host,'') as host,
|
||||
COALESCE(path,'') as path,
|
||||
COALESCE(model_key,'') as model_key,
|
||||
COALESCE(tags,'') as tags,
|
||||
COALESCE(cb_online_json,'') as cb_online_json,
|
||||
last_stream,
|
||||
COALESCE(profile_image_url,''),
|
||||
profile_image_updated_at,
|
||||
%s as has_profile_image,
|
||||
COALESCE(room_status,'') as room_status,
|
||||
COALESCE(is_online,false) as is_online,
|
||||
COALESCE(chat_room_url,'') as chat_room_url,
|
||||
COALESCE(image_url,'') as image_url,
|
||||
last_online_at,
|
||||
last_offline_at,
|
||||
last_room_sync_at,
|
||||
watching,favorite,hot,keep,liked,
|
||||
created_at, updated_at
|
||||
FROM models
|
||||
ORDER BY updated_at DESC;
|
||||
`, s.profileImagePathPresenceExpr()))
|
||||
if err != nil {
|
||||
appLogln("models ListForModelTab query err:", err)
|
||||
return []StoredModel{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]StoredModel, 0, 256)
|
||||
scanErrCount := 0
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, input, host, path, modelKey, tags string
|
||||
cbOnlineJSON string
|
||||
isURL bool
|
||||
|
||||
lastStream dbNullTime
|
||||
|
||||
profileImageURL string
|
||||
profileImageUpdatedAt dbNullTime
|
||||
hasProfileImage int64
|
||||
|
||||
roomStatus string
|
||||
isOnline bool
|
||||
chatRoomURL string
|
||||
imageURL string
|
||||
lastOnlineAt dbNullTime
|
||||
lastOfflineAt dbNullTime
|
||||
lastRoomSyncAt dbNullTime
|
||||
|
||||
watching, favorite, hot, keep bool
|
||||
liked sql.NullBool
|
||||
|
||||
createdAt, updatedAt dbNullTime
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&id, &input, &isURL, &host, &path, &modelKey,
|
||||
&tags, &cbOnlineJSON, &lastStream,
|
||||
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
||||
&roomStatus, &isOnline, &chatRoomURL, &imageURL,
|
||||
&lastOnlineAt, &lastOfflineAt, &lastRoomSyncAt,
|
||||
&watching, &favorite, &hot, &keep, &liked,
|
||||
&createdAt, &updatedAt,
|
||||
); err != nil {
|
||||
scanErrCount++
|
||||
continue
|
||||
}
|
||||
|
||||
var gender, country string
|
||||
if strings.TrimSpace(cbOnlineJSON) != "" {
|
||||
var snap ChaturbateOnlineSnapshot
|
||||
if err := json.Unmarshal([]byte(cbOnlineJSON), &snap); err == nil {
|
||||
gender = strings.TrimSpace(snap.Gender)
|
||||
country = strings.TrimSpace(snap.Country)
|
||||
}
|
||||
}
|
||||
|
||||
m := StoredModel{
|
||||
ID: id,
|
||||
Input: input,
|
||||
IsURL: isURL,
|
||||
Host: host,
|
||||
Path: path,
|
||||
ModelKey: modelKey,
|
||||
|
||||
Tags: tags,
|
||||
LastStream: fmtNullTime(lastStream.NullTime),
|
||||
Gender: gender,
|
||||
Country: country,
|
||||
|
||||
ProfileImageURL: profileImageURL,
|
||||
ProfileImageUpdatedAt: fmtNullTime(profileImageUpdatedAt.NullTime),
|
||||
|
||||
RoomStatus: roomStatus,
|
||||
IsOnline: isOnline,
|
||||
ChatRoomURL: chatRoomURL,
|
||||
ImageURL: imageURL,
|
||||
LastOnlineAt: fmtNullTime(lastOnlineAt.NullTime),
|
||||
LastOfflineAt: fmtNullTime(lastOfflineAt.NullTime),
|
||||
LastRoomSyncAt: fmtNullTime(lastRoomSyncAt.NullTime),
|
||||
|
||||
Watching: watching,
|
||||
Favorite: favorite,
|
||||
Hot: hot,
|
||||
Keep: keep,
|
||||
Liked: ptrLikedFromNullBool(liked),
|
||||
|
||||
CreatedAt: fmtNullTime(createdAt.NullTime),
|
||||
UpdatedAt: fmtNullTime(updatedAt.NullTime),
|
||||
}
|
||||
|
||||
if hasProfileImage != 0 {
|
||||
m.ProfileImageCached = "/api/models/image?id=" + url.QueryEscape(id)
|
||||
}
|
||||
|
||||
out = append(out, m)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
appLogln("models ListForModelTab rows err:", err)
|
||||
}
|
||||
if scanErrCount > 0 {
|
||||
appLogln("models ListForModelTab skipped rows after scan errors:", scanErrCount)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *ModelStore) Meta() ModelsMeta {
|
||||
if err := s.ensureInit(); err != nil {
|
||||
return ModelsMeta{Count: 0, UpdatedAt: ""}
|
||||
@ -2207,7 +2349,7 @@ SELECT id FROM updated;
|
||||
return StoredModel{}, err
|
||||
}
|
||||
|
||||
return s.getByID(updatedID)
|
||||
return s.getModelTabByID(updatedID)
|
||||
}
|
||||
|
||||
func (s *ModelStore) Delete(id string) error {
|
||||
@ -2299,6 +2441,127 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
return m, inserted, err
|
||||
}
|
||||
|
||||
func (s *ModelStore) getModelTabByID(id string) (StoredModel, error) {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return StoredModel{}, errors.New("id fehlt")
|
||||
}
|
||||
|
||||
var (
|
||||
input, host, path, modelKey, tags string
|
||||
cbOnlineJSON string
|
||||
isURL bool
|
||||
|
||||
lastStream dbNullTime
|
||||
|
||||
profileImageURL string
|
||||
profileImageUpdatedAt dbNullTime
|
||||
hasProfileImage int64
|
||||
|
||||
roomStatus string
|
||||
isOnline bool
|
||||
chatRoomURL string
|
||||
imageURL string
|
||||
lastOnlineAt dbNullTime
|
||||
lastOfflineAt dbNullTime
|
||||
lastRoomSyncAt dbNullTime
|
||||
|
||||
watching, favorite, hot, keep bool
|
||||
liked sql.NullBool
|
||||
|
||||
createdAt, updatedAt dbNullTime
|
||||
)
|
||||
|
||||
err := s.db.QueryRow(fmt.Sprintf(`
|
||||
SELECT
|
||||
COALESCE(input,'') as input,
|
||||
is_url,
|
||||
COALESCE(host,'') as host,
|
||||
COALESCE(path,'') as path,
|
||||
COALESCE(model_key,'') as model_key,
|
||||
COALESCE(tags,'') as tags,
|
||||
COALESCE(cb_online_json,'') as cb_online_json,
|
||||
last_stream,
|
||||
COALESCE(profile_image_url,''),
|
||||
profile_image_updated_at,
|
||||
%s as has_profile_image,
|
||||
COALESCE(room_status,'') as room_status,
|
||||
COALESCE(is_online,false) as is_online,
|
||||
COALESCE(chat_room_url,'') as chat_room_url,
|
||||
COALESCE(image_url,'') as image_url,
|
||||
last_online_at,
|
||||
last_offline_at,
|
||||
last_room_sync_at,
|
||||
watching,favorite,hot,keep,liked,
|
||||
created_at, updated_at
|
||||
FROM models
|
||||
WHERE id=$1;
|
||||
`, s.profileImagePathPresenceExpr()), id).Scan(
|
||||
&input, &isURL, &host, &path, &modelKey,
|
||||
&tags, &cbOnlineJSON, &lastStream,
|
||||
&profileImageURL, &profileImageUpdatedAt, &hasProfileImage,
|
||||
&roomStatus, &isOnline, &chatRoomURL, &imageURL,
|
||||
&lastOnlineAt, &lastOfflineAt, &lastRoomSyncAt,
|
||||
&watching, &favorite, &hot, &keep, &liked,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return StoredModel{}, errors.New("model nicht gefunden")
|
||||
}
|
||||
if err != nil {
|
||||
return StoredModel{}, err
|
||||
}
|
||||
|
||||
var gender, country string
|
||||
if strings.TrimSpace(cbOnlineJSON) != "" {
|
||||
var snap ChaturbateOnlineSnapshot
|
||||
if err := json.Unmarshal([]byte(cbOnlineJSON), &snap); err == nil {
|
||||
gender = strings.TrimSpace(snap.Gender)
|
||||
country = strings.TrimSpace(snap.Country)
|
||||
}
|
||||
}
|
||||
|
||||
m := StoredModel{
|
||||
ID: id,
|
||||
Input: input,
|
||||
IsURL: isURL,
|
||||
Host: host,
|
||||
Path: path,
|
||||
ModelKey: modelKey,
|
||||
|
||||
Tags: tags,
|
||||
LastStream: fmtNullTime(lastStream.NullTime),
|
||||
Gender: gender,
|
||||
Country: country,
|
||||
|
||||
ProfileImageURL: profileImageURL,
|
||||
ProfileImageUpdatedAt: fmtNullTime(profileImageUpdatedAt.NullTime),
|
||||
|
||||
RoomStatus: roomStatus,
|
||||
IsOnline: isOnline,
|
||||
ChatRoomURL: chatRoomURL,
|
||||
ImageURL: imageURL,
|
||||
LastOnlineAt: fmtNullTime(lastOnlineAt.NullTime),
|
||||
LastOfflineAt: fmtNullTime(lastOfflineAt.NullTime),
|
||||
LastRoomSyncAt: fmtNullTime(lastRoomSyncAt.NullTime),
|
||||
|
||||
Watching: watching,
|
||||
Favorite: favorite,
|
||||
Hot: hot,
|
||||
Keep: keep,
|
||||
Liked: ptrLikedFromNullBool(liked),
|
||||
|
||||
CreatedAt: fmtNullTime(createdAt.NullTime),
|
||||
UpdatedAt: fmtNullTime(updatedAt.NullTime),
|
||||
}
|
||||
|
||||
if hasProfileImage != 0 {
|
||||
m.ProfileImageCached = "/api/models/image?id=" + url.QueryEscape(id)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *ModelStore) getByID(id string) (StoredModel, error) {
|
||||
var (
|
||||
input, host, path, modelKey, tags string
|
||||
|
||||
@ -966,7 +966,12 @@ func trainingAnalysisValueTruthy(value any) bool {
|
||||
func trainingPublishAnalysisPayload(payload map[string]any) {
|
||||
trainingRememberAnalysisPayload(payload)
|
||||
|
||||
trainingPublishAnalysisPayload(payload)
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
publishSSE("analysisProgress", b)
|
||||
}
|
||||
|
||||
func trainingPublishAnalysisStep(
|
||||
@ -1021,12 +1026,7 @@ func trainingPublishAnalysisStepWithPreview(
|
||||
payload["previewUrl"] = strings.TrimSpace(previewURL)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
publishSSE("analysisProgress", b)
|
||||
trainingPublishAnalysisPayload(payload)
|
||||
}
|
||||
|
||||
func trainingPublishAnalysisStarted(
|
||||
@ -2882,23 +2882,25 @@ func trainingLatestOpenSample(
|
||||
sourceFile = filepath.Base(sample.SourcePath)
|
||||
}
|
||||
|
||||
trainingPublishAnalysisStep(
|
||||
trainingPublishAnalysisStepWithPreview(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
1,
|
||||
2,
|
||||
sourceFile,
|
||||
sample.FrameURL,
|
||||
"Aktuelles Bild wird analysiert…",
|
||||
)
|
||||
|
||||
sample.Prediction = trainingPredictFrame(framePath)
|
||||
|
||||
trainingPublishAnalysisStep(
|
||||
trainingPublishAnalysisStepWithPreview(
|
||||
requestID,
|
||||
startedAtMs,
|
||||
2,
|
||||
2,
|
||||
sourceFile,
|
||||
sample.FrameURL,
|
||||
"Analyse-Ergebnis wird gespeichert…",
|
||||
)
|
||||
|
||||
|
||||
@ -1826,12 +1826,12 @@ export default function App() {
|
||||
const doneSortRef = useRef(doneSort)
|
||||
|
||||
type AppModelsSnapshot = {
|
||||
totalCount: number
|
||||
onlineModelsCount: number
|
||||
onlineWatchedModelsCount: number
|
||||
onlineFavCount: number
|
||||
onlineLikedCount: number
|
||||
models: StoredModel[]
|
||||
totalCount?: number
|
||||
onlineModelsCount?: number
|
||||
onlineWatchedModelsCount?: number
|
||||
onlineFavCount?: number
|
||||
onlineLikedCount?: number
|
||||
models?: StoredModel[]
|
||||
}
|
||||
|
||||
const [headerStats, setHeaderStats] = useState({
|
||||
@ -1873,6 +1873,63 @@ export default function App() {
|
||||
const overviewTimerRef = useRef<number | null>(null)
|
||||
const overviewAbortRef = useRef<AbortController | null>(null)
|
||||
const overviewPendingKeysRef = useRef<string[] | null>(null)
|
||||
const overviewStatsInFlightRef = useRef(false)
|
||||
const overviewStatsLastAtRef = useRef(0)
|
||||
const overviewStatsAbortRef = useRef<AbortController | null>(null)
|
||||
|
||||
const applyHeaderStatsFromSnapshot = useCallback((data: AppModelsSnapshot | null | undefined) => {
|
||||
if (!data) return
|
||||
|
||||
setHeaderStats((prev) => {
|
||||
const totalCount = Number(data.totalCount)
|
||||
const onlineModelsCount = Number(data.onlineModelsCount)
|
||||
const onlineWatchedModelsCount = Number(data.onlineWatchedModelsCount)
|
||||
const onlineFavCount = Number(data.onlineFavCount)
|
||||
const onlineLikedCount = Number(data.onlineLikedCount)
|
||||
|
||||
const next = {
|
||||
totalCount: Number.isFinite(totalCount) ? totalCount : prev.totalCount,
|
||||
onlineModelsCount: Number.isFinite(onlineModelsCount) ? onlineModelsCount : prev.onlineModelsCount,
|
||||
onlineWatchedModelsCount: Number.isFinite(onlineWatchedModelsCount)
|
||||
? onlineWatchedModelsCount
|
||||
: prev.onlineWatchedModelsCount,
|
||||
onlineFavCount: Number.isFinite(onlineFavCount) ? onlineFavCount : prev.onlineFavCount,
|
||||
onlineLikedCount: Number.isFinite(onlineLikedCount) ? onlineLikedCount : prev.onlineLikedCount,
|
||||
}
|
||||
|
||||
return (
|
||||
prev.totalCount === next.totalCount &&
|
||||
prev.onlineModelsCount === next.onlineModelsCount &&
|
||||
prev.onlineWatchedModelsCount === next.onlineWatchedModelsCount &&
|
||||
prev.onlineFavCount === next.onlineFavCount &&
|
||||
prev.onlineLikedCount === next.onlineLikedCount
|
||||
)
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const mergeModelsSnapshot = useCallback((models: StoredModel[] | undefined | null) => {
|
||||
const safeList = Array.isArray(models) ? models : []
|
||||
if (safeList.length === 0) return
|
||||
|
||||
const built = buildModelsByKey(safeList)
|
||||
|
||||
setModelsByKey((prev) => {
|
||||
const next = { ...prev }
|
||||
let changed = false
|
||||
|
||||
for (const [k, incoming] of Object.entries(built)) {
|
||||
const merged = mergeStoredModel(prev[k], incoming)
|
||||
if (!storedModelShallowEqual(prev[k], merged)) {
|
||||
next[k] = merged
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? next : prev
|
||||
})
|
||||
}, [buildModelsByKey])
|
||||
|
||||
const loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
||||
const uniqKeys = Array.from(
|
||||
@ -1894,7 +1951,6 @@ export default function App() {
|
||||
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < MODELS_OVERVIEW_COOLDOWN_MS) return
|
||||
|
||||
overviewInFlightRef.current = true
|
||||
overviewAbortRef.current?.abort()
|
||||
|
||||
const controller = new AbortController()
|
||||
overviewAbortRef.current = controller
|
||||
@ -1907,50 +1963,16 @@ export default function App() {
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
keys: uniqKeys,
|
||||
includeWatched: true,
|
||||
includeWatched: false,
|
||||
includeStats: false,
|
||||
includeModels: true,
|
||||
}),
|
||||
})
|
||||
|
||||
setHeaderStats((prev) => {
|
||||
const next = {
|
||||
totalCount: Number(data?.totalCount ?? 0),
|
||||
onlineModelsCount: Number(data?.onlineModelsCount ?? 0),
|
||||
onlineWatchedModelsCount: Number(data?.onlineWatchedModelsCount ?? 0),
|
||||
onlineFavCount: Number(data?.onlineFavCount ?? 0),
|
||||
onlineLikedCount: Number(data?.onlineLikedCount ?? 0),
|
||||
}
|
||||
|
||||
return (
|
||||
prev.totalCount === next.totalCount &&
|
||||
prev.onlineModelsCount === next.onlineModelsCount &&
|
||||
prev.onlineWatchedModelsCount === next.onlineWatchedModelsCount &&
|
||||
prev.onlineFavCount === next.onlineFavCount &&
|
||||
prev.onlineLikedCount === next.onlineLikedCount
|
||||
)
|
||||
? prev
|
||||
: next
|
||||
})
|
||||
|
||||
overviewLastSigRef.current = sig
|
||||
overviewLastAtRef.current = now
|
||||
|
||||
const safeList = Array.isArray(data?.models) ? data.models : []
|
||||
const built = buildModelsByKey(safeList)
|
||||
|
||||
setModelsByKey((prev) => {
|
||||
const next = { ...prev }
|
||||
let changed = false
|
||||
|
||||
for (const [k, incoming] of Object.entries(built)) {
|
||||
const merged = mergeStoredModel(prev[k], incoming)
|
||||
if (!storedModelShallowEqual(prev[k], merged)) {
|
||||
next[k] = merged
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? next : prev
|
||||
})
|
||||
mergeModelsSnapshot(data?.models)
|
||||
} catch (error: any) {
|
||||
if (error?.name === 'AbortError') return
|
||||
// ignore
|
||||
@ -1974,7 +1996,46 @@ export default function App() {
|
||||
}, MODELS_OVERVIEW_DEBOUNCE_MS)
|
||||
}
|
||||
}
|
||||
}, [buildModelsByKey])
|
||||
}, [mergeModelsSnapshot])
|
||||
|
||||
const loadAppModelsStatsAndWatched = useCallback(async (force = false) => {
|
||||
const now = Date.now()
|
||||
|
||||
if (overviewStatsInFlightRef.current) return
|
||||
if (!force && now - overviewStatsLastAtRef.current < MODELS_OVERVIEW_COOLDOWN_MS) return
|
||||
|
||||
overviewStatsInFlightRef.current = true
|
||||
|
||||
const controller = new AbortController()
|
||||
overviewStatsAbortRef.current = controller
|
||||
|
||||
try {
|
||||
const data = await apiJSON<AppModelsSnapshot>('/api/models/overview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store' as any,
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
keys: [],
|
||||
includeWatched: true,
|
||||
includeStats: true,
|
||||
includeModels: true,
|
||||
}),
|
||||
})
|
||||
|
||||
overviewStatsLastAtRef.current = Date.now()
|
||||
applyHeaderStatsFromSnapshot(data)
|
||||
mergeModelsSnapshot(data?.models)
|
||||
} catch (error: any) {
|
||||
if (error?.name === 'AbortError') return
|
||||
// ignore
|
||||
} finally {
|
||||
overviewStatsInFlightRef.current = false
|
||||
if (overviewStatsAbortRef.current === controller) {
|
||||
overviewStatsAbortRef.current = null
|
||||
}
|
||||
}
|
||||
}, [applyHeaderStatsFromSnapshot, mergeModelsSnapshot])
|
||||
|
||||
const maybeNotifyWatchedModelStatusChange = useCallback((
|
||||
keyLower: string,
|
||||
@ -2349,6 +2410,7 @@ export default function App() {
|
||||
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
||||
lastOverviewSigRef.current = requiredModelKeysSig
|
||||
scheduleOverviewLoad()
|
||||
void loadAppModelsStatsAndWatched(false)
|
||||
}
|
||||
|
||||
const onChanged = (ev: Event) => {
|
||||
@ -2379,6 +2441,7 @@ export default function App() {
|
||||
)
|
||||
|
||||
scheduleOverviewLoad()
|
||||
void loadAppModelsStatsAndWatched(true)
|
||||
return
|
||||
}
|
||||
|
||||
@ -2398,10 +2461,12 @@ export default function App() {
|
||||
}
|
||||
|
||||
scheduleOverviewLoad()
|
||||
void loadAppModelsStatsAndWatched(true)
|
||||
return
|
||||
}
|
||||
|
||||
scheduleOverviewLoad()
|
||||
void loadAppModelsStatsAndWatched(false)
|
||||
}
|
||||
|
||||
window.addEventListener('models-changed', onChanged as any)
|
||||
@ -2413,13 +2478,44 @@ export default function App() {
|
||||
window.clearTimeout(overviewTimerRef.current)
|
||||
overviewTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [authed, loadAppModelsSnapshot, loadAppModelsStatsAndWatched, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||
|
||||
useEffect(() => {
|
||||
if (authed) return
|
||||
|
||||
if (overviewTimerRef.current != null) {
|
||||
window.clearTimeout(overviewTimerRef.current)
|
||||
overviewTimerRef.current = null
|
||||
}
|
||||
|
||||
overviewPendingKeysRef.current = null
|
||||
overviewAbortRef.current?.abort()
|
||||
overviewAbortRef.current = null
|
||||
overviewInFlightRef.current = false
|
||||
overviewStatsAbortRef.current?.abort()
|
||||
overviewStatsAbortRef.current = null
|
||||
overviewStatsInFlightRef.current = false
|
||||
overviewStatsLastAtRef.current = 0
|
||||
}, [authed])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (overviewTimerRef.current != null) {
|
||||
window.clearTimeout(overviewTimerRef.current)
|
||||
overviewTimerRef.current = null
|
||||
}
|
||||
|
||||
overviewPendingKeysRef.current = null
|
||||
overviewAbortRef.current?.abort()
|
||||
overviewAbortRef.current = null
|
||||
overviewInFlightRef.current = false
|
||||
overviewStatsAbortRef.current?.abort()
|
||||
overviewStatsAbortRef.current = null
|
||||
overviewStatsInFlightRef.current = false
|
||||
overviewStatsLastAtRef.current = 0
|
||||
}
|
||||
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
donePageRef.current = donePage
|
||||
@ -2592,6 +2688,7 @@ export default function App() {
|
||||
// der watched/fav/like Counter + modelsByKey
|
||||
if (safeFetchedAt && safeFetchedAt !== lastSeenFetchedAt) {
|
||||
lastSeenFetchedAt = safeFetchedAt
|
||||
void loadAppModelsStatsAndWatched(true)
|
||||
void loadAppModelsSnapshot(requiredModelKeysRef.current)
|
||||
}
|
||||
} catch {
|
||||
@ -2609,7 +2706,7 @@ export default function App() {
|
||||
cancelled = true
|
||||
if (timer != null) window.clearInterval(timer)
|
||||
}
|
||||
}, [authed, recSettings.useChaturbateApi, loadAppModelsSnapshot])
|
||||
}, [authed, recSettings.useChaturbateApi, loadAppModelsSnapshot, loadAppModelsStatsAndWatched])
|
||||
|
||||
// ✅ StartURL (hier habe ich den alten Online-Fetch entfernt und nur Snapshot genutzt)
|
||||
const startUrl = useCallback(async (
|
||||
|
||||
@ -83,6 +83,14 @@ export type StoredModel = {
|
||||
gender?: string | null
|
||||
country?: string | null
|
||||
|
||||
roomStatus?: string
|
||||
isOnline?: boolean
|
||||
chatRoomUrl?: string
|
||||
imageUrl?: string
|
||||
lastOnlineAt?: string
|
||||
lastOfflineAt?: string
|
||||
lastRoomSyncAt?: string
|
||||
|
||||
watching: boolean
|
||||
favorite: boolean
|
||||
hot: boolean
|
||||
@ -497,7 +505,7 @@ export default function ModelsTab({ onAddToDownloads, onModelsCountChange }: Mod
|
||||
setLoading(true)
|
||||
setErr(null)
|
||||
try {
|
||||
const res = await fetch('/api/models', { cache: 'no-store' as any })
|
||||
const res = await fetch('/api/models/list', { cache: 'no-store' as any })
|
||||
if (!res.ok) throw new Error(await res.text().catch(() => `HTTP ${res.status}`))
|
||||
|
||||
const data = await res.json().catch(() => null)
|
||||
|
||||
@ -275,6 +275,8 @@ type TrainingStats = {
|
||||
detectorModelInfo?: TrainingModelInfo
|
||||
poseModelAvailable?: boolean
|
||||
poseModelInfo?: TrainingModelInfo
|
||||
videoMAEModelAvailable?: boolean
|
||||
videoMAEModelInfo?: TrainingModelInfo
|
||||
confidence?: TrainingConfidence
|
||||
labels: {
|
||||
people: TrainingLabelStat[]
|
||||
@ -2649,6 +2651,9 @@ function TrainingStatsModal(props: {
|
||||
error: string | null
|
||||
feedbackCount: number
|
||||
requiredCount: number
|
||||
deletingTrainingData?: boolean
|
||||
deleteTrainingDataDisabled?: boolean
|
||||
onDeleteTrainingData?: () => void | Promise<void>
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState<TrainingStatsTabKey>('people')
|
||||
const stats = props.stats
|
||||
@ -2667,6 +2672,8 @@ function TrainingStatsModal(props: {
|
||||
const detectorModelInfo = stats?.detectorModelInfo ?? stats?.modelInfo
|
||||
const poseModelAvailable = Boolean(stats?.poseModelAvailable)
|
||||
const poseModelInfo = stats?.poseModelInfo
|
||||
const videoMAEModelAvailable = Boolean(stats?.videoMAEModelAvailable)
|
||||
const videoMAEModelInfo = stats?.videoMAEModelInfo
|
||||
const modelTrainedAtLabel = formatModelTrainedAt(detectorModelInfo)
|
||||
const modelMap50Label = formatMapPercent(detectorModelInfo?.map50)
|
||||
const modelMap5095Label = formatMapPercent(detectorModelInfo?.map5095)
|
||||
@ -2683,16 +2690,6 @@ function TrainingStatsModal(props: {
|
||||
return parts.join(' · ')
|
||||
})()
|
||||
|
||||
const availableModelCount =
|
||||
(detectorModelAvailable ? 1 : 0) + (poseModelAvailable ? 1 : 0)
|
||||
const modelSummaryLabel =
|
||||
availableModelCount === 2
|
||||
? 'Detector & Pose verfuegbar'
|
||||
: detectorModelAvailable
|
||||
? 'Detector verfuegbar'
|
||||
: poseModelAvailable
|
||||
? 'Pose verfuegbar'
|
||||
: 'Noch kein trainiertes Modell verfuegbar'
|
||||
const modelCards = [
|
||||
{
|
||||
key: 'detector',
|
||||
@ -2706,6 +2703,12 @@ function TrainingStatsModal(props: {
|
||||
available: poseModelAvailable,
|
||||
info: poseModelInfo,
|
||||
},
|
||||
{
|
||||
key: 'videomae',
|
||||
title: 'VideoMAE',
|
||||
available: videoMAEModelAvailable,
|
||||
info: videoMAEModelInfo,
|
||||
},
|
||||
].map((model) => ({
|
||||
...model,
|
||||
trainedAtLabel: formatModelTrainedAt(model.info),
|
||||
@ -2716,6 +2719,17 @@ function TrainingStatsModal(props: {
|
||||
? modelInfoDetails
|
||||
: formatTrainingModelDetails(model.info),
|
||||
}))
|
||||
const availableModelCount = modelCards.filter((model) => model.available).length
|
||||
const totalModelCount = modelCards.length
|
||||
const availableModelNames = modelCards
|
||||
.filter((model) => model.available)
|
||||
.map((model) => model.title)
|
||||
const modelSummaryLabel =
|
||||
availableModelCount === totalModelCount
|
||||
? 'Alle Modelle verfuegbar'
|
||||
: availableModelCount > 0
|
||||
? `${availableModelNames.join(' + ')} verfuegbar`
|
||||
: 'Noch kein trainiertes Modell verfuegbar'
|
||||
|
||||
const history = props.history ?? []
|
||||
|
||||
@ -2893,7 +2907,7 @@ function TrainingStatsModal(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-1.5">
|
||||
<div className="mt-3 grid grid-cols-3 gap-1.5">
|
||||
{modelCards.map((model) => (
|
||||
<div
|
||||
key={model.key}
|
||||
@ -3037,7 +3051,7 @@ function TrainingStatsModal(props: {
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wide text-indigo-700/80 dark:text-indigo-200/80">
|
||||
{modelSummaryLabel} - {availableModelCount}/2
|
||||
{modelSummaryLabel} - {availableModelCount}/{totalModelCount}
|
||||
</div>
|
||||
|
||||
{modelCards.map((model) => (
|
||||
@ -3295,6 +3309,39 @@ function TrainingStatsModal(props: {
|
||||
total={activeTabItem.total}
|
||||
confidence={activeTabItem.confidence}
|
||||
/>
|
||||
|
||||
<div className="rounded-xl border border-red-100 bg-red-50 p-3 dark:border-red-400/20 dark:bg-red-500/10 sm:p-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-red-950 dark:text-red-100">
|
||||
Trainingsdaten löschen
|
||||
</div>
|
||||
|
||||
<div className="mt-0.5 text-xs leading-relaxed text-red-800/80 dark:text-red-100/70">
|
||||
Entfernt Feedback, Frames, Samples und Trainingsdaten. Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
color="red"
|
||||
className="shrink-0 justify-center px-3 text-xs sm:w-auto"
|
||||
disabled={
|
||||
Boolean(props.deletingTrainingData) ||
|
||||
Boolean(props.deleteTrainingDataDisabled) ||
|
||||
!props.onDeleteTrainingData
|
||||
}
|
||||
onClick={() => void props.onDeleteTrainingData?.()}
|
||||
title="Löscht Feedback, Frames, Samples und Trainingsdaten."
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{props.deletingTrainingData ? 'Lösche…' : 'Trainingsdaten löschen'}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -5262,6 +5309,12 @@ export default function TrainingTab(props: {
|
||||
),
|
||||
poseModelAvailable: Boolean(data?.poseModelAvailable),
|
||||
poseModelInfo: parseTrainingModelInfo(data?.poseModelInfo),
|
||||
videoMAEModelAvailable: Boolean(
|
||||
data?.videoMAEModelAvailable ?? data?.videomaeModelAvailable
|
||||
),
|
||||
videoMAEModelInfo: parseTrainingModelInfo(
|
||||
data?.videoMAEModelInfo ?? data?.videomaeModelInfo
|
||||
),
|
||||
confidence: data?.confidence,
|
||||
labels: {
|
||||
people: Array.isArray(data?.labels?.people) ? data.labels.people : [],
|
||||
@ -6355,12 +6408,15 @@ export default function TrainingTab(props: {
|
||||
|
||||
await loadTrainingStatus()
|
||||
await loadNext({ forceNew: true, preserveNotice: true })
|
||||
if (statsModalOpen) {
|
||||
await loadTrainingStats()
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setDeletingTrainingData(false)
|
||||
}
|
||||
}, [loadNext, loadTrainingStatus, requiredCount])
|
||||
}, [loadNext, loadTrainingStats, loadTrainingStatus, requiredCount, statsModalOpen])
|
||||
|
||||
const getPointerPosFromRect = useCallback((
|
||||
rect: ImageContentRect,
|
||||
@ -7562,7 +7618,10 @@ export default function TrainingTab(props: {
|
||||
onClick={() => setFeedbackModalOpen(true)}
|
||||
title="Bisher abgegebenes Feedback ansehen."
|
||||
>
|
||||
Abgegebenes Feedback ansehen
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<InboxArrowDownIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Abgegebenes Feedback ansehen
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -7608,22 +7667,6 @@ export default function TrainingTab(props: {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 border-t border-gray-100 pt-3 dark:border-white/10">
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
color="red"
|
||||
className="w-full justify-center px-2 text-[11px]"
|
||||
disabled={uiLocked}
|
||||
onClick={() => void deleteAllTrainingData()}
|
||||
title="Löscht Feedback, Frames, Samples und Detector-Daten."
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{deletingTrainingData ? 'Lösche…' : 'Trainingsdaten löschen'}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -9569,7 +9612,10 @@ export default function TrainingTab(props: {
|
||||
disabled={!canConfirmTrainingStart || trainingRunning}
|
||||
onClick={startTrainingFromModal}
|
||||
>
|
||||
Training starten
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<BoltIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Training starten
|
||||
</span>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@ -9741,6 +9787,9 @@ export default function TrainingTab(props: {
|
||||
error={trainingStatsError}
|
||||
feedbackCount={feedbackCount}
|
||||
requiredCount={requiredCount}
|
||||
deletingTrainingData={deletingTrainingData}
|
||||
deleteTrainingDataDisabled={uiLocked}
|
||||
onDeleteTrainingData={deleteAllTrainingData}
|
||||
/>
|
||||
|
||||
<TrainingFeedbackHistoryModal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user