bugfixes
This commit is contained in:
parent
4484fb978d
commit
d538a664cf
@ -248,7 +248,10 @@ _POSE_RELIABLE_MIN_QUALITY = float(os.environ.get("YOLO_POSE_RELIABLE_MIN_QUALIT
|
|||||||
_POSITION_CONTEXT_MIN_SCORE = float(os.environ.get("YOLO_POSITION_CONTEXT_MIN_SCORE", "0.22"))
|
_POSITION_CONTEXT_MIN_SCORE = float(os.environ.get("YOLO_POSITION_CONTEXT_MIN_SCORE", "0.22"))
|
||||||
_POSITION_CONTEXT_MAX_SCORE = float(os.environ.get("YOLO_POSITION_CONTEXT_MAX_SCORE", "0.44"))
|
_POSITION_CONTEXT_MAX_SCORE = float(os.environ.get("YOLO_POSITION_CONTEXT_MAX_SCORE", "0.44"))
|
||||||
_POSITION_CONTEXT_BOOST_WEIGHT = float(os.environ.get("YOLO_POSITION_CONTEXT_BOOST_WEIGHT", "0.60"))
|
_POSITION_CONTEXT_BOOST_WEIGHT = float(os.environ.get("YOLO_POSITION_CONTEXT_BOOST_WEIGHT", "0.60"))
|
||||||
_POSITION_CONTEXT_OVERRIDE_MARGIN = float(os.environ.get("YOLO_POSITION_CONTEXT_OVERRIDE_MARGIN", "0.16"))
|
_POSE_CONFIRMING_CONTEXT_MIN_SCORE = float(os.environ.get("YOLO_POSE_CONFIRMING_CONTEXT_MIN_SCORE", "0.14"))
|
||||||
|
_POSE_UNCONFIRMED_MAX_SCORE = float(os.environ.get("YOLO_POSE_UNCONFIRMED_MAX_SCORE", "0.38"))
|
||||||
|
_POSE_STRONG_UNCONFIRMED_MIN_SCORE = float(os.environ.get("YOLO_POSE_STRONG_UNCONFIRMED_MIN_SCORE", "0.70"))
|
||||||
|
_POSE_STRONG_UNCONFIRMED_MAX_SCORE = float(os.environ.get("YOLO_POSE_STRONG_UNCONFIRMED_MAX_SCORE", "0.46"))
|
||||||
_BATCH = int(os.environ.get("YOLO_BATCH", "16"))
|
_BATCH = int(os.environ.get("YOLO_BATCH", "16"))
|
||||||
_IMGSZ = int(os.environ.get("YOLO_IMGSZ", "640"))
|
_IMGSZ = int(os.environ.get("YOLO_IMGSZ", "640"))
|
||||||
_HALF = os.environ.get("YOLO_HALF", "0").lower() in {"1", "true", "yes", "on"}
|
_HALF = os.environ.get("YOLO_HALF", "0").lower() in {"1", "true", "yes", "on"}
|
||||||
@ -919,6 +922,24 @@ def box_horizontal_overlap_ratio(a: dict, b: dict) -> float:
|
|||||||
return clamp01((right - left) / min_width)
|
return clamp01((right - left) / min_width)
|
||||||
|
|
||||||
|
|
||||||
|
def box_vertical_overlap_ratio(a: dict, b: dict) -> float:
|
||||||
|
a = normalized_box(a)
|
||||||
|
b = normalized_box(b)
|
||||||
|
if not a or not b:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
top = max(float(a["y"]), float(b["y"]))
|
||||||
|
bottom = min(float(a["y"]) + float(a["h"]), float(b["y"]) + float(b["h"]))
|
||||||
|
if bottom <= top:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
min_height = min(float(a["h"]), float(b["h"]))
|
||||||
|
if min_height <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return clamp01((bottom - top) / min_height)
|
||||||
|
|
||||||
|
|
||||||
def is_person_like_label(label: str) -> bool:
|
def is_person_like_label(label: str) -> bool:
|
||||||
clean = str(label or "").strip().lower()
|
clean = str(label or "").strip().lower()
|
||||||
return clean == "person" or clean.startswith("person_")
|
return clean == "person" or clean.startswith("person_")
|
||||||
@ -1170,6 +1191,49 @@ def point_distance(a: tuple[float, float] | None, b: tuple[float, float] | None)
|
|||||||
return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
|
return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def projected_point_distance(
|
||||||
|
a: tuple[float, float] | None,
|
||||||
|
b: tuple[float, float] | None,
|
||||||
|
axis_x: float,
|
||||||
|
axis_y: float,
|
||||||
|
) -> float:
|
||||||
|
if not a or not b:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return abs((a[0] - b[0]) * axis_x + (a[1] - b[1]) * axis_y)
|
||||||
|
|
||||||
|
|
||||||
|
def pose_extents_along_axis(
|
||||||
|
person: dict,
|
||||||
|
origin: tuple[float, float],
|
||||||
|
axis_x: float,
|
||||||
|
axis_y: float,
|
||||||
|
perp_x: float,
|
||||||
|
perp_y: float,
|
||||||
|
min_conf: float = 0.20,
|
||||||
|
) -> tuple[float, float] | None:
|
||||||
|
long_values: list[float] = []
|
||||||
|
cross_values: list[float] = []
|
||||||
|
|
||||||
|
for point in person.get("keypoints", []) or []:
|
||||||
|
conf = float(point.get("conf") or 0.0)
|
||||||
|
x = float(point.get("x") or 0.0)
|
||||||
|
y = float(point.get("y") or 0.0)
|
||||||
|
|
||||||
|
if conf < min_conf or not is_finite01(x) or not is_finite01(y):
|
||||||
|
continue
|
||||||
|
|
||||||
|
dx = x - origin[0]
|
||||||
|
dy = y - origin[1]
|
||||||
|
long_values.append(dx * axis_x + dy * axis_y)
|
||||||
|
cross_values.append(dx * perp_x + dy * perp_y)
|
||||||
|
|
||||||
|
if len(long_values) < 3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return abs(max(long_values) - min(long_values)), abs(max(cross_values) - min(cross_values))
|
||||||
|
|
||||||
|
|
||||||
def pose_person_geometry(person: dict) -> dict:
|
def pose_person_geometry(person: dict) -> dict:
|
||||||
box = normalized_box(person.get("box"))
|
box = normalized_box(person.get("box"))
|
||||||
box_center_point = box_center(box) if box else None
|
box_center_point = box_center(box) if box else None
|
||||||
@ -1189,30 +1253,74 @@ def pose_person_geometry(person: dict) -> dict:
|
|||||||
torso_dx = 0.0
|
torso_dx = 0.0
|
||||||
torso_dy = 0.0
|
torso_dy = 0.0
|
||||||
torso_len = 0.0
|
torso_len = 0.0
|
||||||
|
torso_angle = 0.0
|
||||||
|
has_torso_axis = False
|
||||||
|
axis_x = 0.0
|
||||||
|
axis_y = 1.0
|
||||||
|
perp_x = -1.0
|
||||||
|
perp_y = 0.0
|
||||||
if hip and shoulder:
|
if hip and shoulder:
|
||||||
torso_dx = abs(hip[0] - shoulder[0])
|
raw_dx = hip[0] - shoulder[0]
|
||||||
torso_dy = abs(hip[1] - shoulder[1])
|
raw_dy = hip[1] - shoulder[1]
|
||||||
torso_len = math.sqrt(torso_dx * torso_dx + torso_dy * torso_dy)
|
torso_dx = abs(raw_dx)
|
||||||
|
torso_dy = abs(raw_dy)
|
||||||
|
torso_len = math.sqrt(raw_dx * raw_dx + raw_dy * raw_dy)
|
||||||
|
if torso_len >= 0.07:
|
||||||
|
has_torso_axis = True
|
||||||
|
axis_x = raw_dx / torso_len
|
||||||
|
axis_y = raw_dy / torso_len
|
||||||
|
perp_x = -axis_y
|
||||||
|
perp_y = axis_x
|
||||||
|
torso_angle = math.atan2(axis_y, axis_x)
|
||||||
|
|
||||||
hip_width = point_distance(left_hip, right_hip)
|
hip_width = point_distance(left_hip, right_hip)
|
||||||
knee_width = point_distance(left_knee, right_knee)
|
knee_width = point_distance(left_knee, right_knee)
|
||||||
|
|
||||||
knees_below_hips = bool(knee and hip and knee[1] > hip[1] + 0.045)
|
knees_below_hips = bool(knee and hip and knee[1] > hip[1] + 0.045)
|
||||||
|
if has_torso_axis and knee and hip:
|
||||||
|
knee_projection = (knee[0] - hip[0]) * axis_x + (knee[1] - hip[1]) * axis_y
|
||||||
|
knees_below_hips = knee_projection > 0.045
|
||||||
|
|
||||||
|
if has_torso_axis and left_knee and right_knee:
|
||||||
|
knee_width = projected_point_distance(left_knee, right_knee, perp_x, perp_y)
|
||||||
|
if left_hip and right_hip:
|
||||||
|
hip_width = projected_point_distance(left_hip, right_hip, perp_x, perp_y)
|
||||||
|
|
||||||
knees_wide = bool(
|
knees_wide = bool(
|
||||||
knee_width > 0
|
knee_width > 0
|
||||||
and knee_width >= max(0.11, hip_width * 1.12)
|
and knee_width >= max(0.11, hip_width * 1.12)
|
||||||
)
|
)
|
||||||
straddling = knees_below_hips and knees_wide
|
straddling = knees_below_hips and knees_wide
|
||||||
|
|
||||||
|
body_long = torso_len
|
||||||
|
body_cross = max(hip_width, knee_width)
|
||||||
|
|
||||||
|
if has_torso_axis:
|
||||||
|
pose_extents = pose_extents_along_axis(person, center, axis_x, axis_y, perp_x, perp_y)
|
||||||
|
if pose_extents:
|
||||||
|
body_long = max(body_long, pose_extents[0])
|
||||||
|
body_cross = max(body_cross, pose_extents[1])
|
||||||
|
|
||||||
|
if box:
|
||||||
|
box_long = abs(axis_x) * float(box["w"]) + abs(axis_y) * float(box["h"])
|
||||||
|
box_cross = abs(perp_x) * float(box["w"]) + abs(perp_y) * float(box["h"])
|
||||||
|
body_long = max(body_long, box_long)
|
||||||
|
body_cross = max(body_cross, box_cross)
|
||||||
|
elif box:
|
||||||
|
body_long = max(float(box["w"]), float(box["h"]))
|
||||||
|
body_cross = min(float(box["w"]), float(box["h"]))
|
||||||
|
|
||||||
|
elongated = body_long >= max(0.18, body_cross * 1.18)
|
||||||
|
|
||||||
torso_horizontal = torso_len >= 0.07 and torso_dx >= torso_dy * 1.15
|
torso_horizontal = torso_len >= 0.07 and torso_dx >= torso_dy * 1.15
|
||||||
torso_vertical = torso_len >= 0.07 and torso_dy >= torso_dx * 1.15
|
torso_vertical = torso_len >= 0.07 and torso_dy >= torso_dx * 1.15
|
||||||
|
|
||||||
box_horizontal = bool(box and float(box["w"]) >= float(box["h"]) * 1.05)
|
box_horizontal = bool(box and float(box["w"]) >= float(box["h"]) * 1.05)
|
||||||
box_vertical = bool(box and float(box["h"]) >= float(box["w"]) * 1.25)
|
box_vertical = bool(box and float(box["h"]) >= float(box["w"]) * 1.25)
|
||||||
|
|
||||||
lying = torso_horizontal or box_horizontal
|
lying = torso_horizontal or box_horizontal or (has_torso_axis and elongated and not box_vertical)
|
||||||
upright = torso_vertical or box_vertical
|
upright = torso_vertical or box_vertical
|
||||||
all_fours = torso_horizontal and knees_below_hips
|
all_fours = knees_below_hips and not straddling and (torso_horizontal or (has_torso_axis and elongated))
|
||||||
bent_or_kneeling = all_fours or (knees_below_hips and not straddling)
|
bent_or_kneeling = all_fours or (knees_below_hips and not straddling)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1222,6 +1330,15 @@ def pose_person_geometry(person: dict) -> dict:
|
|||||||
"hip": hip,
|
"hip": hip,
|
||||||
"shoulder": shoulder,
|
"shoulder": shoulder,
|
||||||
"knee": knee,
|
"knee": knee,
|
||||||
|
"torso_angle": torso_angle,
|
||||||
|
"has_torso_axis": has_torso_axis,
|
||||||
|
"axis_x": axis_x,
|
||||||
|
"axis_y": axis_y,
|
||||||
|
"perp_x": perp_x,
|
||||||
|
"perp_y": perp_y,
|
||||||
|
"body_long": body_long,
|
||||||
|
"body_cross": body_cross,
|
||||||
|
"elongated": elongated,
|
||||||
"lying": lying,
|
"lying": lying,
|
||||||
"upright": upright,
|
"upright": upright,
|
||||||
"straddling": straddling,
|
"straddling": straddling,
|
||||||
@ -1242,6 +1359,14 @@ def combine_position_score(scores: dict[str, float], label: str, score: float) -
|
|||||||
scores[label] = clamp01(1 - (1 - current) * (1 - score))
|
scores[label] = clamp01(1 - (1 - current) * (1 - score))
|
||||||
|
|
||||||
|
|
||||||
|
def pose_axis_alignment(a: dict, b: dict) -> tuple[float, bool]:
|
||||||
|
if not a.get("has_torso_axis") or not b.get("has_torso_axis"):
|
||||||
|
return 0.0, False
|
||||||
|
|
||||||
|
# Körperachsen sind richtungslos: 0° und 180° gelten beide als parallel.
|
||||||
|
return abs(math.cos(float(a.get("torso_angle") or 0.0) - float(b.get("torso_angle") or 0.0))), True
|
||||||
|
|
||||||
|
|
||||||
def append_prediction_source(prediction: dict, source: str) -> None:
|
def append_prediction_source(prediction: dict, source: str) -> None:
|
||||||
source = str(source or "").strip()
|
source = str(source or "").strip()
|
||||||
if not source:
|
if not source:
|
||||||
@ -1272,9 +1397,6 @@ def fuse_hybrid_position_scores(
|
|||||||
best_score = 0.0
|
best_score = 0.0
|
||||||
best_has_pose = False
|
best_has_pose = False
|
||||||
best_has_context = False
|
best_has_context = False
|
||||||
best_pose_position = ""
|
|
||||||
best_pose_score = 0.0
|
|
||||||
best_pose_has_context = False
|
|
||||||
|
|
||||||
for label in labels:
|
for label in labels:
|
||||||
pose_score = clamp01(pose_scores.get(label, 0.0))
|
pose_score = clamp01(pose_scores.get(label, 0.0))
|
||||||
@ -1284,10 +1406,15 @@ def fuse_hybrid_position_scores(
|
|||||||
|
|
||||||
score = 0.0
|
score = 0.0
|
||||||
if has_pose:
|
if has_pose:
|
||||||
score = pose_score
|
if has_context and context_score >= _POSE_CONFIRMING_CONTEXT_MIN_SCORE:
|
||||||
if has_context:
|
score = pose_score
|
||||||
boost = clamp01(context_score * _POSITION_CONTEXT_BOOST_WEIGHT)
|
boost = clamp01(context_score * _POSITION_CONTEXT_BOOST_WEIGHT)
|
||||||
score = clamp01(1 - (1 - score) * (1 - boost))
|
score = clamp01(1 - (1 - score) * (1 - boost))
|
||||||
|
else:
|
||||||
|
max_unconfirmed_score = _POSE_UNCONFIRMED_MAX_SCORE
|
||||||
|
if pose_score >= _POSE_STRONG_UNCONFIRMED_MIN_SCORE:
|
||||||
|
max_unconfirmed_score = _POSE_STRONG_UNCONFIRMED_MAX_SCORE
|
||||||
|
score = min(max_unconfirmed_score, pose_score)
|
||||||
elif context_score >= _POSITION_CONTEXT_MIN_SCORE:
|
elif context_score >= _POSITION_CONTEXT_MIN_SCORE:
|
||||||
score = min(_POSITION_CONTEXT_MAX_SCORE, context_score)
|
score = min(_POSITION_CONTEXT_MAX_SCORE, context_score)
|
||||||
|
|
||||||
@ -1297,21 +1424,6 @@ def fuse_hybrid_position_scores(
|
|||||||
best_has_pose = has_pose
|
best_has_pose = has_pose
|
||||||
best_has_context = has_context
|
best_has_context = has_context
|
||||||
|
|
||||||
if has_pose and score > best_pose_score:
|
|
||||||
best_pose_position = label
|
|
||||||
best_pose_score = score
|
|
||||||
best_pose_has_context = has_context
|
|
||||||
|
|
||||||
if (
|
|
||||||
best_pose_position
|
|
||||||
and not best_has_pose
|
|
||||||
and best_score <= best_pose_score + _POSITION_CONTEXT_OVERRIDE_MARGIN
|
|
||||||
):
|
|
||||||
best_position = best_pose_position
|
|
||||||
best_score = best_pose_score
|
|
||||||
best_has_pose = True
|
|
||||||
best_has_context = best_pose_has_context
|
|
||||||
|
|
||||||
return best_position, clamp01(best_score), best_has_pose, best_has_context
|
return best_position, clamp01(best_score), best_has_pose, best_has_context
|
||||||
|
|
||||||
|
|
||||||
@ -1338,6 +1450,7 @@ def add_pose_pair_geometry_scores(scores: dict[str, float], persons: list[dict])
|
|||||||
gap = box_gap(left_box, right_box)
|
gap = box_gap(left_box, right_box)
|
||||||
overlap = box_overlap_ratio(left_box, right_box)
|
overlap = box_overlap_ratio(left_box, right_box)
|
||||||
horizontal_overlap = box_horizontal_overlap_ratio(left_box, right_box)
|
horizontal_overlap = box_horizontal_overlap_ratio(left_box, right_box)
|
||||||
|
vertical_overlap = box_vertical_overlap_ratio(left_box, right_box)
|
||||||
close = gap <= 0.12 or overlap >= 0.08
|
close = gap <= 0.12 or overlap >= 0.08
|
||||||
|
|
||||||
if not close:
|
if not close:
|
||||||
@ -1349,34 +1462,79 @@ def add_pose_pair_geometry_scores(scores: dict[str, float], persons: list[dict])
|
|||||||
bottom = right if top is left else left
|
bottom = right if top is left else left
|
||||||
|
|
||||||
top_above = top["center"][1] <= bottom["center"][1] - 0.055
|
top_above = top["center"][1] <= bottom["center"][1] - 0.055
|
||||||
strong_stack = horizontal_overlap >= 0.35 and (top_above or overlap >= 0.22)
|
horizontal_stack = horizontal_overlap >= 0.35 and (top_above or overlap >= 0.22)
|
||||||
top_has_rider_shape = (
|
lateral_stack = vertical_overlap >= 0.35 and overlap >= 0.12
|
||||||
top["straddling"]
|
strong_stack = horizontal_stack or lateral_stack
|
||||||
or top["knees_wide"]
|
|
||||||
or (top["upright"] and top["knees_below_hips"])
|
|
||||||
)
|
|
||||||
|
|
||||||
if strong_stack and top_has_rider_shape:
|
axis_alignment, has_axis_alignment = pose_axis_alignment(left, right)
|
||||||
|
axes_parallel = has_axis_alignment and axis_alignment >= 0.74
|
||||||
|
axes_crossed = has_axis_alignment and axis_alignment <= 0.56
|
||||||
|
|
||||||
|
def has_strong_rider_shape(geometry: dict) -> bool:
|
||||||
|
return bool(
|
||||||
|
geometry["straddling"]
|
||||||
|
or (
|
||||||
|
geometry["knees_wide"]
|
||||||
|
and geometry["knees_below_hips"]
|
||||||
|
and (geometry["upright"] or axes_crossed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_weak_rider_shape(geometry: dict) -> bool:
|
||||||
|
return bool(geometry["knees_wide"] and geometry["knees_below_hips"])
|
||||||
|
|
||||||
|
left_has_strong_rider_shape = has_strong_rider_shape(left)
|
||||||
|
right_has_strong_rider_shape = has_strong_rider_shape(right)
|
||||||
|
top_has_strong_rider_shape = has_strong_rider_shape(top)
|
||||||
|
top_has_weak_rider_shape = has_weak_rider_shape(top)
|
||||||
|
|
||||||
|
rider = top
|
||||||
|
base = bottom
|
||||||
|
rider_has_strong_shape = top_has_strong_rider_shape
|
||||||
|
rider_has_weak_shape = top_has_weak_rider_shape
|
||||||
|
if left_has_strong_rider_shape != right_has_strong_rider_shape:
|
||||||
|
if left_has_strong_rider_shape:
|
||||||
|
rider = left
|
||||||
|
base = right
|
||||||
|
rider_has_strong_shape = True
|
||||||
|
rider_has_weak_shape = has_weak_rider_shape(left)
|
||||||
|
else:
|
||||||
|
rider = right
|
||||||
|
base = left
|
||||||
|
rider_has_strong_shape = True
|
||||||
|
rider_has_weak_shape = has_weak_rider_shape(right)
|
||||||
|
|
||||||
|
if strong_stack and rider_has_strong_shape and not axes_parallel:
|
||||||
add("cowgirl", 0.20)
|
add("cowgirl", 0.20)
|
||||||
add("reverse_cowgirl", 0.17)
|
add("reverse_cowgirl", 0.17)
|
||||||
|
|
||||||
if bottom["lying"]:
|
if base["lying"]:
|
||||||
add("cowgirl", 0.12)
|
add("cowgirl", 0.12)
|
||||||
add("reverse_cowgirl", 0.10)
|
add("reverse_cowgirl", 0.10)
|
||||||
if top["straddling"]:
|
if rider["straddling"]:
|
||||||
add("cowgirl", 0.08)
|
add("cowgirl", 0.08)
|
||||||
add("reverse_cowgirl", 0.06)
|
add("reverse_cowgirl", 0.06)
|
||||||
|
elif strong_stack and rider_has_weak_shape and not has_axis_alignment:
|
||||||
|
# Ohne verwertbare Achsen bleibt Cowgirl nur ein schwaches Signal.
|
||||||
|
# Sobald die Achsen parallel sind, sieht die Szene eher nach
|
||||||
|
# Missionary/Überlagerung aus und soll nicht in Cowgirl kippen.
|
||||||
|
add("cowgirl", 0.08)
|
||||||
|
add("reverse_cowgirl", 0.06)
|
||||||
|
|
||||||
if strong_stack and bottom["lying"]:
|
if strong_stack and (bottom["lying"] or (axes_parallel and top_above)):
|
||||||
add("missionary", 0.14)
|
if not top_has_strong_rider_shape or axes_parallel:
|
||||||
if not top["straddling"]:
|
add("missionary", 0.14)
|
||||||
|
if axes_parallel:
|
||||||
|
add("missionary", 0.08)
|
||||||
|
if not top["straddling"] and not top_has_strong_rider_shape:
|
||||||
add("missionary", 0.08)
|
add("missionary", 0.08)
|
||||||
|
|
||||||
both_lying = left["lying"] and right["lying"]
|
both_lying = left["lying"] and right["lying"]
|
||||||
same_level = abs(left_center[1] - right_center[1]) <= 0.15
|
same_level = abs(left_center[1] - right_center[1]) <= 0.15
|
||||||
side_by_side = abs(left_center[0] - right_center[0]) >= 0.10
|
side_by_side = abs(left_center[0] - right_center[0]) >= 0.10
|
||||||
|
|
||||||
if both_lying and (same_level or side_by_side):
|
parallel_side_by_side = axes_parallel and left["elongated"] and right["elongated"] and close and not strong_stack
|
||||||
|
if (both_lying and (same_level or side_by_side)) or parallel_side_by_side:
|
||||||
add("spooning", 0.18)
|
add("spooning", 0.18)
|
||||||
if overlap >= 0.10:
|
if overlap >= 0.10:
|
||||||
add("prone_bone", 0.07)
|
add("prone_bone", 0.07)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -115,12 +115,16 @@ func buildAnalyzeVideoMAEClips(
|
|||||||
func predictVideoMAEPositionClipsForAnalyze(
|
func predictVideoMAEPositionClipsForAnalyze(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clips []analyzeVideoMAEClipReqItem,
|
clips []analyzeVideoMAEClipReqItem,
|
||||||
|
onProgress func(current int, total int),
|
||||||
) ([]analyzeVideoMAEClipPrediction, error) {
|
) ([]analyzeVideoMAEClipPrediction, error) {
|
||||||
if len(clips) == 0 {
|
if len(clips) == 0 {
|
||||||
return []analyzeVideoMAEClipPrediction{}, nil
|
return []analyzeVideoMAEClipPrediction{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !trainingRecognitionEnabled() {
|
if !trainingRecognitionEnabled() {
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(len(clips), len(clips))
|
||||||
|
}
|
||||||
return []analyzeVideoMAEClipPrediction{}, nil
|
return []analyzeVideoMAEClipPrediction{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,10 +196,16 @@ func predictVideoMAEPositionClipsForAnalyze(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !parsed.Available {
|
if !parsed.Available {
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(len(clips), len(clips))
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out = append(out, parsed.Predictions...)
|
out = append(out, parsed.Predictions...)
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(end, len(clips))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
@ -207,6 +217,7 @@ func applyVideoMAEPositionClipsForAnalyze(
|
|||||||
duration float64,
|
duration float64,
|
||||||
highlightHits []analyzeHit,
|
highlightHits []analyzeHit,
|
||||||
positionEvidence []analyzePositionEvidence,
|
positionEvidence []analyzePositionEvidence,
|
||||||
|
onProgress func(current int, total int),
|
||||||
) ([]analyzeHit, []analyzePositionEvidence) {
|
) ([]analyzeHit, []analyzePositionEvidence) {
|
||||||
if !analyzeVideoMAEEnabled() {
|
if !analyzeVideoMAEEnabled() {
|
||||||
return highlightHits, positionEvidence
|
return highlightHits, positionEvidence
|
||||||
@ -217,9 +228,16 @@ func applyVideoMAEPositionClipsForAnalyze(
|
|||||||
return highlightHits, positionEvidence
|
return highlightHits, positionEvidence
|
||||||
}
|
}
|
||||||
|
|
||||||
predictions, err := predictVideoMAEPositionClipsForAnalyze(ctx, clips)
|
if onProgress != nil {
|
||||||
|
onProgress(0, len(clips))
|
||||||
|
}
|
||||||
|
|
||||||
|
predictions, err := predictVideoMAEPositionClipsForAnalyze(ctx, clips, onProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() == nil {
|
if ctx.Err() == nil {
|
||||||
|
if onProgress != nil {
|
||||||
|
onProgress(len(clips), len(clips))
|
||||||
|
}
|
||||||
appLogln("VideoMAE Clip-Analyse übersprungen:", err)
|
appLogln("VideoMAE Clip-Analyse übersprungen:", err)
|
||||||
}
|
}
|
||||||
return highlightHits, positionEvidence
|
return highlightHits, positionEvidence
|
||||||
@ -227,7 +245,7 @@ func applyVideoMAEPositionClipsForAnalyze(
|
|||||||
|
|
||||||
for _, pred := range predictions {
|
for _, pred := range predictions {
|
||||||
label := strings.ToLower(strings.TrimSpace(pred.SexPosition))
|
label := strings.ToLower(strings.TrimSpace(pred.SexPosition))
|
||||||
if isNoSexPositionLabel(label) || !isKnownPositionLabel(label) {
|
if !isAnalyzeTimelinePositionLabel(label) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,16 +268,10 @@ func applyVideoMAEPositionClipsForAnalyze(
|
|||||||
source = "videomae"
|
source = "videomae"
|
||||||
}
|
}
|
||||||
|
|
||||||
highlightHits = append(highlightHits, analyzeHit{
|
|
||||||
Time: pred.Time,
|
|
||||||
Label: "position:" + label,
|
|
||||||
Score: score,
|
|
||||||
Start: start,
|
|
||||||
End: end,
|
|
||||||
})
|
|
||||||
|
|
||||||
positionEvidence = append(positionEvidence, analyzePositionEvidence{
|
positionEvidence = append(positionEvidence, analyzePositionEvidence{
|
||||||
Time: pred.Time,
|
Time: pred.Time,
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
Label: label,
|
Label: label,
|
||||||
Score: score,
|
Score: score,
|
||||||
Source: source,
|
Source: source,
|
||||||
|
|||||||
@ -172,15 +172,17 @@ func ensureAnalyzeAllGoalsForVideoCtxForce(
|
|||||||
return false, skipAnalysisBecauseBadSpriteError(reason)
|
return false, skipAnalysisBecauseBadSpriteError(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
hits, analyzeStartedAtMs, analyzeTotalFrames, err := analyzeVideoFromFrames(actx, videoPath)
|
hits, analyzeStartedAtMs, _, err := analyzeVideoFromFrames(actx, videoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer publishAnalysisFinished(analyzeStartedAtMs, analyzeTotalFrames, filepath.Base(videoPath), "Analyse abgeschlossen")
|
analyzeFile := filepath.Base(videoPath)
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs, analyzeFile, 0.1, "")
|
||||||
|
|
||||||
segments := buildAnalyzeSegmentsForGoal(hits, durationSec)
|
segments := buildAnalyzeSegmentsForGoal(hits, durationSec)
|
||||||
segments = prepareAIRatingSegments(segments)
|
segments = prepareAIRatingSegments(segments)
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs, analyzeFile, 0.45, "")
|
||||||
|
|
||||||
rating := computeHighlightRatingForVideo(segments, durationSec, videoPath)
|
rating := computeHighlightRatingForVideo(segments, durationSec, videoPath)
|
||||||
|
|
||||||
@ -195,17 +197,22 @@ func ensureAnalyzeAllGoalsForVideoCtxForce(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := writeVideoAIForFile(actx, videoPath, meta.sourceURL, ai); err != nil {
|
if err := writeVideoAIForFile(actx, videoPath, meta.sourceURL, ai); err != nil {
|
||||||
|
publishAnalysisError(analyzeStartedAtMs, analyzeFile, "Speichern fehlgeschlagen", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
analysisReady := hasAIAnalysisForAllOutputGoals(videoPath, requiredAnalyzeGoals())
|
analysisReady := hasAIAnalysisForAllOutputGoals(videoPath, requiredAnalyzeGoals())
|
||||||
if !analysisReady {
|
if !analysisReady {
|
||||||
return false, nil
|
err := appErrorf("analyse-meta konnte nicht verifiziert werden")
|
||||||
|
publishAnalysisError(analyzeStartedAtMs, analyzeFile, "Analyse fehlgeschlagen", err)
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direkt nach erfolgreicher Analyse löschen.
|
// Direkt nach erfolgreicher Analyse löschen.
|
||||||
// Wichtig: hier rating übergeben, nicht nil.
|
// Wichtig: hier rating übergeben, nicht nil.
|
||||||
autoDeleteLowRatedDownloadAfterAnalysis(actx, videoPath, rating)
|
autoDeleteLowRatedDownloadAfterAnalysis(actx, videoPath, rating)
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs, analyzeFile, 1, "")
|
||||||
|
publishAnalysisFinished(analyzeStartedAtMs, analyzeProgressTotal, analyzeFile, "Analyse abgeschlossen")
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@ -1587,15 +1594,17 @@ func prepareVideoForSplit(ctx context.Context, videoPath, sourceURL, goal string
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hits, analyzeStartedAtMs2, analyzeTotalFrames2, aerr := analyzeVideoFromFrames(ctx, videoPath)
|
hits, analyzeStartedAtMs2, _, aerr := analyzeVideoFromFrames(ctx, videoPath)
|
||||||
if aerr != nil {
|
if aerr != nil {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer publishAnalysisFinished(analyzeStartedAtMs2, analyzeTotalFrames2, filepath.Base(videoPath), "Analyse abgeschlossen")
|
analyzeFile2 := filepath.Base(videoPath)
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs2, analyzeFile2, 0.1, "")
|
||||||
|
|
||||||
segments := buildAnalyzeSegmentsForGoal(hits, durationSec)
|
segments := buildAnalyzeSegmentsForGoal(hits, durationSec)
|
||||||
segments = prepareAIRatingSegments(segments)
|
segments = prepareAIRatingSegments(segments)
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs2, analyzeFile2, 0.45, "")
|
||||||
|
|
||||||
rating := computeHighlightRatingForVideo(segments, durationSec, videoPath)
|
rating := computeHighlightRatingForVideo(segments, durationSec, videoPath)
|
||||||
|
|
||||||
@ -1609,9 +1618,21 @@ func prepareVideoForSplit(ctx context.Context, videoPath, sourceURL, goal string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if werr := writeVideoAIForFile(ctx, videoPath, sourceURL, ai); werr != nil {
|
if werr := writeVideoAIForFile(ctx, videoPath, sourceURL, ai); werr != nil {
|
||||||
|
publishAnalysisError(analyzeStartedAtMs2, analyzeFile2, "Speichern fehlgeschlagen", werr)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out.AnalyzeReady = hasAIAnalysisForOutputGoal(videoPath, "highlights")
|
out.AnalyzeReady = hasAIAnalysisForOutputGoal(videoPath, "highlights")
|
||||||
|
if out.AnalyzeReady {
|
||||||
|
publishAnalyzePersistProgress(analyzeStartedAtMs2, analyzeFile2, 1, "")
|
||||||
|
publishAnalysisFinished(analyzeStartedAtMs2, analyzeProgressTotal, analyzeFile2, "Analyse abgeschlossen")
|
||||||
|
} else {
|
||||||
|
publishAnalysisError(
|
||||||
|
analyzeStartedAtMs2,
|
||||||
|
analyzeFile2,
|
||||||
|
"Analyse fehlgeschlagen",
|
||||||
|
appErrorf("analyse-meta konnte nicht verifiziert werden"),
|
||||||
|
)
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
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.
@ -91,6 +91,14 @@ func resolveMyFreeCamsURL(m WatchedModelLite) string {
|
|||||||
return fmt.Sprintf("https://www.myfreecams.com/#%s", user)
|
return fmt.Sprintf("https://www.myfreecams.com/#%s", user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func myFreeCamsAutostartCanRun() bool {
|
||||||
|
s := getSettings()
|
||||||
|
return s.UseProviderAPIs &&
|
||||||
|
s.UseMyFreeCamsAPI &&
|
||||||
|
s.AutoStartAddedDownloads &&
|
||||||
|
!isAutostartPaused()
|
||||||
|
}
|
||||||
|
|
||||||
// Startet watched MyFreeCams Models (ohne API) "best-effort".
|
// Startet watched MyFreeCams Models (ohne API) "best-effort".
|
||||||
// Wenn nach kurzer Zeit keine Output-Datei existiert (oder 0 Bytes), wird abgebrochen und der Job wieder entfernt.
|
// Wenn nach kurzer Zeit keine Output-Datei existiert (oder 0 Bytes), wird abgebrochen und der Job wieder entfernt.
|
||||||
func startMyFreeCamsAutoStartWorker(store *ModelStore) {
|
func startMyFreeCamsAutoStartWorker(store *ModelStore) {
|
||||||
@ -116,8 +124,7 @@ func startMyFreeCamsAutoStartWorker(store *ModelStore) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-scanTicker.C:
|
case <-scanTicker.C:
|
||||||
s := getSettings()
|
if !myFreeCamsAutostartCanRun() {
|
||||||
if !s.UseProviderAPIs || !s.UseMyFreeCamsAPI || !s.AutoStartAddedDownloads || isAutostartPaused() {
|
|
||||||
queue = queue[:0]
|
queue = queue[:0]
|
||||||
queued = map[string]bool{}
|
queued = map[string]bool{}
|
||||||
|
|
||||||
@ -390,8 +397,9 @@ func startMyFreeCamsAutoStartWorker(store *ModelStore) {
|
|||||||
pendingAutoStartMu.Unlock()
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
case <-startTicker.C:
|
case <-startTicker.C:
|
||||||
s := getSettings()
|
if !myFreeCamsAutostartCanRun() {
|
||||||
if !s.UseProviderAPIs || !s.UseMyFreeCamsAPI || !s.AutoStartAddedDownloads || isAutostartPaused() {
|
queue = queue[:0]
|
||||||
|
queued = map[string]bool{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(queue) == 0 {
|
if len(queue) == 0 {
|
||||||
@ -407,6 +415,12 @@ func startMyFreeCamsAutoStartWorker(store *ModelStore) {
|
|||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
delete(queued, it.userKey)
|
delete(queued, it.userKey)
|
||||||
|
|
||||||
|
if !myFreeCamsAutostartCanRun() {
|
||||||
|
queue = queue[:0]
|
||||||
|
queued = map[string]bool{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if isJobRunningForURL(it.url) {
|
if isJobRunningForURL(it.url) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -405,6 +405,49 @@ func removeManualPendingAutoStartItemForProvider(provider, modelKey string) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeAllPendingAutoStartItemsForProvider(provider string) (int, error) {
|
||||||
|
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||||
|
if provider == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userKeys, err := listPendingAutoStartUserKeys()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := append([]string{pendingAutoStartGlobalUserKey}, userKeys...)
|
||||||
|
removed := 0
|
||||||
|
|
||||||
|
for _, userKey := range keys {
|
||||||
|
items, err := loadPendingAutoStartItems(userKey)
|
||||||
|
if err != nil {
|
||||||
|
return removed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := false
|
||||||
|
next := make([]PendingAutoStartItem, 0, len(items))
|
||||||
|
|
||||||
|
for _, it := range items {
|
||||||
|
if pendingProviderFromURL(it.URL) == provider {
|
||||||
|
removed++
|
||||||
|
changed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
next = append(next, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
if err := savePendingAutoStartItems(userKey, next); err != nil {
|
||||||
|
return removed, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed, nil
|
||||||
|
}
|
||||||
|
|
||||||
func loadPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
|
func loadPendingAutoStartItems(userKey string) ([]PendingAutoStartItem, error) {
|
||||||
path := pendingAutoStartFilePath(userKey)
|
path := pendingAutoStartFilePath(userKey)
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -315,7 +316,7 @@ func (pq *PostWorkQueue) workerLoop(id int) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
_ = r
|
appLogln("postwork task panic:", task.Key, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
pq.mu.Lock()
|
pq.mu.Lock()
|
||||||
@ -357,7 +358,13 @@ func (pq *PostWorkQueue) workerLoop(id int) {
|
|||||||
pq.mu.Unlock()
|
pq.mu.Unlock()
|
||||||
|
|
||||||
if task.Run != nil {
|
if task.Run != nil {
|
||||||
_ = task.Run(ctx)
|
if err := task.Run(ctx); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
appLogln("postwork task abgebrochen:", task.Key, err)
|
||||||
|
} else {
|
||||||
|
appLogln("postwork task fehlgeschlagen:", task.Key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1603,11 +1603,26 @@ func recordStopAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type stopAllResp struct {
|
type stopAllResp struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Stopped int `json:"stopped"`
|
Stopped int `json:"stopped"`
|
||||||
IDs []string `json:"ids,omitempty"`
|
IDs []string `json:"ids,omitempty"`
|
||||||
|
AutostartPaused bool `json:"autostartPaused"`
|
||||||
|
PendingRemoved int `json:"pendingRemoved,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAutostartPaused(true)
|
||||||
|
|
||||||
|
pendingRemoved := 0
|
||||||
|
pendingAutoStartMu.Lock()
|
||||||
|
for _, provider := range []string{"mfc", "chaturbate"} {
|
||||||
|
if n, err := removeAllPendingAutoStartItemsForProvider(provider); err != nil {
|
||||||
|
appLogln("⚠️ [stop-all] pending cleanup failed:", provider, err)
|
||||||
|
} else {
|
||||||
|
pendingRemoved += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingAutoStartMu.Unlock()
|
||||||
|
|
||||||
jobsMu.RLock()
|
jobsMu.RLock()
|
||||||
toStop := make([]*RecordJob, 0, len(jobs))
|
toStop := make([]*RecordJob, 0, len(jobs))
|
||||||
ids := make([]string, 0, len(jobs))
|
ids := make([]string, 0, len(jobs))
|
||||||
@ -1638,9 +1653,11 @@ func recordStopAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, stopAllResp{
|
respondJSON(w, stopAllResp{
|
||||||
OK: true,
|
OK: true,
|
||||||
Stopped: len(toStop),
|
Stopped: len(toStop),
|
||||||
IDs: ids,
|
IDs: ids,
|
||||||
|
AutostartPaused: true,
|
||||||
|
PendingRemoved: pendingRemoved,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1210,6 +1210,7 @@ func main() {
|
|||||||
startPostWorkStatusRefresher()
|
startPostWorkStatusRefresher()
|
||||||
startEnrichStatusRefresher()
|
startEnrichStatusRefresher()
|
||||||
|
|
||||||
|
startCleanupTaskOnceOnStartup(appCtx)
|
||||||
startPostworkLeftoverScanOnStartup()
|
startPostworkLeftoverScanOnStartup()
|
||||||
|
|
||||||
appGo("generated-garbage-collector", startGeneratedGarbageCollector)
|
appGo("generated-garbage-collector", startGeneratedGarbageCollector)
|
||||||
|
|||||||
@ -90,8 +90,6 @@ func startPostworkLeftoverScanOnStartup() {
|
|||||||
finishCleanupJob(jobID, summary, nil)
|
finishCleanupJob(jobID, summary, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
runPostworkLeftoverScan("startup")
|
|
||||||
|
|
||||||
ticker := time.NewTicker(postworkLeftoverScanInterval)
|
ticker := time.NewTicker(postworkLeftoverScanInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,16 @@ type cleanupReq struct {
|
|||||||
BelowMB *int `json:"belowMB,omitempty"`
|
BelowMB *int `json:"belowMB,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cleanupTaskConfig struct {
|
||||||
|
DoneAbs string
|
||||||
|
RecordAbs string
|
||||||
|
BelowMB int
|
||||||
|
DeleteLowRated bool
|
||||||
|
MaxStars int
|
||||||
|
QueuedText string
|
||||||
|
RunningText string
|
||||||
|
}
|
||||||
|
|
||||||
type cleanupTaskJob struct {
|
type cleanupTaskJob struct {
|
||||||
State CleanupTaskState
|
State CleanupTaskState
|
||||||
Cancel context.CancelFunc
|
Cancel context.CancelFunc
|
||||||
@ -75,6 +85,8 @@ type cleanupTaskJob struct {
|
|||||||
var cleanupJobsMu sync.Mutex
|
var cleanupJobsMu sync.Mutex
|
||||||
var cleanupJobs = map[string]*cleanupTaskJob{}
|
var cleanupJobs = map[string]*cleanupTaskJob{}
|
||||||
|
|
||||||
|
const startupCleanupInitialDelay = 8 * time.Second
|
||||||
|
|
||||||
func createCleanupJob(jobID string, text string, cancel context.CancelFunc) CleanupTaskState {
|
func createCleanupJob(jobID string, text string, cancel context.CancelFunc) CleanupTaskState {
|
||||||
st := CleanupTaskState{
|
st := CleanupTaskState{
|
||||||
ID: jobID,
|
ID: jobID,
|
||||||
@ -183,149 +195,193 @@ func finishCleanupJob(jobID string, text string, err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanupTaskConfigFromSettings(req *cleanupReq, queuedText string, runningText string) (cleanupTaskConfig, error) {
|
||||||
|
s := getSettings()
|
||||||
|
|
||||||
|
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
||||||
|
if err != nil || strings.TrimSpace(doneAbs) == "" {
|
||||||
|
return cleanupTaskConfig{}, fmt.Errorf("doneDir auflösung fehlgeschlagen")
|
||||||
|
}
|
||||||
|
|
||||||
|
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
||||||
|
if err != nil || strings.TrimSpace(recordAbs) == "" {
|
||||||
|
recordAbs = strings.TrimSpace(s.RecordDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
mb := int(s.AutoDeleteSmallDownloadsBelowMB)
|
||||||
|
if req != nil && req.BelowMB != nil {
|
||||||
|
mb = *req.BelowMB
|
||||||
|
}
|
||||||
|
if mb < 0 {
|
||||||
|
mb = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanupTaskConfig{
|
||||||
|
DoneAbs: doneAbs,
|
||||||
|
RecordAbs: recordAbs,
|
||||||
|
BelowMB: mb,
|
||||||
|
DeleteLowRated: s.AutoDeleteLowRatedDownloads,
|
||||||
|
MaxStars: clampAutoDeleteRatingStars(s.AutoDeleteLowRatedDownloadsMaxStars),
|
||||||
|
QueuedText: strings.TrimSpace(queuedText),
|
||||||
|
RunningText: strings.TrimSpace(runningText),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCleanupTask(parentCtx context.Context, jobPrefix string, cfg cleanupTaskConfig) CleanupTaskState {
|
||||||
|
if parentCtx == nil {
|
||||||
|
parentCtx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
jobPrefix = strings.TrimSpace(jobPrefix)
|
||||||
|
if jobPrefix == "" {
|
||||||
|
jobPrefix = "cleanup"
|
||||||
|
}
|
||||||
|
|
||||||
|
queuedText := strings.TrimSpace(cfg.QueuedText)
|
||||||
|
if queuedText == "" {
|
||||||
|
queuedText = "Wartet…"
|
||||||
|
}
|
||||||
|
|
||||||
|
runningText := strings.TrimSpace(cfg.RunningText)
|
||||||
|
if runningText == "" {
|
||||||
|
runningText = "Räume auf…"
|
||||||
|
}
|
||||||
|
|
||||||
|
jobID := newTaskID(jobPrefix)
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
|
st := createCleanupJob(jobID, queuedText, cancel)
|
||||||
|
|
||||||
|
go func(jobID string, ctx context.Context, cancel context.CancelFunc, cfg cleanupTaskConfig, runningText string) {
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := acquireExclusiveTask(ctx); err != nil {
|
||||||
|
finishCleanupJob(jobID, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer releaseExclusiveTask()
|
||||||
|
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Queued = false
|
||||||
|
st.Running = true
|
||||||
|
st.StartedAt = time.Now()
|
||||||
|
st.Text = runningText
|
||||||
|
})
|
||||||
|
|
||||||
|
resp := cleanupResp{}
|
||||||
|
|
||||||
|
if cfg.BelowMB > 0 || cfg.DeleteLowRated {
|
||||||
|
threshold := int64(cfg.BelowMB) * 1024 * 1024
|
||||||
|
if err := cleanupSmallFiles(ctx, jobID, cfg.DoneAbs, threshold, cfg.DeleteLowRated, cfg.MaxStars, &resp); err != nil {
|
||||||
|
finishCleanupJob(jobID, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neuer Cleanup-Abschnitt: alten 100%-Fortschritt vom Small-Files-Scan zurücksetzen.
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Queued = false
|
||||||
|
st.Running = true
|
||||||
|
if st.StartedAt.IsZero() {
|
||||||
|
st.StartedAt = time.Now()
|
||||||
|
}
|
||||||
|
st.Done = 0
|
||||||
|
st.Total = 0
|
||||||
|
st.CurrentFile = ""
|
||||||
|
st.Text = "Räume Record-Reste auf…"
|
||||||
|
st.Error = ""
|
||||||
|
st.FinishedAt = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := cleanupRecordDirOrphanAVFiles(ctx, jobID, cfg.RecordAbs, &resp); err != nil {
|
||||||
|
finishCleanupJob(jobID, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
finishCleanupJob(jobID, "", ctx.Err())
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
||||||
|
st.Queued = false
|
||||||
|
st.Running = true
|
||||||
|
st.Done = 0
|
||||||
|
st.Total = 0
|
||||||
|
st.CurrentFile = ""
|
||||||
|
st.Text = "Räume Generated-Assets auf…"
|
||||||
|
st.Error = ""
|
||||||
|
st.FinishedAt = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
gcStats := triggerGeneratedGarbageCollectorSync()
|
||||||
|
|
||||||
|
resp.GeneratedOrphansChecked = gcStats.Checked
|
||||||
|
resp.GeneratedOrphansRemoved = gcStats.Removed
|
||||||
|
resp.GeneratedOrphansRemovedBytes = gcStats.RemovedBytes
|
||||||
|
resp.GeneratedOrphansRemovedBytesHuman = formatBytesSI(gcStats.RemovedBytes)
|
||||||
|
|
||||||
|
resp.DeletedBytes += gcStats.RemovedBytes
|
||||||
|
resp.DeletedBytesHuman = formatBytesSI(resp.DeletedBytes)
|
||||||
|
|
||||||
|
if resp.DeletedFiles > 0 || resp.GeneratedOrphansRemoved > 0 {
|
||||||
|
notifyDoneChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
finishCleanupJob(
|
||||||
|
jobID,
|
||||||
|
cleanupProgressText(resp),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}(jobID, ctx, cancel, cfg, runningText)
|
||||||
|
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCleanupTaskOnceOnStartup(parentCtx context.Context) {
|
||||||
|
if parentCtx == nil {
|
||||||
|
parentCtx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
appGo("startup-cleanup", func() {
|
||||||
|
timer := time.NewTimer(startupCleanupInitialDelay)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-parentCtx.Done():
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := cleanupTaskConfigFromSettings(nil, "Wartet…", "Räume auf…")
|
||||||
|
if err != nil {
|
||||||
|
appLogln("⚠️ [startup-cleanup] cleanup config failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startCleanupTask(parentCtx, "cleanup-startup", cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// /api/settings/cleanup (POST)
|
// /api/settings/cleanup (POST)
|
||||||
// - löscht kleine Dateien < threshold MB (mp4/ts; skip .part/.tmp; skip keep-Ordner)
|
// - löscht kleine Dateien < threshold MB (mp4/ts; skip .part/.tmp; skip keep-Ordner)
|
||||||
// - räumt Orphans (preview/thumbs + generated) auf
|
// - räumt Orphans (preview/thumbs + generated) auf
|
||||||
func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
s := getSettings()
|
|
||||||
|
|
||||||
doneAbs, err := resolvePathRelativeToApp(s.DoneDir)
|
|
||||||
if err != nil || strings.TrimSpace(doneAbs) == "" {
|
|
||||||
http.Error(w, "doneDir auflösung fehlgeschlagen", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
recordAbs, err := resolvePathRelativeToApp(s.RecordDir)
|
|
||||||
if err != nil || strings.TrimSpace(recordAbs) == "" {
|
|
||||||
recordAbs = strings.TrimSpace(s.RecordDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
mb := int(s.AutoDeleteSmallDownloadsBelowMB)
|
|
||||||
deleteLowRated := s.AutoDeleteLowRatedDownloads
|
|
||||||
maxStars := clampAutoDeleteRatingStars(s.AutoDeleteLowRatedDownloadsMaxStars)
|
|
||||||
|
|
||||||
var req cleanupReq
|
var req cleanupReq
|
||||||
if r.Body != nil {
|
if r.Body != nil {
|
||||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
}
|
}
|
||||||
if req.BelowMB != nil {
|
|
||||||
mb = *req.BelowMB
|
cfg, err := cleanupTaskConfigFromSettings(&req, "Wartet…", "Räume auf…")
|
||||||
}
|
if err != nil {
|
||||||
if mb < 0 {
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
mb = 0
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jobID := newTaskID("cleanup")
|
st := startCleanupTask(context.Background(), "cleanup", cfg)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
st := CleanupTaskState{
|
|
||||||
ID: jobID,
|
|
||||||
Queued: true,
|
|
||||||
Running: false,
|
|
||||||
Cancellable: true,
|
|
||||||
QueuedAt: time.Now(),
|
|
||||||
StartedAt: time.Time{},
|
|
||||||
FinishedAt: nil,
|
|
||||||
Text: "Wartet…",
|
|
||||||
Error: "",
|
|
||||||
CurrentFile: "",
|
|
||||||
Done: 0,
|
|
||||||
Total: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupJobsMu.Lock()
|
|
||||||
cleanupJobs[jobID] = &cleanupTaskJob{
|
|
||||||
State: st,
|
|
||||||
Cancel: cancel,
|
|
||||||
}
|
|
||||||
cleanupJobsMu.Unlock()
|
|
||||||
|
|
||||||
publishTaskState()
|
|
||||||
|
|
||||||
go func(jobID string, ctx context.Context, doneAbs string, recordAbs string, mb int, deleteLowRated bool, maxStars int) {
|
|
||||||
if err := acquireExclusiveTask(ctx); err != nil {
|
|
||||||
finishCleanupJob(jobID, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer releaseExclusiveTask()
|
|
||||||
|
|
||||||
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
|
||||||
st.Queued = false
|
|
||||||
st.Running = true
|
|
||||||
st.StartedAt = time.Now()
|
|
||||||
st.Text = "Räume auf…"
|
|
||||||
})
|
|
||||||
|
|
||||||
resp := cleanupResp{}
|
|
||||||
|
|
||||||
if mb > 0 || deleteLowRated {
|
|
||||||
threshold := int64(mb) * 1024 * 1024
|
|
||||||
if err := cleanupSmallFiles(ctx, jobID, doneAbs, threshold, deleteLowRated, maxStars, &resp); err != nil {
|
|
||||||
finishCleanupJob(jobID, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neuer Cleanup-Abschnitt: alten 100%-Fortschritt vom Small-Files-Scan zurücksetzen.
|
|
||||||
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
|
||||||
st.Queued = false
|
|
||||||
st.Running = true
|
|
||||||
if st.StartedAt.IsZero() {
|
|
||||||
st.StartedAt = time.Now()
|
|
||||||
}
|
|
||||||
st.Done = 0
|
|
||||||
st.Total = 0
|
|
||||||
st.CurrentFile = ""
|
|
||||||
st.Text = "Räume Record-Reste auf…"
|
|
||||||
st.Error = ""
|
|
||||||
st.FinishedAt = nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := cleanupRecordDirOrphanAVFiles(ctx, jobID, recordAbs, &resp); err != nil {
|
|
||||||
finishCleanupJob(jobID, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
finishCleanupJob(jobID, "", ctx.Err())
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCleanupJobState(jobID, func(st *CleanupTaskState) {
|
|
||||||
st.Queued = false
|
|
||||||
st.Running = true
|
|
||||||
st.Done = 0
|
|
||||||
st.Total = 0
|
|
||||||
st.CurrentFile = ""
|
|
||||||
st.Text = "Räume Generated-Assets auf…"
|
|
||||||
st.Error = ""
|
|
||||||
st.FinishedAt = nil
|
|
||||||
})
|
|
||||||
|
|
||||||
gcStats := triggerGeneratedGarbageCollectorSync()
|
|
||||||
|
|
||||||
resp.GeneratedOrphansChecked = gcStats.Checked
|
|
||||||
resp.GeneratedOrphansRemoved = gcStats.Removed
|
|
||||||
resp.GeneratedOrphansRemovedBytes = gcStats.RemovedBytes
|
|
||||||
resp.GeneratedOrphansRemovedBytesHuman = formatBytesSI(gcStats.RemovedBytes)
|
|
||||||
|
|
||||||
resp.DeletedBytes += gcStats.RemovedBytes
|
|
||||||
resp.DeletedBytesHuman = formatBytesSI(resp.DeletedBytes)
|
|
||||||
|
|
||||||
if resp.DeletedFiles > 0 || resp.GeneratedOrphansRemoved > 0 {
|
|
||||||
notifyDoneChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
finishCleanupJob(
|
|
||||||
jobID,
|
|
||||||
cleanupProgressText(resp),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
}(jobID, ctx, doneAbs, recordAbs, mb, deleteLowRated, maxStars)
|
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, st)
|
writeJSON(w, http.StatusOK, st)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1311,7 +1311,10 @@ const trainingPoseReliableMinQuality = 0.45
|
|||||||
const trainingPositionContextMinScore = 0.22
|
const trainingPositionContextMinScore = 0.22
|
||||||
const trainingPositionContextMaxScore = 0.44
|
const trainingPositionContextMaxScore = 0.44
|
||||||
const trainingPositionContextBoostWeight = 0.60
|
const trainingPositionContextBoostWeight = 0.60
|
||||||
const trainingPositionContextOverrideMargin = 0.16
|
const trainingPoseConfirmingContextMinScore = 0.14
|
||||||
|
const trainingPoseUnconfirmedMaxScore = 0.38
|
||||||
|
const trainingPoseStrongUnconfirmedMinScore = 0.70
|
||||||
|
const trainingPoseStrongUnconfirmedMaxScore = 0.46
|
||||||
|
|
||||||
var errTrainingCancelled = errors.New("training cancelled")
|
var errTrainingCancelled = errors.New("training cancelled")
|
||||||
|
|
||||||
@ -3471,6 +3474,48 @@ func trainingNegativeCorrection() *TrainingCorrection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trainingNormalizeCorrectionForStorage(correction *TrainingCorrection) *TrainingCorrection {
|
||||||
|
if correction == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := *correction
|
||||||
|
normalized.SexPosition = normalizeSexPositionLabel(normalized.SexPosition)
|
||||||
|
if isNoSexPositionLabel(normalized.SexPosition) {
|
||||||
|
normalized.SexPosition = trainingNoSexPositionLabel
|
||||||
|
normalized.PosePersons = []TrainingPosePerson{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainingAnnotationEffectiveSexPosition(annotation TrainingAnnotation) string {
|
||||||
|
if annotation.Negative {
|
||||||
|
return trainingNoSexPositionLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
if annotation.Correction != nil {
|
||||||
|
return normalizeSexPositionLabel(annotation.Correction.SexPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeSexPositionLabel(annotation.Prediction.SexPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainingStripPosePersonsForNoSexPosition(annotation TrainingAnnotation) TrainingAnnotation {
|
||||||
|
if !isNoSexPositionLabel(trainingAnnotationEffectiveSexPosition(annotation)) {
|
||||||
|
return annotation
|
||||||
|
}
|
||||||
|
|
||||||
|
annotation.Prediction.Persons = nil
|
||||||
|
if annotation.Correction != nil {
|
||||||
|
correction := trainingNormalizeCorrectionForStorage(annotation.Correction)
|
||||||
|
correction.PosePersons = []TrainingPosePerson{}
|
||||||
|
annotation.Correction = correction
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotation
|
||||||
|
}
|
||||||
|
|
||||||
func trainingFeedbackHandler(w http.ResponseWriter, r *http.Request) {
|
func trainingFeedbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
trainingWriteError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
@ -3492,6 +3537,7 @@ func trainingFeedbackHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
req.Accepted = false
|
req.Accepted = false
|
||||||
req.Correction = trainingNegativeCorrection()
|
req.Correction = trainingNegativeCorrection()
|
||||||
}
|
}
|
||||||
|
req.Correction = trainingNormalizeCorrectionForStorage(req.Correction)
|
||||||
|
|
||||||
root, err := trainingRootDir()
|
root, err := trainingRootDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -3520,6 +3566,7 @@ func trainingFeedbackHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
Correction: req.Correction,
|
Correction: req.Correction,
|
||||||
Notes: strings.TrimSpace(req.Notes),
|
Notes: strings.TrimSpace(req.Notes),
|
||||||
}
|
}
|
||||||
|
annotation = trainingStripPosePersonsForNoSexPosition(annotation)
|
||||||
|
|
||||||
if !annotation.Accepted && annotation.Correction == nil {
|
if !annotation.Accepted && annotation.Correction == nil {
|
||||||
trainingWriteError(w, http.StatusBadRequest, "correction missing")
|
trainingWriteError(w, http.StatusBadRequest, "correction missing")
|
||||||
@ -3569,6 +3616,7 @@ func trainingFeedbackUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
req.Accepted = false
|
req.Accepted = false
|
||||||
req.Correction = trainingNegativeCorrection()
|
req.Correction = trainingNegativeCorrection()
|
||||||
}
|
}
|
||||||
|
req.Correction = trainingNormalizeCorrectionForStorage(req.Correction)
|
||||||
|
|
||||||
if req.SampleID == "" {
|
if req.SampleID == "" {
|
||||||
trainingWriteError(w, http.StatusBadRequest, "sampleId missing")
|
trainingWriteError(w, http.StatusBadRequest, "sampleId missing")
|
||||||
@ -3629,6 +3677,7 @@ func trainingFeedbackUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
updated.Correction = req.Correction
|
updated.Correction = req.Correction
|
||||||
}
|
}
|
||||||
|
updated = trainingStripPosePersonsForNoSexPosition(updated)
|
||||||
|
|
||||||
items[matchIndex] = updated
|
items[matchIndex] = updated
|
||||||
|
|
||||||
@ -4874,19 +4923,25 @@ func trainingEffectiveCorrection(annotation TrainingAnnotation) TrainingCorrecti
|
|||||||
}
|
}
|
||||||
|
|
||||||
if annotation.Correction != nil {
|
if annotation.Correction != nil {
|
||||||
return *annotation.Correction
|
correction := trainingNormalizeCorrectionForStorage(annotation.Correction)
|
||||||
|
return *correction
|
||||||
}
|
}
|
||||||
|
|
||||||
p := annotation.Prediction
|
p := annotation.Prediction
|
||||||
|
sexPosition := normalizeSexPositionLabel(p.SexPosition)
|
||||||
|
posePersons := p.Persons
|
||||||
|
if isNoSexPositionLabel(sexPosition) {
|
||||||
|
posePersons = []TrainingPosePerson{}
|
||||||
|
}
|
||||||
|
|
||||||
return TrainingCorrection{
|
return TrainingCorrection{
|
||||||
SexPosition: p.SexPosition,
|
SexPosition: sexPosition,
|
||||||
PeoplePresent: trainingScoredLabelsToStrings(p.PeoplePresent),
|
PeoplePresent: trainingScoredLabelsToStrings(p.PeoplePresent),
|
||||||
BodyPartsPresent: trainingScoredLabelsToStrings(p.BodyPartsPresent),
|
BodyPartsPresent: trainingScoredLabelsToStrings(p.BodyPartsPresent),
|
||||||
ObjectsPresent: trainingScoredLabelsToStrings(p.ObjectsPresent),
|
ObjectsPresent: trainingScoredLabelsToStrings(p.ObjectsPresent),
|
||||||
ClothingPresent: trainingScoredLabelsToStrings(p.ClothingPresent),
|
ClothingPresent: trainingScoredLabelsToStrings(p.ClothingPresent),
|
||||||
Boxes: p.Boxes,
|
Boxes: p.Boxes,
|
||||||
PosePersons: p.Persons,
|
PosePersons: posePersons,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6830,9 +6885,6 @@ func trainingFuseHybridPositionScores(
|
|||||||
bestPositionScore := 0.0
|
bestPositionScore := 0.0
|
||||||
bestHasPose := false
|
bestHasPose := false
|
||||||
bestHasContext := false
|
bestHasContext := false
|
||||||
bestPosePosition := ""
|
|
||||||
bestPoseScore := 0.0
|
|
||||||
bestPoseHasContext := false
|
|
||||||
|
|
||||||
for label := range labels {
|
for label := range labels {
|
||||||
poseScore := clamp01(poseScores[label])
|
poseScore := clamp01(poseScores[label])
|
||||||
@ -6843,12 +6895,21 @@ func trainingFuseHybridPositionScores(
|
|||||||
hasContext := contextScore > 0
|
hasContext := contextScore > 0
|
||||||
|
|
||||||
if hasPose {
|
if hasPose {
|
||||||
score = poseScore
|
if hasContext && contextScore >= trainingPoseConfirmingContextMinScore {
|
||||||
|
score = poseScore
|
||||||
|
|
||||||
// Kontext boostet die Pose, bleibt aber bewusst Nebeninformation.
|
// Kontext boostet die Pose nur, wenn er dieselbe Position wirklich stützt.
|
||||||
if hasContext {
|
|
||||||
boost := clamp01(contextScore * trainingPositionContextBoostWeight)
|
boost := clamp01(contextScore * trainingPositionContextBoostWeight)
|
||||||
score = clamp01(1 - (1-score)*(1-boost))
|
score = clamp01(1 - (1-score)*(1-boost))
|
||||||
|
} else {
|
||||||
|
// Unbestätigte Pose bleibt nutzbar, dominiert aber nicht mehr gegen
|
||||||
|
// stärkere Box-/Szenen-Signale. Das reduziert False-Positives bei
|
||||||
|
// falsch erkannten oder schlecht sitzenden Skeletten.
|
||||||
|
maxUnconfirmedScore := trainingPoseUnconfirmedMaxScore
|
||||||
|
if poseScore >= trainingPoseStrongUnconfirmedMinScore {
|
||||||
|
maxUnconfirmedScore = trainingPoseStrongUnconfirmedMaxScore
|
||||||
|
}
|
||||||
|
score = math.Min(maxUnconfirmedScore, poseScore)
|
||||||
}
|
}
|
||||||
} else if contextScore >= trainingPositionContextMinScore {
|
} else if contextScore >= trainingPositionContextMinScore {
|
||||||
// Reiner Box-/Szenen-Kontext darf eine unsichere Prediction liefern,
|
// Reiner Box-/Szenen-Kontext darf eine unsichere Prediction liefern,
|
||||||
@ -6863,20 +6924,6 @@ func trainingFuseHybridPositionScores(
|
|||||||
bestHasContext = hasContext
|
bestHasContext = hasContext
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasPose && score > bestPoseScore {
|
|
||||||
bestPosePosition = label
|
|
||||||
bestPoseScore = score
|
|
||||||
bestPoseHasContext = hasContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bestPosePosition != "" &&
|
|
||||||
!bestHasPose &&
|
|
||||||
bestPositionScore <= bestPoseScore+trainingPositionContextOverrideMargin {
|
|
||||||
bestPosition = bestPosePosition
|
|
||||||
bestPositionScore = bestPoseScore
|
|
||||||
bestHasPose = true
|
|
||||||
bestHasContext = bestPoseHasContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestPosition, clamp01(bestPositionScore), bestHasPose, bestHasContext
|
return bestPosition, clamp01(bestPositionScore), bestHasPose, bestHasContext
|
||||||
@ -6991,6 +7038,27 @@ func trainingBoxHorizontalOverlapRatio(a TrainingBox, b TrainingBox) float64 {
|
|||||||
return clamp01((right - left) / minWidth)
|
return clamp01((right - left) / minWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trainingBoxVerticalOverlapRatio(a TrainingBox, b TrainingBox) float64 {
|
||||||
|
a, okA := trainingNormalizedBox(a)
|
||||||
|
b, okB := trainingNormalizedBox(b)
|
||||||
|
if !okA || !okB {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
top := math.Max(a.Y, b.Y)
|
||||||
|
bottom := math.Min(a.Y+a.H, b.Y+b.H)
|
||||||
|
if bottom <= top {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
minHeight := math.Min(a.H, b.H)
|
||||||
|
if minHeight <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return clamp01((bottom - top) / minHeight)
|
||||||
|
}
|
||||||
|
|
||||||
func trainingBoxesByLabel(boxes []TrainingBox, labels ...string) []TrainingBox {
|
func trainingBoxesByLabel(boxes []TrainingBox, labels ...string) []TrainingBox {
|
||||||
wanted := map[string]bool{}
|
wanted := map[string]bool{}
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
@ -7261,6 +7329,15 @@ type trainingPosePersonGeometry struct {
|
|||||||
box TrainingBox
|
box TrainingBox
|
||||||
hasBox bool
|
hasBox bool
|
||||||
center trainingPosePoint
|
center trainingPosePoint
|
||||||
|
torsoAngle float64
|
||||||
|
hasTorsoAxis bool
|
||||||
|
axisX float64
|
||||||
|
axisY float64
|
||||||
|
perpX float64
|
||||||
|
perpY float64
|
||||||
|
bodyLong float64
|
||||||
|
bodyCross float64
|
||||||
|
elongated bool
|
||||||
lying bool
|
lying bool
|
||||||
upright bool
|
upright bool
|
||||||
straddling bool
|
straddling bool
|
||||||
@ -7328,6 +7405,64 @@ func trainingPosePointDistance(a trainingPosePoint, okA bool, b trainingPosePoin
|
|||||||
return math.Sqrt(dx*dx + dy*dy)
|
return math.Sqrt(dx*dx + dy*dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func trainingPoseProjectedDistance(a trainingPosePoint, okA bool, b trainingPosePoint, okB bool, axisX float64, axisY float64) float64 {
|
||||||
|
if !okA || !okB {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Abs((a.x-b.x)*axisX + (a.y-b.y)*axisY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainingPoseAxisAlignment(a trainingPosePersonGeometry, b trainingPosePersonGeometry) (float64, bool) {
|
||||||
|
if !a.hasTorsoAxis || !b.hasTorsoAxis {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Körperachsen sind richtungslos: 0° und 180° gelten beide als parallel.
|
||||||
|
return math.Abs(math.Cos(a.torsoAngle - b.torsoAngle)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainingPoseExtentsAlongAxis(person TrainingPosePerson, origin trainingPosePoint, axisX float64, axisY float64, perpX float64, perpY float64) (float64, float64, bool) {
|
||||||
|
minLong := 0.0
|
||||||
|
maxLong := 0.0
|
||||||
|
minCross := 0.0
|
||||||
|
maxCross := 0.0
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for _, point := range person.Keypoints {
|
||||||
|
if point.Conf < trainingPoseKeypointMinConfidence ||
|
||||||
|
!trainingIsFinite01(point.X) ||
|
||||||
|
!trainingIsFinite01(point.Y) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dx := point.X - origin.x
|
||||||
|
dy := point.Y - origin.y
|
||||||
|
long := dx*axisX + dy*axisY
|
||||||
|
cross := dx*perpX + dy*perpY
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
minLong = long
|
||||||
|
maxLong = long
|
||||||
|
minCross = cross
|
||||||
|
maxCross = cross
|
||||||
|
} else {
|
||||||
|
minLong = math.Min(minLong, long)
|
||||||
|
maxLong = math.Max(maxLong, long)
|
||||||
|
minCross = math.Min(minCross, cross)
|
||||||
|
maxCross = math.Max(maxCross, cross)
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count < 3 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Abs(maxLong - minLong), math.Abs(maxCross - minCross), true
|
||||||
|
}
|
||||||
|
|
||||||
func trainingPosePersonGeometryFor(person TrainingPosePerson) trainingPosePersonGeometry {
|
func trainingPosePersonGeometryFor(person TrainingPosePerson) trainingPosePersonGeometry {
|
||||||
box, hasBox := trainingNormalizedBox(person.Box)
|
box, hasBox := trainingNormalizedBox(person.Box)
|
||||||
center := trainingPosePoint{x: 0.5, y: 0.5}
|
center := trainingPosePoint{x: 0.5, y: 0.5}
|
||||||
@ -7353,34 +7488,94 @@ func trainingPosePersonGeometryFor(person TrainingPosePerson) trainingPosePerson
|
|||||||
torsoDX := 0.0
|
torsoDX := 0.0
|
||||||
torsoDY := 0.0
|
torsoDY := 0.0
|
||||||
torsoLen := 0.0
|
torsoLen := 0.0
|
||||||
|
torsoAngle := 0.0
|
||||||
|
hasTorsoAxis := false
|
||||||
|
axisX := 0.0
|
||||||
|
axisY := 1.0
|
||||||
|
perpX := -1.0
|
||||||
|
perpY := 0.0
|
||||||
if okHip && okShoulder {
|
if okHip && okShoulder {
|
||||||
torsoDX = math.Abs(hip.x - shoulder.x)
|
rawDX := hip.x - shoulder.x
|
||||||
torsoDY = math.Abs(hip.y - shoulder.y)
|
rawDY := hip.y - shoulder.y
|
||||||
torsoLen = math.Sqrt(torsoDX*torsoDX + torsoDY*torsoDY)
|
torsoDX = math.Abs(rawDX)
|
||||||
|
torsoDY = math.Abs(rawDY)
|
||||||
|
torsoLen = math.Sqrt(rawDX*rawDX + rawDY*rawDY)
|
||||||
|
if torsoLen >= 0.07 {
|
||||||
|
hasTorsoAxis = true
|
||||||
|
axisX = rawDX / torsoLen
|
||||||
|
axisY = rawDY / torsoLen
|
||||||
|
perpX = -axisY
|
||||||
|
perpY = axisX
|
||||||
|
torsoAngle = math.Atan2(axisY, axisX)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hipWidth := trainingPosePointDistance(leftHip, okLeftHip, rightHip, okRightHip)
|
hipWidth := trainingPosePointDistance(leftHip, okLeftHip, rightHip, okRightHip)
|
||||||
kneeWidth := trainingPosePointDistance(leftKnee, okLeftKnee, rightKnee, okRightKnee)
|
kneeWidth := trainingPosePointDistance(leftKnee, okLeftKnee, rightKnee, okRightKnee)
|
||||||
|
|
||||||
kneesBelowHips := okKnee && okHip && knee.y > hip.y+0.045
|
kneesBelowHips := okKnee && okHip && knee.y > hip.y+0.045
|
||||||
|
if hasTorsoAxis && okKnee && okHip {
|
||||||
|
kneeProjection := (knee.x-hip.x)*axisX + (knee.y-hip.y)*axisY
|
||||||
|
kneesBelowHips = kneeProjection > 0.045
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasTorsoAxis && okLeftKnee && okRightKnee {
|
||||||
|
kneeWidth = trainingPoseProjectedDistance(leftKnee, okLeftKnee, rightKnee, okRightKnee, perpX, perpY)
|
||||||
|
|
||||||
|
if okLeftHip && okRightHip {
|
||||||
|
hipWidth = trainingPoseProjectedDistance(leftHip, okLeftHip, rightHip, okRightHip, perpX, perpY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kneesWide := kneeWidth > 0 && kneeWidth >= math.Max(0.11, hipWidth*1.12)
|
kneesWide := kneeWidth > 0 && kneeWidth >= math.Max(0.11, hipWidth*1.12)
|
||||||
straddling := kneesBelowHips && kneesWide
|
straddling := kneesBelowHips && kneesWide
|
||||||
|
|
||||||
|
bodyLong := torsoLen
|
||||||
|
bodyCross := math.Max(hipWidth, kneeWidth)
|
||||||
|
|
||||||
|
if hasTorsoAxis {
|
||||||
|
if poseLong, poseCross, ok := trainingPoseExtentsAlongAxis(person, center, axisX, axisY, perpX, perpY); ok {
|
||||||
|
bodyLong = math.Max(bodyLong, poseLong)
|
||||||
|
bodyCross = math.Max(bodyCross, poseCross)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasBox {
|
||||||
|
boxLong := math.Abs(axisX)*box.W + math.Abs(axisY)*box.H
|
||||||
|
boxCross := math.Abs(perpX)*box.W + math.Abs(perpY)*box.H
|
||||||
|
bodyLong = math.Max(bodyLong, boxLong)
|
||||||
|
bodyCross = math.Max(bodyCross, boxCross)
|
||||||
|
}
|
||||||
|
} else if hasBox {
|
||||||
|
bodyLong = math.Max(box.W, box.H)
|
||||||
|
bodyCross = math.Min(box.W, box.H)
|
||||||
|
}
|
||||||
|
|
||||||
|
elongated := bodyLong >= math.Max(0.18, bodyCross*1.18)
|
||||||
|
|
||||||
torsoHorizontal := torsoLen >= 0.07 && torsoDX >= torsoDY*1.15
|
torsoHorizontal := torsoLen >= 0.07 && torsoDX >= torsoDY*1.15
|
||||||
torsoVertical := torsoLen >= 0.07 && torsoDY >= torsoDX*1.15
|
torsoVertical := torsoLen >= 0.07 && torsoDY >= torsoDX*1.15
|
||||||
|
|
||||||
boxHorizontal := hasBox && box.W >= box.H*1.05
|
boxHorizontal := hasBox && box.W >= box.H*1.05
|
||||||
boxVertical := hasBox && box.H >= box.W*1.25
|
boxVertical := hasBox && box.H >= box.W*1.25
|
||||||
|
|
||||||
lying := torsoHorizontal || boxHorizontal
|
lying := torsoHorizontal || boxHorizontal || (hasTorsoAxis && elongated && !boxVertical)
|
||||||
upright := torsoVertical || boxVertical
|
upright := torsoVertical || boxVertical
|
||||||
allFours := torsoHorizontal && kneesBelowHips
|
allFours := kneesBelowHips && !straddling && (torsoHorizontal || (hasTorsoAxis && elongated))
|
||||||
bentOrKneeling := allFours || (kneesBelowHips && !straddling)
|
bentOrKneeling := allFours || (kneesBelowHips && !straddling)
|
||||||
|
|
||||||
return trainingPosePersonGeometry{
|
return trainingPosePersonGeometry{
|
||||||
box: box,
|
box: box,
|
||||||
hasBox: hasBox,
|
hasBox: hasBox,
|
||||||
center: center,
|
center: center,
|
||||||
|
torsoAngle: torsoAngle,
|
||||||
|
hasTorsoAxis: hasTorsoAxis,
|
||||||
|
axisX: axisX,
|
||||||
|
axisY: axisY,
|
||||||
|
perpX: perpX,
|
||||||
|
perpY: perpY,
|
||||||
|
bodyLong: bodyLong,
|
||||||
|
bodyCross: bodyCross,
|
||||||
|
elongated: elongated,
|
||||||
lying: lying,
|
lying: lying,
|
||||||
upright: upright,
|
upright: upright,
|
||||||
straddling: straddling,
|
straddling: straddling,
|
||||||
@ -7425,6 +7620,7 @@ func trainingAddPosePairGeometryScores(
|
|||||||
gap := trainingBoxGap(left.box, right.box)
|
gap := trainingBoxGap(left.box, right.box)
|
||||||
overlap := trainingBoxOverlapRatio(left.box, right.box)
|
overlap := trainingBoxOverlapRatio(left.box, right.box)
|
||||||
horizontalOverlap := trainingBoxHorizontalOverlapRatio(left.box, right.box)
|
horizontalOverlap := trainingBoxHorizontalOverlapRatio(left.box, right.box)
|
||||||
|
verticalOverlap := trainingBoxVerticalOverlapRatio(left.box, right.box)
|
||||||
close := gap <= 0.12 || overlap >= 0.08
|
close := gap <= 0.12 || overlap >= 0.08
|
||||||
if !close {
|
if !close {
|
||||||
continue
|
continue
|
||||||
@ -7438,28 +7634,73 @@ func trainingAddPosePairGeometryScores(
|
|||||||
}
|
}
|
||||||
|
|
||||||
topAbove := top.center.y <= bottom.center.y-0.055
|
topAbove := top.center.y <= bottom.center.y-0.055
|
||||||
strongStack := horizontalOverlap >= 0.35 && (topAbove || overlap >= 0.22)
|
horizontalStack := horizontalOverlap >= 0.35 && (topAbove || overlap >= 0.22)
|
||||||
topHasRiderShape := top.straddling ||
|
lateralStack := verticalOverlap >= 0.35 && overlap >= 0.12
|
||||||
top.kneesWide ||
|
strongStack := horizontalStack || lateralStack
|
||||||
(top.upright && top.kneesBelowHips)
|
|
||||||
|
|
||||||
if strongStack && topHasRiderShape {
|
axisAlignment, hasAxisAlignment := trainingPoseAxisAlignment(left, right)
|
||||||
add("cowgirl", 0.20)
|
axesParallel := hasAxisAlignment && axisAlignment >= 0.74
|
||||||
add("reverse_cowgirl", 0.17)
|
axesCrossed := hasAxisAlignment && axisAlignment <= 0.56
|
||||||
|
|
||||||
if bottom.lying {
|
hasStrongRiderShape := func(g trainingPosePersonGeometry) bool {
|
||||||
add("cowgirl", 0.12)
|
return g.straddling ||
|
||||||
add("reverse_cowgirl", 0.10)
|
(g.kneesWide && g.kneesBelowHips && (g.upright || axesCrossed))
|
||||||
}
|
}
|
||||||
if top.straddling {
|
hasWeakRiderShape := func(g trainingPosePersonGeometry) bool {
|
||||||
add("cowgirl", 0.08)
|
return g.kneesWide && g.kneesBelowHips
|
||||||
add("reverse_cowgirl", 0.06)
|
}
|
||||||
|
|
||||||
|
leftHasStrongRiderShape := hasStrongRiderShape(left)
|
||||||
|
rightHasStrongRiderShape := hasStrongRiderShape(right)
|
||||||
|
topHasStrongRiderShape := hasStrongRiderShape(top)
|
||||||
|
topHasWeakRiderShape := hasWeakRiderShape(top)
|
||||||
|
|
||||||
|
rider := top
|
||||||
|
base := bottom
|
||||||
|
riderHasStrongShape := topHasStrongRiderShape
|
||||||
|
riderHasWeakShape := topHasWeakRiderShape
|
||||||
|
if leftHasStrongRiderShape != rightHasStrongRiderShape {
|
||||||
|
if leftHasStrongRiderShape {
|
||||||
|
rider = left
|
||||||
|
base = right
|
||||||
|
riderHasStrongShape = true
|
||||||
|
riderHasWeakShape = hasWeakRiderShape(left)
|
||||||
|
} else {
|
||||||
|
rider = right
|
||||||
|
base = left
|
||||||
|
riderHasStrongShape = true
|
||||||
|
riderHasWeakShape = hasWeakRiderShape(right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strongStack && bottom.lying {
|
if strongStack && riderHasStrongShape && !axesParallel {
|
||||||
add("missionary", 0.14)
|
add("cowgirl", 0.20)
|
||||||
if !top.straddling {
|
add("reverse_cowgirl", 0.17)
|
||||||
|
|
||||||
|
if base.lying {
|
||||||
|
add("cowgirl", 0.12)
|
||||||
|
add("reverse_cowgirl", 0.10)
|
||||||
|
}
|
||||||
|
if rider.straddling {
|
||||||
|
add("cowgirl", 0.08)
|
||||||
|
add("reverse_cowgirl", 0.06)
|
||||||
|
}
|
||||||
|
} else if strongStack && riderHasWeakShape && !hasAxisAlignment {
|
||||||
|
// Ohne verwertbare Achsen bleibt Cowgirl nur ein schwaches Signal.
|
||||||
|
// Sobald die Achsen parallel sind, sieht die Szene eher nach
|
||||||
|
// Missionary/Überlagerung aus und soll nicht in Cowgirl kippen.
|
||||||
|
add("cowgirl", 0.08)
|
||||||
|
add("reverse_cowgirl", 0.06)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strongStack && (bottom.lying || (axesParallel && topAbove)) {
|
||||||
|
if !topHasStrongRiderShape || axesParallel {
|
||||||
|
add("missionary", 0.14)
|
||||||
|
}
|
||||||
|
if axesParallel {
|
||||||
|
add("missionary", 0.08)
|
||||||
|
}
|
||||||
|
if !top.straddling && !topHasStrongRiderShape {
|
||||||
add("missionary", 0.08)
|
add("missionary", 0.08)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7467,7 +7708,8 @@ func trainingAddPosePairGeometryScores(
|
|||||||
bothLying := left.lying && right.lying
|
bothLying := left.lying && right.lying
|
||||||
sameLevel := math.Abs(left.center.y-right.center.y) <= 0.15
|
sameLevel := math.Abs(left.center.y-right.center.y) <= 0.15
|
||||||
sideBySide := math.Abs(left.center.x-right.center.x) >= 0.10
|
sideBySide := math.Abs(left.center.x-right.center.x) >= 0.10
|
||||||
if bothLying && (sameLevel || sideBySide) {
|
parallelSideBySide := axesParallel && left.elongated && right.elongated && close && !strongStack
|
||||||
|
if (bothLying && (sameLevel || sideBySide)) || parallelSideBySide {
|
||||||
add("spooning", 0.18)
|
add("spooning", 0.18)
|
||||||
if overlap >= 0.10 {
|
if overlap >= 0.10 {
|
||||||
add("prone_bone", 0.07)
|
add("prone_bone", 0.07)
|
||||||
|
|||||||
@ -132,6 +132,79 @@ func TestTrainingEffectiveCorrectionClearsNegativeAnnotation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrainingEffectiveCorrectionClearsPosePersonsForUnknownCorrection(t *testing.T) {
|
||||||
|
effective := trainingEffectiveCorrection(TrainingAnnotation{
|
||||||
|
Correction: &TrainingCorrection{
|
||||||
|
SexPosition: "Unknown",
|
||||||
|
Boxes: []TrainingBox{
|
||||||
|
{Label: "person_female", X: 0, Y: 0, W: 1, H: 1},
|
||||||
|
},
|
||||||
|
PosePersons: []TrainingPosePerson{
|
||||||
|
{Label: "person", Score: 0.9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if effective.SexPosition != trainingNoSexPositionLabel {
|
||||||
|
t.Fatalf("sex position = %q, want %s", effective.SexPosition, trainingNoSexPositionLabel)
|
||||||
|
}
|
||||||
|
if len(effective.PosePersons) != 0 {
|
||||||
|
t.Fatalf("pose persons = %d, want 0 for unknown sex position", len(effective.PosePersons))
|
||||||
|
}
|
||||||
|
if len(effective.Boxes) != 1 {
|
||||||
|
t.Fatalf("boxes = %d, want detector boxes preserved", len(effective.Boxes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrainingEffectiveCorrectionClearsPosePersonsForNoPositionPrediction(t *testing.T) {
|
||||||
|
effective := trainingEffectiveCorrection(TrainingAnnotation{
|
||||||
|
Accepted: true,
|
||||||
|
Prediction: TrainingPrediction{
|
||||||
|
SexPosition: "unknown",
|
||||||
|
Persons: []TrainingPosePerson{
|
||||||
|
{Label: "person", Score: 0.9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if effective.SexPosition != trainingNoSexPositionLabel {
|
||||||
|
t.Fatalf("sex position = %q, want %s", effective.SexPosition, trainingNoSexPositionLabel)
|
||||||
|
}
|
||||||
|
if len(effective.PosePersons) != 0 {
|
||||||
|
t.Fatalf("pose persons = %d, want 0 for unknown sex position", len(effective.PosePersons))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrainingStripPosePersonsForNoSexPositionBeforeStorage(t *testing.T) {
|
||||||
|
annotation := trainingStripPosePersonsForNoSexPosition(TrainingAnnotation{
|
||||||
|
Prediction: TrainingPrediction{
|
||||||
|
SexPosition: "keine",
|
||||||
|
Persons: []TrainingPosePerson{
|
||||||
|
{Label: "person", Score: 0.9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Correction: &TrainingCorrection{
|
||||||
|
SexPosition: "Unknown",
|
||||||
|
PosePersons: []TrainingPosePerson{
|
||||||
|
{Label: "person", Score: 0.8},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(annotation.Prediction.Persons) != 0 {
|
||||||
|
t.Fatalf("stored prediction persons = %d, want 0", len(annotation.Prediction.Persons))
|
||||||
|
}
|
||||||
|
if annotation.Correction == nil {
|
||||||
|
t.Fatal("correction missing")
|
||||||
|
}
|
||||||
|
if len(annotation.Correction.PosePersons) != 0 {
|
||||||
|
t.Fatalf("stored correction pose persons = %d, want 0", len(annotation.Correction.PosePersons))
|
||||||
|
}
|
||||||
|
if annotation.Correction.SexPosition != trainingNoSexPositionLabel {
|
||||||
|
t.Fatalf("stored correction sex position = %q, want %s", annotation.Correction.SexPosition, trainingNoSexPositionLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNoSexPositionAliasesTreatUnknownAsNoPosition(t *testing.T) {
|
func TestNoSexPositionAliasesTreatUnknownAsNoPosition(t *testing.T) {
|
||||||
for _, value := range []string{"Unknown", "unknown", "unbekannt", "none", "no_position"} {
|
for _, value := range []string{"Unknown", "unknown", "unbekannt", "none", "no_position"} {
|
||||||
if !isNoSexPositionLabel(value) {
|
if !isNoSexPositionLabel(value) {
|
||||||
@ -303,6 +376,85 @@ func TestTrainingApplyPoseToPredictionUsesOcclusionTolerantCowgirlGeometry(t *te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrainingPosePairGeometryDoesNotTreatParallelAxesAsCowgirl(t *testing.T) {
|
||||||
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
||||||
|
scores := map[string]float64{}
|
||||||
|
|
||||||
|
pose := TrainingPosePrediction{
|
||||||
|
Available: true,
|
||||||
|
Persons: []TrainingPosePerson{
|
||||||
|
{
|
||||||
|
Label: "person",
|
||||||
|
Score: 0.74,
|
||||||
|
Box: TrainingBox{X: 0.30, Y: 0.10, W: 0.30, H: 0.42},
|
||||||
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
||||||
|
"left_shoulder": {X: 0.40, Y: 0.18, Conf: 0.92},
|
||||||
|
"right_shoulder": {X: 0.50, Y: 0.18, Conf: 0.92},
|
||||||
|
"left_hip": {X: 0.40, Y: 0.38, Conf: 0.92},
|
||||||
|
"right_hip": {X: 0.50, Y: 0.38, Conf: 0.92},
|
||||||
|
"left_knee": {X: 0.32, Y: 0.58, Conf: 0.90},
|
||||||
|
"right_knee": {X: 0.58, Y: 0.58, Conf: 0.90},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "person",
|
||||||
|
Score: 0.72,
|
||||||
|
Box: TrainingBox{X: 0.31, Y: 0.24, W: 0.30, H: 0.42},
|
||||||
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
||||||
|
"left_shoulder": {X: 0.40, Y: 0.30, Conf: 0.92},
|
||||||
|
"right_shoulder": {X: 0.50, Y: 0.30, Conf: 0.92},
|
||||||
|
"left_hip": {X: 0.40, Y: 0.50, Conf: 0.92},
|
||||||
|
"right_hip": {X: 0.50, Y: 0.50, Conf: 0.92},
|
||||||
|
"left_knee": {X: 0.39, Y: 0.62, Conf: 0.90},
|
||||||
|
"right_knee": {X: 0.51, Y: 0.62, Conf: 0.90},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
trainingAddPosePairGeometryScores(scores, positionSet, pose)
|
||||||
|
|
||||||
|
if got := scores["cowgirl"]; got > 0 {
|
||||||
|
t.Fatalf("cowgirl score = %.3f, want 0 for parallel body axes", got)
|
||||||
|
}
|
||||||
|
if got := scores["reverse_cowgirl"]; got > 0 {
|
||||||
|
t.Fatalf("reverse_cowgirl score = %.3f, want 0 for parallel body axes", got)
|
||||||
|
}
|
||||||
|
if got := scores["missionary"]; got <= 0 {
|
||||||
|
t.Fatalf("missionary score = %.3f, want > 0 for stacked parallel body axes", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrainingPosePersonGeometryUsesBodyAxisForAllFours(t *testing.T) {
|
||||||
|
person := TrainingPosePerson{
|
||||||
|
Label: "person",
|
||||||
|
Score: 0.84,
|
||||||
|
Box: TrainingBox{X: 0.38, Y: 0.22, W: 0.24, H: 0.58},
|
||||||
|
Keypoints: trainingTestPoseKeypoints(map[string]TrainingKeypoint{
|
||||||
|
"left_shoulder": {X: 0.45, Y: 0.30, Conf: 0.92},
|
||||||
|
"right_shoulder": {X: 0.55, Y: 0.30, Conf: 0.92},
|
||||||
|
"left_hip": {X: 0.45, Y: 0.50, Conf: 0.92},
|
||||||
|
"right_hip": {X: 0.55, Y: 0.50, Conf: 0.92},
|
||||||
|
"left_knee": {X: 0.46, Y: 0.68, Conf: 0.90},
|
||||||
|
"right_knee": {X: 0.54, Y: 0.68, Conf: 0.90},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry := trainingPosePersonGeometryFor(person)
|
||||||
|
if !geometry.kneesBelowHips {
|
||||||
|
t.Fatalf("kneesBelowHips = false, want true")
|
||||||
|
}
|
||||||
|
if geometry.straddling {
|
||||||
|
t.Fatalf("straddling = true, want false")
|
||||||
|
}
|
||||||
|
if !geometry.elongated {
|
||||||
|
t.Fatalf("elongated = false, want true")
|
||||||
|
}
|
||||||
|
if !geometry.allFours {
|
||||||
|
t.Fatalf("allFours = false, want true for body-axis aligned bent pose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrainingApplyPoseToPredictionKeepsUnreliablePoseOutOfContext(t *testing.T) {
|
func TestTrainingApplyPoseToPredictionKeepsUnreliablePoseOutOfContext(t *testing.T) {
|
||||||
pred := TrainingPrediction{
|
pred := TrainingPrediction{
|
||||||
SexPosition: trainingNoSexPositionLabel,
|
SexPosition: trainingNoSexPositionLabel,
|
||||||
@ -369,6 +521,45 @@ func TestTrainingApplyPoseToPredictionUsesBoxContextWithoutPose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrainingFuseHybridPositionScoresCapsUnconfirmedPose(t *testing.T) {
|
||||||
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
||||||
|
poseScores := map[string]float64{}
|
||||||
|
contextScores := map[string]float64{}
|
||||||
|
|
||||||
|
trainingCombinePositionScore(poseScores, positionSet, "doggy", 0.82)
|
||||||
|
|
||||||
|
gotPosition, gotScore, gotPose, gotContext := trainingFuseHybridPositionScores(poseScores, contextScores)
|
||||||
|
if gotPosition != "doggy" {
|
||||||
|
t.Fatalf("position = %q, want doggy", gotPosition)
|
||||||
|
}
|
||||||
|
if !gotPose || gotContext {
|
||||||
|
t.Fatalf("signals pose=%v context=%v, want pose only", gotPose, gotContext)
|
||||||
|
}
|
||||||
|
if gotScore > trainingPoseStrongUnconfirmedMaxScore {
|
||||||
|
t.Fatalf("score = %.3f, want capped <= %.3f", gotScore, trainingPoseStrongUnconfirmedMaxScore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrainingFuseHybridPositionScoresPrefersStrongContextOverUnconfirmedPose(t *testing.T) {
|
||||||
|
positionSet := stringSet(defaultTrainingLabelsFromJSON().SexPositions)
|
||||||
|
poseScores := map[string]float64{}
|
||||||
|
contextScores := map[string]float64{}
|
||||||
|
|
||||||
|
trainingCombinePositionScore(poseScores, positionSet, "doggy", 0.62)
|
||||||
|
trainingCombinePositionScore(contextScores, positionSet, "blowjob", trainingPositionContextMaxScore)
|
||||||
|
|
||||||
|
gotPosition, gotScore, gotPose, gotContext := trainingFuseHybridPositionScores(poseScores, contextScores)
|
||||||
|
if gotPosition != "blowjob" {
|
||||||
|
t.Fatalf("position = %q, want blowjob", gotPosition)
|
||||||
|
}
|
||||||
|
if gotPose || !gotContext {
|
||||||
|
t.Fatalf("signals pose=%v context=%v, want context only", gotPose, gotContext)
|
||||||
|
}
|
||||||
|
if gotScore < trainingPositionContextMinScore {
|
||||||
|
t.Fatalf("score = %.3f, want context score >= %.3f", gotScore, trainingPositionContextMinScore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrainingApplyPoseToPredictionBoostsPoseWithBoxContext(t *testing.T) {
|
func TestTrainingApplyPoseToPredictionBoostsPoseWithBoxContext(t *testing.T) {
|
||||||
pred := TrainingPrediction{
|
pred := TrainingPrediction{
|
||||||
ModelAvailable: true,
|
ModelAvailable: true,
|
||||||
@ -450,6 +641,108 @@ func TestBuildClipPositionHitsFromEvidenceCapsContextOnlyScore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppendVideoFrameHighlightHitsFromPredictionOmitsRawPosition(t *testing.T) {
|
||||||
|
hits := appendVideoFrameHighlightHitsFromPrediction(nil, TrainingPrediction{
|
||||||
|
ModelAvailable: true,
|
||||||
|
SexPosition: "doggy",
|
||||||
|
SexPositionScore: 0.85,
|
||||||
|
ObjectsPresent: []TrainingScoredLabel{
|
||||||
|
{Label: "vibrator", Score: 0.90},
|
||||||
|
},
|
||||||
|
}, 4)
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
t.Fatal("expected non-position video frame highlight")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hit := range hits {
|
||||||
|
if strings.Contains(hit.Label, "position:") {
|
||||||
|
t.Fatalf("hit label = %q, want raw position removed", hit.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildClipPositionHitsFromEvidencePrefersVideoMAEClipOverFramePose(t *testing.T) {
|
||||||
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
||||||
|
{Time: 1, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
||||||
|
{Time: 2, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
||||||
|
{Time: 3, Label: "doggy", Score: 0.60, HasPose: true, PersonCount: 2},
|
||||||
|
{Time: 2, Label: "cowgirl", Score: 0.58, HasClip: true},
|
||||||
|
}, 8)
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
t.Fatal("expected VideoMAE-backed position hit")
|
||||||
|
}
|
||||||
|
if hits[0].Label != "position:cowgirl" {
|
||||||
|
t.Fatalf("label = %q, want position:cowgirl", hits[0].Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildClipPositionHitsFromEvidenceUsesVideoMAEClipSpan(t *testing.T) {
|
||||||
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
||||||
|
{Time: 12, Start: 10, End: 14, Label: "cowgirl", Score: 0.62, HasClip: true},
|
||||||
|
}, 20)
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
t.Fatal("expected VideoMAE clip ledger hit")
|
||||||
|
}
|
||||||
|
if hits[0].Label != "position:cowgirl" {
|
||||||
|
t.Fatalf("label = %q, want position:cowgirl", hits[0].Label)
|
||||||
|
}
|
||||||
|
if hits[0].Start > 10 || hits[0].End < 14 {
|
||||||
|
t.Fatalf("span = %.1f..%.1f, want to cover clip 10..14", hits[0].Start, hits[0].End)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildClipPositionHitsFromEvidenceKeepsStableToyPlayTimelinePosition(t *testing.T) {
|
||||||
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
||||||
|
{Time: 12, Start: 10, End: 24, Label: "toy_play", Score: 0.90, HasClip: true},
|
||||||
|
}, 120)
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
t.Fatal("expected stable toy_play timeline position")
|
||||||
|
}
|
||||||
|
if hits[0].Label != "position:toy_play" {
|
||||||
|
t.Fatalf("label = %q, want position:toy_play", hits[0].Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildClipPositionHitsFromEvidenceDropsShortWeakPositionFlipInLongVideo(t *testing.T) {
|
||||||
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
||||||
|
{Time: 10, Start: 8, End: 28, Label: "missionary", Score: 0.70, HasClip: true},
|
||||||
|
{Time: 32, Start: 30, End: 34, Label: "cowgirl", Score: 0.46, HasClip: true},
|
||||||
|
{Time: 35, Start: 34, End: 38, Label: "toy_play", Score: 0.46, HasClip: true},
|
||||||
|
{Time: 50, Start: 36, End: 64, Label: "missionary", Score: 0.72, HasClip: true},
|
||||||
|
}, 120)
|
||||||
|
|
||||||
|
if len(hits) == 0 {
|
||||||
|
t.Fatal("expected stable missionary position hits")
|
||||||
|
}
|
||||||
|
for _, hit := range hits {
|
||||||
|
if hit.Label == "position:cowgirl" {
|
||||||
|
t.Fatalf("hits = %+v, want short weak cowgirl flip omitted", hits)
|
||||||
|
}
|
||||||
|
if hit.Label == "position:toy_play" {
|
||||||
|
t.Fatalf("hits = %+v, want short weak toy_play flip omitted", hits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildClipPositionHitsFromEvidenceDropsCloseFrameConflict(t *testing.T) {
|
||||||
|
hits := buildClipPositionHitsFromEvidence([]analyzePositionEvidence{
|
||||||
|
{Time: 1, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
{Time: 2, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
{Time: 3, Label: "doggy", Score: 0.52, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
{Time: 1, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
{Time: 2, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
{Time: 3, Label: "cowgirl", Score: 0.50, HasPose: true, HasContext: true, PersonCount: 2},
|
||||||
|
}, 8)
|
||||||
|
|
||||||
|
if len(hits) != 0 {
|
||||||
|
t.Fatalf("hits = %+v, want no hard position for close conflict", hits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrainingFilterPosePersonsByContextDropsUnannotatedPeople(t *testing.T) {
|
func TestTrainingFilterPosePersonsByContextDropsUnannotatedPeople(t *testing.T) {
|
||||||
persons := []TrainingPosePerson{
|
persons := []TrainingPosePerson{
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2771,6 +2771,10 @@ export default function App() {
|
|||||||
|
|
||||||
// Nur automatische Starts in pending-autostart schieben.
|
// Nur automatische Starts in pending-autostart schieben.
|
||||||
// Manuelle UI-Starts müssen sofort direkt starten.
|
// Manuelle UI-Starts müssen sofort direkt starten.
|
||||||
|
if (!immediate && !recSettingsRef.current.autoStartAddedDownloads) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (!immediate) {
|
if (!immediate) {
|
||||||
if (
|
if (
|
||||||
provider === 'chaturbate' &&
|
provider === 'chaturbate' &&
|
||||||
@ -3159,6 +3163,13 @@ export default function App() {
|
|||||||
: 0
|
: 0
|
||||||
|
|
||||||
const percent = Math.round(progress * 100)
|
const percent = Math.round(progress * 100)
|
||||||
|
const message = String(msg?.message ?? '').trim()
|
||||||
|
const runningLabel =
|
||||||
|
total > 0
|
||||||
|
? /\d{1,3}%/.test(message)
|
||||||
|
? message
|
||||||
|
: `Analyse ${percent}%`
|
||||||
|
: message || 'Analyse läuft'
|
||||||
|
|
||||||
const state =
|
const state =
|
||||||
phase === 'error'
|
phase === 'error'
|
||||||
@ -3172,9 +3183,7 @@ export default function App() {
|
|||||||
? 'Analyse Fehler'
|
? 'Analyse Fehler'
|
||||||
: phase === 'done'
|
: phase === 'done'
|
||||||
? 'Analyse fertig'
|
? 'Analyse fertig'
|
||||||
: total > 0
|
: runningLabel
|
||||||
? `Analyse ${percent}%`
|
|
||||||
: String(msg?.message ?? '').trim() || 'Analyse läuft'
|
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('finished-downloads:postwork', {
|
new CustomEvent('finished-downloads:postwork', {
|
||||||
@ -3450,6 +3459,8 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
await apiJSON('/api/record/stop-all', { method: 'POST' })
|
await apiJSON('/api/record/stop-all', { method: 'POST' })
|
||||||
void loadJobs()
|
void loadJobs()
|
||||||
|
void loadPendingAutoStarts()
|
||||||
|
void loadAutostartState().catch(() => {})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
notify.error('Alle stoppen fehlgeschlagen', e?.message ?? String(e))
|
notify.error('Alle stoppen fehlgeschlagen', e?.message ?? String(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1557,14 +1557,17 @@ export default function Downloads({
|
|||||||
}, [pending])
|
}, [pending])
|
||||||
|
|
||||||
const totalCount = downloadJobRows.length + postworkRows.length + pendingRows.length
|
const totalCount = downloadJobRows.length + postworkRows.length + pendingRows.length
|
||||||
|
const stopAllTargetCount = stoppableIds.length + pendingRows.length
|
||||||
|
|
||||||
const stopAll = useCallback(async () => {
|
const stopAll = useCallback(async () => {
|
||||||
if (stopAllBusy) return
|
if (stopAllBusy) return
|
||||||
if (stoppableIds.length === 0) return
|
if (stopAllTargetCount === 0) return
|
||||||
|
|
||||||
setStopAllBusy(true)
|
setStopAllBusy(true)
|
||||||
try {
|
try {
|
||||||
markStopRequested(stoppableIds)
|
if (stoppableIds.length > 0) {
|
||||||
|
markStopRequested(stoppableIds)
|
||||||
|
}
|
||||||
|
|
||||||
if (onStopAllJobs) {
|
if (onStopAllJobs) {
|
||||||
await onStopAllJobs()
|
await onStopAllJobs()
|
||||||
@ -1574,7 +1577,7 @@ export default function Downloads({
|
|||||||
} finally {
|
} finally {
|
||||||
setStopAllBusy(false)
|
setStopAllBusy(false)
|
||||||
}
|
}
|
||||||
}, [stopAllBusy, stoppableIds, markStopRequested, onStopAllJobs, onStopJob])
|
}, [stopAllBusy, stopAllTargetCount, stoppableIds, markStopRequested, onStopAllJobs, onStopJob])
|
||||||
|
|
||||||
const rowClassName = useCallback((r: DownloadRow) => {
|
const rowClassName = useCallback((r: DownloadRow) => {
|
||||||
if (r.kind === 'pending') {
|
if (r.kind === 'pending') {
|
||||||
@ -1678,16 +1681,16 @@ export default function Downloads({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={stopAllBusy || stoppableIds.length === 0}
|
disabled={stopAllBusy || stopAllTargetCount === 0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
void stopAll()
|
void stopAll()
|
||||||
}}
|
}}
|
||||||
className="w-full justify-center"
|
className="w-full justify-center"
|
||||||
title={stoppableIds.length === 0 ? 'Nichts zu stoppen' : 'Alle laufenden stoppen'}
|
title={stopAllTargetCount === 0 ? 'Nichts zu stoppen' : 'Alle laufenden und wartenden stoppen'}
|
||||||
>
|
>
|
||||||
{stopAllBusy ? 'Stoppe…' : `Alle stoppen (${stoppableIds.length})`}
|
{stopAllBusy ? 'Stoppe…' : `Alle stoppen (${stopAllTargetCount})`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1752,15 +1755,15 @@ export default function Downloads({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={stopAllBusy || stoppableIds.length === 0}
|
disabled={stopAllBusy || stopAllTargetCount === 0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
void stopAll()
|
void stopAll()
|
||||||
}}
|
}}
|
||||||
title={stoppableIds.length === 0 ? 'Nichts zu stoppen' : 'Alle laufenden stoppen'}
|
title={stopAllTargetCount === 0 ? 'Nichts zu stoppen' : 'Alle laufenden und wartenden stoppen'}
|
||||||
>
|
>
|
||||||
{stopAllBusy ? 'Stoppe alle…' : `Alle stoppen (${stoppableIds.length})`}
|
{stopAllBusy ? 'Stoppe alle…' : `Alle stoppen (${stopAllTargetCount})`}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -161,6 +161,31 @@ const keyFor = (j: RecordJob) => {
|
|||||||
return stableFromOutput || String((j as any)?.output || '')
|
return stableFromOutput || String((j as any)?.output || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStringSet(
|
||||||
|
prev: Set<string>,
|
||||||
|
values: Iterable<string>,
|
||||||
|
enabled: boolean
|
||||||
|
) {
|
||||||
|
let changed = false
|
||||||
|
const next = new Set(prev)
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
const clean = String(value || '').trim()
|
||||||
|
if (!clean) continue
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
if (!next.has(clean)) {
|
||||||
|
next.add(clean)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else if (next.delete(clean)) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed ? next : prev
|
||||||
|
}
|
||||||
|
|
||||||
const isTrashOutput = (output?: string) => {
|
const isTrashOutput = (output?: string) => {
|
||||||
const p = norm(String(output ?? ''))
|
const p = norm(String(output ?? ''))
|
||||||
// match: ".../.trash/file.ext" oder "...\ .trash\file.ext"
|
// match: ".../.trash/file.ext" oder "...\ .trash\file.ext"
|
||||||
@ -1685,7 +1710,7 @@ function samePostworkBadge(a: FinishedPostworkBadge, b: FinishedPostworkBadge):
|
|||||||
function analysisProgressPercent(label?: string): number | null {
|
function analysisProgressPercent(label?: string): number | null {
|
||||||
const s = String(label ?? '').trim()
|
const s = String(label ?? '').trim()
|
||||||
|
|
||||||
const percentMatch = s.match(/^analyse\s+(\d{1,3})%$/i)
|
const percentMatch = s.match(/^(?:analyse|videomae|speichern|finalisieren)\s+(\d{1,3})%$/i)
|
||||||
if (percentMatch) {
|
if (percentMatch) {
|
||||||
const n = Number(percentMatch[1])
|
const n = Number(percentMatch[1])
|
||||||
if (!Number.isFinite(n)) return null
|
if (!Number.isFinite(n)) return null
|
||||||
@ -1725,8 +1750,22 @@ function runningStepStateLabel(step: FinishedPostworkStepSummary): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAnalysisProgressLabel(label?: string): string | undefined {
|
function normalizeAnalysisProgressLabel(label?: string): string | undefined {
|
||||||
|
const raw = String(label ?? '').trim()
|
||||||
const percent = analysisProgressPercent(label)
|
const percent = analysisProgressPercent(label)
|
||||||
if (percent == null) return label
|
if (percent == null) return label
|
||||||
|
|
||||||
|
const phaseMatch = raw.match(/^(videomae|speichern|finalisieren)\s+\d{1,3}%$/i)
|
||||||
|
if (phaseMatch) {
|
||||||
|
const phase = phaseMatch[1].toLowerCase()
|
||||||
|
const prefix =
|
||||||
|
phase === 'videomae'
|
||||||
|
? 'VideoMAE'
|
||||||
|
: phase === 'speichern'
|
||||||
|
? 'Speichern'
|
||||||
|
: 'Finalisieren'
|
||||||
|
return `${prefix} ${percent}%`
|
||||||
|
}
|
||||||
|
|
||||||
return `Analyse ${percent}%`
|
return `Analyse ${percent}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2316,6 +2355,7 @@ export default function FinishedDownloads({
|
|||||||
const [bulkDeletedSuccessKeys, setBulkDeletedSuccessKeys] = useState<Set<string>>(() => new Set())
|
const [bulkDeletedSuccessKeys, setBulkDeletedSuccessKeys] = useState<Set<string>>(() => new Set())
|
||||||
const [keepingKeys, setKeepingKeys] = useState<Set<string>>(() => new Set())
|
const [keepingKeys, setKeepingKeys] = useState<Set<string>>(() => new Set())
|
||||||
const [keptSuccessKeys, setKeptSuccessKeys] = useState<Set<string>>(() => new Set())
|
const [keptSuccessKeys, setKeptSuccessKeys] = useState<Set<string>>(() => new Set())
|
||||||
|
const [bulkKeptSuccessKeys, setBulkKeptSuccessKeys] = useState<Set<string>>(() => new Set())
|
||||||
const [removingKeys, setRemovingKeys] = useState<Set<string>>(() => new Set())
|
const [removingKeys, setRemovingKeys] = useState<Set<string>>(() => new Set())
|
||||||
const [hiddenFiles, setHiddenFiles] = useState<Set<string>>(() => new Set())
|
const [hiddenFiles, setHiddenFiles] = useState<Set<string>>(() => new Set())
|
||||||
const [restoredJobsByKey, setRestoredJobsByKey] = useState<Record<string, RecordJob>>({})
|
const [restoredJobsByKey, setRestoredJobsByKey] = useState<Record<string, RecordJob>>({})
|
||||||
@ -3163,12 +3203,22 @@ export default function FinishedDownloads({
|
|||||||
return next
|
return next
|
||||||
}, [bulkDeletedSuccessKeys, deletedSuccessKeys])
|
}, [bulkDeletedSuccessKeys, deletedSuccessKeys])
|
||||||
|
|
||||||
|
const visibleKeptSuccessKeys = useMemo(() => {
|
||||||
|
if (bulkKeptSuccessKeys.size === 0) return keptSuccessKeys
|
||||||
|
|
||||||
|
const next = new Set(keptSuccessKeys)
|
||||||
|
for (const key of bulkKeptSuccessKeys) {
|
||||||
|
next.add(key)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}, [bulkKeptSuccessKeys, keptSuccessKeys])
|
||||||
|
|
||||||
const autoPageSizeSuspended =
|
const autoPageSizeSuspended =
|
||||||
bulkBusy ||
|
bulkBusy ||
|
||||||
deletingKeys.size > 0 ||
|
deletingKeys.size > 0 ||
|
||||||
visibleDeletedSuccessKeys.size > 0 ||
|
visibleDeletedSuccessKeys.size > 0 ||
|
||||||
keepingKeys.size > 0 ||
|
keepingKeys.size > 0 ||
|
||||||
keptSuccessKeys.size > 0 ||
|
visibleKeptSuccessKeys.size > 0 ||
|
||||||
removingKeys.size > 0
|
removingKeys.size > 0
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@ -3592,6 +3642,26 @@ export default function FinishedDownloads({
|
|||||||
return aliases
|
return aliases
|
||||||
}, [fileAliasesFor])
|
}, [fileAliasesFor])
|
||||||
|
|
||||||
|
const markDeletingAliases = useCallback((key: string, file: string | undefined, value: boolean) => {
|
||||||
|
const aliases = rowKeyAliasesFor(key, file)
|
||||||
|
setDeletingKeys((prev) => updateStringSet(prev, aliases, value))
|
||||||
|
}, [rowKeyAliasesFor])
|
||||||
|
|
||||||
|
const markKeepingAliases = useCallback((key: string, file: string | undefined, value: boolean) => {
|
||||||
|
const aliases = rowKeyAliasesFor(key, file)
|
||||||
|
setKeepingKeys((prev) => updateStringSet(prev, aliases, value))
|
||||||
|
}, [rowKeyAliasesFor])
|
||||||
|
|
||||||
|
const markBulkDeletedSuccessAliases = useCallback((key: string, file: string | undefined, value: boolean) => {
|
||||||
|
const aliases = rowKeyAliasesFor(key, file)
|
||||||
|
setBulkDeletedSuccessKeys((prev) => updateStringSet(prev, aliases, value))
|
||||||
|
}, [rowKeyAliasesFor])
|
||||||
|
|
||||||
|
const markBulkKeptSuccessAliases = useCallback((key: string, file: string | undefined, value: boolean) => {
|
||||||
|
const aliases = rowKeyAliasesFor(key, file)
|
||||||
|
setBulkKeptSuccessKeys((prev) => updateStringSet(prev, aliases, value))
|
||||||
|
}, [rowKeyAliasesFor])
|
||||||
|
|
||||||
const removeRowNow = useCallback(
|
const removeRowNow = useCallback(
|
||||||
(key: string, file?: string) => {
|
(key: string, file?: string) => {
|
||||||
cancelRemoveTimer(key)
|
cancelRemoveTimer(key)
|
||||||
@ -3645,8 +3715,10 @@ export default function FinishedDownloads({
|
|||||||
markDeletedSuccess(key, false)
|
markDeletedSuccess(key, false)
|
||||||
markBulkDeletedSuccess(key, false)
|
markBulkDeletedSuccess(key, false)
|
||||||
markKeptSuccess(key, false)
|
markKeptSuccess(key, false)
|
||||||
markDeleting(key, false)
|
markDeletingAliases(key, file, false)
|
||||||
markKeeping(key, false)
|
markKeepingAliases(key, file, false)
|
||||||
|
markBulkDeletedSuccessAliases(key, file, false)
|
||||||
|
markBulkKeptSuccessAliases(key, file, false)
|
||||||
markDeleted(key)
|
markDeleted(key)
|
||||||
markRemoving(key, false)
|
markRemoving(key, false)
|
||||||
|
|
||||||
@ -3659,8 +3731,10 @@ export default function FinishedDownloads({
|
|||||||
markDeletedSuccess,
|
markDeletedSuccess,
|
||||||
markBulkDeletedSuccess,
|
markBulkDeletedSuccess,
|
||||||
markKeptSuccess,
|
markKeptSuccess,
|
||||||
markDeleting,
|
markDeletingAliases,
|
||||||
markKeeping,
|
markKeepingAliases,
|
||||||
|
markBulkDeletedSuccessAliases,
|
||||||
|
markBulkKeptSuccessAliases,
|
||||||
markDeleted,
|
markDeleted,
|
||||||
markRemoving,
|
markRemoving,
|
||||||
refreshHoverPreviewFromPointer,
|
refreshHoverPreviewFromPointer,
|
||||||
@ -3694,6 +3768,7 @@ export default function FinishedDownloads({
|
|||||||
setBulkDeletedSuccessKeys(removeKeyAliases)
|
setBulkDeletedSuccessKeys(removeKeyAliases)
|
||||||
setKeepingKeys(removeKeyAliases)
|
setKeepingKeys(removeKeyAliases)
|
||||||
setKeptSuccessKeys(removeKeyAliases)
|
setKeptSuccessKeys(removeKeyAliases)
|
||||||
|
setBulkKeptSuccessKeys(removeKeyAliases)
|
||||||
|
|
||||||
if (fileAliases.size > 0) {
|
if (fileAliases.size > 0) {
|
||||||
setHiddenFiles((prev) => {
|
setHiddenFiles((prev) => {
|
||||||
@ -4560,20 +4635,17 @@ export default function FinishedDownloads({
|
|||||||
|
|
||||||
if (selectedDeleteItems.length === 0) return
|
if (selectedDeleteItems.length === 0) return
|
||||||
|
|
||||||
const selectedDeleteKeys = selectedDeleteItems.map((item) => item.key)
|
|
||||||
const selectedDeleteFiles = selectedDeleteItems.map((item) => item.file)
|
const selectedDeleteFiles = selectedDeleteItems.map((item) => item.file)
|
||||||
|
const aliasesForDeleteItem = (item: { key: string; file: string }) =>
|
||||||
|
Array.from(rowKeyAliasesFor(item.key, item.file))
|
||||||
|
const selectedDeleteAliases = selectedDeleteItems.flatMap(aliasesForDeleteItem)
|
||||||
const deletedKeys = new Set<string>()
|
const deletedKeys = new Set<string>()
|
||||||
const failedKeys = new Set<string>()
|
const failedKeys = new Set<string>()
|
||||||
|
const failedAliases = new Set<string>()
|
||||||
const deletedItems: Array<{ file: string; key: string }> = []
|
const deletedItems: Array<{ file: string; key: string }> = []
|
||||||
|
|
||||||
const clearDeleting = (keys: Iterable<string>) => {
|
const clearDeleting = (keys: Iterable<string>) => {
|
||||||
setDeletingKeys((prev) => {
|
setDeletingKeys((prev) => updateStringSet(prev, keys, false))
|
||||||
const next = new Set(prev)
|
|
||||||
for (const key of keys) {
|
|
||||||
next.delete(key)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResult = (item: any) => {
|
const handleResult = (item: any) => {
|
||||||
@ -4587,29 +4659,27 @@ export default function FinishedDownloads({
|
|||||||
selectedItem?.key ||
|
selectedItem?.key ||
|
||||||
fileToKeyRef.current.get(file) ||
|
fileToKeyRef.current.get(file) ||
|
||||||
file
|
file
|
||||||
|
const aliases = rowKeyAliasesFor(key, file)
|
||||||
|
|
||||||
if (item?.ok) {
|
if (item?.ok) {
|
||||||
if (!deletedKeys.has(key)) {
|
if (!deletedKeys.has(key)) {
|
||||||
deletedKeys.add(key)
|
deletedKeys.add(key)
|
||||||
deletedItems.push({ key, file })
|
deletedItems.push({ key, file })
|
||||||
markDeletedRowSuccess(key)
|
markDeletedRowSuccess(key)
|
||||||
markBulkDeletedSuccess(key, true)
|
markBulkDeletedSuccessAliases(key, file, true)
|
||||||
|
clearDeleting(aliases)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
failedKeys.add(key)
|
failedKeys.add(key)
|
||||||
clearDeleting([key])
|
for (const alias of aliases) failedAliases.add(alias)
|
||||||
|
markBulkDeletedSuccessAliases(key, file, false)
|
||||||
|
clearDeleting(aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sofort Overlay anzeigen, noch bevor der Bulk-Request läuft.
|
// Sofort Overlay anzeigen, noch bevor der Bulk-Request läuft.
|
||||||
setDeletingKeys((prev) => {
|
setDeletingKeys((prev) => updateStringSet(prev, selectedDeleteAliases, true))
|
||||||
const next = new Set(prev)
|
|
||||||
for (const key of selectedDeleteKeys) {
|
|
||||||
next.add(key)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
setBulkBusy(true)
|
setBulkBusy(true)
|
||||||
|
|
||||||
@ -4671,11 +4741,12 @@ export default function FinishedDownloads({
|
|||||||
for (const item of selectedDeleteItems) {
|
for (const item of selectedDeleteItems) {
|
||||||
if (!deletedKeys.has(item.key) && !failedKeys.has(item.key)) {
|
if (!deletedKeys.has(item.key) && !failedKeys.has(item.key)) {
|
||||||
failedKeys.add(item.key)
|
failedKeys.add(item.key)
|
||||||
|
for (const alias of aliasesForDeleteItem(item)) failedAliases.add(alias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedKeys.size > 0) {
|
if (failedKeys.size > 0) {
|
||||||
clearDeleting(failedKeys)
|
clearDeleting(failedAliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedCount = deletedKeys.size
|
const deletedCount = deletedKeys.size
|
||||||
@ -4689,7 +4760,11 @@ export default function FinishedDownloads({
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// Bei komplettem Fehler alle zuvor markierten Overlays wieder entfernen.
|
// Bei komplettem Fehler alle zuvor markierten Overlays wieder entfernen.
|
||||||
clearDeleting(selectedDeleteKeys.filter((key) => !deletedKeys.has(key)))
|
clearDeleting(
|
||||||
|
selectedDeleteItems
|
||||||
|
.filter((item) => !deletedKeys.has(item.key))
|
||||||
|
.flatMap(aliasesForDeleteItem)
|
||||||
|
)
|
||||||
|
|
||||||
if (deletedKeys.size > 0) {
|
if (deletedKeys.size > 0) {
|
||||||
clearSelection()
|
clearSelection()
|
||||||
@ -4709,8 +4784,9 @@ export default function FinishedDownloads({
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
emitCountHint,
|
emitCountHint,
|
||||||
markDeletedRowSuccess,
|
markDeletedRowSuccess,
|
||||||
markBulkDeletedSuccess,
|
markBulkDeletedSuccessAliases,
|
||||||
removeRowsTogether,
|
removeRowsTogether,
|
||||||
|
rowKeyAliasesFor,
|
||||||
notify,
|
notify,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -4737,19 +4813,16 @@ export default function FinishedDownloads({
|
|||||||
if (selectedKeepItems.length === 0) return
|
if (selectedKeepItems.length === 0) return
|
||||||
|
|
||||||
const selectedKeepFiles = selectedKeepItems.map((item) => item.file)
|
const selectedKeepFiles = selectedKeepItems.map((item) => item.file)
|
||||||
const selectedKeepKeys = selectedKeepItems.map((item) => item.key)
|
const aliasesForKeepItem = (item: { key: string; file: string }) =>
|
||||||
|
Array.from(rowKeyAliasesFor(item.key, item.file))
|
||||||
|
const selectedKeepAliases = selectedKeepItems.flatMap(aliasesForKeepItem)
|
||||||
const keptKeys = new Set<string>()
|
const keptKeys = new Set<string>()
|
||||||
const failedKeys = new Set<string>()
|
const failedKeys = new Set<string>()
|
||||||
|
const failedAliases = new Set<string>()
|
||||||
const keptItems: Array<{ file: string; key: string }> = []
|
const keptItems: Array<{ file: string; key: string }> = []
|
||||||
|
|
||||||
const clearKeeping = (keys: Iterable<string>) => {
|
const clearKeeping = (keys: Iterable<string>) => {
|
||||||
setKeepingKeys((prev) => {
|
setKeepingKeys((prev) => updateStringSet(prev, keys, false))
|
||||||
const next = new Set(prev)
|
|
||||||
for (const key of keys) {
|
|
||||||
next.delete(key)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeepSuccess = (file: string) => {
|
const handleKeepSuccess = (file: string) => {
|
||||||
@ -4761,21 +4834,18 @@ export default function FinishedDownloads({
|
|||||||
selectedItem?.key ||
|
selectedItem?.key ||
|
||||||
fileToKeyRef.current.get(cleanFile) ||
|
fileToKeyRef.current.get(cleanFile) ||
|
||||||
cleanFile
|
cleanFile
|
||||||
|
const aliases = rowKeyAliasesFor(key, cleanFile)
|
||||||
|
|
||||||
if (keptKeys.has(key)) return
|
if (keptKeys.has(key)) return
|
||||||
|
|
||||||
keptKeys.add(key)
|
keptKeys.add(key)
|
||||||
keptItems.push({ key, file: cleanFile })
|
keptItems.push({ key, file: cleanFile })
|
||||||
markKeptRowSuccess(key)
|
markKeptRowSuccess(key)
|
||||||
|
markBulkKeptSuccessAliases(key, cleanFile, true)
|
||||||
|
clearKeeping(aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
setKeepingKeys((prev) => {
|
setKeepingKeys((prev) => updateStringSet(prev, selectedKeepAliases, true))
|
||||||
const next = new Set(prev)
|
|
||||||
for (const key of selectedKeepKeys) {
|
|
||||||
next.add(key)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
setBulkBusy(true)
|
setBulkBusy(true)
|
||||||
try {
|
try {
|
||||||
@ -4809,7 +4879,9 @@ export default function FinishedDownloads({
|
|||||||
}
|
}
|
||||||
|
|
||||||
failedKeys.add(key)
|
failedKeys.add(key)
|
||||||
clearKeeping([key])
|
for (const alias of rowKeyAliasesFor(key, file)) failedAliases.add(alias)
|
||||||
|
markBulkKeptSuccessAliases(key, file, false)
|
||||||
|
clearKeeping(rowKeyAliasesFor(key, file))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
@ -4826,11 +4898,12 @@ export default function FinishedDownloads({
|
|||||||
for (const item of selectedKeepItems) {
|
for (const item of selectedKeepItems) {
|
||||||
if (!keptKeys.has(item.key) && !failedKeys.has(item.key)) {
|
if (!keptKeys.has(item.key) && !failedKeys.has(item.key)) {
|
||||||
failedKeys.add(item.key)
|
failedKeys.add(item.key)
|
||||||
|
for (const alias of aliasesForKeepItem(item)) failedAliases.add(alias)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedKeys.size > 0) {
|
if (failedKeys.size > 0) {
|
||||||
clearKeeping(failedKeys)
|
clearKeeping(failedAliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keptCount = keptKeys.size
|
const keptCount = keptKeys.size
|
||||||
@ -4843,7 +4916,11 @@ export default function FinishedDownloads({
|
|||||||
notify.success?.('Keep erfolgreich', `${keptCount} Downloads behalten.`)
|
notify.success?.('Keep erfolgreich', `${keptCount} Downloads behalten.`)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
clearKeeping(selectedKeepKeys.filter((key) => !keptKeys.has(key)))
|
clearKeeping(
|
||||||
|
selectedKeepItems
|
||||||
|
.filter((item) => !keptKeys.has(item.key))
|
||||||
|
.flatMap(aliasesForKeepItem)
|
||||||
|
)
|
||||||
|
|
||||||
if (keptKeys.size > 0) {
|
if (keptKeys.size > 0) {
|
||||||
clearSelection()
|
clearSelection()
|
||||||
@ -4861,10 +4938,12 @@ export default function FinishedDownloads({
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
selectedJobsForBulk,
|
selectedJobsForBulk,
|
||||||
markKeptRowSuccess,
|
markKeptRowSuccess,
|
||||||
|
markBulkKeptSuccessAliases,
|
||||||
removeRowsTogether,
|
removeRowsTogether,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
emitCountHint,
|
emitCountHint,
|
||||||
includeKeep,
|
includeKeep,
|
||||||
|
rowKeyAliasesFor,
|
||||||
notify,
|
notify,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -6831,7 +6910,7 @@ export default function FinishedDownloads({
|
|||||||
inlinePlay={inlinePlay}
|
inlinePlay={inlinePlay}
|
||||||
deletingKeys={deletingKeys}
|
deletingKeys={deletingKeys}
|
||||||
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
||||||
keptSuccessKeys={keptSuccessKeys}
|
keptSuccessKeys={visibleKeptSuccessKeys}
|
||||||
keepingKeys={keepingKeys}
|
keepingKeys={keepingKeys}
|
||||||
removingKeys={removingKeys}
|
removingKeys={removingKeys}
|
||||||
swipeRefs={swipeRefs}
|
swipeRefs={swipeRefs}
|
||||||
@ -6914,7 +6993,7 @@ export default function FinishedDownloads({
|
|||||||
assetNonceForJob={assetNonceForJob}
|
assetNonceForJob={assetNonceForJob}
|
||||||
deletingKeys={deletingKeys}
|
deletingKeys={deletingKeys}
|
||||||
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
||||||
keptSuccessKeys={keptSuccessKeys}
|
keptSuccessKeys={visibleKeptSuccessKeys}
|
||||||
keepingKeys={keepingKeys}
|
keepingKeys={keepingKeys}
|
||||||
removingKeys={removingKeys}
|
removingKeys={removingKeys}
|
||||||
modelsByKey={modelsByKey}
|
modelsByKey={modelsByKey}
|
||||||
@ -6967,7 +7046,7 @@ export default function FinishedDownloads({
|
|||||||
formatBytes={formatBytes}
|
formatBytes={formatBytes}
|
||||||
deletingKeys={deletingKeys}
|
deletingKeys={deletingKeys}
|
||||||
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
deletedSuccessKeys={visibleDeletedSuccessKeys}
|
||||||
keptSuccessKeys={keptSuccessKeys}
|
keptSuccessKeys={visibleKeptSuccessKeys}
|
||||||
keepingKeys={keepingKeys}
|
keepingKeys={keepingKeys}
|
||||||
removingKeys={removingKeys}
|
removingKeys={removingKeys}
|
||||||
registerTeaserHost={registerTeaserHost}
|
registerTeaserHost={registerTeaserHost}
|
||||||
|
|||||||
@ -1982,6 +1982,7 @@ function updatePosePersonQuality(person: TrainingPosePerson): TrainingPosePerson
|
|||||||
|
|
||||||
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
|
||||||
const p = sample?.prediction
|
const p = sample?.prediction
|
||||||
|
const sexPosition = normalizeSexPositionValue(p?.sexPosition)
|
||||||
|
|
||||||
const boxes = (p?.boxes ?? [])
|
const boxes = (p?.boxes ?? [])
|
||||||
.map((box) => ({
|
.map((box) => ({
|
||||||
@ -1995,25 +1996,30 @@ function predictionToCorrection(sample: TrainingSample | null): CorrectionState
|
|||||||
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sexPosition: normalizeSexPositionValue(p?.sexPosition),
|
sexPosition,
|
||||||
peoplePresent: (p?.peoplePresent ?? []).map((x) => x.label),
|
peoplePresent: (p?.peoplePresent ?? []).map((x) => x.label),
|
||||||
bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
|
bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
|
||||||
objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
|
objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
|
||||||
clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
|
clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
|
||||||
boxes,
|
boxes,
|
||||||
posePersons: clonePosePersons(p?.persons),
|
posePersons: isNoSexPositionValue(sexPosition)
|
||||||
|
? []
|
||||||
|
: clonePosePersons(p?.persons),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneCorrectionState(value: CorrectionState): CorrectionState {
|
function cloneCorrectionState(value: CorrectionState): CorrectionState {
|
||||||
|
const sexPosition = normalizeSexPositionValue(value.sexPosition)
|
||||||
return {
|
return {
|
||||||
sexPosition: value.sexPosition,
|
sexPosition,
|
||||||
peoplePresent: [...value.peoplePresent],
|
peoplePresent: [...value.peoplePresent],
|
||||||
bodyPartsPresent: [...value.bodyPartsPresent],
|
bodyPartsPresent: [...value.bodyPartsPresent],
|
||||||
objectsPresent: [...value.objectsPresent],
|
objectsPresent: [...value.objectsPresent],
|
||||||
clothingPresent: [...value.clothingPresent],
|
clothingPresent: [...value.clothingPresent],
|
||||||
boxes: (value.boxes ?? []).map((box) => ({ ...box })),
|
boxes: (value.boxes ?? []).map((box) => ({ ...box })),
|
||||||
posePersons: clonePosePersons(value.posePersons),
|
posePersons: isNoSexPositionValue(sexPosition)
|
||||||
|
? []
|
||||||
|
: clonePosePersons(value.posePersons),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3969,10 +3975,13 @@ function annotationToCorrectionState(item: TrainingAnnotation): CorrectionState
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.correction) {
|
if (item.correction) {
|
||||||
|
const sexPosition = normalizeSexPositionValue(item.correction.sexPosition)
|
||||||
return {
|
return {
|
||||||
...item.correction,
|
...item.correction,
|
||||||
sexPosition: normalizeSexPositionValue(item.correction.sexPosition),
|
sexPosition,
|
||||||
posePersons: clonePosePersons(item.correction.posePersons ?? item.prediction.persons),
|
posePersons: isNoSexPositionValue(sexPosition)
|
||||||
|
? []
|
||||||
|
: clonePosePersons(item.correction.posePersons ?? item.prediction.persons),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7218,11 +7227,16 @@ export default function TrainingTab(props: {
|
|||||||
const normalizedBoxes = (correction.boxes ?? [])
|
const normalizedBoxes = (correction.boxes ?? [])
|
||||||
.map(normalizeBox)
|
.map(normalizeBox)
|
||||||
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
.filter((box) => box.label && box.w > 0 && box.h > 0)
|
||||||
|
const normalizedSexPosition = normalizeSexPositionValue(correction.sexPosition)
|
||||||
|
const posePersonsForFeedback = isNoSexPositionValue(normalizedSexPosition)
|
||||||
|
? []
|
||||||
|
: clonePosePersons(correction.posePersons)
|
||||||
|
|
||||||
const feedbackCorrection = {
|
const feedbackCorrection = {
|
||||||
...correction,
|
...correction,
|
||||||
|
sexPosition: normalizedSexPosition,
|
||||||
boxes: normalizedBoxes,
|
boxes: normalizedBoxes,
|
||||||
posePersons: clonePosePersons(correction.posePersons),
|
posePersons: posePersonsForFeedback,
|
||||||
}
|
}
|
||||||
const negative =
|
const negative =
|
||||||
options?.negative ??
|
options?.negative ??
|
||||||
@ -7239,9 +7253,10 @@ export default function TrainingTab(props: {
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...correction,
|
...correction,
|
||||||
|
sexPosition: normalizedSexPosition,
|
||||||
peoplePresent: peopleLabelsFromBoxes(normalizedBoxes, labelsRef.current),
|
peoplePresent: peopleLabelsFromBoxes(normalizedBoxes, labelsRef.current),
|
||||||
boxes: normalizedBoxes,
|
boxes: normalizedBoxes,
|
||||||
posePersons: clonePosePersons(correction.posePersons),
|
posePersons: posePersonsForFeedback,
|
||||||
}
|
}
|
||||||
const effectiveAccepted = negative ? false : accepted
|
const effectiveAccepted = negative ? false : accepted
|
||||||
setSavingOverlayText(
|
setSavingOverlayText(
|
||||||
@ -12473,9 +12488,13 @@ export default function TrainingTab(props: {
|
|||||||
setHasManualCorrection(true)
|
setHasManualCorrection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextSexPosition = normalizeSexPositionValue(value)
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
sexPosition: value,
|
sexPosition: nextSexPosition,
|
||||||
|
posePersons: isNoSexPositionValue(nextSexPosition)
|
||||||
|
? []
|
||||||
|
: p.posePersons,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -12677,9 +12696,13 @@ export default function TrainingTab(props: {
|
|||||||
setHasManualCorrection(true)
|
setHasManualCorrection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextSexPosition = normalizeSexPositionValue(value)
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
sexPosition: value,
|
sexPosition: nextSexPosition,
|
||||||
|
posePersons: isNoSexPositionValue(nextSexPosition)
|
||||||
|
? []
|
||||||
|
: p.posePersons,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user