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 {
|
type modelsOverviewRequest struct {
|
||||||
Keys []string `json:"keys"`
|
Keys []string `json:"keys"`
|
||||||
IncludeWatched bool `json:"includeWatched"`
|
IncludeWatched bool `json:"includeWatched"`
|
||||||
|
IncludeStats *bool `json:"includeStats,omitempty"`
|
||||||
|
IncludeModels *bool `json:"includeModels,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type modelsOverviewModel struct {
|
type modelsOverviewModel struct {
|
||||||
@ -187,6 +189,13 @@ func normalizeModelKeySet(in []string) map[string]bool {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func overviewBool(v *bool, fallback bool) bool {
|
||||||
|
if v == nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
func isOnlineRoomStatus(status string, isOnline bool) bool {
|
func isOnlineRoomStatus(status string, isOnline bool) bool {
|
||||||
s := strings.ToLower(strings.TrimSpace(status))
|
s := strings.ToLower(strings.TrimSpace(status))
|
||||||
if s == "offline" {
|
if s == "offline" {
|
||||||
@ -213,17 +222,24 @@ func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverview
|
|||||||
|
|
||||||
keySet := normalizeModelKeySet(req.Keys)
|
keySet := normalizeModelKeySet(req.Keys)
|
||||||
|
|
||||||
|
includeStats := overviewBool(req.IncludeStats, true)
|
||||||
|
includeModels := overviewBool(req.IncludeModels, true)
|
||||||
|
|
||||||
resp := modelsOverviewResponse{
|
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 {
|
for _, m := range list {
|
||||||
key := strings.ToLower(strings.TrimSpace(m.ModelKey))
|
key := strings.ToLower(strings.TrimSpace(m.ModelKey))
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if includeStats {
|
||||||
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
|
online := isOnlineRoomStatus(m.RoomStatus, m.IsOnline)
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
|
if strings.Contains(strings.ToLower(m.Host), "chaturbate") {
|
||||||
@ -241,10 +257,12 @@ func buildModelsOverview(rawList any, req modelsOverviewRequest) (modelsOverview
|
|||||||
if m.Liked != nil && *m.Liked && online {
|
if m.Liked != nil && *m.Liked && online {
|
||||||
resp.OnlineLikedCount++
|
resp.OnlineLikedCount++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
needModel :=
|
needModel :=
|
||||||
keySet[key] ||
|
includeModels &&
|
||||||
(req.IncludeWatched && m.Watching)
|
(keySet[key] ||
|
||||||
|
(req.IncludeWatched && m.Watching))
|
||||||
|
|
||||||
if needModel {
|
if needModel {
|
||||||
resp.Models = append(resp.Models, m)
|
resp.Models = append(resp.Models, m)
|
||||||
@ -259,6 +277,13 @@ func loadModelsOverview(store *ModelStore, req modelsOverviewRequest) (modelsOve
|
|||||||
return modelsOverviewResponse{}, err
|
return modelsOverviewResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
includeStats := overviewBool(req.IncludeStats, true)
|
||||||
|
includeModels := overviewBool(req.IncludeModels, true)
|
||||||
|
|
||||||
|
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,'')) <> ''))`
|
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
|
var totalCount, onlineCount, onlineWatchedCount, onlineFavCount, onlineLikedCount int64
|
||||||
@ -275,13 +300,15 @@ FROM models;
|
|||||||
return modelsOverviewResponse{}, err
|
return modelsOverviewResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := modelsOverviewResponse{
|
resp.TotalCount = int(totalCount)
|
||||||
TotalCount: int(totalCount),
|
resp.OnlineModelsCount = int(onlineCount)
|
||||||
OnlineModelsCount: int(onlineCount),
|
resp.OnlineWatchedModelsCount = int(onlineWatchedCount)
|
||||||
OnlineWatchedModelsCount: int(onlineWatchedCount),
|
resp.OnlineFavCount = int(onlineFavCount)
|
||||||
OnlineFavCount: int(onlineFavCount),
|
resp.OnlineLikedCount = int(onlineLikedCount)
|
||||||
OnlineLikedCount: int(onlineLikedCount),
|
}
|
||||||
Models: make([]modelsOverviewModel, 0, len(req.Keys)+32),
|
|
||||||
|
if !includeModels {
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
keySet := normalizeModelKeySet(req.Keys)
|
keySet := normalizeModelKeySet(req.Keys)
|
||||||
@ -578,6 +605,23 @@ func RegisterModelAPI(mux *http.ServeMux, store *ModelStore) {
|
|||||||
modelsWriteJSON(w, http.StatusOK, resp)
|
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) {
|
mux.HandleFunc("/api/models", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
modelsWriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
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())
|
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 {
|
func (s *ModelStore) emptyTextExpr() string {
|
||||||
return "''::text"
|
return "''::text"
|
||||||
}
|
}
|
||||||
@ -1577,6 +1581,144 @@ ORDER BY updated_at DESC;
|
|||||||
return out
|
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 {
|
func (s *ModelStore) Meta() ModelsMeta {
|
||||||
if err := s.ensureInit(); err != nil {
|
if err := s.ensureInit(); err != nil {
|
||||||
return ModelsMeta{Count: 0, UpdatedAt: ""}
|
return ModelsMeta{Count: 0, UpdatedAt: ""}
|
||||||
@ -2207,7 +2349,7 @@ SELECT id FROM updated;
|
|||||||
return StoredModel{}, err
|
return StoredModel{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.getByID(updatedID)
|
return s.getModelTabByID(updatedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ModelStore) Delete(id string) error {
|
func (s *ModelStore) Delete(id string) error {
|
||||||
@ -2299,6 +2441,127 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
return m, inserted, err
|
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) {
|
func (s *ModelStore) getByID(id string) (StoredModel, error) {
|
||||||
var (
|
var (
|
||||||
input, host, path, modelKey, tags string
|
input, host, path, modelKey, tags string
|
||||||
|
|||||||
@ -966,7 +966,12 @@ func trainingAnalysisValueTruthy(value any) bool {
|
|||||||
func trainingPublishAnalysisPayload(payload map[string]any) {
|
func trainingPublishAnalysisPayload(payload map[string]any) {
|
||||||
trainingRememberAnalysisPayload(payload)
|
trainingRememberAnalysisPayload(payload)
|
||||||
|
|
||||||
trainingPublishAnalysisPayload(payload)
|
b, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishSSE("analysisProgress", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func trainingPublishAnalysisStep(
|
func trainingPublishAnalysisStep(
|
||||||
@ -1021,12 +1026,7 @@ func trainingPublishAnalysisStepWithPreview(
|
|||||||
payload["previewUrl"] = strings.TrimSpace(previewURL)
|
payload["previewUrl"] = strings.TrimSpace(previewURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(payload)
|
trainingPublishAnalysisPayload(payload)
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
publishSSE("analysisProgress", b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trainingPublishAnalysisStarted(
|
func trainingPublishAnalysisStarted(
|
||||||
@ -2882,23 +2882,25 @@ func trainingLatestOpenSample(
|
|||||||
sourceFile = filepath.Base(sample.SourcePath)
|
sourceFile = filepath.Base(sample.SourcePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
trainingPublishAnalysisStep(
|
trainingPublishAnalysisStepWithPreview(
|
||||||
requestID,
|
requestID,
|
||||||
startedAtMs,
|
startedAtMs,
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
sourceFile,
|
sourceFile,
|
||||||
|
sample.FrameURL,
|
||||||
"Aktuelles Bild wird analysiert…",
|
"Aktuelles Bild wird analysiert…",
|
||||||
)
|
)
|
||||||
|
|
||||||
sample.Prediction = trainingPredictFrame(framePath)
|
sample.Prediction = trainingPredictFrame(framePath)
|
||||||
|
|
||||||
trainingPublishAnalysisStep(
|
trainingPublishAnalysisStepWithPreview(
|
||||||
requestID,
|
requestID,
|
||||||
startedAtMs,
|
startedAtMs,
|
||||||
2,
|
2,
|
||||||
2,
|
2,
|
||||||
sourceFile,
|
sourceFile,
|
||||||
|
sample.FrameURL,
|
||||||
"Analyse-Ergebnis wird gespeichert…",
|
"Analyse-Ergebnis wird gespeichert…",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1826,12 +1826,12 @@ export default function App() {
|
|||||||
const doneSortRef = useRef(doneSort)
|
const doneSortRef = useRef(doneSort)
|
||||||
|
|
||||||
type AppModelsSnapshot = {
|
type AppModelsSnapshot = {
|
||||||
totalCount: number
|
totalCount?: number
|
||||||
onlineModelsCount: number
|
onlineModelsCount?: number
|
||||||
onlineWatchedModelsCount: number
|
onlineWatchedModelsCount?: number
|
||||||
onlineFavCount: number
|
onlineFavCount?: number
|
||||||
onlineLikedCount: number
|
onlineLikedCount?: number
|
||||||
models: StoredModel[]
|
models?: StoredModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const [headerStats, setHeaderStats] = useState({
|
const [headerStats, setHeaderStats] = useState({
|
||||||
@ -1873,6 +1873,63 @@ export default function App() {
|
|||||||
const overviewTimerRef = useRef<number | null>(null)
|
const overviewTimerRef = useRef<number | null>(null)
|
||||||
const overviewAbortRef = useRef<AbortController | null>(null)
|
const overviewAbortRef = useRef<AbortController | null>(null)
|
||||||
const overviewPendingKeysRef = useRef<string[] | 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 loadAppModelsSnapshot = useCallback(async (keys: string[]) => {
|
||||||
const uniqKeys = Array.from(
|
const uniqKeys = Array.from(
|
||||||
@ -1894,7 +1951,6 @@ export default function App() {
|
|||||||
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < MODELS_OVERVIEW_COOLDOWN_MS) return
|
if (overviewLastSigRef.current === sig && now - overviewLastAtRef.current < MODELS_OVERVIEW_COOLDOWN_MS) return
|
||||||
|
|
||||||
overviewInFlightRef.current = true
|
overviewInFlightRef.current = true
|
||||||
overviewAbortRef.current?.abort()
|
|
||||||
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
overviewAbortRef.current = controller
|
overviewAbortRef.current = controller
|
||||||
@ -1907,50 +1963,16 @@ export default function App() {
|
|||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
keys: uniqKeys,
|
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
|
overviewLastSigRef.current = sig
|
||||||
overviewLastAtRef.current = now
|
overviewLastAtRef.current = now
|
||||||
|
|
||||||
const safeList = Array.isArray(data?.models) ? data.models : []
|
mergeModelsSnapshot(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
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.name === 'AbortError') return
|
if (error?.name === 'AbortError') return
|
||||||
// ignore
|
// ignore
|
||||||
@ -1974,7 +1996,46 @@ export default function App() {
|
|||||||
}, MODELS_OVERVIEW_DEBOUNCE_MS)
|
}, 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((
|
const maybeNotifyWatchedModelStatusChange = useCallback((
|
||||||
keyLower: string,
|
keyLower: string,
|
||||||
@ -2349,6 +2410,7 @@ export default function App() {
|
|||||||
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
if (lastOverviewSigRef.current !== requiredModelKeysSig) {
|
||||||
lastOverviewSigRef.current = requiredModelKeysSig
|
lastOverviewSigRef.current = requiredModelKeysSig
|
||||||
scheduleOverviewLoad()
|
scheduleOverviewLoad()
|
||||||
|
void loadAppModelsStatsAndWatched(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChanged = (ev: Event) => {
|
const onChanged = (ev: Event) => {
|
||||||
@ -2379,6 +2441,7 @@ export default function App() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
scheduleOverviewLoad()
|
scheduleOverviewLoad()
|
||||||
|
void loadAppModelsStatsAndWatched(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2398,10 +2461,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduleOverviewLoad()
|
scheduleOverviewLoad()
|
||||||
|
void loadAppModelsStatsAndWatched(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleOverviewLoad()
|
scheduleOverviewLoad()
|
||||||
|
void loadAppModelsStatsAndWatched(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('models-changed', onChanged as any)
|
window.addEventListener('models-changed', onChanged as any)
|
||||||
@ -2413,13 +2478,44 @@ export default function App() {
|
|||||||
window.clearTimeout(overviewTimerRef.current)
|
window.clearTimeout(overviewTimerRef.current)
|
||||||
overviewTimerRef.current = null
|
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
|
overviewPendingKeysRef.current = null
|
||||||
overviewAbortRef.current?.abort()
|
overviewAbortRef.current?.abort()
|
||||||
overviewAbortRef.current = null
|
overviewAbortRef.current = null
|
||||||
overviewInFlightRef.current = false
|
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
|
||||||
}
|
}
|
||||||
}, [authed, loadAppModelsSnapshot, requiredModelKeysSig, requiredModelKeys, upsertModelCache])
|
|
||||||
|
overviewPendingKeysRef.current = null
|
||||||
|
overviewAbortRef.current?.abort()
|
||||||
|
overviewAbortRef.current = null
|
||||||
|
overviewInFlightRef.current = false
|
||||||
|
overviewStatsAbortRef.current?.abort()
|
||||||
|
overviewStatsAbortRef.current = null
|
||||||
|
overviewStatsInFlightRef.current = false
|
||||||
|
overviewStatsLastAtRef.current = 0
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
donePageRef.current = donePage
|
donePageRef.current = donePage
|
||||||
@ -2592,6 +2688,7 @@ export default function App() {
|
|||||||
// der watched/fav/like Counter + modelsByKey
|
// der watched/fav/like Counter + modelsByKey
|
||||||
if (safeFetchedAt && safeFetchedAt !== lastSeenFetchedAt) {
|
if (safeFetchedAt && safeFetchedAt !== lastSeenFetchedAt) {
|
||||||
lastSeenFetchedAt = safeFetchedAt
|
lastSeenFetchedAt = safeFetchedAt
|
||||||
|
void loadAppModelsStatsAndWatched(true)
|
||||||
void loadAppModelsSnapshot(requiredModelKeysRef.current)
|
void loadAppModelsSnapshot(requiredModelKeysRef.current)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -2609,7 +2706,7 @@ export default function App() {
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
if (timer != null) window.clearInterval(timer)
|
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)
|
// ✅ StartURL (hier habe ich den alten Online-Fetch entfernt und nur Snapshot genutzt)
|
||||||
const startUrl = useCallback(async (
|
const startUrl = useCallback(async (
|
||||||
|
|||||||
@ -83,6 +83,14 @@ export type StoredModel = {
|
|||||||
gender?: string | null
|
gender?: string | null
|
||||||
country?: string | null
|
country?: string | null
|
||||||
|
|
||||||
|
roomStatus?: string
|
||||||
|
isOnline?: boolean
|
||||||
|
chatRoomUrl?: string
|
||||||
|
imageUrl?: string
|
||||||
|
lastOnlineAt?: string
|
||||||
|
lastOfflineAt?: string
|
||||||
|
lastRoomSyncAt?: string
|
||||||
|
|
||||||
watching: boolean
|
watching: boolean
|
||||||
favorite: boolean
|
favorite: boolean
|
||||||
hot: boolean
|
hot: boolean
|
||||||
@ -497,7 +505,7 @@ export default function ModelsTab({ onAddToDownloads, onModelsCountChange }: Mod
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
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}`))
|
if (!res.ok) throw new Error(await res.text().catch(() => `HTTP ${res.status}`))
|
||||||
|
|
||||||
const data = await res.json().catch(() => null)
|
const data = await res.json().catch(() => null)
|
||||||
|
|||||||
@ -275,6 +275,8 @@ type TrainingStats = {
|
|||||||
detectorModelInfo?: TrainingModelInfo
|
detectorModelInfo?: TrainingModelInfo
|
||||||
poseModelAvailable?: boolean
|
poseModelAvailable?: boolean
|
||||||
poseModelInfo?: TrainingModelInfo
|
poseModelInfo?: TrainingModelInfo
|
||||||
|
videoMAEModelAvailable?: boolean
|
||||||
|
videoMAEModelInfo?: TrainingModelInfo
|
||||||
confidence?: TrainingConfidence
|
confidence?: TrainingConfidence
|
||||||
labels: {
|
labels: {
|
||||||
people: TrainingLabelStat[]
|
people: TrainingLabelStat[]
|
||||||
@ -2649,6 +2651,9 @@ function TrainingStatsModal(props: {
|
|||||||
error: string | null
|
error: string | null
|
||||||
feedbackCount: number
|
feedbackCount: number
|
||||||
requiredCount: number
|
requiredCount: number
|
||||||
|
deletingTrainingData?: boolean
|
||||||
|
deleteTrainingDataDisabled?: boolean
|
||||||
|
onDeleteTrainingData?: () => void | Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState<TrainingStatsTabKey>('people')
|
const [activeTab, setActiveTab] = useState<TrainingStatsTabKey>('people')
|
||||||
const stats = props.stats
|
const stats = props.stats
|
||||||
@ -2667,6 +2672,8 @@ function TrainingStatsModal(props: {
|
|||||||
const detectorModelInfo = stats?.detectorModelInfo ?? stats?.modelInfo
|
const detectorModelInfo = stats?.detectorModelInfo ?? stats?.modelInfo
|
||||||
const poseModelAvailable = Boolean(stats?.poseModelAvailable)
|
const poseModelAvailable = Boolean(stats?.poseModelAvailable)
|
||||||
const poseModelInfo = stats?.poseModelInfo
|
const poseModelInfo = stats?.poseModelInfo
|
||||||
|
const videoMAEModelAvailable = Boolean(stats?.videoMAEModelAvailable)
|
||||||
|
const videoMAEModelInfo = stats?.videoMAEModelInfo
|
||||||
const modelTrainedAtLabel = formatModelTrainedAt(detectorModelInfo)
|
const modelTrainedAtLabel = formatModelTrainedAt(detectorModelInfo)
|
||||||
const modelMap50Label = formatMapPercent(detectorModelInfo?.map50)
|
const modelMap50Label = formatMapPercent(detectorModelInfo?.map50)
|
||||||
const modelMap5095Label = formatMapPercent(detectorModelInfo?.map5095)
|
const modelMap5095Label = formatMapPercent(detectorModelInfo?.map5095)
|
||||||
@ -2683,16 +2690,6 @@ function TrainingStatsModal(props: {
|
|||||||
return parts.join(' · ')
|
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 = [
|
const modelCards = [
|
||||||
{
|
{
|
||||||
key: 'detector',
|
key: 'detector',
|
||||||
@ -2706,6 +2703,12 @@ function TrainingStatsModal(props: {
|
|||||||
available: poseModelAvailable,
|
available: poseModelAvailable,
|
||||||
info: poseModelInfo,
|
info: poseModelInfo,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'videomae',
|
||||||
|
title: 'VideoMAE',
|
||||||
|
available: videoMAEModelAvailable,
|
||||||
|
info: videoMAEModelInfo,
|
||||||
|
},
|
||||||
].map((model) => ({
|
].map((model) => ({
|
||||||
...model,
|
...model,
|
||||||
trainedAtLabel: formatModelTrainedAt(model.info),
|
trainedAtLabel: formatModelTrainedAt(model.info),
|
||||||
@ -2716,6 +2719,17 @@ function TrainingStatsModal(props: {
|
|||||||
? modelInfoDetails
|
? modelInfoDetails
|
||||||
: formatTrainingModelDetails(model.info),
|
: 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 ?? []
|
const history = props.history ?? []
|
||||||
|
|
||||||
@ -2893,7 +2907,7 @@ function TrainingStatsModal(props: {
|
|||||||
</div>
|
</div>
|
||||||
</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) => (
|
{modelCards.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.key}
|
key={model.key}
|
||||||
@ -3037,7 +3051,7 @@ function TrainingStatsModal(props: {
|
|||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
<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">
|
<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>
|
</div>
|
||||||
|
|
||||||
{modelCards.map((model) => (
|
{modelCards.map((model) => (
|
||||||
@ -3295,6 +3309,39 @@ function TrainingStatsModal(props: {
|
|||||||
total={activeTabItem.total}
|
total={activeTabItem.total}
|
||||||
confidence={activeTabItem.confidence}
|
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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -5262,6 +5309,12 @@ export default function TrainingTab(props: {
|
|||||||
),
|
),
|
||||||
poseModelAvailable: Boolean(data?.poseModelAvailable),
|
poseModelAvailable: Boolean(data?.poseModelAvailable),
|
||||||
poseModelInfo: parseTrainingModelInfo(data?.poseModelInfo),
|
poseModelInfo: parseTrainingModelInfo(data?.poseModelInfo),
|
||||||
|
videoMAEModelAvailable: Boolean(
|
||||||
|
data?.videoMAEModelAvailable ?? data?.videomaeModelAvailable
|
||||||
|
),
|
||||||
|
videoMAEModelInfo: parseTrainingModelInfo(
|
||||||
|
data?.videoMAEModelInfo ?? data?.videomaeModelInfo
|
||||||
|
),
|
||||||
confidence: data?.confidence,
|
confidence: data?.confidence,
|
||||||
labels: {
|
labels: {
|
||||||
people: Array.isArray(data?.labels?.people) ? data.labels.people : [],
|
people: Array.isArray(data?.labels?.people) ? data.labels.people : [],
|
||||||
@ -6355,12 +6408,15 @@ export default function TrainingTab(props: {
|
|||||||
|
|
||||||
await loadTrainingStatus()
|
await loadTrainingStatus()
|
||||||
await loadNext({ forceNew: true, preserveNotice: true })
|
await loadNext({ forceNew: true, preserveNotice: true })
|
||||||
|
if (statsModalOpen) {
|
||||||
|
await loadTrainingStats()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : String(e))
|
setError(e instanceof Error ? e.message : String(e))
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingTrainingData(false)
|
setDeletingTrainingData(false)
|
||||||
}
|
}
|
||||||
}, [loadNext, loadTrainingStatus, requiredCount])
|
}, [loadNext, loadTrainingStats, loadTrainingStatus, requiredCount, statsModalOpen])
|
||||||
|
|
||||||
const getPointerPosFromRect = useCallback((
|
const getPointerPosFromRect = useCallback((
|
||||||
rect: ImageContentRect,
|
rect: ImageContentRect,
|
||||||
@ -7562,7 +7618,10 @@ export default function TrainingTab(props: {
|
|||||||
onClick={() => setFeedbackModalOpen(true)}
|
onClick={() => setFeedbackModalOpen(true)}
|
||||||
title="Bisher abgegebenes Feedback ansehen."
|
title="Bisher 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
|
Abgegebenes Feedback ansehen
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -7608,22 +7667,6 @@ export default function TrainingTab(props: {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -9569,7 +9612,10 @@ export default function TrainingTab(props: {
|
|||||||
disabled={!canConfirmTrainingStart || trainingRunning}
|
disabled={!canConfirmTrainingStart || trainingRunning}
|
||||||
onClick={startTrainingFromModal}
|
onClick={startTrainingFromModal}
|
||||||
>
|
>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<BoltIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
Training starten
|
Training starten
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@ -9741,6 +9787,9 @@ export default function TrainingTab(props: {
|
|||||||
error={trainingStatsError}
|
error={trainingStatsError}
|
||||||
feedbackCount={feedbackCount}
|
feedbackCount={feedbackCount}
|
||||||
requiredCount={requiredCount}
|
requiredCount={requiredCount}
|
||||||
|
deletingTrainingData={deletingTrainingData}
|
||||||
|
deleteTrainingDataDisabled={uiLocked}
|
||||||
|
onDeleteTrainingData={deleteAllTrainingData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TrainingFeedbackHistoryModal
|
<TrainingFeedbackHistoryModal
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user