singlebehaviorlab 2.3.2__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {singlebehaviorlab-2.3.2/singlebehaviorlab.egg-info → singlebehaviorlab-2.3.4}/PKG-INFO +2 -3
  2. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/pyproject.toml +2 -3
  3. singlebehaviorlab-2.3.4/singlebehaviorlab/backend/embedding_refine.py +158 -0
  4. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/inference_widget.py +40 -2
  5. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/inference_worker.py +1 -0
  6. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/registration_widget.py +1 -10
  7. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4/singlebehaviorlab.egg-info}/PKG-INFO +2 -3
  8. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab.egg-info/SOURCES.txt +1 -0
  9. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab.egg-info/requires.txt +0 -1
  10. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/models.py +3 -3
  11. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/tokenizers.py +4 -6
  12. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/utils.py +1 -10
  13. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/LICENSE +0 -0
  14. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/README.md +0 -0
  15. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/setup.cfg +0 -0
  16. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/__init__.py +0 -0
  17. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/__main__.py +0 -0
  18. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/_paths.py +0 -0
  19. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/__init__.py +0 -0
  20. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/augmentations.py +0 -0
  21. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/clustering.py +0 -0
  22. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/contrastive.py +0 -0
  23. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/data_store.py +0 -0
  24. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/inference.py +0 -0
  25. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/model.py +0 -0
  26. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/registration.py +0 -0
  27. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/segmentation.py +0 -0
  28. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/segments.py +0 -0
  29. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/train.py +0 -0
  30. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/training_runner.py +0 -0
  31. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/uncertainty.py +0 -0
  32. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/video_processor.py +0 -0
  33. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/backend/video_utils.py +0 -0
  34. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/cli.py +0 -0
  35. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/config.py +0 -0
  36. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/data/config/config.yaml +0 -0
  37. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/data/training_profiles.json +0 -0
  38. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/demo.py +0 -0
  39. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/__init__.py +0 -0
  40. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/analysis_widget.py +0 -0
  41. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/attention_export.py +0 -0
  42. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/clip_extraction_widget.py +0 -0
  43. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/clustering_widget.py +0 -0
  44. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/inference_popups.py +0 -0
  45. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/interactive_timeline.py +0 -0
  46. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/labeling_widget.py +0 -0
  47. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/main_window.py +0 -0
  48. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/metadata_management_widget.py +0 -0
  49. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/motion_tracking.py +0 -0
  50. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/overlay_export.py +0 -0
  51. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/plot_integration.py +0 -0
  52. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/qt_helpers.py +0 -0
  53. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/review_widget.py +0 -0
  54. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/segmentation_tracking_widget.py +0 -0
  55. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/tab_tutorial_dialog.py +0 -0
  56. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/timeline_themes.py +0 -0
  57. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/training_profiles.py +0 -0
  58. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/training_widget.py +0 -0
  59. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/gui/video_utils.py +0 -0
  60. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/licenses/SAM2-LICENSE +0 -0
  61. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab/licenses/VideoPrism-LICENSE +0 -0
  62. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab.egg-info/dependency_links.txt +0 -0
  63. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab.egg-info/entry_points.txt +0 -0
  64. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/singlebehaviorlab.egg-info/top_level.txt +0 -0
  65. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_clustering_smoke.py +0 -0
  66. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_config.py +0 -0
  67. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_motion_tracking.py +0 -0
  68. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_paths.py +0 -0
  69. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_sam2_smoke.py +0 -0
  70. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/tests/test_segments.py +0 -0
  71. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/__init__.py +0 -0
  72. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/automatic_mask_generator.py +0 -0
  73. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/benchmark.py +0 -0
  74. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/build_sam.py +0 -0
  75. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2/sam2_hiera_b+.yaml +0 -0
  76. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2/sam2_hiera_l.yaml +0 -0
  77. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2/sam2_hiera_s.yaml +0 -0
  78. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2/sam2_hiera_t.yaml +0 -0
  79. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2.1/sam2.1_hiera_b+.yaml +0 -0
  80. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2.1/sam2.1_hiera_l.yaml +0 -0
  81. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2.1/sam2.1_hiera_s.yaml +0 -0
  82. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2.1/sam2.1_hiera_t.yaml +0 -0
  83. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/configs/sam2.1_training/sam2.1_hiera_b+_MOSE_finetune.yaml +0 -0
  84. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/__init__.py +0 -0
  85. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/backbones/__init__.py +0 -0
  86. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/backbones/hieradet.py +0 -0
  87. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/backbones/image_encoder.py +0 -0
  88. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/backbones/utils.py +0 -0
  89. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/memory_attention.py +0 -0
  90. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/memory_encoder.py +0 -0
  91. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/position_encoding.py +0 -0
  92. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam/__init__.py +0 -0
  93. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam/mask_decoder.py +0 -0
  94. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam/prompt_encoder.py +0 -0
  95. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam/transformer.py +0 -0
  96. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam2_base.py +0 -0
  97. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/modeling/sam2_utils.py +0 -0
  98. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_hiera_b+.yaml +0 -0
  99. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_hiera_l.yaml +0 -0
  100. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_hiera_s.yaml +0 -0
  101. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_hiera_t.yaml +0 -0
  102. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_image_predictor.py +0 -0
  103. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_video_predictor.py +0 -0
  104. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/sam2_video_predictor_legacy.py +0 -0
  105. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/utils/__init__.py +0 -0
  106. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/utils/amg.py +0 -0
  107. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/utils/misc.py +0 -0
  108. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/sam2_backend/sam2/utils/transforms.py +0 -0
  109. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/__init__.py +0 -0
  110. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/encoders.py +0 -0
  111. {singlebehaviorlab-2.3.2 → singlebehaviorlab-2.3.4}/third_party/videoprism_backend/videoprism/layers.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: singlebehaviorlab
3
- Version: 2.3.2
4
- Summary: Semi-automated behavioral video annotation, training, and analysis tool
3
+ Version: 2.3.4
4
+ Summary: Behavioral sequencing and phenotyping with lightweight task specific adaptation
5
5
  Author: Almir Aljovic
6
6
  Maintainer: Almir Aljovic
7
7
  License: MIT License
@@ -59,7 +59,6 @@ Requires-Dist: einshape
59
59
  Requires-Dist: huggingface-hub
60
60
  Requires-Dist: sentencepiece
61
61
  Requires-Dist: absl-py
62
- Requires-Dist: tensorflow-cpu
63
62
  Provides-Extra: test
64
63
  Requires-Dist: pytest; extra == "test"
65
64
  Requires-Dist: pytest-cov; extra == "test"
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "singlebehaviorlab"
7
- version = "2.3.2"
8
- description = "Semi-automated behavioral video annotation, training, and analysis tool"
7
+ version = "2.3.4"
8
+ description = "Behavioral sequencing and phenotyping with lightweight task specific adaptation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
11
11
  requires-python = ">=3.10"
@@ -43,7 +43,6 @@ dependencies = [
43
43
  "huggingface-hub",
44
44
  "sentencepiece",
45
45
  "absl-py",
46
- "tensorflow-cpu",
47
46
  ]
48
47
 
49
48
  [project.urls]
@@ -0,0 +1,158 @@
1
+ """Embedding-based timeline refinement.
2
+
3
+ Uses per-frame embeddings from the inference model to correct predictions
4
+ via semi-supervised label propagation on a nearest-neighbor graph, then
5
+ detects true behavior boundaries from embedding distance spikes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+ from typing import Optional
12
+
13
+ __all__ = ["refine_with_embeddings"]
14
+
15
+
16
+ def _cosine_distance_adjacent(embeddings: np.ndarray) -> np.ndarray:
17
+ norms = np.maximum(np.linalg.norm(embeddings, axis=1, keepdims=True), 1e-8)
18
+ normed = embeddings / norms
19
+ return 1.0 - np.sum(normed[:-1] * normed[1:], axis=1)
20
+
21
+
22
+ def _detect_boundaries(distances: np.ndarray, threshold_factor: float) -> list[int]:
23
+ if len(distances) == 0:
24
+ return []
25
+ median = float(np.median(distances))
26
+ mad = float(np.median(np.abs(distances - median)))
27
+ threshold = median + threshold_factor * max(mad, 1e-6)
28
+ boundaries = [0]
29
+ for i, d in enumerate(distances):
30
+ if d > threshold:
31
+ boundaries.append(i + 1)
32
+ return boundaries
33
+
34
+
35
+ def _majority_label(labels: np.ndarray, weights: Optional[np.ndarray] = None) -> int:
36
+ valid_mask = labels >= 0
37
+ valid = labels[valid_mask]
38
+ if len(valid) == 0:
39
+ return -1
40
+ if weights is not None:
41
+ w = weights[valid_mask]
42
+ counts: dict[int, float] = {}
43
+ for lbl, wt in zip(valid, w):
44
+ counts[int(lbl)] = counts.get(int(lbl), 0.0) + float(wt)
45
+ return max(counts, key=counts.get)
46
+ vals, cnts = np.unique(valid, return_counts=True)
47
+ return int(vals[np.argmax(cnts)])
48
+
49
+
50
+ def _label_propagation_correction(
51
+ frame_labels: np.ndarray,
52
+ frame_embeddings: np.ndarray,
53
+ frame_confidences: np.ndarray,
54
+ confidence_threshold: float,
55
+ ) -> np.ndarray:
56
+ from sklearn.semi_supervised import LabelSpreading
57
+
58
+ n_frames = len(frame_labels)
59
+ labels_for_propagation = frame_labels.copy()
60
+
61
+ for i in range(n_frames):
62
+ if frame_confidences[i] < confidence_threshold:
63
+ labels_for_propagation[i] = -1
64
+
65
+ n_labeled = np.sum(labels_for_propagation >= 0)
66
+ if n_labeled < 2 or n_labeled == n_frames:
67
+ return frame_labels.copy()
68
+
69
+ n_neighbors = min(7, n_frames - 1)
70
+ lp = LabelSpreading(kernel="knn", n_neighbors=n_neighbors, max_iter=30, alpha=0.2)
71
+ lp.fit(frame_embeddings, labels_for_propagation)
72
+ propagated = lp.transduction_
73
+
74
+ result = frame_labels.copy()
75
+ for i in range(n_frames):
76
+ if frame_confidences[i] < confidence_threshold and propagated[i] >= 0:
77
+ result[i] = int(propagated[i])
78
+
79
+ return result
80
+
81
+
82
+ def refine_with_embeddings(
83
+ frame_labels: np.ndarray,
84
+ frame_embeddings: np.ndarray,
85
+ frame_confidences: Optional[np.ndarray] = None,
86
+ n_classes: int = 0,
87
+ boundary_sensitivity: float = 1.5,
88
+ min_segment_frames: int = 3,
89
+ confidence_threshold: float = 0.7,
90
+ ) -> np.ndarray:
91
+ """Refine per-frame predictions using label propagation and boundary detection.
92
+
93
+ High-confidence predictions seed a nearest-neighbor graph. Labels
94
+ propagate to uncertain frames through embedding similarity. Boundary
95
+ detection then snaps segment edges to real embedding transitions.
96
+ """
97
+ n_frames = len(frame_labels)
98
+ if n_frames < 4 or frame_embeddings.shape[0] != n_frames:
99
+ return frame_labels.copy()
100
+
101
+ if frame_confidences is None:
102
+ frame_confidences = np.ones(n_frames, dtype=np.float32)
103
+
104
+ corrected = _label_propagation_correction(
105
+ frame_labels, frame_embeddings, frame_confidences, confidence_threshold,
106
+ )
107
+
108
+ distances = _cosine_distance_adjacent(frame_embeddings)
109
+ boundaries = _detect_boundaries(distances, boundary_sensitivity)
110
+ boundaries.append(n_frames)
111
+
112
+ refined = corrected.copy()
113
+ segments: list[tuple[int, int]] = []
114
+ for i in range(len(boundaries) - 1):
115
+ start, end = boundaries[i], boundaries[i + 1]
116
+ if end <= start:
117
+ continue
118
+ majority = _majority_label(corrected[start:end], frame_confidences[start:end])
119
+ refined[start:end] = majority
120
+ segments.append((start, end))
121
+
122
+ changed = True
123
+ while changed:
124
+ changed = False
125
+ new_segments = []
126
+ i = 0
127
+ while i < len(segments):
128
+ start, end = segments[i]
129
+ if (end - start) < min_segment_frames and len(segments) > 1:
130
+ mean_emb = frame_embeddings[start:end].mean(axis=0)
131
+ mean_emb /= max(np.linalg.norm(mean_emb), 1e-8)
132
+ best_sim, merge_with = -1.0, -1
133
+ for j in [i - 1, i + 1]:
134
+ if 0 <= j < len(segments):
135
+ ns, ne = segments[j]
136
+ ne_emb = frame_embeddings[ns:ne].mean(axis=0)
137
+ ne_emb /= max(np.linalg.norm(ne_emb), 1e-8)
138
+ sim = float(np.dot(mean_emb, ne_emb))
139
+ if sim > best_sim:
140
+ best_sim, merge_with = sim, j
141
+ if merge_with >= 0:
142
+ ms, me = segments[merge_with]
143
+ ms2, me2 = min(start, ms), max(end, me)
144
+ majority = _majority_label(corrected[ms2:me2], frame_confidences[ms2:me2])
145
+ refined[ms2:me2] = majority
146
+ if merge_with < i:
147
+ new_segments[-1] = (ms2, me2)
148
+ else:
149
+ new_segments.append((ms2, me2))
150
+ i += 1
151
+ changed = True
152
+ i += 1
153
+ continue
154
+ new_segments.append((start, end))
155
+ i += 1
156
+ segments = new_segments
157
+
158
+ return refined
@@ -320,6 +320,27 @@ class InferenceWidget(QWidget):
320
320
  self.timeline_theme_combo.currentIndexChanged.connect(self._on_theme_changed)
321
321
  timeline_controls_layout.addWidget(self.timeline_theme_combo)
322
322
 
323
+ self.embedding_refine_check = QCheckBox("Embedding refinement")
324
+ self.embedding_refine_check.setToolTip(
325
+ "Use the model's internal frame embeddings to detect true behavior\n"
326
+ "boundaries and correct predictions via label propagation."
327
+ )
328
+ self.embedding_refine_check.stateChanged.connect(self._on_embedding_refine_changed)
329
+ timeline_controls_layout.addWidget(self.embedding_refine_check)
330
+
331
+ self.embedding_refine_threshold = QDoubleSpinBox()
332
+ self.embedding_refine_threshold.setRange(0.1, 0.99)
333
+ self.embedding_refine_threshold.setSingleStep(0.05)
334
+ self.embedding_refine_threshold.setValue(0.70)
335
+ self.embedding_refine_threshold.setToolTip(
336
+ "Confidence threshold for embedding refinement.\n"
337
+ "Frames above this are trusted as seed labels.\n"
338
+ "Frames below defer to their embedding neighbors.\n"
339
+ "Lower = more aggressive correction."
340
+ )
341
+ self.embedding_refine_threshold.valueChanged.connect(self._on_embedding_refine_changed)
342
+ timeline_controls_layout.addWidget(self.embedding_refine_threshold)
343
+
323
344
  self._advanced_toggle = QPushButton("Advanced")
324
345
  self._advanced_toggle.setCheckable(True)
325
346
  self._advanced_toggle.setChecked(False)
@@ -2910,6 +2931,11 @@ class InferenceWidget(QWidget):
2910
2931
  self._interactive_timeline.set_model(model, colors)
2911
2932
  self._minimap.set_model(model, colors)
2912
2933
 
2934
+ def _on_embedding_refine_changed(self, *_args):
2935
+ if self.predictions and self.frame_aggregation_check.isChecked():
2936
+ self._build_timeline_segments()
2937
+ self._display_results()
2938
+
2913
2939
  def _toggle_advanced_controls(self, show: bool):
2914
2940
  for w in self._advanced_widgets:
2915
2941
  w.setVisible(show)
@@ -3247,10 +3273,22 @@ class InferenceWidget(QWidget):
3247
3273
  if float(fs[fi, ci]) < thr:
3248
3274
  frame_labels[fi] = -1
3249
3275
 
3250
- # Merge-gap and min-segment run ONCE as the final cleanup, after all
3251
- # other preprocessing (smoothing + threshold) is done.
3252
3276
  frame_labels = self._apply_gap_merge_and_min_seg(frame_labels, T)
3253
3277
 
3278
+ if self.embedding_refine_check.isChecked():
3279
+ frame_embs = None
3280
+ if hasattr(self, 'video_path') and self.video_path and self.video_path in self.results_cache:
3281
+ frame_embs = self.results_cache[self.video_path].get("aggregated_frame_embs")
3282
+ if isinstance(frame_embs, np.ndarray) and frame_embs.shape[0] >= T:
3283
+ from singlebehaviorlab.backend.embedding_refine import refine_with_embeddings
3284
+ frame_conf = np.max(fs, axis=1)
3285
+ frame_labels = refine_with_embeddings(
3286
+ frame_labels[:T], frame_embs[:T], frame_conf,
3287
+ n_classes=C,
3288
+ min_segment_frames=max(3, self._min_segment_frames),
3289
+ confidence_threshold=float(self.embedding_refine_threshold.value()),
3290
+ )
3291
+
3254
3292
  segments = []
3255
3293
  cur_cls = int(frame_labels[0])
3256
3294
  cur_start = 0
@@ -598,6 +598,7 @@ class InferenceWorker(QThread):
598
598
  "orig_fps": video_orig_fps,
599
599
  "frame_interval": video_frame_interval,
600
600
  "aggregated_frame_probs": aggregated_frame_probs,
601
+ "aggregated_frame_embs": aggregated_frame_embs,
601
602
  }
602
603
  if sample_ranges:
603
604
  res_entry["sample_ranges"] = sample_ranges
@@ -326,7 +326,7 @@ class EmbeddingExtractionWorker(QThread):
326
326
  error = pyqtSignal(str)
327
327
  log_message = pyqtSignal(str)
328
328
 
329
- def __init__(self, clip_paths: list, output_dir: str, experiment_name: str = None, model_name: str = 'videoprism_public_v1_base', clip_frame_ranges: dict = None, append_to_existing: bool = False, flip_invariant: bool = False, align_orientation: bool = False, mask_path: str = None):
329
+ def __init__(self, clip_paths: list, output_dir: str, experiment_name: str = None, model_name: str = 'videoprism_public_v1_base', clip_frame_ranges: dict = None, append_to_existing: bool = False, flip_invariant: bool = False):
330
330
  super().__init__()
331
331
  self.clip_paths = clip_paths
332
332
  self.clip_frame_ranges = clip_frame_ranges or {}
@@ -336,8 +336,6 @@ class EmbeddingExtractionWorker(QThread):
336
336
  self.should_stop = False
337
337
  self.append_to_existing = append_to_existing
338
338
  self.flip_invariant = flip_invariant
339
- self.align_orientation = align_orientation
340
- self.mask_path = mask_path
341
339
 
342
340
  def stop(self):
343
341
  self.should_stop = True
@@ -1256,11 +1254,6 @@ class RegistrationWidget(QWidget):
1256
1254
  experiment_name = self.config.get("experiment_name", None)
1257
1255
 
1258
1256
  # Start extraction worker with frame ranges if available
1259
- mask_path = None
1260
- if self.align_orientation_check.isChecked() and self.video_mask_pairs:
1261
- mask_path = self.video_mask_pairs[0][1] if len(self.video_mask_pairs) > 0 else None
1262
- self.log_text.append(f"Align orientation: mask_path={mask_path}, pairs={len(self.video_mask_pairs)}, frame_ranges={len(self.clip_frame_ranges)}")
1263
-
1264
1257
  self.embedding_worker = EmbeddingExtractionWorker(
1265
1258
  clip_paths,
1266
1259
  self.output_dir,
@@ -1269,8 +1262,6 @@ class RegistrationWidget(QWidget):
1269
1262
  clip_frame_ranges=self.clip_frame_ranges if hasattr(self, 'clip_frame_ranges') else None,
1270
1263
  append_to_existing=self.append_embeddings_check.isChecked(),
1271
1264
  flip_invariant=self.flip_invariant_check.isChecked(),
1272
- align_orientation=self.align_orientation_check.isChecked(),
1273
- mask_path=mask_path,
1274
1265
  )
1275
1266
  self.embedding_worker.progress.connect(self._on_embedding_progress)
1276
1267
  self.embedding_worker.finished.connect(self._on_embedding_finished)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: singlebehaviorlab
3
- Version: 2.3.2
4
- Summary: Semi-automated behavioral video annotation, training, and analysis tool
3
+ Version: 2.3.4
4
+ Summary: Behavioral sequencing and phenotyping with lightweight task specific adaptation
5
5
  Author: Almir Aljovic
6
6
  Maintainer: Almir Aljovic
7
7
  License: MIT License
@@ -59,7 +59,6 @@ Requires-Dist: einshape
59
59
  Requires-Dist: huggingface-hub
60
60
  Requires-Dist: sentencepiece
61
61
  Requires-Dist: absl-py
62
- Requires-Dist: tensorflow-cpu
63
62
  Provides-Extra: test
64
63
  Requires-Dist: pytest; extra == "test"
65
64
  Requires-Dist: pytest-cov; extra == "test"
@@ -18,6 +18,7 @@ singlebehaviorlab/backend/augmentations.py
18
18
  singlebehaviorlab/backend/clustering.py
19
19
  singlebehaviorlab/backend/contrastive.py
20
20
  singlebehaviorlab/backend/data_store.py
21
+ singlebehaviorlab/backend/embedding_refine.py
21
22
  singlebehaviorlab/backend/inference.py
22
23
  singlebehaviorlab/backend/model.py
23
24
  singlebehaviorlab/backend/registration.py
@@ -26,7 +26,6 @@ einshape
26
26
  huggingface-hub
27
27
  sentencepiece
28
28
  absl-py
29
- tensorflow-cpu
30
29
 
31
30
  [test]
32
31
  pytest
@@ -45,7 +45,6 @@ import jax.numpy as jnp
45
45
  import huggingface_hub
46
46
  import numpy as np
47
47
  from videoprism import encoders
48
- from videoprism import tokenizers
49
48
  from videoprism import utils
50
49
 
51
50
  K400_NUM_CLASSES: int = 400
@@ -336,7 +335,8 @@ def load_pretrained_weights(
336
335
  return jax.tree_util.tree_map(jnp.asarray, variables)
337
336
 
338
337
 
339
- def load_text_tokenizer(name: str) -> tokenizers.Tokenizer:
338
+ def load_text_tokenizer(name: str):
339
+ from videoprism import tokenizers # lazy: avoids top-level TF dependency
340
340
  """Loads a text tokenizer by name.
341
341
 
342
342
  Args:
@@ -353,7 +353,7 @@ def load_text_tokenizer(name: str) -> tokenizers.Tokenizer:
353
353
 
354
354
 
355
355
  def tokenize_texts(
356
- tokenizer: tokenizers.Tokenizer,
356
+ tokenizer,
357
357
  inputs: Sequence[str],
358
358
  max_length: int = TEXT_MAX_LEN,
359
359
  add_bos: bool | None = None,
@@ -17,9 +17,6 @@
17
17
  from collections.abc import Sequence
18
18
  from typing import Protocol
19
19
 
20
- import tensorflow as tf
21
- from tensorflow.io import gfile
22
-
23
20
  import sentencepiece
24
21
 
25
22
  SentencePieceProcessor = sentencepiece.SentencePieceProcessor
@@ -44,7 +41,7 @@ class Tokenizer(Protocol):
44
41
 
45
42
  def to_int_tf_op(
46
43
  self, text: str | Sequence[str], *, bos: bool = False, eos: bool = False
47
- ) -> tf.Tensor | tf.RaggedTensor:
44
+ ):
48
45
  """Same as `to_int()`, but as TF ops to be used in data pipelines.
49
46
 
50
47
  Args:
@@ -55,6 +52,7 @@ class Tokenizer(Protocol):
55
52
  Returns:
56
53
  A tf.Tensor of tokens.
57
54
  """
55
+ import tensorflow as tf
58
56
 
59
57
  @property
60
58
  def pad_token(self) -> int:
@@ -82,7 +80,7 @@ class SentencePieceTokenizer(Tokenizer):
82
80
  Args:
83
81
  model_path: A path to load the SentencePiece model.
84
82
  """
85
- with gfile.GFile(model_path, "rb") as f:
83
+ with open(model_path, "rb") as f:
86
84
  model_bytes = f.read()
87
85
 
88
86
  self._model = SentencePieceProcessor()
@@ -115,7 +113,7 @@ class SentencePieceTokenizer(Tokenizer):
115
113
 
116
114
  def to_int_tf_op(
117
115
  self, text: str | Sequence[str], *, bos: bool = False, eos: bool = False
118
- ) -> tf.Tensor | tf.RaggedTensor:
116
+ ):
119
117
  """Same as `to_int()`, but as TF ops to be used in data pipelines.
120
118
 
121
119
  Args:
@@ -16,13 +16,10 @@
16
16
 
17
17
  import collections
18
18
  from collections.abc import Mapping, Sequence
19
- import io
20
- import os
21
19
  import string
22
20
 
23
21
  import jax
24
22
  import numpy as np
25
- from tensorflow.io import gfile
26
23
 
27
24
 
28
25
  def traverse_with_names(tree, with_inner_nodes=False):
@@ -106,13 +103,7 @@ def recover_tree(keys, values):
106
103
  def npload(fname):
107
104
  """Loads `fname` and returns an np.ndarray or dict thereof."""
108
105
  # Load the data; use local paths directly if possible:
109
- if os.path.exists(fname):
110
- loaded = np.load(fname, allow_pickle=False)
111
- else:
112
- # For other (remote) paths go via gfile+BytesIO as np.load requires seeks.
113
- with gfile.GFile(fname, "rb") as f:
114
- data = f.read()
115
- loaded = np.load(io.BytesIO(data), allow_pickle=False)
106
+ loaded = np.load(fname, allow_pickle=False)
116
107
 
117
108
  # Support loading both single-array files (np.save) and zips (np.savez).
118
109
  if isinstance(loaded, np.ndarray):